引入文件
DLL比較複雜時,能夠爲它的聲明專門建立一個引入單元,這會使該DLL變得更加容易維護和查看。引入單元的格式以下:
unit MyDllImport; {Import unit for MyDll.dll }
interface
procedure MyDllProc;
…
implementation
procedure MyDllProc;external 'MyDll' index 1;
…
end.
這樣之後想要使用MyDll中的例程時,只要簡單的在程序模塊中的uses子句中加上MyDllImport便可。其實這僅僅是種方便開發的技巧,你們打開Windows等引入windows API的單元,能夠看到相似的作法。
動態(顯式)調用DLL
前面講述靜態調用DLL時提到,DLL會在啓動調用程序時即被調入。因此這樣的作法只能起到公用DLL以及減少運行文件大小的做用,並且DLL裝載出錯會馬上致使整個啓動過程終止,哪怕該DLL在運行中只起到微不足道的做用。
使用動態調用DLL的方式,僅在調用外部例程時纔將DLL裝載內存(引用記數爲0時自動將該DLL從內存中清除),從而節約了內存空間。並且能夠判斷裝載是否正確以免調用程序崩潰的狀況,最多損失該例程功能而已。
動態調用雖然有上述優勢,可是對於頻繁使用的例程,因DLL的調入和釋放會有額外的性能損耗,因此這樣的例程則適合使用靜態引入。
調用範例
DLL動態調用的原理是首先聲明一個函數/過程類型並建立一個指針變量。爲了保證該指針與外部例程指針一致以確保賦值正確,函數/過程的聲明必須和外部例程的原始聲明兼容(兼容的意思是一、參數名稱能夠不同;二、參數/返回值類型至少保持能夠相互賦值,好比原始類型聲明爲Word,新的聲明能夠爲Integer,假如傳遞的實參老是在Word的範圍內,就不會出錯)。
接下來經過windows API函數LoadLibrary引入指定的庫文件,LoadLibrary的參數是DLL文件名,返回一個THandle。若是該步驟成功,再經過另外一個API函數GetProcAddress得到例程的入口地址,參數分別爲LoadLibrary的指針和例程名,最終返回例程的入口指針。將該指針賦值給咱們預先定義好的函數/過程指針,而後就可使用這個函數/過程了。記住最後還要使用API函數FreeLibrary來減小DLL引用記數,以保證DLL使用結束後能夠清除出內存。這三個API函數的Delphi聲明以下:
Function LoadLibrary(LibFileName:PChar):THandle;
Function GetProcAddress(Module:THandle;ProcName:PChar):TfarProc;
Procedure FreeLibrary(LibModule:THandle);
將前面靜態調用DLL例程的代碼更改成動態調用,以下所示:
type
TDllProc = function (PathName : Pchar):boolean;stdcall;
var
LibHandle: THandle;
DelPath : TDllProc;
begin
LibHandle := LoadLibrary(PChar('FileOperate.dll'));
if LibHandle >= 32 then begin
try
DelPath := GetProcAddress(LibHandle,PChar('DeleteDir'));
if DirectoryExists(ShellTreeView.Path) then
if Application.MessageBox(Pchar('肯定刪除目錄'+QuotedStr(ShellTreeView.Path)+'嗎?'), 'Information',MB_YESNO) = IDYes then
if DelPath(PChar(ShellTreeView.Path)) then
showmessage('刪除成功');
finally
FreeLibrary(LibHandle);
end;
end;
end;
16位DLL的動態調入
下面將演示一個16位DLL例程調用的例子,該例程是windows9x中的一個隱藏API函數。代碼混合了靜態、動態調用兩種方式,除了進一步熟悉外,還能夠看到調用16位DLL的解決方法。先解釋一下問題所在:
我要實現的功能是得到win9x的「系統資源」。在winNT/2000下是沒有「系統資源」這個概念的,由於winNT/2000中堆棧和句柄再也不象win9X那樣被限制在64K大小。爲了取該值,可使用win9x的user dll中一個隱藏的API函數GetFreeSystemResources。
該DLL例程必須動態引入。若是靜態聲明的話,在win2000裏執行就會當即出錯。這個兼容性不解決是不行的。因此必須先判斷系統版本,若是是win9x再動態加載。檢查操做系統版本的代碼是:
var
OSversion : _OSVERSIONINFOA;
FWinVerIs9x: Boolean;
begin
OSversion.dwOSVersionInfoSize := sizeof(_OSVERSIONINFOA);
GetVersionEx(OSversion);
FWinVerIs9x := OSversion.dwPlatformId = VER_PLATFORM_WIN32_WINDOWS;
End;
以上直接調用API函數,已在Windows單元中被聲明。
function LoadLibrary16(LibraryName: PChar): THandle; stdcall; external kernel32 index 35;
procedure FreeLibrary16(HInstance: THandle); stdcall; external kernel32 index 36;
function GetProcAddress16(Hinstance: THandle; ProcName: PChar): Pointer; stdcall; external kernel32 index 37;
function TWinResMonitor.GetFreeSystemResources(SysResource: Word): Word;
type
TGetFreeSysRes = function (value : integer):integer;stdcall;
TQtThunk = procedure();cdecl;
var
ProcHandle : THandle;
GetFreeSysRes : TGetFreeSysRes;
ProcThunkH : THandle;
QtThunk : TQtThunk;
ThunkTrash: array[0..$20] of Word;
begin
Result := 0;
ThunkTrash[0] := ProcHandle;
if FWinVerIs9x then begin
ProcHandle := LoadLibrary16('user.exe');
if ProcHandle >= 32 then begin
GetFreeSysRes := GetProcAddress16(ProcHandle,Pchar('GetFreeSystemResources'));
if assigned(GetFreeSysRes) then begin
ProcThunkH := LoadLibrary(Pchar('kernel32.dll'));
if ProcThunkH >= 32 then begin
QtThunk := GetProcAddress(ProcThunkH,Pchar('QT_Thunk'));
if assigned(QtThunk) then
asm
push SysResource //push arguments
mov edx, GetFreeSysRes //load 16-bit procedure pointer
call QtThunk //call thunk
mov Result, ax //save the result
end;
end;
FreeLibrary(ProcThunkH);
end;
end;
FreeLibrary16(ProcHandle);
end
else Result := 100;
end;
首先,LoadLibrary16等三個API是靜態聲明的(也能夠動態聲明,我這麼作是爲了減小代碼)。因爲LoadLibrary沒法正常調入16位的例程(微軟啊!),因此改用 LoadLibrary1六、FreeLibrary1六、GetProcAddress16,它們與LoadLibrary、FreeLibrary、GetProcAddress的意義、用法、參數都一致,惟一不一樣的是必須用它們才能正確加載16位的例程。
在定義部分聲明瞭函數指針TGetFreeSysRes 和TQtThunk。Stdcall、cdecl參數定義堆棧的行爲,必須根據原函數定義,不能更改。
假如相似通常的例程調用方式,跟蹤到這一步:if assigned(GetFreeSysRes) then begin GetFreeSysRes已經正確加載而且有了函數地址,卻沒法正常使用GetFreeSysRes(int)!!!
因此這裏動態加載(理由也是在win2k下沒法執行)了一個看似多餘的過程QT_Thunk。對於一個32位的外部例程,是不須要QT_Thunk的, 可是,對於一個16位的例程,就必須使用如上彙編代碼(不清楚的朋友請參考Delphi語法資料)
asm
push SysResource
mov edx, GetFreeSysRes
call QtThunk
mov Result, ax
end;
它的做用是將壓入參數壓入堆棧,找到GetFreeSysRes的地址,用QtThunk來轉換16位地址到32位,最後才能正確的執行並返回值!
以上16位DLL的部分在小倉系列中曾經提到過
Delphi開發DLL常見問題
字符串參數
前面曾提到過,爲了保證DLL參數/返回值傳遞的正確性,尤爲是爲C++等其餘語言開發的宿主程序使用時,應儘可能使用指針或基本類型,由於其餘語言與Delphi的變量存儲分配方法多是不同的。C++中字符纔是基本類型,串則是字符型的線形鏈表。因此最好將string強制轉換爲Pchar。
若是DLL和宿主程序都用Delphi開發,且使用string(還有動態數組,它們的數據結構相似)做爲導出例程的參數/返回值,那麼添加ShareMem爲工程文件uses語句的第一個引用單元。ShareMem是Borland共享的內存管理器Borlndmm.dll的接口單元。引用該單元的DLL的發佈須要包括Borlndmm.dll,不然就得避免使用string。
初始化COM庫
若是在DLL中使用了TADOConnection之類的COM組件,或者ActiveX控件,調用時會提示 「標記沒有引用存儲」等錯誤,這是由於沒有初始化COM。DLL中不會調用CoInitilizeEx,初始化COM庫被認爲是應用程序的責任,這是Borland的實現策略。
你須要作的是一、引用Activex單元,保證CoInitilizeEx函數被正確調用了
二、在單元級加入初始化和退出代碼:
initialization
Coinitialize(nil);
finalization
CoUninitialize;
end.
三、 在結束時記住將鏈接和數據集關閉,不然也會報地址錯誤。
在DLL中創建及顯示窗體
凡是基於窗體的Delphi應用程序都自動包含了一個全局對象Application,這點你們是很熟悉的。值得注意的是Delphi建立的DLL一樣有一個獨立的Application。因此如果在DLL中建立的窗體要成爲應用程序的模式窗體的話,就必須將該Application替換爲應用程序的,不然結果難以預料(該窗體建立後,對它的操做好比最小化將不會隸屬於任何主窗體)。在DLL中要避免使用ShowMessage而用MessageBox。
建立DLL中的模式窗體比較簡單,把Application.Handle屬性做爲參數傳遞給DLL例程,將該句柄賦與Dll的Application.Handle,而後再用Application建立窗體就能夠了。
無模式窗體則要複雜一些,除了建立顯示窗體例程,還必須有一個對應的釋放窗體例程。對於無模式窗體須要十分當心,建立和釋放例程的調用都需在調用程序中獲得控制。這有兩層意思:一要防止同一個窗體實例的屢次建立;二由應用程序建立一個無模式窗體必須保證由應用程序釋放,不然假如DLL中有另外一處代碼先行釋放,再調用釋放例程將會失敗。
下面是DLL窗體的代碼:
unit uSampleForm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, StdCtrls;
type
TSampleForm = class(TForm)
Panel: TPanel;
end;
procedure CreateAndShowModalForm(AppHandle : THandle;Caption : PChar);export;stdcall;
function CreateAndShowForm(AppHandle : THandle):LongInt;export;stdcall;
procedure CloseShowForm(AFormRef : LongInt);export;stdcall;
implementation
{$R *.dfm}
//模式窗體
procedure CreateAndShowModalForm(AppHandle : THandle;Caption : PChar);
var
Form : TSampleForm;
str : string;
begin
Application.Handle := AppHandle;
Form := TSampleForm.Create(Application);
try
str := Caption;
Form.Caption := str;
Form.ShowModal;
finally
Form.Free;
end;
end;
//非模式窗體
function CreateAndShowForm(AppHandle : THandle):LongInt;
var
Form : TSampleForm;
begin
Application.Handle := AppHandle;
Form := TSampleForm.Create(Application);
Result := LongInt(Form);
Form.Show;
end;
procedure CloseShowForm(AFormRef : LongInt);
begin
if AFormRef > 0 then
TSampleForm(AFormRef).Release;
end;
end.
DLL工程單元的引出聲明:
exports
CloseShowForm,
CreateAndShowForm,
CreateAndShowModalForm;
應用程序調用聲明:
procedure CreateAndShowModalForm(Handle : THandle;Caption : PChar);stdcall;external 'FileOperate.dll';
function CreateAndShowForm(AppHandle : THandle):LongInt;stdcall;external 'FileOperate.dll';
procedure CloseShowForm(AFormRef : LongInt);stdcall;external 'FileOperate.dll';
除了普通窗體外,怎麼在DLL中建立TMDIChildForm呢?其實與建立普通窗體相似,不過此次須要傳遞調用程序的Application.MainForm做爲參數:
function ShowForm(mainForm:TForm):integer;stdcall
var
Form1: TForm1;
ptr:PLongInt;
begin
ptr:=@(Application.MainForm);//先把DLL的MainForm句柄保存起來,也無須釋放,只不過是替換一下
ptr^:=LongInt(mainForm);//用調用程序的mainForm替換DLL的MainForm
Form1:=TForm1.Create(mainForm);//用參數創建
end;
代碼中用了一個臨時指針的緣由在Application.MainForm是隻讀屬性。MDI窗體的FormStyle不用設爲fmMDIChild。
引出DLL中的對象
從DLL窗體的例子中能夠發現,將句柄作爲參數傳遞給DLL,DLL能指向這個句柄的實例。一樣的道理,從DLL中引出對象,基本思路是經過函數返回DLL中對象的指針,將該指針賦值到宿主程序的變量,使該變量指向內存中某對象的地址。對該變量的操做即對DLL中的對象的操做。
本文再也不詳解代碼,僅說明須要注意的幾點規則:
一、 應用程序只能訪問對象中的虛擬方法,因此要引用的對象方法必須聲明爲虛方法;
二、 DLL和應用程序中都須要相同的對象及方法定義,且方法定義順序必須一致;
三、 DLL中的對象沒法繼承;
四、 對象實例只能在DLL中建立。
聲明虛方法的目的不是爲了重載,而是爲了將該方法加入虛擬方法表中。對象的方法與普通例程是不一樣的,這樣作才能讓應用程序獲得方法的指針。
DLL畢竟是結構化編程時代的產物,基於函數級的代碼共享,實現對象化已經力不從心。如今相似DLL功能,但對對象提供強大支持的新方式已經獲得廣泛應用,象接口(COM/DCOM/COM+)之類的技術。進程內的服務端程序從外表看就是一個dll文件,但它不經過外部例程引出應用,而是經過註冊發佈一系列接口來提供支持。它與DLL從使用上有兩個較大區別:須要註冊,經過建立接口對象調用服務。能夠看出,DLL雖然經過一些技巧也能夠引出對象,可是使用不便,並且經常將對象化強制轉爲過程化的方式,這種狀況下最好考慮新的實現方法。編程