標籤:
Delphi 中的DLL 封裝和調用對象技術
本文刊登2003 年10 月份出版的Dr.Dobb‘s 軟體研發第3 期
劉 藝
摘 要
DLL 是一種應用最為廣泛的動態連結技術但是由於在DLL 中封裝和調用對象受到對
象動態綁定機制的限制使得DLL 在封裝對象方面有一定的技術難度導致有些Delphi 程
序員誤以為DLL 只支援封裝函數不支援封裝對象本文著重介紹了DLL 中封裝和調用對
象的原理和思路並結合執行個體給出了多種不同的實現方法
關鍵字動態連結程式庫DLL 對象介面虛方法動態綁定類引用物件導向
1 物理封裝與動態連結
物理上的封裝意味著將程式封裝成若干個獨立的物理組成部分各部分之間通過動態鏈
接共同完成系統的功能而且各個物理組成部分可以單獨維護和編譯不影響其他部分要
理解物理封裝首先要搞清楚靜態連結和動態連結
在Delphi 中如果程式的各個模組分別儲存在不同的單元檔案中並通過uses 指令來
互相調用這就是一個典型的靜態連結於是各個靜態子常式編譯之後連接器從Delphi
編譯過的單元或靜態庫中取出子常式編譯代碼並添加到執行檔案中最終EXE 檔案包
括了程式及其所屬單元的所有代碼顯然靜態連結的單元或模組最終以一個獨立的物理形
式可執行檔存在除了自己編寫的單元檔案Delphi 還自動uses 了一些預設的單元
如Windows Messages 等這些都是靜態連結
靜態連結無法實現物理上的切割和封裝而且一旦其中某個單元或模組改動其他所有
單元或模組都得隨之重新編譯和串連
用於實現物理切割和封裝的bpl 包DLL 動態連結程式庫或COM+組件都是一種動態連結
的形式在動態連結情況中連接器只使用子常式external 聲明中的資訊在執行檔案中產生
一些資料表格當Windows 向記憶體中裝載執行檔案時它首先裝載所有必需的DLL 然後
程式才會啟動在裝載過程中Windows 用函數在記憶體中的地址填充程式的內部表格
每當程式調用一個外部函數時它就會使用該內部資料表格直接對DLL 代碼它當前
裝載在程式的地址空間中進行調用注意該模式不會涉及兩個不同的應用程式DLL
已經變成了應用程式的一部分並裝載在同一地址空間所有參數的傳遞都發生在堆棧上
與其它任何函數調用一樣這裡我們不打算討論DLL 的編譯因為我們首先想重點介紹
Delphi 中的DLL 封裝和調用對象技術
2 用DLL 封裝對象
DLL Dynamic Link Library 動態連結程式庫就目前來講已經不再是什麼新技術讀者可
以在書店過時的Delphi 書籍裡隨便找到討論DLL 編程的章節但這些涉及DLL 編程的書
中幾乎都是談論用DLL 來封裝函數的實際上大量的程式員也是在使用DLL 來封裝函數
或面向過程的一個模組一個函數集合而在這裡我只想討論如何用DLL 來封裝對象
這可能是讀者未曾有過的DLL 使用經驗但這卻是這本完全圍繞物件導向編程的書中重要
的部分之一或許你能從中發現一些與眾不同的實用技巧
參見考慮到目前關於DLL的現成資料很多這裡我省略了DLL的基本知識和編寫
方法假設讀者已經有了一定的DLL編程基礎如果你沒有這樣的基礎建議參閱
拙作Delphi6企業級解決方案及應用剖析DLL編程技術一節P271
一般來說使用DLL 封裝對象主要有以下好處
?? 節約記憶體多個程式可以使用同一個DLL 時該DLL 只需載入一次而且可以只
在使用時載入不用時銷毀
?? 使程式碼實現複用這就是說用DLL 封裝的對象可以重複使用甚至可以讓不
同的程式語言調用
?? 使程式模組化組件化這樣利於團隊開發維護和更新方便
然而DLL 在封裝對象方面卻有一定的技術難度這方面資料極少甚至有的程式員誤
以為DLL 只支援封裝函數不支援封裝對象
通過研究我們發現DLL 在封裝對象上主要的限制在於
?? 調用DLL 的應用程式只能使用DLL 中對象的動態綁定的方法
?? DLL 封裝對象的執行個體只能在DLL 中建立
?? 在DLL 和調用DLL 的應用程式中都需要對封裝的對象及其被調用的方法進行聲
明
下面我先通過一個簡單的例子來示範如何使用DLL 封裝對象並在應用程式中調用該
對象然後再討論相關的技術細節
3 一個簡單的例子
讀者一定還記得我們在前面章節中示範了車的繼承關係和合成關係這個程式由邏輯單
元的Demo 和介面單元frmDemo 組成我們現在就用DLL 封裝Demo 單元的所有對象並
在frmDemo 單元實現調用讀者可以通過這個具體的例子來學習如何使用DLL 封裝對象
開啟專案檔ObjDemo.dpr 1 所示在專案管理器Project Manager 中滑鼠
右擊ProjectGroup1 然後在快顯功能表中選擇Add New Project...功能表項目此時彈出 2 所
示的New Items 對話方塊選擇DLL Wizard Delphi 的DLL 嚮導將建立一個DLL 項目我
們將該項目重新命名為DemoSvr 並儲存在項目組同一目錄下
圖 1 滑鼠右擊ProjectGroup1 在快顯功能表中選擇Add New Project...功能表項目
圖 2 在New Items 對話方塊中選擇DLL Wizard
修改DemoSvr 中的代碼如樣本程式 1 所示
樣本程式 1 動態連結程式庫DemoSvr 的主程式
library DemoSvr;
{ Important note about DLL memory management: ShareMem must be the
first unit in your library‘s USES clause AND your project‘s (select
Project-View Source) USES clause if your DLL exports any procedures or
functions that pass strings as parameters or function results. This
applies to all strings passed to and from your DLL--even those that
are nested in records and classes. ShareMem is the interface unit to
the BORLNDMM.DLL shared memory manager, which must be deployed along
with your DLL. To avoid using BORLNDMM.DLL, pass string information
using PChar or ShortString parameters. }
uses
ShareMem,
SysUtils,
Classes,
Demo in ‘Demo.PAS‘;
{$R *.res}
function CarObj:TCar;
begin
Result:=TCar.create;
end;
function BicycleObj:TBicycle;
begin
Result:=TBicycle.create;
end;
exports
CarObj,
BicycleObj;
end.
由此可見 DLL 封裝對象的執行個體是在DLL 中建立的CarObj 和BicycleObj 函數建立
並輸出了Car 對象和Bicycle 對象的引用這樣DemoSvr 動態連結程式庫就可以通過CarObj 和
BicycleObj 函數輸出Car 對象和Bicycle 對象了但是Car 對象和Bicycle 對象是在Demo.pas
檔案中聲明和實現的所以這裡uses 了Demo.PAS
為了能夠使用Demo.PAS 在專案管理器中直接把Demo.pas 檔案從ObjDemo 項目中拖
放到DemoSvr 項目中 3 所示
圖 3 將Demo.pas 檔案從ObjDemo 項目中拖放到DemoSvr 項目中
開啟Demo.pas 修改TBicycle 和TCar 的聲明如下
TBicycle = class(TVehicle)
public
constructor create;
destructor Destory;
procedure ride;virtual;
end;
TCar = class(TVehicle)
protected
FEngine: TEngine;
public
constructor create;
destructor Destory;
procedure drive;virtual;
end;
請注意這裡我把應用程式中需要調用的對象方法ride 和drive 改成了虛方法顯然
這麼做不是為了讓TBicycle 和TCar 的衍生類別來覆蓋ride 和drive 方法這是因為編譯串連
應用程式時編譯器無法知道也無需知道對象在DLL 中的方法是如何?的這就意
味著對於應用程式來說要使用動態綁定晚綁定技術所以調用DLL 的應用程式只能使
用DLL 中對象的動態綁定的方法前面我們講過虛方法的動態綁定技術是把虛方法的入
口放到虛方法表VMT 中VMT 是一塊包含對象方法指標的記憶體區通過VMT 調用程式可
以得到虛方法的指標如果我們不把ride 和drive 聲明為虛方法VMT 中就不會有這些方
法的入口指標因此調用程式也就無法得到這個方法的入口指標
接下來回到frmDemo 單元在調用DLL 的應用程式中同步聲明需要調用的的對象及其
被調用的方法這裡除了將ride 和drive 聲明為虛方法外還要聲明為抽象方法因為
frmDemo 單元不提供ride 和drive 方法的實現不把它們聲明為抽象方法則編譯時間無法通過
應用程式frmDemo 單元的完整代碼如樣本程式 2 所示
樣本程式 2 調用DLL 對象的應用程式
unit frmDemo;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
//---這裡聲明需要用到的DLL 中對象的方法---
TVehicle = class(TObject);
TCar = class(TVehicle)
public
procedure drive;virtual;abstract;
end;
TBicycle = class(TVehicle)
public
procedure ride;virtual;abstract;
end;
//----------------------------
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
procedure Button2Click(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
//---這裡匯入DLL 檔案及其函數---
function CarObj:TCar ;external ‘DemoSvr.dll‘;
function BicycleObj:TBicycle ;external ‘DemoSvr.dll‘;
implementation
{$R *.dfm}
procedure TForm1.Button2Click(Sender: TObject);
var MyCar:TCar;
begin
MyCar:=CarObj;
if Mycar=nil then exit;
try
MyCar.drive;
finally
MyCar.Free;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var Bicycle:TBicycle;
begin
Bicycle:=BicycleObj;
try
Bicycle.ride;
finally
Bicycle.Free;
end;
end;
end.
最後選擇Build All Projects 功能表項目編譯和串連所有的項目 4 所示我們就得
到了需要的應用程式可執行檔以及DLL 運行測試可以看到這個程式實現了和原先一
樣的功能
但是我對這樣的DLL 封裝對象實現不是太滿意因為在DLL 和應用程式中都需要聲明
封裝的對象還要使用好virtual 和abstract 限定符很容易造成閱讀程式理解上的錯覺
如果一旦對象發生變化就需要分別在兩邊修改對象聲明以保持同步稍有不慎就會出錯
對此Steve Teixeira 在Delphi6 開發人員指南機械工業出版社2003 年出版相關內容
參見該書209 頁一書中提出了使用標頭檔的方法並通過加上編譯指令來控制DLL 和應
用程式分別讀到不同的標頭檔內容這個方法雖然可以通過只修改標頭檔來保持聲明的同
步但編譯指令和標頭檔使得閱讀程式更加困難
在這裡我有一個更好的方法供讀者分享這就是使用介面的方法
4 利用Delphi 介面實現DLL 中對象的動態綁定
前面我們分析過調用DLL 的應用程式只能使用DLL 中對象的動態綁定的方法理解
這一點是實現DLL 封裝和使用對象的關鍵那麼Delphi 介面技術為我們提供了一個最佳
選擇
圖 4 選擇Build All Projects 功能表項目編譯和串連所有的項目
為此我們建立一個介面單元IDemo 分別聲明ICar 和IBicycle 介面介面方法分別是
應用程式要用到的Drive 和Ride 完整代碼如樣本程式 3 所示
樣本程式 3 介面單元IDemo 的代碼
unit IDemo;
interface
type
ICar = interface (IInterface)
[‘{ED52E264-6683-11D7-B847-001060806215}‘]
procedure Drive;
end;
IBicycle = interface (IInterface)
[‘{ED52E264-6683-11D7-B847-001060806216}‘]
procedure Ride;
end;
implementation
end.
注意介面單元IDemo 中沒有也不能有任何實現它同時被應用程式和DLL 所用
Use 這樣當需要修改應用程式調用的對象方法時只要在一個地方即該介面單元
修改即可避免了可能出現的聲明不一致錯誤
使用介面還帶來了更多的好處首先無需使用virtual 和abstract 限定符修改對象方法聲
明避免了程式閱讀上的錯覺其次利用介面執行個體計數器自動管理對象的生命期避免了
程式員遺忘銷毀對象造成的記憶體流失
為了使用介面我將Demo 單元的型別宣告部分作了以下修改以便TBicycle 和TCar
類能夠實現介面方法值得高興的是該單元僅僅需要修改聲明部分而程式實現部分根本
不需要做任何改動
unit Demo;
interface
uses
SysUtils, Windows, Messages, Classes, Dialogs,IDemo;
type
TVehicle = class(TInterfacedObject)
protected
FColor: string;
FMake: string;
FTopSpeed: Integer;
FWheel: TWheel;
FWheels: TList;
procedure SlowDown;
procedure SpeedUp;
procedure Start;
procedure Stop;
end;
TBicycle = class(TVehicle,IBicycle)
public
constructor create;
destructor Destory;
procedure ride;
end;
TCar = class(TVehicle,ICar)
protected
FEngine: TEngine;
public
constructor create;
destructor Destory;
procedure drive;
end;
最後檢查一下專案管理器確保在應用程式項目和DLL 項目中都添加了IDemo 單元
5 所示
圖 5 確保在應用程式項目和DLL 項目中都添加了介面單元IDemo
樣本程式 4 使用介面技術調用DLL 對象的應用程式
unit frmDemo;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, IDemo;//在這裡Use IDemo 單元
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
procedure Button2Click(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
function CarObj:ICar ;external ‘DemoSvr.dll‘;
function BicycleObj:IBicycle ;external ‘DemoSvr.dll‘;
implementation
{$R *.dfm}
procedure TForm1.Button2Click(Sender: TObject);
var MyCar:ICar;
begin
MyCar:=CarObj;
MyCar.drive;
Mycar:=nil;
end;
procedure TForm1.Button1Click(Sender: TObject);
var Bicycle:IBicycle;
begin
Bicycle:=BicycleObj;
Bicycle.ride;
Bicycle:=nil;
end;
end.
在樣本程式 4 中改動不是很多這裡Use 了IDemo 單元而沒有額外的聲明實現
部分通過介面調用了DLL 中的介面方法也可以說是對象方法運行樣本程式 4 和運行示
常式序 2 實現的功能完全一樣
5 使用抽象類別實現DLL 中對象的動態綁定
既然DLL 中封裝和調用對象受到了對象動態綁定機制的限制那麼除了利用Delphi 接
口技術外我們還可以考慮使用抽象類別來實現DLL 中對象的動態綁定機制
圖 6 顯示了一個基於資料庫應用的樣本程式的物件導向設計我將介面部分設計成一
個瘦客戶機的形式這是一個供使用者互動的可執行檔distributabel2.exe 它封裝了外觀
類TfrmUsers 我把業務部分包括資料模組設計成提供服務的伺服器這是一個動態鏈
接庫檔案UserSvr.dll 它封裝了業務類TuserMaint 和資料庫訪問類TuserDM 這種設計
體現了介面和業務分離的思想
圖 6 介面和業務的物理分離的設計
由於原來的邏輯獨立的類和代碼存放在不同的單元檔案中我們很容易重新將它們劃分
到不同的項目裡 7 所示
圖 7 重新將邏輯獨立的單元檔案劃分到不同的項目裡
瘦客戶機其實上就是一個空殼只提供互動的介面它的外觀類TfrmUsers 向
TUserMaint 的執行個體對象請求服務該對象封裝在DLL 中前面我已經講過如何用DLL 來封
裝對象除了前面講過的兩種方法外這裡我想介紹第三種方法即使用抽象類別做介面的方
法
由於調用DLL 的應用程式只能使用DLL 中對象的動態綁定的方法我們不妨專門設計
一個抽象類別TIUserMaint 作為提供對象方法的介面在抽象類別TIUserMaint 中有供應用程
序使用的對象方法不過它們都是虛抽象方法目的是支援動態綁定而又無需提供實現
我將新增的TIUserMaint 放在抽象類別介面單元uIUserMaint.pas 檔案中其原始碼如樣本
程式 5 所示這個單元將作為介面檔案分別包含在UserSvr 和Distributable2 項目中 7
所示
樣本程式 5 抽象類別介面單元uIUserMaint 代碼
unit uIUserMaint;
interface
uses
Classes;
type
TIUserMaint = class (TObject)
public
function GetDepList: TStrings;virtual;abstract;
function GetUserList(strName:String): OLEVariant;virtual;abstract;
procedure UpdateUserData(UserData:OleVariant; out ErrCount: Integer);
virtual;abstract;
constructor create;virtual;abstract;
end;
TIUserMaintClass=class of TIUserMaint;
implementation
//沒有實現代碼
end.
在樣本程式 5 中還定義了TIUserMaintClass 類型它是TIUserMaint 的類引用這對
於把實作類別從DLL 傳遞到進行調用的應用程式是必要的
一般抽象類別只定義介面它由虛抽象方法組成而沒有實際的資料為了實現抽象類別
TIUserMaint 的抽象方法原來的TUserMaint 類需要繼承TIUserMaint 類並覆蓋其所有的
虛抽象方法新的TUserMaint 類聲明如下
TUserMaint = class (TIUserMaint)
private
UserDM:TUserDM;
public
function GetDepList: TStrings;override;
function GetUserList(strName:String): OLEVariant;override;
procedure UpdateUserData(UserData:OleVariant; out ErrCount: Integer);
override;
constructor create;override;
destructor Destroy;override;
end;
但實際上TUserMaint 類原有的實現部分並不需要改動所以我們的工作量不大
樣本程式 6 是動態連結程式庫UserSvr.dll 的原始碼這裡我使用了TObjUsers 函數該函
數返回了一個類型為TIUserMaintClass 的類引用而不是對象引用所以在應用程式中可以使
用這樣的代碼來建立DLL 封裝的對象
objUsers:=TObjUsers.Create;
但這不意味著TObjUsers 是一個類記住這裡TObjUsers 是一個DLL 輸出的函數它
的傳回型別是一個類參考型別
樣本程式 6 動態連結程式庫UserSvr.dll 的原始碼
library UserSvr;
uses
ShareMem,
SysUtils,
Classes,
uUserMaint in ‘uUserMaint.pas‘,
udmUser in ‘udmUser.pas‘ {UserDM: TDataModule},
uIUserMaint in ‘uIUserMaint.pas‘;
{$R *.res}
function TObjUsers:TIUserMaintClass;
begin
result:=TUserMaint;
end;
exports
TObjUsers;
begin
end.
細心的讀者可能已經發現既然TObjUsers 函數的傳回型別為TIUserMaintClass
TIUserMaintClass 在樣本程式 5 中聲明為:
TIUserMaintClass=class of TIUserMaint;
那麼result:=TUserMaint 會不會是寫錯了呢
沒有寫錯我們在應用程式中聲明了TIUserMaint 類型的對象 objUsers 通過傳遞類引
用那條objUsers:=TObjUsers.Create 語句實現的是objUsers:= TUserMaint.Create 功能這裡
面隱含了TUserMaint 向TIUserMaint 轉型的過程當然TIUserMaint 作為抽象類別本身也無
法直接建立自己的執行個體所以必須通過轉型才行另外在TIUserMaint 的衍生類別中可以隨
意改變方法的實現卻不會影響到方法的介面這就是說你以後通過進一步修改DLL 封
裝對象的實現方法來升級DLL 無需重新修改和編譯應用程式因為TIUserMaint 作為抽象
類提供的方法介面沒有改變
樣本程式 7 是應用程式實現介面和業務的物理分離後的介面單元的原始碼這裡要注
意幾點
?? 在interface 部分要Uses 抽象類別介面單元uIUserMaint
?? objUsers 聲明為TIUserMaint 類型
?? 聲明DLL 函數function TObjUsers:TIUserMaintClass; external ‘UserSvr.dll‘;
除此之外幾乎不需要進行其他的改動由此可見從介面和業務的邏輯分離演化到界
面和業務的物理分離實際上並不是想象的那樣困難
樣本程式 7 物理分離後的介面單元代碼
unit ufrmUsers;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, DB, DBClient, StdCtrls, DBCtrls, Grids, DBGrids, Mask, ExtCtrls,
Buttons,uIUserMaint;
type
TfrmUsers = class(TForm)
btnExit: TButton;
btnQryByName: TSpeedButton;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label5: TLabel;
Label6: TLabel;
Label7: TLabel;
edtQryByName: TLabeledEdit;
DBEdit1: TDBEdit;
DBEdit2: TDBEdit;
DBEdit3: TDBEdit;
DBEdit4: TDBEdit;
DBGrid1: TDBGrid;
dbcbSex: TDBComboBox;
dbcbDep: TDBComboBox;
DataSource1: TDataSource;
cdsUserMaint: TClientDataSet;
cdsUserMaintID: TWideStringField;
cdsUserMaintNAME: TWideStringField;
cdsUserMaintSEX: TWideStringField;
cdsUserMaintJOB: TWideStringField;
cdsUserMaintTEL: TWideStringField;
cdsUserMaintCALL: TWideStringField;
cdsUserMaintDEP: TWideStringField;
cdsUserMaintGROUP_ID: TWideStringField;
cdsUserMaintPASSWORD: TWideStringField;
btnUpdate: TBitBtn;
procedure btnUpdateClick(Sender: TObject);
procedure btnQryByNameClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure btnExitClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
objUsers:TIUserMaint;
public
{ Public declarations }
end;
var
frmUsers: TfrmUsers;
const
M_TITLE=‘操作提示‘;//所有提示對話方塊的標題
implementation
{$R *.dfm}
function TObjUsers:TIUserMaintClass;
external ‘UserSvr.dll‘;
procedure TfrmUsers.btnUpdateClick(Sender: TObject);
var
nErr:integer;
begin
if cdsUserMaint.State=dsEdit then cdsUserMaint.Post;
if (cdsUserMaint.ChangeCount > 0) then
begin
objUsers.UpdateUserData(cdsUserMaint.Delta,nErr);
if nErr>0 then
application.MessageBox(‘更新失敗‘,M_TITLE,MB_ICONWARNING)
else
begin
application.MessageBox(‘更新成功‘,M_TITLE,MB_ICONINFORMATION) ;
btnQryByNameClick(nil);
end;
end;
http://www.cnblogs.com/kfarvid/archive/2010/06/24/1764441.html
Delphi 中的DLL 封裝和調用對象技術(劉藝,有)