Artykuły

A A A
Drukuj Ekportuj do PDF
Opublikowane: 2007.08.03 21:40 | pwlodek | Aktualizacja: 2010.01.21 2:04

CLR Unmanaged API - Tworzenie własnych hostów, część II

Celem artykuły jest zaznajomienie czytelnika z bardziej zaawansowanymi aspektami tworzenia hostów Common Language Runtime. W artykule zostaną wyjaśnione podstawy implementacji własnych managerów poprzez wykorzystywanie interfejsów managerów hosta, a także wykorzystanie gotowych implementacji managerów CLR dostarczających gotową do użycia funkcjonalność. Zostanie opisana także typowa architektura poprawnie zaprojektowanego hosta wraz z korzyściami jakie ze sobą niesie. Na początku zostanie jedna

CLR Unmanaged API

Tworzenie własnych hostów

Część druga 

Streszczenie

 

Celem artykuły jest zaznajomienie czytelnika z bardziej zaawansowanymi aspektami tworzenia hostów Common Language Runtime. W artykule zostaną wyjaśnione podstawy implementacji własnych  managerów poprzez wykorzystywanie interfejsów managerów hosta, a także wykorzystanie gotowych implementacji managerów CLR dostarczających gotową do użycia funkcjonalność. Zostanie opisana także typowa architektura poprawnie zaprojektowanego hosta wraz z korzyściami jakie ze sobą niesie. Na początku zostanie jednak omówione zagadnienie opóźnionego startu. Nie ma ono bezpośredniego związku z główną tematyką, jednak jego znajomość może stać się nieoceniona podczas pisania własnych programów hostujących silnik wykonawczy platformy Microsoft .NET.

Aby Czytelnik mógł w pełni zrozumieć całość prezentowanego materiału, podstawowa znajomość niezarządzanego API CLR’a jest wymagana. W szczególności, Czytelnik powinien wiedzieć w jaki sposób CLR jest ładowany do pamięci i jak można go załadować z poziomu kodu C++, co oznaczają poszczególne parametry inicjalizacyjne, jak uruchomić i zatrzymać CLR w procesie systemu operacyjnego. Ponadto, podstawowa wiedza dotycząca technologii COM będzie przydatna. Czytelnik niezaznajomiony z opisaną problematyką powinien wcześniej zapoznać się z tekstem części pierwszej tego artykułu dostępnej pod adresem http://codeguru.pl/article-665.aspx.

Wprowadzenie

 

Firma Microsoft tworząc platformę .NET zaprojektowała ją tak, aby była jak najbardziej rozszerzalna, aby można było dzięki niej tworzyć zarówno klasyczne oprogramowanie użytkowe czy biznesowe, ale także żeby łatwo i wygodnie można było  implementować komponenty złożonych systemów komputerowych, wymagających najwyższej wydajności i niezawodności. W przypadku klasycznych aplikacji rzadko kiedy programista potrzebuje dostosować działanie silnika wykonawczego platformy .NET do wymagań konstruowanej aplikacji. Powodem tego jest fakt, iż CLR w swojej domyślnej konfiguracji jest zoptymalizowany pod kątem wydajności i bezpieczeństwa właśnie takich aplikacji. Z resztą podstawową konfigurację samego działania CLR taką jak wybór pomiędzy wersją serwerową a workstation czy współbieżność odśmiecacza pamięci (możliwość przeprowadzania oczyszczenia pamięci w wątkach aplikacji lub w wątkach pobocznych) można przeprowadzić wprost z pliku konfiguracyjnego. Istnieją jednak sytuacje, kiedy budowany system ma pewne szczególne wymagania względem silnika wykonawczego, które nie mogą być spełnione przez domyślną konfigurację CLR czy domyślne jego zachowania. Te wymagania mogą dotyczyć takich obszarów jak własny podsystem odpowiedzialny za bezpieczeństwo, ładowanie assemblies ze specyficznych miejsc (np. poprzez sieć) czy własne mechanizmy przydziału pamięci czy tworzenia i szeregowania wątków.

 Aby możliwy był opisany powyżej scenariusz, .NET musi dostarczyć możliwość konfiguracji oraz podmiany działań kluczowych komponentów składających się na silnik wykonawczy platformy. Aby można było dostarczyć własne komponenty CLR’a w celu podmiany domyślnych, należy dostarczyć implementację własnych managerów odpowiedzialnych za funkcjonalność którą chcemy zmienić a która swoim domyślnym zachowaniem nie spełniała wszystkich wymagań aplikacji. Należy wyraźnie zaznaczyć, że decydując się na implementację własnego managera hosta programista musi dokonać pełnej jego implementacji. Nie jest możliwe zaimplementowanie wybranego tylko interfejsu będącego częścią implementowanego managera, i wykorzystanie domyślnych implementacji pozostałych interfejsów, gdyż programista nie posiada dostępu do domyślnych implementacji zawartych w CLR.

Opóźniony start

 

Mechanizm opóźnionego startu (ang. delayed startup)  nie jest bezpośrednio związany z prezentowaną tematyką, jednak jego znajomość może się przydać w szczególności gdy konstruowany system nie jest implementowany w technologii .NET a napisany jest w klasycznym kodzie niezarządzanym. W takim przypadku często zdarza się, iż CLR nie jest wykorzystywany od razu przez taką aplikację. W szczególności może się zdarzyć, iż aplikacja ani razu nie skorzysta z CLR. Dobrą praktyką jest więc nie inicjowanie w procesie aplikacji silnika wykonawczego wraz z jej uruchomieniem, a tylko dostarczenie własnego mechanizmu uruchamiającego CLR dopiero w momencie, kiedy będzie on rzeczywiście potrzebny. Dzięki temu cały czas programista ma pełną kontrolę nad tym jak Common Language Runtime zostanie załadowany do procesu, jednocześnie nie uruchamiając go od razu oszczędza się w ten sposób zasoby komputera.

Funkcja udostępniająca omawianą funkcjonalność posiada następujący nagłówek.

[Kod C++]

typedef HRESULT (__stdcall *FLockClrVersionCallback) ();

 

STDAPI LockClrVersion(FLockClrVersionCallback hostCallback,FLockClrVersionCallback

*pBeginHostSetup,FLockClrVersionCallback *pEndHostSetup);

Pierwszy parametr hostCallback jest wskaźnikiem do funkcji która zostanie wykonana w momencie, kiedy CLR będzie musiał być załadowany do procesu. Będzie miało to miejsce kiedy bezpośrednio w programie hosta nastąpi odwołanie do funkcji która wymaga, aby CLR był wczytany do procesu oraz uruchomiony. Nastąpi to także w przypadku, kiedy z kodu niezarządzanego nastąpi odwołanie do komponentu COM który został napisany w kodzie zarządzanym. W takim przypadku warstwa pośrednia COM bez wiedzy programu natywnego wczyta CLR do procesu. Jednak dzięki powyższej funkcji, nawet w tej sytuacji programista ma pełną kontrolę nad sposobem wczytania CLR. Dwa następne parametry są parametrami wyjściowymi, zwracają one wskaźniki do funkcji które programista musi wywołać bezpośrednio przed i zaraz po inicjalizacji CLR w funkcji do której wskaźnik został przekazany w pierwszym parametrze. Dzięki temu powstaje cos na wzór sekcji krytycznej – żaden inny wątek nie może już przeprowadzić inicjalizacji CLR w tej funkcji.  Również dzięki temu mechanizmowi CLR wie w którym wątku nastąpiła inicjalizacja CLR. Poniżej znajduje się fragment kodu demonstrujący użycie omawianej funkcji.

 

[Kod C++]

ICLRRuntimeHost *pClrHost = NULL;

FLockClrVersionCallback beginInit, endInit;

 

STDAPI init() {

       beginInit(); // Inicjalizacja – co najwyżej jeden raz

 

       HRESULT hr = CorBindToRuntimeEx(

              L"v2.0.50727",

              L"svr",

              STARTUP_CONCURRENT_GC | STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN,

              CLSID_CLRRuntimeHost,

              IID_ICLRRuntimeHost,

              (PVOID*) &pClrHost);

       pClrHost->Start();

 

       endInit(); // Koniec inicjalizacji

       return S_OK;

}

 

int _tmain(int argc, _TCHAR* argv[]) {

 

       // Jeżeli kiedykolwiek CLR będzie ładowany do procesu, ma costać załadowany

       // w funkcji init – pełna kontrola nad tym procesem należy do hosta 

       LockClrVersion(init, &beginInit, &endInit);

 

       // Dalsze działanie, być może wykorzystujące CLR

}

Listing 1 – wykorzystanie mechanizmu opóźnionego startu

Discovery process

 

Aby programista mógł wykorzystać możliwość  zmiany zachowania wybranych komponentów CLR, musi dostarczyć implementacje wymaganych interfejsów COM składających się na danego managera hosta. Jednocześnie często trzeba wykorzystać pewne gotowe funkcjonalności oferowane przez silnik wykonawczy takie jak np. zażądanie od garbage collector’a oczyszczenia pamięci. Programista chcący wykorzystać jakiś interfejs managera CLR (np. ICLRGCManager) musi zapytać czy ten interfejs jest przez daną wersję CLR implementowany. Jeżeli tak, można otrzymać do niego wskaźnik. Podobnie musi postąpić CLR w przypadku wszystkich managerów hosta. CLR musi zapytać hosta po kolei, czy dany manager jest przez hosta implementowany. Jeśli tak, host musi zwrócić wskaźnik do głównego interfejsu (ang. primary interface) dzięki któremu CLR będzie mógł wykorzystać dostarczoną funkcjonalność. W obu przedstawionych przypadkach mamy do czynienia z procesem odkrywania (ang. discovery process).

Wśród wszystkich interfejsów składających się na managery hosta i CLR’a, wyróżnić należy dwa, które uczestniczą w procesie odkrywania. Pierwszy z nich, ICLRControl, wykorzystać należy kiedy host chce otrzymać wskaźnik do interfejsu implementowanego przez silnik wykonawczy (ICLR*). Jego definicja przedstawiona została poniżej.

[Kod C++]

ICLRControl : public IUnknown {

public:

    virtual HRESULT STDMETHODCALLTYPE GetCLRManager(

        /* [in] */ REFIID riid,

        /* [out] */ void **ppObject) = 0;

   

    virtual HRESULT STDMETHODCALLTYPE SetAppDomainManagerType(

        /* [in] */ LPCWSTR pwzAppDomainManagerAssembly,

        /* [in] */ LPCWSTR pwzAppDomainManagerType) = 0;   

};

Wskaźnik do tego interfejsu można uzyskać bezpośrednio z uchwytu silnika wykonawczego, interfejsu ICLRRuntimeHost, wykonując na nim metodę GetCLRControl. W jedynym parametrze (wyjściowym) tej funkcji zostanie zwrócony wskaźnik do tego właśnie interfejsu. Bardzo ważne jest, aby tą operację wykonać przed uruchomieniem CLR w procesie (przed wykonaniem metody Start). Inaczej metoda GetCLRControl zwróci błąd.

Powyższy interfejs posiada bardzo ważną metodę – GetCLRManager – która jako pierwszy parametr przyjmuje identyfikator interfejsu, w drugim parametrze zwraca wskaźnik do niego o ile dany interfejs jest implementowany (sama funkcja zwraca wartość S_OK). Jeżeli podany został błędny lub nie obsługiwany identyfikator interfejsu, parametr ppObject przyjmuje wartość NULL, sama funkcja zwróci wartość E_NOINTERFACE. Zwrócony przez powyższą funkcję żądany interfejs można od razu wykorzystać.

Drugim interfejsem, wykorzystywanym przez CLR a implementowanym przez programistę hosta jest interfejs IHostControl. Jego definicja została podana poniżej.

[Kod C++]

 

IHostControl : public IUnknown {

public:

    virtual HRESULT STDMETHODCALLTYPE GetHostManager(

        /* [in] */ REFIID riid,

        /* [out] */ void **ppObject) = 0;

   

    virtual HRESULT STDMETHODCALLTYPE SetAppDomainManager(

        /* [in] */ DWORD dwAppDomainID,

        /* [in] */ IUnknown *pUnkAppDomainManager) = 0;

   

};

Powyższy interfejs jest jedynym interfejsem, który należy zaimplementować w celu zarejestrowania własnych managerów. Funkcja GetHostManager zostanie automatycznie uruchomiona przez CLR, osobno dla każdego identyfikatora interfejsu hosta który programista może zaimplementować. Dla każdego interfejsu implementowanego należy zwrócić wskaźnik do niego oraz wartość S_OK. Jeżeli danego interfejsu tworzony host nie implementuje, należy w parametrze ppObject zwrócić NULL, sama funkcja musi zwrócić wartość E_NOINTERFACE. W tym miejscu należy wyraźnie zaznaczyć, że funkcja GetHostManager zwraca w parametrze ppObject wskaźnik wyłącznie do głównego interfejsu danego managera (manager jest zbiorem interfejsów COM dostarczających razem pewną określoną funkcjonalność) a nie do pojedynczych interfejsów składających się na danego managera. CLR będzie mieć dostęp do pozostałych interfejsów zaimplementowanego managera poprzez główny interfejs, który na żądanie będzie zwracał CLR’owi pozostałe interfejsy wchodzące w skład danego managera. W tym sensie główny manager pełni rolę fabryki obiektów implementujących interfejsy danego managera.

Managery CLR

 

W poprzednim punkcie Czytelnik dowiedział się jak uzyskać dostęp do różnych interfejsów implementowanych przez CLR oraz jak uzyskać dostęp do samego interfejsu ICLRControl. Poniżej znajduje się kod programu który zostanie uruchomiony przez prezentowanego w tym podpunkcie hosta. Warto zwrócić uwagę na ostatnią linijkę funkcji Main – w tym miejscu zostanie wypisana liczba przeprowadzonych odmieceń zerowej generacji.

[Kod C#]

 

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);

        Console.WriteLine("\tCollection Count: " + GC.CollectionCount(0));

    }

 

    private static int _Main(string args)

    {

        Main(args.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));

        return 0;

    }

}

Listing 2 – prosty program w kodzie zarządzanym, uruchamiany przez przykładowego hosta

Poniżej znajduje się program hostujący wykorzystujący omówione do tej pory składniki niezarządzanego API który uruchomi powyższy program. Wykorzystuje on interfejs ICLRGCManager w celu przeprowadzenia odświecania pamięci. Po jej przeprowadzeniu CLR udostępnia statystyki związane z zarządzaną stertą. Aby z nich skorzystać, należy wykonać metodę GetStats która wypełnia danymi strukturę COR_GC_STATS przekazaną jako jedyny parametr tej funkcji (wywołanie funkcji przed przeprowadzeniem odświecania nie zakończy się błędem ale zwrócone zostaną nieprawdziwe informacje). Powyższa struktura posiada jedno ważne pole, Flags, które określa jakimi statystykami host jest zainteresowany. Musi ono zostać ustawione przed wywołaniem funkcji GetStats. Wartości jakie można ustawić to:

·         COR_GC_COUNTS – struktura COR_GC_STATS będzie zawierać wyłącznie informacje o ilości przeprowadzonych odmieceń pamięci

·         COR_GC_MEMORYUSAGE – ustawienie tej flagi spowoduje, iż struktura COR_GC_STATS zawierać będzie informacje o zużyciu pamięci na zarządzanych stertach.

W celu uzyskania pełnej informacji, można wykorzystać alternatywę tych dwóch flag tak jak w przykładzie poniżej. Opis wszystkich pól struktury COR_GC_STATS Czytelnik znajdzie w jej dokumentacji .

[Kod C++]

 

ICLRRuntimeHost *pClrHost = NULL;

ICLRControl *pCLRControl = NULL;

FLockClrVersionCallback beginInit, endInit;

 

STDAPI init() {

       beginInit(); // Inicjalizacja – co najwyżej jeden raz

 

       HRESULT hr = CorBindToRuntimeEx(...);

 

// Pobierz ICLRControl przed uruchomieniem ICLRRuntimeHost::Start

       hr = pClrHost->GetCLRControl(&pCLRControl);   

pClrHost->Start();

 

       endInit();

 

       return S_OK;

}

 

int _tmain(int argc, _TCHAR* argv[]) {

       HRESULT hr;

       DWORD retVal, strLen;

       ICLRGCManager *pCLRGCManager = NULL;

 

       LockClrVersion(init, &beginInit, &endInit);

 

       hr = pCLRControl->GetCLRManager(IID_ICLRGCManager, (PVOID *)&pCLRGCManager);   

      

       // Wykonaj odśmiecanie na wszystkich stertach

       pCLRGCManager->Collect(0);

 

       // Uruchom zarządzany kod w kontekście domyślnej AppDomain

       hr = pClrHost->ExecuteInDefaultAppDomain(...);

 

       COR_GC_STATS stats;

       stats.Flags = COR_GC_COUNTS | COR_GC_MEMORYUSAGE;

       pCLRGCManager->GetStats(&stats);

      

       printf("CommittedKBytes: %d\n", stats.CommittedKBytes);

       printf("ExplicitGCCount: %d\n", stats.ExplicitGCCount);

       printf("Gen0HeapSizeKBytes: %d\n", stats.Gen0HeapSizeKBytes);

       printf("Gen1HeapSizeKBytes: %d\n", stats.Gen1HeapSizeKBytes);

       printf("Gen2HeapSizeKBytes: %d\n", stats.Gen2HeapSizeKBytes);

       printf("LargeObjectHeapSizeKBytes: %d\n", stats.LargeObjectHeapSizeKBytes);

       printf("ReservedKBytes: %d\n", stats.ReservedKBytes);

 

       pCLRControl->Release();

       pCLRGCManager->Release();

       pClrHost->Stop();

       pClrHost->Release();

 

       return 0;

}

Listing 3 – prosty host CLR wykorzystujący managera CLR

Managery hosta

 

Tworzenie własnych managerów jest często procesem trudnym i skomplikowanym. Odnosi się to w szczególności do managerów hosta odpowiedzialnych za wielowątkowość, synchronizację pomiędzy zadaniami czy zarządzanie pamięcią. W bieżącym przykładzie zostanie przedstawiony prosty host dostarczający własnego managera wielozadaniowości. Jego celem będzie śledzenie zarządzanych wątków oraz wypisanie ich PIDów na końcu jego działania. Można go uruchomić z konsoli podając jako parametr nazwę programu do uruchomienia: DemoHost3.exe mój_prog.exe.

Prezentowany host  składa się z dwóch części – niezarządzanej, która dostarcza implementację własnego managera wielozadaniowości, oraz zarządzanej, która przygotowuje środowisko do uruchomienia właściwego kodu zarządzanego użytkownika (konfiguruje nową AppDomain dla kodu użytkownika). Temat związany z architekturą hosta został omówiony w dalszej części artykułu.

Manager wielozadaniowości (ang. threading manager) posługuje się abstrakcyjnym pojęciem zadania. Jest to najmniejsza jednostka mogąca otrzymać czas procesora. W podstawowej implementacji zadanie jest zaimplementowane w postaci wątku z WIN32 API. W bieżącym przykładzie zadanie również będzie zaimplementowane w oparciu o watki Windows.  Najważniejsze dwa interfejsy wchodzące w skład managera wielozadaniowości to IHostTask – interfejs definiujący podstawowe zachowania zadania, oraz IHostTaskManager – główny interfejs managera wielozadaniowości udostępniający CLR’owi implementację zadania.

IUnknown

Czytelnik z pewnością zwrócił uwagę na interfejs IUnknown z którego dziedziczą wszystkie interfejsy w niezarządzanym API CLR’a. Jest to bazowy interfejs technologii COM który musi być rozszerzony przez każdy inny interfejs który ma być prawidłowym interfejsem COM. Ponieważ całe niezarządzane API oparte jest o tę technologię, wszystkie interfejsy bez wyjątku implementują IUnknown.

Posiada on trzy ważne metody. Dwie z nich, AddRef i Release, implementują zliczanie referencji. Jest to jedno z podejść do zagadnienia automatycznego odświecania pamięci. Za każdym razem kiedy programista chce uzyskać referencję do obiektu, musi zwiększyć licznik referencji metodą AddRef. Kiedy kończy korzystanie z obiektu, zwalnia go zmniejszając liczbę referencji o jeden wywołując metodę Release. Jeżeli licznik referencji spadnie do zera, obiekt jest usuwany z pamięci. Należy pamiętać, aby zwiększanie i zmniejszanie licznika były operacjami atomowymi, inaczej w środowisku wielowątkowym może dojść do wycieku pamięci jeżeli dwa wątki na raz chciałyby zwiększyć lub zmniejszyć licznik. Można w tym celu wykorzystać funkcje InterlockedIncrement oraz InterlockedDecrement. Ostatnia metoda, QueryInterface, służy do zwracania wskaźnika do interfejsu obiektu który dany obiekt może zwrócić. Metoda QueryInterface powinna zawsze zwrócić wskaźnik do własnego obiektu na rzecz którego została wywołana (przez wskaźnik this), jeżeli przekazany jako pierwszy parametr identyfikator interfejsu to IUnknown lub identyfikator interfejsu obiektu na którym wywoływana jest metoda QueryInterface. Metoda ta zawsze powinna wykonać AddRef na interfejsie który jest zwracany (jako drugi parametr).

IHostTask

Implementując ten interfejs programista hosta dostarcza własną wersję zadania. W prezentowanym przykładzie implementacja ta będzie zwykłą realizacją w oparciu o wątki Windows i nie będzie posiadać żadnej specjalnej logiki. Należy jednak pamiętać, iż decydując się na implementację własnego managera należy dostarczyć implementacje wszystkich wymaganych interfejsów.

Z klasą implementującą zadanie związane są dwa obiekty (poprzez kompozycję). Pierwszym obiektem jest uchwyt do wątku który będzie wykonywać dane zadanie. Drugim obiektem jest obiekt implementujący interfejs ICLRTask. Zadanie w CLR jest bowiem reprezentowane przez dwa obiekty które powiązane są ze sobą relacją jeden-do-jeden. Z każdym obiektem IHostTask, Common language Runtime wiąże obiekt ICLRTask od którego można zażądać czynności których IHostTask nie posiada (np. oddać czas procesora innemu zadaniu). CLR sam dokonuje tej asocjacji poprzez wykonanie metody SetCLRTask na interfejsie IHostTask przekazując instancję obiektu ICLRTask jako parametr. Poniżej znajduje się definicja klasy która implementuje zadanie.

[Kod C++]

 

class DDTask : public IHostTask {

private:

    volatile LONG m_cRef; // Licznik referencji

    HANDLE m_hThread; // Uchwyt do wątku

    ICLRTask *m_pCLRTask; // Reprezentacja zadania po stronie CLR

 

public:

    DDTask(HANDLE hThread);

    ~DDTask();

 

    // IUnknown functions

    STDMETHODIMP_(DWORD) AddRef();

    STDMETHODIMP_(DWORD) Release();

    STDMETHODIMP QueryInterface(const IID &riid, void **ppvObject);

 

    // IHostTask functions

    STDMETHODIMP Start();

    STDMETHODIMP Alert();

    STDMETHODIMP Join(/* in */ DWORD dwMilliseconds, /* in */ DWORD dwOption);

    STDMETHODIMP SetPriority(/* in */ int newPriority);

    STDMETHODIMP GetPriority(/* out */ int *pPriority);

    STDMETHODIMP SetCLRTask(/* in */ ICLRTask *pCLRTask);

};

 

Jak widać, sam interfejs IHostTask posiada niewiele metod, ich implementacja sprowadza się do wywoływania odpowiednich funkcji z WIN API. Czytelnik nie powinien mieć problemów z samodzielną ich implementacją.

IHostTaskManager

Jest to główny interfejs managera wielozadaniowości. Dzięki niemu CLR otrzyma dostęp do dostarczonej przez programistę implementacji zadania. Interfejs posiada bardzo dużo metod, jednak aby host nie był przesadnie skomplikowany, ich implementacja została pominięta (host dalej działa poprawnie, takie postępowanie nie jest jednak zalecane). Poniżej znajduje się definicja klasy implementującej ten interfejs (wymienione zostały tylko kluczowe metody).

[Kod C++]

 

class DDTaskManager : public IHostTaskManager {

private:

    volatile LONG m_cRef;

    ICLRTaskManager *m_pCLRTaskManager;

    map<DWORD, IHostTask*> *m_pThreadMap;

 

    CrstLock *m_pThreadMapCrst;

 

    // W tym obiekcie znajduje się lista wątków uruchomionych przez CLR

    DDContext *m_pContext;

public:

    DDTaskManager(DDContext *pContext);

    ~DDTaskManager();

 

    // IUnknown functions

    STDMETHODIMP_(DWORD) AddRef();

    STDMETHODIMP_(DWORD) Release();

    STDMETHODIMP QueryInterface(const IID &riid, void **ppvObject);

 

    // IHostTaskManager functions

    STDMETHODIMP GetCurrentTask(/* out */ IHostTask **pTask);

    STDMETHODIMP CreateTask(/* in */ DWORD dwStackSize, /* in */

        LPTHREAD_START_ROUTINE pStartAddress, /* in */ PVOID pParameter, /* out */

        IHostTask **ppTask);

    (...)

    STDMETHODIMP SetCLRTaskManager(/* in */ ICLRTaskManager *pManager);

};

Podobnie jak w przypadku poprzedniego interfejsu, także i ten posiada swój odpowiednik po stronie CLR’a – ICLRTaskManager. CLR sam przekaże interfejsowi IHostTaskManager wskaźnik do instancji tego interfejsu wywołując metodę SetCLRTaskManager.

Z punktu widzenia prezentowanego przykładu najważniejsze dwie metody w interfejsie IHostaTaskManager to CreateTask i GetCurrentTask gdyż w nich zaszyta będzie logika hosta – śledzenie uruchomionych wątków.

Funkcja CreateTask odpowiedzialna jest za stworzenie nowego zadania. Pierwszy parametr określa początkowy rozmiar stosu który ma zostać przydzielony nowo tworzonemu zadaniu. Drugi parametr jest wskaźnikiem do funkcji która zostanie przez zadanie wykonana, trzeci to opcjonalny parametr dla funkcji wykonującej kod użytkownika. W ostatnim parametrze funkcja musi zwrócić wskaźnik do nowo utworzonego zadania. Oczywiście wszystkie parametry poza ostatnim dostarcza CLR gdyż tylko on bezpośrednio wykorzystuje ten interfejs.

Pierwszą czynnością funkcji CreateTask będzie utworzenie nowego wątku w stanie uśpionym (ang. suspended) korzystając z funkcji CreateThread i przekazując jej wyżej wymienione parametry. Kolejną rzeczą jest stworzenie instancji klasy DDHostTask implementującej IHostTask i przekazanie jej uchwytu do utworzonego wątku. Instancję tej klasy należy również zwrócić CLR’owi w ostatnim parametrze. Następnie trzeba zapamiętać parę (ID wątku, zadanie) w strukturze mapy. Informacja ta będzie potrzebna w drugiej funkcji. Ostatnią rzeczą jest wykonanie niezwykle prostej logiki hosta – zapisanie identyfikatora wątku wraz z jego uchwytem w liście. Lista ta pobierana jest z klasy DDContext tworzonej na początku działania hosta. Należy zaznaczyć, iż operacje na wspólnych strukturach danych (w tym przypadku pojemniki lista i mapa) muszą być synchronizowane. Do tego celu można wykorzystać sekcję krytyczną z WIN API która w przykładzie została opakowana w klasie CrstLock. Poniżej znajduje się listing prezentujący omawianą funkcję.

[Kod C++]

 

STDMETHODIMP DDTaskManager::CreateTask(/* in */ DWORD dwStackSize, /* in */ LPTHREAD_START_ROUTINE pStartAddress, /* in */ PVOID pParameter, /* out */ IHostTask **ppTask) {

    DWORD dwThreadId;

    HANDLE hThread = CreateThread( // Utworzenie nowego wątku

        NULL,

        dwStackSize,

        pStartAddress,

        pParameter,

        CREATE_SUSPENDED | STACK_SIZE_PARAM_IS_A_RESERVATION,

        &dwThreadId);

 

    IHostTask* task = new DDTask(hThread); // Tworzymy nowe zadanie

 

    m_pThreadMapCrst->Enter();   

    m_pThreadMap->insert(map<DWORD, IHostTask*>::value_type(dwThreadId, task));

 

    // Logika hosta – zapisanie informacji o utworzonym zadaniu

    ThreadInfo info;

    info.threadHandle = hThread;

    info.threadId = dwThreadId;

    m_pContext->GetTasksList()->insert(m_pContext->GetTasksList()->begin(),

       list<ThreadInfo>::value_type(info));           

    m_pThreadMapCrst->Exit();

 

    task->AddRef(); // Zwiększamy licznik referencji

    *ppTask = task;

 

    return S_OK;

}

Listing 4 – tworzenie zadania

 

Ostatnią ważną funkcją jest GetCurrentTask. Funkcja ta powinna zwrócić klasę implementującą zadanie dla wątku z którego nastąpiło jej wywołanie. Ponieważ można bez przeszkód poznać identyfikator wątku z którego nastąpiło wywołanie, poprawną instancję klasy DDHostTask można otrzymać przeszukując mapę wypełnianą w funkcji CreateTask. W funkcji GetCurrentTask znajduje się pewna pułapka. Może bowiem zdarzyć się, iż dany wątek który wywołał metodę GetCurrentTask nie był tworzony przez metodę CreateTask a przez wewnętrzne mechanizmy CLR. W takim przypadku mapa nie będzie zawierać wpisu dla takiego wątku, należy więc utworzyć nową instancję klasy DDHostTask i przypisać jej uchwyt bieżącego wątku. Na końcu należy zaktualizować strukturę mapy i listy. Poniżej znajduje się listing prezentujący omawianą funkcję.

[Kod C++]

 

STDMETHODIMP DDTaskManager::GetCurrentTask(/* out */ IHostTask **pTask) {

    DWORD currentThreadId = GetCurrentThreadId();

          

    m_pThreadMapCrst->Enter();

    map<DWORD, IHostTask*>::iterator match = m_pThreadMap->find(currentThreadId);

    if (match == m_pThreadMap->end()) {        

        // Zadanie nie zostało odnalezione – trzeba utworzyć nowe

        *pTask = new DDTask(GetCurrentThread());

        m_pThreadMap->insert(map<DWORD, IHostTask*>::value_type(currentThreadId,

     *pTask));

 

        ThreadInfo info;

        info.threadHandle = GetCurrentThread();

        info.threadId = currentThreadId;

        m_pContext->GetTasksList()->insert(m_pContext->GetTasksList()->begin(),

     list<ThreadInfo>::value_type(info));             

    } else {

        *pTask = match->second;

    }

    (*pTask)->AddRef();

    m_pThreadMapCrst->Exit();

 

    return S_OK;

}

Listing 5 – pobranie klasy zadania dla bieżącego wątku

 

IHostControl

Po zaimplementowaniu własnego managera ostatnią rzeczą jaką trzeba wykonać jest jego zarejestrowanie w CLR tak aby mógł on być przez niego wykorzystany. Do tego celu służy interfejs IHostControl którego implementację należy dostarczyć. Cała logika rejestrująca znajduje się w funkcji GetHostManager która jest zaimplementowana w klasie DDHostControl (która rozszerza omawiany interfejs). Logika jest niezwykle prosta i sprowadza się do jednej instrukcji warunkowej sprawdzającej czy podany jako pierwszy parametr identyfikator interfejsu jest równy identyfikatorowi głównego interfejsu managera wielowątkowości. Jeżeli tak, zwracany jest do niego wskaźnik poprzez drugi parametr. W każdej innej sytuacji funkcja zwraca E_NOINTERFACE. Poniżej znajduje się kod omawianej funkcji.

[Kod C++]

 

STDMETHODIMP DDHostControl::GetHostManager(const IID &riid, void **ppvHostManager) {

    if (riid == IID_IHostTaskManager) {

        m_pTaskManager->QueryInterface(IID_IHostTaskManager, ppvHostManager);

        return S_OK;

    }

   

    ppvHostManager = NULL;

    return E_NOINTERFACE;

}

Listing 6 – rejestracja managera wielozadaniowości w CLR

Wskaźnik do głównego interfejsu manager wielowątkowości zwracany jest przez funkcję QueryInterface, dzięki temu nie trzeba manualnie inkrementować licznika referencji – odpowiednie wywołanie znajduje się właśnie w QueryInterface.

Mając gotową implementację klasy DDHostControl (implementującej IHostControl), należy przekazać jej instancję CLR’owi. Dokonuje się tego poprzez wywołanie funkcji SetHostControl na uchwycie do silnika wykonawczego (interfejs ICLRRuntimeHost) tak jak pokazano na poniższym listingu.

[Kod C++]

 

DDHostControl *pHostControl = new DDHostControl();

 

// pClrHost jest typu ICLRRuntimeHost

HRESULT hr = pClrHost->SetHostControl(pHostControl);

Listing 7– zarejestrowanie w CLR obiektu DDHostControl

Ważne aby metoda SetHostControl została wykonana przed uruchomieniem CLR’a w procesie. Inaczej jej wywołanie nie przyniesie żadnego efektu.

Architektura

 

Typowy host CLR jest implementowany w postaci dwóch odrębnych, choć współpracujących modułów. Pierwszy z nich implementowany jest w kodzie niezarządzanym (praktycznie zawsze C++). Jego celem jest wczytanie CLR do procesu, skonfigurowanie i uruchomienie. Moduł ten jest także odpowiedzialny za dostarczenie własnych implementacji managerów hostów. Ta część najczęściej jest zdecydowanie największa i najtrudniejsza w implementacji. Drugi moduł implementowany jest w kodzie zarządzanym jako tzw. Shim i jest odpowiedzialny za przygotowanie środowiska dla zarządzanego kodu użytkownika. Przygotowanie środowiska obejmuje przede wszystkim skonfigurowanie domen aplikacji oraz uruchomienie kodu użytkownika w osobnych wątkach.

Architecture

Rysunek 1 - typowa architektura hosta CLR

Wykonanie kodu użytkownika w innej niż domyślna domenie aplikacji niesie ze sobą szereg korzyści:

·         Izolacja kodu – kod użytkownika nie będzie wpływał na zarządzaną część hosta. Dodatkowo kod użytkownika może być uruchomiony w wielu różnych domenach aplikacji po to, by newralgiczne jego części były od siebie odseparowane i nie wpływały na siebie.

·         Osobna konfiguracja – każdą AppDomain można oddzielnie skonfigurować przypisując jej wymagane ustawienia w sposób deklaratywny w kodzie. Można także każdej AppDomain osobno przypisać plik xml (App.config) z którego będzie czerpać wszystkie ustawienia.

·         Bezpieczeństwo – dla każdej domeny aplikacji można osobno zdefiniować zasady bezpieczeństwa.

·         Możliwość wyładowania – pojedynczą domenę aplikacji można usunąć z pamięci jeżeli nie jest już dalej potrzebna oddając zasoby systemowi operacyjnemu. Oczywistym jest fakt, że domyślna AppDomain nie może zostać wyładowana bez zamknięcia procesu.

Rozbicie hosta na cześć napisaną w kodzie natywnym i zarządzanym posiada dodatkowo dwie zalety. Po pierwsze zwiększa się nieznacznie wydajność rozwiązania. Ma to związek z tym, iż koszt wywołań pomiędzy kodem zarządzanym i niezarządzanym nie jest zaniedbywalny. Ale jeżeli sterowanie przebiegiem wykonania programu użytkownika przeprowadzane jest przez zarządzaną część hosta, liczba takich wywołań jest zredukowana do niezbędnej komunikacji pomiędzy oboma modułami hosta. Druga zaleta to łatwość implementacji. Jeżeli host musi w jakikolwiek sposób nadzorować wykonywanie kodu użytkownika, taką logikę warto napisać w kodzie zarządzanym gdyż będzie to prostsze zarówno na etapie implementacji, jak i utrzymania. Poniżej znajduje się prosty moduł hosta CLR napisany w kodzie zarządzanym, którego celem jest uruchomienie kodu użytkownika w osobnym wątku i osobnej AppDomain. Host uruchamia swoją zarządzaną część (metoda Start klasy Shim) korzystając z funkcji ExecuteInDefaultAppDomain interfejsu ICLRRuntimeHost.

[Kod C#]

 

public class Shim

{

    private static int Start(string args)

    {

        int exitCode = -1;

 

        try

        {

            // Rozdziel wejście między spacjami

            string[] shimArgs = args.Split(new char[] { ' ' }, 

                StringSplitOptions.RemoveEmptyEntries);               

 

            string[] _args = new string[shimArgs.Length - 1];

            Array.Copy(shimArgs, 1, _args, 0, _args.Length);

 

            // Uruchom program użytkownika we własnym wątku i AppDomain

            AppDomain appDomain = AppDomain.CreateDomain("UserCodeDomain");

            appDomain.AssemblyResolve += new ResolveEventHler(Shim.AssemblyResolve);

 

            Thread mainThread = new Thread(delegate()

            {

                try

                {

                    // Uruchom assembly użtkownika

                    exitCode = appDomain.ExecuteAssembly(shimArgs[0],

                        appDomain.Evidence, _args);

 

                    // Wyładuj domenę aplikacji

                    AppDomain.Unload(appDomain);

                }

                catch (Exception ex)

                {

                    Console.WriteLine("Program error: " + ex.Message);

                }

            });

 

            mainThread.Start();

            mainThread.Join();

        }

        catch (Exception ex)

        {

            Console.WriteLine("Shim error:" + ex.Message);

        }

 

        return exitCode;

    }

 

    private static Assembly AssemblyResolve(object sender, ResolveEventArgs args)

    {

        // Nie znaleziono żądanego assembly, wyszukaj je manualnie

    }

}

Listing 8– część hosta napisana w kodzie zarządzanym odpowiedzialna za konfigurację środowiska w którym nastąpi uruchomienie kodu użytkownika

Podsumowanie

 

W drugiej części artykułu zostały poruszone bardziej zaawansowane tematy związane z tworzeniem własnej warstwy hostującej silnik wykonawczy platformy Microsoft .NET. Przedstawiony został zarówno mechanizm wykorzystania managerów CLR jak i sposób dostarczenia własnych implementacji managerów CLR. Na koniec została przedstawiona architektura poprawnie zaprojektowanego hosta. Artykuł omawia zaledwie niezbędne podstawy które umożliwią Czytelnikowi dalsze eksperymenty z niezarządzanym API jakie dostarcza firma Microsoft wraz ze swoją nowoczesną platformą programistyczną. To właśnie dzięki możliwości tworzenia własnych hostów .NET Framework jest tak konfigurowalny i rozszerzalny. Dzięki temu, dla platformy Microsoft .NET mogą powstawać najbardziej skomplikowane systemy komputerowe mogące sprostać dzisiejszym wymaganiom.

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

 


Podobne artykuły

Komentarze 0 Masz uwagi do tej strony? Napisz

Dodaj komentarz

avatar

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.

Autor pwlodek
avatar
 

Załóż konto
CodeGuru to miejsce dla każdego programisty. Przez lata portal rozwijany był siłami społeczności i to właśnie społeczność programistów jest tutaj najważniejsza. CG od wielu lat gromadzi wokół siebie coraz większą grupę pasjonatów. Warto być jej częścią!

Dowiedz się więcej o CodeGuru

Geek Club - Windows Phone

 

MetroOne

Idź na górę strony