如何处理类库

时间:2016-07-06 15:58:37

标签: c# com-interop

我在实用程序库中遇到问题,它会执行一些COM互操作。它保留了对调用之间使用的COM对象的引用。

如果使用相同的COM线程模型从线程调用所有方法,则该类工作正常。

但是如果创建COM对象的调用使用与后续调用不同的线程模型,则QueryInterface将失败并显示E_NOINTERFACE

当我们在单元测试中添加async分支时,我们才发现这一点;在此之前它在所有MTA应用程序中运行良好的所有STA单元测试......

我想我理解失败的原因(通过COM docsChris Brumme's blog) - 正在使用的COM对象支持"两者"线程模型,它导致C#在STA和MTA创建的实例之间创建一个围栏​​。

然而,从图书馆的角度来看,我能想到的唯一解决方法是垃圾:

  • 将此库设为仅适用于MTA线程的不成文规则
  • 更改库以检测来自STA线程的调用并失败(使用例如CurrentThread.ApartmentState
  • 更改库以为所有COM互操作创建自己的MTA线程(或者仅当传入呼叫在STA线程上时)

有更清洁/更简单的选择吗?这是一个MCVE:

class Program
{
    [ComImport, Guid("62BE5D10-60EB-11d0-BD3B-00A0C911CE86")] class SystemDeviceEnum { };
    [ComVisible(true), ComImport, Guid("29840822-5B84-11D0-BD3B-00A0C911CE86"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    interface ICreateDevEnum { [PreserveSig] int CreateClassEnumerator([In] ref Guid pType, [Out] out IEnumMoniker ppEnumMoniker, [In] int dwFlags); }
    static ICreateDevEnum createDeviceEnum;
    static Guid VideoInputDeviceCategory = new Guid("860BB310-5D01-11d0-BD3B-00A0C911CE86");
    static void Prepare()
    {
        var coSystemDeviceEnum = new SystemDeviceEnum();
        createDeviceEnum = (ICreateDevEnum)coSystemDeviceEnum;
    }
    static int GetDeviceCount()
    {
        IEnumMoniker enumMoniker;
        createDeviceEnum.CreateClassEnumerator(ref VideoInputDeviceCategory, out enumMoniker, 0);
        if (enumMoniker == null) return 0;
        int count = 0;
        IMoniker[] moniker = new IMoniker[1];
        while (enumMoniker.Next(1, moniker, IntPtr.Zero) == 0) count++;
        return count;
    }
    [STAThread] 
    static void Main(string[] args)
    {
        RunTestAsync().Wait();
    }
    private static async Task RunTestAsync()
    {
        Prepare();        
        await Task.Delay(1);
        var count = GetDeviceCount();
        Console.WriteLine(string.Format("{0} video capture device(s) found", count));
    }    
}

1 个答案:

答案 0 :(得分:6)

众所周知,COM线程很难理解。实际上,很多比线程化.NET类更容易上手。几乎每个人都知道,例如List<>或Random类不是线程安全的。并不是很多人都知道如何以线程安全的方式使用它们。 COM设计人员有更高的目标,并认为程序员一般不知道如何编写线程安全的代码,聪明的人应该处理它。

确实需要处理少数细节。首先,您必须告诉COM您愿意为不是线程安全的coclass提供什么样的支持,但无论如何都要从工作线程中使用。在那里你犯了一个可怕的,可怕的罪行。当您使用[STAThread]时,您会做出承诺。你必须做的两件事:你必须永远不要阻止线程,你必须抽一个消息循环(又名Application.Run)。请注意您如何破坏两个要求。永远不要撒谎,当你这么做时会发生非常糟糕的事情。但是你还没有那么远。

您可以从您正在使用的coclass中获得的线程支持类型很容易被发现。启动Regedit.exe并导航到HKLM \ Software \ Wow6432Node \ Classes \ CLSID。找到您使用的{guid}并查看您在InProcServer32密钥中看到的ThreadingModel值。对于您正在使用的那个,它是“两者”。意味着它被编写为从STA线程和根本不支持线程安全的线程工作并在MTA中运行。就像你的主线程和你的任务一样。正如您所发现的,它可以正常工作。请注意,这不是很平常,绝大多数COM服务器只支持“Apartment”线程模型。微软通常需要额外的千里才能支持两者。

因此,您在STA线程上创建了枚举器对象,并在MTA中的线程上使用它。现在COM运行时必须做一些非常重要的事情,它必须确保从你调用的方法调用可能的任何回调(也就是事件)在同一个STA线程上运行,这样任何代码都可以回调也是线程安全的。换句话说,它必须将来自工作线程的调用封送回主线程。相当于.NET应用程序中的Control.Invoke或Dispatcher.Invoke。在COM中完全自动完成。

这需要在.NET中执行非常简单的操作,但在非托管代码中却非常困难。必须将方法的参数从一个堆栈帧复制到另一个堆栈帧,以便可以在另一个线程上进行调用。借助Reflection可以轻松完成.NET。对于非托管代码来说,这并不容易,它需要一个知道方法参数类型是什么的oracle,它是缺少元数据的替代品。

该oracle也可以在注册表中找到。使用Regedit并导航到HKLM \ Software \ Wow6432Node \ Classes \ Interface键。正如异常消息所示,找到那里的接口guid,{29840822-5B84-11D0-BD3B-00A0C911CE86}。您会注意到问题:它不存在。是的,异常消息非常糟糕。报告了真实的 E_NOINTERFACE,因为COM运行时也找不到其他方式,不支持IMarshal。如果它会在那里,那么你将处理[STAThread]谎言,你的线程将陷入僵局。

这是不寻常的,使用“Both”的ThreadingModel的COM对象模型几乎总是支持编组。只是不适合您尝试使用的特定。 DirectShow在过去10年中已被弃用,取而代之的是Media Foundation。你找到了微软决定退休的一个很好的理由。

所以这只是你需要知道的事情。与必须知道Random类不是线程安全的细节没有什么不同。它在MSDN中没有很好的记录,但如上所述,它很容易被你自己发现。