zoukankan      html  css  js  c++  java
  • Delphi 的接口机制——接口操作的编译器实现过程(2)

    接口对象的内存空间


            假设我们定义了如下两个接口 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

  • 相关阅读:
    how to pass a Javabean to server In Model2 architecture.
    What is the Web Appliation Archive, abbreviation is "WAR"
    Understaning Javascript OO
    Genetic Fraud
    poj 3211 Washing Clothes
    poj 2385 Apple Catching
    Magic Star
    关于memset的用法几点
    c++ 函数
    zoj 2972 Hurdles of 110m
  • 原文地址:https://www.cnblogs.com/findumars/p/5122544.html
Copyright © 2011-2022 走看看