天天看點

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接口,而不是一個其它什麼類型,例如結構什麼的.