外部 DLL 函數調用兩大模式:Static Load (靜態) vs Dynamic Load (動態)
Delphi 調用外部 DLL 函數有兩種根本不同的方式:靜態加載 (編譯期綁定,聲明簡單) 和 動態加載 (運行期綁定,靈活控制)。理解它們的語法、生命週期管理、錯誤處理差異,能根據場景選擇最適合的方案。
模式 1:靜態加載 (Static Load)
// 最簡單聲明:直接在單元級聲明
procedure DoSomething; external 'MYLIB.DLL';
// 帶調用慣例與重命名
function MyFunc(X, Y: Integer): Integer; stdcall; external 'MYLIB.DLL' name 'RealFuncName';
// 帶參數的完整聲明
procedure ProcessData(const Input: PChar; var Output: PChar; Len: Integer); cdecl; external 'DATA.DLL';
特點
- 優點:語法簡潔、像呼叫普通函數、編譯器檢查參數類型
- 缺點:啟動時必須找到 DLL、缺失即報錯無法啟動、無法延遲加載、無法根據條件選擇 DLL 版本
- 適用場景:核心依賴、必須存在的系統 DLL、簡單工具函數
模式 2:動態加載 (Dynamic Load)
uses Windows;
type
// 1. 定義函數指標類型 (必須匹配 DLL 導出簽名)
TTimeRec = record
Second: Integer;
Minute: Integer;
Hour: Integer;
end;
TGetTime = procedure(var Time: TTimeRec); // 函數指標類型
var
Time: TTimeRec;
Handle: THandle; // HMODULE
GetTime: TGetTime; // 函數指標變數
begin
// 2. 載入 DLL
Handle := LoadLibrary('MYLIB.DLL'); // 返回 0 表示失敗
if Handle <> 0 then
try
// 3. 獲取函數地址
@GetTime := GetProcAddress(Handle, 'GetTime');
if @GetTime <> nil then
begin
// 4. 調用函數
GetTime(Time);
WriteLn('The time is ', Time.Hour, ':', Time.Minute, ':', Time.Second);
end
else
ShowMessage('函數 GetTime 未找到');
finally
// 5. 釋放 DLL (引用計數歸零時真正卸載)
FreeLibrary(Handle);
end
else
ShowMessage('無法載入 MYLIB.DLL: ' + SysErrorMessage(GetLastError));
end;
特點
- 優點:完全控制載入時機、失敗可優雅處理、支援插件架構、可根據條件載入不同版本、減少啟動依賴
- 缺點:代碼較繁瑣、需手動定義函數指標類型、參數類型安全靠開發者保證
- 適用場景:插件系統、可選功能模組、多版本 DLL 兼容、延遲加載優化啟動速度
關鍵 API 對照表
| 操作 | API | 說明 |
| 載入 DLL | LoadLibrary / LoadLibraryEx | 返回 HMODULE (THandle),失敗返回 0 |
| 獲取函數地址 | GetProcAddress | 返回 FARPROC,需轉型為函數指標類型 |
| 釋放 DLL | FreeLibrary | 引用計數 -1,歸零時卸載 |
| 獲取錯誤碼 | GetLastError | LoadLibrary/GetProcAddress 失敗後調用 |
動態加載完整封裝模式 (推薦)
type
TDynamicDLL = class
private
FHandle: THandle;
function GetProc(const Name: string): Pointer;
public
constructor Create(const DLLName: string);
destructor Destroy; override;
function LoadFunction(const Name: string; var ProcPtr): Boolean;
property Handle: THandle read FHandle;
end;
constructor TDynamicDLL.Create(const DLLName: string);
begin
FHandle := LoadLibrary(PChar(DLLName));
if FHandle = 0 then
RaiseLastOSError;
end;
destructor TDynamicDLL.Destroy;
begin
if FHandle <> 0 then FreeLibrary(FHandle);
inherited;
end;
function TDynamicDLL.GetProc(const Name: string): Pointer;
begin
Result := GetProcAddress(FHandle, PAnsiChar(Name));
if Result = nil then RaiseLastOSError;
end;
function TDynamicDLL.LoadFunction(const Name: string; var ProcPtr): Boolean;
begin
Pointer(ProcPtr) := GetProc(Name);
Result := ProcPtr <> nil;
end;
// 使用
var
DLL: TDynamicDLL;
MyFunc: function(X: Integer): Integer; stdcall;
begin
DLL := TDynamicDLL.Create('MYLIB.DLL');
try
if DLL.LoadFunction('MyFunc', @MyFunc) then
ShowMessage(IntToStr(MyFunc(123)));
finally
DLL.Free;
end;
end;
調用慣例與參數傳遞對照
| 慣例 | 參數順序 | 堆棧清理 | 寄存器傳參 | Delphi 指令字 |
| register | 左→右 | 被調用者 | 是 (EAX,EDX,ECX) | 預設 |
| pascal | 左→右 | 被調用者 | 否 | pascal |
| cdecl | 右→左 | 調用者 | 否 | cdecl |
| stdcall | 右→左 | 被調用者 | 否 | stdcall (WinAPI 標準) |
| safecall | 右→左 | 被調用者 | 否 | safecall (COM/自動異常) |
常見陷阱與避坑
- 靜態加載 DLL 缺失導致應用無法啟動:改用動態加載或安裝檢查
- 動態加載函數指標類型不匹配:導致堆棧破壞、隨機崩潰,必須精確對應 DLL 導出簽名
- 調用慣例不匹配:stdcall vs cdecl 導致參數錯位、堆棧不平衡
- 字串參數傳遞:DLL 為 ANSI 還是 Unicode,Delphi 版本差異,建議用
PAnsiChar/PWideChar 顯式指定
- FreeLibrary 過早調用:仍在使用函數指標時釋放 DLL → 訪問違規
- 線程安全:DLL 內部全局狀態非線程安全,需同步或每線程載入
選型決策樹
DLL 是系統核心/必須存在?
├─ 是 → 靜態加載 (簡單、類型安全)
└─ 否 → 需要插件/可選/多版本/延遲加載?
├─ 是 → 動態加載 (靈活、可控)
└─ 否 → 靜態加載 (開發效率優先)
掌握靜態與動態加載的語法細節、生命週期管理、錯誤處理模式,能讓 Delphi 與外部 DLL 互操作既安全又靈活。
- 上一篇文章: Delphi TCanvas 類成員完整清單:方法、函數、屬性、事件
- 下一篇文章: Delphi 類成員深度解析:字段、方法、屬性與事件
Copyright ©
程式碼之家 All Rights Reserved