TN058: Implementace stavu modulu MFC
Poznámka
Následující technická poznámka se od prvního zahrnutí do online dokumentace neaktualizovala. V důsledku toho můžou být některé postupy a témata zastaralé nebo nesprávné. Nejnovější informace doporučujeme vyhledat v online indexu dokumentace, které vás zajímá.
Tato technická poznámka popisuje implementaci konstruktorů stavu modulu MFC. Znalost implementace stavu modulu je důležitá pro použití sdílených knihoven DLL knihovny MFC z knihovny DLL (nebo procesového serveru OLE).
Než si přečtete tuto poznámku, přečtěte si článek "Správa dat o stavu modulů MFC" při vytváření nových dokumentů, oken a zobrazení. Tento článek obsahuje důležité informace o využití a přehled informací o tomto tématu.
Přehled
Existují tři druhy informací o stavu MFC: Stav modulu, Stav procesu a Stav vlákna. Někdy lze tyto typy stavů zkombinovat. Mapy popisovačů MFC jsou například místní moduly i místní podprocesy. To umožňuje dvěma různým modulům mít v každém z jejich vláken různé mapy.
Stav procesu a stav vlákna jsou podobné. Tyto datové položky jsou věci, které tradičně byly globální proměnné, ale musí být specifické pro daný proces nebo vlákno pro správnou podporu Win32s nebo pro správnou podporu vícevláknového formátování. Do které kategorie se daná datová položka hodí, závisí na této položce a na požadované sémantice s ohledem na hranice procesů a vláken.
Stav modulu je jedinečný v tom, že může obsahovat buď skutečně globální stav, nebo stav, který je proces místní nebo vlákno místní. Kromě toho je možné ho rychle přepnout.
Přepínání stavu modulu
Každé vlákno obsahuje ukazatel na "aktuální" nebo "aktivní" stav modulu (není překvapením, že ukazatel je součástí místního stavu vlákna mfc). Tento ukazatel se změní, když vlákno provádění předává hranici modulu, jako je například aplikace volající do ovládacího prvku OLE nebo knihovny DLL, nebo ovládací prvek OLE volající zpět do aplikace.
Aktuální stav modulu se přepne voláním AfxSetModuleState
. Ve většině případů se nebudete zabývat přímo s rozhraním API. MFC v mnoha případech za vás bude volat (ve WinMain, OLE vstupní body atd AfxWndProc
.). To se provádí v jakékoli komponentě, kterou napíšete statickým propojením ve speciálním WndProc
modulu a speciálním WinMain
(nebo DllMain
), který ví, který stav modulu by měl být aktuální. Tento kód můžete zobrazit tak, že se podíváte na knihovnu DLLMODUL. CPP nebo APPMODUL. CPP v adresáři MFC\SRC.
Je vzácné, že chcete nastavit stav modulu a pak ho nenastavit zpět. Ve většině případů chcete "nasdílit" svůj vlastní stav modulu jako aktuální a potom po dokončení znovu otevřít původní kontext. To provádí makro AFX_MANAGE_STATE a speciální třídu AFX_MAINTAIN_STATE
.
CCmdTarget
má speciální funkce pro podporu přepínání stavu modulu. Konkrétně CCmdTarget
je kořenová třída používaná pro automatizaci OLE a vstupní body OLE COM. Stejně jako jakýkoli jiný vstupní bod vystavený systému musí tyto vstupní body nastavit správný stav modulu. Jak daná CCmdTarget
znalost, jaký je "správný" stav modulu by měl být Odpověď je, že si pamatuje, jaký je stav "aktuálního" modulu při jeho vytvoření, aby mohl nastavit aktuální stav modulu na tuto "zapamatovanou" hodnotu při pozdějším zavolání. Výsledkem je stav modulu, ke kterému je daný CCmdTarget
objekt přidružený, stav modulu, který byl aktuální při vytváření objektu. Podívejte se na jednoduchý příklad načtení serveru INPROC, vytvoření objektu a volání jeho metod.
Knihovna DLL je načtena ole pomocí
LoadLibrary
.RawDllMain
je volána jako první. Nastaví stav modulu na známý stav statického modulu pro knihovnu DLL. Z tohoto důvoduRawDllMain
je staticky propojena s knihovnou DLL.Volá se konstruktor objektu pro vytváření tříd přidružených k objektu.
COleObjectFactory
je odvozena zCCmdTarget
a v důsledku toho si pamatuje, ve kterém stavu modulu byla vytvořena instance. To je důležité – když je objekt pro vytváření tříd požádán o vytvoření objektů, teď ví, jaký stav modulu má být aktuální.DllGetClassObject
je volána k získání objektu pro vytváření tříd. MFC prohledá seznam objektů pro vytváření tříd přidružený k tomuto modulu a vrátí ho.Volá se
COleObjectFactory::XClassFactory2::CreateInstance
. Před vytvořením objektu a jeho vrácením nastaví tato funkce stav modulu na stav modulu, který byl aktuální v kroku 3 (ten, který byl aktuální přiCOleObjectFactory
vytvoření instance). To se provádí uvnitř METHOD_PROLOGUE.Když je objekt vytvořen, je
CCmdTarget
to také derivát a stejným způsobemCOleObjectFactory
, jak si vzpomenout, který stav modulu byl aktivní, takže tento nový objekt. Teď objekt ví, na jaký stav modulu se má přepnout, kdykoli je volána.Klient volá funkci objektu OLE COM, kterou přijala z jeho
CoCreateInstance
volání. Když se objekt nazývá, používáMETHOD_PROLOGUE
se k přepnutí stavu modulu stejně jakoCOleObjectFactory
vy.
Jak vidíte, stav modulu se při vytváření rozšíří z objektu na objekt. Je důležité, aby se správně nastavil stav modulu. Pokud není nastavena, může objekt DLL nebo objekt COM špatně pracovat s aplikací MFC, která ji volá, nebo nemusí být schopen najít své vlastní prostředky nebo může selhat jinými mizernými způsoby.
Všimněte si, že některé druhy knihoven DLL, konkrétně knihovny DLL "rozšíření MFC", nepřepínají stav modulu v jejich RawDllMain
(ve skutečnosti, obvykle nemají ani RawDllMain
). Je to proto, že se mají chovat "jako kdyby" byly ve skutečnosti přítomny v aplikaci, která je používá. Jsou velmi součástí aplikace, která je spuštěná, a je jejich záměrem změnit globální stav této aplikace.
Ovládací prvky OLE a další knihovny DLL jsou velmi odlišné. Nechtějí měnit stav volající aplikace; aplikace, která je volá, nemusí být ani aplikace MFC, a proto nemusí existovat žádný stav, který by bylo možné upravit. To je důvod, proč bylo vynalezeno přepínání stavu modulu.
Pro exportované funkce z knihovny DLL, například funkce, která spouští dialogové okno v knihovně DLL, musíte na začátek funkce přidat následující kód:
AFX_MANAGE_STATE(AfxGetStaticModuleState())
Tím se aktuální stav modulu prohodí se stavem vráceným z AfxGetStaticModuleState do konce aktuálního oboru.
Pokud se makro AFX_MODULE_STATE nepoužívá, dojde k problémům s prostředky v knihovnách DLL. Mfc ve výchozím nastavení používá k načtení šablony prostředku popisovač hlavní aplikace. Tato šablona je ve skutečnosti uložena v knihovně DLL. Hlavní příčinou je, že AFX_MODULE_STATE makro nepřepnuly informace o stavu modulu MFC. Popisovač prostředku se obnoví ze stavu modulu MFC. Přepnutí stavu modulu způsobí použití nesprávného popisovače prostředků.
AFX_MODULE_STATE nemusí být vložena do každé funkce v knihovně DLL. Lze například volat kódem MFC v aplikaci bez AFX_MODULE_STATE, InitInstance
protože MFC automaticky posune stav modulu před InitInstance
a potom ho přepne zpět po InitInstance
vrácení. Totéž platí pro všechny obslužné rutiny mapování zpráv. Běžné knihovny MFC DLL mají ve skutečnosti speciální hlavní okno procedura, která automaticky přepne stav modulu před směrováním jakékoli zprávy.
Zpracování místních dat
Zpracování místních dat by nebylo tak velké obavy, pokud to nebylo pro potíže modelu Win32s DLL. Ve Win32s všechny knihovny DLL sdílejí své globální data, i když je načítá více aplikací. To se velmi liší od skutečného datového modelu Win32 DLL, kde každá knihovna DLL získá samostatnou kopii svého datového prostoru v každém procesu, který se připojí k knihovně DLL. Pro zvýšení složitosti jsou data přidělená haldě v knihovně DLL win32s ve skutečnosti specifická (alespoň pokud jde o vlastnictví). Zvažte následující data a kód:
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);
}
Zvažte, co se stane, když je výše uvedený kód umístěn v knihovně DLL a tato knihovna DLL je načtena dvěma procesy A a B (ve skutečnosti může být dvěma instancemi stejné aplikace). Volání SetGlobalString("Hello from A")
. V důsledku toho je paměť přidělena pro CString
data v kontextu procesu A. Mějte na paměti, že CString
samotný je globální a je viditelný pro A i B. Teď B volá GetGlobalString(sz, sizeof(sz))
. B bude moct zobrazit data, která sada obsahuje. Důvodem je to, že Win32s nenabízí žádnou ochranu mezi procesy, jako je Win32. To je první problém; v mnoha případech není žádoucí mít jednu aplikaci vliv na globální data, která jsou považována za vlastněná jinou aplikací.
Existují i další problémy. Řekněme, že A se teď ukončí. Když A skončí, uvolní se paměť používaná řetězcem 'strGlobal
' pro systém – to znamená, že operační systém automaticky uvolní veškerou paměť přidělenou procesem A. Není uvolněn, protože CString
je volán destruktor; ještě nebyl volán. Uvolní se jednoduše, protože aplikace, která ji přidělila, opustila scénu. Pokud se teď volá GetGlobalString(sz, sizeof(sz))
B, nemusí získat platná data. Některé jiné aplikace můžou použít paměť pro něco jiného.
Jasně existuje problém. MFC 3.x používal techniku označovanou jako místní úložiště (TLS). MFC 3.x by přidělil index TLS, který v rámci Win32s skutečně funguje jako index místního úložiště procesu, i když není volána, a pak by odkazoval na všechna data založená na daném indexu TLS. Podobá se tomu index TLS, který se použil k ukládání místních dat vlákna ve Win32 (další informace o tomto tématu najdete níže). To způsobilo, že každá knihovna MFC DLL využívá alespoň dva indexy TLS na proces. Při načítání mnoha knihoven DLL ovládacích prvků OLE (OCX) rychle dochází k indexům TLS (k dispozici je pouze 64). Prostředí MFC navíc muselo umístit všechna tato data na jedno místo v jedné struktuře. Nebyl příliš rozšiřitelný a nebyl ideální pro použití indexů TLS.
MFC 4.x to řeší pomocí sady šablon tříd, které můžete obtékat kolem dat, která by měla být místní. Například výše uvedený problém by mohl být opraven zápisem:
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 to implementuje ve dvou krocích. Nejprve existuje vrstva nad rozhraními API win32 Tls* (TlsAlloc, TlsSetValue, TlsGetValue atd.), která na proces používají pouze dva indexy TLS bez ohledu na to, kolik knihoven DLL máte. Za druhé, CProcessLocal
šablona je poskytována pro přístup k tomuto datu. Přepíše operátor–> což je to, co umožňuje intuitivní syntaxi, kterou vidíte výše. Všechny objekty, které jsou zabaleny CProcessLocal
podle musí být odvozeny z CNoTrackObject
. CNoTrackObject
poskytuje alokátor nižší úrovně (LocalAlloc/LocalFree) a virtuální destruktor tak, aby MFC mohl automaticky zničit proces místní objekty při ukončení procesu. Tyto objekty mohou mít vlastní destruktor, pokud je vyžadováno další vyčištění. Výše uvedený příklad nevyžaduje jeden, protože kompilátor vygeneruje výchozí destruktor pro zničení vloženého CString
objektu.
Tento přístup má další zajímavé výhody. Nejen že všechny objekty jsou CProcessLocal
zničeny automaticky, nejsou sestaveny, dokud nebudou potřeba. CProcessLocal::operator->
vytvoří instanci přidruženého objektu při prvním zavolání a ne dříve. V předchozím příkladu to znamená, že řetězec 'strGlobal
' nebude vytvořen, dokud se poprvé SetGlobalString
nebo GetGlobalString
nebude volán. V některých případech to může pomoct zkrátit dobu spuštění knihovny DLL.
Místní data vlákna
Podobně jako při zpracování místních dat se místní data vlákna používají v případě, že data musí být místní pro dané vlákno. To znamená, že potřebujete samostatnou instanci dat pro každé vlákno, které přistupuje k datům. To může být často používáno v případě rozsáhlých synchronizačních mechanismů. Pokud data nemusí být sdílena více vlákny, mohou být tyto mechanismy nákladné a zbytečné. Předpokládejme, že jsme měli CString
objekt (podobně jako vzorek výše). Můžeme ho nastavit jako místní tak, že ho zabalíme pomocí CThreadLocal
šablony:
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
}
}
}
Pokud MakeRandomString
by byl volán ze dvou různých vláken, každý by řetězec "prohazoval" různými způsoby, aniž by zasahoval do druhého. Důvodem je skutečnost, že existuje instance strThread
na vlákno místo pouze jedné globální instance.
Všimněte si, jak se odkaz používá k zachycení CString
adresy jednou místo jednou pro iteraci smyčky. Kód smyčky by mohl být napsán threadData->strThread
všude, kde se používá 'str
', ale kód by byl mnohem pomalejší při provádění. Nejlepší je ukládat odkazy na data, pokud k těmto odkazům dochází ve smyčce.
Šablona CThreadLocal
třídy používá stejné mechanismy, které CProcessLocal
dělá a stejné techniky implementace.
Viz také
Technické poznámky podle čísel
Technické poznámky podle kategorií