• COM套间对.NET程序使用COM对象的影响


    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.           [default] interface IStaObject;

    38.    };

    39.    [

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

    41.           helpstring("StaObject2 Class")

    42.    ]

    43.    coclass StaObject2

    44.    {

    45.           [default] interface 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. }

  • 相关阅读:
    ThinkPhp6.x+AntDesign+Vue前后端分离快速开发平台
    Laravel8.x+AntDesign+Vue前后端分离快速开发平台
    JavaWeb v1.0.0后台开发框架,专业版发布——细节完善,体验优化
    高并发情况下的DB重复插入解决方案随笔
    jQuery.qrcode.js客户端生成二维码,支持中文并且可以生成LOGO
    新能源车牌的问题
    OCR识别移动端的实现与应用
    在拥有vin码识别的时代,您还在傻乎乎手工录入吗?
    国内的车牌识别算法怎样选择
    一码在手运车无忧
  • 原文地址:https://www.cnblogs.com/killmyday/p/1395099.html
Copyright © 2020-2023  润新知