深入Delphi下的DLL編程

引 言

相信有些計算機知識的朋友都應該聽說過“DLL”。尤其是那些使用過windows操作系統的人,都應該有過多次重裝系統的“悲慘”經歷——無論再怎樣小心,沒有驅動損壞,沒有病毒侵擾,仍然在使用(安裝)了一段時間軟件後,發現windows系統越來越龐大,操作越來越慢,還不時的出現曾經能使用的軟件無法使用的情況,導致最終不得不重裝系統。這種情況常常是由於dll文件的大量安裝和衝突造成的。這一方面說明DLL的不足,另一方面也說明DLL的重要地位,以至我們無法杜絕它的使用。
DLL(動態鏈接庫,Dynamic Link Library)簡單來說是一種可通過調用執行的已編譯的代碼模塊。DLL是windows系統的早期產物。當時的主要目的是爲了減少應用程序對內存的使用。只有當某個函數或過程需要被使用時,才從硬盤調用它進入內存,一旦沒有程序再調用該DLL了,纔將其從內存中清除。光說整個windows系統,就包括了成百上千個dll文件,有些dll文件的功能是比較專業(比如網絡、數據庫驅動)甚至可以不安裝的。假如這些功能全部要包括在一個應用程序(Application program)裏,windows將是一個數百M大小的exe文件。這個簡單的例子很容易解釋DLL的作用,而調用DLL帶來的性能損失則變得可被忽略不計。
多個應用程序調用同一個DLL,在內存裏只有一個代碼副本。而不會象靜態編譯的程序那樣每一個都必須全部的被裝入。裝載DLL時,它將被映射到進程的地址空間,同時使用DLL的動態鏈接並非將庫代碼拷貝,而僅僅記錄函數的入口點和接口。
同時DLL還能帶來的共享的好處。一家公司開發的不同軟件可能需要一些公用的函數/過程,這些函數/過程可能是直接的使用一些內部開發的DLL;一些常用的功能則可以直接使用windows的標準DLL,我們常說的windows API就是包含在windows幾個公用DLL文件裏的函數/過程;理論上(如果不牽涉作者的版權),知道一個DLL的聲明及作用(函數定義的輸入參數及返回值),我們完全可以在不清楚其實現(算法或編譯方式)的情況下直接使用它。
假如一個DLL中函數/過程的算法得到了更新,BUG得到了修正,整個dll文件會得到升級。一般來說爲了保證向下兼容,調用聲明與返回結果應該保持不變。但實際上,即使是同一家開發的DLL,隨着功能的改善,也很難保證某個調用執行完全不變。在使用其他人開發的DLL時這種糟糕情況更加的嚴重。比如我在一個繪圖程序裏使用了某著名圖形軟件商舊版本的DLL包,我所有的調用都是根據他發佈的舊版的聲明來執行的。假設用戶安裝了該軟件商的一個新軟件,導致其中部分DLL被更新升級,假如這些DLL已經有過改動,直接後果將是我的軟件不再穩定甚至無法運行!不要輕視這種情況,事實上它是很普遍的,比如windows在修正BUG和升級過程中,就不斷改動它包含的那些DLL。往往新版DLL不是簡單的增加新的函數/過程,而是更換甚至取消了原有的聲明,這時候我們再也無法保證所有程序都運行正常。
DLL除了上面提到的改善計算機資源利用率、增加開發效率、隱藏實現細節外,還可以包含數據和各種資源。比如開發一個軟件的多國語言版,就可以使用DLL將依賴於語言的函數和資源分離出來,然後讓各地的用戶安裝不同對應的DLL,以獲取本地字符集的支持。再比如一個軟件必須的圖形、圖標等資源,也可以直接放在dll文件中統一安裝管理。


創建一個DLL

在進行後面的講解之前,我想大家應該先清楚一個概念:例程聲明的是一個指針變量,調用函數/過程,其實是通過指針轉入該函數/過程的執行代碼。
我們先嚐試用Delphi來建立一個自己的DLL文件。這個DLL包含一個標準的目錄刪除(包含子目錄及文件)函數。

建立DLL
通過Delphi建立一個DLL是很容易的。New一個新Project,選擇DLL Wizard,然後會生成一個非常簡單的單元。該單元不象一般的工程文件以program開始,而是以library開始的。
該工程單元缺省引用了SysUtils、Classes兩個單元。可以直接在該單元的uses之後,begin … end部分之前添加函數/過程代碼,也可以在工程中添加包含代碼的單元,然後該單元將會被自動uses。
接下來是編寫DLL例程的代碼。如果是引用單元裏的例程,需要通過聲明時添加export後綴引出。假如是直接寫在library單元中的,則不必再寫export了。
最後一步是在library單元的begin語句之上,uses部分及函數定義之下添加exports部分,並列舉需要引出的例程名稱。注意僅僅是名稱,不包含procedure或function關鍵字,也不需要參數、返回值和後綴。
exports語句後的語法有三種形式(例程指具體的函數/過程):
exports例程名;
exports例程名 index 索引值;
exports例程名 name新名稱;
索引值和新名稱便於其他程序確定函數地址;也可以不指定,如果沒有使用Index關鍵字,Delphi將按照exports後的順序從1開始自動分配索引號。Exports後可跟多個例程,之間以逗號分隔。
編譯,build最終的dll文件。

需注意的格式
爲了保證生成的DLL能正確與C++等語言兼容,需要注意以下幾點:
儘量使用簡單類型或指針作爲參數及返回值的類型。這裏的簡單類型是指C++的簡單類型,所以string字符串類型最好轉換成Pchar字符指針。直接使用string的DLL例程在Delphi開發的程序中調用是沒有問題的(有資料指出需加入ShareMem做爲第一單元以確保正確),但如果使用C++或其他語言開發的程序調用,則不能保證參數傳遞正確;
雖然過程是允許的,但是最好習慣全部寫成函數。過程則返回執行正確與否的true/false;
對於參數的指示字比如const(只讀)、out(只寫)等等,爲保證調用的兼容性,最好使用缺省方式(缺省var,即可讀寫的地址);
使用stdcall聲明後綴,以保證正確的異常處理。16位DLL無法通過這種方式處理異常,所以還得在例程最外層用Try … Except將異常處理掉;
一般不使用far後綴,除非爲了保持與16位兼容。

範例代碼
DLL工程單元:
library FileOperate;

uses
  SysUtils,
  Classes,
  uDirectory in 'uDirectory.pas';

{$R *.res}

exports
  DeleteDir;

begin
end.

函數功能實現單元:
unit uDirectory;

interface

uses
  Classes, SysUtils;

  function DeleteDir(DirName : Pchar):boolean;export;stdcall;

implementation

function DeleteDir(DirName : Pchar):boolean;
var
  FindFile: TSearchRec;
  s : string;
begin
  s := DirName;
  if copy(s,length(s),1) <> '/' then s := s+ '/';
  if DirectoryExists(s) then begin
    if FindFirst(s + '*.*', faAnyFile, FindFile) = 0 then begin
      repeat
        if FindFile.Attr <> faDirectory then begin
          //文件則刪除
          DeleteFile(s + FindFile.Name);
        end
        else begin
          //目錄則嵌套自身
          if (FindFile.Name <> '.') and (FindFile.Name <> '..') then
            DeleteDir(Pchar(s + FindFile.Name));
        end;
      until FindNext(FindFile) <> 0;
      FindCLose(FindFile);
    end;
  end;

  Result := RemoveDir(s);
end;

end.

初始化及釋放資源
Delphi中初始化有幾種方法。一種是利用Unit的Initalization與Finalization這兩個小節(不知道“單元小節”?你該先去惡補Delphi語法了)進行該單元中變量的初始化工作。注意,DLL雖然在內存中只有一個副本,但是例程隸屬於調用者的不同進程空間。如果想初始化公共變量來達到多進程共享是不可行的,同時也要注意公共變量帶來的衝突問題。
二是在library單元的begin … end部分進行DLL的初始化。假如想在DLL結束時有對應代碼,則可以利用DLL自動創建的一個ExitProc過程變量,這是一個退出過程的指針。建立一個自己的過程,並將該過程的地址賦與ExitProc。因爲ExitProc是DLL創建時就存在的,所以在begin … end部分就應該進行此步操作。同時建立一個臨時指針變量保存最初的ExitProc值,在自己的退出過程中將ExitProc值賦回來。這樣做是爲了進行自己的退出操作後,能完成缺省的DLL退出操作(與在重載的Destory方法中inherated的意義是一樣的,完成了自己的destory,還需要進行缺省的父類destory才完整)。
示例如下:
library MyDLL;
  ...
var
OldExitProc: pointer;  //公共變量,爲的保存最初的ExitProc指針以便賦回
procedure MyExitProc;
  begin
…//對應初始化的結束代碼
    ExitProc := OldExitProc; //自己的退出過程中要記住將ExitProc賦回
end;
  ...
  begin
    ... //初始化代碼
    OldExitProc := ExitProc;
    ExitProc := @MyExitProc;
  end.
第三種方法和ExitProc類似,在System單元中預定義了一個指針變量DllProc(該方法需要引用 Windows單元)。在使用DLLProc時, 必須先寫好一個具有以下原型的程序:
  procedure DLLHandler(dwReason: DWORD); stdcall;
並在library的begin..end.之間, 將這個DLLHandler程序的執行地址賦給DLLProc中, 這時就可以根據參數Reason的值分別作出相應的處理。示例如下:
  library MyDLL;
  ...
  procedure MyDLLHandler(dwReason: DWORD);
  begin
   case dwReason of
    DLL_Process_Attach: //進程進入時
    DLL_Process_Detach: //進程退出時
    DLL_Thread_Attach: //線程進入時
    DLL_Thread_Detach: //線程退出時
   end;
  end;
  ...
  begin
    ... //初始化代碼
    DLLProc := @MyDLLHandler;
    MyDLLHandle(DLL_Process_Attach);
  end.
可見,通過DLLProc 在處理多進程時比ExitProc更加強大和靈活。


靜態(隱式)調用DLL

DLL已經有了,接下來我們看如何調用並調試它。普通的DLL是不需要註冊的,但是要包含在windows搜索路徑中才能被找到。搜索路徑的順序是:當前目錄;Path路徑;windows目錄;widows系統目錄(system、system32)。

引入DLL例程的聲明方法
在需要使用外部例程(DLL函數/過程)的代碼之前預定義該函數/過程。即按DLL中的定義原樣聲明,且僅需要聲明。同時加上external後綴引入,與export引出相對應。根據exports的三種索引語法,也有三種確定例程的方式(以函數聲明爲例):
function 函數名(參數表):返回值;external ’DLL文件名’;
function 函數名(參數表):返回值;external ’DLL文件名’ index 索引號;
function 函數名(參數表):返回值;external ’DLL文件名’ name 新名稱;
如果不確定例程名稱,可以用索引方式引入。如果按原名引入會發生衝突,則可以用“新名稱”引入。
進行聲明後DLL函數的使用就和一般函數相同了。靜態調用方式簡單,但在啓動調用程序時即調入DLL作爲備用過程。如果此DLL文件不存在,那麼啓動時即會提示錯誤並立刻終止程序,不管定義是否使用。
快速查看DLL例程定義可以使用Borland附帶的工具tdump.exe(在Delphi或BCB的bin目錄下),示例如下:
Tdump c:/windows/system/user32.dll > user32.txt
然後打開user32.txt文件,找到Exports from USER32.dll行,之下的部分就是DLL例程定義了,比如:
    RVA      Ord. Hint Name
    -------- ---- ---- ----
    00001371    1 0000 ActivateKeyboardLayout
    00005C20    2 0001 AdjustWindowRect
    0000161B    3 0002 AdjustWindowRectEx
Name列就是例程的名稱,Ord就是該例程索引號。注意,該工具是不能得到例程的參數表的。如果參數錯誤,調用DLL例程會引起堆棧錯誤而導致調用程序崩潰。

調用代碼
建立一個普通工程,在Main窗體上放置一個TShellTreeView控件(Samples頁),再放置一個按鈕,添加代碼如下:
function DeleteDir(DirName : Pchar):boolean;stdcall;external 'FileOperate.dll';

procedure TForm1.Button1Click(Sender: TObject);
begin
  if DirectoryExists(ShellTreeView.Path) then
    if Application.MessageBox(Pchar('確定刪除目錄'+QuotedStr(ShellTreeView.Path)+'嗎?'), 'Information',MB_YESNO) = IDYes then
      if DeleteDir(PChar(ShellTreeView.Path)) then
        showmessage('刪除成功');
end;
該範例調用的就是前面建立的DLL。
注意,聲明時要包括stdcall後綴,這樣才能保證調用Delphi開發的DLL的例程中類似PChar這樣的參數值傳遞正確。大家有興趣可以試驗一下,不加入stdcall或者safecall後綴執行上面代碼,將不能保證成功傳遞字符串參數給DLL函數。

調試方法
在Delphi主菜單Run項目中選擇Parameters,打開“Run Parameters”對話框。在Host Application中填入一個宿主程序(該程序調用了將要調試的DLL),還可以在Parameters中輸入參數。保存內容,然後就可以在DLL工程中設置斷點、跟蹤/單步執行了。
Run該DLL工程,然後將運行宿主程序。執行會調用DLL的操作,然後就能跟蹤進入該DLL的代碼,接下來的調試操作和普通程序是一樣的。
因爲操作系統或其他軟件影響的原因,可能會出現進行了上述步驟仍然無法正常跟蹤/中斷DLL代碼的情況。這時可以試試在菜單Project |Options 對話框的 Linker 頁面裏將 EXE and DLL Options 中的Include TD32 debug info及include remote debug symbols兩個選項選中。
假如還是不能中斷 -_____-||| 那隻好另外建立一個引用執行代碼單元的應用程序,寫代碼調用例程調試完成後再編譯DLL了(其實該方法有時候蠻方便的,但有時候亦非常麻煩)。

引入文件
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動態調用的原理是首先聲明一個函數/過程類型並創建一個指針變量。爲了保證該指針與外部例程指針一致以確保賦值正確,函數/過程的聲明必須和外部例程的原始聲明兼容(兼容的意思是1、參數名稱可以不一樣;2、參數/返回值類型至少保持可以相互賦值,比如原始類型聲明爲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位的例程(微軟啊!),所以改用 LoadLibrary16、FreeLibrary16、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位,最後才能正確的執行並返回值!


Delphi開發DLL常見問題

字符串參數
前面曾提到過,爲了保證DLL參數/返回值傳遞的正確性,尤其是爲C++等其他語言開發的宿主程序使用時,應儘量使用指針或基本類型,因爲其他語言與Delphi的變量存儲分配方法可能是不一樣的。C++中字符纔是基本類型,串則是字符型的線形鏈表。所以最好將string強制轉換爲Pchar。
如果DLL和宿主程序都用Delphi開發,且使用string(還有動態數組,它們的數據結構類似)作爲導出例程的參數/返回值,那麼添加ShareMem爲工程文件uses語句的第一個引用單元。ShareMem是Borland共享的內存管理器Borlndmm.dll的接口單元。引用該單元的DLL的發佈需要包括Borlndmm.dll,否則就得避免使用string。

在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。

初始化COM庫
如果在DLL中使用了TADOConnection之類的COM組件,或者ActiveX控件,調用時會提示 “標記沒有引用存儲”等錯誤,這是因爲沒有初始化COM。DLL中不會調用CoInitilizeEx,初始化COM庫被認爲是應用程序的責任,這是Borland的實現策略。
你需要做的是1、引用Activex單元,保證CoInitilizeEx函數被正確調用了
2、在單元級加入初始化和退出代碼:
initialization
   Coinitialize(nil);
finalization
   CoUninitialize;
end.
3、 在結束時記住將連接和數據集關閉,否則也會報地址錯誤。

引出DLL中的對象
從DLL窗體的例子中可以發現,將句柄做爲參數傳遞給DLL,DLL能指向這個句柄的實例。同樣的道理,從DLL中引出對象,基本思路是通過函數返回DLL中對象的指針,將該指針賦值到宿主程序的變量,使該變量指向內存中某對象的地址。對該變量的操作即對DLL中的對象的操作。
本文不再詳解代碼,僅說明需要注意的幾點規則:
應用程序只能訪問對象中的虛擬方法,所以要引用的對象方法必須聲明爲虛方法;
DLL和應用程序中都需要相同的對象及方法定義,且方法定義順序必須一致;
DLL中的對象無法繼承;
對象實例只能在DLL中創建。
聲明虛方法的目的不是爲了重載,而是爲了將該方法加入虛擬方法表中。對象的方法與普通例程是不同的,這樣做才能讓應用程序得到方法的指針。

DLL畢竟是結構化編程時代的產物,基於函數級的代碼共享,實現對象化已經力不從心。現在類似DLL功能,但對對象提供強大支持的新方式已經得到普遍應用,象接口(COM/DCOM/COM+)之類的技術。進程內的服務端程序從外表看就是一個dll文件,但它不通過外部例程引出應用,而是通過註冊發佈一系列接口來提供支持。它與DLL從使用上有兩個較大區別:需要註冊,通過創建接口對象調用服務。可以看出,DLL雖然通過一些技巧也可以引出對象,但是使用不便,而且常常將對象化強制轉爲過程化的方式,這種情況下最好考慮新的實現方法。

注:本文代碼在Delphi6、7中調試通過 

發佈了19 篇原創文章 · 獲贊 0 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章