天天看點

Delphi環境中編寫調用DLL的方法和技巧 【轉】

第一章 為什麼要使用動态連結庫(DLL) top

提起DLL您一定不會陌生,在Windows中有着大量的以DLL為字尾的檔案,它們是保證Windows正常運作和維護更新的重要保證。(舉個例子,筆者的Win95 System目錄下盡有500多個DLL檔案。)其實,DLL是一種特殊的可執行檔案。說它特殊主要是因為一般它都不能直接運作,需要宿主程式比如*.EXE程式或其他DLL的動态調用才能夠使用。簡單的說,在通常情況下DLL是經過編譯的函數和過程的集合。

使用DLL技術主要有以下幾個原因:

一、減小可執行檔案大小。

DLL技術的産生有很大一部分原因是為了減小可執行檔案的大小。當作業系統進入Windows時代後,其大小已經達到幾十兆乃至幾百兆。試想如果還是使用DOS時代的單執行檔案體系的話一個可執行檔案的大小可能将達到數十兆,這是大家都不能接受的。解決的方法就是采用動态連結技術将一個大的可執行檔案分割成許多小的可執行程式。

二、實作資源共享。

這裡指的資源共享包括很多方面,最多的是記憶體共享、代碼共享等等。早期的程式員經常碰到這樣的事情,在不同的程式設計任務中編寫同樣的代碼。這種方法顯然浪費了很多時間,為了解決這個問題人們編寫了各種各樣的庫。但由于程式設計語言和環境的不同這些庫一般都不能通用,而且使用者在運作程式時還需要這些庫才行,極不友善。DLL的出現就像制定了一個标準一樣,使這些庫有了統一的規範。這樣一來,用不同程式設計語言的程式員可以友善的使用用别的程式設計語言編寫的DLL。另外,DLL還有一個突出的特點就是在記憶體中隻裝載一次,這一點可以節省有限的記憶體,而且可以同時為多個程序服務。

三、便于維護和更新。

細心的朋友可能發現有一些DLL檔案是有版本說明的。(檢視DLL檔案的屬性可以看到,但不是每一個DLL檔案都有)這是為了便于維護和更新。舉個例子吧,早期的Win95中有一個BUG那就是在閏年不能正确顯示2月29日這一天。後來,Microsoft釋出了一個更新檔程式糾正了這個BUG。值得一提的是,我們并沒有重裝Win95,而是用新版本的DLL代替了舊版本的DLL。(具體是哪一個DLL檔案筆者一時想不起來了。)另一個常見的例子是驅動程式的更新。例如,著名的DirectX就多次更新,現在已經發展到了6.0版了。更妙的是,當我們試圖安裝較低版本的DLL時,系統會給我們提示,避免人為的操作錯誤。例如我們更新某硬體的驅動程式時,經常碰到Windows提示我們目前安裝的驅動程式比原來的驅動程式舊。

四、比較安全。

這裡說的安全也包括很多方面。比如,DLL檔案遭受病毒的侵害機率要比普通的EXE檔案低很多。另外,由于是動态連結的,這給一些從事破壞工作的“高手”們多少帶來了一些反彙編的困難。

第二章 在Delphi中編寫DLL top

注意:在這裡筆者假定讀者使用的是Delphi 3或Delphi 4開場白說了那麼多,總該言歸正傳了。編寫DLL其實也不是一件十分困難的事,隻是要注意一些事項就夠了。為便于說明,我們先舉一個例子。

library Delphi;

uses

SysUtils,

Classes;

function TestDll(i:integer):integer;stdcall;

begin

Result:=i;

end;

exports

TestDll;

begin

end.

上面的例子是不是很簡單?熟悉Delphi的朋友可以看出以上代碼和一般的Delphi程式的編寫基本是相同的,隻是在TestDll函數後多了一個stdcall參數并且用exports語句聲明了TestDll函數。隻要編譯上面的代碼,就可以得到一個名為Delphi.dll的動态連結庫。現在,讓我們來看看有哪些需要注意的地方。 一、在DLL中編寫的函數或過程都必須加上stdcall調用參數。在Delphi 1或Delphi 2環境下該調用參數是far。從Delphi 3以後将這個參數變為了stdcall,目的是為了使用标準的Win32參數傳遞技術來代替優化的register參數。忘記使用stdcall參數是常見的錯誤,這個錯誤不會影響DLL的編譯和生成,但當調用這個DLL時會發生很嚴重的錯誤,導緻作業系統的死鎖。原因是register參數是Delphi的預設參數。

二、所寫的函數和過程應該用exports語句聲明為外部函數。

正如大家看到的,TestDll函數被聲明為一個外部函數。這樣做可以使該函數在外部就能看到,具體方法是單激滑鼠右鍵用“快速檢視(Quick View)”功能檢視該DLL檔案。(如果沒有“快速檢視”選項可以從Windows CD上安裝。)TestDll函數會出現在Export Table欄中。另一個很充分的理由是,如果不這樣聲明,我們編寫的函數将不能被調用,這是大家都不願看到的。

三、當使用了長字元串類型的參數、變量時要引用ShareMem。

Delphi中的string類型很強大,我們知道普通的字元串長度最大為256個字元,但Delphi中string類型在預設情況下長度可以達到2G。(對,您沒有看錯,确實是兩兆。)這時,如果您堅持要使用string類型的參數、變量甚至是記錄資訊時,就要引用ShareMem單元,而且必須是第一個引用的。既在uses語句後是第一個引用的單元。如下例:

uses

ShareMem,

SysUtils,

Classes;

還有一點,在您的工程檔案(*.dpr)中而不是單元檔案(*.pas)中也要做同樣的工作,這一點Delphi自帶的幫助檔案沒有說清楚,造成了很多誤會。不這樣做的話,您很有可能付出當機的代價。避免使用string類型的方法是将string類型的參數、變量等聲明為Pchar或ShortString(如:s:string[10])類型。同樣的問題會出現在當您使用了動态數組時,解決的方法同上所述。

第三章 在Delphi中靜态調用DLL top

調用一個DLL比寫一個DLL要容易一些。首先給大家介紹的是靜态調用方法,稍後将介紹動态調用方法,并就兩種方法做一個比較。同樣的,我們先舉一個靜态調用的例子。

unit Unit1;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics,

Controls, Forms, Dialogs, StdCtrls;

type

TForm1 = class(TForm)

Edit1: TEdit;

Button1: TButton;

procedure Button1Click(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

//本行以下代碼為我們真正動手寫的代碼

function TestDll(i:integer):integer;stdcall;

external ’Delphi.dll’;

procedure TForm1.Button1Click(Sender: TObject);

begin

Edit1.Text:=IntToStr(TestDll(1));

end;

end.

上面的例子中我們在窗體上放置了一個編輯框(Edit)和一個按鈕(Button),并且書寫了很少的代碼來測試我們剛剛編寫的Delphi.dll。大家可以看到我們唯一做的工作是将TestDll函數的說明部分放在了implementation中,并且用external語句指定了Delphi.dll的位置。(本例中調用程式和Delphi.dll在同一個目錄中。)讓人興奮的是,我們自己編寫的TestDll函數很快被Delphi認出來了。您可做這樣一個實驗:輸入“TestDll(”,很快Delphi就會用fly-by提示條提示您應該輸入的參數是什麼,就像我們使用Delphi中定義的其他函數一樣簡單。注意事項有以

下一些:

一、調用參數用stdcall。

和前面提到的一樣,當引用DLL中的函數和過程時也要使用stdcall參數,原因和前面提到的一樣。

二、用external語句指定被調用的DLL檔案的路徑和名稱。

正如大家看到的,我們在external語句中指定了所要調用的DLL檔案的名稱。沒有寫路徑是因為該DLL檔案和調用它的主程式在同一目錄下。如果該DLL檔案在C:\,則我們可将上面的引用語句寫為external ’C:\Delphi.dll’。注意檔案的字尾.dll必須寫上。

三、不能從DLL中調用全局變量。

如果我們在DLL中聲明了某種全局變量,如:var s:byte 。這樣在DLL中s這個全局變量是可以正常使用的,但s不能被調用程式使用,既s不能作為全局變量傳遞給調用程式。不過在調用程式中聲明的變量可以作為參數傳遞給DLL。

四、被調用的DLL必須存在。

這一點很重要,使用靜态調用方法時要求所調用的DLL檔案以及要調用的函數或過程等等必須存在。如果不存在或指定的路徑和檔案名不正确的話,運作主程式時系統會提示“啟動程式時出錯”或“找不到*.dll檔案”等運作錯誤。

第四章 在Delphi中動态調用DLL top

動态調用DLL相對複雜很多,但非常靈活。為了全面的說明該問題,這次我們舉一個調用由C++編寫的DLL的例子。首先在C++中編譯下面的DLL源程式。

#include

extern ”C” _declspec(dllexport)

int WINAPI TestC(int i)

{

return i;

}

編譯後生成一個DLL檔案,在這裡我們稱該檔案為Cpp.dll,該DLL中隻有一個傳回整數類型的函數TestC。為了友善說明,我們仍然引用上面的調用程式,隻是将原來的Button1Click過程中的語句用下面的代碼替換掉了。

procedure TForm1.Button1Click(Sender: TObject);

type

TIntFunc=function(i:integer):integer;stdcall;

var

Th:Thandle;

Tf:TIntFunc;

Tp:TFarProc;

begin

Th:=LoadLibrary(’Cpp.dll’); {裝載DLL}

if Th>0 then

try

Tp:=GetProcAddress(Th,PChar(’TestC’));

if Tp<>nil

then begin

Tf:=TIntFunc(Tp);

Edit1.Text:=IntToStr(Tf(1)); {調用TestC函數}

end

else

ShowMessage(’TestC函數沒有找到’);

finally

FreeLibrary(Th); {釋放DLL}

end

else

ShowMessage(’Cpp.dll沒有找到’);

end;

大家已經看到了,這種動态調用技術很複雜,但隻要修改參數,如修改LoadLibrary(’Cpp.dll’)中的DLL名稱為’Delphi.dll’就可動态更改所調用的DLL。

一、定義所要調用的函數或過程的類型。

在上面的代碼中我們定義了一個TIntFunc類型,這是對應我們将要調用的函數TestC的。在其他調用情況下也要做同樣的定義工作。并且也要加上stdcall調用參數。

二、釋放所調用的DLL。

我們用LoadLibrary動态的調用了一個DLL,但要記住必須在使用完後手動地用FreeLibrary将該DLL釋放掉,否則該DLL将一直占用記憶體直到您退出Windows或關機為止。

現在我們來評價一下兩種調用DLL的方法的優缺點。靜态方法實作簡單,易于掌握并且一般來說稍微快一點,也更加安全可靠一些;但是靜态方法不能靈活地在運作時裝卸所需的DLL,而是在主程式開始運作時就裝載指定的DLL直到程式結束時才釋放該DLL,另外隻有基于編譯器和連結器的系統(如Delphi)才可以使用該方法。動态方法較好地解決了靜态方法中存在的不足,可以友善地通路DLL中的函數和過程,甚至一些老版本DLL中新添加的函數或過程;但動态方法難以完全掌握,使用時因為不同的函數或過程要定義很多很複雜的類型和調用方法。對于初學者,筆者建議您使用靜态方法,待熟練後再使用動态調用方法。

第五章 使用DLL的實用技巧 top

一、編寫技巧。

1 、為了保證DLL的正确性,可先編寫成普通的應用程式的一部分,調試無誤後再從主程式中分離出來,編譯成DLL。

2 、為了保證DLL的通用性,應該在自己編寫的DLL中杜絕出現可視化控件的名稱,如:Edit1.Text中的Edit1名稱;或者自定義非Windows定義的類型,如某種記錄。

3 、為便于調試,每個函數和過程應該盡可能短小精悍,并配合具體詳細的注釋。

4 、應多利用try-finally來處理可能出現的錯誤和異常,注意這時要引用SysUtils單元。

5 、盡可能少引用單元以減小DLL的大小,特别是不要引用可視化單元,如Dialogs單元。例如一般情況下,我們可以不引用Classes單元,這樣可使編譯後的DLL減小大約16Kb。

二、調用技巧。

1 、在用靜态方法時,可以給被調用的函數或過程更名。在前面提到的C++編寫的DLL例子中,如果去掉extern ”C”語句,C++會編譯出一些奇怪的函數名,原來的TestC函數會被命名為@TestC$s等等可笑的怪名字,這是由于C++采用了C++ name mangling技術。這個函數名在Delphi中是非法的,我們可以這樣解決這個問題:

改寫引用函數為

function TestC(i:integer):integer;stdcall;

external ’Cpp.dll’;name ’@TestC$s’;

其中name的作用就是重命名。

2 、可把我們編寫的DLL放到Windows目錄下或者Windows\system目錄下。這樣做可以在external語句中或LoadLibrary語句中不寫路徑而隻寫DLL的名稱。但這樣做有些不妥,這兩個目錄下有大量重要的系統DLL,如果您編的DLL與它們重名的話其後果簡直不堪設想,況且您的程式設計技術還不至于達到将自己編寫的DLL放到系統目錄中的地步吧!

三、調試技巧。

1 、我們知道DLL在編寫時是不能運作和單步調試的。有一個辦法可以,那就是在Run|parameters菜單中設定一個宿主程式。在Local頁的Host Application欄中添上宿主程式的名字就可進行單步調試、斷點觀察和運作了。

2 、添加DLL的版本資訊。開場白中提到了版本資訊對于DLL是很重要的,如果包含了版本資訊,DLL的大小會增加2Kb。增加這麼一點空間是值得的。很不幸我們如果直接使用Project|options菜單中Version選項是不行的,這一點Delphi的幫助檔案中沒有提到,經筆者研究發現,隻要加一行代碼就可以了。如下例:

library Delphi;

uses

SysUtils,

Classes;

{$R *.RES}

//注意,上面這行代碼必須加在這個位置

function TestDll(i:integer):integer;stdcall;

begin

Result:=i;

end;

exports

TestDll;

begin

end.

3 、為了避免與别的DLL重名,在給自己編寫的DLL起名字的時候最好采用字元數字和下劃線混合的方式。如:jl_try16.dll。

4 、如果您原來在Delphi 1或Delphi 2中已經編譯了某些DLL的話,您原來編譯的DLL是16位的。隻要将源代碼在新的Delphi 3或Delphi 4環境下重新編譯,就可以得到32位的DLL了。

[後記]:除了上面介紹的DLL最常用的使用方法外,DLL還可以用于做資源的載體。例如,在Windows中更改圖示就是使用的DLL中的資源。另外,熟練掌握了DLL的設計技術,對使用更為進階的OLE、COM以及ActiveX程式設計都有很多益處。

Delphi中如何調用DLL

馬上想得到的使用說明有以下幾點:

1. 所需動态連結的 DLL 須置放在與執行檔同一目錄或Windows System 目錄2. 确認 DLL export 出來的函式的原型, 以目前的情況而言, 通常隻拿得到 C語言的函數原型,這時要注意 C 與 object Pascal 相對應的型别, 如果需要, 在interface 一節定義所需的資料類别

3. 在 implementation 節中宣告欲使用的函式, 文法大緻如下:

procedure ProcName(Argu...); far; external ’DLL檔名’;

index n;

function FuncName(Argr...): DataType; far;

external ’DLL檔名’; index n;

宣告時, index n 如果不寫, 便是參考資料中所謂 import by name 的方式, 此時, 由於需要從 DLL 的 name table 中找出這個函式, 是以, 連結執行速度比import by ordinal稍慢一些, 此外, 還有一種 by new name, 由於我沒用過, 您可以查一參考資料, 大意是可以 import 後改用另一個程式命名呼叫這個函式

4. 然後, 呼叫與使用就與一般的Delphi 沒有兩樣5. 上述是直接寫到呼叫DLL函式的程式單元中, 此外,也可以将DLL的呼叫宣告集中到一個程式單元(Import unit), Delphi 内附的 WinTypes, WinProcs是一個例子,

您可以參考一下,同時觀察一下 C 與 Pascal 互相對應的資料型态6. 除了上述的 static import 的方式, 另外有一種 dynamic import 的寫法,先宣告一個程式類型(procedural-type),程式執行時, 以LoadLibrary() API Load進來後, 再以 GetProcAddress() API 取得函式的位址的方式來連結呼叫, 在ObjectPascal Language Guide P.132-133 有一個例子, 您可以參考看看

如果要舉個例子, 以下是從我以前的程式節錄出來的片斷:

(* for CWindows 3.1 *)

unit Ime31;

interface

uses

SysUtils, WinTypes, WinProcs, Dialogs;

type

(* 必要的資料型态宣告 *)

tDateNTime = record

wYear, wMonth, wDay: word;

wHour, wMin, wSec: word;

end;

TImePro = record

hWndIme: HWnd; { IME handle }

dtInstDate: tDateNTime; { Date and time of installation }

wVersion: word; { the version of IME }

szDescription: array[0..49] of byte; { Description of IME module}

szName: array[0..79] of byte; { Module name of the IME }

szOptions: array[0..29] of byte; { options of IME at startup}

fEnable: boolean; { IME status; True=activated,False=deactivated }

end;

pTImePro = ^TImePro;

function SetIme(const sImeFileName: string): boolean; far;

implementation

(* begin 呼叫 winnls.dll export 函數的宣告 *)

function ImpSetIme(hWndIme: HWND; lpImePro: pTImePro): boolean;far; external ’winnls.dll’;

(* end 呼叫 winnls.dll export 函數的宣告 *)

(* -------------------------------------------------- *)

(* SetIme(const sImeFileName: string): boolean;

(* ======

(* 切換到某一特定的輸入法

(*

(* 傳入引數:

(* sImeFileName: 輸入法 IME 檔名, 例: phon.ime;

(* 空字串: 英數輸入法

(*

(* 傳回值:

(* True: 切換成功

(* False: 失敗

(* -------------------------------------------------- *)

function SetIme(const sImeFileName: string): boolean;

var

pImePro: pTImePro;

begin

Result := False;

if MaxAvail < SizeOf(TImePro) then

begin

MessageDlg(’記憶體不足’, mtWarning, [mbOk], 0);

Exit;

end

else

begin

New(pImePro);

try

if sImeFileName = ’’ then (* 空字串, 還原到英數輸入法 *)

pImePro^.szName[0] := 0

else

StrPCopy(@pImePro^.szName, sImeFileName);

Result := ImpSetIme(0, pImePro); (* 呼叫 ImpSetIme *)

finally

Dispose(pImePro);

end; { of try }

end;

end; { of SetIme }

end.

;

轉載于:https://www.cnblogs.com/alina_yan/archive/2009/08/03/1537340.html

繼續閱讀