目 錄
第1章 基本概念
1.1 一個簡單的例子
下面將使用VC++創建一個動態鏈接庫文件。這個文件將導出兩個函數StringReverseA、StringReverseW,前者將一個ANSI字符串逆序,後者將一個Unicode字符串逆序。
1.1.1 新建一個VC++項目
對於VC++6.0而言,項目類型請選擇Win32 Dynamic-Link Library。輸入項目名稱後,單擊"OK"按鈕。
在接下來的界面裏,選擇"An empty DLL project",然後單擊"Finish"按鈕。
在接下來的界面裏單擊"OK"按鈕完成項目創建。
對於VC++9.0(即VC++2008)而言,項目類型請選擇Win32。輸入項目名稱後,單擊"確定"按鈕。
在接下來的界面裏,請選擇"應用程序設置"下的"DLL"和"空項目"。單擊"完成"按鈕完成項目創建。
1.1.2 添加源文件
對於VC++6.0而言,在Workspace 窗口的 FileView 選項卡內,右鍵單擊"Test files",在右鍵菜單裏單擊【Add Files to Project...】菜單項
輸入源文件名後,單擊"OK"按鈕
彈出對話框裏詢問是否在項目裏增加Test.c這個文件的引用。請單擊"是"按鈕。
此時鼠標雙擊Test.c。因爲這個文件還不存在,VC++6.0會提示是否創建,請單擊"是"按鈕。
對於VC++9.0而言,在解決方案資源管理器裏,右鍵單擊"Test",在右鍵菜單裏單擊【添加】【新建項】菜單項。
接下來的界面內,請選擇"C++文件(.cpp)",並輸入源文件名Test.c,然後單擊"添加"按鈕。完成Test.c文件的添加和創建。
1.1.3 輸入源代碼
在Test.c裏輸入如下源代碼:
#include <windows.h>
/***************************************************************************\ 將一個 Unicode 字符串逆序 \***************************************************************************/ __declspec(dllexport) wchar_t* WINAPI StringReverseW(wchar_t*wzStr) { if(wzStr) { int p1 = 0; int p2 = wcslen(wzStr) - 1; wchar_t t;
while(p1 < p2) { t = wzStr[p1]; wzStr[p1++] = wzStr[p2]; wzStr[p2--] = t; } } return wzStr; }
/***************************************************************************\ 將一個 ANSI 字符串逆序 \***************************************************************************/ __declspec(dllexport) char* WINAPI StringReverseA(char*szStr) { if(szStr) { int nLenA = strlen(szStr) + 1; int nLenW = MultiByteToWideChar(CP_ACP,0,szStr,nLenA,NULL,0); wchar_t*pStrW = (wchar_t*)malloc(nLenW * sizeof(wchar_t));
MultiByteToWideChar(CP_ACP,0,szStr,nLenA,pStrW,nLenW); StringReverseW(pStrW); WideCharToMultiByte(CP_ACP,0,pStrW,nLenW,szStr,nLenA,NULL,NULL); free(pStrW); } return szStr; } |
1.1.4 __declspec(dllexport)
__declspec(dllexport)修飾符用來導出函數StringReverseA和StringReverseW。它還可以導出變量和類,這個後面介紹。
1.1.5 WINAPI
WINAPI 其實就是__stdcall。以StringReverseA爲例,調用它時,參數szStr將被壓入棧中,從StringReverseA返回時,參數szStr需要出棧。__stdcall表示由StringReverseA自己執行出棧操作。假如將__stdcall去掉或換爲__cdecl,則由調用StringReverseA的函數負責執行出棧操作。說了這麼多,最重要的是:某些語言,如VB6.0只支持__stdcall,所以爲了讓這個dll被儘可能多的編程語言支持,請使用WINAPI。
1.1.6 導出符號
現在可以編譯程序,生成Test.dll了。使用eXeScope6.30打開Test.dll,可以看到Test.dll確實導出了兩個函數。請注意:每個導出函數都有一個序號,它是一個正整數。
不過有意思的是:導出函數的名稱並不是StringReverseA和StringReverseW,而是_StringReverseA@4和_StringReverseW@4。
如果把Test.c改名爲Test.cpp,則導出的名稱更爲複雜。請參考下圖:
這是什麼原因呢?因爲Test.c的擴展名爲c,VC++使用C編譯器進行編譯。Test.cpp的擴展名爲cpp,VC++使用C++編譯器進行編譯。C++爲了實現函數重載,編譯時會根據參數類型和個數對函數名進行再次命名。
1.1.7 DEF文件
如何防止VC++編譯器生成dll時將導出函數名更改掉?答案就是使用模塊定義文件。請在VC++項目裏增加模塊定義文件Test.def。這個文件名可以是1.def、A.def……只要擴展名是def即可。編輯Test.def,使其內容如下:
EXPORTS StringReverseA StringReverseW |
上述內容表示:導出函數StringReverseA和StringReverseW。此時,這兩個函數前面的__declspec(dllexport)修飾符將不再需要。
DEF文件的功能還有很多,具體請參考MSDN。
1.2 調用動態庫
生成的動態庫文件可以被多種編程語言使用。限於篇幅下面僅介紹VC++如何調用動態庫。
1.2.1 隱式鏈接
編譯動態庫文件時,同時會生成Lib文件。使用動態庫的VC++程序可以鏈接這個Lib文件,這就是隱式鏈接。可參考的代碼如下:
#include <windows.h> #include <stdio.h>
__declspec(dllimport) char* WINAPI StringReverseA(char*szStr); #pragma comment(lib,"D:/VC6/Test/Debug/Test.lib")
void main() { char szStr[] = "隱式鏈接動態庫"; puts(StringReverseA(szStr)); } |
__declspec(dllimport) char* WINAPI StringReverseA(char*szStr); 是函數聲明。修飾符__declspec(dllimport)表示這是一個導入函數。去除這個修飾符不影響程序的編譯、運行,但有了__declspec(dllimport)之後,生成的代碼更小,運行更快。
注意:對於C++程序而言,可能需要這樣聲明函數:
extern "C"
{
__declspec(dllimport) char* WINAPI StringReverseA(char*szStr);
}
extern "C" 的作用是:告訴C++編譯器連接時不要以C++語法修改StringReverseA的名稱。爲什麼說是"可能"需要extern "C"呢?這與dll的編譯有關係。如果使用C編譯器編譯dll,則需要extern "C";如果使用C++編譯器編譯dll,則不需要extern "C"。
#pragma comment(lib,"D:/VC6/Test/Debug/Test.lib")表示鏈接的時候使用D:\VC6\Test\Debug\Test.Lib文件。就是編譯動態庫時產生的那個Lib文件。
採用隱式鏈接,運行程序的時候動態庫文件首先被加載至內存。系統如何定位dll文件呢?其搜索順序爲:exe所在目錄、當前目錄(GetCurrentDirectory)、System32目錄(GetSystemDirectory)、Windows目錄(GetWindowsDirectory)、環境變量PATH指定的目錄。不用記這麼多,最保險的做法就是將dll和exe放在同一文件夾下。如果爲了多個exe程序共享一個dll,請將這個dll文件複製到System32目錄下。
1.2.2 顯式鏈接
顯式鏈接可以靈活控制動態庫文件的加載、卸載。其使用步驟如下:
1、使用LoadLibrary函數載入動態庫文件至內存;
2、使用GetProcAddress函數獲得導出函數的地址;
3、調用導出函數;
4、使用FreeLibrary卸載動態庫文件。
可參考如下代碼:
#include <windows.h> #include <stdio.h>
void main() { HINSTANCE hDll = LoadLibrary("Test.dll"); //載入動態庫文件 if(hDll) {//載入成功 char* (WINAPI*pfn)(char*szStr) = NULL; //聲明一個函數指針 //獲得函數StringReverseA的指針 #ifdef __cplusplus (FARPROC&)pfn = GetProcAddress(hDll,"StringReverseA"); #else (FARPROC)pfn = GetProcAddress(hDll,"StringReverseA"); #endif if(pfn) {//成功獲得函數指針 char szStr[] = "顯式鏈接動態庫"; pfn(szStr); //調用函數,等價於(*pfn)(szStr) puts(szStr); } FreeLibrary(hDll); //卸載動態庫文件 } } |
需要說明的是
1、LoadLibrary("Test.dll")在載入Test.dll時是有搜索順序的:首先在exe所在目錄查找,然後在當前目錄(GetCurrentDirectory)下查找,然後在System32目錄下查找……具體請參考MSDN幫助;
2、注意獲得函數指針的C代碼和C++代碼是不同的,它們通過#ifdef __cplusplus這個條件編譯語句來區分。或者使用C/C++通用的代碼:
typedef char* (WINAPI*STRINGREVERSEA)(char*szStr);
STRINGREVERSEA pfn = (STRINGREVERSEA)GetProcAddress(hDll,"StringReverseA");
3、GetProcAddress的第二個參數可以指定函數名,如:"StringReverseA"。還可以指定爲序號,如:GetProcAddress(hDll,(LPCSTR)1)或GetProcAddress(hDll,MAKEINTRESOURCEA(1))。其中1表示導出函數的序號爲1。GetProcAddress如何區分第2個參數是名稱還是序號?對於字符串而言,首地址是一定大於0xFFFF的,而序號必須小於等於0xFFFF。這就是判斷的依據。使用序號定位導出函數,效率上會高一些,但是這樣的代碼不利於閱讀和維護;
4、關於FreeLibrary,需要說明的是:不能自己釋放自己。如下面的函數在Test.dll內,其意圖是自己釋放自己。實際上它是行不通的:
void FreeMyself(HINSTANCE hDll)
{
FreeLibrary(hDll);
}
1.3 導出數據
dll導出數據很簡單,下面是一個示例:
__declspec(dllexport) int nDataInDll;
或者
extern "C"
{//C++編譯時,防止重命名導出符號nDataInDll
__declspec(dllexport) int nDataInDll;
}
或者使用DEF文件,其內容如下
EXPORTS nDataInDll DATA |
1.3.1 隱式鏈接
客戶端程序隱式鏈接dll時,使用導出數據很簡單。首先是按下列語法聲明變量,然後就可以使用了。
extern "C" //是否使用extern "C"需要根據實際情況而定
{
__declspec(dllimport) int nDataInDll;
}
1.3.2 顯式鏈接
客戶端程序顯式鏈接dll時,使用導出數據稍顯麻煩,其代碼如下:
HINSTANCE hDll = LoadLibrary("Test.dll"); //載入動態庫文件 if(hDll) {//載入成功 int* nDataDll = NULL; #ifdef __cplusplus (FARPROC&)nDataDll = GetProcAddress(hDll,"nDataInDll"); #else (FARPROC)nDataDll = GetProcAddress(hDll,"nDataInDll"); #endif //使用數據 ... ... ... FreeLibrary(hDll); //卸載動態庫文件 } |
注意:GetProcAddress獲得的是數據的地址。
1.4 導出類
導出類的語法有兩種。方法1是定義類的時候同時定義導出,方法2是先聲明類導出,再定義類。方法2比方法1靈活,但有時會有限制。
方法1: class __declspec(dllexport) CTest { public: int m_nValue; CObj m_obj; }; | 方法2: //類聲明,說明是一個導出類 class __declspec(dllexport) CTest; class CTest { public: int m_nValue; CObj m_obj; }; |
導出類的實質其實就是把類的成員函數給導出了。
1.4.1 成員類
以上面的代碼爲例,實例化CTest時需要構造m_obj。因此CObj也必須被導出,否則編譯的時候會產生警告,客戶程序可能無法正常構造CTest類(Debug版正常,Release版分配內存但不調用構造函數)。
1.4.2 導出模板類
首先看下面的代碼
template <class TYPE> class CTemplate { public: CTemplate() { a = 0; } public: TYPE a; };
template class __declspec(dllexport) CTemplate<int>; template class __declspec(dllexport) CTemplate<double>;
class __declspec(dllexport) CUseTemplate { public: CTemplate<int> i; CTemplate<double> d; }; |
導出CUseTemplate時,CTemplate<int>和CTemplate<double>也應該被導出。
注意:類模板是無法導出的,如下面的代碼無法導出CTemplate。
template <class TYPE> class __declspec(dllexport) CTemplate { public: CTemplate() { a = 0; } public: TYPE a; }; |
如果不想導出模板類,請修改成員變量爲指針類型。這樣的話,成員變量的構造、析構將在DLL內完成,而不是在客戶程序裏完成。
class __declspec(dllexport) CUseTemplate { public: CTemplate<int>* pi; CTemplate<double>* pd; }; |
1.4.3 內聯成員函數
內聯函數相當於宏,編譯的時候用來替換源代碼,用以提高效率。一般它是不會被編譯成目標代碼的,但是一旦使用了__declspec(dllexport),編譯程序將會爲其生成一份目標代碼,並導出。
1.4.4 友元函數
友元函數的實質上還是一個函數,只不過它們是類的朋友,可以訪問類的私有成員變量。友元函數的導出,需要專門聲明。具體方法如下:
方法1: class __declspec(dllexport) CTest { public: int m_nValue; public: //導出友元函數要專門聲明 friend __declspec(dllexport) CTest operator+(const CTest&a,const CTest&b); }; |
方法2: //類聲明 class __declspec(dllexport) CTest; //友元函數聲明 __declspec(dllexport) CTest operator+(const CTest&a,const CTest&b); class CTest { public: int m_nValue; public: friend CTest operator+(const CTest&a,const CTest&b); }; |
1.4.5 嵌套類
下面是導出嵌套類的示例代碼:
class __declspec(dllexport) CTest { public: //導出嵌套類,前面也要使用__declspec(dllexport) class __declspec(dllexport) CNest { } }; |
1.4.6 靜態成員變量
一個類被導出,則該類的所有靜態成員變量被當作變量導出。如:下面的代碼:
class __declspec(dllexport) CTest { public: static int s_nValue; //s_nValue 被當作變量導出 }; |
1.4.7 查看導出
按下圖設置VC++6.0,使得編譯時生成map文件。
編譯如下代碼
class __declspec(dllexport) CTest { public: void SetValue(int v) {m_Value = v;} int GetValue(); private: int m_Value; public: static int s_nValue; }; int CTest::s_nValue = 1; int CTest::GetValue() {return m_Value;} |
編譯後查看 map 文件,提取包含 CTest 的函數或變量:
Publics by Value | Rva+Base | 說明 |
?SetValue@CTest@@QAEXH@Z | 10001030 f i | SetValue函數 |
??4CTest@@QAEAAV0@ABV0@@Z | 10001070 f i | 構造函數 |
?GetValue@CTest@@QAEHXZ | 100010b0 f | GetValue函數 |
?s_nValue@CTest@@2HA | 1002ba30 | s_nValue |
注意上表的第2列,f表示函數,i表示內聯。如果將 class __declspec(dllexport) CTest 中的 __declspec(dllexport) 去掉,重新編譯,則上表第2列包含 f i 的在map文件中不會再出現。
去掉CTest定義中的__declspec(dllexport)後,可以使用 DEF文件導出一些CTest成員函數,如:下面的DEF文件導出了函數CTest::GetValue和變量CTest::s_nValue。
EXPORTS ?GetValue@CTest@@QAEHXZ ?s_nValue@CTest@@2HA DATA |
1.5 導入類
客戶端程序可以使用dll導出的類,定義類的時候需要__declspec(dllimport)
方法1: class __declspec(dllimport) CTest { public: int m_nValue; public: //友元函數 friend __declspec(dllimport) CTest operator+(const CTest&a,const CTest&b); }; |
方法2: class __declspec(dllimport) CTest; //類聲明 //下面這句話不再需要 //friend __declspec(dllimport) CTest operator+(const CTest&a,const CTest&b); class CTest { public: int m_nValue; public: //友元函數 friend CTest operator+(const CTest&a,const CTest&b); }; |
1.5.1 內聯成員函數
考慮如下代碼:
class __declspec(dllimport) CTest { public: int m_nValue; public: int GetValue() { return m_nValue; } }; |
現在的問題是:對於內聯函數GetValue,到底使用這裏的定義,還是使用dll導出的內聯函數?經測試發現:在__declspec(dllimport)存在的情況下,將使用dll導出的內聯函數(此時就不是內聯了);在刪除上面的__declspec(dllimport)之後,將使用上面定義的內聯函數。
第2章 MFC Regular DLL
2.1 三種DLL
使用VC++生成dll,共有三種類型。分別爲:non-MFC Win32 DLL、MFC Regular DLL和MFC Extension DLL。
2.1.1 non-MFC Win32 DLL
上一章創建的 Win32 Dynamic-Link Library 即爲non-MFC Win32 DLL。它的特點是可以使用MFC,也可以不使用MFC。導出的函數、類涉及界面、Windows消息處理較少。它特別適合封裝數值計算、硬件控制等功能,此外它還可用於製作純資源dll(連接時指定 /NOENTRY 即可)。
它可以被多種編程語言所支持。
2.1.2 MFC Regular DLL
對於過多的界面處理,如果不借助MFC,則難度是比較大的。此時,可以使用MFC Regular DLL。它必須使用MFC共享庫,可以靜態連接也可以動態鏈接MFC共享庫,建議動態鏈接。
它也可以被多種編程語言所支持。
2.1.3 MFC Extension DLL
擴展的含義是對MFC類的擴展,它主要用於實現MFC類的派生類。如:如果覺得CButton類不太好,可以派生一個CButtonEx類,並在MFC Extension DLL裏實現、導出CButtonEx。任何MFC程序都可以直接使用這個CButtonEx。
它必須使用MFC共享庫,而且必須動態連接MFC共享庫。只有MFC程序才能使用它。
2.2 模塊狀態
使用MFC Regular DLL要特別注意模塊狀態。在導出函數的第一行請增加如下代碼:
AFX_MANAGE_STATE(AfxGetStaticModuleState());
它在棧上創建了一個對象。該對象記錄下當前的模塊狀態,然後設置當前模塊狀態爲本dll模塊狀態。導出函數返回的時候,對象被析構。析構時,會恢復當前模塊狀態。
如果不注意模塊狀態的切換,則本dll內的資源可能無法被調用,甚至程序會崩潰。
2.3 InitInstance
CWinApp派生類的InitInstance函數必須返回 TRUE,否則dll將加載失敗。
如果dll內部需要 ActiveX 控件或 OLE 拖放,請在InitInstance裏調用OleInitialize(NULL),ExitInstance裏調用OleUninitialize。因爲每個MFC模塊都有自己的模塊狀態,所以exe裏調用了AfxOleInit對MFC Regular DLL是沒有什麼影響的,後者需要再次初始化COM庫。但是AfxOleInit這個函數在MFC Regular DLL裏沒有發揮應有的作用(可能是MFC的BUG吧)。
請參考如下代碼:
class CDllApp : public CWinApp { public: BOOL InitInstance() { AfxEnableControlContainer(); OleInitialize(NULL); return TRUE; } int ExitInstance() { OleUninitialize(); return CWinApp::ExitInstance(); } }theApp; |
順便說一句OleInitialize與CoInitialize、CoInitializeEx的區別:CoInitializeEx是CoInitialize的擴展;OleInitialize將調用CoInitialize初始化COM庫,並且還做了其它一些工作,以支持OLE拖放等操作。
2.4 AfxGetApp
AfxGetApp() 等價於 AfxGetModuleState()->m_pCurrentWinApp,而AfxGetModuleState()則是當前模塊狀態。
所以AFX_MANAGE_STATE(AfxGetStaticModuleState());之後,AfxGetModuleState()就是AfxGetStaticModuleState(),即當前模塊狀態是本dll模塊狀態。AfxGetApp()將返回本dll內CWinApp全局對象的地址(即theApp的地址)。
如果從exe調用dll的導出函數,而導出函數沒有調用AFX_MANAGE_STATE(AfxGetStaticModuleState()),則AfxGetModuleState()將是AfxGetAppModuleState(),即當前模塊狀態是exe模塊狀態。AfxGetApp()將返回exe內CWinApp全局對象的地址。如果exe不是MFC程序,而是VB6.0、VC#程序,則AfxGetAppModuleState的返回值是不能使用的。
2.5 PreTranslateMessage
雖然MFC Regular DLL派生了CWinApp類,並有一個theApp全局對象。但它不包含CWinApp::Run機制,主消息由exe負責接收、分發。如果DLL 生成了無模式對話框或有自己的主框架窗口,則它應該導出函數來調用 PreTranslateMessage。exe程序需要調用這個導出函數。示例代碼如下:
DLL端需要導出函數,調用AfxGetApp()->PreTranslateMessage __declspec(dllexport) BOOL DllPreTranslateMessage(MSG* pMsg) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); //切換模塊狀態 return AfxGetApp()->PreTranslateMessage(pMsg); } |
exe端需要調用DLL的導出函數 class CTestApp : public CWinApp { public: BOOL PreTranslateMessage(MSG* pMsg) { if(DllPreTranslateMessage(pMsg)) { return TRUE; } return CWinApp::PreTranslateMessage(pMsg); } ... ... ... }theApp; |
如果不這麼做,則dll內部的非模態對話框PreTranslateMessage函數不會被執行,對話框內按Tab鍵也無法切換焦點。
如果exe不是MFC程序,而是VB6.0、VC#程序,該如何處理PreTranslateMessage?這個問題需要再進行深入研究。此時,非模態對話框內按Tab鍵無法切換焦點,該如何處理?估計得使用鍵盤鉤子了……
2.6 OnIdle
一個標準的MFC程序裏,主窗口菜單項、工具欄的顯示更新,以及臨時對象的銷燬是在應用程序類的OnIdle裏進行處理的。如果MFC Regular DLL裏有框架窗口(CFrameWnd),並有菜單、工具欄,則它也需要處理OnIdle。方法與PreTranslateMessage的處理相同,其代碼如下:
DLL端需要導出函數,調用AfxGetApp()->OnIdle __declspec(dllexport) void DllOnIdle(LONG lCount) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); //切換模塊狀態 AfxGetApp()->OnIdle(lCount); } |
exe端需要調用DLL的導出函數 class CTestApp : public CWinApp { public: BOOL OnIdle(LONG lCount) { DllOnIdle(lCount); return CWinApp::OnIdle(lCount); } ... ... ... }theApp; |
如果exe端不是MFC程序,則在dll內部需要定時調用如下代碼:
AFX_MANAGE_STATE(AfxGetStaticModuleState());
AfxGetApp()->OnIdle(0); //更新菜單、工具欄
AfxGetApp()->OnIdle(1); //刪除臨時對象
可以把這幾行代碼放在導出函數的最前面。
第3章 MFC Extension DLL
如前所言,MFC Extension DLL主要用於實現MFC類的派生類。如:如果覺得CButton類不太好,可以派生一個CButtonEx類,並在MFC Extension DLL裏實現、導出CButtonEx。任何MFC程序都可以直接使用這個CButtonEx。當然是可以在MFC Extension DLL裏創建一個非模態對話框的,但它的PreTranslateMessage同樣不會被主動執行。也就是說MFC Extension DLL的主要功能不是處理界面、Windows消息,而是擴展MFC已有的類。
MFC Extension DLL必須使用MFC共享庫,而且必須動態連接MFC共享庫。只有MFC程序才能使用它。
順便說一句:MFC Extension DLL不再像MFC Reguler DLL那樣需要切換模塊狀態。
3.1 顯式鏈接
大多數情況下,MFC Extension DLL都是隱式連接的,因爲它導出的基本上都是擴展MFC的類。但顯式鏈接也是可行的,不過使用的不是LoadLibrary,而是AfxLoadLibrary。卸載的函數也由FreeLibrary改爲AfxFreeLibrary。
3.2 查找資源
代碼CDialog(_T("DlgRes")).DoModal();用於顯示一個對話框。爲了顯示這個對話框,需要查找對話框資源"DlgRes"。查找資源的順序如下圖所示:
即:首先在本模塊(MFC Regular DLL或EXE)裏查找,然後在各個MFC Extension DLL裏查找,最後在MFC42.DLL裏查找。
注意:如果是MFC Regular DLL,則默認情況下不會在各個MFC Extension DLL裏查找資源。需要做如下處理後才行:
1、MFC Extension DLL導出一個函數
__declspec(dllexport) void WINAPI ExtFunc() { new CDynLinkLibrary(ExtDLL); } |
2、在MFC Regular DLL的InitInstance函數裏導入調用該函數
BOOL CRegDllApp::InitInstance() { {// AFX_MANAGE_STATE(AfxGetStaticModuleState()); ExtFunc(); } return CWinApp::InitInstance(); } |
現在MFC Regular DLL裏的代碼CDialog(_T("DlgRes")).DoModal();在執行時就會在MFC Extension DLL裏查找資源"DlgRes"了。
3.3 代碼解析
MFC模塊狀態是一個AFX_MODULE_STATE,它裏面有一個成員變量
CTypedSimpleList<CDynLinkLibrary*> m_libraryList;
它是exe模塊用到的所有的MFC Extension DLL鏈表。MFC Extension DLL是通過DllMain完成初始化的,其典型代碼如下:
static AFX_EXTENSION_MODULE TestDLL = { NULL, NULL };
extern "C" int APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { if (dwReason == DLL_PROCESS_ATTACH) { if (!AfxInitExtensionModule(TestDLL, hInstance)) return 0; new CDynLinkLibrary(TestDLL); } else if (dwReason == DLL_PROCESS_DETACH) { AfxTermExtensionModule(TestDLL); } return 1; // ok } |
new CDynLinkLibrary(TestDLL)將創建一個CDynLinkLibrary對象,並將自己加入m_libraryList,其代碼如下:
CDynLinkLibrary::CDynLinkLibrary(AFX_EXTENSION_MODULE&,BOOL) { ... ... ... m_pModuleState->m_libraryList.AddHead(this); ... ... ... } |
C:\Program Files\Microsoft Visual Studio\VC98\MFC\SRC\DLLINIT.CPP裏AfxFindResourceHandle函數說明了資源查找的順序:
HINSTANCE AFXAPI AfxFindResourceHandle(LPCTSTR lpszName, LPCTSTR lpszType) { HINSTANCE hInst; ... ... if(!pModuleState->m_bSystem) {//第1步 hInst = AfxGetResourceHandle(); } ... ... for(CDynLinkLibrary* pDLL = pModuleState->m_libraryList; pDLL != NULL;pDLL = pDLL->m_pNextDLL) {//第2步 if(!pDLL->m_bSystem && pDLL->m_hResource != NULL && ::FindResource(pDLL->m_hResource, lpszName, lpszType)) { return pDLL->m_hResource; } } ... ... hInst = pModuleState->m_appLangDLL; //第3步 ... ... if(!pModuleState->m_bSystem) {//第4步 hInst = AfxGetResourceHandle(); } ... ... for(pDLL = pModuleState->m_libraryList; pDLL != NULL; pDLL = pDLL->m_pNextDLL) {//第5步 if(pDLL->m_bSystem && pDLL->m_hResource != NULL && ::FindResource(pDLL->m_hResource, lpszName, lpszType)) { return pDLL->m_hResource; } } ... ... return AfxGetResourceHandle();//第6步 } |
注意:第2步與第5步的區別在於MFC Extension DLL一個是非系統的(!pDLL->m_bSystem),另一個是系統的(pDLL->m_bSystem)。MFC42.DLL就是一個系統的MFC Extension DLL。
注意CDynLinkLibrary構造函數裏的代碼m_pModuleState->m_libraryList.AddHead(this);其中的m_pModuleState是exe的模塊狀態,即AfxGetAppModuleState()的返回值。默認情況下MFC Extension DLL中的new CDynLinkLibrary(TestDLL)不會加入MFC Regular DLL的m_pModuleState->m_libraryList,在此情況下MFC Regular DLL需要的資源不可能在MFC Extension DLL裏找到。下面的代碼把MFC Extension DLL中的new CDynLinkLibrary(TestDLL)加入到MFC Regular DLL的m_pModuleState->m_libraryList。
BOOL CRegDllApp::InitInstance() { {// AFX_MANAGE_STATE(AfxGetStaticModuleState()); ExtFunc(); } return CWinApp::InitInstance(); } |