AFX_MANAGE_STATE(AfxGetStaticModuleState())講解

以前寫MFC的DLL的時候,總會在自動生成的代碼框架裏看到提示,需要在每一個輸出的函數開始添加上AFX_MANAGE_STATE(AfxGetStaticModuleState())。一直不明白這樣做的含義,也一直沒有這樣做,而且代碼也工作得好好的,所以感覺這好像一句廢話。

最近的項目中,需要在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 isan 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 avoidWindows 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_DATADEFCThreadLocal<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 stateserves 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();

至此,這個困擾我很久的問題,終於脈絡清晰起來了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章