码迷,mamicode.com
首页 > Windows程序 > 详细

Delphi 的接口机制——接口操作的编译器实现过程(2)

时间:2016-01-11 22:12:59      阅读:338      评论:0      收藏:0      [点我收藏+]

标签:

接口对象的内存空间


        假设我们定义了如下两个接口 IIntfA 和 IIntfB,其中 ProcA 和 ProcB 将实现为静态方法,而 VirtA 和 VirtB 将以虚方法实现:

[delphi] view plaincopyprint?
 
  1. IIntfA = interface  
  2.     procedure ProcA;  
  3.     procedure VirtA;  
  4.   end;  
  5.   
  6.   IIntfB = interface  
  7.     procedure ProcB;  
  8.     procedure VirtB;  
  9.   end;  


       然后我们定义一个 TMyObject 类,它继承自 TInterfacedObject,并实现 IIntfA 和 IIntfB 两个接口:

[delphi] view plaincopyprint?
 
  1. TMyObject = class(TInterfacedObject, IIntfA, IIntfB)  
  2.     FFieldA: Integer;  
  3.     FFieldB: Integer;  
  4.     procedure ProcA;  
  5.     procedure VirtA; virtual;  
  6.     procedure ProcB;  
  7.     procedure VirtB; virtual;  
  8.   end;  

       然后我们执行以下代码:

[delphi] view plaincopyprint?
 
  1. var  
  2.     MyObject: TMyObject;  
  3.     MyIntf:  IInterface;  
  4.     MyIntfA: IIntfA;  
  5.     MyIntfB: IIntfB;  
  6.   begin  
  7.     MyObject := TMyObject.Create;  // 创建 TMyObject 对象  
  8.     MyIntf  := MyObject;           // 将接口指向 MyObject 对象  
  9.     MyIntfA := MyObject;  
  10.     MyIntfB := MyObject;  
  11.   end;  



        以上代码的执行过程中,编译器实现的内存空间情况图如下所示:

技术分享
        先看最左边一列。MyObject 是对象指针,指向对象数据空间中的 0 偏移处(虚方法表指针)。可以看到 MyIntf/MyIntfA/MyIntfB 三个接口都实现为指针,这三个指针分别指向 MyObject 对象数据空间中一个 4 bytes 的区域。
       中间一列是对象内存空间。可以看到,与不支持接口的对象相比,TMyObject 的对象内存空间中增加了三个字段:IInterface/IIntfB/IIntfA。这些字段也是指针,指向“接口跳转表”的内存地址。注意 MyIntfA/MyIntfB 的存放顺序与 TMyObject 类声明的顺序相反,为什么?
       第三列是类的虚方法表,与一般的类(不支持接口的类)一致。
-----------
接口跳转表
-----------
     “接口跳转表”就是一排函数指针,指向实现当前接口的函数地址,这些函数按接口中声明的顺序排列。现在让我们来看一看所谓的“接口跳转表”有什么用处。
       我们知道,一个对象在调用类的成员函数的时候,比如执行 MyObject.ProcA,会隐含传递一个 Self 指针给这个成员函数:MyObject.ProcA(Self)。Self 就是对象数据空间的地址。那么编译器如何知道 Self 指针?原来对象指针 MyObject 指向的地址就是 Self,编译器直接取出 MyObject^ 就可以作为 Self。
       在以接口的方式调用成员函数的时候,比如 MyIntfA.ProcA,这时编译器不知道 MyIntfA 到底指向哪种类型(class)的对象,无法知道 MyIntfA 与 Self 之间的距离(实际上,在上面的例子中 Delphi 编译器知道 MyIntfA 与 Self 之间的距离,只是为了与 COM 的二进制格式兼容,使其它语言也能够使用接口指针调用接口成员函数,必须使用后期的 Self 指针修正),编译器直接把 MyIntfA 指向的地址设置为 Self。从上图可以看到,MyIntfA 指向 MyObject 对象空间中 $18 偏移地址。这时的 Self 指针当然是错误的,编译器不能直接调用 TMyObject.ProcA,而是调用 IIntfA 的“接口跳转表”中的 ProcA。“接口跳转表”中的 ProcA 的内容就是对 Self 指针进行修正(Self - $18),然后再调用 TMyObject.ProcA,这时就是正确调用对象的成员函数了。由于每个类实现接口的顺序不一定相同,因此对于相同的接口在不同的类中实现,就有不同的接口跳转表(当然,可能编辑器能够聪明地检查到一些类的“接口跳转表”偏移量相同,也可以共享使用)。
       上面说的是编译器的实现过程,使用“接口跳转表”真正的原因是 interface 必须支持 COM 的二进制格式标准。下图是从《〈COM 原理与应用〉学习笔记》中摘录的 COM 二进制规格图:

技术分享
----------------------------------------
对象内存空间中接口跳转指针的初始化
----------------------------------------
       还有一个问题,那就是对象内存空间中的接口跳转指针是如何初始化的。原来,在TObject.InitInstance 中,用 FillChar 清零对象内存空间后,进行的工作就是初始化对象的接口跳转指针:

[delphi] view plaincopyprint?
 
  1. function TObject.InitInstance(Instance: Pointer): TObject;  
  2. var  
  3.  IntfTable: PInterfaceTable;  
  4.  ClassPtr: TClass;  
  5.  I: Integer;  
  6.  begin  
  7.     FillChar(Instance^, InstanceSize, 0);  
  8.     PInteger(Instance)^ := Integer(Self);  
  9.     ClassPtr := Self;  
  10.     while ClassPtr <> nil do  
  11.     begin  
  12.       IntfTable := ClassPtr.GetInterfaceTable;  
  13.       if IntfTable <> nil then  
  14.         for I := to IntfTable.EntryCount-do  
  15.     with IntfTable.Entries[I] do  
  16.     begin  
  17.       if VTable <> nil then  
  18.         PInteger(@PChar(Instance)[IOffset])^ := Integer(VTable);  
  19.     end;  
  20.       ClassPtr := ClassPtr.ClassParent;  
  21.     end;  
  22.     Result := Instance;  
  23.   end;  

----------------------
implements 的实现
----------------------
       Delphi 中可以使用 implements 关键字将接口方法委托给另一个接口或对象来实现。下面以 TMyObject 为基类,考查 implements 的实现方法。

[delphi] view plaincopyprint?
 
  1. TMyObject = class(TInterfacedObject, IIntfA, IIntfB)  
  2.     FFieldA: Integer;  
  3.     FFieldB: Integer;  
  4.     procedure ProcA;  
  5.     procedure VirtA; virtual;  
  6.     procedure ProcB;  
  7.     procedure VirtB; virtual;  
  8.     destructor Destroy; override;  
  9.   end;  

      (1)以接口成员变量实现 implements

[delphi] view plaincopyprint?
 
  1. TMyObject2 = class(TInterfacedObject, IIntfA)  
  2. FIntfA: IIntfA;  
  3. property IntfA: IIntfA read FIntfA implements IIntfA;  
  4. end;  

         这时编译器的实现是非常简单的,因为 FIntfA 就是接口指针,这时如果使用接口赋值 MyIntfA := MyObject2 这样的语句调用时,MyIntfA 就直接指向 MyObject2.FIntfA。

      (2)以对象成员变量实现 implements

       如下例,如果一个接口类 TMyObject3 以对象的方式实现 implements (通常应该是这样),其对象内存空间的排列与TMyObject内存空间情况几乎是一样的:

[delphi] view plaincopyprint?
 
  1. TMyObject3 = class(TInterfacedObject, IIntfA, IIntfB)  
  2.     FMyObject: TMyObject;  
  3.     function GetMyObject: TMyObject;  
  4.     property MyObject: TMyObject read GetMyObject implements IIntfA, IIntfB;  
  5.   end;  



       不同的地方在于 TMyObject3 的“接口跳转表”的内容发生了变化。由于 TMyObject3 并没有自己实现 IIntfA 和 IIntfB,而是由 FMyObject 对象来实现这两个接口。这时,“接口跳转表”中调用的方法就必须改变为调用 FMyObject 对象的方法。比如下面的代码:

[delphi] view plaincopyprint?
 
  1. var  
  2.     MyObject3: TMyObject3;  
  3.     MyIntfA: IIntfA;  
  4.   begin  
  5.     MyObject3:= TMyObject3.Create;  
  6.     MyObject3.FMyObject := TMyObject.Create;  
  7.     MyIntfA := MyObject3;  
  8.     MyIntfA._AddRef;  
  9.     MyIntfA.ProcA;  
  10.     MyIntfA._Release;  
  11.   end;  


       当执行 MyIntfA._AddRef 语句时,编译器生成的“接口跳转”代码为:

[delphi] view plaincopyprint?
 
  1. {MyIntfA._AddRef;}  
  2. mov eax,[ebp-$0c]              // eax = MyIntfA^  
  3. push eax                       // MyIntfA^ 设置为 Self  
  4. mov eax,[eax]                  // eax = 接口跳转表地址指针  
  5. call dword ptr [eax+$04]       // 转到接口跳转表  
  6.   
  7. { “接口跳转段”中的代码 }  
  8. mov eax,[esp+$04]              // [esp+$04] 是接口指针内容 (MyIntfA^)  
  9. add eax,-$14                   // 修正 eax = Self (MyObject2)  
  10. call TMyObject2.GetMyObject  
  11. mov [esp+$04],eax              // 获得 FMyObject 对象,注意 [esp+$04]  
  12. jmp TInterfacedObject._AddRef  // 调用 FMyObject._AddRef  

          [esp+$04] 是值得注意的地方。“接口跳转表”中只修正一个参数 Self,其它的调用参数(如果有的话)在执行过程进入“接口跳转表”之前就由编译器设置好了。在这里 _AddRef 是采用 stdcall 调用约定,因此 esp+$04 就是 Self。前面说过,编译器直接把接口指针的内容作为 Self 参数,然后转到“接口跳转表”中对 Self 进行修正,然后才能调用对象方法。上面的汇编代码就是修正 Self 为 FMyObject 并调用 FMyObject 的方法。
       可以看到 FMyObject._AddRef 方法增加的是 FMyObject 对象的引用计数,看来 implements 的实现只是简单地把接口传送给对象执行,而要实现 COM 组件聚合,必须使用其它方法。

http://blog.csdn.net/tht2009/article/details/6768032

Delphi 的接口机制——接口操作的编译器实现过程(2)

标签:

原文地址:http://www.cnblogs.com/findumars/p/5122544.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!