Delphi 的接口機制——接口操作的編譯器實(shí)現過(guò)程,1
學(xué)習COM編程技術(shù)也快有半個(gè)月了,這期間看了很多資料和別人的程序源碼,也嘗試了用delphi、C++、C#編寫(xiě)COM程序,個(gè)人感覺(jué)Delphi是最好上手的。C++的模版生成的代碼太過(guò)復雜繁瑣,大量使用編譯宏替代函數代碼,讓初學(xué)者知其然而不知其所以然;C#封裝過(guò)度,COM編程注定是要與操作系統頻繁打交道的,需要調用大量API函數和使用大量系統預定義的常量與類(lèi)型(結構體),這些在C#中都需手工聲明,不夠簡(jiǎn)便;Delphi就簡(jiǎn)單多了,通過(guò)模版創(chuàng )建的工程代碼關(guān)系結構非常清晰,而且其能非常容易使用API函數和系統預定義的常量和類(lèi)型(只需引用先關(guān)單元即可),但在使用過(guò)程中也發(fā)現了一些缺點(diǎn)。【注1】
(1)有些類(lèi)型(結構體)的成員類(lèi)型與C++中的不是等效對應關(guān)系,如SHFileOperation函數的參數類(lèi)型是SHFILEOPSTRUCT結構體,delphi中它的兩個(gè)路徑成員被定義成PWideChar型,與C++的LPCTSTR不一致,PWideChar是以空字符(\0)結尾的,致使這兩個(gè)成員不能包含多個(gè)文件路徑。【注2】
(2)有些接口的函數參數定義不一致,如IContextMenu.InvokeCommand函數參數在Delphi中是CMINVOKECOMMANDINFO類(lèi)型,在c++中是LPCMINVOKECOMMANDINFO型 ,致使該接口函數不能使用擴展的CMINVOKECOMMANDINFOEX型參數。【注3】
Delphi操作COM的另一便處在于他的接口的引用計數管理,這為我們寫(xiě)程序解決了一大麻煩:不用管接口的AddRef和Release了,直接把接口當“接口指針變量”(【注4】)使用,編譯器會(huì )執行一些特殊的代碼自動(dòng)維護接口的引用計數。當然,這也會(huì )帶來(lái)另一個(gè)問(wèn)題,接口相當于“變量”一樣使用,這就涉及到“變量”的生命周期問(wèn)題,當把這樣一個(gè)局部“變量”通過(guò)強制類(lèi)型轉換(【注5】)給一個(gè)全局變量時(shí),待之后轉換回來(lái)時(shí)將引發(fā)錯誤。因為局部“變量”生命已結束,要被清理,其所代表的接口被減少引用計數釋放了,如果人為讓“變量”AddRef一次,就能消除這個(gè)錯誤。
關(guān)于Delphi的接口引用計數管理,在網(wǎng)上看到的一篇介紹的文章,查很久了它的出處,目前已知最早是SaveTime于2004年2月3日發(fā)表于大富翁論壇。【注6】
下面將它整理了一下,以便加深對delphi對接口引用計數的理解。
接口指針變量賦值
接口是生存期自管理對象,即使是局部接口指針變量,也總是被初始化為 nil。接口被初始化為nil是很重要的,從下文中Delphi生成維護接口引用計數的代碼時(shí)可以看到這一點(diǎn)。
[delphi]view plaincopyprint?
- var
- MyObject: TMyObject;
- MyIntf, MyIntf2: IInterface;
- begin
- MyObject := TMyObject.Create; // 創(chuàng )建 TMyObject 對象
- MyIntf := MyObject; // 將接口指向 MyObject 對象
- MyIntf2 := MyIntf; // 接口指針的賦值
- end;
當接口與一個(gè)對象連接時(shí),編譯器會(huì )執行一些特殊的代碼維護接口對象的引用計數。例如以上代碼,當執行到MyIntf :=MyObject 語(yǔ)句時(shí),編譯器的實(shí)現是:
1. 如果 MyObject <> nil,則設置一臨時(shí)接口指針 P 指向 MyObject 對象內存空間中的“接口跳轉表”指針(后面會(huì )分析“接口跳轉表”),否則 P := nil;
2. 執行 System.pas 中的 _IntfCopy(MyIntf, P) 操作,進(jìn)行引用計數管理。
[delphi]view plaincopyprint?
- 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 的代碼比較簡(jiǎn)單,就是增加 Source 接口對象的引用計數,減少被賦值的接口對象的引用計數,最后把源接口賦值至目標接口。
對于兩個(gè)接口的賦值的情況,如MyIntf2 := MyIntf,這時(shí)比 MyIntf := MyObject 的情況要簡(jiǎn)單一些,編譯器不需要進(jìn)行對象到接口的轉換工作,這時(shí)真正執行的代碼是:_IntfCopy(MyIntf2, MyIntf)。
接口指針變量的清除工作
在一個(gè)過(guò)程(procedure/function)執行結束時(shí),編譯器會(huì )生成代碼減少接口指針變量的引用計數。編譯器使用接口指針為參數調用 _IntfClear 函數,_IntfClear 函數的作用是減少接口對象的引用計數并設置接口為 nil :
[delphi]view plaincopyprint?
- 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;
通過(guò)對以上代碼及分析,我們可以總結過(guò)程(procedure/function)中的接口引用計數使用規則:
1. 一般不需要使用 _AddRef/_Release 函數設置接口引用計數;
2. 可以將接口賦值為接口或對象,Delphi 自動(dòng)處理源/目標接口對象的引用計數;
3. 如果要提前釋放接口對象,可以設置接口指針為 nil,但不要調用 _Release。因為 _Release 不會(huì )把接口指針變量設置為 nil,最后 Delphi 自動(dòng)調用 _IntfClear時(shí)會(huì )出錯。
對于全局接口指針變量,在接口指針變量被賦值時(shí)增加對象的引用計數,在程序退出之前編譯器自動(dòng)調用 _IntfClear 函數減少引用計數以清除對象。
接口指針作為參數
1. 以var 或const 方式傳遞接口指針時(shí),像普通的參數傳遞一樣。
2. 以out 方式傳遞接口指針時(shí),編譯器會(huì )先調用_IntfClear 函數減少引用計數,清除接口指針為 nil 。(out 也是以引用方式傳送參數)。
3. 以傳值方式傳遞接口指針時(shí),編譯器會(huì )在參數被使用之前調用_IntfAddRef 函數增加引用計數,在過(guò)程結束之前調用_IntfClear 函數減少引用計數。
[delphi]view plaincopyprint?
- { System.pas }
- procedure _IntfAddRef(const Dest: IInterface);
- begin
- if Dest <> nil then Dest._AddRef;
- end;
為什么以傳值方式要特別處理引用計數呢?因為復制了接口指針。
1 我用的是Delphi2010,更新的XE、XE2版本可能已更正了這些問(wèn)題,在此舉例說(shuō)明而已。
2 有關(guān)結構體SHFILEOPSTRUCT及其兩個(gè)路徑成員的詳細介紹請參見(jiàn)http://blog.csdn.net/tht2009/article/details/6753706和http://msdn.microsoft.com/en-us/library/bb759795(VS.85).aspx。
3 有關(guān)接口函數InvokeCommand的詳細介紹請參見(jiàn)http://msdn.microsoft.com/en-us/library/bb776096(VS.85).aspx。
4 我也不知嚴格上能否這樣稱(chēng)呼,姑且這樣類(lèi)比吧!
5 如通過(guò)Pointer(IShellFolder)將一個(gè)局部聲明的IShellFolder接口保存到一個(gè)Pointer型的變量Data中,通過(guò)Data:=Pointer(IShellFolder)不會(huì )增加IShellFolder接口對象的引用。實(shí)際中很少遇到這種情況,我也是在無(wú)意中發(fā)現這個(gè)問(wèn)題的。
6 請見(jiàn)http://blog.csdn.net/huangsn10/article/details/6112546,由于大富翁論壇好像已關(guān)閉了,所以真正出處已無(wú)從考證。
http://blog.csdn.net/tht2009/article/details/6767435
- 上一篇 ?CSS預編譯器
- 下一篇 ?Java編譯異常捕捉與上報筆記