C#中com操作實例化詳解


原文出處http://blog.csdn.net/bindsang/archive/2008/08/08/2788574.aspx
用C#做WinForm程序,時間長了難免會遇到和COM組件打交道的地方,用什么方式創建COM對象也成了我們必須面對的一個問題.據我所知道的創建COM對象的方法一共有以下幾種:
1 使用.NET包裝COM組件

    這是最簡單的就是導入COM組件所在的DLL,讓IDE生成.NET一個IL包裝加到項目中,這樣原來COM里面所有實現了IDispatch,Dual 的COM類型及其相關類型就可以直接在.NET程序里面使用,比如以前在2003時代,想要寫自己的基於IE的瀏覽器,就得手動加入與 IWebBrowser2接口相關的DLL,這種方式是大家最常用的,也是最傻瓜化的,因此也沒什么可解釋的.

    但是這種方式有個至命的缺點---不是所有的COM對象都能用這種方式導出.正如前面所說的,只有實現了IDispatch,Dual類型的接口才支持被 導出,而且面對不同版本的COM或許會生成不一樣的導出DLL,比如說A機器上寫代碼時導入了一個Jet2.6版本的包裝DLL,代碼編譯了拿到B機器上 去運行,但是B機器上的Jet版本是2.8的,就可能會出現運行時錯誤.

2 用反射動態創建

    包括使用Type.GetTypeFromCLSID和Type.GetFromProgID兩種方法獲取COM對象的Type再創建.這種方式也好理 解,就是說使用這兩個方法之前,必須得知道COM對象的GUID或ProgID,好在這也不是什么難事,一般我們要使一個COM對象,多多少少都了解一些 這個COM對象的GUID或ProgID信息.用這種方獲取到了一個Type對象后,就可以用.NET里面通用的反射創建對象的方法來做了.

 

這里給出一個創建JetEngine 的COM對象的代碼實例:

 

 1 public object GetActiveXObject(Guid clsid)
 2 {
 3     Type t = Type.GetTypeFromCLSID(clsid);
 4     if (t == null) return null;
 5
 6     return Activator.CreateInstance(t);
 7 }
 8
 9 Guid g = new Guid("DE88C160-FF2C-11D1-BB6F-00C04FAE22DA"); // JetEngine
10 object jet = GetActiveXObject(g);
是不是覺得最后調用GetActiveXObject(g)的地方和IE里面JavaScript里面用new ActiveXOjbect創建COM對象的方法很相像?


3 聲明CoCreateInstance外部函數,用這個函數去創建相應的COM實例

    M$在2005里面包裝的WebBrowser控件內部就是用這個函數去創建的, 使用這種方式創建COM,就跟在C++里面不什么兩樣了.有一點需要說明的是,一般我們在代碼中引入外部方法的時候,方法的參數和返回值的類型不一定是唯 一的一種,只要在邏輯上相互能轉化,一般都可以使用.


比如說如下幾種聲明都是正確的: 


 

 

 1 [return: MarshalAs(UnmanagedType.Interface)]
 2 [DllImport("ole32.dll", ExactSpelling=true, PreserveSig=false)]
 3 public static extern object CoCreateInstance([In] ref Guid clsid,
 4     [MarshalAs(UnmanagedType.Interface)] object punkOuter, int context, [In] ref Guid iid);
 5  
 6 [DllImport("ole32.dll", ExactSpelling=true, PreserveSig=false)]
 7 public static extern IntPtr CoCreateInstance([In] ref Guid clsid,
 8     IntPtr punkOuter, int context, [In] ref Guid iid);
 9
10 [DllImport("ole32.dll", ExactSpelling=true)]
11 public static extern int CoCreateInstance([In] ref Guid clsid,
12     IntPtr punkOuter, int context, [In] ref Guid iid, [Out] out IntPtr pVoid);
13
14 [DllImport("ole32.dll", ExactSpelling=true)]
15 public static extern int CoCreateInstance([In] ref Guid clsid,
16     [MarshalAs(UnmanagedType.Interface)] object punkOuter, int context,
17     [In] ref Guid iid, [MarshalAs(UnmanagedType.Interface), Out] out object pVoid);
 

 

 甚至於當你有里面對應的接口類型的聲明的時候,完全可以把上面的object或IntPtr換成相應的接口類型,前提是你的接口 類型的聲明一定要正確.讀者中用C++做過COM的一定對這種方式記憶猶新吧,只不過這里不再需要什么CoInitialize和 CoUninitialize,.NET 內部自己幫你搞定了.順便提一下,上面例子中的object與IntPtr聲明是相通的,我們可以用 Marshal.GetObjectForIUnknown和Marshal.GetIUnknownForObject這兩個方法在object和 IntPtr之間互轉,前題當然是這兩種方式所指向的都是COM對象才行.這種方式提供的傳入參數最多,創建對象也最靈活.

 

3.直接聲明空成員的類

    可能很多程序員對於這個不太理解這是什么意思,沒關系咱還是"用代碼來說話".

 

 1 [ComImport, Guid("DE88C160-FF2C-11D1-BB6F-00C04FAE22DA")]
 2 public class JetEngineClass
 3 {
 4 }
 5
 6 [ComImport, CoClass(typeof(JetEngineClass)), Guid("9F63D980-FF25-11D1-BB6F-00C04FAE22DA")]
 7 public interface IJetEngine
 8 {
 9     void CompactDatabase(
10         [In, MarshalAs(UnmanagedType.BStr)] string SourceConnection,
11         [In, MarshalAs(UnmanagedType.BStr)] string Destconnection
12         );
13     void RefreshCache([In, MarshalAs(UnmanagedType.Interface)] object Connection);
14 }
15
16 JetEngineClass engine = new JetEngineClass();
17 IJetEngine iengine = engine as IJetEngine;
18 // iengine即是所要用的接口的引用
 

 大家看到了上面聲明的JetEngineClass類只有一個單單的類聲明,但是沒有一個成員聲明,但是和一般的類聲明有些不一 樣的是這個類多了兩個特性(Attribute),把這個類和COM對象聯系在一起的就是這兩個特性了,其中一個是 ComImportAttribute,這個特性指明了所作用的類是從COM對象中來的,GuidAttribute指明了COM對象的GUID,也就是 說明了創建這個COM需用到的GUID。有了這兩個特性以后,這個類就不是一個普通的類了,當我們使用new去創建實例的時候,CLR看到了聲明的這兩特 性就知道要創建的是一個COM對象,根據提供的GUID 也就能創建出指定的COM對象,並和new返回的對象實例關聯在一起了。

 

    終上4種方法我們可以看出來,第一種方式只對特定的COM對象有效,不具有通用性;第二種方式只需要知道COM對象的CLSID或PROGID就可以了, 是我們在.NET里平時比較常用的創建COM對象的方法;第三種方式需要自己聲明一個外部方法,而且需要傳入若干的參數,還需要知道COM對象模型,是單 線程呢還是多線程,進程內呢還是進程外,兩個字"麻煩"。對CoCreateInstance這個方法不是很熟悉的人來說,用起來就不那么順手了;第四種 方式用起來最像是.NET的方式,也最簡單省事,和其它.NET對象的創建方式最為接近。四種方法各有各有好處,我覺得簡單的COM對象,用第二種和第四 種是最好的(我個人來說最喜歡第四種)又不生成額外的程序集;要是COM對象相關的比較多,比如說Excel之類的COM對象,我建議還是用導入類型庫包 裝吧,雖然是有可能出現版本問題,但這種應該很容易要求目標機器上運行的COM版和開發的時候一致的,更何況版本問題也不是100%出現,只是很少一部分 會出這樣的問題。最不推薦的就是第三種方式了,這種方式在我看來唯一用到的地方就是使用IntPtr作為COM對象和接口的指針的時候,或者是想要在創建 COM對象的時候,對參數作最靈活的控制的時候. 因為其它三種方式既不能返回IntPtr指針(其實也可以通過前面提到的的Marshal類的方法把.NET包裝的COM對象轉成指針),也不能提供與直 接調用CoCreateInstance函數提供最全面的參數相匹配的方式。

 

    最后提個小問題

1 讀者有興趣的話可以去看看這幾種方式(不包括第三種)生成的COM對象的引用的類型是否是一致的,也就是用GetType得到的Type是否是一致的

2 大家猜猜這段代碼運行后,iengine的類型會是什么(GetType的結果), 會和engine的類型一樣嗎?

   

結論就是t1,t2,t3是三個不同的引用,也就是說在.NET里面代表了三種不同的類型,但是三種類型的GUID卻是一樣的,因 為在COM里 GUID代表了一個COM類,只要GUID是一樣的那么就表示是一個COM類,因此僅從COM類這一角度出發的話,這三種類型就是同一個COM類型。

 

第1種方式創建的COM對象的.NET包裝的類型一般來說就是COM導入的.NET包裝程序集里面對應聲明的類型.

第2種方式創建的COM對象的.NET包裝的類型永遠都是__ComObject.

第3種方式創建的COM對象的.NET包裝或者是指針經過Marshal類的方法轉成的.NET的包裝,這兩種方式對應的類型__ComObject.

第4種從本質上來講是第1種方式的變種,只是更為靈活,使用范圍更加廣范了,因此對應的類型也應該是聲明的時候的.NET中的類型

 

上一文里面留的第二個問題的結果就是原來是什么類型,經過一次Marshal類的方法與IntPtr互轉換后的結果還是什么類型, 應該是CLR內部記錄了指針和.NET類型之前的對應關系,不會每次由IntPtr轉到object的時候都用一個不同的包裝(感覺有點像WinForm 里面從 Handle找Control一樣).

 

      上一篇我們講到了C#中創建COM對象的幾種方式。不知大家也注意到了,最后一種方式中JetEngineClass類並沒有提供方法供我們調用,要使用 它的話必須先把這個引用轉成接口引用才能直接使用里面的方法,實現早期函數綁定。雖然我們在聲明JetEngineClass類的時候並沒指定該類實現了 IJetEngine接口,但是后面在使用的時候卻直接把engine用as操作轉成了IJetEngine接口,而且居然轉成功了。而且大家也可以用 is操作符測試一下,engine is IJetEngine反回的結果也為true。這就是本篇要講的---C#中COM對象接口的查詢。

 

 與COM創建的方法一樣,C#中COM接口查詢的方法也有好幾種:

第1種 Marshal.QueryInterface方法

    這個方法本身就是Framework提供的正統的用來查詢COM對象的方法,這種方式MSDN上已經有詳細的說明了,我也不再多說.唯一注意的是這里只能 傳COM對象的指針IntPtr,而且這個方法成功返回后,所引用的COM對象的計數會自增1.以后當返回的查詢到的接口的指針不再使用了的時候,需要手 動調用Marshal.Release,達到平衡COM引用計數的目的.雖說是簡單,還是給段代碼吧

 

1 IntPtr pJetClass = GetJetEngine(); // return JetEngineClass Ptr
2
3 IntPtr pJet;
4 Guid g = typeof(IJetEngine).Guid;
5 int hr = Marshal.QueryInterface(pJetClass, ref g, out pJet);
6 if(hr <0)
7     Marshal.ThrowExceptionFromHR(hr);
8
 

其實在使用IntPtr引用COM對象的時候,就像是在C++里面直接使用COM指針一樣,理論上來說這個指針每復制一次,都需要 我們手動的調用一次AddRef方法,增加COM對象的引用計數,每當我們把指針設置為無效或不再使用這個指針的時候,同樣需要手動的把這個指針用 Release方法減少引用計數,當引用計數變為0的時候就釋放COM對象.這還是沒有擺脫C++里面使用原始的COM指針的時候容易忘記平衡引用計數的 問題.這里我故意使用了"原始的COM指針"這外概念,主要是區別於在C++里面我們常使用COM指針的另外一種方式COMPtr<T>泛型 類,有了這個泛型類 C++里面的COM對象的引用計數就能夠正常及時的增加和減少了,使得開發人員不用花心思在COM引用計數的維護上.但是就算是這樣,要想查詢一個接口還 是擺脫不了那個QueryInterface方法.

 

C#作為一種繼承了C++大部分優點的一種語言,當然也提供了類似的方式讓我們遠離引用計數的陷阱,而且還提供了更加優雅的方式供我們使用.

這就是我們要講的第2種COM接口查詢的方式

 

第2種 與C#語言一致的類型轉換方式

    大家知道在C#里面我們要想把一種類型的引用轉成另外一種類型有兩種方式,第一種類似於(IJetEngine)engine這樣;第二種類似於 engine as IJetEngine這樣.這兩種方式有的時候產生的效果是一樣的,但是嚴格說來還是有很多差別的,這個在學C#的時候大家都遇到了,這里我也不在多說, 只是提幾個下面會用到的相同點和區別.

    對於都是引用類型的轉換,大家都不產生新的對象,如果轉換成功的話都是返回指向給定對象的新的類型的引用.第一種強制類型轉換(暫且稱作這樣吧),在遇到 轉換不成功的時候會拋出異常,但是大多數時候我們都不希望拋出異常,而是希望當轉換不成功的時候,返回null引用就可以了,而這正是第二種方式'as' 方式所能夠達到的.

    這兩種類型轉換方式同樣可以作用在COM對象的C#包裝的引用上,而產生的效果與前面用QueryInterface產生的效果是一樣的,都是返回一個給 定的接口,只不過這里以具體的接口聲明的引用代替了之前的接口指針.而且這種轉換方式與一個普通的C#托管類轉換到實現的接口的方式簡直是一模一樣.代碼 風格的一致性也得到了更好的體現.

    需要注意的是我們用這種方式用COM對象的類型轉換(其實是接口查詢)的時候,還是與普通的拖管類的類型轉換有一些細微的差別,但不是體現在代碼上,而是體現在轉換前后的兩個類型的關系上:

 

 1 public interface IDemo
 2 {
 3 }
 4 public class Demo : IDemo
 5 {
 6 }
 7
 8 public class Demo1
 9 {
10 }
11
12 object o1 = new Demo();
13 object o2 = new Demo();
14 IDemo d1 = o1 as IDemo;  // d1獲得了一個IDemo的引用
15 IDemo d2 = o2 as IDemo;  // d2 值為 null
16
17 IJetEngine e = new JetEngineClass() as IJetEngine; // e獲得了一個IJetEngine的引用
 

從這里我們可以看到普通托管類如果聲明的時候沒有實現某個接口,那么在類型轉換的時候,一定不會轉成功,但是一旦某個托管類聲明成 了COM類的包裝類以后,不管在聲明的時候有沒有實現相應的接口,只要所指代的COM類用QueryInterface能夠找到這個接口,甚至是一個聚合 的接口,那么這里的轉換一定成功.在這里類型轉換的功能就好像就成了QueryInterface的功能了. 同樣的C#里面與as操作符是孿生兄弟的"is"操作符在這里也不在是面向對象里面的"is a...", "has a ..."的定義,變成了QueryInterface能不能成功的標志了.

 

第3種 聲明的接口從IUnknown接口派生,或包含IUnknown接口的三個方法,我們還是來看看具體的代碼:

 

 1
 2
 3[ComImport, Guid("00000000-0000-0000-C000-000000000046")]
 4public interface IUnknown
 5{
 6    void QueryInterface([In] ref Guid iid, [Out] out IntPtr ppvObj);
 7    int AddRef();
 8    int Release();
 9}
10
11[ComImport, CoClass(typeof(JetEngineClass)), Guid("9F63D980-FF25-11D1-BB6F-00C04FAE22DA")]
12public interface IJetEngine1 : IUnknown
13{
14    void CompactDatabase(
15        [In, MarshalAs(UnmanagedType.BStr)] string SourceConnection,
16        [In, MarshalAs(UnmanagedType.BStr)] string Destconnection
17        );
18    void RefreshCache([In, MarshalAs(UnmanagedType.Interface)] object Connection);
19}
20
21[ComImport, CoClass(typeof(JetEngineClass)), Guid("9F63D980-FF25-11D1-BB6F-00C04FAE22DA")]
22public interface IJetEngine2
23{
24    void QueryInterface([In] ref Guid iid, [Out] out IntPtr ppvObj);
25    int AddRef();
26    int Release();
27   
28    void CompactDatabase(
29        [In, MarshalAs(UnmanagedType.BStr)] string SourceConnection,
30        [In, MarshalAs(UnmanagedType.BStr)] string Destconnection
31        );
32    void RefreshCache([In, MarshalAs(UnmanagedType.Interface)] object Connection);
33}
34
35IJetEngine1 iJetEngine = GetJetEngine() as IJetEngine1;
36IntPtr p1;
37iJetEngine.QueryInterface(typeof(IUnknown).Guid, out p1);
38
39IJetEngine2 iJetEngine = GetJetEngine() as IJetEngine2;
40IntPtr p2;
41iJetEngine.QueryInterface(typeof(IUnknown).Guid, out p2);
 

    上面兩種方式都是正確的,需要注意的是如果把IUnknown的方法放到IJetEngine2接口內部聲明的話,必須放到函數聲明的最開始位置,想想虛 函數編譯后函數指針的順序就明白了.不過這種方式有個不太好的地方就是搞了老半天好不容易才得到的一個對COM對象的包裝類,經過這么一查詢接口,又回到 了指針形態,很是不爽.

 

    這里說了幾種COM接口查詢的方式,無非就是COM對象的.NET包裝類的引用或者IntPtr指針轉來轉去的,這兩種COM對象的引用到底哪種更好點 呢.我的建議是能用包裝類引用的盡量用包裝類引用吧,實在不濟的時候沒有聲明包裝類也可以用object作為引用類型.

    我是不太喜歡直接操作COM對象的IntPtr指針的(非它類型的IntPtr指針除外,例如一個指向內存數據塊的指針),除非是實在沒有辦法的時候.原 因嘛,就是因為COM引用計數器的問題.前面我們也提到過了,使用COM包裝類的引用的時候,不管在接口之間怎么轉換,都不會產生新的對象;還有一點就是 COM對象的引用計數只會在生成包裝類的實例的時候才會增加1;另外COM包裝類也是一個托管類,只不過是一個比較特殊的托管類而以,所以它的實例的生命 周期還是遵循了一般托管類的生命周期的定義----當該對象沒有被任何一個變量所引用的時候,這個對象就需要被垃圾回收了.結合以上幾條,一個COM對象 只被包裝類的實例引用時,在整個包裝類的生命周期內,COM的引用計數都只是1,直到包裝類被垃圾回收了,這個時候CLR會自動減少這個包裝類所指向的 COM對象的引用計數,當計數器為0時COM對象也就被銷毀了.這個比C++里面的ComPtr還要妙,ComPtr在每賦值一次的時候還要對引用計數加 1呢.

    回過頭來我們再看看使用IntPtr的情況,正如前面所說的,理論上來講每賦值一次IntPtr都需要對COM計數加1,每當一個有效的IntPtr不再 使用了又要對其所引用的COM對象的計數器減1,對於現在C#程序員來說,很多甚至對內存的動態分配和釋放都沒有概念,更是會經常還要忘了COM計數器的 這些操作,編程的樂趣就這樣被消磨得沒有了,何其痛苦呀.

   另外就是在使用自己定義COM包裝類和接口的時候,經常會遇到一個接口的方法里面用到了另外的接口,如果一層一層展開下去會需要聲明一大堆的接口定義,而 我們其實中是需要其中的一個很少的功能,這樣太得不嘗失了.最簡單的方法就從我們的需要出發,保留我們需要調用的方法的接口的聲明,其它不相干的接口的參 數用object類型或IntPtr定義,在用object作為參數類型的時候需要在參數上加上 MarshalAs(UnmanagedType.Interface)特性,以表明這是一個COM接口,而不是一個其它什么類型,例如結構什么的.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。