在 Windows 早期開發環境中,靜態連結庫 (.lib) 是程式碼重用的主要方式。連結器會將所需的函式碼直接複製進可執行檔 (.exe) 中。雖然這解決了重用問題,但隨著 Windows 多任務環境的普及,缺點逐漸暴露:每個程式都包含一份相同的系統函式副本,導致記憶體嚴重浪費,執行檔體積龐大。
動態連結庫 (DLL) 應運而生。它允許多個執行中的程式共享記憶體中同一份 DLL 程式碼,實現真正的程式碼共享與資源複用。本文將深入探討 DLL 的工作原理、Windows 系統 DLL 架構,並以 Delphi (Object Pascal) 為例,詳細說明自訂 DLL 的編寫、匯出、靜態/動態載入及進階技巧。
傳統靜態連結在「連結階段」就解析所有函式位址;而 DLL 採用「執行階段動態連結」。當應用程式啟動時,作業系統載入器只載入 DLL 檔案,不解析內部函式。只有當程式真正呼叫 DLL 內的函式時,系統才在 DLL 中尋找該函式入口點並傳回位址。這種「按需載入、共享單一副本」的機制,極大地降低了記憶體佔用與磁碟空間。
DLL 另一大優勢在於維護性:修復 DLL 中的 Bug 或更新功能,只需替換 DLL 檔案,所有呼叫端程式無需重新編譯即可受益。此外,DLL 不僅共享程式碼,還提供「共享資料區」機制,讓多個進程透過記憶體對應檔案共享資料塊,這是進程間通訊 (IPC) 的高效手段之一。
Windows 核心本身即建構於大量 DLL 之上:KERNEL32.DLL (核心服務)、USER32.DLL (使用者介面)、GDI32.DLL (圖形裝置介面) 等。Delphi 透過單元 (Windows.pas 等) 封裝了這些 API,開發者鮮少直接處理 DLL 載入細節。本文重點聚焦於使用者自訂 DLL 的開發與調用。
Delphi 專案原始碼 (.dpr) 的關鍵字決定輸出類型:
program ProjectName; → 產生 .exelibrary ProjectName; → 產生 .dllDLL 專案必須包含 exports 子句,明確宣告對外公開的函式/程序名稱。
File > New > DLL Wizard)。export 指示詞 (或 stdcall; export)。exports 區塊列出匯出名稱,可搭配 name (別名)、index (序數)、resident (常駐記憶體) 指示詞優化呼叫效能。begin...end. 區塊),處理全域變數初始化、記憶體配置等。.dll 檔。每個 DLL 擁有獨立的資料段 (DS),全域變數預設為私有。呼叫端無法直接存取 DLL 變數,必須透過匯出函式介面。DLL 無獨立堆疊段 (SS),共用呼叫端進程的堆疊,因此嚴禁在 DLL 內假設 DS = SS (Delphi 編譯器保證不會產生此類代碼)。
DLL 發生執行期錯誤時,不會自動卸載 (因可能被其他進程使用),但會導致呼叫端異常終止。此時舊版 DLL 可能仍駐留記憶體,造成重測試失效。解決方案:
1. 呼叫端捕獲異常後顯式呼叫 FreeLibrary 卸載。
2. 重啟 Windows (極端情況)。
最佳實務:DLL 內部務必嚴謹防禦性程式設計,盡量不拋出異常跨越 DLL 邊界。
Delphi 以 begin...end. 區塊取代傳統 LibMain/WEP。特點:
- 可選性:系統自動處理資料段解鎖等基礎工作。
- 多重退出程序:可註冊多個 ExitProc,按序執行清理。
- 透明化:系統自動在 DLL_PROCESS_ATTACH/DETACH 時呼叫。
library StrUtilsDLL;
uses
SysUtils, Classes;
{ 回傳字元在字串中首次出現的索引 (0-based),找不到回傳 -1 }
function InStr(SourceStr: PChar; Ch: Char): Integer; export;
var
Len, i: Integer;
begin
Len := StrLen(SourceStr);
for i := 0 to Len - 1 do
if SourceStr[i] = Ch then
begin
Result := i;
Exit;
end;
Result := -1;
end;
exports
InStr index 1 name 'MyInStr' resident;
begin
{ 選擇性初始化程式碼 }
end.
關鍵點:為跨語言相容 (C/C++, VB 等),字串參數統一使用 PChar (以空字元結尾的字元陣列),避免 Delphi 專有的 String 類型造成二進位不相容。
在呼叫端宣告 external 指示詞,連結器產生匯入表。程式啟動時系統自動載入 DLL。
function MyInStr(SourceStr: PChar; Ch: Char): Integer; stdcall; external 'StrUtilsDLL.dll';
優點:呼叫語法簡潔,如同本地函式。
缺點:DLL 缺失或版本不符會導致應用程式無法啟動;無法根據執行狀態動態決定載入哪個 DLL。
透過三大 API 完全掌控 DLL 生命週期:
| API 函式 | 用途 | 關鍵參數/回傳 |
|---|---|---|
LoadLibrary | 載入 DLL 至進程空間 | 檔名 (含路徑搜尋順序) → HINSTANCE 模組句柄或錯誤碼 (< 32) |
GetProcAddress | 取得匯出函式入口位址 | 模組句柄 + 函式名/序數 → FARPROC 函式指標 |
FreeLibrary | 遞減參考計數並卸載 | 模組句柄 → 計數歸零時真正卸載 |
type
TInStrFunc = function(Source: PChar; Ch: Char): Integer; stdcall;
procedure TForm1.Button1Click(Sender: TObject);
var
hMod: THandle;
InStrPtr: TInStrFunc;
Pos: Integer;
begin
hMod := LoadLibrary('StrUtilsDLL.dll');
if hMod > 32 then
try
@InStrPtr := GetProcAddress(hMod, 'MyInStr'); // 使用 exports 定義的別名
if Assigned(InStrPtr) then
begin
Pos := InStrPtr(PChar(Edit1.Text), Edit2.Text[1]);
if Pos = -1 then
Label1.Caption := '未找到該字元'
else
Label1.Caption := Format('位於第 %d 位', [Pos + 1]);
end;
finally
FreeLibrary(hMod); // 必須配對釋放
end
else
ShowMessage('載入 DLL 失敗,錯誤碼: ' + IntToStr(hMod));
end;
進階提示:使用序數 (index) 呼叫 GetProcAddress 速度較快,但需確保 DLL 版本穩定;使用函式名稱相容性更佳。
stdcall 呼叫慣例、PChar/基本類型參數、避免傳遞 Delphi 專有類型 (String, TObject, 介面等)。FreeMem。TCriticalSection) 保護;考慮 DLL_PROCESS_ATTACH/DETACH 及 DLL_THREAD_ATTACH/DETACH 初始化。name 別名維持二進位相容;資源檔 (VERSIONINFO) 記錄版本資訊。DLL 是 Windows 模組化開發的基石。掌握其動態連結原理、記憶體模型、匯出/呼叫機制,並遵循跨語言介面規範,即可構建高效、可維護、可擴充的元件化應用系統。Delphi 強大的 library 支援讓 DLL 開發變得直觀高效,配合動態載入技術,更能實現外掛架構、熱更新等進階應用場景。