跳轉到內容

Windows 程式設計/動態連結庫

來自華夏公益教科書,開放的書籍,開放的世界
[編輯 | 編輯原始碼]

動態連結庫 (DLL) 是隨著微軟 Windows 作業系統的第一個版本引入的,如今是作業系統的一個基本結構元件。它們不僅存在於作業系統核心,而且是微軟建立的許多框架的一部分,如 MFC、ATL、.NET 等,甚至 C 和 C++ 執行時庫也作為 DLL 分發。

DLL 允許將某些程式碼片段編譯到一個庫中,並供多個程式連結。這意味著只需要存在一個庫副本,多個程式可以共享庫之間的函式和資料。Windows 系統透過提供程式設計師可以使用的 DLL 來使自身在使用者空間的幾個功能中變得可用。

DLL 與靜態庫的區別在於,當您編譯程式時,DLL 不會編譯到您的可執行檔案中,而是保持為一個單獨的模組。此功能有助於保持可執行檔案大小較小,並且還允許僅在需要時將 DLL 載入到記憶體中。此外,DLL 程式碼在多個不同的程序中共享。幾乎每個 Windows 可執行檔案都共享 kernel32.dll,許多檔案都共享 msvcrt.dll,即 Visual C 執行時。作為獨立的實體,DLL 還允許對系統和應用程式進行更新。只需用包含修復或改進的較新版本替換 DLL,即可輕鬆將更改立即擴充套件到多個依賴程式。

構建 DLL 檔案的具體方法取決於您使用的編譯器。但是,DLL 的程式設計方式是通用的。本章將討論如何編寫 DLL 檔案。

DLL 地獄

[編輯 | 編輯原始碼]

通常被稱為 "DLL 地獄" 的常見問題一直是 Windows 程式設計師的困擾,而且似乎在短期內沒有解決方案。這個問題是在 90 年代提出的,當時這個詞被創造出來。問題在於作業系統允許載入錯誤版本的 DLL 來響應應用程式的請求,這會導致崩潰。如今,應用程式將 simply refuse to run.

雖然穩定的 DLL 不會開啟地獄,但更改或升級 DLL 可能會使依賴於舊 DLL 的舊行為或未記錄行為的應用程式中的錯誤可見。通常,DLL 載入透過檔名解析。對於 Windows XP 及更高版本,所謂的應用程式清單(帶有 ID 24 的 XML 資源)參與更精細地解析 DLL 載入。此外,對於 64 位 Windows,檔案系統虛擬化存在以解決位數等效性問題。出於未知原因,Microsoft 決定在 32 位和 64 位之間保持 DLL 檔名相同。根本沒有 kernel64.dll,kernel32.dll 存在兩次,一個用於 32 位,一個用於 64 位。

__declspec

[編輯 | 編輯原始碼]

__declspec 關鍵字是一個奇怪的新關鍵字,它不是 ANSI C 標準的一部分,但大多數編譯器仍然會理解它。__declspec 允許指定各種非標準選項,這些選項將影響程式的執行方式。具體來說,我們想要討論兩個 __declspec 識別符號。

  • __declspec(dllexport)
  • __declspec(dllimport)

在編寫 DLL 時,我們需要使用 dllexport 關鍵字來表示將對其他程式可用的函式。沒有此關鍵字的函式只能從庫本身內部使用。以下是一個例子。

__declspec(dllexport) int MyFunc1(int foo)

在構建 DLL 時,函式的 __declspec 識別符號需要在函式原型和函式宣告中都指定。

要將 DLL 函式“匯入”到常規程式中,程式必須連結到 DLL,並且程式必須使用 dllimport 關鍵字原型化要匯入的函式,如下所示。

__declspec(dllimport) int MyFunc1(int foo);

現在,程式可以使用該函式,即使該函式存在於外部庫中。編譯器與 Windows 協同工作以處理所有細節。

許多人發現為他們的 DLL 定義一個頭檔案非常有用,而不是為構建 DLL 保持一個頭檔案,為匯入 DLL 保持一個頭檔案。以下是在 DLL 建立中常用的宏。

#ifdef BUILDING_DLL
#define DLL_FUNCTION __declspec(dllexport)
#else
#define DLL_FUNCTION __declspec(dllimport)
#endif

現在,要構建 DLL,我們需要定義 BUILDING_DLL 宏,而當我們匯入 DLL 時,不需要使用該宏。然後可以按如下方式原型化函式。

DLL_FUNCTION int MyFunc1(void);
DLL_FUNCTION int MyFunc2(void);
.......

(只是一個說明:微軟並不打算使用這種 __declspec 語法。相反,他們的意圖是在一個“匯出”檔案中宣告 DLL 的公共 API。然而,上面的語法儘管需要使用宏來回切換,但它更方便,而且幾乎被當今所有軟體使用)。

當 Windows 將 DLL 連結到程式時,Windows 會呼叫庫的 DllMain 函式。這意味著每個 DLL 都需要有一個 DllMain 函式。DllMain 函式需要按如下方式定義。

BOOL APIENTRY DllMain (HINSTANCE hInstance, DWORD reason, LPVOID reserved)

關鍵字“BOOL”、“APIENTRY”、“HINSTANCE”等都定義在 <windows.h> 中,因此即使您在庫中不使用任何 Win32 API 函式,也必須包含該檔案。

APIENTRYWINAPI 是表示 Windows API 呼叫約定的關鍵字。它們都被定義為 __stdcall。請記住,呼叫約定是函式簽名的一部分。變數“hInstance”是庫的 HINSTANCE 控制代碼,您可以保留它並使用它,也可以丟棄它。reason 將是四個不同值之一。

DLL_PROCESS_ATTACH
一個新程式剛剛第一次連結到庫。
DLL_PROCESS_DETACH
一個程式已解除連結庫。
DLL_THREAD_ATTACH
一個程式執行緒已連結到庫。
DLL_THREAD_DETACH
一個程式執行緒剛剛解除連結庫。

DllMain 函式不需要針對這些情況進行任何特殊操作,儘管某些庫會發現為每個與庫一起使用的執行緒或程序分配儲存很有用。

如果庫載入成功,DllMain 函式必須返回 TRUE,否則如果庫發生錯誤且無法載入,則返回 FALSE。無法成功開啟庫的應用程式應正常失敗。

以下是一個 DllMain 函式的一般模板。

BOOL APIENTRY DllMain (HINSTANCE hInst, DWORD reason, LPVOID lpReserved)
{
   switch (reason)
   {
     case DLL_PROCESS_ATTACH:
       break;
     case DLL_PROCESS_DETACH:
       break;
     case DLL_THREAD_ATTACH:
       break;
     case DLL_THREAD_DETACH:
       break;
   }
   return TRUE;
}

但是,如果您對任何原因都不感興趣,可以從程式中刪除整個 switch 語句並返回 TRUE。

連結 DLL

[編輯 | 編輯原始碼]

DLL 庫可以以兩種方式連結到可執行檔案:靜態方式和動態方式。

靜態連結到 DLL

[編輯 | 編輯原始碼]

在靜態連結到 DLL 時,連結器將完成所有工作,對於程式設計師來說,函式位於外部庫中是透明的。也就是說,如果庫編寫者在庫的標頭檔案中正確使用了 _DECLSPEC 修飾符,那麼它是透明的。

在編譯 DLL 時,編譯器將生成兩個檔案:DLL 庫檔案和一個靜態連結存根 .LIB 檔案。.LIB 檔案就像一個小型靜態庫,它告訴連結器靜態連結關聯的 DLL 檔案。在專案中使用 DLL 時,您可以向連結器提供 .LIB 存根檔案,或者某些連結器允許您直接指定 DLL(然後連結器將嘗試查詢 .LIB 檔案,甚至可能嘗試自動建立 .LIB 檔案)。

動態載入 DLL

[編輯 | 編輯原始碼]

DLL 檔案的真正強大之處在於它們可以在執行時動態載入到你的程式中。這意味著你的程式在執行時可以搜尋並載入新的元件,而無需重新編譯。這對於允許在執行時載入外掛和擴充套件的程式來說是一種必不可少的機制。要動態載入 DLL 檔案,你可以呼叫 **LoadLibrary** 函式獲取該庫的控制代碼,然後將該控制代碼傳遞給其他幾個函式之一以從 DLL 中檢索資料。LoadLibrary 的原型是

HMODULE WINAPI LoadLibrary(LPCTSTR lpFileName);

HMODULE 是程式模組的 HANDLE。lpFileName 是要載入的 DLL 的檔名。請記住,在載入模組時,系統首先會在你的 PATH 中檢查。如果你希望系統首先在其他指定的目錄中檢查,請先使用 **SetDllDirectory** 函式。

DLL 載入後,你擁有了該模組的控制代碼,就可以進行各種操作

  • 使用 **GetProcAddress** 返回指向該庫中函式的函式指標。
  • 使用 **LoadResource** 從 DLL 中檢索資源。

完成後,如果想要從記憶體中刪除 DLL 檔案,可以使用 DLL 的模組控制代碼呼叫 **FreeLibrary** 函式。

下一章

[編輯 | 編輯原始碼]
華夏公益教科書