Delphi 的接口機制——接口操作的編譯器實現過程

來源: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生成維護接口引用計數的代碼時可以看到這一點。 

  1. var  
  2.     MyObject: TMyObject;  
  3.     MyIntf, MyIntf2: IInterface;  
  4.   begin  
  5.     MyObject := TMyObject.Create;  // 創建 TMyObject 對象  
  6.     MyIntf  := MyObject;           // 將接口指向 MyObject 對象  
  7.     MyIntf2 := MyIntf;             // 接口指針的賦值  
  8.   end;  

 

        當接口與一個對象連接時,編譯器會執行一些特殊的代碼維護接口對象的引用計數。例如以上代碼,當執行到MyIntf :=MyObject 語句時,編譯器的實現是:     

       1. 如果 MyObject <> nil,則設置一臨時接口指針 P 指向 MyObject 對象內存空間中的“接口跳轉表”指針(後面會分析“接口跳轉表”),否則 P := nil;     

       2. 執行 System.pas 中的 _IntfCopy(MyIntf, P) 操作,進行引用計數管理。

  1. procedure _IntfCopy(var Dest: IInterface; const Source: IInterface);  
  2. var  
  3.   P: Pointer;  
  4. begin  
  5.   P := Pointer(Dest);  
  6.   if Source <> nil then  
  7.     Source._AddRef;  
  8.   Pointer(Dest) := Pointer(Source);  
  9.   if P <> nil then  
  10.     IInterface(P)._Release;  
  11. end;  

        函數_IntfCopy 的代碼比較簡單,就是增加 Source 接口對象的引用計數,減少被賦值的接口對象的引用計數,最後把源接口賦值至目標接口。

       對於兩個接口的賦值的情況,如MyIntf2 := MyIntf,這時比 MyIntf := MyObject 的情況要簡單一些,編譯器不需要進行對象到接口的轉換工作,這時真正執行的代碼是:_IntfCopy(MyIntf2, MyIntf)。

 

接口指針變量的清除工作

      

       在一個過程(procedure/function)執行結束時,編譯器會生成代碼減少接口指針變量的引用計數。編譯器使用接口指針爲參數調用 _IntfClear 函數,_IntfClear 函數的作用是減少接口對象的引用計數並設置接口爲 nil :

  1. function _IntfClear(var Dest: IInterface): Pointer;    
  2. var   
  3. P:Pointer;    
  4. begin     
  5.  Result := @Dest;     
  6.  if Dest <> nil then      
  7.     begin        
  8.        P := Pointer(Dest);        
  9.        Pointer(Dest) := nil;        
  10.        IInterface(P)._Release;      
  11.     end;    
  12. 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 函數減少引用計數。

  1. { System.pas }  
  2.   procedure _IntfAddRef(const Dest: IInterface);  
  3.   begin  
  4.     if Dest <> nil then Dest._AddRef;  
  5.   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 plaincopy
  1. IIntfA = interface  
  2.     procedure ProcA;  
  3.     procedure VirtA;  
  4.   end;  
  5.   
  6.   IIntfB = interface  
  7.     procedure ProcB;  
  8.     procedure VirtB;  
  9.   end;  


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

[delphi] view plaincopy
  1. TMyObject = class(TInterfacedObject, IIntfA, IIntfB)  
  2.     FFieldA: Integer;  
  3.     FFieldB: Integer;  
  4.     procedure ProcA;  
  5.     procedure VirtA; virtual;  
  6.     procedure ProcB;  
  7.     procedure VirtB; virtual;  
  8.   end;  

       然後我們執行以下代碼:

[delphi] view plaincopy
  1. var  
  2.     MyObject: TMyObject;  
  3.     MyIntf:  IInterface;  
  4.     MyIntfA: IIntfA;  
  5.     MyIntfB: IIntfB;  
  6.   begin  
  7.     MyObject := TMyObject.Create;  // 創建 TMyObject 對象  
  8.     MyIntf  := MyObject;           // 將接口指向 MyObject 對象  
  9.     MyIntfA := MyObject;  
  10.     MyIntfB := MyObject;  
  11.   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 plaincopy
  1. function TObject.InitInstance(Instance: Pointer): TObject;  
  2. var  
  3.  IntfTable: PInterfaceTable;  
  4.  ClassPtr: TClass;  
  5.  I: Integer;  
  6.  begin  
  7.     FillChar(Instance^, InstanceSize, 0);  
  8.     PInteger(Instance)^ := Integer(Self);  
  9.     ClassPtr := Self;  
  10.     while ClassPtr <> nil do  
  11.     begin  
  12.       IntfTable := ClassPtr.GetInterfaceTable;  
  13.       if IntfTable <> nil then  
  14.         for I := 0 to IntfTable.EntryCount-1 do  
  15.     with IntfTable.Entries[I] do  
  16.     begin  
  17.       if VTable <> nil then  
  18.         PInteger(@PChar(Instance)[IOffset])^ := Integer(VTable);  
  19.     end;  
  20.       ClassPtr := ClassPtr.ClassParent;  
  21.     end;  
  22.     Result := Instance;  
  23.   end;  

----------------------
implements 的實現
----------------------
       Delphi 中可以使用 implements 關鍵字將接口方法委託給另一個接口或對象來實現。下面以 TMyObject 爲基類,考查 implements 的實現方法。

[delphi] view plaincopy
  1. TMyObject = class(TInterfacedObject, IIntfA, IIntfB)  
  2.     FFieldA: Integer;  
  3.     FFieldB: Integer;  
  4.     procedure ProcA;  
  5.     procedure VirtA; virtual;  
  6.     procedure ProcB;  
  7.     procedure VirtB; virtual;  
  8.     destructor Destroy; override;  
  9.   end;  

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

[delphi] view plaincopy
  1. TMyObject2 = class(TInterfacedObject, IIntfA)  
  2. FIntfA: IIntfA;  
  3. property IntfA: IIntfA read FIntfA implements IIntfA;  
  4. end;  

         這時編譯器的實現是非常簡單的,因爲 FIntfA 就是接口指針,這時如果使用接口賦值 MyIntfA := MyObject2 這樣的語句調用時,MyIntfA 就直接指向 MyObject2.FIntfA。

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

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

[delphi] view plaincopy
  1. TMyObject3 = class(TInterfacedObject, IIntfA, IIntfB)  
  2.     FMyObject: TMyObject;  
  3.     function GetMyObject: TMyObject;  
  4.     property MyObject: TMyObject read GetMyObject implements IIntfA, IIntfB;  
  5.   end;  



       不同的地方在於 TMyObject3 的“接口跳轉表”的內容發生了變化。由於 TMyObject3 並沒有自己實現 IIntfA 和 IIntfB,而是由 FMyObject 對象來實現這兩個接口。這時,“接口跳轉表”中調用的方法就必須改變爲調用 FMyObject 對象的方法。比如下面的代碼:

[delphi] view plaincopy
  1. var  
  2.     MyObject3: TMyObject3;  
  3.     MyIntfA: IIntfA;  
  4.   begin  
  5.     MyObject3:= TMyObject3.Create;  
  6.     MyObject3.FMyObject := TMyObject.Create;  
  7.     MyIntfA := MyObject3;  
  8.     MyIntfA._AddRef;  
  9.     MyIntfA.ProcA;  
  10.     MyIntfA._Release;  
  11.   end;  


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

[delphi] view plaincopy
  1. {MyIntfA._AddRef;}  
  2. mov eax,[ebp-$0c]              // eax = MyIntfA^  
  3. push eax                       // MyIntfA^ 設置爲 Self  
  4. mov eax,[eax]                  // eax = 接口跳轉表地址指針  
  5. call dword ptr [eax+$04]       // 轉到接口跳轉表  
  6.   
  7. { “接口跳轉段”中的代碼 }  
  8. mov eax,[esp+$04]              // [esp+$04] 是接口指針內容 (MyIntfA^)  
  9. add eax,-$14                   // 修正 eax = Self (MyObject2)  
  10. call TMyObject2.GetMyObject  
  11. mov [esp+$04],eax              // 獲得 FMyObject 對象,注意 [esp+$04]  
  12. jmp TInterfacedObject._AddRef  // 調用 FMyObject._AddRef  

          [esp+$04] 是值得注意的地方。“接口跳轉表”中只修正一個參數 Self,其它的調用參數(如果有的話)在執行過程進入“接口跳轉表”之前就由編譯器設置好了。在這裏 _AddRef 是採用 stdcall 調用約定,因此 esp+$04 就是 Self。前面說過,編譯器直接把接口指針的內容作爲 Self 參數,然後轉到“接口跳轉表”中對 Self 進行修正,然後才能調用對象方法。上面的彙編代碼就是修正 Self 爲 FMyObject 並調用 FMyObject 的方法。
       可以看到 FMyObject._AddRef 方法增加的是 FMyObject 對象的引用計數,看來 implements 的實現只是簡單地把接口傳送給對象執行,而要實現 COM 組件聚合,必須使用其它方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章