接口对象的内存空间

假设我们定义了如下两个接口 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. 轻松解决ubuntu系统引导问题

    什么是ppa PPA,表示 Personal Package Archives,也就是个人软件包集. 有很多软件因为种种原因,不能进入官方的 Ubuntu 软件仓库. 为了方便 Ubuntu 用户使用 ...

  2. nginx 禁止非指定域名访问

    nginx 配置如下: server { listen 80 default_server; server_name _; return 404; } # server conf server { l ...

  3. mysql学习(七)-索引学习

    常规索引: 在常用查询的字段上使用常规索引 创建表时一块创建索引 create table if not exists carshop(id int not null auto_increment, ...

  4. jquery编写的简单日历

    以前在开发web页面的时候遇到日历,都是直接引入一些日历组件来用,一直不太明白实现原理,总感觉挺复杂的. 今天尝试着用jquery写了一个简单的日历功能,可以选择年份,月份,返回今天,原来简单的日历功 ...

  5. PHP面试题之小杂鱼

    这里的题目都是比较老的,但是做笔试题时经常碰到,因为这些题目比较凌乱,考的知识点不好分类,就放一块了 /** * 题目:最少代码实现求3个数的最大值 * 三目运算符实现 */ function get ...

  6. windows程序设计读书笔记3——字符显示2

    由于显示的字符可能会不全,我们很容易想到的一个解决办法是使用滚动条. 先看一下代码,再进行分析: /*------------------------------------------------- ...

  7. React 同构

    React 同构 搬运 https://segmentfault.com/a/1190000004671209 究竟什么是同构呢? 同构就是希望前端 后端都使用同一套逻辑 同一套代码 Nodejs出现 ...

  8. Dll注入的几个注意事项

    1. 使用钩子SetWindowHookEx注入时,设置钩子的代码必须和钩子回调函数在注入DLL中,并且调用CallNextHookEx时第一个参数必须为钩子的句柄,否则只有一个进程响应钩子. 2.关 ...

  9. CC++初学者编程教程(8) VS2013配置编程助手与QT

    1. 2. 配置编程助手 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26 ...

  10. 2014第13周四Webservice概念问题记

    晚上回来看网页学习了这两天一直疑惑的两个问题: 1.REST和SOAP架构下的Webservice的区别? 2.axis2和CXF的区别. 大部分是理论,暂时摘录一下,以后有更多实践后再回顾. 一.R ...