接口对象的内存空间

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

  1. IIntfA = interface
  2. procedure ProcA;
  3. procedure VirtA;
  4. end;
  5. IIntfB = interface
  6. procedure ProcB;
  7. procedure VirtB;
  8. end;

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

  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;

然后我们执行以下代码:

  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 清零对象内存空间后,进行的工作就是初始化对象的接口跳转指针:

  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 := 0 to IntfTable.EntryCount-1 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 的实现方法。

  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

  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内存空间情况几乎是一样的:

  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 对象的方法。比如下面的代码:

  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 语句时,编译器生成的“接口跳转”代码为:

  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. mov eax,[esp+$04]              // [esp+$04] 是接口指针内容 (MyIntfA^)
  8. add eax,-$14                   // 修正 eax = Self (MyObject2)
  9. call TMyObject2.GetMyObject
  10. mov [esp+$04],eax              // 获得 FMyObject 对象,注意 [esp+$04]
  11. 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)的更多相关文章

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

    学习COM编程技术也快有半个月了,这期间看了很多资料和别人的程序源码,也尝试了用delphi.C++.C#编写COM程序,个人感觉Delphi是最好上手的.C++的模版生成的代码太过复杂繁琐,大量使用 ...

  2. java8中的接口与时间操作

    java8中接口可以有默认方法(用default修饰,可以有多个)和静态方法了. public interface Tran { default public String getName() { r ...

  3. MySQL数据库学习笔记(九)----JDBC的ResultSet接口(查询操作)、PreparedStatement接口重构增删改查(含SQL注入的解释)

    [声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...

  4. c++关于接口机制和不完全类型的小问题

    都和typedef有关 一个是接口机制时用到的 就是所有用到接口的源文件只需包含简单的接口声明 接口的具体实现在其他源文件中实现 接口可以是 //interface.h typedef struct ...

  5. Java 8-Lambda表达式、方法引用、标准函数接口与流操作、管道操作之间的关系

    1.Lambda表达式与接口之间的关系 只要Lambda表达式的声明形式与接口相一致,在很多情况下都可以替换接口.见如下代码 Thread t1 = new Thread(new Runnable() ...

  6. Go part 6 接口,接口排序,接口嵌套组合,接口与类型转换,接口断言

    接口 接口是一种协议,比如一个汽车的协议,就应该有 “行驶”,“按喇叭”,“开远光” 等功能(方法),这就是实现汽车的协议规范,完成了汽车的协议规范,就实现了汽车的接口,然后使用接口 接口的定义:本身 ...

  7. C#-概念-接口:接口

    ylbtech-C#-概念-接口:接口 接口(硬件类接口)是指同一计算机不同功能层之间的通信规则称为接口. 接口(软件类接口)是指对协定进行定义的引用类型.其他类型实现接口,以保证它们支持某些操作.接 ...

  8. Java中的集合(七)双列集合顶层接口------Map接口架构

    Java中的集合(七)双列集合顶层接口------Map接口 一.Map接口的简介 通过List接口,我们知道List接口下的集合是单列集合,数据存储是单列的结构.Map接口下是一个键值对(key-v ...

  9. java:接口特性 接口与抽象类/普通类的区别

    接口 书面定义: Java接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能). 在ja ...

随机推荐

  1. php的SQL连接操作的方法

    初学php,于是一开始就在sql(具体点说是mysql)里受挫严重.于是这里记下一些方法. 首先是基本方法.基本方法现在我不用了,所以我就附一下hustoj中的部分吧 基本方法的数据库连接操作: if ...

  2. Molecule to atoms

    For a given chemical formula represented by a string, count the number of atoms of each element cont ...

  3. windows系统——mysql自动定时备份数据库的最佳方法

    网上有很多关于window下Mysql自动备份的方法,可是真的能用的也没有几个,有些说的还非常的复杂,难以操作. 我们都知道mssql本身就自带了计划任务可以用来自动备份,可是mysql咱们要怎么样自 ...

  4. 存储过程与SQL的结合使用

    --1调用存储过程exec 存储过程名 参数 openrowset方法使用: select * from openrowset('sqlncli', 'server=192.168.247.64;ui ...

  5. 快速学习javascript对象-遍历对象

    为了方便了解每个javascript对象包含的方法,我写一个函数. function GetCollection(obj){ try{ if(obj){ var sType=""; ...

  6. 嵌入式平台组件白盒测试gcov、lcov和genhtml 使用指导

    在嵌入式平台上使用了gtest白盒测试工具,覆盖了被测函数,但是不知道自己测试的效果如何,测试行覆盖率.函数覆盖率,分支覆盖率的数据. 便开始研究gcov这个代码覆盖率工具能否使用,来检查白盒测试的效 ...

  7. 使用ASP.NET MVC+Entity Framework快速搭建博客系统

    学习 ASP.NET MVC 也有一段时间了,打算弄个小程序练练手,做为学习过程中的记录和分享. 首先,得确定需求,木有需求的话,那还搞个毛线呀!嗯……大致思考了一下,终于得出如下需求: 1.能自定义 ...

  8. Android06-Fragment碎片

    ¨Fragment简介 ¨Fragment生命周期 ¨动态加载碎片Fragment Manager的使用   1.Fragment表示Activity中的一种行为或者一部分用户界面. 可以将Fragm ...

  9. PSAM SAM

    第一个问题: 为什么要用SAM? 究竟谁最开始使用SAM这个词,已经无从考证,能够确认的是:这个世界上先有了PSAM,然后才有了SAM.由于网络状况的原因,或者是应用环境的要求,使用IC卡作为支付介质 ...

  10. Aix 文件名补齐及aix6.1 bash安装

    Aix历史查询快捷键=>按ESC+k 设置KSH的自动补全(仅仅是文件名补全,没有命令补全)和历史命令功能 方法一: set -o vi 历史命令功能(esc -,esc +)自动补全文件名(e ...