COM套間對.NET程式使用COM物件的影響

技術小美發表於2017-11-12

COM時代裡,套間是用來簡化多執行緒環境下使用COM物件的,然而在.NET裡面,微軟又放棄了套間的概念,這樣給我們在.NET裡面使用COM物件的時候造成了很多的麻煩。例如有的時候你會發現在有的執行緒裡面建立了COM物件並將它的引用儲存在全域性變數裡面,在其他的執行緒裡面使用的時候,卻發現.NET扔出一個InvalidCastException的異常,發生這種情況大多數都是因為兩個.NET執行緒執行在不同的套間引起的。比如下面的COM伺服器和C#客戶端:

C#客戶端的原始碼

1. using System;

2. using System.Collections.Generic;

3. using System.Linq;

4. using System.Runtime.InteropServices;

5. using System.Text;

6. using System.Diagnostics;

7. using System.Security.Cryptography;

8. using System.Security.Principal;

9. using Microsoft.Win32.SafeHandles;

10. using System.ComponentModel;

11. using System.Reflection;

12. using System.Security;

13. using System.IO;

14. using System.Threading;

15. using System.Security.Permissions;

16.

17. using ApartmentComponentLib;

18.

19. namespace CSharpQuestions

20. {

21.     public class Watcher

22.     {

23.         private object m_IStaObject = null;

24.

25.         [STAThread]

26.         public static void Main()

27.         {

28.             Console.WriteLine(Thread.CurrentThread.GetApartmentState());

29.             Watcher watcher = new Watcher();

30.             watcher.Initialize();

31.             watcher.CreateThreads().Join();

32.

33.             Console.WriteLine(“Press any key”);

34.             Console.ReadLine();

35.         }

36.

37.         private Thread CreateThreads()

38.         {

39.             Thread thread = new Thread(ThreadFunc);

40.             thread.Start();

41.

42.             return thread;

43.         }

44.

45.         private void ThreadFunc()

46.         {

47.             Console.WriteLine(Thread.CurrentThread.GetApartmentState());

48.             IStaObject2 obj = (IStaObject2)m_IStaObject;

49.             obj.TestMethod();

50.        }

51.

52.         private void Initialize()

53.         {

54.             m_IStaObject = new StaObject2Class();

55.         }

56.     }

57. }

 

COM伺服器端

IDL檔案

1. import “oaidl.idl”;

2. import “ocidl.idl”;

3.

4. [

5.     object,

6.     uuid(34CF395D-F7F8-41FC-9074-E966304DA425),

7.     dual,

8.     nonextensible,

9.     helpstring(“IStaObject Interface”),

10.    pointer_default(unique)

11. ]

12. interface IStaObject : IDispatch{

13.    [id(1), helpstring(“method TestMethod”)] HRESULT TestMethod(void);

14. };

15. [

16.    object,

17.    uuid(2451960E-F141-4F09-AB71-124E62B6A25E),

18.    helpstring(“IStaObject2 Interface”),

19.    pointer_default(unique)

20. ]

21. interface IStaObject2 : IUnknown{

22.    HRESULT TestMethod(void);

23. };

24. [

25.    uuid(E410D347-D200-4362-82B8-F3361FA54446),

26.    helpstring(“ApartmentComponentLib Type Library”)

27. ]

28. library ApartmentComponentLib

29. {

30.    importlib(“stdole2.tlb”);

31.    [

32.           uuid(2C0624C9-4C88-4114-A165-9E4AA59A241F),

33.           helpstring(“StaObject Class”)

34.    ]

35.    coclass StaObject

36.    {

37.           [defaultinterface IStaObject;

38.    };

39.    [

40.           uuid(274BDE35-680D-48DC-A2F3-3AE26E7700DA),

41.           helpstring(“StaObject2 Class”)

42.    ]

43.    coclass StaObject2

44.    {

45.           [defaultinterface IStaObject2;

46.    };

47. };

 

標頭檔案

1. // StaObject2.h : Declaration of the CStaObject2

2.

3. #pragma once

4. #include “resource.h”       // main symbols

5.

6. #include “ApartmentComponent_i.h”

7.

8. #if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)

9. #error “Single-threaded COM objects are not properly supported on Windows CE platform”

10. #endif

11.

12. // CStaObject2

13. class ATL_NO_VTABLE CStaObject2 :

14.    public CComObjectRootEx<CComSingleThreadModel>,

15.    public CComCoClass<CStaObject2, &CLSID_StaObject2>,

16.    public IStaObject2

17. {

18. public:

19.    CStaObject2()

20.    {

21.    }

22.

23. DECLARE_REGISTRY_RESOURCEID(IDR_STAOBJECT2)

24.

25.

26. BEGIN_COM_MAP(CStaObject2)

27.    COM_INTERFACE_ENTRY(IStaObject2)

28. END_COM_MAP()

29.

30.

31.

32.    DECLARE_PROTECT_FINAL_CONSTRUCT()

33.

34.    HRESULT FinalConstruct()

35.    {

36.           return S_OK;

37.    }

38.

39.    void FinalRelease()

40.    {

41.    }

42.

43. public:

44.    STDMETHOD(TestMethod)(void);

45.

46. };

47.

48. OBJECT_ENTRY_AUTO(__uuidof(StaObject2), CStaObject2)

 

CPP檔案

1. #include “stdafx.h”

2. #include “StaObject2.h”

3. #include <iostream>

4.

5. using namespace std;

6.

7. // CStaObject

8. STDMETHODIMP CStaObject2::TestMethod(void)

9. {

10.    cout << “CStaObject2::TestMethod” << endl;

11.

12.    return S_OK;

13. }

Rgs檔案

1. HKCR

2. {

3.     ApartmentComponent.StaObject2.1 = s `StaObject2 Class`

4.     {

5.            CLSID = s `{274BDE35-680D-48DC-A2F3-3AE26E7700DA}`

6.     }

7.     ApartmentComponent.StaObject2 = s `StaObject2 Class`

8.     {

9.            CLSID = s `{274BDE35-680D-48DC-A2F3-3AE26E7700DA}`

10.           CurVer = s `ApartmentComponent.StaObject2.1`

11.    }

12.    NoRemove CLSID

13.    {

14.           ForceRemove {274BDE35-680D-48DC-A2F3-3AE26E7700DA} = s `StaObject2 Class`

15.           {

16.                  ProgID = s `ApartmentComponent.StaObject2.1`

17.                  VersionIndependentProgID = s `ApartmentComponent.StaObject2`

18.                  InprocServer32 = s `%MODULE%`

19.                  {

20.                        val ThreadingModel = s `Free`

21.                  }

22.                  `TypeLib` = s `{E410D347-D200-4362-82B8-F3361FA54446}`

23.           }

24.    }

25. }

 

COM 伺服器註冊,然後使用tlbimp.exe生成一個可以被C#客戶端程式引用的IAInterop Assembly),並且編譯執行上面的C#客戶端程式,你會發現.NET會丟擲一個System.InvalidCastException異常:

System.InvalidCastException occurred

 Message=”Unable to cast COM object of type `ApartmentComponentLib.StaObject2Class` to interface type `ApartmentComponentLib.IStaObject2`. This operation failed because the QueryInterface call on the COM component for the interface with IID `{2451960E-F141-4F09-AB71-124E62B6A25E}` failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).”

 Source=”ApartmentComponentLib”

 StackTrace:

       at ApartmentComponentLib.StaObject2Class.TestMethod()

 InnerException:

從高亮顯示的訊息裡面可以看出,當我們試圖在另外一個執行緒使用另一個執行緒建立的物件的時候,查詢所需要的COM介面失敗也就是為什麼.NET扔出來一個InvalidCastException

這就是一個典型的跨套間使用COM物件失敗的例子,因為跨套間使用COM物件時,COM要求所使用的COM介面是可列集(Marshal)的。如果所要求(QueryInterface)的介面不能被列集(Marshal),在呼叫端那裡QueryInterface返回E_NOINTERFACE,雖然看起來好像是不支援所查詢的介面,實際上是因為COM沒有辦法將介面從一個套間列集到另外一個套間裡面去。

COM裡面,套間是一個 想象中的邊界,用來在多執行緒環境中安全使用執行緒安全和執行緒不安全的COM物件。什麼叫做執行緒安全的COM物件呢?再多執行緒環境中,如果這個COM物件自己實現了同步機制,可以被多個執行緒同時呼叫而不破壞物件內部資料的完整性的話,那麼這個物件就叫做執行緒安全的物件。然而COM物件有一個目標就是,即使在多執行緒環境裡面也可以安全地使用執行緒不安全的COM物件。也就是說,即使COM物件內部沒有實現同步機制,COM也有一個機制可以建立一個執行緒安全的環境來使用這個物件,這個機制就是套間。在多執行緒環境裡面,套間為執行緒不安全的COM物件建立了一個同步機制,COM保證在任意時刻都只有一個客戶端在呼叫執行緒不安全的COM物件(呼叫它的函式—COM世界裡面只有函式和介面)。

 

關於套間的知識,可以參考下面兩篇文章:

http://www.codeguru.com/cpp/com-tech/activex/apts/article.php/c5529

http://www.codeguru.com/cpp/com-tech/activex/apts/article.php/c5533

而如果需要跨套間呼叫COM物件,這個函式呼叫,呼叫使用的引數和函式呼叫返回值都需要在套間之間被列集。而如果你的引數裡面使用到了COM介面的話,例如跨套間使用一個COM物件,並且呼叫這個物件的QueryInterface方法,QueryInterface返回的介面就需要被列集。COM庫使用CoMarshalInterThreadInterfaceInStream CoGetInterfaceAndReleaseStream來列集介面。

CoMarshalInterThreadInterfaceInStream 查詢登錄檔HKEY_CLASSES_ROOT”Interface”{IID}”ProxyStubClsid32中要列集的介面是否註冊有列集程式(ProxyStub程式)。

1.       如果這個鍵值存在,CoMarshalInterThreadInterfaceInStream會啟用裡面CLSID對應的COM物件來完成介面的列集;

2.       如果沒有這個鍵值,那麼說明沒有提供方法列集介面,因此QueryInterface返回E_NOINTERFACE

如果你細心一點的話,會發現很多介面的ProxyStubClsid32裡面的CLSID是一樣的,而且這些介面通常都會有另外一個子鍵:TypeLib。這是因為手工編寫處理介面列集的COM物件的工作繁瑣又容易出錯,

1.       所以對於一些Dual介面,COM庫(實際上是OLEAUT32.dll)提供了一個通用的類來列集所有的Dual介面,它所需要的就是型別庫檔案因為型別庫裡面包含了所有COM物件的後設資料(Meta Data);

2.       另外,對於非Dual介面,你也可以使用MIDL根據IDL檔案生成對應的列集介面的COM物件。

這是在上一篇文章裡面例子程式裡面出現InvalidCastException的原因。

由於所有的COM物件都會被分配到一個相應的套間裡面,因此在.NET裡面,為了方便.NET程式呼叫COM物件,每一個.NET執行緒都會被分配到一個套間裡面――即使你沒有在程式碼裡面指定執行緒執行的套間。在.NET執行緒裡面建立的COM物件都會被分配到特定的套間裡面,如果兩個.NET執行緒 被分配到了不同的套間裡,那麼兩個執行緒之間互相呼叫COM物件就需要列集函式呼叫。

.NET 2.0以後,預設情況下.NET的執行緒是執行在多套間(MTA)裡面的,但是在Visual Studio裡面建立C#工程的時候,Visual Studio的專案模板會在你程式碼的Main函式上加上[STAThread]屬性,表示主執行緒執行在STA套間裡面,這樣就會造成.NET程式有執行緒執行在兩個不同的套間裡面:

        [STAThread]

        public static void Main()

Main函式上面加上[STAThread]屬性會使.NET將主執行緒放在STA套間裡面,而加上[MTAThread]屬性則會將主執行緒放在MTA套間裡面。

而對於其他執行緒,則需要使用Thread.SetApartmentState()函式來設定執行緒所執行的套間,而這個函式必須線上程啟動之前呼叫,也就是在Thread.Start ()之前呼叫,這也是為什麼.NET提供一個[STAThread]屬性和[MTAThread]屬性的原因――因為你沒有辦法在主執行緒啟動之前設定主執行緒執行的套間。線上程裡面,你可以使用Thread.GetApartmentState()函式來獲取執行緒所執行的套間資訊。

COM套間對.NET程式使用COM物件的影響(上)文章裡面的程式碼的修復方案如下,即將主執行緒的STAThread屬性去掉,讓大家都執行在程式裡面唯一一個MTA套間裡面,這樣就沒有列集介面的問題了:

1. using System;

2. using System.Collections.Generic;

3. using System.Linq;

4. using System.Runtime.InteropServices;

5. using System.Text;

6. using System.Diagnostics;

7. using System.Security.Cryptography;

8. using System.Security.Principal;

9. using Microsoft.Win32.SafeHandles;

10. using System.ComponentModel;

11. using System.Reflection;

12. using System.Security;

13. using System.IO;

14. using System.Threading;

15. using System.Security.Permissions;

16.

17. using ApartmentComponentLib;

18.

19. namespace CSharpQuestions

20. {

21.     public class Watcher

22.     {

23.         private object m_IStaObject = null;

24.

25.         public static void Main()

26.         {

27.             Console.WriteLine(Thread.CurrentThread.GetApartmentState());

28.             Watcher watcher = new Watcher();

29.             watcher.Initialize();

30.             watcher.CreateThreads().Join();

31.

32.             Console.WriteLine(“Press any key”);

33.             Console.ReadLine();

34.         }

35.

36.         private Thread CreateThreads()

37.         {

38.             Thread thread = new Thread(ThreadFunc);

39.             thread.Start();

40.

41.             return thread;

42.         }

43.

44.         private void ThreadFunc()

45.         {

46.             Console.WriteLine(Thread.CurrentThread.GetApartmentState());

47.             IStaObject2 obj = (IStaObject2)m_IStaObject;

48.             obj.TestMethod();

49.         }

50.

51.         private void Initialize()

52.         {

53.             m_IStaObject = new StaObject2Class();

54.         }

55.     }

56. }

標籤: CLRCOMCOM互操作
本文轉自 donjuan 部落格園部落格,原文連結:  http://www.cnblogs.com/killmyday/archive/2009/02/20/1395099.html ,如需轉載請自行聯絡原作者


相關文章