您的位置:首页 > 编程语言 > Delphi

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

2018-02-16 06:36 471 查看
来源:https://www.cnblogs.com/findumars/p/5008571.html   学习COM编程技术也快有半个月了,这期间看了很多资料和别人的程序源码,也尝试了用delphi、C++、C#编写COM程序,个人感觉Delphi是最好上手的。C++的模版生成的代码太过复杂繁琐,大量使用编译宏替代函数代码,让初学者知其然而不知其所以然;C#封装过度,COM编程注定是要与操作系统频繁打交道的,需要调用大量API函数和使用大量系统预定义的常量与类型(结构体),这些在C#中都需手工声明,不够简便;Delphi就简单多了,通过模版创建的工程代码关系结构非常清晰,而且其能非常容易使用API函数和系统预定义的常量和类型(只需引用先关单元即可),但在使用过程中也发现了一些缺点。【注1】       (1)有些类型(结构体)的成员类型与C++中的不是等效对应关系,如SHFileOperation函数的参数类型是SHFILEOPSTRUCT结构体,delphi中它的两个路径成员被定义成PWideChar型,与C++的LPCTSTR不一致,PWideChar是以空字符(\0)结尾的,致使这两个成员不能包含多个文件路径。【注2】       (2)有些接口的函数参数定义不一致,如IContextMenu.InvokeCommand函数参数在Delphi中是CMINVOKECOMMANDINFO类型,在c++中是LPCMINVOKECOMMANDINFO型 ,致使该接口函数不能使用扩展的CMINVOKECOMMANDINFOEX型参数。【注3】        Delphi操作COM的另一便处在于他的接口的引用计数管理,这为我们写程序解决了一大麻烦:不用管接口的AddRef和Release了,直接把接口当“接口指针变量”(【注4】)使用,编译器会执行一些特殊的代码自动维护接口的引用计数。当然,这也会带来另一个问题,接口相当于“变量”一样使用,这就涉及到“变量”的生命周期问题,当把这样一个局部“变量”通过强制类型转换(【注5】)给一个全局变量时,待之后转换回来时将引发错误。因为局部“变量”生命已结束,要被清理,其所代表的接口被减少引用计数释放了,如果人为让“变量”AddRef一次,就能消除这个错误。       关于Delphi的接口引用计数管理,在网上看到的一篇介绍的文章,查很久了它的出处,目前已知最早是SaveTime于2004年2月3日发表于大富翁论坛。【注6】       下面将它整理了一下,以便加深对delphi对接口引用计数的理解。 

接口指针变量赋值

       接口是生存期自管理对象,即使是局部接口指针变量,也总是被初始化为 nil。接口被初始化为nil是很重要的,从下文中Delphi生成维护接口引用计数的代码时可以看到这一点。 [delphi] view plaincopyprint? var  
    MyObject: TMyObject;  
    MyIntf, MyIntf2: IInterface;  
  begin  
    MyObject := TMyObject.Create;  // 创建 TMyObject 对象  
    MyIntf  := MyObject;           // 将接口指向 MyObject 对象  
    MyIntf2 := MyIntf;             // 接口指针的赋值  
  end;  
         当接口与一个对象连接时,编译器会执行一些特殊的代码维护接口对象的引用计数。例如以上代码,当执行到MyIntf :=MyObject 语句时,编译器的实现是:            1. 如果 MyObject <> nil,则设置一临时接口指针 P 指向 MyObject 对象内存空间中的“接口跳转表”指针(后面会分析“接口跳转表”),否则 P := nil;            2. 执行 System.pas 中的 _IntfCopy(MyIntf, P) 操作,进行引用计数管理。[delphi] view plaincopyprint? procedure _IntfCopy(var Dest: IInterface; const Source: IInterface);  
var  
  P: Pointer;  
begin  
  P := Pointer(Dest);  
  if Source <> nil then  
    Source._AddRef;  
  Pointer(Dest) := Pointer(Source);  
  if P <> nil then  
    IInterface(P)._Release;  
end;  
        函数_IntfCopy 的代码比较简单,就是增加 Source 接口对象的引用计数,减少被赋值的接口对象的引用计数,最后把源接口赋值至目标接口。       对于两个接口的赋值的情况,如MyIntf2 := MyIntf,这时比 MyIntf := MyObject 的情况要简单一些,编译器不需要进行对象到接口的转换工作,这时真正执行的代码是:_IntfCopy(MyIntf2, MyIntf)。 

接口指针变量的清除工作

             在一个过程(procedure/function)执行结束时,编译器会生成代码减少接口指针变量的引用计数。编译器使用接口指针为参数调用 _IntfClear 函数,_IntfClear 函数的作用是减少接口对象的引用计数并设置接口为 nil :[delphi] view plaincopyprint? function _IntfClear(var Dest: IInterface): Pointer;    
var   
P:Pointer;    
begin     
 Result := @Dest;     
 if Dest <> nil then      
    begin        
       P := Pointer(Dest);        
       Pointer(Dest) := nil;        
       IInterface(P)._Release;      
    end;    
end;  
       通过对以上代码及分析,我们可以总结过程(procedure/function)中的接口引用计数使用规则:
       1. 一般不需要使用 _AddRef/_Release 函数设置接口引用计数;
       2. 可以将接口赋值为接口或对象,Delphi 自动处理源/目标接口对象的引用计数;
       3. 如果要提前释放接口对象,可以设置接口指针为 nil,但不要调用 _Release。因为 _Release 不会把接口指针变量设置为 nil,最后 Delphi 自动调用 _IntfClear时会出错。      对于全局接口指针变量,在接口指针变量被赋值时增加对象的引用计数,在程序退出之前编译器自动调用 _IntfClear 函数减少引用计数以清除对象。


接口指针作为参数

       1. 以var 或const 方式传递接口指针时,像普通的参数传递一样。
       2. 以out 方式传递接口指针时,编译器会先调用_IntfClear 函数减少引用计数,清除接口指针为 nil 。(out 也是以引用方式传送参数)。
       3. 以传值方式传递接口指针时,编译器会在参数被使用之前调用_IntfAddRef 函数增加引用计数,在过程结束之前调用_IntfClear 函数减少引用计数。[delphi] view plaincopyprint? { System.pas }  
  procedure _IntfAddRef(const Dest: IInterface);  
  begin  
    if Dest <> nil then Dest._AddRef;  
  end;  
       为什么以传值方式要特别处理引用计数呢?因为复制了接口指针。       下一节介绍接口对象的内存空间。     1   我用的是Delphi2010,更新的XE、XE2版本可能已更正了这些问题,在此举例说明而已。2   有关结构体SHFILEOPSTRUCT及其两个路径成员的详细介绍请参见http://blog.csdn.net/tht2009/article/details/6753706http://msdn.microsoft.com/en-us/library/bb759795(VS.85).aspx。3   有关接口函数InvokeCommand的详细介绍请参见http://msdn.microsoft.com/en-us/library/bb776096(VS.85).aspx。4   我也不知严格上能否这样称呼,姑且这样类比吧!5   如通过Pointer(IShellFolder)将一个局部声明的IShellFolder接口保存到一个Pointer型的变量Data中,通过Data:=Pointer(IShellFolder)不会增加IShellFolder接口对象的引用。实际中很少遇到这种情况,我也是在无意中发现这个问题的。6   请见http://blog.csdn.net/huangsn10/article/details/6112546,由于大富翁论坛好像已关闭了,所以真正出处已无从考证。http://blog.csdn.net/tht2009/article/details/6767435
来源:http://blog.csdn.net/ilvu999/article/details/8149458接口对象的内存空间
        假设我们定义了如下两个接口 IIntfA 和 IIntfB,其中 ProcA 和 ProcB 将实现为静态方法,而 VirtA 和 VirtB 将以虚方法实现:[delphi] view plaincopyIIntfA = interface  
    procedure ProcA;  
    procedure VirtA;  
  end;  
  
  IIntfB = interface  
    procedure ProcB;  
    procedure VirtB;  
  end;  

       然后我们定义一个 TMyObject 类,它继承自 TInterfacedObject,并实现 IIntfA 和 IIntfB 两个接口:[delphi] view plaincopyTMyObject = class(TInterfacedObject, IIntfA, IIntfB)  
    FFieldA: Integer;  
    FFieldB: Integer;  
    procedure ProcA;  
    procedure VirtA; virtual;  
    procedure ProcB;  
    procedure VirtB; virtual;  
  end;  
       然后我们执行以下代码:[delphi] view plaincopyvar  
    MyObject: TMyObject;  
    MyIntf:  IInterface;  
    MyIntfA: IIntfA;  
    MyIntfB: IIntfB;  
  begin  
    MyObject := TMyObject.Create;  // 创建 TMyObject 对象  
    MyIntf  := MyObject;           // 将接口指向 MyObject 对象  
    MyIntfA := MyObject;  
    MyIntfB := MyObject;  
  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 plaincopyfunction 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.GetInterfaceTable;  
      if IntfTable <> nil then  
        for I := 0 to IntfTable.EntryCount-1 do  
    with IntfTable.Entries[I] do  
    begin  
      if VTable <> nil then  
        PInteger(@PChar(Instance)[IOffset])^ := Integer(VTable);  
    end;  
      ClassPtr := ClassPtr.ClassParent;  
    end;  
    Result := Instance;  
  end;  
----------------------
implements 的实现
----------------------
       Delphi 中可以使用 implements 关键字将接口方法委托给另一个接口或对象来实现。下面以 TMyObject 为基类,考查 implements 的实现方法。
[delphi] view plaincopyTMyObject = class(TInterfacedObject, IIntfA, IIntfB)  
    FFieldA: Integer;  
    FFieldB: Integer;  
    procedure ProcA;  
    procedure VirtA; virtual;  
    procedure ProcB;  
    procedure VirtB; virtual;  
    destructor Destroy; override;  
  end;  
      (1)以接口成员变量实现 implements[delphi] view plaincopyTMyObject2 = class(TInterfacedObject, IIntfA)  
FIntfA: IIntfA;  
property IntfA: IIntfA read FIntfA implements IIntfA;  
end;  
         这时编译器的实现是非常简单的,因为 FIntfA 就是接口指针,这时如果使用接口赋值 MyIntfA := MyObject2 这样的语句调用时,MyIntfA 就直接指向 MyObject2.FIntfA。      (2)以对象成员变量实现 implements       如下例,如果一个接口类 TMyObject3 以对象的方式实现 implements (通常应该是这样),其对象内存空间的排列与TMyObject内存空间情况几乎是一样的:[delphi] view plaincopyTMyObject3 = class(TInterfacedObject, IIntfA, IIntfB)  
    FMyObject: TMyObject;  
    function GetMyObject: TMyObject;  
    property MyObject: TMyObject read GetMyObject implements IIntfA, IIntfB;  
  end;  

       不同的地方在于 TMyObject3 的“接口跳转表”的内容发生了变化。由于 TMyObject3 并没有自己实现 IIntfA 和 IIntfB,而是由 FMyObject 对象来实现这两个接口。这时,“接口跳转表”中调用的方法就必须改变为调用 FMyObject 对象的方法。比如下面的代码:
[delphi] view plaincopyvar  
    MyObject3: TMyObject3;  
    MyIntfA: IIntfA;  
  begin  
    MyObject3:= TMyObject3.Create;  
    MyObject3.FMyObject := TMyObject.Create;  
    MyIntfA := MyObject3;  
    MyIntfA._AddRef;  
    MyIntfA.ProcA;  
    MyIntfA._Release;  
  end;  

       当执行 MyIntfA._AddRef 语句时,编译器生成的“接口跳转”代码为:
[delphi] view plaincopy{MyIntfA._AddRef;}  
mov eax,[ebp-$0c]              // eax = MyIntfA^  
push eax                       // MyIntfA^ 设置为 Self  
mov eax,[eax]                  // eax = 接口跳转表地址指针  
call dword ptr [eax+$04]       // 转到接口跳转表  
  
{ “接口跳转段”中的代码 }  
mov eax,[esp+$04]              // [esp+$04] 是接口指针内容 (MyIntfA^)  
add eax,-$14                   // 修正 eax = Self (MyObject2)  
call TMyObject2.GetMyObject  
mov [esp+$04],eax              // 获得 FMyObject 对象,注意 [esp+$04]  
jmp TInterfacedObject._AddRef  // 调用 FMyObject._AddRef  
          [esp+$04] 是值得注意的地方。“接口跳转表”中只修正一个参数 Self,其它的调用参数(如果有的话)在执行过程进入“接口跳转表”之前就由编译器设置好了。在这里 _AddRef 是采用 stdcall 调用约定,因此 esp+$04 就是 Self。前面说过,编译器直接把接口指针的内容作为 Self 参数,然后转到“接口跳转表”中对 Self 进行修正,然后才能调用对象方法。上面的汇编代码就是修正 Self 为 FMyObject 并调用 FMyObject 的方法。
       可以看到 FMyObject._AddRef 方法增加的是 FMyObject 对象的引用计数,看来 implements 的实现只是简单地把接口传送给对象执行,而要实现 COM 组件聚合,必须使用其它方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  delphi delphi xe interface