最近的項目中,需要在DLL裏使用MFC生成界面,這才發現一旦資源放在不同的動態庫裏,而且還和多線程攪和在一起的時候,事情就變得異常的複雜,以前對MFC的一知半解已經不足與應付了。程序莫名的崩潰,莫名的ASSERT,資源怎樣也裝載不起來,爲什麼呢?每次,總是嘗試着,在每一個線程的開始,把AFX_MANAGE_STATE(AfxGetStaticModuleState())添加上去,或者在某些地方用AfxSetResourceHandler()一把,然後問題就解決了,但是不是很明白到底是怎麼回事,總感覺這種解決辦法讓人很不安心,彷彿在下一秒問題又會突然冒出來。
前天,這個問題終於發揮到了極致,任我花費了好幾個小時,怎樣的嘗試都不能成功,在項目的關鍵時候發生這種事情,讓我暗暗發誓以後再也不用MFC了。正像很多的電影情節一樣,事情最後還是得到了解決,這次我決定不能再這麼算了,一定要把這個事情理解得明明白白。
在這裏,我遇到的問題就是,如何讓DLL裏的界面代碼使用該DLL的資源(Resource),如何在工作線程里加載有IE控件的對話框?
我問同事,他們是如何實現DLL資源切換的?AFX_MANAGE_STATE(AfxGetStaticModuleState())這就是他們的答案,一如微軟的推薦,原來就是這麼簡單啊!讓我們來看看,這句代碼到底做了什麼?
#define AFX_MANAGE_STATE(p) AFX_MAINTAIN_STATE2 _ctlState(p);
AFX_MAINTAIN_STATE2::AFX_MAINTAIN_STATE2(AFX_MODULE_STATE* pNewState)
{
m_pThreadState = _afxThreadState;
m_pPrevModuleState = m_pThreadState->m_pModuleState;
m_pThreadState->m_pModuleState = pNewState;
}
_AFXWIN_INLINE AFX_MAINTAIN_STATE2::~AFX_MAINTAIN_STATE2()
{ m_pThreadState->m_pModuleState = m_pPrevModuleState; }
原來,就是定義一個局部的對象,利用其構造和析構函數在函數的入口和函數的出口進行State狀態的切換,我猜AfxGetStaticModuleState()一定是獲取當前代碼所在DLL的State。
果然,請看
static _AFX_DLL_MODULE_STATE afxModuleState;
AFX_MODULE_STATE* AFXAPI AfxGetStaticModuleState()
{
AFX_MODULE_STATE* pModuleState = &afxModuleState;
return pModuleState;
}
class _AFX_DLL_MODULE_STATE : public AFX_MODULE_STATE
// AFX_MODULE_STATE (global data for a module)
class AFX_MODULE_STATE : public CNoTrackObject
{
...
CWinApp* m_pCurrentWinApp;
HINSTANCE m_hCurrentInstanceHandle;
HINSTANCE m_hCurrentResourceHandle;
LPCTSTR m_lpszCurrentAppName;
BYTE m_bDLL; // TRUE if module is a DLL, FALSE if it is an EXE
...
COccManager* m_pOccManager;
...
這裏不得不說,MFC把很多的數據都堆放在這裏,搞得很複雜,結構性非常的差。
}
afxModuleState是dll的靜態成員,自然可以被同樣的dll裏的代碼所訪問,但是何時初始化的?
extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{
...
AfxWinInit(hInstance, NULL, _T(""), 0);
...
}
BOOL AFXAPI AfxWinInit(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPTSTR lpCmdLine, int nCmdShow)
{
ASSERT(hPrevInstance == NULL);
// handle critical errors and avoid Windows message boxes
SetErrorMode(SetErrorMode(0) |
SEM_FAILCRITICALERRORS|SEM_NOOPENFILEERRORBOX);
// set resource handles
AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
pModuleState->m_hCurrentInstanceHandle = hInstance;
pModuleState->m_hCurrentResourceHandle = hInstance;
...
}
原來在DLL的入口函數,用該DLL的hInstance初始化了該結構。
到這時候,我們還是不明白,爲什麼要進行資源切換?前面開始的_afxThreadState到底是什麼?好像跟Thread有關係,到底是什麼呢?
THREAD_LOCAL(_AFX_THREAD_STATE, _afxThreadState)
#define THREAD_LOCAL(class_name, ident_name) /
AFX_DATADEF CThreadLocal<class_name> ident_name;
template<class TYPE>
class CThreadLocal : public CThreadLocalObject
再往下跟蹤,發現其實代碼越發生澀難懂,但是基本的功能就是訪問當前此行代碼的線程的私有數據。所謂線程的私有數據,就是說,不同的線程執行同樣的一段代碼,得到的數據可能是不同的。這纔想起來,MFC的很多句柄啦,都是保存在全局的Map裏的,而且放在線程的私有數據區裏,所以跨線程傳遞MFC對象是很不安全的。但是,MFC爲什麼要這麼做呢?這個問題,到目前爲止,我還是搞不明白。
還是回到開始的代碼,資源切換到底是如何進行的?
int CDialog::DoModal()
{
...
HINSTANCE hInst = AfxGetResourceHandle();
if (m_lpszTemplateName != NULL)
{
hInst = AfxFindResourceHandle(m_lpszTemplateName, RT_DIALOG);
HRSRC hResource = ::FindResource(hInst, m_lpszTemplateName, RT_DIALOG);
hDialogTemplate = LoadResource(hInst, hResource);
...
}
_AFXWIN_INLINE HINSTANCE AFXAPI AfxGetResourceHandle()
{ ASSERT(afxCurrentResourceHandle != NULL);
return afxCurrentResourceHandle; }
#define afxCurrentResourceHandle AfxGetModuleState()->m_hCurrentResourceHandle
AFX_MODULE_STATE* AFXAPI AfxGetModuleState()
{
_AFX_THREAD_STATE* pState = _afxThreadState;
AFX_MODULE_STATE* pResult;
if (pState->m_pModuleState != NULL)
{
// thread state's module state serves as override
pResult = pState->m_pModuleState;
}
else
{
// otherwise, use global app state
pResult = _afxBaseModuleState.GetData();
}
ASSERT(pResult != NULL);
return pResult;
}
原來MFC的對話框裝載資源是通過獲取當前線程對應的ModuleState保存的ResourceHandler來裝載資源的。所以,DLL裏的代碼,需要在函數的入口,首先把當前執行線程的ModuleState換成該Dll的State,這樣才能裝載該dll的資源!這時候,我突然明白過來,爲什麼需要要依賴線程的私有數據來保存ModuleState,其實確切的說是傳遞!--這其實是因爲CDialog是存放在另一個DLL裏的,比如MFC40.dll,如果以共享模式連接MFC庫的話。而用戶自己編寫的CDialog的子類並不放在CDialog同樣的Dll裏,他們如何來傳遞這個資源句柄呢?兩種解決辦法:1,利用參數傳遞。2,存放在一個公共的地方。前者需要增加參數,顯得很麻煩,Win32的API好像就是這樣實現的吧?後者,需要確定這個公共地方在何處?這讓人想起來,建立一個公共的動態庫?由主程序的提供?再多說一句,J2EE裏有一個容器的概念(COM+好像也有,不知道.NET是如何的),組件都是生存在容器裏,這時候我們就可以設想把該數據存放在容器裏。不管怎樣,MFC的實現就是放在線程的私有數據區,不需要公共的動態庫,也不需要麻煩主程序,它自己就搞定了!它自以爲很好的解決方式,很完美,卻引發了我們的一系列的問題,特別是不明白就裏的人。
關於資源裝載,問題似乎已經解決了,但是還有一點點小麻煩就是,我實現的dll不是以普通的輸出函數進行輸出的,而是輸出類,我可不想在每一個類的成員函數裏添加AFX_MANAGE_STATE(AfxGetStaticModuleState())。怎麼辦呢?既然已經知道了資源切換的原理,我們添加兩個輸出函數,分別對應AFX_MAINTAIN_STATE2的構造和析構函數,在類的使用前後調用,就可以了。或者,分別放在類的構造和析構函數裏。又或者,就聲明爲成員變量。無論怎樣,需要保證的一點就是資源的切換要正確嵌套,不可交叉--這種情況在不同的DLL之間交叉調用的時候會發生。
好了,現在DLL裏的資源可以正確調用了,但是在當Dialog上包含有IE控件的時候,我們還是失敗了,爲什麼呢?我知道對於ActiveX控件,Dialog需要做一些特殊的處理,AfxEnableControlContainer(),我也知道,要使用COM,需要CoInitialize(),但是我一直沒有想過需要兩個一起用才能把IE弄出來,但是最後就是這樣的。奇怪的是,如果不是在工作線程裏,根本不需要CoInitialize(),就能裝載IE控件的,這個暫時就先不管了。
PROCESS_LOCAL(COccManager, _afxOccManager)
void AFX_CDECL AfxEnableControlContainer(COccManager* pOccManager)
{
if (pOccManager == NULL)
afxOccManager = _afxOccManager.GetData();
else
afxOccManager = pOccManager;
}
#define afxOccManager AfxGetModuleState()->m_pOccManager
這樣看來,這個_afxOccManager應該是屬於整個進程的,整個進程只有一個,就在那個定義它的dll裏。但是,你需要把該對象(或者創建一個自定義的)傳給ModuleState(請注意前面的AFX_MODULE_STATE裏就包含了該屬性),也就是要AfxEnableControlContainer()一下,這樣特定的ModuleState就有了OccManager的信息!但是,請注意,一定要在目標dll裏,正確切換了資源之後,才能進行,如下:
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CoInitialize(NULL);
AfxEnableControlContainer();
至此,這個困擾我很久的問題,終於脈絡清晰起來了。