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

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





發佈了14 篇原創文章 · 獲贊 4 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章