Tematem artykułu jest niezarządzany interfejs programowania dostarczany razem z technologią Microsoft .NET umożliwiający wykorzystanie w natywnych aplikacjach kodu zarządzanego oraz ingerencję w zachowanie Common Language Runtime. Poznanie tego API, często pomijanego przez programistów, jest bezwątpienia cenne. Dzięki niemu można silnik wykonawczy platformy .NET w łatwy sposób zintegrować z natywnym oprogramowaniem i tym samym podnieść jego funkcjonalność. W pierwszej części artykułu zaprezento
CLR Unmanaged API
Tworzenie własnych hostów
Część pierwsza
Streszczenie
Tematem artykułu jest niezarządzany interfejs programowania dostarczany razem z technologią Microsoft .NET umożliwiający wykorzystanie w natywnych aplikacjach kodu zarządzanego oraz ingerencję w zachowanie Common Language Runtime. Poznanie tego API, często pomijanego przez programistów, jest bezwątpienia cenne. Dzięki niemu można silnik wykonawczy platformy .NET w łatwy sposób zintegrować z natywnym oprogramowaniem i tym samym podnieść jego funkcjonalność.
W pierwszej części artykułu zaprezentowano czym są hosty CLR. Na przykładzie prostego hosta zostały wyjaśnione podstawy niezarządzanego API. Ponieważ tematyka ściśle związana jest z CLR, wcześniej przedstawiono czym on jest, z jakich fizycznych komponentów się składa i jak jest ładowany do pamięci.
W drugiej części artykułu zostanie omówiona możliwość wykorzystania managerów dostarczonych wraz z Common Language Runtime jak i możliwość zastępowania pewnych domyślnych zachowań CLR własnymi poprzez implementację managerów hosta.
Wprowadzenie
Platforma Microsoft .NET jest komercyjną, w sensie tworzonych z jej wykorzystaniem rozwiązań, implementacją specyfikacji Common Language Infrastructure która jest zatwierdzona jako standard ECMA-335. Należy podkreślić, iż CLI jest specyfikacją otwartą, omawiającą wszystkie aspekty środowiska wykonawczego czy kodu wynikowego. Opisuje także zbiór typów, które powinny być wspierane (Common Type System) czy zbiór reguł które muszą być przestrzegane przez język programowania aby mógł być poprawnie być uruchamiany (Common Language Specification). Konsekwencją tego podejścia jest możliwość zaimplementowania własnego środowiska zarządzanego, zgodnego z platformą .NET. Nie trzeba przecież dodawać, że poza podstawową implementacją w postaci .NET Framework, istnieją implementacje Open Source takie jak Mono wspierane przez firmę Novell. Ponadto, firma Microsoft stworzyła wersję edukacyjną o nazwie Shared Source CLI (Rotor), udostępnianą w postaci źródłowej na zasadach Shared Source.
Sercem platformy .NET jest wspomniany już CLR – Common Language Runtime. Podstawowe komponenty wchodzące w jego skład przedstawiono na poniższym rysunku.

Rysunek 1 - podstawowe komponenty środowiska wykonawczego
Głównym zadaniem CLR z punktu widzenia programisty jest załadowanie odpowiednich assemblies do pamięci oraz ich uruchomienie w kontekście wcześniej utworzonej Application Domain. Można więc powiedzieć, że CLR jest maszyną wirtualną wykonującą kod pośredni zawarty w uruchamianych assemblies. Trzeba zwrócić uwagę na fakt, iż CLR nie jest w stanie załadować sam siebie do procesu. Dlatego musi istnieć jakiś zewnętrzny mechanizm który będzie za to odpowiedzialny. Tym właśnie mechanizmem jest program hostujący CLR.
Hosty CLR
Host CLR to fragment kodu niezarządzanego, który wczytuje CLR do procesu systemu operacyjnego, przeprowadza jego wstępną konfigurację (jeśli jest wymagana) i uruchamia go. Po przekazaniu przez hosta informacji do CLR o punkcie wejścia do kodu zarządzanego (funkcja static void Main(string[] args)) następuje jego wykonanie.
Wraz z instalacją platformy .NET w systemie pojawia się kilka hostów uruchamiających kod zarządzany. Wymienione są one poniżej.
· Host ASP.NET – wykorzystuje filtr zaimplementowany w postaci wtyczki ISAPI która wczytuje CLR do worker procesu serwera IIS, który obsługuje przychodzące żądania. Każda aplikacja ASP.NET działająca na serwerze jest wczytywana do swojej AppDomain dzięki czemu niepoprawnie napisana aplikacja internetowa nie spowoduje przerwania działania całego procesu.
· Host Internet Explorer – IE posiada możliwość uruchamiania kontrolek napisanych w kodzie zarządzanym osadzonych na stronie WWW. Domyślnie, IE tworzy osobną AppDomain dla każdej odwiedzanej strony.
· Shell executables – za każdym razem kiedy uruchamiana jest aplikacja *.exe, najpierw wykonywany jest krótki (jak bardzo okaże się za chwilę) kod niezarządzany wczytujący CLR i przekazujący mu kontrolę.
Pisząc o hostach CLR trudno nie wspomnieć hosta wbudowanego w serwer baz danych firmy Microsoft – SQL Server 2005. Jest to przykład aplikacji, która podmienia wiele domyślnych funkcji CLR, w tym całkowicie przejmuje kontrolę nad tym jak CLR zarządza pamięcią oraz wielozadaniowością. Czytelnik nie powinien być zaskoczony tym podejściem gdyż MS SQL posiada własne mechanizmy związane z przydziałem pamięci czy tworzeniem i szeregowaniem wątków.
Struktura fizyczna silnika wykonawczego
Platformę .NET można zainstalować na jednym systemie operacyjnym w wielu różnych wersjach , przede wszystkim w celu zapewnienia płynnej migracji do nowszych wersji. Stwarza to jednak pewne trudności podczas uruchamiania Common Language Runtime gdyż host musi zdecydować którą wersję należy załadować do procesu – a zatem host musi wiedzieć gdzie i w jakich plikach znajduje się żądana wersja środowiska wykonawczego. Warto również dodać, że w procesie może działać tylko jedna wersja CLR która raz załadowana, nie może zostać zmieniona bez restartowania procesu.
Do wczytania oraz uruchomienia CLR potrzebny jest więc mechanizm pośredniczący, który jako argumenty przyjmie wersję CLR do uruchomienia oraz niezbędne parametry inicjalizacyjne. Musi on oczywiście wiedzieć gdzie na danym komputerze zainstalowana jest odpowiednia wersja platformy .NET. Ten pośrednik to tzw. startup shim i jest on zaimplementowany w bibliotece MsCorEE.dll (Component Object Runtime Execution Engine), która znajduje się w katalogu %windir%\system32. Biblioteka ta, pomimo instalacji wielu różnych wersji .NET Framework, zawsze pochodzi z najnowszej wersji platformy zainstalowanej na danym komputerze w celu zapewnienia kompatybilności wstecznej. Jej obecność w systemie pokazuje również czy na komputerze zainstalowana jest któraś z wersji .NET Framework. Należy podkreślić, iż wspomniany startup shim jest wykorzystywany nie tylko w samym procesie ładowania CLR – wszystkie operacje wykonywane przez hosta które dotyczą samego CLR są przeprowadzane za pośrednictwem biblioteki MsCorEE.dll która deleguje żądane czynności do odpowiedniej wersji silnika wykonawczego platformy.
Pliki składające się na konkretną implementację CLR znajdują się w katalogu %windir%\Microsoft.NET\Framework\v<version> w 32 bitowej wersji systemu Windows, oraz w %windir%\Microsoft.NET\Framework64\v<version> w wersji 64 bitowej. Są to między innymi:
· Mscorwks.dll – wersja „Workstation” silnika wykonawczego CLR
· Mscorsvr.dll – serwerowa wersja CLR
· Mscorlib.dll – zawiera część namespace’a System mającego związek z CLR
· Mscorjit.dll – zawiera implementację kompilatora Just-In-Time odpowiedzialnego za kompilację w locie kodu pośredniego do postaci wykonywalnej bezpośrednio na procesorze
Ciekawostką jest fakt, iż od wersji 2.0 CLR, biblioteka Mscorsvr.dll jest dołączona do biblioteki Mscorwks.dll i nie występuje już jako samodzielny plik.
Mechanizm ładowania CLR do procesu
Każde assembly dla platformy .NET występuje w dwóch postaciach – jako plik wykonywalny z rozszerzeniem exe oraz jako biblioteka dll. Nie są to jednak klasyczne pliki zawierające kod wynikowy.

Rysunek 2 – fizyczna struktura assembly
Pomimo iż zawierają standardowy nagłówek PE , posiadają dodatkowo:
· nagłówek CLR – zawiera wersję CLR wymaganą do uruchomienia assembly, różne flagi oraz definicję metody wejściowej Main do kodu zarządzanego,
· metadane – dokładnie opisują wszystkie typy znajdujące się w assembly, ich nazwy, kwalifikatory dostępu, itd.,
· MSIL – kod pośredni, zamiast natywnego kodu wykonywalnego.
Kiedy kompilator generuje wykonywalny plik assembly, w sekcji .text nagłówka PE dodawana jest następująca, 6 bajtowa instrukcja procesora x86: JMP _CorExeMain (lub _CorDllMain dla biblioteki dll).
Wykonanie tej instrukcji powoduje uruchomienie jednej z wymienionych w powyższej instrukcji funkcji które zdefiniowane są we wspomnianym pliku MsCorEE.dll (link w sekcji .idata). Obie funkcje mają za zadanie załadować CLR do procesu z którego zostały wywołane. Ponieważ wszystkie potrzebne informacje takie jak parametry startowe czy punkt wejścia do programu zdefiniowane są w sekcji nagłówka CLR, od razu można przejść do wykonywania kodu zarządzanego.
Powyższą instrukcję wykorzystują systemy Windows 2000 i wcześniejsze, gdyż podczas ich powstawania nie znana była jeszcze Platforma .NET. Warto również zauważyć iż owa instrukcja należy do instrukcji procesora x86 – a zatem nie wykona się poprawnie na innych architekturach sprzętowych. Ponieważ systemy Windows XP oraz nowsze dostępne są w wersjach dla innych architektur, dlatego też ich loadery zostały zmodyfikowane tak aby same wykrywały zarządzane assemblies. Systemy te same wykrywają istnienie sekcji nagłówkowej CLR w pliku i same wywołują jedną z dwóch wymienionych funkcji. Instrukcja JMP jest w takich przypadkach ignorowana.
Przedstawiony powyżej mechanizm ładowania CLR nie ma zastosowania w klasycznych aplikacjach gdyż aplikacje natywne nie posiadają sekcji nagłówkowej oraz sekcji danych CLR a co za tym idzie nie ma mowy o wykorzystaniu funkcji _CorExeMain lub _CorDllMain.
CLR Unmanaged API
Twórcy Microsoft .NET Framework zadbali o to by możliwe było napisanie własnego fragmentu natywnego kodu w celu samodzielnego załadowania CLR do procesu. Nietrudno wskazać duże korzyści płynące z możliwości hostowania Common Language Runtime we własnej aplikacji. Dzięki temu można znacznie podnieść funkcjonalność projektowanego systemu. Można również podmienić kluczowe elementy CLR w celu dostosowania zachowania środowiska wykonawczego do potrzeb i wymagań systemu. Common Language Runtime od zawsze dostarczał niezarządzany interfejs programistyczny pozwalający na tworzenie własnych hostów. W wersji 1.x .NET Framework można było wczytać konkretną wersję CLR, uruchomić go i zatrzymać a także skonfigurować podstawowe ustawienia. W wersji 2.0 API zostało znacznie udoskonalone.
I tak w wersji 2.0 CLR hosting API składa się z interfejsów COM, które można podzielić na managery hosta oraz managery CLR, oraz funkcji na nich operujących (słowo manager odnosi się więc do grupy interfejsów COM tworzących razem pewną funkcjonalność).
Interfejsy managerów hosta rozpoczynają się od IHost (np. IHostTaskManager). Ich implementację dostarcza programista tworzący hosta, następnie implementacja interfejsu jest rejestrowana w CLR który może zacząć korzystać z dostarczonej funkcjonalności (zamiast swojej domyślnej).
Interfejsy managerów CLR posiadają przedrostek ICLR (np. ICLRTaskManager) i ich implementacja dostraczana jest przez silnik wykonawczy platformy. Interfejsy te są wykorzystywane przez hosta w celu żądania od CLR wykonania różnych operacji (np. host może zarządać od CLR przeprowadzenia odśmiecenia pamięci poprzez wywołanie funkcji Collect interfejsu ICLRGCManager).
Zagadnienia dotyczące wykorzystania dostępnych managerów jak i implementacja własnych będzie przedmiotem drugiej części artykułu.
Dostępne managery
Niezarządzane API CLR dostarcza ośmiu podstawowych managerów. Każdy manager składa się z jednego bądź kilku głównych interfejsów (ang. primary interface) które dostraczają CLR’owi funkcji dzięki którym może on wykorzystaywać funkcjonalność danego managera oraz interfejsów których implementacja dostarcza już konkretną funkcjonalność. Przykładowo, jednym z głównych interfejsów managera wielozadaniowości jest IHostThreadingManager który dostarcza CLR’owi możliwość tworzenia nowych zadań. Samo zadanie jest natomiast enkapsulowane w interfejsie IHostTask. Poniżej znajduje się opis trzech najczęściej wykorzystywanych managerów, bardziej szczegółowy opis pozostałych czytelnik może znaleźć w [1] i [4].
Threading Manager
Platforma .NET wykorzystuje abstrakcyjne pojęcie zadania (ang. Task) do opisania jednostki dostającej przydział czasu procesora. Oczywiście w standardowej implementacji wykorzystywane są klasyczne wątki WIN32 API. Nic nie stoi jednak na przeszkodzie by stworzyć własną implementację np. w oparciu o włókna (ang. Fiber) czyli wątki lekkie. Threading manager w szczególności pozwala na:
· dostarczenie CLR’owi mechanizmu tworzenia i uruchamiania nowych zadań oraz wykonywania na nich standartowych operacji takich jak Join czy Abort,
· zaimplementowanie własnej puli wątków,
· zmienianie priorytetów zadań.
Synchronization Manager
Umożliwienie hostowi ingerencji w sposób zarządzania zadaniami dla wielu aplikacji jest wystarczający. Może jednak się zdarzyć, iż host będzie chciał dostarczyć własne implementacje podstawowych obiektów synchronizujących. Dzięki temu pewnym jest, iż żadne blokady nie zostaną zabrane przez zadanie bez wiedzy hosta, a także można nadzorować jak wspólne zasoby są wykorzystywane przez wątki np. w celu wykrywania i przeciwdziałania zakleszczeniom. Korzystając z interfejsów Synchronization Managera, można dostarczyć implementację dla takich obiektów synchronizujących jak zdarzenia (automatyczne i manualne), sekcje krytyczne czy semafory.
Memory Manager
Zarządca ten dostarcza interfejs przez który CLR żąda wszystkich alokacji pamięci. Dostarczenie własnej implementacji managera pamięci daje więc możliwość przejęcia kontroli nad sposobem jej przydziału. Umożliwia również poinformowanie silnika wykonawczego kiedy przydział pamięci się nie powiódł np. poprzez wygenerowanie wyjątku OutOfMemoryException.
Załadownie CLR do procesu hosta
Wcześniej Czytelnik zapoznał się z procesem ładowania CLR w przypadku zarządzanego assembly. Ponieważ ma ono specjalną sekcję nagłówkową i sekcję danych CLR, wystarczy wywołać jedną z funkcji _CorExeMain lub _CorDllMain (funkcje te nie przyjmują żadnych parametrów) które załadują, zainicjalizują i uruchomią w procesie CLR. Ponieważ nagłówek CLR w assembly zawiera informacje o punkcie wejścia do programu, sterowanie może od razu zostać przekazane do silnika wykonawczego aby ten zaczął wykonywać kod niezarządzany.
Oczywistym staje się fakt, iż musi istnieć inna funkcja służąca do załadowania CLR do własnej natywnej aplikacji. Służy do tego funkcja o następującym nagłówku:
[Kod C++]
STDAPI CorBindToRuntimeEx(LPCWSTR pwszVersion, LPCWSTR pwszBuildFlavor, DWORD startupFlags, REFCLSID rclsid, REFIID riid, LPVOID FAR *ppv); |
Pierwszym argumentem powyższej funkcji jest łańcuch znaków (wszystkie parametry typu string w niezarządzany API są łańcuchami Unicode) będący wersją CLR do uruchomienia. String powinien przyjąć następującą formę: ”v<major>.<minor>.<build>” (np. L”v2.0.50727”). Przekazanie wartości NULL spowoduje uruchomienie najnowszej wersji CLR. Drugi parametr opisuje rodzaj CLR do załadowania – można uruchomić wersję desktopową przekazując L”wks” albo wersję serwerową przekazując L”srv” (przekazanie NULL spowoduje uruchomienie wersji desktopowej). Różnica za chwilę zostanie wyjaśniona. Kolejny parametr, typu DWORD, przyjmuje różne ustawienia dla CLR. Zebrane są one w następujący typ wyliczeniowy (wymienione zostały tylko najczęściej wykorzystywane opcje):
[Kod C++]
typedef /* [public] */
enum __MIDL___MIDL_itf_mscoree_0000_0002
{ STARTUP_CONCURRENT_GC = 0x1,
STARTUP_SERVER_GC = 0x1000,
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN = 0x1 << 1,
STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN = 0x2 << 1,
STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST = 0x3 << 1,
...
} STARTUP_FLAGS; |
Do najważniejszych należą wartości STARTUP_CONCURRENT_GC oraz STARTUP_SERVER_GC których znaczenie za chwilę stanie się jasne (oba parametry można także ustawić w pliku konfiguracyjnym App.config). Pozostałe trzy wartości dotyczą współdzielenia kodu wynikowego, skompilowanego przez kompilator JIT, pomiędzy Application Domains z tego samego procesu (ładowanie domain neutral). Pierwsza wartość określa, iż żaden kod, poza biblioteką mscorlib, nie będzie współdzielony. Druga wartość sprawia, iż wszystkie assemblies po kompilacje będą współdzielone. Ostatnia z prezentowanych wartości określa, że tylko assemblies posiadające strong names będą po kompilacji współdzielone. W dalszej części artykułu zostało wyjaśnione czym dokładnie jest ładowanie domain neutral.
Następne dwa parametry wspomnianej funkcji informują CLR o rodzaju interfejsu który ma być uchwytem do ładowanego CLRa. We wcześniejszych wersjach platformy jako uchwyt do CLR używany był interfejs ICorRuntimeHost, w wersji 2.0 CLR natomiast jest to ICLRRuntimeHost. Ponieważ wszystkie interfejsy w zarządzanym API CLR są interfejsami COM, należy powiadomić CLR który interfejs ma zostać zwrócony jako uchwyt. W tym celu należy wykorzystać wartości CLSID (identyfikator komponentu) oraz IID (identyfikator interfejsu) interfejsu ICLRRuntimeHost. Mają one wartości odpowiednio CLSID_CLRRuntimeHost oraz IID_ICLRRuntimeHost. W ostatnim parametrze funkcja zwraca referencję do żądanego interfejsu będącego uchwytem do silnika wykonawczego platformy.
Wersja desktopowa oraz serwerowa CLR
Common Language Runtime dostarczany jest w dwóch wersjach – serwerowej i desktopowej. Różnica jest subtelna ale zasadnicza – build serwerowy jest zoptymalizowany do pracy w konfiguracjach wieloprocesorowych. Dotyczy to przede wszystkim odśmiecacza pamięci. W wersji serwerowej GC działa w wielu wątkach jednocześnie (dokładniej na jeden CPU przypada jeden wątek odśmiecacza pamięci który pracuje na własnej zarządzanej stercie, po zakończeniu procesu czyszczenia pamięci wątki są usypiane) co skutkuje oczywiście szybszym usunięciem nieużywanych obiektów. Warto również wiedzieć, że wersja serwerowa może zostać uruchomiona wyłącznie na komputerze wyposażonym w więcej niż jedną jednostkę centralną. Próba uruchomienia wersji serwerowej na komputerze jednoprocesorowym zakończy się wczytaniem wersji desktopowej. Takie podejście podyktowane jest aspektami wydajnościowymi – na komputerze posiadającym tylko jeden CPU build serwerowy będzie działał wolniej od wersji desktopowej.
Wersję serwerową można ustawić przekazując jako drugi parametr funkcji CorBindToRuntimeEx wartość L”srv” albo wykorzystując flagę STARTUP_SERVER_GC.
Odśmiecanie w osobnym wątku
Jak wspomniano powyżej, w wersji serwerowej CLR posiada współbieżny GC. Dzięki temu odśmiecanie pamięci może przebiegać równolegle na wszystkich dostępnych procesorach. Dodatkowo, istnieje możliwość takiego skonfigurowania GC, by odśmiecanie odbywało się w wątku użytkownika lub w wątkach pobocznych (ang. background thread). Ustawienie flagi STARTUP_CONCURRENT_GC spowoduje, że proces odśmiecania (tylko drugiej generacji) będzie przeprowadzone w specjalnie utworzonych wątkach (po zakończeniu tego procesu wątki są niszczone). W rezultacie, aplikacje z graficznym interfejsem będą płynniej reagowały na interakcję z użytkownikiem, jednak będzie to okupione trochę niższą wydajnością samego odśmiecania. Jest to domyślne ustawienie desktopowej wersji CLR. Natomiast w wersji ”server” CLR jedynym dostępnym trybem jest odśmiecanie w tych samych wątkach co kod aplikacji. Dzięki temu odśmiecanie pamięci w aplikacjach serwerowych działa z maksymalną szybkością.
Ładowanie domain neutral
Nowoczesne, wielozadaniowe systemy operacyjne muszą oferować pewną izolację pomiędzy działającymi aplikacjami tak, aby nie wpływały one na siebie, w szczególności aby błąd w jednej nie powodował błędu w innej. System operacyjny taką izolację osiąga poprzez uruchamianie każdego programu w osobnym procesie. Również platforma .NET potrzebuje mechanizmu izolacji, jednak w jej przypadku wykorzystanie procesów mogłoby być zbyt kosztowne, np. w przypadku aplikacji ASP.NET. Dlatego .NET dostarcza własny mechanizm izolujący działające aplikacje – domenę aplikacji (ang. application domain). CLR umożliwia działanie kilku zarządzanych aplikacji w jednym procesie systemu operacyjnego, każda w swojej AppDomain – błąd w jednej nie powoduje błędów w innej a cały proces może kontynuować swoje działanie. Można powiedzieć, że domena aplikacji jest dla platformy .NET tym czym proces dla systemu operacyjnego. Aplikacje z różnych domen działających w jednym procesie nie mogą się ze sobą bezpośrednio komunikować – muszą wykorzystywać mechanizmy IPC (inter process communication). Współdzielą jednak między sobą zarządzane sterty z których przydzielana jest pamięć.
W celu zapewnienia izolacji, każda domena aplikacji posiada własny kod i własne struktury danych. Dzieje się tak nawet w przypadku, gdy kilka różnych aplikacji w jednym procesie (każda w swojej domenie) korzystają ze wspólnego assembly. W efekcie w każdej domenie aplikacji znajduje się ten sam kod wynikowy (po kompilacji JIT) dla takiego assembly. W celach optymalizacyjnych można tak skonfigurować CLR, aby kod dla assembly wykorzystywanego przez wiele domen był pomiędzy nimi współdzielony. O tak wczytanym assembly mówimy, że jest domain neutral. Warto wspomnieć, iż biblioteka mscorlib zawsze wczytywana jest jako domain neutral.
U Czytelnika może zrodzić się pytanie, dlaczego zawsze wszystkie assemblies nie są ładowane domain neutral skoro dzięki temu oszczędzamy pamięci komputera. Jest tak dla tego, iż assembly które zostało w ten sposób załadowanie nie może zostać wyładowane z procesu bez jego restartu. Takie zachowanie z pewnością nie byłoby pożądane dla hosta ASP.NET czy serwera bazodanowego SLQ Server 2005 gdzie podmiana assembly powodowałaby konieczność restartowania serwera.
Interfejs ICLRRuntimeHost
Funkcja CorBindToRuntimeEx jako ostatni argument zwraca wskaźnik do interfejsu ICLRRuntimeHost. dzięki któremu Host ma bezpośredni kontakt z Common Language Runtime. Przyjrzyjmy się kilku ważnym jego metodom (oczywiście ten interfejs posiada ich więcej).
[Kod C++]
ICLRRuntimeHost : public IUnknown {
public:
virtual HRESULT STDMETHODCALLTYPE Start(void) = 0;
virtual HRESULT STDMETHODCALLTYPE Stop(void) = 0;
virtual HRESULT STDMETHODCALLTYPE ExecuteInDefaultAppDomain(
LPCWSTR pwzAssemblyPath, LPCWSTR pwzTypeName,
LPCWSTR pwzMethodName, LPCWSTR pwzArgument, DWORD *pReturnValue) = 0;
virtual HRESULT STDMETHODCALLTYPE GetCLRControl(
ICLRControl **pCLRControl) = 0;
}; |
Pierwsza, jak nazwa wskazuje, ładuje do procesu i uruchamia CLR (samo wywołanie funkcji CorBindToRuntimeEx nie wczytuje CLR do procesu a więc i nie zajmuje żadnych zasobów). Funkcja Stop zatrzymuje silnik wykonawczy – po jej wykonaniu niema możliwości ponownego jego uruchomienia metodą Start. Trzecia pozwala wywołać funkcję z danego assembly w kontekście domyślnej AppDomain którą CLR tworzy zawsze przy swoim starcie. Pierwszy parametr określa dokładną ścieżkę do uruchamianego assembly. Parametr pwzTypeName określa nazwę klasy wraz z namespace w którym się ona znajduje, posiadającą wywoływaną funkcję której nazwa przekazywana jest jako trzeci argument. Parametr pwzArgument określa string, który będzie przekazany jako parametr wywoływanej funkcji. Ostatni parametr pozwala na pobranie wartości zwróconej przez wywołaną funkcję. Jeżeli ta nie jest wykorzystana w programie, jako parametr należy przekazać wartość NULL. Ostatnia funkcja zostanie omówiona w drugiej części artykułu.
Należy zaznaczyć, iż wywoływana w danym assembly funkcja nie może być byle jaka. Musi to być funkcja o następującym nagłówku.
[Kod C#]
static int FunctionName(string paramName); |
Pierwszy host
Prostego hosta najlepiej zrealizować jako najzwyklejszą aplikację Win32 działającą na konsoli. W tym celu należy założyć nowy projekt Visual C++\Win32 i zaznaczyć opcję Console Application. Zanim Czytelnik przystąpi do tworzenia hosta, należy poinstruować linker, aby wykorzystał statyczną bibliotekę mscoree.lib. Inaczej proces linkowania zakończy się niepowodzeniem. Można to zrobić w ustawieniach projektu, w zakładce Configuration Properties -> Linker -> Input. W polu Additional Dependencies należy dodać ścieżkę do wymienionej biblioteki. Znaleźć ją można w ”%Program Files%\Microsoft Visual Studio 8\VC\PlatformSDK\Lib\”.

Rysunek 3 - konfiguracja projektu Visual C++
Należy również pamiętać o dołączeniu pliku nagłówkowego mscoree.h w którym zdefiniowana jest większa część API CLR. Poniżej znajduje się kod wczytujący i uruchamiający CLR.
[Kod C++]
#include <mscoree.h>
int _tmain(int argc, _TCHAR* argv[]) {
ICLRRuntimeHost *pClrHost = NULL;
HRESULT hrCorBind = CorBindToRuntimeEx(
L"v2.0.50727", // Załaduj konkretną wersję CLR
L"wks", // build ”workstation”
STARTUP_CONCURRENT_GC,
CLSID_CLRRuntimeHost,
IID_ICLRRuntimeHost,
(PVOID*) &pClrHost);
// Uruchamiamy CLR
pClrHost->Start();
// Tu wykorzystujemy CLR
...
// Zatrzymujemy CLR i zwalniamy jego zasoby
pClrHost->Stop();
pClrHost->Release();
return 0;
} |
Listing 1 – prosty Host CLR
Ponieważ CLR został już załadowany do procesu, pozostało już tylko wywołać funkcję z kodu zarządzanego. Trzeba podkreślić, iż każda funkcja z niezarządzanego API zwraca status wykonania w zmiennej HRESULT. Można następnie wykorzystać jedno z dostępnych makr do sprawdzania tego statusu (oczywiście można dokonywać porównania samemu). Dla zwiększenia czytelność kodu, na przedstawionych listingach sprawdzanie statusu zostało pominięte.
Poniżej zamieszczony został kod który zostanie przez prezentowanego hosta uruchomiony.
[Kod C#]
namespace ManagedApp
{
internal class Program
{
private static void Main(string[] args)
{
Console.WriteLine("Hello from managed code!");
Console.WriteLine("\tCLR Version: " + Environment.Version);
Console.WriteLine("\tCPUs: " + Environment.ProcessorCount);
Console.WriteLine("\tOS: " + Environment.OSVersion);
Console.WriteLine("\tGC Server: " +
System.Runtime.GCSettings.IsServerGC);
}
private static int _Main(string args)
{
Main(args.Split(new char[] { ' ' },
StringSplitOptions.RemoveEmptyEntries));
return 0;
}
}
} |
Listing 2 – prosty program napisany w kodzie zarządzanym, uruchamiany przez przykładowego hosta
Poniższy kod uruchamia funkcję _Main z powyższego listingu.
[Kod C++]
HRESULT hrExec = pClrHost->ExecuteInDefaultAppDomain(L"ManagedApp.exe",
L"ManagedApp.Program", L"_Main", L"", NULL); |
Listing 3 – funkcja uruchamiająca program z listingu 2 w kontekście domyślnej AppDomain
W wywołaniu przesyłany jest pusty string reprezentujący argumenty dla uruchamianej funkcji _Main, pomijana jest również wartość jaką ona zwróci. Ponieważ podana została ścieżka względna do assembly które zawiera wywoływaną funkcję, zarówno host jak i assembly powinny znajdować się w tym samym katalogu.
Inne funkcje API
Przedstawione wcześniej funkcje CorBindToRuntimeEx, _CorExeMain czy _CorDllMain nie są jedynymi dostępnymi w niezarządzany API. Interfejs programistyczny dla CLR posiada ich znacznie więcej. Pełny opis wszystkich Czytelnik może znaleźć w [5]. Poniżej znajdują się nagłówki czterech ciekawych funkcji:
[Kod C++]
STDAPI GetCORSystemDirectory(LPWSTR pbuffer, DWORD cchBuffer, DWORD* dwLength);
STDAPI GetCORVersion(LPWSTR pbBuffer, DWORD cchBuffer, DWORD* dwLength);
STDAPI GetFileVersion(LPCWSTR szFilename, LPWSTR szBuffer, DWORD cchBuffer, DWORD* dwLength);
STDAPI LockClrVersion(FLockClrVersionCallback hostCallback,FLockClrVersionCallback *pBeginHostSetup,FLockClrVersionCallback *pEndHostSetup); |
Pierwsze dwie funkcje są niezwykle proste - zwracają w podanym jako argument buforze informacje odpowiednio o katalogu instalacyjnym Common Language Runtime który aktualnie jest wczytany do procesu oraz jego wersji. Trzecia funkcja, dla podanej w pierwszym argumencie ścieżki do pewnego assembly, zwraca wersję CLR dla której zostało ono skompilowane. Poniżej znajduje się kod ilustrujący działanie wspomnianych funkcji.
| [Kod C++]
DWORD strLen;
WCHAR verstr[16];
WCHAR asmverstr[64];
WCHAR instdirstr[255];
// Pobranie wersji CLR załadowanej do procesu oraz jej fizycznego położenia
GetCORVersion(verstr, 16, &strLen);
GetCORSystemDirectory(instdirstr, 255, &strLen);
wprintf(L"Loaded CLR %s from %s\n", verstr, instdirstr);
GetFileVersion(L"ManagedApp.exe", asmverstr, 64, &strLen);
wprintf(L"Assembly's target runtime: %s\n", asmverstr, strLen); |
Listing 4 – fragment kodu wykorzystujący omawiane funkcje
W poprzednich paragrafach Czytelnik zapoznał się z możliwością załadowania środowiska wykonawczego platformy .NET do procesu systemu operacyjnego. Ponieważ proces wczytywania CLR był manualny, host miał pełną kontrolę nad ustawieniami CLR. Istnieją jednak sytuacje, kiedy do procesu CLR zostanie załadowany automatycznie. Taki przypadek ma miejsce np. kiedy program odwoła się do obiektu COM który powstał na platformie .NET. W takim wypadku CLR zostanie uruchomiony z domyślnymi wartościami. Aby zapobiec takiej sytuacji, API CLR udostępnia funkcję LockClrVersion. Pozwala ona na pełne skonfigurowanie CLR bez jego uruchamiania (proces ten nazywa się delayed startup). W momencie automatycznego wczytania CLR, zostaną wykorzystane ustawienia hosta zamiast domyślne. Ponadto, wykorzystując omawianą funkcję, można uniemożliwić załadowania CLR do procesu. Szczegółowe omówienie zagadnienia opóźnionego startu Czytelnik znajdzie w [4].
Podsumowanie
Przedstawiony w pierwszej części artykułu interfejs programowania jest, niestety, często pomijany przez wielu programistów aplikacji powstających w technologii niezarządzanej. A przecież dzięki niemu można silnik wykonawczy platformy .NET w łatwy sposób zintegrować z natywnym oprogramowaniem i tym samym podnieść jego funkcjonalność. Dzięki niezarządzanemu API można np. stosunkowo niedużym nakładem pracy stworzyć system wtyczek do istniejącego systemu które mogły by być implementowane w kodzie zarządzanym o którego zaletach nikogo dziś nie trzeba przekonywać.
W kolejnej części artykułu omówione zostaną możliwości płynące z wykorzystania managerów dostarczonych wraz z Common Language Runtime oraz sposoby dostarczania własnych implementacji managerów na przykładzie hosta implementującego własną wersję Threading Managera. Na koniec zostanie omówiona architektura poprawnie zaprojektowanego hosta.
Referencje
[1] CLR Inside Out: CLR hosting APIs, Alessandro Catorcini and Piotr Puszkiewicz http://msdn.microsoft.com/msdnmag/issues/06/08/CLRInsideOut/default.aspx
[2] Bart De Smet's on-line blog http://community.bartdesmet.net/blogs/bart/default.aspx
[3] Implement a Custom Common Language Runtime Host for Your Managed App, Steven Pratschner http://msdn.microsoft.com/msdnmag/issues/01/03/clr/
[4] A Tour of the CLR Hosting API http://www.programmingtutorialz.com/articles/59/1/A-Tour-of-the-CLR-Hosting-API
[5] Unmanaged API Reference http://msdn2.microsoft.com/en-us/library/ms404385.aspx