Delphi 的接口機制——接口操做的編譯器實現過程(2)

接口對象的內存空間編輯器


        假設咱們定義了以下兩個接口 IIntfA 和 IIntfB,其中 ProcA 和 ProcB 將實現爲靜態方法,而 VirtA 和 VirtB 將以虛方法實現:ide

IIntfA = interface
  procedure ProcA;
  procedure VirtA;
end;

IIntfB = interface
  procedure ProcB;
  procedure VirtB;
end;


       而後咱們定義一個 TMyObject 類,它繼承自 TInterfacedObject,並實現 IIntfA 和 IIntfB 兩個接口:函數

TMyObject = class(TInterfacedObject, IIntfA, IIntfB)
  FFieldA: Integer;
  FFieldB: Integer;
  procedure ProcA;
  procedure VirtA; virtual;
  procedure ProcB;
  procedure VirtB; virtual;
end;

       而後咱們執行如下代碼:學習

var
  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 二進制規格圖:code


----------------------------------------
對象內存空間中接口跳轉指針的初始化
----------------------------------------
       還有一個問題,那就是對象內存空間中的接口跳轉指針是如何初始化的。原來,在TObject.InitInstance 中,用 FillChar 清零對象內存空間後,進行的工做就是初始化對象的接口跳轉指針:對象

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;

----------------------
implements 的實現
----------------------
       Delphi 中可使用 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;

      (1)以接口成員變量實現 implements接口

TMyObject2 = class(TInterfacedObject, IIntfA)
  FIntfA: IIntfA;
  property IntfA: IIntfA read FIntfA implements IIntfA;
end;

         這時編譯器的實現是很是簡單的,由於 FIntfA 就是接口指針,這時若是使用接口賦值 MyIntfA := MyObject2 這樣的語句調用時,MyIntfA 就直接指向 MyObject2.FIntfA。內存

      (2)以對象成員變量實現 implements

       以下例,若是一個接口類 TMyObject3 以對象的方式實現 implements (一般應該是這樣),其對象內存空間的排列與TMyObject內存空間狀況幾乎是同樣的:

TMyObject3 = class(TInterfacedObject, IIntfA, IIntfB)
  FMyObject: TMyObject;
  function GetMyObject: TMyObject;
  property MyObject: TMyObject read GetMyObject implements IIntfA, IIntfB;
end;



       不一樣的地方在於 TMyObject3 的「接口跳轉表」的內容發生了變化。因爲 TMyObject3 並無本身實現 IIntfA 和 IIntfB,而是由 FMyObject 對象來實現這兩個接口。這時,「接口跳轉表」中調用的方法就必須改變爲調用 FMyObject 對象的方法。好比下面的代碼:

var
  MyObject3: TMyObject3;
  MyIntfA: IIntfA;
begin
  MyObject3:= TMyObject3.Create;
  MyObject3.FMyObject := TMyObject.Create;
  MyIntfA := MyObject3;
  MyIntfA._AddRef;
  MyIntfA.ProcA;
  MyIntfA._Release;
end;


       當執行 MyIntfA._AddRef 語句時,編譯器生成的「接口跳轉」代碼爲:

{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 組件聚合,必須使用其它方法。

相關文章
相關標籤/搜索