接口對象的內存空間編輯器
假設咱們定義了以下兩個接口 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 組件聚合,必須使用其它方法。