TN058:MFC 模組狀態實作
注意
下列技術提示自其納入線上文件以來,未曾更新。 因此,有些程序和主題可能已過期或不正確。 如需最新資訊,建議您在線上文件索引中搜尋相關的主題。
此技術附注描述 MFC「模組狀態」建構的實作。 瞭解模組狀態實作對於從 DLL 使用 MFC 共用 DLL(或 OLE 同進程伺服器)而言非常重要。
閱讀此附注之前,請參閱建立新檔、Windows 和檢視 中的 <管理 MFC 模組的狀態資料>。 本文包含此主題的重要使用資訊和概觀資訊。
概觀
MFC 狀態資訊有三種:模組狀態、進程狀態和執行緒狀態。 有時候可以合併這些狀態類型。 例如,MFC 的控制碼對應同時是模組本機和執行緒本機。 這可讓兩個不同的模組在每個執行緒中有不同的對應。
進程狀態和執行緒狀態很類似。 這些資料項目是傳統上是全域變數的專案,但必須針對適當的 Win32s 支援或適當的多執行緒支援指定進程或執行緒。 指定資料項目適用的類別取決於該專案及其所需的語意,以處理和執行緒界限。
模組狀態是唯一的,因為它可以包含真正的全域狀態或狀態,也就是處理本機或執行緒本機的狀態。 此外,也可以快速切換。
模組狀態切換
每個執行緒都包含「目前」或「使用中」模組狀態的指標(毫不奇怪,指標是 MFC 執行緒本機狀態的一部分)。 當執行執行緒通過模組界限時,會變更此指標,例如呼叫 OLE 控制項或 DLL 的應用程式,或呼叫回應用程式的 OLE 控制項。
目前的模組狀態會藉由呼叫 AfxSetModuleState
來切換。 在大多數情況下,您永遠不會直接處理 API。 在許多情況下,MFC 會為您呼叫它(在 WinMain、OLE 進入點 AfxWndProc
等)。 這可在您撰寫的任何元件中完成,方法是以靜態方式連結在特殊 WndProc
中,以及知道哪些模組狀態應該是目前的特殊 WinMain
(或 DllMain
) 。 您可以查看 DLLMODUL 來查看此程式碼。CPP 或 APPMODUL。MFC\SRC 目錄中的 CPP。
您很少想設定模組狀態,然後不要將它設回。 大部分時候,您想要「推送」自己的模組狀態作為目前的模組狀態,然後在完成之後,將原始內容「彈出」回來。 這是由宏 AFX_MANAGE_STATE 和特殊類別 AFX_MAINTAIN_STATE
來完成。
CCmdTarget
具有支援模組狀態切換的特殊功能。 特別是 , CCmdTarget
是用於 OLE 自動化和 OLE COM 進入點的根類別。 如同向系統公開的任何其他進入點,這些進入點必須設定正確的模組狀態。 指定 CCmdTarget
如何知道「正確」模組狀態應該是「答案」是,它會在建構時「記住」「目前」模組狀態是什麼,如此一來,它就可以在稍後呼叫時,將目前的模組狀態設定為該「記住」值。 因此,與指定 CCmdTarget
物件相關聯的模組狀態是建構物件時目前狀態的模組狀態。 以載入 INPROC 伺服器、建立物件及呼叫其方法的簡單範例為例。
DLL 是使用
LoadLibrary
來載入 OLE。RawDllMain
會先呼叫 。 它會將模組狀態設定為 DLL 的已知靜態模組狀態。 因此,RawDllMain
靜態連結至 DLL。呼叫與物件相關聯的類別處理站建構函式。
COleObjectFactory
衍生自CCmdTarget
,因此會記住其具現化模組狀態。 這很重要— 當要求類別處理站建立物件時,它現在知道要讓目前的模組狀態。DllGetClassObject
呼叫 以取得類別處理站。 MFC 會搜尋與此模組相關聯的類別處理站清單,並傳回它。呼叫
COleObjectFactory::XClassFactory2::CreateInstance
。 建立物件並傳回物件之前,此函式會將模組狀態設定為步驟 3 中目前的模組狀態(具現化 時COleObjectFactory
目前的模組狀態)。 這是在METHOD_PROLOGUE 內 完成的。建立物件時,它也是
CCmdTarget
衍生專案,而且以相同方式COleObjectFactory
記住哪些模組狀態為使用中,所以這個新物件也是如此。 現在物件知道每當呼叫模組時要切換至哪個模組狀態。用戶端會在其
CoCreateInstance
呼叫所收到的 OLE COM 物件上呼叫函式。 呼叫 物件時,它會使用METHOD_PROLOGUE
來切換模組狀態,就像COleObjectFactory
這樣。
如您所見,模組狀態會在建立物件時從 物件傳播到 物件。 請務必適當地設定模組狀態。 如果未設定,您的 DLL 或 COM 物件可能會與呼叫它的 MFC 應用程式互動不佳,或可能找不到自己的資源,或可能在其他錯誤的方式失敗。
請注意,某些類型的 DLL,特別是「MFC 擴充功能」DLL 不會在其中切換模組狀態 RawDllMain
(實際上,它們通常沒有 )。 RawDllMain
這是因為它們的目的是「好像」它們實際上存在於使用它們的應用程式中。 它們非常屬於正在執行的應用程式,而且其打算修改該應用程式的全域狀態。
OLE 控制項和其他 DLL 非常不同。 他們不想修改呼叫端應用程式的狀態;呼叫它們的應用程式甚至可能不是 MFC 應用程式,因此可能沒有任何狀態可修改。 這是模組狀態切換發明的原因。
針對從 DLL 匯出的函式,例如在 DLL 中啟動對話方塊的函式,您需要將下列程式碼新增至函式的開頭:
AFX_MANAGE_STATE(AfxGetStaticModuleState())
這會將目前的模組狀態與從 AfxGetStaticModuleState 傳回的狀態交換至目前範圍的結尾。
如果未使用AFX_MODULE_STATE宏,DLL 中的資源就會發生問題。 根據預設,MFC 會使用主應用程式的資源控制代碼來載入資源範本。 這個範本實際上會儲存在 DLL 中。 根本原因是 MFC 的模組狀態資訊尚未由AFX_MODULE_STATE宏切換。 資源控制代碼是從 MFC 的模組狀態復原。 由於未切換模組狀態而導致使用錯誤的資源控制代碼。
AFX_MODULE_STATE不需要放入 DLL 中的每個函式中。 例如, InitInstance
您可以在應用程式中由 MFC 程式碼呼叫,而不需AFX_MODULE_STATE,因為 MFC 會在之前 InitInstance
自動將模組狀態移轉,然後在傳回之後 InitInstance
切換回。 所有訊息對應處理常式也是如此。 一般 MFC DLL 實際上有特殊的主視窗程式,可自動切換模組狀態,再路由傳送任何訊息。
處理本機資料
處理本機資料不會如此令人擔心,因為它不是 Win32s DLL 模型的困難。 在 Win32 中,所有 DLL 都會共用其全域資料,即使由多個應用程式載入也是如此。 這與「實際」Win32 DLL 資料模型非常不同,其中每個 DLL 會在附加至 DLL 的每個進程中取得其資料空間的個別複本。 為了增加複雜性,在 Win32s DLL 中堆積上配置的資料實際上是特定的程式(至少就擁有權而言)。 請考慮下列資料和程式碼:
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);
}
如果上述程式碼位於 DLL 中,而且 DLL 是由兩個進程 A 和 B 載入,會發生什麼情況(事實上,可能是相同應用程式的兩個實例)。 呼叫 SetGlobalString("Hello from A")
。 因此,記憶體會配置給 CString
進程 A 內容中的資料。請記住, CString
本身是全域的,而且對 A 和 B 都是可見的。現在 B 會呼叫 GetGlobalString(sz, sizeof(sz))
。 B 將可以看到 A 集的資料。 這是因為 Win32s 在 Win32 之類的程式之間不提供任何保護。 這是第一個問題:在許多情況下,不想要讓一個應用程式影響被視為由不同應用程式所擁有的全域資料。
還有其他問題。 假設 A 現在結束。 當 A 結束時,' strGlobal
' 字串所使用的記憶體可供系統使用,也就是說,由進程 A 配置的所有記憶體都會由作業系統自動釋放。 它不會釋放, CString
因為正在呼叫解構函式;它尚未呼叫。 它只是因為配置它的應用程式已離開場景而釋出。 現在,如果呼叫 GetGlobalString(sz, sizeof(sz))
B,它可能無法取得有效的資料。 其他一些應用程式可能已使用該記憶體做為其他專案。
顯然存在問題。 MFC 3.x 使用了稱為執行緒本機儲存體 (TLS) 的技術。 MFC 3.x 會配置 TLS 索引,在 Win32s 下,它實際上會作為進程本機儲存體索引,即使它未呼叫,然後會根據該 TLS 索引參考所有資料。 這類似于用來在 Win32 上儲存執行緒本機資料的 TLS 索引(如需該主題的詳細資訊,請參閱下文)。 這會導致每個 MFC DLL 在每個進程中至少使用兩個 TLS 索引。 當您考慮載入許多 OLE 控制 DLL (OCX)時,您很快就用完 TLS 索引(只有 64 個可用)。 此外,MFC 必須將所有這些資料放在單一結構中一個位置。 它不是非常可延伸的,對於其使用 TLS 索引並不理想。
MFC 4.x 會使用一組類別範本來解決這個問題,您可以「包裝」應該處理本機的資料。 例如,上述問題可藉由撰寫來修正:
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 會在兩個步驟中實作此動作。 首先,在 Win32 Tls* API 的頂端有一層( TlsAlloc 、TlsSetValue、 TlsGetValue 等),不論您有多少 DLL,每個進程只能使用兩個 TLS 索引。 其次, CProcessLocal
會提供範本來存取此資料。 它會覆寫運算子, > 這是可讓您看到上述直覺式語法的內容。 所包裝 CProcessLocal
的所有物件都必須衍生自 CNoTrackObject
。 CNoTrackObject
提供較低層級的配置器 ( LocalAlloc / LocalFree ) 和虛擬解構函式,讓 MFC 可以在進程終止時自動終結進程本機物件。 如果需要額外的清除,這類物件可以有自訂解構函式。 上述範例不需要一個,因為編譯器會產生預設解構函式來終結內嵌 CString
物件。
此方法還有其他有趣的優點。 這些物件不僅 CProcessLocal
會自動終結,而且在需要它們之前不會建構它們。 CProcessLocal::operator->
會在第一次呼叫關聯物件時具現化關聯物件,而且不會更快。 在上述範例中,這表示在第一次 SetGlobalString
呼叫 或 GetGlobalString
之前,將不會建構 ' strGlobal
' 字串。 在某些情況下,這有助於減少 DLL 啟動時間。
執行緒本機資料
與處理本機資料類似,當資料必須是指定執行緒的本機資料時,就會使用執行緒本機資料。 也就是說,您需要每個存取該資料之執行緒的資料個別實例。 這可以多次用於取代廣泛的同步處理機制。 如果資料不需要由多個執行緒共用,這類機制可能相當昂貴且不必要。 假設我們有 物件 CString
(就像上述範例一樣)。 我們可以將執行緒包裝成 CThreadLocal
範本,使其成為本機執行緒:
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
}
}
}
如果 MakeRandomString
從兩個不同的執行緒呼叫,則每個執行緒都會以不同的方式「隨機」字串,而不會干擾另一個執行緒。 這是因為每個執行緒實際上有一個實例, strThread
而不只是一個全域實例。
請注意參考如何用來擷取位址一次, CString
而不是每個迴圈反復專案一次。 迴圈程式碼可以隨 threadData->strThread
處使用 ' str
' 來撰寫,但執行速度會變慢。 在迴圈中發生這類參考時,最好快取資料的參考。
類別 CThreadLocal
範本會使用相同的機制 CProcessLocal
,以及相同的實作技術。