來源: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生成維護接口引用計數的代碼時可以看到這一點。
- 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) 操作,進行引用計數管理。
- 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 :
- 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 函數減少引用計數。
- { 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/6753706和http://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 將以虛方法實現:
- 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 二進制規格圖:
----------------------------------------
對象內存空間中接口跳轉指針的初始化
----------------------------------------
還有一個問題,那就是對象內存空間中的接口跳轉指針是如何初始化的。原來,在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 組件聚合,必須使用其它方法。