Delphi接口的底层实现_第1页
Delphi接口的底层实现_第2页
Delphi接口的底层实现_第3页
免费预览已结束,剩余9页可下载查看

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、Delphi接口的底层实现引言接口是面向对象程序语言中一个很重要的元素,它被描述为一组服务的集合,对于客户端来说,我们关心的只是提供的服务,而不必关心服务是 如何实现的;对于服务端的类来说,如果它想实现某种服务,实现与该服务相关 的接口即可,它也不必与使用服务的客户端进行过多的交互。 这种良好的设计方 式已经受到很广泛的应用。早在Delphi 3的时候就引入了接口的概念,当时完全是因为 COM的出现 而诞生的,但经过这么多版本的进化, Delphi的接口已经成为Object Pascal语 言的一部分,我们完全可以用接口来完成我们的设计,而不用考虑与 COM相关 的东西。那么接口在Delphi

2、中是如何实现的呢,很多人想得很复杂,其实它的本质 不过也是一些简单的数据结构和调用规则。笔者假设读者已经有接口的使用经 验,本文试图向你展示接口在 Delphi中的实现过程,使你在使用接口的时候, 知其然而知其所以然。接口在内存中的分布接口在概念上并不是一个实体,它需要与实现接口的类关联,如 果脱离了这些类,接口就变得没有意义了。但接口在内存中仍然有其布局,它依 附在对象的内存空间中。Delphi对象本质上是一个指向特定内存空间的指针,这块内存的前四个字 节是一个指针指向类的VMT表,接下来排布对象的数据成员,如果对象实现了 接口,则在后面又排着一系列指针,我们可以认为这些指针就是对应的接口,

3、每个指针就指向一个接口方法表。我们来看一下简单的例子:typeITestl = in terface'5347BB0D-89B7-4674-A991-5C527BE6F8A8'procedure SayHello1;end;ITest2 = in terface'567B86BB-711D-40C2-8E5E-364B742C2FF1' procedure SayHello2;ITes t2)end;TTest = class (TInterfacedObject,ITest1,publicprocedure SayHello1;procedure SayHel

4、lo2;end;impleme ntati on TTest procedure TTest.SayHello1;beginshowMessage(l ntToStr(FRefCou nt);ShowMessage( 'Itest1 say hello');end ;procedure TTest.SayHello2;beginShowMessage(I ntToStr(FRefCou nt);ShowMessage( 'Itest2 say hello');end ;end .上面是两个接口的声明以及一个实现接口的类,TTest类在内存中的分布可以用下图来表示

5、:其中FRefCount为父类TInterfacedObject的一个成员,接下来存放的是TinterfacedObject实现的接口 IInterface,再下来分别是TTest类实现的ITest2 和ITestl指针。各个接口指针分别指向各自的方法表,注意ITest2和ITestl是从IInterface继承下来的,所以自然就有了IInterface的所有方法。方法表中每个指针指向方法真正实现的地方,其实这个说法只是暂时的,稍后会解释方法表 中的指针真正指向的地方,并说明其原因。上面的内存分布并非笔者随意想出来的,而是经过多次测试证实的,下面我们用一些代码来证实上面分布图:vartest:

6、 Itest2;beg intest := TTest.Create;test.SayHello2;end;在证明接口的内存布局之前,需要了解接口的变量是个什么东西,比如上面 的test是什么,它的本质上是一个指针,在没有被赋值之前,它指向空;而得到对象的赋值之后,它指向上面分布图中的Itest2处,对于同一个对象的多个接 口变量来说,它们的“值”不一定是相等的,比如有下面的代码:VarTesti: ITestl;Test2: ITest2;Test: TTest;Beg inTest := Ttest.Create;Testi := Test;Test2 := Test;If In tege

7、r(Testl) <> In teger(Tes t2) the nShowMessage('it is not eqeua l');En d;最后,会弹出一个对话框,说明Test1和Test2是不相等的;只有属性同一 种接口类型,这两个变量才会相等,比如 Test1和Test2都是Iinterface,则他 们的“值”是相等的。好了,回过头来看看之前的代码片段吧,在第 4行设置断点,运行程序并使上面代码执行,程序执行到断点处中止,按下Ctrl+Alt+C调用CPU窗口 ,可以看 到下面的反汇编代码:= TTest.Create;mov dl,$01mov eax,

8、$00458e0c;eax指向VMT的地址call TObject.Create;创建TTest对象,eax指向TTest对象的首地址mov edx,eax;edx指向eax指向的地方,edx也指向TTest对象的首地址jz +$03sub edx,-$0c;对象首地址偏移12个字节,到ITest2指针处lea eax,ebp-$04;test变量的地址是 ebp-04的值,eax指向这个地址call lntfCopy;调用IntfCopy,将edx的值拷贝给eax,引用计数管理Unit1.pas.50: test.SayHello2;mov eax,ebp-$04;将test指向的地址赋给

9、eax,此时eax指向Itest2的地址mov edx,eax;将eax的内容赋给edx,此时edx指向ITest2指向的方法表call dword ptr edx+$0c;调用ITest2指向的方法表偏移12个字节处retsub edx,-$0c这一句,edx原来指向对象的内存空间,偏移12个字节刚好到哪 里呢?刚好到ITest2接口指针处。接下来eax指向Test变量在栈中的地址,此 时如果直接将edx赋值给eax在逻辑上也没有错,但这样就不能对接口进行引 用计数的管理了。因此要调用IntfCopy,进行接口地址的赋值,再加上一个引用 计数。IntfCopy其实是调用System单元中的_

10、lntfCopy,它的实现如下:procedure _lntfCopy( var Dest: llnterface;const Source: llnterface);$IFDEF PUREPASCALvarP: Pointer;beginP := Pointer(Dest);/保存 Dest,无引用计数if Source <> nil thenSource._AddRef;增加Source的引用计数,即增加 ITest2的引用计数Pointer(Dest) := Pointer(Source);将 Source 的值赋给 Dest,无引用计数if P <> nil t

11、henllnterface(P)._Release; /减少目标接口的引用计数,但这里的P为空指针,所以不会调用这句end ;此时的Dest参数是eax,亦即Test变量的地址,Source参数是edx,正好是对象内容空间中的ITest2的地址。我们看到其中只是对接口地址的拷贝,及增加接口的引用计数。如果Dest有内容,则减少它的引用计数,不过这里Dest 为空,所以不会调用减少引用计数的代码。接下来到call dword ptr edx+$0c ,edx指向ITest2指向的方法表首地址,而edx+$0c偏移到哪里呢,看看上面的内存图,正好到ISayHello2处。此时调用ISayHello

12、2指向地址的代码,我们可以简单地认为就是调用 TTest.SayHello2。但事实上却不是这样的,为什么?因为在调用SayHello2之前,要先指定eax的值为TTest对象的Self指针,以此作为隐含参数传进 SayHello2。我们可以到edx+$0c的地址看看,按F8将执行点执行到call dword ptredx+$0c这一句,再按F7,跳到edx+$0c的地址,可以看到下面的反汇编代 码:add eax,-$0c;eax向上偏移12个字节正好是对象内存首地址。jmp TTest.SayHello2 ; 跳到 TTest.SayHello2 处。仔细看前面的汇编码,可以知道 eax正

13、好指向ITest2指针,向上偏移12个字节则好就到了对象内存的首地址。接着调用TTest.SayHello2完成。通过上面的例子,不仅证明了接口在对象内存空间中的布局,还可以得出以下结论:1. 一个实现特定接口的对象创建完之后赋给该接口,编译器作了一些工作,使得接口变量指向了对象内存中的某个特定 地址。2. 调用接口的方法时,实际上调用的是接口方法表中特定的地址,在该地址处编译器计算出实现该接口的对象内存首地 址,再调用对象相应的方法。接口内存空间的形成上节说明了接口在对象内存空间中的分布,但对象内存空间是在运行时生成的,那么接口的内存空间是如何生成的呢,这一节将阐述之。在此之前,让我们再回到

14、上面的对象内存图,对象内存的首地址是一个指 针,指向一张VMT表,而Delphi的类其实也是一个指针,这个指针正好也指向 VMT表。类是在编译时就确定下来的,VMT表当然也是编译器生成的。VMT表在负偏移vmtIntfTable (-72)字节处是一个指针,它指向下面的数 据结构:Pin terfaceTable = ATI nterfaceTable;Tin terfaceTable = packed recordEn tryCo unt: in teger;En tries: array0.9999 of Tin terfaceE ntry;end;EntryCount表示对象实现的接口数

15、。Entries 是一个指向 TinterfaceEntry 结构的数组,TinterfaceEntry 表示了一个接 口的进入点,它的声明如下:Pin terfaceE ntry = atI nterfaceE ntry;Tin terfaceE ntry = packed recordiiD: TGUiD;VTable: Poi nter;iOffset: in teger;implGetter: in teger;end;iiD表示接口的GUID,如果接口没有指定 GUID,则它里面的值全为0。VTable指向接口的方法表。IOffset指明接口与对象首地址的偏移。ImplGetter是

16、一个方法指针,当IOffset不可用时指向接口的地址,一般不用, 初始化为0。上面的数据结构在编译期就生成了,那么当一个对象创建时,相应的接口内存是如何生成的呢。在对象创建完毕之后,会调用TObejct .In it In sta nce(l nsta nee: Poin ter)类方法初始化对象的数据。看其代码:class function TObject.Initlnstance(lnstanee:Poi nter): TObject;$IFDEF PUREPASCALvarIn tfTable:Pin terfaceTable;ClassPtr: TClass;I: Integer;be

17、gin/将对象全部清0FillChar(I nsta nceA,In sta nceSize,0);/指定首地址为 Self,即指向VMT的指针Pin teger(I nsta nceIn teger(Self);ClassPtrSelf;建立对象的接口内存分布while ClassPtr <> nil dobegin/取得接口表In tfTableClassPtr.Get In terfaceTable;if IntfTable <> nil thendofor I := 0 to IntfTable.EntryCount-with In tfTable.E ntri

18、esldo beginif VTable <> nil then对象偏移IOffset处,设定为指向VTable的指针Pin teger(PChar(l nsta nce)IOffset)AIn teger(VTable);end ;/继续建立其父类的接口内存内存ClassPtrClassPtr.ClassPare nt;end;ResultIn sta nee;end ;我们看 Plnteger(PChar(Instance)IOffset)A := Integer(VTable)这一句, PChar(Instance)IOffset是对象偏移 IOffset 的地址,而 IOf

19、fset 是IntfTable.Entriesl的IOffset,这个值在编译期就指定了,是接口到对象的偏移 值。所以,经过上面方法调用之后,对象的内存空间就如同前面所画一样了。现在我们对接口在内存的来龙去脉已经了如指掌,可以利用这些知识来实 现一些非常的功能了。在我们的经验中,对象生成之后可以直接赋给一个接口, 编译器会自动将指针偏移到接口处。但如果反过来,将一个接口赋给一个对象却是不允许的,因为信息不足啊,任何类都可以实现这个接口,编译器并不知道这 个接口是由那个类实现的,所以就无从转换了。如果我们提供一个现实该接口的 类,再根据该类的VMT中的接口信息,就可以得到lOffset 了,如此

20、一来不就可 以偏移到对象的首地址了吗,下面的例程可以从一个接口得到实现该接口的对 象,前提是必须提供实现这个接口的类:fun cti on GetObjFroml ntf(ACIass:TClass; con st Intf: II nterface): TObject;varPI ntfTable:Pin terfaceTable;In tfE ntry: Tin terfaceE ntry;i: Integer;beginResult := n il;取得接口表结构Pin tfTable := AClass.GetI nterfaceTable;if PIntfTable = nil th

21、en Exit;while AClass <> nil dobeginfor i := 0 to PintfTableA.EntryCount- 1 dobeginIntfEntry := PI ntfTableA.E ntriesi;/判断接口表指向的地址是否和传入接口指向的地址相同if PPointer(lntf)A= intfEntry.VTablethenbegin偏移到对象首地址Result := TObject(Integer(lntf)- intfEntry.lOffset);Exit;end ;end ;/继续在父类中找AClass := AClass.ClassP

22、are nt;end;end ;看下面例子:varIntf: Itest2;Obj: TTest;beginIntf := TTest.Create;In tf.SayHello2;Obj := TTest(GetObjFromlntf(TTest,Intf);Obj.SayHello1;end ;执行上面代码,先弹出Hello2的对话框,再弹出Hello1的对象,说明 GetObjFromIntf函数执行成功,我们实现了从接口到对象的转换过程。接口的引用计数上面接口的内存空间与COM的接口在二进制上是兼容的,即接 口就是一个指向VTable的指针,与COM兼容的还有另一个特性,就是通过引 用

23、计数自动管理COM对象的生命周期。C+程序员必须手工去管理引用计数的 增减,而Delphi编译器帮我们做了这些事情,因为引用计数是有规律,只要遵 循这些规律,便能自动管理引用计数的增减。llnterface的声明如下:lln terface = in terface'00000000-0000-0000-C000-000000000046'function Querylnterface(const IID: TGUID; out Obj): HResult; stdcall;fun cti on _AddRef: In teger; stdcall;fun cti on _Re

24、lease: In teger; stdcall;end;任何实现llnterface的类都必须实现上面三个方法,其中的 _AddRef和 _Release就是实现引用计数管理的。Delphi提供了 llnterfaceObject类默认实现 In terface,它声明一个成员FRefCou nt: In teger指定引用计数,_AddRef被调用 时只是将FRefCount增1:Result := In terlockedl ncreme nt(FRefC oun t);Release被调用时,减少FRefCount,如果FRefCount为0时,即调用Destroy 消毁自己:Result := In terlockedDecreme nt(FRefC oun t);if Result = 0 thenDestroy;如果即想实现接口,而不想通过引用计数管理生命周期的,可以在AddRef 和Release中简单地将结果返回为-1即可,TComponent类即是如此

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论