動態連結庫(DLL)
|
|
接著,再建立一個Win32 Console Application工程DLL_Test,同樣將該工程加入先前的DLLTest工作區中,並直接儲存在該工作區目錄下。然後向工程DLL_Test加入下面的檔案:
|
此時,工作差不多做完了,但還需進行一下設定。在Project|Settings裡,把兩個工程裡的General標籤裡的Intermediate files和Output files都設定為Debug。這樣確保兩個工程的輸出檔案在一個目錄中,以便後面動態庫連結時的查詢。另外,設定DLL_Test為活動工程(Project|Set Active Project),設定DLL_Test依賴於DLL_Lib(Project|Dependencies)。此時,就可以編譯運行了。執行結果為:
> process attach of dll
max(2, 3) = 3
> process detach of dll
Press any key to continue
下面對上面的程式碼和結果進行分析。
在dll_lib.h中,EXPORT巨集實質上就是一個匯出函式所需要的關鍵字。__declspec (dllexport)是Windows擴充套件關鍵字的組合,表示DLL裡的物件的儲存型別關鍵字。extern "C"用於C++程式使用該函式時的函式宣告的連結屬性。WINAPI是巨集定義,等價於__stdcall。下面列出Windows程式設計中常見的幾種有關呼叫約定的巨集,它們都是與__stdcall和__cdecl有關的(from windef.h):
#define CALLBACK __stdcall // 用於回撥函式
#define WINAPI __stdcall // 用於API函式
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
另外,關於__stdcall:如果通過VC++編寫的DLL欲被其他語言編寫的程式呼叫,應將函式的呼叫約定宣告為__stdcall方式,WINAPI、CALLBACK都採用這種方式,而C/C++預設的呼叫方式卻為__cdecl。__stdcall方式與__cdecl對函式名最終生成符號的方式不同。若採用C編譯方式(在C++中需將函式宣告為extern "C"),__stdcall呼叫約定在輸出函式名前面加下劃線,後面加“@”符號和引數的位元組數,形如_functionN[email protected] ,而__cdecl呼叫約定僅在輸出函式名前面加下劃線,形如_functionName。(小技巧:如何檢視這些符號?寫一個程式,只提供函式的宣告而不給定義,就可以看到連結器給出的符號了)
因此,在前面例子中,該DLL聲明瞭一個匯出函式GetMax,其連線屬性採用CALLBACK(即__stdcall)。另外,請注意,例子中的巨集EXPORT會根據是在C程式還是在C++程式中被呼叫選擇相應的連線方式。在定義匯出函式時,不需要EXPORT巨集,只需要在函式宣告時使用即可。
DllMain函式在DLL載入和解除安裝時被呼叫。它的第一個引數是DLL控制代碼,第三個引數保留。第二個引數用來區分該DLLMain函式是在什麼情況下被呼叫的,如程式所示。如果初始化成功,則DllMain應該返回一個非零值。如果返回零值將導致程式停止執行(你可以修改上面例子中的DllMain的返回值為0,將看到相應的出錯結果)。如果在你的DLL程式中沒有編寫DllMain函式,那麼在執行該DLL時,系統將引入一個不做任何操作的預設DllMain函式版本。
|
此時,不再需要動態的.h檔案和.lib檔案,只需要提供.dll檔案即可。在具體使用時,先用LoadLibrary載入Dll檔案,然後用GetProcAddress尋找函式的地址,此時必須提供該函式的在Dll中的名字(不一定與函式名相同)。
然後編譯連結、執行,結果與前面的執行結果相同。
下面將解釋,為什麼前面要去掉WINAPI呼叫約定(即採用預設的__cdecl方式)。我們可以先看看DLL_Lib.dll裡面的連結符號。在cmd中執行命令:
dumpbin /exports DLL_Lib.dll
得到如下結果:
Dump of file f:\code\DLLTest\Debug\Dll_lib.dll File Type: DLL Section contains the following exports for DLL_Lib.dll
0 characteristics ordinal hint RVA name 1 0 0000100A GetMax Summary
4000 .data |
可以看到GetMax函式在編譯後在Dll中的名字仍為GetMax,所以在前面的程式中使用的是:
pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax");
然後,我們把WINAPI添加回去,重新編譯DLL_Lib工程。執行剛才的DLL_Test程式,執行出錯,結果如下:
> process attach of dll
Can't find function "GetMax"
> process detach of dll
Press any key to continue
顯然,執行失敗原因是因為沒有找到GetMax函式。再次執行命令:dumpbin /exports DLL_Lib.dll,結果如下(部分結果):
1 ordinal base ordinal hint RVA name 1 0 0000100A [email protected] |
從上面dumpbin的輸出看,GetMax函式在WINAPI呼叫約定方式下在DLL裡的名字與原始碼中的函式定義時的名字不再相同,其匯出名是"[email protected]"。此時,你把testMain.c中的函式指標型別宣告和函式查詢語句作如下修改:
typedef int (WINAPI* PGetMax)(int, int);
pGetMax = (PGetMax)GetProcAddress(hDll, "[email protected]");
再次編譯連結,然後執行,發現結果又正確了。
現在找到了問題所在。很顯然,這種修改方式並不適用,而預設生成的名字又不是我們所想要的。那麼該怎麼解決這個問題呢?這就需要用到.def檔案來解決。
模組定義檔案(.def)
模組定義檔案(.def檔案)是一個描述DLL的各種屬性的檔案,可以包含一個或多個模組定義語句。如果你不使用關鍵字__declspec(dllexport)關鍵字匯出DLL中的函式,那麼DLL就需要一個.def檔案。
一個最小的.def檔案必須包含下面的模組定義語句:
(1)檔案中第一個語句必須是LIBRARY語句。該語句標記該.def檔案屬於哪個DLL。語法形式為:LIBRARY 。
(2)EXPORTS語句列表。第一個匯出語句的形式為:entryname[=internalname] [@ordinal],列出DLL中要匯出的函式的名字和可選的序號(ordinal value)。要匯出的函式名可以是程式原始碼中的函式名,也可以定義新的函式別名(但後面必須緊跟[=<原函式名>]);序號必須在範圍1到N之間且不能重複,其中N是DLL中匯出的函式個數。因此,EXPORTS語句語法形式為:
EXPORTS
[=<internalname1] [@]
[=<internalname2] [@]
;...
(3)雖然不是必須的,一個.def檔案也常常包含DESCRIPTION語句,用來描述該DLL的用途之類,語法形式為:
DESCRIPTION ""
(4)在任意位置,可以包含註釋語句,以分號(;)開始。
例如,在本文中後面將用到的.def檔案為:
; DLL_Lib.def
LIBRARY DLL_Lib ; the dll name
EXPORTS ; Ok, over |
現在,讓我們回到DLL_Lib工程,修改GetMax函式的宣告,把EXPORT去掉,重新編譯該工程。然後,執行dumpbin命令,我們發現此時沒有匯出函式。再將上面的DLL_Lib.def檔案新增進DLL_Lib工程,再次編譯,並執行dumpbin命令,得到如下結果(引用部分結果):
1 ordinal base ordinal hint RVA name
1 0 0000100A GetMax |
正如我們所預期的,有兩個匯出函式GetMax和Max。注意,此時原始碼中的GetMax函式的匯出名不再是預設的“[email protected]”。另外,需要注意的是,兩個匯出函式有相同的相對虛擬地址(RVA),也說明了兩個匯出名實質是同一個函式的不同名字而已,都是原始碼中GetMax函式的匯出名。
現在,回到DLL_Test工程,修改testMain.c檔案內容如下:
|
編譯連結、執行,結果如下:
> process attach of dll
GetMax(2, 3) = 3
Max(2, 3) = 3
> process detach of dll
Press any key to continue
執行結果正如前面分析的那樣,GetMax和Max都得到了相同的結果。
到這裡,我們解決了DLL匯出函式名在各種呼叫約定下的預設名可能不同於原始碼中函式名的問題。此時,你就可以製作跟Windows的自帶API函式庫相同的庫了:使用__stdcall呼叫約定以滿足Windows下的任何語言都可以呼叫DLL庫,同時使用函式名作為匯出名,以方便使用者使用DLL裡的函式。
匯出全域性變數 前面我們介紹了DLL中的函式的匯出方法,這裡也介紹一下DLL中全域性變數的匯出。 首先需要明確的是,當多個應用程式同時使用同一個DLL時,系統中只有一個DLL例項(這裡主要指程式碼段,一般不包含資料段)。也就是說,如果沒有特殊處理,DLL中的資料都是每個使用DLL的應用都保留一份副本的(但是,可以根據需要實現DLL資料的共享,後面進行介紹)。因此,使用DLL的各應用程式之間不會發生干擾。 要匯出DLL中的全域性變數,方法與匯出函式基本一樣。只是,在定義.def檔案時,在EXPORTS定義語句之後用DATA識別符號表明這是變數。例如:g_oneNumber DATA 或者 g_oneNumber @3 DATA。 在使用DLL中匯出的全域性變數時,對於前面DLL的兩種連結方式,有不同的方法。其中,對於執行時連結的DLL,其使用方法與函式一樣(流程:LoadLibrary, GetProcAddress),只是在使用時要知道這是一個變數的地址,而不再是一個函式的地址即可(其實,用dumpbin工具檢視DLL的匯出列表,會發現匯出的資料也被當作函式計數)。 對於裝載時連結,要匯入DLL中的變數,有點與函式不一樣的地方,那就是必須顯示地用關鍵字__declspec(dllimport)匯入DLL中的變數。例如,在使用前面的g_oneNumber前,應先匯入:__declspec(dllimport) extern int g_oneNumber。然後,其它與函式的使用方法無異。 共享DLL中的資料 有時,可能需要在使用DLL的多個應用之間共享DLL的資料,而預設情況下,DLL的資料是每個應用擁有一份副本的。要實現這個需求,就需要做些特殊處理。 首先,定義一個數據段,裡面有需要共享的變數,並要初始化這些變數。然後設定該資料段為共享即可,比較簡單。例如,要在DLL中共享int型變數g_oneNumber,那麼應按如下方式定義該變數:#pragma data_seg ("shared")
int g_oneNumber = 0;
#pragma data_seg () #pragma comment(linker,"/SECTION:shared,RWS") 對上面的程式碼做些解釋:#pragma data_seg ("shared")建立了一個數據段,命名為Shared;#pragma data_seg()標記該資料段的結束;它們之間定義的是該資料段中的變數。注意:這裡對變數的初始化是必須的,否則,編譯器會把未初始化的變數放在普通的未初始化資料段,而不是在共享的資料段。 #pragma comment(linker, "SECTION:shared,RWS")告訴連結器shared資料段具有RWS屬性。這裡的RWS是指Read、Write和Shared三個屬性。也可以在IDE中設定工程屬性:在Settings|Link|Project Options中,新增連結引數:/SECTION:shared,RWS。 資源DLL的製作及使用 有了前面的基礎,資源DLL的製作及使用相對簡單多了。如果是純資源DLL的話(沒有匯出函式),那麼只需要定義一個有DLLMain函式的檔案即可,然後加入資源,編譯成DLL庫即可。在使用時,只需要動態載入這個資源庫,然後載入庫裡的資源即可。例如,資源庫裡有點陣圖資源,那麼只需要LoadBitmap即可。