




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、Delphi 的接口机制浅探Delphi 的 Open Tools API 全是用 interface 实现的。为了学习 Open Tools API,只好先学 interface ;为了学习 interface ,又必须先学 COM 。这样,我先花了三天看完了 COM 原理,再花二天考查 Delphi 的 interface 实现过程,得以整理出此文。这不是一篇关于接口应用的文章,而是接口操作的编译器实现过程。我还没有真正使用过接口,也不知道包容和聚合模型是如何实现的,更不知道 ActiveX 和 OLE 是何物,所以不要问我应用方面的问题,我现在真的不懂。我倒是很希望有人能说一说 Delp
2、hi 的聚合是如何实现的(欢迎在本贴回复 。在 interface 的学习过程中,我觉得对 COM 原理的理解是很重要的。本来我不想公开的自己的 COM 学习笔记,毕竟那是潘爱民的作品。不过我想如果有人和我一样,是完全不懂 COM 也希望能了解 interface 的实现机制,我愿意将此笔记公开作为本文的参考,希望不要在公共媒体张贴。目 录= 接口的引用计数管理接口指针总是被初始化为 nil接口指针赋值为对象接口指针赋值为接口指针接口引用计数使用规则小结 接口对象的编译器实现接口对象的内存空间接口跳转表对象内存空间中接口跳转指针的初始化 implements 的实现以接口成员变量实现 impl
3、ements以对象成员变量实现 implements=本文排版格式为:正文由窗口自动换行;所有代码以 80 字符为边界;中英文字符以空格符分隔。(作者保留对本文的所有权利,未经作者同意请勿在在任何公共媒体转载。正 文= 接口的引用计数管理=-接口指针总是被初始化为 nil-接口是生存期自管理对象,即使是局部接口指针,也总是被初始化为 nil 。接口指针被初始化为 nil 是很重要的,从下文中 Delphi 生成维护接口引用计数的代码时可以看到这一点。当接口与一个对象连接时,编译器会执行一些特殊的代码维护接口对象的引用计数。例如以下代码:varMyObject: TMyObject;MyIntf
4、, MyIntf2: IInterface;beginMyObject := TMyObject.Create; / 创建 TMyObject 对象MyIntf := MyObject; / 将接口指向 MyObject 对象MyIntf2 := MyIntf; / 接口指针的赋值end;-接口指针赋值为对象-当执行到 MyIntf := MyObject 语句时,编译器的实现是:1. 如果 MyObject <> nil,则设置一临时接口指针 P 指向 MyObject 对象内存空间中的“接口跳转表”指针(后面会分析“接口跳转表”;否则 P := nil;2. 执行 System
5、.pas 中的 _IntfCopy(MyIntf, P 操作,进行引用计数管理; System.pas procedure _IntfCopy(var Dest: IInterface; const Source: IInterface;varP: Pointer;beginP := Pointer(Dest; / 保存目的接口指针,用于后面的 Release 调用if Source <> nil then / 源接口指针增加引用计数Source._AddRef;Pointer(Dest := Pointer(Source; / 目的接口指针赋值为源接口指针if P <>
6、; nil then / 原目的接口指针减少引用计数IInterface(P._Release;end;_IntfCopy 的代码比较简单,就是增加 Source 接口对象的引用计数,减少被赋值的接口对象的引用计数,最后把源接口指针赋值至目标接口指针。(其中还有源接口指针为 nil 的情况,看源代码比我说得还要清楚-接口指针赋值为接口指针-对于两个接口指针的赋值的情况,如MyIntf2 := MyIntf,这时比 MyIntf := MyObject 的情况要简单一些,编译器不需要进行对象到接针的转换工作,这时真正执行的代码是:_IntfCopy(MyIntf2, MyIntf。-接口对象清除
7、工作-在一个过程(procedure/function执行结束时,编译器会生成代码减少接口对象的引用计数。编译器使用接口指针为参数调用 _IntfClear 函数,_IntfClear 函数的作用是减少接口对象的引用计数并设置接口指针为 nil : System.pas function _IntfClear(var Dest: IInterface: Pointer;varP: Pointer;beginResult := Dest;if Dest <> nil thenbeginP := Pointer(Dest;Pointer(Dest := nil;IInterface(P
8、._Release;end;end;-接口引用计数使用规则小结-根据以上代码及分析,我们可以小结过程(procedure/function中的接口引用计数使用规则:1. 一般不需要使用 _AddRef/_Release 函数设置接口引用计数;2. 可以将接口赋值为接口或对象,Delphi 自动处理源/目标接口对象的引用计数;3. 如果要提前释放接口对象,可以设置接口指针为 nil ,但不要调用 _Release。因为 _Release 不会把接口指针设置为 nil ,最后 Delphi 自动调用 _IntfClear时会出错;下面我们看看将接口指针作为参数传送时的情况:1. 以 var 或 c
9、onst 方式传递接口指针时,像普通的参数传递一样。2. 以 out 方式传递接口指针时,编译器会先调用 _IntfClear 函数减少引用计数,清除接口指针为 nil 。(out 也是以引用方式传送参数 。3. 以传值方式传递接口指针时,编译器会在参数被使用之前调用 _IntfAddRef 函数增加引用计数,在过程结束之前调用 _IntfClear 函数减少引用计数。(* 为什么以传值方式要特别处理引用计数呢?因为复制了接口指针? System.pas procedure _IntfAddRef(const Dest: IInterface;beginif Dest <> nil
10、 then Dest._AddRef;end;对于全局接口指针变量,在接口变量被赋值时增加对象的引用计数,在程序退出之前编译器自动调用 _IntfClear 函数减少引用计数以清除对象。= 接口对象的编译器实现=-接口对象的内存空间-假设我们定义了如下两个接口 IIntfA 和 IIntfB ,其中 ProcA 和 ProcB 将实现为静态方法,而 VirtA 和 VirtB 将以虚方法实现:IIntfA = interfaceprocedure ProcA;procedure VirtA;end;IIntfB = interfaceprocedure ProcB;procedure Virt
11、B;end;然后我们定义一个 TMyObject 类,它继承自 TInterfacedObject ,并实现 IIntfA 和 IIntfB 两个接口:TMyObject = class(TInterfacedObject, IIntfA, IIntfBFFieldA: Integer;FFieldB: Integer;procedure ProcA;procedure VirtA; virtual;procedure ProcB;procedure VirtB; virtual;end;然后我们执行以下代码:varMyObject: TMyObject;MyIntf: IInterface;
12、MyIntfA: IIntfA;MyIntfB: IIntfB;beginMyObject := TMyObject.Create; / 创建 TMyObject 对象MyIntf := MyObject; / 将接口指向 MyObject 对象MyIntfA := MyObject;MyIntfB := MyObject;end;以上代码的执行过程中,编译器实现的内存空间情况如下:( 后文简称“图一” -|-|-|-|-对象/接口指针 | 对象内存空间 | | 虚方法表 |-|-|-|-|-MyObject -> | VMTptr 00|->| VirtA 00| FRefCou
13、nt 04| | VirtB 04|MyIntf -> | IInterface 08|-| FFieldA 0C| | | IInterface 跳转表 | FFieldB 10| |-> | addr of QueryInterface |MyIntfB -> | IIntfB 14|-| | addr of _AddRef |MyIntfA -> | IIntfA 18|-| | | addr of _Release | | | | IIntfB 跳转表 | |-> | addr of ProcB | | addr of VirtB | | IIntfA 跳
14、转表 |-> | addr of ProcA | addr of VirtA |-先看最左边一列。MyObject 是对象指针,指向对象数据空间中的 0 偏移处(虚方法表指针 。可以看到 MyIntf/MyIntfA/MyIntfB 三个接口都实现为指针,这三个指针分别指向 MyObject 对象数据空间中一个 4 bytes 的区域。中间一列是对象内存空间。可以看到,与不支持接口的对象相比,TMyObject 的对象内存空间中增加了三个字段:IInterface/IIntfB/IIntfA这些字段也是指针,指向(我暂命名为“接口跳转表”的内存地址。(* 注意 MyIntfA/MyInt
15、fB 的存放顺序与 TMyObject 类声明的顺序相反,为什么?第三列是类的虚方法表,与一般的类(不支持接口的类 一致。-接口跳转表 -"接口跳转表"就是一排函数指针,指向实现当前接口的函数地址,这些函数按接口中声明的顺序排列.现 在让我们来看一看所谓的"接口跳转表"有什么用处. 我们知道,一个对象在调用类的成员函数的时候,比如执行 MyObject.ProcA,会隐含传递一个 Self 指针 给这个成员函数: MyObject.ProcA(Self. Self 就是对象数据空间的地址. 那么编译器如何知道 Self 指针? 原来对象指针 MyObje
16、ct 指向的地址就是 Self,编译器直接取出 MyObject 就可以作为 Self. 在以接口的方式调用成员函数的时候,比如 MyIntfA.ProcA,这时编译器不知道 MyIntfA 到底指向哪种类 型(class的对象,无法知道 MyIntfA 与 Self 之间的距离(实际上,在上面的例子中 Delphi 编译器知道 MyIntfA 与 Self 之间的距离,只是为了与 COM 的二进制格式兼容,使其它语言也能够使用接口指针调 用接口成员函数,必须使用后期的 Self 指针修正,编译器直接把 MyIntfA 指向的地址设置为 Self.从上 图可以看到,MyIntfA 指向 MyO
17、bject 对象空间中 $18 偏移地址.这时的 Self 指针当然是错误的,编译 器不能直接调用 TMyObject.ProcA,而是调用 IIntfA 的"接口跳转表"中的 ProcA."接口跳转表"中的 ProcA 的内容就是对 Self 指针进行修正(Self - $18,然后再调用 TMyObject.ProcA,这时就是正确调用对 象的成员函数了.由于每个类实现接口的顺序不一定相同,因此对于相同的接口在不同的类中实现,就有 不同的接口跳转表(当然,可能编辑器能够聪明地检查到一些类的"接口跳转表"偏移量相同,也可以共享使 用
18、. 上面说的是编译器的实现过程,使用"接口跳转表"真正的原因是 interface 必须支持 COM 的二进制格式 标准.下图是我从 COM 原理与应用学习笔记中摘录的 COM 二进制规格: 接口指针 -> pVtable -> 指针函数 1 -> |-| 指针函数 2 -> | 对象实现 | 指针函数 3 -> |-| -对象内存空间中接口跳转指针的初始化 -还有一个问题,那就是对象内存空间中的接口跳转指针是如何初始化的.原来,在 TObject.InitInstance 中, 用 FillChar 清零对象内存空间后,进行的工作就是初始化对
19、象的接口跳转指针: (* 我还没有细看 class function TObject.InitInstance(Instance: Pointer: TObject; var IntfTable: PInterfaceTable; ClassPtr: TClass; I: Integer; begin FillChar(Instance, InstanceSize, 0; PInteger(Instance := Integer(Self; ClassPtr := Self; while ClassPtr <> nil do begin IntfTable := ClassPtr.
20、GetInterfaceTable; if IntfTable <> nil then for I := 0 to IntfTable.EntryCount-1 do with IntfTable.EntriesI do begin if VTable <> nil then PInteger(PChar(InstanceIOffset := Integer(VTable; end; ClassPtr := ClassPtr.ClassParent; end; Result := Instance; end; = implements 的实现 = Delphi 中可以使
21、用 implements 关键字将接口方法委托给另一个接口或对象来实现. 下面以 TMyObject 为基类,考查 implements 的实现方法. TMyObject = class(TInterfacedObject, IIntfA, IIntfB FFieldA: Integer; FFieldB: Integer; procedure ProcA; procedure VirtA; virtual; procedure ProcB; procedure VirtB; virtual; destructor Destroy; override; end; -以接口成员变量实现 impl
22、ements -TMyObject2 = class(TInterfacedObject, IIntfA FIntfA: IIntfA; property IntfA: IIntfA read FIntfA implements IIntfA; end; 这时编译器的实现是非常简单的,因为 FIntfA 就是接口指针,这时如果使用接口赋值 MyIntfA := MyObject2 这样的语句调用时,MyIntfA 就直接指向 MyObject2.FIntfA. -以对象成员变量实现 implements -如下例,如果一个接口类 TMyObject2 以对象的方式实现 implements (
23、通常应该是这样,其对象内存空间 的排列与 TMyObject(见"图一"几乎是一样的: TMyObject2 = class(TInterfacedObject, IIntfA, IIntfB FMyObject: TMyObject; function GetMyObject: TMyObject; property MyObject: TMyObject read GetMyObject implements IIntfA, IIntfB; end; 不同的地方在于 TMyObject2 的"接口跳转表"的内容发生了变化.由于 TMyObject2
24、并没有自己实现 IIntfA 和 IIntfB,而是由 FMyObject 对象来实现这两个接口.这时,"接口跳转表"中调用的方法就必须改 变为调用 FMyObject 对象的方法.比如下面的代码: var MyObject2: TMyObject2; MyIntfA: IIntfA; begin MyObject2 := TMyObject2.Create; MyObject2.FMyObject := TMyObject.Create; MyIntfA := MyObject2; MyIntfA._AddRef; MyIntfA.ProcA; MyIntfA._Release; end; 当执行 My
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025企业软件外包合同
- 2025建筑室内设计合同协议书范本
- 2025年北京房屋买卖合同范本
- 2025合同法深度解析:无固定期限合同条款详解
- 苏州工业园区翰林小学等苏教版三年级数学下册单元试卷15份
- 二零二五版地质勘察技术服务合同
- 二零二五二手房公积金贷款买卖合同书
- 水田承包使用权转让合同书二零二五年
- 二零二五海外工程项目投标策略及合同管理
- 二零二五家庭居室装饰装修合同书
- 四川政采评审专家入库考试基础题复习测试附答案
- 一轮复习课件:《古代欧洲文明》
- 安装悬浮地板合同范例
- 土族课件教学课件
- 团体医疗补充保险方案
- DB41T 1836-2019 矿山地质环境恢复治理工程施工质量验收规范
- 2024年江苏省高考政治试卷(含答案逐题解析)
- 培训调查问卷分析报告
- 肝癌肝移植中国指南解读
- 2024版年度中华人民共和国传染病防治法
- 后厨岗位招聘笔试题及解答(某大型央企)2025年
评论
0/150
提交评论