科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道[韩小明]Inside VCL:接口指针调用函数的时候,如何获得对象指针以完成函数调用?

[韩小明]Inside VCL:接口指针调用函数的时候,如何获得对象指针以完成函数调用?

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

接口指针调用函数的时候,如果获得对象指针以完成函数调用? 本文以分析的路线带领你去了解Delphi的实现细节,初步掌握接口的实现机制.

作者:韩小明 来源:CSDN 2007年10月10日

关键字: 韩小明 VCL 接口指针 对象指针 函数调用

  • 评论
  • 分享微博
  • 分享邮件

对于Delphi中的对象方法,大家都比较清楚其与一般方法的区别。如果不知道的我们也先了解一下。对象方法相对于一般的方法,会多出一个隐含参数Self,因此对于Form1的一个过程:

        procedure TForm1.Button1Click(Sender: TObject);

  如果不在对象中申明的话,其完整的申明应该是这样的:

         procedure Button1Click(Self: TForm1; Sender: TObject);

  

  对于上面的详细细节不再讲述。很多Delphi的书籍都讲到这点。下面我将默认您已经了解了这点。

  针对上面的知识,我们申明一个类 TA,其实现了接口IAIA有一个方法叫F

 

  IA = interface

  ['{CAD3FF7B-6C23-41A3-AA71-16B4E8D9D35D}']

    procedure F;

  end;

 

  TA = class(TInterfacedObject, IA)

  protected

    procedure F;

  end;

 

  那么,做如下调用:

var

  a: TA;

  ai: IA;

begin

  a := TA.Create;

  a.F;

 

  ai := a as IA;

  ai.F;

end;

 

我的问题是,我们都知道,在方法F实现的时候,其必然有一个参数Self传入,而这个Self必然是对象指针。

当进行a.F调用的时候,显然a可以作为Self指针传入。但是,当进行ai.F调用的时候,Delphi是如何获得对象指针,并传入F函数的呢?我们知道,ai并没有提供方法去获取对象方法。

 

简单点说:接口指针调用函数的时候,如何获得对象指针以完成函数调用?

 

要回答这个问题,有两条线索:

1.   跟踪ai.F调用的汇编代码

2.   跟踪TObject实现接口的细节代码。(Delphi提供了Pure Pascal代码,真好)

 

我先对线索2进行了跟踪:

对于一个实现了接口的对象来讲,在初始化创建对象的时候,必须初始化好其内存结构,通过源码可以发现在NewInstance的时候会调用InitInstance

下面Delphi提供的Pure Pascal

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.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;

 

阅读上面的代码,可以发现,Object在初始化的时候,会先得到所有实现的接口列表,然后针对每个接口,会在Instance中对应一个IOffset位置上,存放一个VTable的指针。

简单点说:一个对象没实现一个接口,会在对象控件中增加一个4字节指针。这个结论可以通过验证InstanceSize来确认。而这个4字节指针指向这个接口的方法表。

 

细心的您,可能已经发现,不对啊,这个VTable,对于所有对象都是一个地址!因此在这里绝对不可能获取我们“可变”的对象实例指针。

不过既然说到这里,我们应该重新认识一下对象指针和接口指针的实质。

 

对象实例一般情况下,是建立在堆上的连续空间,对象指针会指向这段连续空间的首地址。接口指针和对象指针不是一个指针,它指向这个实例空间的某个IOffSet。如下简图所示:

Object Pointer         Interface Pointer

/ ß   IOffSet    –>   /

Object Instance

 

我们的问题并没有回答,虽然我们在表面上能够认识到a就是对象指针,ai就是接口指针,其间的相对位置是能够得到的。但是程序是如何获得的呢?是不是申明地方存储着这两个指针的偏移地址呢?

 

结合一下我们刚才的初始化代码,我们可以发现,可以实现一段代码来通过接口指针来得到对象指针!

function IntfToObj(AClass: TClass; AIntf: IInterface; AIID: TGUID): TObject;

var

  rIntfEntry: PInterfaceEntry;

begin

  rIntfEntry := AClass.GetInterfaceEntry(AIID);

  Assert(rIntfEntry.IOffset <> 0);  // 找到AIID定义的接口

  Result := TObject(Integer(AIntf)- rIntfEntry.IOffset)

end;

方法很简单,取得偏移地址,偏移指针!

这真的是一件很振奋的结论,在很多应用中,我们就曾经要过这样的函数,可以让我们在使用接口的同时,也可以使用对象的一些方法。

可惜的是我们还没有回答接口是如何在程序中得到的。显然,Delphi并没有提供这样的函数。我们必须针对第一条线索进行跟踪!

a.Fai.F分别下断点,调试。调出CPU窗体,看到如下代码:

Unit1.pas.114: a.F;

0045C13E 8BC3             mov eax,ebx

0045C140 E8CFFFFFFF       call TA.F

Unit1.pas.117: ai.F;

0045C15B 8B45FC           mov eax,[ebp-$04]

0045C15E 8B10             mov edx,[eax]

0045C160 FF520C           call dword ptr [edx+$0c]

显然,最后调用的函数并不一样,重点观察ai.F的调用:

1.       先将ai指针存放到EAX寄存器

2.       跳转到[edx+$0c]所指向的代码

 

我们看看这段代码其实是什么。

0045BD49 83C0F4           add eax,-$0c

0045BD4C E9C3030000       jmp TA.F

相信大家看到了熟悉的TA.F了,再看看上面那句话!

Add EAX, -$0c,不就是我们上面那个IntfToObj的函数实现的结果吗?看来编译器在编译的时候,为了加快速度,直接将偏移地址放在了代码里。终于知道Delphi是如何实现接口到对象的转换了。

 

分析到这里,我们总结一下会发现,IntfToObj的缺点是不能针对任意类实现的接口进行计算。如果两个类都实现了同一个接口I,那么你不能简单用那个方法去获取偏移,因为两个类类型可能完全没关系!

而上面这段汇编却给了我们启示。既然CPU能知道偏移,我们也可以直接通过ai指针来获取啊!至于如何分析汇编,那就是另一篇文章了。 



Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1338342

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章