AFX_MANAGE_STATE(AfxGetStaticModuleState())

kendyhj9999發表於2017-11-03

AFX_MANAGE_STATE(AfxGetStaticModuleState())
2013年12月12日 ⁄ 綜合 ⁄ 共 5426字 ⁄ 字號 小 中 大 ⁄ 評論關閉


         寫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 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();

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