基於 Visual C++6.0 的 DLL 編程實現
一、前言
自從微軟推出 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) { switch( ul_reason_for_call ) { case DLL_PROCESS_ATTACH: ....... case DLL_THREAD_ATTACH: ....... case DLL_THREAD_DETACH: ....... case DLL_PROCESS_DETACH: ....... } return TRUE; } |
參數中, 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 extern "C" _declspec(dllexport) int Max(int a, int b); extern "C" _declspec(dllexport) int Min(int a, int b); //MyDll.cpp #i nclude #i nclude"MyDll.h" int Max(int a, int b) { if(a>=b)return a; else return b; } int Min(int a, int b) { if(a>=b)return b; else return a; } |
該動態連結程式庫編譯成功後,開啟 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) { switch(reason) { ……………// 初始化代碼; } return true; } |
參數 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 #pragma comment(lib , "MyDll.lib") extern "C"_declspec(dllimport) int Max(int a,int b); extern "C"_declspec(dllimport) int Min(int a,int b); //TestDll.cpp #i nclude #i nclude"Dlltest.h" void main() {int a; a=min(8,10) printf(" 比較的結果為 %d " , a); } |
在建立 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 #i nclude void main(void) { typedef int(*pMax)(int a,int b); typedef int(*pMin)(int a,int b); HINSTANCE hDLL; PMax Max HDLL=LoadLibrary("MyDll.dll");// 載入動態連結程式庫 MyDll.DLL 檔案; Max=(pMax)GetProcAddress(hDLL,"Max"); A=Max(5,8); Printf(" 比較的結果為 %d " , a); FreeLibrary(hDLL);// 卸載 MyDll.DLL 檔案; } |
在上例中使用類型定義關鍵字 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 中的函數速度很快,但是要記住函數的使用序號,否則會發生錯誤。
原文地址