`
isiqi
  • 浏览: 16591891 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论
阅读更多

Delphi接口的底层实现

引言

接口是面向对象程序语言中一个很重要的元素,它被描述为一组服务的集合,对于客户端来说,我们关心的只是提供的服务,而不必关心服务是如何实现的;对于服务端的类来说,如果它想实现某种服务,实现与该服务相关的接口即可,它也不必与使用服务的客户端进行过多的交互。这种良好的设计方式已经受到很广泛的应用。

早在Delphi 3的时候就引入了接口的概念,当时完全是因为COM的出现而诞生的,但经过这么多版本的进化,Delphi的接口已经成为Object Pascal语言的一部分,我们完全可以用接口来完成我们的设计,而不用考虑与COM相关的东西。

那么接口在Delphi中是如何实现的呢,很多人想得很复杂,其实它的本质不过也是一些简单的数据结构和调用规则。笔者假设读者已经有接口的使用经验,本文试图向你展示接口在Delphi中的实现过程,使你在使用接口的时候,知其然而知其所以然。

接口在内存中的分布

接口在概念上并不是一个实体,它需要与实现接口的类关联,如果脱离了这些类,接口就变得没有意义了。但接口在内存中仍然有其布局,它依附在对象的内存空间中。

Delphi对象本质上是一个指向特定内存空间的指针,这块内存的前四个字节是一个指针指向类的VMT表,接下来排布对象的数据成员,如果对象实现了接口,则在后面又排着一系列指针,我们可以认为这些指针就是对应的接口,每个指针就指向一个接口方法表。我们来看一下简单的例子:

type
ITest1=interface
['{5347BB0D-89B7-4674-A991-5C527BE6F8A8}']
procedureSayHello1;
end;

ITest2=interface
['{567B86BB-711D-40C2-8E5E-364B742C2FF1}']
procedureSayHello2;
end;

TTest=class(TInterfacedObject,ITest1,ITest2)
public
procedureSayHello1;
procedureSayHello2;
end;
......
implementation

{TTest}
procedureTTest.SayHello1;
begin

showMessage(IntToStr(FRefCount));
ShowMessage('Itest1sayhello');
end;

procedureTTest.SayHello2;
begin

ShowMessage(IntToStr(FRefCount));
ShowMessage('Itest2sayhello');
end;

end.

上面是两个接口的声明以及一个实现接口的类,TTest类在内存中的分布可以用下图来表示:

其中FRefCount为父类TInterfacedObject的一个成员,接下来存放的是TInterfacedObject实现的接口IInterface,再下来分别是TTest类实现的ITest2ITest1指针。各个接口指针分别指向各自的方法表,注意ITest2ITest1是从IInterface继承下来的,所以自然就有了IInterface的所有方法。方法表中每个指针指向方法真正实现的地方,其实这个说法只是暂时的,稍后会解释方法表中的指针真正指向的地方,并说明其原因。

上面的内存分布并非笔者随意想出来的,而是经过多次测试证实的,下面我们用一些代码来证实上面分布图:

var

test: Itest2;

begin

test := TTest.Create;

test.SayHello2;

end;

在证明接口的内存布局之前,需要了解接口的变量是个什么东西,比如上面的test是什么,它的本质上是一个指针,在没有被赋值之前,它指向空;而得到对象的赋值之后,它指向上面分布图中的Itest2处,对于同一个对象的多个接口变量来说,它们的“值”不一定是相等的,比如有下面的代码:

Var

Test1: ITest1;

Test2: ITest2;

Test: TTest;

Begin

Test := Ttest.Create;

Test1 := Test;

Test2 := Test;

If Integer(Test1) <> Integer(Test2) then

ShowMessage('it is not eqeual');

End;

最后,会弹出一个对话框,说明Test1Test2是不相等的;只有属性同一种接口类型,这两个变量才会相等,比如Test1Test2都是Iinterface,则他们的“值”是相等的。

好了,回过头来看看之前的代码片段吧,在第4行设置断点,运行程序并使上面代码执行,程序执行到断点处中止,按下Ctrl+Alt+C调用CPU窗口,可以看到下面的反汇编代码:

Unit1.pas.49: test := TTest.Create;

mov dl,$01

mov eax,[$00458e0c]; eax指向VMT的地址

call TObject.Create; 创建TTest对象,eax指向TTest对象的首地址

mov edx,eax; edx指向eax指向的地方,edx也指向TTest对象的首地址

test edx,edx; 测试TTest对象是否有效

jz +$03

sub edx,-$0c; 对象首地址偏移12个字节,到ITest2指针处

lea eax,[ebp-$04]; test变量的地址是ebp-04的值,eax指向这个地址

call @IntfCopy; 调用IntfCopy,将edx的值拷贝给eax,引用计数管理

Unit1.pas.50: test.SayHello2;

mov eax,[ebp-$04]; test指向的地址赋给eax,此时eax指向Itest2的地址

mov edx,[eax]; eax的内容赋给edx,此时edx指向ITest2指向的方法表

call dword ptr [edx+$0c]; 调用ITest2指向的方法表偏移12个字节处。

... ...

ret

sub edx,-$0c这一句,edx原来指向对象的内存空间,偏移12个字节刚好到哪里呢?刚好到ITest2接口指针处。接下来eax指向Test变量在栈中的地址,此时如果直接将edx赋值给eax在逻辑上也没有错,但这样就不能对接口进行引用计数的管理了。因此要调用IntfCopy,进行接口地址的赋值,再加上一个引用计数。

IntfCopy其实是调用System单元中的_IntfCopy,它的实现如下:

procedure_IntfCopy(varDest:IInterface;constSource:IInterface);
{$IFDEFPUREPASCAL}
var
P:Pointer;
begin
P:=Pointer(Dest);//保存Dest,无引用计数
ifSource<>nilthen
Source._AddRef;//增加Source的引用计数,即增加ITest2的引用计数
Pointer(Dest):=Pointer(Source);//Source的值赋给Dest,无引用计数
ifP<>nilthen
IInterface(P)._Release;//减少目标接口的引用计数,但这里的P为空指针,所以不会调用这句
end;

此时的Dest参数是eax,亦即Test变量的地址,Source参数是edx,正好是对象内容空间中的ITest2的地址。我们看到其中只是对接口地址的拷贝,及增加接口的引用计数。如果Dest有内容,则减少它的引用计数,不过这里Dest为空,所以不会调用减少引用计数的代码。

接下来到call dword ptr [edx+$0c]edx指向ITest2指向的方法表首地址,而edx+$0c偏移到哪里呢,看看上面的内存图,正好到ISayHello2处。此时调用ISayHello2指向地址的代码,我们可以简单地认为就是调用TTest.SayHello2。但事实上却不是这样的,为什么?因为在调用SayHello2之前,要先指定eax的值为TTest对象的Self指针,以此作为隐含参数传进SayHello2

我们可以到[edx+$0c]的地址看看,按F8将执行点执行到call dword ptr [edx+$0c]这一句,再按F7,跳到[edx+$0c]的地址,可以看到下面的反汇编代码:

add eax,-$0c; eax向上偏移12个字节正好是对象内存首地址。

jmp TTest.SayHello2 跳到TTest.SayHello2处。

仔细看前面的汇编码,可以知道eax正好指向ITest2指针,向上偏移12个字节则好就到了对象内存的首地址。接着调用TTest.SayHello2完成。

通过上面的例子,不仅证明了接口在对象内存空间中的布局,还可以得出以下结论:

1. 一个实现特定接口的对象创建完之后赋给该接口,编译器作了一些工作,使得接口变量指向了对象内存中的某个特定地址。

2. 调用接口的方法时,实际上调用的是接口方法表中特定的地址,在该地址处编译器计算出实现该接口的对象内存首地址,再调用对象相应的方法。

接口内存空间的形成

上节说明了接口在对象内存空间中的分布,但对象内存空间是在运行时生成的,那么接口的内存空间是如何生成的呢,这一节将阐述之。

在此之前,让我们再回到上面的对象内存图,对象内存的首地址是一个指针,指向一张VMT表,而Delphi的类其实也是一个指针,这个指针正好也指向VMT表。类是在编译时就确定下来的,VMT表当然也是编译器生成的。

VMT表在负偏移vmtIntfTable-72)字节处是一个指针,它指向下面的数据结构:PInterfaceTable = ^TInterfaceTable;

TInterfaceTable = packed record

EntryCount: Integer;

Entries: array[0..9999] of TInterfaceEntry;

end;

EntryCount表示对象实现的接口数。

Entries是一个指向TInterfaceEntry结构的数组,TInterfaceEntry表示了一个接口的进入点,它的声明如下:

PInterfaceEntry = ^TInterfaceEntry;

TInterfaceEntry = packed record

IID: TGUID;

VTable: Pointer;

IOffset: Integer;

ImplGetter: Integer;

end;

IID表示接口的GUID,如果接口没有指定GUID,则它里面的值全为0

VTable指向接口的方法表。

IOffset指明接口与对象首地址的偏移。

ImplGetter是一个方法指针,当IOffset不可用时指向接口的地址,一般不用,初始化为0

上面的数据结构在编译期就生成了,那么当一个对象创建时,相应的接口内存是如何生成的呢。在对象创建完毕之后,会调用TObejct.InitInstance(Instance: Pointer)类方法初始化对象的数据。看其代码:

classfunctionTObject.InitInstance(Instance:Pointer):TObject;
{$IFDEFPUREPASCAL}
var
IntfTable:PInterfaceTable;
ClassPtr:TClass;
I:Integer;
begin
//将对象全部清0
FillChar(Instance^,InstanceSize,0);
//指定首地址为Self,即指向VMT的指针
PInteger(Instance)^:=Integer(Self);
ClassPtr:=Self;
//建立对象的接口内存分布
whileClassPtr<>nildo
begin
//取得接口表
IntfTable:=ClassPtr.GetInterfaceTable;
ifIntfTable<>nilthen
forI:=0toIntfTable.EntryCount-1do
withIntfTable.Entries[I]do
begin
ifVTable<>nilthen
//对象偏移IOffset处,设定为指向VTable的指针
PInteger(@PChar(Instance)[IOffset])^:=Integer(VTable);
end;
//继续建立其父类的接口内存内存
ClassPtr:=ClassPtr.ClassParent;
end;
Result:=Instance;
end;

我们看PInteger(@PChar(Instance)[IOffset])^:=Integer(VTable)这一句,@PChar(Instance)[IOffset]是对象偏移IOffset的地址,而IOffsetIntfTable.Entries[I]IOffset,这个值在编译期就指定了,是接口到对象的偏移值。所以,经过上面方法调用之后,对象的内存空间就如同前面所画一样了。

现在我们对接口在内存的来龙去脉已经了如指掌,可以利用这些知识来实现一些非常的功能了。在我们的经验中,对象生成之后可以直接赋给一个接口,编译器会自动将指针偏移到接口处。但如果反过来,将一个接口赋给一个对象却是不允许的,因为信息不足啊,任何类都可以实现这个接口,编译器并不知道这个接口是由那个类实现的,所以就无从转换了。如果我们提供一个现实该接口的类,再根据该类的VMT中的接口信息,就可以得到IOffset了,如此一来不就可以偏移到对象的首地址了吗,下面的例程可以从一个接口得到实现该接口的对象,前提是必须提供实现这个接口的类:

functionGetObjFromIntf(AClass:TClass;constIntf:IInterface):TObject;
var
PIntfTable:PInterfaceTable;
IntfEntry:TInterfaceEntry;
i:Integer;
begin
Result:=nil;
//取得接口表结构
PIntfTable:=AClass.GetInterfaceTable;
ifPIntfTable=nilthenExit;
whileAClass<>nildo
begin
fori:=0toPIntfTable^.EntryCount-1do
begin
IntfEntry:=PIntfTable^.Entries[i];
//判断接口表指向的地址是否和传入接口指向的地址相同
ifPPointer(Intf)^=IntfEntry.VTablethen
begin

//偏移到对象首地址
Result:=TObject(Integer(Intf)-IntfEntry.IOffset);
Exit;
end;
end;
//继续在父类中找
AClass:=AClass.ClassParent;
end;
end;

看下面例子:

var
Intf:Itest2;
Obj:TTest;
begin
Intf:=TTest.Create;
Intf.SayHello2;
Obj:=TTest(GetObjFromIntf(TTest,Intf));
Obj.SayHello1;
end;

执行上面代码,先弹出Hello2的对话框,再弹出Hello1的对象,说明GetObjFromIntf函数执行成功,我们实现了从接口到对象的转换过程。

接口的引用计数

上面接口的内存空间与COM的接口在二进制上是兼容的,即接口就是一个指向VTable的指针,与COM兼容的还有另一个特性,就是通过引用计数自动管理COM对象的生命周期。C++程序员必须手工去管理引用计数的增减,而Delphi编译器帮我们做了这些事情,因为引用计数是有规律,只要遵循这些规律,便能自动管理引用计数的增减。IInterface的声明如下:

IInterface = interface

['{00000000-0000-0000-C000-000000000046}']

function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

end;

任何实现IInterface的类都必须实现上面三个方法,其中的_AddRef_Release就是实现引用计数管理的。Delphi提供了IInterfaceObject类默认实现Interface,它声明一个成员FRefCount: Integer指定引用计数,_AddRef被调用时只是将FRefCount1

Result := InterlockedIncrement(FRefCount);

_Release被调用时,减少FRefCount,如果FRefCount0时,即调用Destroy消毁自己:

Result := InterlockedDecrement(FRefCount);

if Result = 0 then

Destroy;

如果即想实现接口,而不想通过引用计数管理生命周期的,可以在AddRefRelease中简单地将结果返回为-1即可,TComponent类即是如此。

那么Delphi是如何实现接口引用计数的管理的呢,有下面的规律:

1. 当一个非空的接口变量要赋值给另一个接口变量时,非空的接口变量应该要调用AddRef

2. 当一个非空的接口变量要被另一个接口变量赋值时,非空的接口变量应该要调用Release

EN

分享到:
评论

相关推荐

    Delphi接口的底层实现 .mht

    Delphi接口的底层实现 .mht

    delphi接口编程深浅.doc

    ### Delphi接口编程深入探索 #### 接口的引用计数管理 在Delphi中,接口作为一种特殊类型,其实现了资源的自我管理机制。这种机制的核心在于引用计数(Reference Counting)的管理,它确保了每个接口实例在不再被...

    GDI+ Delphi接口单元

    标题中的"GDI+ Delphi接口单元"指的是一个专门设计的Delphi组件或单元,它封装了GDI+的函数和方法,以面向对象的方式提供给Delphi程序员使用。这样的接口单元使得开发者无需直接与GDI+的C++原生接口打交道,而是通过...

    delphi 通用数据接口组件

    通过这些组件,开发者无需深入了解底层数据库的细节,就能实现数据的查询、更新、删除和插入操作。 描述中的"数据计算"是指在处理数据时进行的各种数学和逻辑运算,这可能涉及到数值分析、统计计算、数据清洗、数据...

    Delphi 调用海康接口

    "Delphi调用海康SDK,实时视频,抓拍,回放"这个主题涉及的是使用Delphi编程语言来操作海康威视(Hikvision)的设备,如监控摄像头,通过其提供的SDK(Software Development Kit)来实现视频流的实时显示、图片抓拍...

    delphi websocket 用socket实现

    总的来说,实现WebSocket通信需要深入理解底层网络协议和Delphi的网络编程。虽然过程较为复杂,但一旦完成,你将拥有一个高效且灵活的实时通信解决方案。在实际开发中,也可以考虑使用现成的第三方库,如Synapse或...

    delphi实现ping demo.

    在Delphi中实现ping功能,主要是通过调用Windows API(应用程序接口)来完成的。Windows API提供了许多底层网络操作的函数,其中`ICMP_ECHO`(Internet Control Message Protocol - Echo)就是用来实现ping功能的。...

    delphi截图程序实现源代码

    同时,这也是一种提升Delphi编程技能和理解操作系统底层机制的好途径。 总之,Delphi截图程序的实现涉及了Delphi编程、Windows API调用、图像处理和用户交互等多个方面的技术。通过学习和实践,开发者不仅可以掌握...

    DELPHI中用TApdComPort实现串行通信

    然而,实现稳定的串行通信并不是一件简单的事情,特别是对于那些不熟悉底层硬件接口的开发者来说。 #### 二、串行通信概述 串行通信是指通过一条数据线将数据一位接一位地按时间顺序传输的方式。这种方式通常用于...

    Delphi11.3实现android系统的步进电机控制delphibujin.rar

    在实现步进电机控制时,我们需要利用这些API编写底层的C++代码,用于处理硬件接口,然后通过JNI调用这些C++函数,从Delphi的用户界面层进行控制。 步骤可能包括以下部分: 1. **配置开发环境**:安装Delphi 11.3 ...

    Api.rar_delphi api接口

    此压缩包包含了针对不同开发平台的接口头文件和库文件,这表明它们是为VB(Visual Basic)、VC(Visual C++)、DELPHI、PB(PowerBuilder)和C#这些语言定制的接口实现。 对于Delphi开发人员来说,这些API接口...

    Delphi底层编程实例.rar

    "Delphi源码-系统相关"的标签暗示了这些实例可能包含了对操作系统内部机制的探索,例如文件系统操作、注册表管理、硬件设备驱动的接口实现等。通过分析和实践这些代码,开发者可以提升自己在系统级编程方面的技能,...

    delphi实现ping

    2. **Winsock组件**:在Delphi中实现网络通信,通常会用到Winsock组件,这是一个封装了Windows Sockets API(Windows套接字接口)的控件。通过Winsock组件,开发者可以处理TCP/IP协议栈中的各种网络操作,包括ping。...

    TradeX.dll的Delphi接口演示,trade.dll扩展版再出新丁

    这个"TradeX.dll的Delphi接口演示"旨在向Delphi程序员展示如何有效地与TradeX.dll交互,实现股票交易的自动化。 TradeX.dll的接口可能包括了一系列用于获取市场数据、执行交易订单、管理账户信息以及订阅实时行情等...

    usb lib.rar_Delphi-USB_Delphi-usb.rar_delphi usb_delphi Usb 接口_

    (lwf8888@163.com)"说明这个压缩包包含的是一个Delphi实现的USB接口程序,可能是一个示例项目或者库,作者提供了邮箱地址,可能意味着用户在遇到问题时可以联系作者寻求帮助。 从标签来看,"delphi-usb"、"delphi...

    delphi禁用USB源代码

    通过这段代码,开发者可以学习到如何在Delphi程序中操作系统底层,与硬件接口进行交互,从而实现对USB设备的禁用。 在Delphi中,要禁用USB接口,主要涉及Windows API调用。Windows API提供了丰富的函数和结构体,...

    (R)-Delphi+Access-Code-力信Pos消费系统(需带硬件接口程序)-源代码全-白金精品

    通过其强大的VCL(Visual Component Library)框架,开发者可以快速构建用户界面,并能方便地访问底层操作系统资源,实现硬件交互。 【硬件接口程序】:Pos系统中的硬件接口程序是为了让系统能够与各种Pos硬件设备...

    delphi winio模拟键盘硬件底层简单示例

    这个"delphi winio模拟键盘硬件底层简单示例"是一个使用Delphi和WinIO库来模拟键盘输入的实践教程。 首先,我们要理解WinIO的核心概念。WinIO通过直接访问端口来实现硬件级别的交互,这在一般的应用程序编程中是不...

    Delphi源码实现C++代码转delphi代码..rar

    本资源“Delphi源码实现C++代码转Delphi代码”提供了一种方法来解决这个问题,它可能包含了一个工具或者一个库,用于帮助程序员将C++编写的代码转换为等效的Delphi源码。 Delphi,由Embarcadero Technologies开发,...

Global site tag (gtag.js) - Google Analytics