一般的,在介紹Windows編程的書中講述DLL的有關知識較多,而介紹MFC的書則比較少地提到。即使使用MFC來編寫動態鏈接庫,對於初步接觸DLL的程序員來說,了解DLL的背景知識是必要的。另外,MFC提供了新的手段來幫助編寫DLL程序。所以,本節先簡潔的介紹有關概念。
DLL的背景知識
靜態鏈接和動態鏈接
當前鏈接的目標代碼(.obj)如果引用了一個函數卻沒有定義它,鏈接程序可能通過兩種途徑來解決這種從外部對該函數的引用:
靜態鏈接
鏈接程序搜索一個或者多個庫文件(標准庫.lib),直到在某個庫中找到了含有所引用函數的對象模塊,然後鏈接程序把這個對象模塊拷貝到結果可執行文件(.exe)中。鏈接程序維護對該函數的所有引用,使它們指向該程序中現在含有該函數拷貝的地方。
動態鏈接
鏈接程序也是搜索一個或者多個庫文件(輸入庫.lib),當在某個庫中找到了所引用函數的輸入記錄時,便把輸入記錄拷貝到結果可執行文件中,產生一次對該函數的動態鏈接。這裡,輸入記錄不包含函數的代碼或者數據,而是指定一個包含該函數代碼以及該函數的順序號或函數名的動態鏈接庫。
當程序運行時,Windows裝入程序,並尋找文件中出現的任意動態鏈接。對於每個動態鏈接,Windows裝入指定的DLL並且把它映射到調用進程的虛擬地址空間(如果沒有映射的話)。因此,調用和目標函數之間的實際鏈接不是在鏈接應用程序時一次完成的(靜態),相反,是運行該程序時由Windows完成的(動態)。
這種動態鏈接稱為加載時動態鏈接。還有一種動態鏈接方式下面會談到。
動態鏈接的方法
鏈接動態鏈接庫裡的函數的方法如下:
加載時動態鏈接(Load_time dynamic linking)
如上所述。Windows搜索要裝入的DLL時,按以下順序:
應用程序所在目錄→當前目錄→Windows SYSTEM目錄→Windows目錄→PATH環境變量指定的路徑。
運行時動態鏈接(Run_time dynamic linking)
程序員使用LoadLibrary把DLL裝入內存並且映射DLL到調用進程的虛擬地址空間(如果已經作了映射,則增加DLL的引用計數)。首先,LoadLibrary搜索DLL,搜索順序如同加載時動態鏈接一樣。然後,使用GetProcessAddress得到DLL中輸出函數的地址,並調用它。最後,使用FreeLibrary減少DLL的引用計數,當引用計數為0時,把DLL模塊從當前進程的虛擬空間移走。
輸入庫(.lib):
輸入庫以.lib為擴展名,格式是COFF(Common object file format)。COFF標准庫(靜態鏈接庫)的擴展名也是.lib。COFF格式的文件可以用dumpbin來查看。
輸入庫包含了DLL中的輸出函數或者輸出數據的動態鏈接信息。當使用MFC創建DLL程序時,會生成輸入庫(.lib)和動態鏈接庫(.dll)。
輸出文件(.exp)
輸出文件以.exp為擴展名,包含了輸出的函數和數據的信息,鏈接程序使用它來創建DLL動態鏈接庫。
映像文件(.map)
映像文件以.map為擴展名,包含了如下信息:
模塊名、時間戳、組列表(每一組包含了形式如section::offset的起始地址,長度、組名、類名)、公共符號列表(形式如section::offset的地址,符號名,虛擬地址flat address,定義符號的.obj文件)、入口點如section::offset、fixup列表。
lib.exe工具
它可以用來創建輸入庫和輸出文件。通常,不用使用lib.exe,如果工程目標是創建DLL程序,鏈接程序會完成輸入庫的創建。
更詳細的信息可以參見MFC使用手冊和文檔。
鏈接規范(Linkage Specification )
這是指鏈接采用不同編程語言寫的函數(Function)或者過程(Procedure)的鏈接協議。MFC所支持的鏈接規范是“C”和“C++”,缺省的是“C++”規范,如果要聲明一個“C”鏈接的函數或者變量,則一般采用如下語法:
#if defined(__cplusplus)
extern "C"
{
#endif
//函數聲明(function declarations)
…
//變量聲明(variables declarations)
#if defined(__cplusplus)
}
#endif
所有的C標准頭文件都是用如上語法聲明的,這樣它們在C++環境下可以使用。
修飾名(Decoration name)
“C”或者“C++”函數在內部(編譯和鏈接)通過修飾名識別。修飾名是編譯器在編譯函數定義或者原型時生成的字符串。有些情況下使用函數的修飾名是必要的,如在模塊定義文件裡頭指定輸出“C++”重載函數、構造函數、析構函數,又如在匯編代碼裡調用“C””或“C++”函數等。
修飾名由函數名、類名、調用約定、返回類型、參數等共同決定。
調用約定
調用約定(Calling convention)決定以下內容:函數參數的壓棧順序,由調用者還是被調用者把參數彈出棧,以及產生函數修飾名的方法。MFC支持以下調用約定:
_cdecl
按從右至左的順序壓參數入棧,由調用者把參數彈出棧。對於“C”函數或者變量,修飾名是在函數名前加下劃線。對於“C++”函數,有所不同。
如函數void test(void)的修飾名是_test;對於不屬於一個類的“C++”全局函數,修飾名是?test@@ZAXXZ。
這是MFC缺省調用約定。由於是調用者負責把參數彈出棧,所以可以給函數定義個數不定的參數,如printf函數。
_stdcall
按從右至左的順序壓參數入棧,由被調用者把參數彈出棧。對於“C”函數或者變量,修飾名以下劃線為前綴,然後是函數名,然後是符號“@”及參數的字節數,如函數int func(int a, double b)的修飾名是_func@12。對於“C++”函數,則有所不同。
所有的Win32 API函數都遵循該約定。
_fastcall
頭兩個DWORD類型或者占更少字節的參數被放入ECX和EDX寄存器,其他剩下的參數按從右到左的順序壓入棧。由被調用者把參數彈出棧,對於“C”函數或者變量,修飾名以“@”為前綴,然後是函數名,接著是符號“@”及參數的字節數,如函數int func(int a, double b)的修飾名是12。對於“C++”函數,有所不同。
未來的編譯器可能使用不同的寄存器來存放參數。
thiscall
僅僅應用於“C++”成員函數。this指針存放於CX寄存器,參數從右到左壓棧。thiscall不是關鍵詞,因此不能被程序員指定。
naked call
采用1-4的調用約定時,如果必要的話,進入函數時編譯器會產生代碼來保存ESI,EDI,EBX,EBP寄存器,退出函數時則產生代碼恢復這些寄存器的內容。naked call不產生這樣的代碼。
naked call不是類型修飾符,故必須和_declspec共同使用,如下:
__declspec( naked ) int func( formal_parameters )
{
// Function body
}
過時的調用約定
原來的一些調用約定可以不再使用。它們被定義成調用約定_stdcall或者_cdecl。例如:
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
表7-1顯示了一個函數在幾種調用約定下的修飾名(表中的“C++”函數指的是“C++”全局函數,不是成員函數),函數原型是void CALLTYPE test(void),CALLTYPE可以是_cdecl、_fastcall、_stdcall。
表7-1 不同調用約定下的修飾名
調用約定 extern “C”或.C文件 .cpp, .cxx或/TP編譯開關 _cdecl _test ?test@@ZAXXZ _fastcall 0 ?test@@YIXXZ _stdcall _test@0 ?test@@YGXXZ
MFC的DLL應用程序的類型
靜態鏈接到MFC的規則DLL應用程序
該類DLL應用程序裡頭的輸出函數可以被任意Win32程序使用,包括使用MFC的應用程序。輸入函數有如下形式:
extern "C" EXPORT YourExportedFunction( );
如果沒有extern “C”修飾,輸出函數僅僅能從C++代碼中調用。
DLL應用程序從CWinApp派生,但沒有消息循環。
動態鏈接到MFC的規則DLL應用程序
該類DLL應用程序裡頭的輸出函數可以被任意Win32程序使用,包括使用MFC的應用程序。但是,所有從DLL輸出的函數應該以如下語句開始:
AFX_MANAGE_STATE(AfxGetStaticModuleState( ))
此語句用來正確地切換MFC模塊狀態。關於MFC的模塊狀態,後面第9章有詳細的討論。
其他方面同靜態鏈接到MFC的規則DLL應用程序。
擴展DLL應用程序
該類DLL應用程序動態鏈接到MFC,它輸出的函數僅可以被使用MFC且動態鏈接到MFC的應用程序使用。和規則DLL相比,有以下不同:
它沒有一個從CWinApp派生的對象;
它必須有一個DllMain函數;
DllMain調用AfxInitExtensionModule函數,必須檢查該函數的返回值,如果返回0,DllMmain也返回0;
如果它希望輸出CRuntimeClass類型的對象或者資源(Resources),則需要提供一個初始化函數來創建一個CDynLinkLibrary對象。並且,有必要把初始化函數輸出。
使用擴展DLL的MFC應用程序必須有一個從CWinApp派生的類,而且,一般在InitInstance裡調用擴展DLL的初始化函數。
為什麼要這樣做和具體的代碼形式,將在後面9.4.2節說明。
MFC類庫也是以DLL的形式提供的。通常所說的動態鏈接到MFC 的DLL,指的就是實現MFC核心功能的MFCXX.DLL或者MFCXXD.DLL(XX是版本號,XXD表示調試版)。至於提供OLE(MFCOXXD.DLL或者MFCOXX0.DLL)和NET(MFCNXXD.DLL或者MFCNXX.DLL)服務的DLL就是動態鏈接到MFC核心DLL的擴展DLL。
其實,MFCXX.DLL可以認為是擴展DLL的一個特例,因為它也具備擴展DLL的上述特點。
DLL的幾點說明
DLL應用程序的入口點是DllMain。
對程序員來說,DLL應用程序的入口點是DllMain。
DllMain負責初始化(Initialization)和結束(Termination)工作,每當一個新的進程或者該進程的新的線程訪問DLL時,或者訪問DLL的每一個進程或者線程不再使用DLL或者結束時,都會調用DllMain。但是,使用TerminateProcess或TerminateThread結束進程或者線程,不會調用DllMain。
DllMain的函數原型符合DllEntryPoint的要求,有如下結構:
BOOL WINAPI DllMain (HANDLE hInst,
ULONG ul_reason_for_call,LPVOID lpReserved)
{
switch( ul_reason_for_call ) {
case DLL_PROCESS_ATTACH:
...
case DLL_THREAD_ATTACH:
...
case DLL_THREAD_DETACH:
...
case DLL_PROCESS_DETACH:
...
}
return TRUE;
}
其中:
參數1是模塊句柄;
參數2是指調用DllMain的類別,四種取值:新的進程要訪問DLL;新的線程要訪問DLL;一個進程不再使用DLL(Detach from DLL);一個線程不再使用DLL(Detach from DLL)。
參數3保留。
如果程序員不指定DllMain,則編譯器使用它自己的DllMain,該函數僅僅返回TRUE。
規則DLL應用程序使用了MFC的DllMain,它將調用DLL程序的應用程序對象(從CWinApp派生)的InitInstance函數和ExitInstance函數。
擴展DLL必須實現自己的DllMain。
_DllMainCRTStartup
為了使用“C”運行庫(CRT,C Run time Library)的DLL版本(多線程),一個DLL應用程序必須指定_DllMainCRTStartup為入口函數,DLL的初始化函數必須是DllMain。
_DllMainCRTStartup完成以下任務:當進程或線程捆綁(Attach)到DLL時為“C”運行時的數據(C Runtime Data)分配空間和初始化並且構造全局“C++”對象,當進程或者線程終止使用DLL(Detach)時,清理C Runtime Data並且銷毀全局“C++”對象。它還調用DllMain和RawDllMain函數。
RawDllMain在DLL應用程序動態鏈接到MFC DLL時被需要,但它是靜態的鏈接到DLL應用程序的。在講述狀態管理時解釋其原因。
DLL的函數和數據
DLL的函數分為兩類:輸出函數和內部函數。輸出函數可以被其他模塊調用,內部函數在定義它們的DLL程序內部使用。
雖然DLL可以輸出數據,但一般的DLL程序的數據僅供內部使用。
DLL程序和調用其輸出函數的程序的關系
DLL模塊被映射到調用它的進程的虛擬地址空間。
DLL使用的內存從調用進程的虛擬地址空間分配,只能被該進程的線程所訪問。
DLL的句柄可以被調用進程使用;調用進程的句柄可以被DLL使用。
DLL使用調用進程的棧。
DLL定義的全局變量可以被調用進程訪問;DLL可以訪問調用進程的全局數據。使用同一DLL的每一個進程都有自己的DLL全局變量實例。如果多個線程並發訪問同一變量,則需要使用同步機制;對一個DLL的變量,如果希望每個使用DLL的線程都有自己的值,則應該使用線程局部存儲(TLS,Thread Local Strorage)。
輸出函數的方法
傳統的方法
在模塊定義文件的EXPORT部分指定要輸入的函數或者變量。語法格式如下:
entryname[=internalname] [@ordinal[NONAME]] [DATA] [PRIVATE]
其中:
entryname是輸出的函數或者數據被引用的名稱;
internalname同entryname;
@ordinal表示在輸出表中的順序號(index);
NONAME僅僅在按順序號輸出時被使用(不使用entryname);
DATA表示輸出的是數據項,使用DLL輸出數據的程序必須聲明該數據項為_declspec(dllimport)。
上述各項中,只有entryname項是必須的,其他可以省略。
對於“C”函數來說,entryname可以等同於函數名;但是對“C++”函數(成員函數、非成員函數)來說,entryname是修飾名。可以從.map映像文件中得到要輸出函數的修飾名,或者使用DUMPBIN /SYMBOLS得到,然後把它們寫在.def文件的輸出模塊。DUMPBIN是VC提供的一個工具。
如果要輸出一個“C++”類,則把要輸出的數據和成員的修飾名都寫入.def模塊定義文件。
在命令行輸出
對鏈接程序LINK指定/EXPORT命令行參數,輸出有關函數。
使用MFC提供的修飾符號_declspec(dllexport)
在要輸出的函數、類、數據的聲明前加上_declspec(dllexport)的修飾符,表示輸出。MFC提供了一些宏,就有這樣的作用,如表7-2所示。
表7-2 MFC定義的輸入輸出修飾符
宏名稱 宏內容 AFX_CLASS_IMPORT __declspec(dllexport) AFX_API_IMPORT __declspec(dllexport) AFX_DATA_IMPORT __declspec(dllexport) AFX_CLASS_EXPORT __declspec(dllexport) AFX_API_EXPORT __declspec(dllexport) AFX_DATA_EXPORT __declspec(dllexport) AFX_EXT_CLASS #ifdef _AFXEXT
像AFX_EXT_CLASS這樣的宏,如果用於DLL應用程序的實現中,則表示輸出(因為_AFX_EXT被定義,通常是在編譯器的標識參數中指定該選項/D_AFX_EXT);如果用於使用DLL的應用程序中,則表示輸入(_AFX_EXT沒有定義)。
要輸出整個的類,對類使用_declspec(_dllexpot);要輸出類的成員函數,則對該函數使用_declspec(_dllexport)。如:
class AFX_EXT_CLASS CTextDoc : public CDocument
{
…
}
extern "C" AFX_EXT_API void WINAPI InitMYDLL();
這幾種方法中,最好采用第三種,方便好用;其次是第一種,如果按順序號輸出,調用效率會高些;最次是第二種。
在“C++”下定義“C”函數,需要加extern “C”關鍵詞。輸出的“C”函數可以從“C”代碼裡調用。