1.Windows的動態鏈接庫原理
動態鏈接庫(DLLs)是從C語言函數庫和Pascal庫單元的概念發展而來的。所有的C語言標准庫函數都存放在某一函數庫中,同時用戶也可以用LIB程序創建自己的函數庫。在鏈接應用程序的過程中,鏈接器從庫文件中拷貝程序調用的函數代碼,並把這些函數代碼添加到可執行文件中。這
種方法同只把函數儲存在已編譯的.OBJ文件中相比更有利於代碼的重用。
但隨著Windows這樣的多任務環境的出現,函數庫的方法顯得過於累贅。如果為了完成屏幕輸出、消息處理、內存管理、對話框等操作,每個程序都不得不擁有自己的函數,那麼Windows程序將變得非常龐大。Windows的發展要求允許同時運行的幾個程序共享一組函數的單一拷貝。動態
鏈接庫就是在這種情況下出現的。動態鏈接庫不用重復編譯或鏈接,一旦裝入內存,Dlls函數可以被系統中的任何正在運行的應用程序軟件所使用,而不必再將DLLs函數的另一拷貝裝入內存。
1.1 動態鏈接庫的工作原理
"動態鏈接"這幾字指明了DLLs是如何工作的。對於常規的函數庫,鏈接器從中拷貝它需要的所有庫函數,並把確切的函數地址傳送給調用這些函數的程序。而對於DLLs,函數儲存在一個獨立的動態鏈接庫文件中。在創建Windows程序時,鏈接過程並不把DLLs文件鏈接到程序上。直到程
序運行並調用一個DLLs中的函數時,該程序才要求這個函數的地址。此時Windows才在DLLs中尋找被調用函數,並把它的地址傳送給調用程序。采用這種方法,DLLs達到了復用代碼的極限。
動態鏈接庫的另一個方便之處是對動態鏈接庫中函數的修改可以自動傳播到所有調用它的程序中,而不必對程序作任何改動或處理。
DLLs不僅提供了函數重用的機制,而且提供了數據共享的機制。任何應用程序都可以共享由裝入內存的DLLs管理的內存資源塊。只包含共享數據的DLLs稱為資源文件。如Windows的字體文件等。
1.2 Windows系統的動態鏈接庫
Windows本身就是由大量的動態鏈接庫支持的。這包括Windows API函數 ( KRNLx86.EXE,USER.EXE,GDI.EXE,…),各種驅動程序文件,各種帶有.Fon和.Fot
擴展名的字體資源文件等。Windows還提供了針對某一功能的專用DLLs,如進行DDE編程的ddeml.dll,進行程序安裝的ver.dll等。
雖然在編寫Windows程序時必然要涉及到DLLs,但利用Delphi ,用戶在大部分時候並不會注意到這一點。這一方面是因為Delphi提供了豐富的函數使用戶不必直接去使用Windows API;另一方面即使使用Windows API,由於Delphi把API函數和其它Windows
DLLs函數重新組織到了幾個庫單元中,因而也不必使用特殊的調用格式。所以本章的重點放在編寫和調用用戶自定義的DLLs上。
使用傳統的Windows編程方法來創建和使用一個DLLs是一件很令人頭痛的事,正如傳統的Windows編程方法本身就令人生畏一樣。用戶需要對定義文件、工程文件進行一系列的修改以適應創建和使用DLLs的需要。Delphi的出現,在這一方面,正如在其它許多方面所做的那樣,減輕了開發
者的負擔。更令人興奮的是Delphi利用DLLs 實現了窗體的重用機制。用戶可以將自己設計好的窗體儲存在一個DLLs中,在需要的時候可隨時調用它。
2 DLLs的編寫和調用
2.1 DLLs的編寫
在Delphi環境中,編寫一個DLLs同編寫一個一般的應用程序並沒有太大的區別。事實上作為DLLs 主體的DLL函數的編寫,除了在內存、資源的管理上有所不同外,並不需要其它特別的手段。真正的區別在工程文件上。
在絕大多數情況下,用戶幾乎意識不到工程文件的存在,因為它一般不顯示在屏幕上。如果想查看工程文件,則可以打開View菜單選擇Project Source項,此時工程文件的代碼就會出現在屏幕的Code Editor(代碼編輯器)中。
一般工程文件的格式為:
program 工程標題;
uses 子句;
程序體
而DLLs工程文件的格式為:
library 工程標題;
uses 子句;
exprots 子句;
程序體
它們主要的區別有兩點:
1.一般工程文件的頭標用program關鍵字,而DLLs工程文件頭標用library 關鍵字。不同的關鍵字通知編譯器生成不同的可執行文件。用program關鍵字生成的是.exe文件,而用library關鍵字生成的是.dll文件;
2.假如DLLs要輸出供其它應用程序使用的函數或過程,則必須將這些函數或過程列在exports子句中。而這些函數或過程本身必須用export編譯指令進行編譯。
根據DLLs完成的功能,我們把DLLs分為如下的三類:
1.完成一般功能的DLLs;
2.用於數據交換的DLLs;
3.用於窗體重用的DLLs。
這一節我們只討論完成一般功能的DLLs,其它內容將在後邊的兩節中討論。
2.1.1 編寫一般DLLs的步驟
編寫一般DLLs的步驟如下:
1.利用Delphi的應用程序模板,建立一個DLLs程序框架。
對於Delphi 1.0的用戶,由於沒有DLLs模板,因此:
(1).建立一個一般的應用程序,並打開工程文件;
(2).移去窗體和相應的代碼單元;
(3).在工程文件中,把program改成library,移去Uses子句中的Forms,並添加適當的庫單元(一般SysUtils、Classes是需要的),刪去begin...end之間的所有代碼。
2.以適當的文件名保持文件,此時library後跟的庫名自動修改;
3.輸入過程、函數代碼。如果過程、函數准備供其它應用程序調用,則在過程、函數頭後加上export 編譯指示;
4.建立exports子句,包含供其它應用程序調用的函數和過程名。可以利用標准指示 name 、Index、resident以方便和加速過程/函數的調用;
5.輸入庫初始化代碼。這一步是可選的;
6.編譯程序,生成動態鏈接庫文件。
2.1.2 動態鏈接庫中的標准指示
在動態鏈接庫的輸出部分,用到了三個標准指示:name、Index、resident。
1.name
name後面接一個字符串常量,作為該過程或函數的輸出名。如:
exports
InStr name MyInstr;
其它應用程序將用新名字(MyInstr)調用該過程或函數。如果仍利用原來的名字(InStr),則在程序執行到引用點時會引發一個系統錯誤。
2.Index
Index指示為過程或函數分配一個順序號。如果不使用Index指示,則由編譯器按順序進行分配。
Index後所接數字的范圍為1...32767。使用Index可以加速調用過程。
3.resident
使用resident,則當DLLs裝入時特定的輸出信息始終保持在內存中。這樣當其它應用程序調用該過程時,可以比利用名字掃描DLL入口降低時間開銷。
對於那些其它應用程序常常要調用的過程或函數,使用resident指示是合適的。例如:
exports
InStr name MyInStr resident;
2.1.3 DLLs中的變量和段
一個DLLs擁有自己的數據段(DS),因而它聲明的任何變量都為自己所私有。調用它的模塊不能直接使用它定義的變量。要使用必須通過過程或函數界面才能完成。而對DLLs來說,它永遠都沒有機會使用調用它的模塊中聲明的變量。
一個DLLs沒有自己的堆棧段(SS),它使用調用它的應用程序的堆棧。因此在DLL中的過程、函數絕對不要假定DS = SS。一些語言在小模式編譯下有這種假設,但使用Delphi可以避免這種情況。Delphi絕不會產生假定DS =
SS的代碼,Delphi的任何運行時間庫過程/函數也都不作這種假定。需注意的是如果讀者想嵌入匯編語言代碼,絕不要使SS和DS登錄同一個值。
2.1.4 DLLs中的運行時間錯和處理
由於DLLs無法控制應用程序的運行,導致很難進行異常處理,因此編寫DLLs時要十分小心,以確保被調用時能正常執行
。當DLLs中發生一個運行時間錯時,相應DLLs並不一定從內存中移去(因為此時其它應用程序可能正在用它),而調用DLLs的程序異常中止。這樣造成的問題是當DLLs已被修改,重新進行調用時,內存中保留的仍然可能是以前的版本,修改後的程序並沒有得到驗證。對於這個問題,有以下
兩種解決方法:
1.在程序的異常處理部分顯式將DLL卸出內存;
2.完全退出Windows,而後重新啟動,運行相應的程序。
同一般的應用程序相比,DLL中運行時間錯的處理是很困難的,而造成的後果也更為嚴重。因此要求程序設計者在編寫代碼時要有充分、周到的考慮。
2.1.5 庫初始化代碼的編寫
傳統Windows中動態鏈接庫的編寫,需要兩個標准函數:LibMain和WEP,用於啟動和關閉DLL。在LibMain中,可以執行開鎖DLL數據段、分配內存、初始化變量等初始化工作;而WEP在從內存中移去DLLs前被調用,一般用於進行必要的清理工作,如釋放內存等。Delphi用自己特有的方式
實現了這兩個標准函數的功能。這就是在工程文件中的begin...end部分添加初始化代碼。和傳統Windows編程方法相比,它的主要特色是:
1.初始化代碼是可選的。一些必要的工作(如開鎖數據段)可以由系統自動完成。所以大部分情況下用戶不會涉及到;
2.可以設置多個退出過程,退出時按順序依次被調用;
3.LibMain和WEP對用戶透明,由系統自動調用。
初始化代碼完成的主要工作是:
1.初始化變量、分配全局內存塊、登錄窗口對象等初始化工作。在(3.2)節"利用DLLs實現應用程序間的數據傳輸"中,用於數據共享的全局內存塊就是在初始化代碼中分配的。
退出過程LibExit中使用了一個系統定義變量ExitCode,用於標志退出時的狀態。 ExitCode的取值與意義如下:
表1 ExitCode的取值與意義
━━━━━━━━━━━━━━━━━━━━━
取 值 意 義
---------------------
WEP_System_Exit Windows關閉
WEP_Free_DLLx DLLs被卸出
━━━━━━━━━━━━━━━━━━━━━
退出過程編譯時必須關閉stack_checking,因而需設置編譯指示 {$S-} 。
2.1.6 編寫一般DLLs的應用舉例
在下面的程序中我們把一個字符串操作的函數儲存到一個DLLs中,以便需要的時候調用它。應該注意的一點是:為了保證這個函數可以被其它語言編寫的程序所調用,作為參數傳遞的字符串應該是無結束符的字符數組類型(即PChar類型),而不是Object
Pascal的帶結束符的Srting類型。程序清單如下:
library Example;
uses
SysUtils,
Classes;
{返回字符在字符串中的位置}
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.
2.2 調用DLLs
有兩種方法可用於調用一個儲存在DLLs中的過程。
1.靜態調用或顯示裝載
使用一個外部聲明子句,使DLLs在應用程序開始執行前即被裝入。例如:
function Instr(SourceStr : PChar;Check : Char); Integer; far; external 'UseStr';
使用這種方法,程序無法在運行時間裡決定DLLs的調用。假如一個特定的DLLs在運行時無法使用,則應用程序將無法執行。
2.動態調用或隱式裝載
使用Windows API函數LoadLibray和GetProcAddress可以實現在運行時間裡動態裝載DLLs並調用其中的過程。
若程序只在其中的一部分調用DLLs的過程,或者程序使用哪個DLLs, 調用其中的哪個過程需要根據程序運行的實際狀態來判斷,那麼使用動態調用就是一個很好的選擇。
使用動態調用,即使裝載一個DLLs失敗了,程序仍能繼續運行。
2.3 靜態調用
在靜態調用一個DLLs中的過程或函數時,external指示增加到過程或函數的聲明語句中。被調用的過程或函數必須采用遠調用模式。這可以使用far過程指示或一個{$F +}編譯指示。
Delphi全部支持傳統Windows動態鏈接庫編程中的三種調用方式,它們是:
● 通過過程/函數名
● 通過過程/函數的別名
● 通過過程/函數的順序號
通過過程或函數的別名調用,給用戶編程提供了靈活性,而通過順序號(Index)調用可以提高相應DLL的裝載速度。
2.4 動態調用
2.4.1 動態調用中的API函數
動態調用中使用的Windows API函數主要有三個,即:Loadlibrary,GetProcAddress和Freelibrary。
1.Loadlibrary: 把指定庫模塊裝入內存
語法為:
function Loadlibrary(LibFileName: PChar): THandle;
LibFileName指定了要裝載DLLs的文件名,如果LibFileName沒有包含一個路徑,則Windows按下述順序進行查找:
(1)當前目錄;
(2)Windows目錄(包含win.com的目錄)。函數GetWindowDirectory返回這一目錄的路徑;
(3)Windows系統目錄(包含系統文件如gdi.exe的目錄)。函數GetSystemDirectory返回這一目錄的路徑;
(4)包含當前任務可執行文件的目錄。利用函數GetModuleFileName可以返回這一目錄的路徑;
(5)列在PATH環境變量中的目錄;
(6)網絡的映象目錄列表。
如果函數執行成功,則返回裝載庫模塊的實例句柄。否則,返回一個小於HINSTANCE_ERROR的錯誤代碼。錯誤代碼的意義如下表:
表2 Loadlibrary返回錯誤代碼的意義
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
錯誤代碼 意 義
--------------------------------------
0 系統內存不夠,可執行文件被破壞或調用非法
2 文件沒有被發現
3 路徑沒有被發現
5 企圖動態鏈接一個任務或者有一個共享或網絡保護錯
6 庫需要為每個任務建立分離的數據段
8 沒有足夠的內存啟動應用程序
10 Windows版本不正確
11 可執行文件非法。或者不是Windows應用程序,或者在.EXE映
像中有錯誤
12 應用程序為一個不同的操作系統設計(如OS/2程序)
13 應用程序為MS DOS4.0設計
14 可執行文件的類型不知道
15 試圖裝載一個實模式應用程序(為早期Windows版本設計)
16 試圖裝載包含可寫的多個數據段的可執行文件的第二個實例
19 試圖裝載一個壓縮的可執行文件。文件必須被解壓後才能被裝裁
20 動態鏈接庫文件非法
21 應用程序需要32位擴展
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
假如在應用程序用Loadlibrary調用某一模塊前,其它應用程序已把該模塊裝入內存,則Loadlibrary並不會裝載該模塊的另一實例,而是使該模塊的"引用計數"加1。
2.GetProcAddress:撿取給定模塊中函數的地址
語法為:
function GetProcAddress(Module: THandle; ProcName: PChar): TFarProc;
Module包含被調用的函數庫模塊的句柄,這個值由Loadlibrary返回。如果把Module設置為nil,則表示要引用當前模塊。
ProcName是指向含有函數名的以nil結尾的字符串的指針,或者也可以是函數的次序值。如果ProcName參數是次序值,則如果該次序值的函數在模塊中並不存在時,GetProcAddress仍返回一個非nil的值。這將引起混亂。因此大部分情況下用函數名是一種更好的選擇。如果用函數名,則
函數名的拼寫必須與動態鏈接庫文件EXPORTS節中的對應拼寫相一致。
如果GetProcAddress執行成功,則返回模塊中函數入口處的地址,否則返回nil。
3.Freelibrary:從內存中移出庫模塊
語法為:
procedure Freelibrary(Module : THandle);
Module為庫模塊的句柄。這個值由Loadlibrary返回。
由於庫模塊在內存中只裝載一次,因而調用Freelibrary首先使庫模塊的引用計數減一。如果引用計數減為0,則卸出該模塊。
每調用一次Loadlibrary就應調用一次FreeLibray,以保證不會有多余的庫模塊在應用程序結束後仍留在內存中。
2.4.2 動態調用舉例
對於動態調用,我們舉了如下的一個簡單例子。系統一共包含兩個編輯框。在第一個編輯框中輸入一個字符串,而後在第二個編輯框中輸入字符。如果該字符包含在第一個編輯框的字符串中,則標簽框顯示信息:"位於第n位。",否則顯示信息:"不包含這個字符。"。
輸入檢查功能的實現在Edit2的OnKeyPress事件處理過程中,程序清單如下。
procedure TForm1.Edit2KeyPress(Sender: TObject; var Key: Char);
var
order: Integer;
txt: PChar;
PFunc: TFarProc;
Moudle: THandle;
begin
Moudle := Loadlibrary('c:\dlls\example.dll');
if Moudle > 32 then
begin
Edit2.text := '';
Pfunc := GetProcAddress(Moudle,'Instr');
txt := StrAlloc(80);
txt := StrPCopy(txt,Edit1.text);
Order := TInstr(PFunc)(txt,Key);
if Order = -1 then
Label1.Caption := '不包含這個字符 '
else
Label1.Caption := '位於第'+IntToStr(Order+1)+'位';
end;
Freelibrary(Moudle);
end;
在利用GetProcAddess返回的函數指針時,必須進行強制類型轉換:
Order := TInstr(PFunc)(text,Key);
TInStr是一個定義好了的函數類型:
type
TInStr = function(Source: PChar;Check: Char): Integer;