一、前言
自從微軟推出 16 位的 Windows 操作系統起,此后每種版本的 Windows 操作系統都非常依賴于動態鏈接庫 (DLL) 中的函數和數據,實際上 Windows 操作系統中幾乎所有的內容都由 DLL 以一種或另外一種形式代表著,例如顯示的字體和圖標存儲在 GDI DLL 中、顯示 Windows 桌面和處理用戶的輸入所需要的代碼被存儲在一個 User DLL 中、 Windows 編程所需要的大量的 API 函數也被包含在 Kernel DLL 中。
在 Windows 操作系統中使用 DLL 有很多優點,最主要的一點是多個應用程序、甚至是不同語言編寫的應用程序可以共享一個 DLL 文件,真正實現了資源 " 共享 " ,大大縮小了應用程序的執行代碼,更加有效的利用了內存;使用 DLL 的另一個優點是 DLL 文件作為一個單獨的程序模塊,封裝性、獨立性好,在軟件需要升級的時候,開發人員只需要修改相應的 DLL 文件就可以了,而且,當 DLL 中的函數改變后,只要不是參數的改變 , 程序代碼并不需要重新編譯。這在編程時十分有用,大大提高了軟件開發和維護的效率。
既然 DLL 那么重要,所以搞清楚什么是 DLL 、如何在 Windows 操作系統中開發使用 DLL 是程序開發人員不得不解決的一個問題。本文針對這些問題,通過一個簡單的例子,即在一個 DLL 中實現比較最大、最小整數這兩個簡單函數,全面地解析了在 Visual C++ 編譯環境下編程實現 DLL 的過程,文章中所用到的程序代碼在 Windows98 系統、 Visual C++6.0 編譯環境下通過。
二、 DLL 的概念
DLL 是建立在客戶 / 服務器通信的概念上,包含若干函數、類或資源的庫文件,函數和數據被存儲在一個 DLL (服務器)上并由一個或多個客戶導出而使用,這些客戶可以是應用程序或者是其它的 DLL 。 DLL 庫不同于靜態庫,在靜態庫情況下,函數和數據被編譯進一個二進制文件(通常擴展名為 *.LIB ), Visual C++ 的編譯器在處理程序代碼時將從靜態庫中恢復這些函數和數據并把他們和應用程序中的其他模塊組合在一起生成可執行文件。這個過程稱為 " 靜態鏈接 " ,此時因為應用程序所需的全部內容都是從庫中復制了出來,所以靜態庫本身并不需要與可執行文件一起發行。
在動態庫的情況下,有兩個文件,一個是引入庫( .LIB )文件,一個是 DLL 文件,引入庫文件包含被 DLL 導出的函數的名稱和位置, DLL 包含實際的函數和數據,應用程序使用 LIB 文件鏈接到所需要使用的 DLL 文件,庫中的函數和數據并不復制到可執行文件中,因此在應用程序的可執行文件中,存放的不是被調用的函數代碼,而是 DLL 中所要調用的函數的內存地址,這樣當一個或多個應用程序運行是再把程序代碼和被調用的函數代碼鏈接起來,從而節省了內存資源。從上面的說明可以看出, DLL 和 .LIB 文件必須隨應用程序一起發行,否則應用程序將會產生錯誤。
微軟的 Visual C++ 支持三種 DLL ,它們分別是 Non-MFC Dll (非 MFC 動態庫)、 Regular Dll (常規 DLL )、 Extension Dll (擴展 DLL )。 Non-MFC DLL 指的是不用 MFC 的類庫結構,直接用 C 語言寫的 DLL ,其導出的函數是標準的 C 接口,能被非 MFC 或 MFC 編寫的應用程序所調用。 Regular DLL: 和下述的 Extension Dlls 一樣,是用 MFC 類庫編寫的,它的一個明顯的特點是在源文件里有一個繼承 CWinApp 的類(注意:此類 DLL 雖然從 CWinApp 派生,但沒有消息循環) , 被導出的函數是 C 函數、 C++ 類或者 C++ 成員函數(注意不要把術語 C++ 類與 MFC 的微軟基礎 C++ 類相混淆),調用常規 DLL 的應用程序不必是 MFC 應用程序,只要是能調用類 C 函數的應用程序就可以,它們可以是在 Visual C++ 、 Dephi 、 Visual Basic 、 Borland C 等編譯環境下利用 DLL 開發應用程序。
常規 DLL 又可細分成靜態鏈接到 MFC 和動態鏈接到 MFC 上的,這兩種常規 DLL 的區別將在下面介紹。與常規 DLL 相比,使用擴展 DLL 用于導出增強 MFC 基礎類的函數或子類,用這種類型的動態鏈接庫,可以用來輸出一個從 MFC 所繼承下來的類。
擴展 DLL 是使用 MFC 的動態鏈接版本所創建的,并且它只被用 MFC 類庫所編寫的應用程序所調用。例如你已經創建了一個從 MFC 的 CtoolBar 類的派生類用于創建一個新的工具欄,為了導出這個類,你必須把它放到一個 MFC 擴展的 DLL 中。擴展 DLL 和常規 DLL 不一樣,它沒有一個從 CWinApp 繼承而來的類的對象,所以,開發人員必須在 DLL 中的 DllMain 函數添加初始化代碼和結束代碼。
三、動態鏈接庫的創建
在 Visual C++6.0 開發環境下,打開 FileNewProject 選項,可以選擇 Win32 Dynamic-Link Library 或 MFC AppWizard[dll] 來以不同的方式來創建 Non-MFC Dll 、 Regular Dll 、 Extension Dll 等不同種類的動態鏈接庫。
1 . Win32 Dynamic-Link Library 方式創建 Non-MFC DLL 動態鏈接庫
每一個 DLL 必須有一個入口點,這就象我們用 C 編寫的應用程序一樣,必須有一個 WINMAIN 函數一樣。在 Non-MFC DLL 中 DllMain 是一個缺省的入口函數,你不需要編寫自己的 DLL 入口函數,用這個缺省的入口函數就能使動態鏈接庫被調用時得到正確的初始化。如果應用程序的 DLL 需要分配額外的內存或資源時,或者說需要對每個進程或線程初始化和清除操作時,需要在相應的 DLL 工程的 .CPP 文件中對 DllMain() 函數按照下面的格式書寫。
BOOL APIENTRY DllMain(HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved) |
參數中, hMoudle 是動態庫被調用時所傳遞來的一個指向自己的句柄 ( 實際上,它是指向 _DGROUP 段的一個選擇符 ) ; ul_reason_for_call 是一個說明動態庫被調原因的標志,當進程或線程裝入或卸載動態鏈接庫的時候,操作系統調用入口函數,并說明動態鏈接庫被調用的原因,它所有的可能值為: DLL_PROCESS_ATTACH: 進程被調用、 DLL_THREAD_ATTACH: 線程被調用、 DLL_PROCESS_DETACH: 進程被停止、 DLL_THREAD_DETACH: 線程被停止; lpReserved 為保留參數。到此為止, DLL 的入口函數已經寫了,剩下部分的實現也不難,你可以在 DLL 工程中加入你所想要輸出的函數或變量了。
我們已經知道 DLL 是包含若干個函數的庫文件,應用程序使用 DLL 中的函數之前,應該先導出這些函數,以便供給應用程序使用。要導出這些函數有兩種方法,一是在定義函數時使用導出關鍵字 _declspec(dllexport) ,另外一種方法是在創建 DLL 文件時使用模塊定義文件 .Def 。需要讀者注意的是在使用第一種方法的時候,不能使用 DEF 文件。下面通過兩個例子來說明如何使用這兩種方法創建 DLL 文件。
1 )使用導出函數關鍵字 _declspec(dllexport) 創建 MyDll.dll ,該動態鏈接庫中有兩個函數,分別用來實現得到兩個數的最大和最小數。在 MyDll.h 和 MyDLL.cpp 文件中分別輸入如下原代碼:
//MyDLL.h |
該動態鏈接庫編譯成功后,打開 MyDll 工程中的 debug 目錄,可以看到 MyDll.dll 、 MyDll.lib 兩個文件。 LIB 文件中包含 DLL 文件名和 DLL 文件中的函數名等,該 LIB 文件只是對應該 DLL 文件的 " 映像文件 " ,與 DLL 文件中, LIB 文件的長度要小的多,在進行隱式鏈接 DLL 時要用到它。讀者可能已經注意到在 MyDll.h 中有關鍵字 "extern C" ,它可以使其他編程語言訪問你編寫的 DLL 中的函數。
2 )用 .def 文件創建工程 MyDll
為了用 .def 文件創建 DLL ,請先刪除上個例子創建的工程中的 MyDll.h 文件,保留 MyDll.cpp 并在該文件頭刪除 #i nclude MyDll.h 語句,同時往該工程中加入一個文本文件,命名為 MyDll.def ,再在該文件中加入如下代碼:
LIBRARY MyDll
EXPORTS
Max
Min
其中 LIBRARY 語句說明該 def 文件是屬于相應 DLL 的, EXPORTS 語句下列出要導出的函數名稱。我們可以在 .def 文件中的導出函數后加 @n ,如 Max@1 , Min@2 ,表示要導出的函數順序號,在進行顯式連時可以用到它。該 DLL 編譯成功后,打開工程中的 Debug 目錄,同樣也會看到 MyDll.dll 和 MyDll.lib 文件。
2 . MFC AppWizard[dll] 方式生成常規 / 擴展 DLL
在 MFC AppWizard[dll] 下生成 DLL 文件又有三種方式,在創建 DLL 是,要根據實際情況選擇創建 DLL 的方式。一種是常規 DLL 靜態鏈接到 MFC ,另一種是常規 DLL 動態鏈接到 MFC 。兩者的區別是:前者使用的是 MFC 的靜態鏈接庫,生成的 DLL 文件長度大,一般不使用這種方式,后者使用 MFC 的動態鏈接庫,生成的 DLL 文件長度小;動態鏈接到 MFC 的規則 DLL 所有輸出的函數應該以如下語句開始:
AFX_MANAGE_STATE(AfxGetStaticModuleState( )) // 此語句用來正確地切換 MFC 模塊狀態 |
最后一種是 MFC 擴展 DLL ,這種 DLL 特點是用來建立 MFC 的派生類, Dll 只被用 MFC 類庫所編寫的應用程序所調用。前面我們已經介紹過, Extension DLLs 和 Regular DLLs 不一樣,它沒有一個從 CWinApp 繼承而來的類的對象,編譯器默認了一個 DLL 入口函數 DLLMain() 作為對 DLL 的初始化,你可以在此函數中實現初始化 , 代碼如下:
BOOL WINAPI APIENTRY DLLMain(HINSTANCE hinstDll , DWORD reason , LPVOID flmpload) |
參數 hinstDll 存放 DLL 的句柄,參數 reason 指明調用函數的原因, lpReserved 是一個被系統所保留的參數。對于隱式鏈接是一個非零值,對于顯式鏈接值是零。
在 MFC 下建立 DLL 文件,會自動生成 def 文件框架,其它與建立傳統的 Non-MFC DLL 沒有什么區別,只要在相應的頭文件寫入關鍵字 _declspec(dllexport) 函數類型和函數名等,或在生成的 def 文件中 EXPORTS 下輸入函數名就可以了。需要注意的是在向其它開發人員分發 MFC 擴展 DLL 時,不要忘記提供描述 DLL 中類的頭文件以及相應的 .LIB 文件和 DLL 本身,此后開發人員就能充分利用你開發的擴展 DLL 了。
應用程序使用DLL可以采用兩種方式:一種是隱式鏈接,另一種是顯式鏈接。在使用DLL之前首先要知道DLL中函數的結構信息。Visual C++6.0在VCin目錄下提供了一個名為Dumpbin.exe的小程序,用它可以查看DLL文件中的函數結構。另外,Windows系統將遵循下面的搜索順序來定位DLL: 1.包含EXE文件的目錄,2.進程的當前工作目錄, 3.Windows系統目錄, 4.Windows目錄,5.列在Path環境變量中的一系列目錄。
1.隱式鏈接
隱式鏈接就是在程序開始執行時就將DLL文件加載到應用程序當中。實現隱式鏈接很容易,只要將導入函數關鍵字_declspec(dllimport)函數名等寫到應用程序相應的頭文件中就可以了。下面的例子通過隱式鏈接調用MyDll.dll庫中的Min函數。首先生成一個項目為TestDll,在DllTest.h、DllTest.cpp文件中分別輸入如下代碼:
//Dlltest.h |
在創建 DllTest.exe 文件之前,要先將 MyDll.dll 和 MyDll.lib 拷貝到當前工程所在的目錄下面,也可以拷貝到 windows 的 System 目錄下。如果 DLL 使用的是 def 文件,要刪除 TestDll.h 文件中關鍵字 extern "C" 。 TestDll.h 文件中的關鍵字 Progam commit 是要 Visual C+ 的編譯器在 link 時,鏈接到 MyDll.lib 文件,當然,開發人員也可以不使用 #pragma comment(lib , "MyDll.lib") 語句,而直接在工程的 Setting->Link 頁的 Object/Moduls 欄填入 MyDll.lib 既可。
2 .顯式鏈接
顯式鏈接是應用程序在執行過程中隨時可以加載 DLL 文件,也可以隨時卸載 DLL 文件,這是隱式鏈接所無法作到的,所以顯式鏈接具有更好的靈活性,對于解釋性語言更為合適。不過實現顯式鏈接要麻煩一些。在應用程序中用 LoadLibrary 或 MFC 提供的 AfxLoadLibrary 顯式的將自己所做的動態鏈接庫調進來,動態鏈接庫的文件名即是上述兩個函數的參數,此后再用 GetProcAddress() 獲取想要引入的函數。自此,你就可以象使用如同在應用程序自定義的函數一樣來調用此引入函數了。在應用程序退出之前,應該用 FreeLibrary 或 MFC 提供的 AfxFreeLibrary 釋放動態鏈接庫。下面是通過顯式鏈接調用 DLL 中的 Max 函數的例子。
#i nclude |
在上例中使用類型定義關鍵字 typedef ,定義指向和 DLL 中相同的函數原型指針,然后通過 LoadLibray() 將 DLL 加載到當前的應用程序中并返回當前 DLL 文件的句柄,然后通過 GetProcAddress() 函數獲取導入到應用程序中的函數指針,函數調用完畢后,使用 FreeLibrary() 卸載 DLL 文件。在編譯程序之前,首先要將 DLL 文件拷貝到工程所在的目錄或 Windows 系統目錄下。
使用顯式鏈接應用程序編譯時不需要使用相應的 Lib 文件。另外,使用 GetProcAddress() 函數時,可以利用 MAKEINTRESOURCE() 函數直接使用 DLL 中函數出現的順序號,如將 GetProcAddress(hDLL,"Min") 改為 GetProcAddress(hDLL, MAKEINTRESOURCE(2)) (函數 Min() 在 DLL 中的順序號是 2 ),這樣調用 DLL 中的函數速度很快,但是要記住函數的使用序號,否則會發生錯誤。