Skip to main content

CLR (Common Language Runtime)

Opublikowano: 24 sierpnia 2004 r. | Zaktualizowano: 24 sierpnia 2004 r.

Wspólne środowisko uruchomieniowe (Common Language Runtime, w skrócie CLR) to podstawa całego systemu .NET Framework. Wszystkie języki środowiska .NET (na przykład C# czy Visual Basic .NET), a także wszystkie biblioteki klas obecne w .NET Framework (ASP.NET, ADO.NET i inne) oparte są na CLR. Ponieważ nowe, tworzone przez Microsoft oprogramowanie, także oparte jest na .NET Framework, każdy, kto chce korzystać ze środowiska Microsoft, prędzej czy później będzie musiał zetknąć się z CLR.

Środowisko CLR kompiluje i wykonuje zapisany w standardowym języku pośrednim Microsoft (MSIL) kod aplikacji zwany kodem zarządzanym (ang. managed code), zapewniając wszystkie podstawowe funkcje konieczne do działania aplikacji. Podstawowym elementem CLR jest standardowy zestaw typów danych, wykorzystywanych przez wszystkie języki oparte na CLR, a także standardowy format metadanych, służących do opisu oprogramowania wykorzystującego te typy danych. CLR zapewnia także mechanizmy umożliwiające pakowanie kodu zarządzanego w jednostki zwane podzespołami.

W CLR wbudowane są także mechanizmy kontroli bezpieczeństwa wykonywania aplikacji — bezpieczeństwo oparte na uprawnieniach kodu (Code Access Security — CAS) oraz bezpieczeństwo oparte na rolach (Role-Based Security — RBS).

Zobacz też:

*

Zawartość strony

Obsługa typów danychObsługa typów danych

Kod zarządzanyKod zarządzany

Obsługa typów danych

Język programowania to połączenie składni oraz zbioru słów kluczowych, umożliwiające definiowanie danych oraz operacji przeprowadzanych na tych danych. Różne języki różnią się pod względem składni, jednak podstawowe pojęcia są dość podobne — większość języków obsługuje takie typy danych jak liczba całkowita czy łańcuch znaków i umożliwia porządkowanie kodu w metody oraz zbieranie metod i danych w klasy.

CTS

Przy zachowaniu odpowiedniego poziomu abstrakcji, możliwe jest zdefiniowanie zestawu typów danych, który będzie niezależny od składni języka. Zamiast łączyć składnię i semantykę, można je określić oddzielne, co pozwoli na zdefiniowanie większej liczby języków korzystających z tych samych pojęć (typów danych). Takie właśnie podejście zastosowano w CLR. Wspólny zestaw typów danych (Common Type System— CTS) nie jest związany z żadną składnią lub słowami kluczowymi — definiuje jedynie zestaw typów danych, który może być wykorzystywany przez wiele języków. Każdy język zgodny z CLR może używać dowolnej składni, ale musi korzystać przynajmniej z części typów danych zdefiniowanych przez CTS.

Zestaw typów danych definiowany przez CTS należy do głównych składników CLR. Każdy oparty na CLR język programowania może udostępniać programiście te typy danych na właściwy sobie sposób. Twórca języka może skorzystać tylko z niektórych typów danych, może też definiować własne typy danych. Jednak większość języków wszechstronnie korzysta z CTS.

Najważniejsze typy danych zdefiniowane w CTS przedstawiono na ilustracji 2. Warto zwrócić uwagę na to, że wszystkie typy danych to albo typy skalarne (wartości), albo typy referencyjne. Innym godnym zauważenia faktem jest to, że każdy typ danych dziedziczy pośrednio lub bezpośrednio po typie Object — wszystkie typy referencyjne dziedziczą bezpośrednio po klasie Object, natomiast typy skalarne dziedziczą po klasie ValueType, która z kolei dziedziczy po Object.

Typy danych obsługiwane przez CLR

Ilustracja 2. Typy danych obsługiwane przez CLR


 

Wszystkie zdefiniowane typy skalarne są typami prostymi. Można wśród nich znaleźć różnej długości liczby całkowite ze znakiem i bez znaku, liczby zmiennoprzecinkowe pojedynczej i podwójnej precyzji, liczby dziesiętne, wartości binarne, znaki Unicode i inne.

Typy referencyjne (na przykład Class, Interface, Array, String, zwane też obiektami) przechowują wskaźnik do adresu pamięci, pod którym zapisana jest wartość. Jeśli więc dany adres wskazywany jest przez różne zmienne typu referencyjnego, to zmiana wartości jednej zmiennej spowoduje zmianę wartości wszystkich pozostałych zmiennych.

Sposób obsługi podstawowych skalarnych typów danych w językach programowania Microsoft przedstawiono w tabeli 1. W tabeli ujęto także typy referencyjne Object i String, dla których w większości języków programowania istnieją odpowiednie słowa kluczowe.

Kategoria: liczba całkowita

Nazwa klasyOpisVBC#C++J#
Byte8-bitowa liczba całkowita bez znakuBytebytecharByte
SByte8-bitowa liczba całkowita ze znakiemSbytesbytesigned charSByte
Int1616-bitowa liczba całkowita ze znakiemShortshortshortshort
Int3232-bitowa liczba całkowita ze znakiemIntegerintint -lub- longint
Int6464-bitowa liczba całkowita ze znakiemLonglong__int64long
UInt1616-bitowa liczba całkowita bez znakuUInt16ushortunsigned shortUInt16
UInt3232-bitowa liczba całkowita bez znakuUInt32uintunsigned int -lub- unsigned longUInt32
UInt6464-bitowa liczba całkowita bez znakuUInt64ulongunsigned__int64UInt64

 

Kategoria: liczba zmiennoprzecinkowa

Nazwa klasyOpisVBC#C++J#
Singleliczba zmiennoprzecinkowa pojedynczej precyzji (32-bity)Singlefloatfloatfloat
Doubleliczba zmiennoprzecinkowa podwójnej precyzji (64-bit)Doubledoubledoubledouble

 

Kategoria: wartość logiczna

Nazwa klasyOpisVBC#C++J#
Booleanwartość logiczna (prawda lub fałsz)Booleanboolboolbool

 

Kategoria: inne

Nazwa klasyOpisVBC#C++J#
Charznak Unicode (16-bitowy)Charcharwchar_tchar
Decimal96-bitowa liczba dziesiętnaDecimaldecimalDecimalDecimal
IntPtrliczba całkowita ze znakiem, o rozmiarze zależnym od platformy, na której działa aplikacja (32 bity na platformie 32-bitowej, 64 bity na platformie 64-bitowej)IntPtrIntPtrIntPtrIntPtr
UIntPtrliczba całkowita bez znaku, o rozmiarze zależnym od platformy, na której działa aplikacja (32 bity na platformie 32-bitowej, 64 bity na platformie 64-bitowej)UIntPtrUIntPtrUIntPtrUIntPtr

 

Kategoria: obiekty

Nazwa klasyOpisVBC#C++J#
Objectklasa — korzeń hierarchii obiektówObjectobjectObject*Object
Stringciąg znaków Unicode o stałej długości, obiekt niezmienny (immutable)StringstringString*String

Tabela (1-5). Lista typów skalarnych CTS oraz ich odpowiedników w różnych językach programowania

Pakowanie — konwersja typów skalarnych na typy referencyjne (boxing)

W pewnych sytuacjach konieczne jest, by instancja typu skalarnego mogła być traktowana jak instancja typu referencyjnego — na przykład gdy chcemy przekazać jakąś wartość skalarną w wywołaniu metody, która wymaga, by jej parametry były typu referencyjnego.

Gdy zmienna typu skalarnego poddawana jest operacji pakowania (boxing), jej wartość jest umieszczana na stercie w odpowiednio zaalokowanym obszarze pamięci. Natomiast na stosie umieszczana jest referencja do tego obszaru. Pokazano to na ilustracji 3. Zmienną typu Int32 o wartości 167 zapakowano, w wyniku czego zmienna ta zmieniła typ na Object, a jej wartość umieszczona została na stercie. Zapakowaną zmienną można skonwertować z powrotem na typ skalarny. Proces taki nazywany jest rozpakowaniem (unboxing).

Ilustracja 3. Pakowanie (boxing)

Ilustracja 3. Pakowanie (boxing)


 

Języki programowania oparte na CLR z reguły ukrywają proces pakowania. Programista nie musi jawnie wywoływać operacji pakowania — w razie potrzeby jest ono wykonywane automatycznie. Nie jest to prawdą dla każdego języka — na przykład dla języka C++ z rozszerzeniami Managed Extensions.

Operacje pakowania mają wpływ na wydajność, ponieważ sama operacja zajmuje trochę czasu, również odwołania do wartości zapakowanych wymagają więcej cykli procesora niż odwołania do wartości niezapakowanych. Mimo że proces pakowania zwykle wykonywany jest bez wiedzy programisty, warto zdawać sobie sprawę z tego, co dzieje się w pamięci komputera.

Zobacz też:

Zarządzanie pamięcią

W kodzie zarządzanym pamięć dla danych może być alokowana na dwa sposoby — na stosie albo na stercie (jest jeszcze trzeci sposób — zmienne globalne i statyczne alokowane są w oddzielnym obszarze pamięci). Różnica pomiędzy typami skalarnymi i referencyjnymi polega na tym, że w przypadku typów skalarnych na stosie znajduje się faktyczna wartość danego wystąpienia (instancji) typu danych. Natomiast w przypadku typów referencyjnych na stosie znajduje się jedynie referencja (wskaźnik) do rzeczywistej wartości, dla której pamięć została zaalokowana na stercie.

Należy zauważyć, że utworzenie wystąpienia typu referencyjnego powoduje także zapisanie pewnej informacji na stosie (referencja do obszaru pamięci zaalokowanego na stercie), jednak sama wartość danego wystąpienia przechowywana jest na stercie.

Pamięć alokowana dla zmiennych na stosie jest automatycznie zwalniania w momencie zakończenia działania metody, która te zmienne utworzyła. Z kolei obiekty zaalokowane na stercie nie są automatycznie zwalniane w momencie zakończenia działania metody — dealokacją pamięci takich obiektów zajmuje się proces odzyskiwania pamięci.

Odzyskiwanie pamięci

W czasie działania aplikacji sterta zapełnia się kolejnymi obiektami typów referencyjnych. Gdy zapełniona zostanie cała dostępna pamięć sterty, automatycznie uruchamiany jest proces odzyskiwania pamięci (dosłownie — proces odśmiecania, ang. garbage collector). Aplikacja może także jawnie uruchomić ten proces, nie jest to jednak zalecane zachowanie.

Aby zrozumieć działanie procesu odzyskiwania pamięci, raz jeszcze przyjrzyjmy się sposobowi alokacji pamięci na stercie. Jak widać na ilustracji 4, każde wystąpienie typu referencyjnego posiada na stosie wpis wskazujący na miejsce na stercie, w którym zaalokowana jest pamięć dla tego wystąpienia.

Ilustracja 4. Stan pamięci przed odśmiecaniem

Ilustracja 4. Stan pamięci przed odśmiecaniem


Na stosie znajduje się liczba zmiennoprzecinkowa o wartości 18,9, referencja do łańcucha znaków o treści „Tekst”, liczba całkowita o wartości 132 oraz referencja do zapakowanej liczby całkowitej o wartości 167.

Jak łatwo zauważyć, na stercie obecny jest jeszcze jeden obiekt — klasa X. Obiekt ten znajduje się pomiędzy łańcuchem znaków a liczbą całkowitą. Prawdopodobnie został utworzony przez metodę, która uruchomiona została po utworzeniu ciągu znaków, a przed utworzeniem liczby całkowitej. Metoda ta zakończyła już swoje działanie, dlatego na stosie nie ma już referencji do obiektu klasy X.

Gdy proces odzyskiwania pamięci zostanie uruchomiony, przegląda stertę i zlicza referencje wskazujące na każdy z obiektów. Jeśli do jakiegoś obiektu nie prowadzi żadna referencja, obiekt ten klasyfikowany jest jako śmieć i może zostać usunięty. Po usunięciu takich obiektów sterta jest porządkowana — obiekty, do których istnieją jeszcze referencje, zostają upakowane w taki sposób, by zajmowały ciągły obszar pamięci. Natomiast istniejące referencje są zmieniane tak, by wciąż wskazywały właściwe obiekty (które po uporządkowaniu sterty znajdują się w innych miejscach pamięci). Po zakończeniu procesu odśmiecania, stan pamięci w naszym przykładowym systemie wyglądałby tak, jak na ilustracji 5.

Stan pamięci po odśmiecaniu

Ilustracja 5. Stan pamięci po odśmiecaniu


Taki sposób działania procesu odzyskiwania pamięci sprawia, że najstarsze obiekty będą przesuwane ku końcowi sterty. Sposób korzystania z pamięci przez programy powoduje, że najwięcej obiektów-śmieci powstaje wśród obiektów najmłodszych. Dlatego właśnie proces odśmiecania przegląda stertę od początku, czyli od obszaru, w którym znajduje się najmłodsze pokolenie obiektów. Jeśli odzyskana ilość pamięci jest niewystarczająca do dalszej pracy aplikacji, przeglądane są starsze pokolenia obiektów.

Napisanie dobrego procesu odzyskiwania pamięci wymaga poświęcenia większej ilości zasobów (pamięć operacyjna i cykle procesora) niż byłaby potrzebna na standardowy menedżer pamięci. Warto to jednak zrobić, gdyż automatyczne zarządzanie pamięcią pozwoli programistom zapomnieć o konieczności zwalniania pamięci i umożliwi im poświęcenie uwagi szybszemu pisaniu lepszych aplikacji.

Finalizatory

Każdy obiekt alokowany na stercie posiada specjalną metodę zwaną finalizatorem (finalizer). Kiedy proces odzyskiwania pamięci znajdzie obiekt do usunięcia z pamięci, umieszcza go na liście obiektów oczekujących na finalizację. Po zakończeniu przeszukiwania pamięci, dla każdego obiektu znajdującego się na tej liście wykonywany jest jego finalizator. Domyślnie metoda ta nie wykonuje żadnego kodu. Jeśli dany typ danych wymaga przeprowadzenia jakiejś operacji „oczyszczania” przed usunięciem go z pamięci, programista może zastąpić domyślny pusty finalizator własnym kodem.

Finalizatorów nie należy mylić z destruktorami, znanymi z języka C++ i innych języków obiektowych. Nie da się określić, kiedy kod finalizatora zostanie wykonany, a nawet czy zostanie wykonany (aplikacja może się zawiesić albo zakończyć działanie przed usunięciem wszystkich obiektów ze sterty). Jeśli wymagane jest wykonanie jakiejś operacji w celu poprawnego zamknięcia obiektu, najlepszym rozwiązaniem jest napisanie odpowiedniej metody oraz postawienie programistom wymogu wykonania tej metody, gdy przestają używać danego wystąpienia obiektu.

Zobacz też:

Wspólna specyfikacja języka (CLS)

Wspólny system typów (CTS) definiuje dość złożony i rozbudowany zestaw typów. Niektóre języki nie muszą obsługiwać wszystkich typów, zdefiniowanych w CTS. Jednym z głównych celów CLR jest umożliwienie napisania kodu w jednym języku i wywoływania go z kodu napisanego w innym języku. Aby było to możliwe, obydwa języki muszą w ten sam sposób obsługiwać te same typy danych. Wymóg, by w każdym języku programowania były implementowane wszystkie typy danych zdefiniowane w CTS, byłby zbyt uciążliwy dla projektantów języków programowania. Dlatego powstało rozwiązanie umożliwiające kompromis. Rozwiązaniem tym jest wspólna specyfikacja języka (Common Language Specification — CLS). CLS określa, jak duży podzbiór CTS musi zostać zaimplementowany, by język był zgodny z innymi językami wykorzystującymi CLS. CLS wymaga na przykład, by język obsługiwał większość typów skalarnych — między innymi Boolean, Byte, Char, Decimal, Int16, Int32, Int64, Single, Double. Nie jest natomiast wymagana obsługa typów takich jak UInt16, UInt32, UInt64. CTS pozwala, by najniższy indeks tablicy był dowolną liczbą całkowitą, natomiast CLS wymaga, by najniższy indeks tablicy wynosił 0. CLS określa też inne ograniczenia ułatwiające efektywną współpracę różnych języków zgodnych z CLR.

Zobacz też:

What is the Common Language Specification?, MSDN Library (j. ang.)
http://msdn.microsoft.com/library/en-us/cpguide/html/cpconwhatiscommonlanguagespecification.aspx

Do początku stronyDo początku strony

Kod zarządzany

Kompilatory zgodne z CLR zamieniają kod źródłowy aplikacji na kod wykonywalny, zapisany w standardowym języku pośrednim MSIL, oraz na metadane — informacje na temat kodu wykonywalnego oraz danych wykorzystywanych przez ten kod. Niezależnie od języka, w którym napisany jest kod źródłowy aplikacji, kompilator zamienia wszystkie operacje na typach danych, to jest klasach, strukturach, liczbach całkowitych, łańcuchach znaków — na język MSIL i metadane.

W czasie wykonywania aplikacji, CLR tłumaczy kod MSIL na kod maszynowy (natywny) procesora, na którym wykonywana jest aplikacja. Konwersja kodu aplikacji z MSIL na kod maszynowy daje możliwość zarządzania wykonywaniem aplikacji, co pozwala uniknąć wielu problemów — stąd nazwa kod zarządzany.

Microsoft Intermediate Language — MSIL

MSIL to kod dość podobny do zestawu instrukcji procesora. Obecnie nie istnieje jednak żaden sprzęt, który mógłby bezpośrednio wykonywać kod MSIL (nie jest jednak wykluczone, że w przyszłości taki sprzęt powstanie). Na razie kod MSIL musi być tłumaczony na język maszynowy procesora, na którym ma być uruchomiony. Polecenia MSIL operują bezpośrednio na pojęciach zdefiniowanych przez CTS. Nawet takie operacje, jak pakowanie i rozpakowanie, są obsługiwane bezpośrednio — są one po prostu instrukcjami MSIL.

Kompilacja kodu źródłowego języka wyższego poziomu na kod pośredni jest podstawową techniką, wykorzystywaną przez nowoczesne kompilatory. Kompilatory pakietu Visual Studio 6 tłumaczą kod źródłowy różnych języków na taki sam kod pośredni, który następnie kompilowany jest na kod maszynowy przez jeden wspólny kompilator. To właśnie ten kod maszynowy stanowił finalny kod aplikacji przed wprowadzeniem środowiska .NET Framework.

Co jest tak wielką zaletą kodu pośredniego, że w .NET Framework kod pośredni stanowi finalny kod aplikacji, mimo że poprzednio jego odpowiednik ukryty był głęboko w kompilatorze? Zaletą tą jest potencjalna przenośność aplikacji. Przeniesienie całego .NET Framework na inne systemy operacyjne lub inne procesory jest nie lada wyzwaniem, jednak Microsoft stara się, by na innych urządzeniach dostępne było co najmniej środowisko .NET Compact Framework.

Przenośność nie jest jedyną zaletą stosowania języka pośredniego. Odmiennie niż w przypadku kodu maszynowego, który może zawierać wskaźniki do dowolnych adresów, kod MSIL może przed uruchomieniem zostać sprawdzony pod względem bezpieczeństwa typów. Podnosi to poziom bezpieczeństwa i daje większą niezawodność, gdyż działanie takie pozwala na wykrycie pewnych rodzajów błędów oraz wielu prób ataków.

Metadane

Kompilacja kodu zarządzanego, oprócz wygenerowania kodu pośredniego MSIL, powoduje utworzenie metadanych opisujących powstały kod. Metadane to szczegółowy opis typów zdefiniowanych w kodzie zarządzanym, z którym są związane. Opis ten przechowywany jest w tym samym pliku, w którym znajduje się kod MSIL. Programiści znający technologię COM zauważą, że rola metadanych zbliżona jest do roli biblioteki typów COM (COM type library).

Metadane opisują typy znajdujące się w danym fragmencie kodu. Informacje te zawierają:

  • nazwę typu,
  • zasięg typu (publiczny lub w granicach podzespołu),
  • nazwę typu, po którym dziedziczy opisywany typ,
  • implementowane interfejsy,
  • implementowane metody,
  • udostępniane właściwości,
  • obsługiwane zdarzenia.

Informacje te są bardzo szczegółowe. Na przykład opis każdej metody zawiera informacje o jej parametrach i typach tych parametrów, a także informację o typie zwracanej przez tę metodę wartości.

Ponieważ metadane zawsze towarzyszą kodowi pośredniemu, wiele narzędzi wykorzystuje ich obecność. Na przykład z metadanych korzysta funkcja IntelliSense w Visual Studio .NET, wyświetlająca listę metod zaimplementowanych w klasie, której nazwę programista tuż przed chwilą wpisał.

Innymi informacjami, zapisywanymi w metadanych, są atrybuty. Mogą one służyć do sterowania sposobem wykonywania kodu. Atrybuty pozwalają określić, które metody mają zostać udostępnione jako usługi Web Service, wywoływane za pomocą protokołu SOAP. Dzięki atrybutom można też opisać wymagania bezpieczeństwa. Atrybuty mają określone nazwy i funkcje, definiowane przez różne części środowiska .NET Framework. Programiści mogą także definiować własne atrybuty.

Zobacz też:

Podzespoły

Na kompletną aplikację często składa się wiele różnych plików. Niektóre z nich zawierają kod wykonywalny, a inne zawierają zasoby (na przykład grafikę lub treść komunikatów). W aplikacjach opartych na .NET Framework pliki, które stanowią jeden logiczny moduł, udostępniający określoną funkcjonalność, grupowane są w podzespoły (assembly).

Podzespół to twór logiczny. Określenie, które pliki należą do danego podzespołu, nie jest możliwe bez analizy manifestu tego podzespołu. Manifest podzespołu to odpowiednik metadanych związanych z pojedynczym modułem. Manifest znajduje się w jednym z plików podzespołu i zawiera:

  • nazwę podzespołu (może to być nazwa silna),
  • numer wersji podzespołu (wspólny i taki sam dla wszystkich modułów, które stanowią podzespół),
  • listę wszystkich plików, które stanowią podzespół, wraz z ich sumami kontrolnymi,
  • listę innych wymaganych do pracy podzespołów wraz z numerami ich wersji.

Silna nazwa (strong name) to identyfikator podzespołu, co do którego możemy mieć pewność, że będzie unikalny w całym systemie. Można ją utworzyć poprzez cyfrowe podpisanie manifestu podzespołu za pomocą klucza prywatnego.

Większość podzespołów to pojedyncze pliki DLL. Niezależnie od tego, czy podzespół składa się z wielu plików, czy też jest to pojedynczy plik, podzespół jest logiczną, niepodzielną całością. Granice podzespołu wyznaczają granice zasięgu zdefiniowanych w nim typów danych — w CLR nazwa typu danych składa się z nadanej mu nazwy oraz nazwy podzespołu, w którym został zdefiniowany. Należy zauważyć, że podzespoły nie odwzorowują przestrzeni nazw stosowanych w .NET Framework. Przestrzenie nazw są wygodnym sposobem grupowania typów zdefiniowanych w językach zgodnych z CLR, ale nie są widoczne w samym środowisku CLR.

Ważną różnicą pomiędzy podzespołami a klasami COM jest to, że instalacja podzespołu nie wymaga dokonania wpisu w rejestrze systemowym (o ile podzespół nie jest dostępny także jako klasa COM dla zachowania zgodności ze starszymi wersjami). Aby zainstalować podzespół, wystarczy tylko skopiować tworzące go pliki do odpowiedniego katalogu. Odinstalowanie polega na skasowaniu plików.

To, że podzespół może posiadać silną nazwę, a także fakt, że w manifeście podzespołu zapisana jest lista innych wymaganych do pracy podzespołów wraz z ich dokładnymi numerami wersji, pozwala uniknąć znanych z przeszłości konfliktów plików DLL. Plik DLL, podmieniony przez program instalacyjny jednej aplikacji, mógł skutecznie uniemożliwić uruchomienie innej aplikacji, która wymagała do działania innej wersji tego samego pliku. Jeśli podzespół posiada silną nazwę, w systemie mogą być jednocześnie zainstalowane różne jego wersje. Nie rozwiązuje to jednak wszystkich problemów — można sobie wyobrazić, jaki bałagan powstanie, jeśli dwie różne wersje jednego podzespołu będą zapisywały dane w tym samym pliku tymczasowym.

Zobacz też:

Wykonywanie kodu zarządzanego

Gdy uruchamiana jest aplikacja wykorzystująca środowisko .NET Framework, podzespoły, składające się na tę aplikację, muszą zostać odnalezione i załadowane do pamięci. Nie są one ładowane tak długo, aż będą potrzebne — jeśli aplikacja nie wywoła żadnej metody z danego podzespołu, nie zostanie on załadowany do pamięci. Nie musi być nawet obecny w systemie!

Potrzebne podzespoły muszą zostać odnalezione w systemie plików. Pierwszym miejscem, w którym CLR szuka podzespołów, jest globalna pamięć podręczna podzespołów (Global Assembly Cache — GAC). Jest to specjalny katalog, w którym przechowywane są podzespoły wykorzystywane przez więcej niż jedną aplikację. Proces instalacji podzespołu w GAC jest bardziej skomplikowany niż kopiowanie plików. W GAC instalowane mogą być wyłącznie podzespoły, którym nadano silne nazwy

Jeśli podzespół nie zostanie znaleziony w GAC, CLR poszuka go w innych miejscach. Algorytm poszukiwania jest dość rozbudowany, ale dobrze opisany. Programiści, chcący dokładnie zapoznać się z tym algorytmem, mogą znaleźć jego opis pod adresem: http://msdn.microsoft.com/library/en-us/cpguide/html/cpconhowruntimelocatesassemblies.aspx (j. ang.)

Po załadowaniu potrzebnych podzespołów do pamięci, kod aplikacji nadal jest kodem MSIL, który nie może być bezpośrednio wykonywany przez procesor. Potrzebna jest jeszcze jedna kompilacja, która dokona zamiany kodu MSIL na kod maszynowy procesora, na którym uruchomiona będzie aplikacja.

Dwie metody kompilacji

Najczęściej stosowaną metodą kompilacji kodu MSIL na kod natywny jest załadowanie przez CLR podzespołu do pamięci, a następnie kompilacja każdej metody w momencie pierwszego jej wywołania. Ponieważ każda metoda kompilowana jest tylko w momencie pierwszego uruchomienia, proces kompilacji nazywa się kompilacją w samą porę (just-in-time compilation — JIT).

Kompilacja JIT umożliwia kompilowanie tylko tych metod, które są rzeczywiście wykorzystywane. Jeśli metoda została załadowana do pamięci razem z całym podzespołem, ale nigdy nie została wywołana, pozostanie w postaci MSIL. Skompilowany kod maszynowy nie jest zapisywany z powrotem na dysk twardy — przy ponownym uruchomieniu aplikacji kod MSIL będzie musiał zostać ponownie skompilowany.

Inną metodą kompilacji jest wygenerowanie całego kodu binarnego danego podzespołu z użyciem narzędzia Native Image Generator (NGEN), dostępnego w .NET Framework SDK. Narzędzie to, uruchamiane poleceniem ngen.exe, kompiluje cały podzespół i umieszcza jego kod maszynowy w obszarze zwanym pamięcią podręczną obrazów kodu natywnego (Native Image Cache). Pozwala to na szybsze uruchamianie aplikacji, ponieważ podzespoły nie muszą już być kompilowane metodą JIT.

Kompilacja kodu MSIL na kod maszynowy pozwala na sprawdzenie bezpieczeństwa typów danych. Proces ten, zwany weryfikacją, sprawdza kod MSIL oraz metadane metod pod kątem prób niepowołanego uzyskania dostępu do zasobów systemu. Na etapie tym sprawdzane są także ustawienia bezpieczeństwa dla kodu. Administrator systemu może wyłączyć tę funkcję, jeśli nie jest ona potrzebna.

Zobacz też:

Mieszanie kodu zarządzanego i niezarządzanego

Każda nowa technologia — by mieć szanse na szersze przyjęcie — musi zapewniać możliwość integracji z technologiami już istniejącymi. Tak jak system Windows pozwalał na uruchamianie aplikacji napisanych dla systemu DOS, oferując przy tym wielozadaniowość, tak platforma .NET Framework musi zapewnić możliwość współpracy z istniejącymi aplikacjami.

W zakresie komunikacji pomiędzy aplikacjami system Windows opiera się na COM. Środowisko .NET Framework, by móc współpracować ze starszymi aplikacjami, musi zapewniać obsługę COM. Możliwa jest współpraca w obu kierunkach — klient .NET może korzystać z serwera COM, jak i klient COM może korzystać z serwera .NET.

Współpraca klienta .NET z serwerem COM

Klient .NET odwołuje się do serwera COM za pomocą specjalnego modułu pośredniczącego (proxy), zwanego opakowaniem wywoływanym w czasie wykonywania (Runtime Callable Wrapper — RCW). RCW opakowuje obiekt COM i tłumaczy wszystkie wywołania w taki sposób, że obiekt COM prezentuje się przed klientami .NET tak, jakby był obiektem .NET, natomiast wywołania klientów .NET wyglądają dla serwera COM tak jak wywołania klientów COM.

RCW można wygenerować na kilka sposobów. Jeśli programista korzysta z Visual Studio .NET, wystarczy kilka kliknięć myszą. Innym sposobem jest wykorzystanie narzędzia wiersza polecenia o nazwie TlbImp.exe, dostępnego w pakiecie .NET Framework SDK. W obu przypadkach wykorzystywana jest klasa System.Runtime.InteropServices.TypeLibConverter. Aby utworzyć RCW można też ręcznie użyć tej klasy.

Po wygenerowaniu RCW dla obiektu COM można zaimportować przestrzeń nazw tego obiektu. Używanie obiektu RCW jest proste — używa się go tak samo, jak każdego innego obiektu .NET. Instancje obiektów RCW tworzy się za pomocą polecenia new. Po utworzeniu obiekt RCW, wywołując funkcję CoCreateInstance, automatycznie tworzy właściwy obiekt COM. Od tej pory środowisko .NET może korzystać z obiektu COM tak, jakby był on zwykłym obiektem .NET. RCW zamienia wszystkie wywołania w taki sposób, by były zgodne z konwencją COM — na przykład ciągi znaków zgodne z CLR przekształca na typ danych BSTR, wymagany przez COM. Przed zwróceniem wyniku wywołania funkcji obiektu COM, RCW konwertuje go z powrotem na format zrozumiały przez .NET.

Współpraca klienta .NET z serwerem COM

Ilustracja 6. Współpraca klienta .NET z serwerem COM


 

Obiekty COM, opakowane przez RCW, zachowują się jak obiekty .NET także pod względem zarządzania pamięcią — zwalnianiem zasobów zajmuje się moduł odzyskiwania pamięci. Może to pociągnąć za sobą niekorzystne zajmowanie kosztownych zasobów systemowych, które powinny zostać zwolnione natychmiast po zakończeniu pracy klienta, a tymczasem zajmowane są przez bezużyteczny już obiekt COM z opakowaniem. Problem ten można usunąć na dwa sposoby — albo przez jawne wywołanie procesu odśmiecania pamięci za pomocą funkcji System.GC.Collect, albo przez wywołanie funkcji System.Runtime.InteropServices.Marshal.ReleaseComObject, co jest rozwiązaniem bardziej optymalnym ze względu na mniejsze nakłady pracy procesora.

Mechanizm RCW można wykorzystać jedynie w przypadku wcześnie wiązanych obiektów COM. Aby umożliwić korzystanie z obiektów późno wiązanych, środowisko .NET Framework udostępnia funkcję Type.GetTypeFromProgID, która tworzy typ systemowy, oparty na identyfikatorze ProgID, oraz funkcję Type.GetTypeFromCLSID, tworzącą typ systemowy na podstawie identyfikatora CLSID. Gdy mamy już utworzony typ, można utworzyć obiekt korzystając z metody Activator.CreateInstance. Dostęp do metod obiektu uzyskuje się za pomocą funkcji Type.InvokeMember.

Współpraca klienta COM z serwerem .NET

Środowisko .NET zapewnia także możliwość komunikacji w przeciwnym kierunku, to jest w sytuacji, gdy klient COM korzysta z serwera .NET. Podobnie jak w poprzednim przypadku, funkcjonalność tę uzyskuje się poprzez utworzenie odpowiedniego obiektu pośredniczącego. Jest to opakowanie wywoływane z COM (COM Callable Wrapper — CCW). CCW opakowuje obiekt .NET i przekazuje wywołania w taki sposób, że przez klienta COM widziany jest on jak zwykły obiekt COM.

Podzespół .NET, aby mógł współpracować z opakowaniem CCW, musi być podpisany za pomocą silnej nazwy i znajdować się w GAC lub w drzewie katalogów aplikacji klienta — w przeciwnym wypadku środowisko .NET miałoby problemy z jego odnalezieniem. Każda klasa .NET, która ma być dostępna dla klientów COM, musi mieć zaimplementowany domyślny konstruktor bezparametrowy. Klasa może mieć dowolnie wiele parametryzowanych konstruktorów, pod warunkiem, że zostanie zapewniony jeden konstruktor, który nie będzie wymagał użycia żadnych parametrów.

Aby klient COM mógł odnaleźć obiekt .NET, informacje o obiekcie .NET muszą znaleźć się w rejestrze systemowym. Można to zrobić za pomocą narzędzia RegAsm.exe dostarczanego razem z pakietem .NET Framework SDK.

Współpraca klienta COM z serwerem .NET

Ilustracja 7. Współpraca klienta COM z serwerem .NET


 

Klient COM sięga do obiektu .NET tak, jakby był to natywny obiekt COM. Gdy klient, który chce utworzyć obiekt, wywołuje funkcję CoCreateInstance, żądanie kierowane jest do zarejestrowanego serwera Mscoree.dll. Biblioteka ta sprawdza żądany identyfikator CLSID w rejestrze w celu odnalezienia klasy .NET, którą ma utworzyć. Następnie tworzy oparte na tej klasie opakowanie CCW. Od tej pory obiekt .NET może być traktowany jak zwykły obiekt COM — klasa CCW konwertuje w locie wszystkie wywołania, zamieniając przy tym typy danych (na przykład BSTR używany przez COM konwertowany jest na dotnetowy typ String).

Za pomocą atrybutu System.Runtime.InteropServices.ComVisible autor obiektu .NET może określić, które metody jego klasy mogą być wywoływane przez obiekty COM. Atrybut można zastosować do podzespołu, klasy, interfejsu lub pojedynczej metody. W Visual Studio .NET domyślną wartością tego atrybutu dla podzespołu jest False. Ustawienia zmienione na niższych poziomach hierarchii przesłaniają te ustawione na wyższych, można więc łatwo ustalić, co będzie widoczne dla klientów COM.

Zobacz też:

Do początku stronyDo początku strony