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