天天看點

MFC深入淺出-MFC的DLL

MFC的DLL

一般的,在介紹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)

}

所有的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)的修飾名是@func@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編譯開關

_test

?test@@ZAXXZ

@test@0

?test@@YIXXZ

_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

AFX_DATA_IMPORT

AFX_CLASS_EXPORT

AFX_API_EXPORT

AFX_DATA_EXPORT

AFX_EXT_CLASS

#ifdef _AFXEXT

#else

AFX_EXT_API

AFX_EXT_DATA

AFX_EXT_DATADEF

 

像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”代碼裡調用。