TN058: implementacja stanu modułu MFC

Uwaga

Następująca uwaga techniczna nie została zaktualizowana, ponieważ została po raz pierwszy uwzględniona w dokumentacji online. W związku z tym niektóre procedury i tematy mogą być nieaktualne lub nieprawidłowe. Aby uzyskać najnowsze informacje, zaleca się wyszukanie interesującego tematu w indeksie dokumentacji online.

Ta uwaga techniczna opisuje implementację konstrukcji "stanu modułu" MFC. Zrozumienie implementacji stanu modułu ma kluczowe znaczenie dla używania udostępnionych bibliotek DLL MFC z biblioteki DLL (lub serwera przetwarzania OLE).

Przed przeczytaniem tej notatki zapoznaj się z tematem "Zarządzanie danymi stanu modułów MFC" w temacie Tworzenie nowych dokumentów, systemu Windows i widoków. Ten artykuł zawiera ważne informacje o użyciu i informacje o omówieniu tego tematu.

Omówienie

Istnieją trzy rodzaje informacji o stanie MFC: stan modułu, stan procesu i stan wątku. Czasami można połączyć te typy stanów. Na przykład mapy dojścia MFC są lokalne i wątkowe modułu. Dzięki temu dwa różne moduły mogą mieć różne mapy w każdym z ich wątków.

Stan procesu i stan wątku są podobne. Te elementy danych to elementy, które tradycyjnie były zmiennymi globalnymi, ale muszą być specyficzne dla danego procesu lub wątku w celu zapewnienia odpowiedniej obsługi win32s lub odpowiedniej obsługi wielowątkowej. Kategoria, w której pasuje dany element danych, zależy od tego elementu i jego żądanej semantyki w odniesieniu do granic procesów i wątków.

Stan modułu jest unikatowy w tym, że może zawierać stan prawdziwie globalny lub stan, który jest procesem lokalnym lub wątkowym lokalnym. Ponadto można go szybko przełączać.

Przełączanie stanu modułu

Każdy wątek zawiera wskaźnik do stanu modułu "current" lub "active" (nic dziwnego, że wskaźnik jest częścią stanu lokalnego wątku MFC). Ten wskaźnik jest zmieniany, gdy wątek wykonywania przechodzi granicę modułu, taką jak aplikacja wywołująca kontrolkę OLE lub bibliotekę DLL lub kontrolkę OLE wywołującą z powrotem do aplikacji.

Bieżący stan modułu jest przełączany przez wywołanie metody AfxSetModuleState. W większości przypadków nigdy nie będziesz radzić sobie bezpośrednio z interfejsem API. MFC, w wielu przypadkach, wywoła go za Ciebie (w WinMain, punktów wejścia OLE, AfxWndProcitp.). Odbywa się to w dowolnym składniku zapisywanym przez statyczne łączenie w specjalnym WndProcobiekcie i specjalne WinMain (lub DllMain), które wie, który stan modułu powinien być aktualny. Ten kod można zobaczyć, przeglądając bibliotekę DLLMODUL. CPP lub APPMODUL. CPP w katalogu MFC\SRC.

Rzadko zdarza się, że chcesz ustawić stan modułu, a następnie nie ustawić go z powrotem. Przez większość czasu chcesz "wypchnąć" własny stan modułu jako bieżący, a następnie, po zakończeniu, "pop" oryginalny kontekst z powrotem. Jest to wykonywane przez makro AFX_MANAGE_STATE i klasę AFX_MAINTAIN_STATEspecjalną .

CCmdTarget Ma specjalne funkcje do obsługi przełączania stanu modułu. W szczególności klasa a jest klasą CCmdTarget główną używaną do automatyzacji OLE i punktów wejścia OLE COM. Podobnie jak każdy inny punkt wejścia uwidoczniony w systemie, te punkty wejścia muszą ustawić prawidłowy stan modułu. W jaki sposób dana CCmdTarget funkcja wie, jaki powinien być stan modułu "poprawny", to odpowiedź polega na tym, że "zapamiętuje" stan modułu "current" podczas jego konstruowania, tak aby można było ustawić bieżący stan modułu na wartość "zapamiętaną", gdy zostanie ona później wywołana. W związku z tym moduł stwierdza, że dany CCmdTarget obiekt jest skojarzony ze stanem modułu, który był bieżący podczas konstruowania obiektu. Zapoznaj się z prostym przykładem ładowania serwera INPROC, tworzenia obiektu i wywoływania jego metod.

  1. Biblioteka DLL jest ładowana przez obiekt OLE przy użyciu polecenia LoadLibrary.

  2. RawDllMain jest wywoływana jako pierwsza. Ustawia stan modułu na znany stan statycznego modułu dla biblioteki DLL. Z tego powodu RawDllMain jest statycznie połączony z biblioteką DLL.

  3. Wywoływany jest konstruktor fabryki klas skojarzonej z naszym obiektem. COleObjectFactory pochodzi z CCmdTarget elementu i w rezultacie zapamiętuje, w którym stanie modułu zostało utworzone wystąpienie. Jest to ważne — gdy zostanie poproszona fabryka klas o utworzenie obiektów, teraz wie, jaki stan modułu ma być aktualny.

  4. DllGetClassObject jest wywoływany w celu uzyskania fabryki klas. MFC wyszukuje listę fabryk klas skojarzona z tym modułem i zwraca ją.

  5. Wywołano metodę COleObjectFactory::XClassFactory2::CreateInstance. Przed utworzeniem obiektu i zwróceniem go ta funkcja ustawia stan modułu na stan modułu, który był bieżący w kroku 3 (ten, który był bieżący podczas COleObjectFactory tworzenia wystąpienia). Odbywa się to wewnątrz METHOD_PROLOGUE.

  6. Po utworzeniu obiektu jest on również pochodną CCmdTarget i w ten sam sposób COleObjectFactory zapamiętany, który stan modułu był aktywny, więc czy ten nowy obiekt. Teraz obiekt wie, który stan modułu ma zostać przełączony na zawsze, gdy jest wywoływany.

  7. Klient wywołuje funkcję w obiekcie OLE COM odebranym od wywołania CoCreateInstance . Gdy obiekt jest wywoływany, używa METHOD_PROLOGUE go do przełączania stanu modułu tak samo jak COleObjectFactory w przypadku.

Jak widać, stan modułu jest propagowany z obiektu do obiektu podczas ich tworzenia. Należy odpowiednio ustawić stan modułu. Jeśli go nie ustawiono, obiekt DLL lub COM może źle współdziałać z aplikacją MFC, która ją wywołuje, lub może nie być w stanie znaleźć własnych zasobów lub może zakończyć się niepowodzeniem w inny nieszczęśliwy sposób.

Należy pamiętać, że niektóre rodzaje bibliotek DLL, a w szczególności biblioteki DLL "Rozszerzenia MFC" nie przełączają stanu modułu w swoich RawDllMain bibliotekach (w rzeczywistości zwykle nie mają RawDllMainnawet biblioteki ). Jest to spowodowane tym, że mają zachowywać się "tak, jakby" były rzeczywiście obecne w aplikacji, która ich używa. Są one bardzo częścią uruchomionej aplikacji i jej zamiarem jest zmodyfikowanie stanu globalnego tej aplikacji.

Kontrolki OLE i inne biblioteki DLL są bardzo różne. Nie chcą modyfikować stanu aplikacji wywołującej; aplikacja, która je wywołuje, może nawet nie być aplikacją MFC i dlatego nie może być żadnego stanu do zmodyfikowania. Jest to powód, dla którego zostało wynalezione przełączanie stanu modułu.

W przypadku wyeksportowanych funkcji z biblioteki DLL, takiej jak ta, która uruchamia okno dialogowe w bibliotece DLL, należy dodać następujący kod na początku funkcji:

AFX_MANAGE_STATE(AfxGetStaticModuleState())

Spowoduje to zamianę bieżącego stanu modułu ze stanem zwróconym z elementu AfxGetStaticModuleState do końca bieżącego zakresu.

Jeśli makro AFX_MODULE_STATE nie zostanie użyte, wystąpią problemy z zasobami w bibliotekach DLL. Domyślnie MFC używa dojścia zasobów głównej aplikacji do załadowania szablonu zasobu. Ten szablon jest rzeczywiście przechowywany w dll. Główną przyczyną jest to, że informacje o stanie modułu MFC nie zostały przełączone przez makro AFX_MODULE_STATE. Dojście zasobów jest odzyskiwane ze stanu modułu MFC. Nie przełączaj stanu modułu powoduje użycie nieprawidłowego dojścia zasobu.

AFX_MODULE_STATE nie musi być umieszczana w każdej funkcji w bibliotece DLL. Na przykład można wywołać kod MFC w aplikacji bez AFX_MODULE_STATE, InitInstance ponieważ MFC automatycznie przesuwa stan modułu przed InitInstance , a następnie przełącza go z powrotem po InitInstance powrocie. To samo dotyczy wszystkich procedur obsługi map komunikatów. Zwykłe biblioteki DLL MFC mają specjalną procedurę okna głównego, która automatycznie przełącza stan modułu przed routingiem dowolnego komunikatu.

Przetwarzanie danych lokalnych

Przetwarzanie danych lokalnych nie byłoby tak dużym problemem, gdyby nie trudności z modelem DLL Win32s. W win32s wszystkie biblioteki DLL współużytkują swoje dane globalne, nawet jeśli są ładowane przez wiele aplikacji. Różni się to bardzo od "rzeczywistego" modelu danych win32 DLL, w którym każda biblioteka DLL pobiera oddzielną kopię przestrzeni danych w każdym procesie dołączanym do biblioteki DLL. Aby zwiększyć złożoność, dane przydzielone do sterty w dll Win32s są w rzeczywistości specyficzne dla procesu (przynajmniej jeśli chodzi o własność). Rozważ następujące dane i kod:

static CString strGlobal; // at file scope

__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
    strGlobal = lpsz;
}

__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, size_t cb)
{
    StringCbCopy(lpsz, cb, strGlobal);
}

Zastanów się, co się stanie, jeśli powyższy kod znajduje się w dll i że biblioteka DLL jest ładowana przez dwa procesy A i B (w rzeczywistości może to być dwa wystąpienia tej samej aplikacji). Wywołania .SetGlobalString("Hello from A") W związku z tym pamięć jest przydzielana dla CString danych w kontekście procesu A. Należy pamiętać, że CString sama pamięć jest globalna i jest widoczna zarówno dla A, jak i B. Teraz B wywołuje metodę GetGlobalString(sz, sizeof(sz)). B będzie mógł zobaczyć dane ustawione przez zestaw A. Dzieje się tak, ponieważ win32s nie zapewnia żadnej ochrony między procesami, takimi jak Win32. Jest to pierwszy problem; w wielu przypadkach nie jest pożądane, aby jedna aplikacja miała wpływ na dane globalne, które są uważane za należące do innej aplikacji.

Istnieją również dodatkowe problemy. Załóżmy, że A teraz kończy działanie. Po zakończeniu działania A pamięć używana przez ciąg "strGlobal" jest udostępniana dla systemu — czyli cała pamięć przydzielona przez proces A jest automatycznie zwalniana przez system operacyjny. Nie jest uwolniony, ponieważ CString destruktor jest wywoływany; nie został jeszcze wywołany. Jest on zwalniany po prostu dlatego, że aplikacja, która ją przydzieliła, opuściła scenę. Teraz, jeśli B o nazwie GetGlobalString(sz, sizeof(sz)), może nie uzyskać prawidłowych danych. Niektóre inne aplikacje mogły używać tej pamięci dla czegoś innego.

Oczywiście istnieje problem. W MFC 3.x użyto techniki nazywanej magazynem lokalnym wątku (TLS). MFC 3.x przydzieli indeks TLS, który w systemie Win32s naprawdę działa jako indeks magazynu lokalnego procesu, mimo że nie jest wywoływany, a następnie odwołuje się do wszystkich danych na podstawie tego indeksu PROTOKOŁU TLS. Jest to podobne do indeksu TLS, który był używany do przechowywania danych lokalnych wątków w systemie Win32 (zobacz poniżej, aby uzyskać więcej informacji na ten temat). Spowodowało to, że każda biblioteka DLL MFC korzysta z co najmniej dwóch indeksów TLS na proces. Podczas ładowania wielu bibliotek DLL kontrolek OLE (OCX) szybko zabraknie indeksów TLS (dostępnych jest tylko 64). Ponadto MFC musiał umieścić wszystkie te dane w jednym miejscu w jednej strukturze. Nie był bardzo rozszerzalny i nie był idealny w odniesieniu do stosowania indeksów TLS.

Adresy MFC 4.x z zestawem szablonów klas, które można "zawinąć" wokół danych, które powinny być przetwarzane lokalnie. Na przykład problem wymieniony powyżej można rozwiązać, pisząc:

struct CMyGlobalData : public CNoTrackObject
{
    CString strGlobal;
};
CProcessLocal<CMyGlobalData> globalData;

__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
    globalData->strGlobal = lpsz;
}

__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, size_t cb)
{
    StringCbCopy(lpsz, cb, globalData->strGlobal);
}

MFC implementuje to w dwóch krokach. Po pierwsze, istnieje warstwa na szczycie interfejsów API Protokołu Tls* Win32 (TlsAlloc, TlsSetValue, TlsGetValue itp.), które używają tylko dwóch indeksów TLS na proces, niezależnie od liczby bibliotek DLL, które masz. Po drugie, szablon jest udostępniany w CProcessLocal celu uzyskania dostępu do tych danych. Zastępuje operator ,> który umożliwia intuicyjną składnię, którą widzisz powyżej. Wszystkie obiekty, które są opakowane przez CProcessLocal element , muszą pochodzić z CNoTrackObjectelementu . CNoTrackObjectzapewnia alokator niższego poziomu (LocalAlloc/LocalFree) i destruktor wirtualny, tak aby MFC mógł automatycznie zniszczyć proces obiektów lokalnych po zakończeniu procesu. Takie obiekty mogą mieć niestandardowy destruktor, jeśli jest wymagane dodatkowe czyszczenie. Powyższy przykład nie wymaga jednego, ponieważ kompilator wygeneruje domyślny destruktor do zniszczenia osadzonego CString obiektu.

Istnieją inne interesujące zalety tego podejścia. Nie tylko wszystkie CProcessLocal obiekty są niszczone automatycznie, nie są konstruowane, dopóki nie będą potrzebne. CProcessLocal::operator-> utworzy wystąpienie skojarzonego obiektu przy pierwszym wywołaniu i nie wcześniej. W powyższym przykładzie oznacza to, że ciąg "strGlobal" nie zostanie skonstruowany do momentu SetGlobalString pierwszego wywołania lub GetGlobalString wywołania. W niektórych przypadkach może to pomóc zmniejszyć czas uruchamiania biblioteki DLL.

Dane lokalne wątku

Podobnie jak w przypadku przetwarzania danych lokalnych, dane lokalne wątku są używane, gdy dane muszą być lokalne dla danego wątku. Oznacza to, że potrzebujesz oddzielnego wystąpienia danych dla każdego wątku, który uzyskuje dostęp do tych danych. Może to być wiele razy używane zamiast rozbudowanych mechanizmów synchronizacji. Jeśli dane nie muszą być współużytkowane przez wiele wątków, takie mechanizmy mogą być kosztowne i niepotrzebne. Załóżmy, że mamy CString obiekt (podobnie jak w powyższym przykładzie). Możemy sprawić, że wątek jest lokalny, opakowując go za pomocą CThreadLocal szablonu:

struct CMyThreadData : public CNoTrackObject
{
    CString strThread;
};
CThreadLocal<CMyThreadData> threadData;

void MakeRandomString()
{
    // a kind of card shuffle (not a great one)
    CString& str = threadData->strThread;
    str.Empty();
    while (str.GetLength() != 52)
    {
        unsigned int randomNumber;
        errno_t randErr;
        randErr = rand_s(&randomNumber);

        if (randErr == 0)
        {
            TCHAR ch = randomNumber % 52 + 1;
            if (str.Find(ch) <0)
            str += ch; // not found, add it
        }
    }
}

Jeśli MakeRandomString został wywołany z dwóch różnych wątków, każdy będzie "przetasować" ciąg na różne sposoby bez zakłócania drugiej. Dzieje się tak dlatego, że istnieje strThread wystąpienie na wątek, a nie tylko jedno wystąpienie globalne.

Zwróć uwagę, jak odwołanie jest używane do przechwytywania CString adresu raz, a nie raz na iterację pętli. Kod pętli mógł zostać napisany wszędzie threadData->strThread "str", ale kod będzie znacznie wolniejszy w wykonaniu. Najlepiej buforować odwołanie do danych, gdy takie odwołania występują w pętlach.

Szablon CThreadLocal klasy używa tych samych mechanizmów, które CProcessLocal korzystają z tych samych technik implementacji.

Zobacz też

Uwagi techniczne według numerów
Uwagi techniczne według kategorii