如何处理类库中的COM查询接口失败



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

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

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

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

我想我理解失败的原因(通过 COM 文档,Chris Brumme 的博客) - 使用的 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));
    }    
}
众所周知,

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"线程模型。 Microsoft通常会多出一千英里来支持两者。

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

这需要执行一些在 .NET 中非常容易但在非托管代码中非常困难的事情。 必须将方法的参数从一个堆栈帧复制到另一个堆栈帧,以便可以在另一个线程上进行调用。 由于反射,在.NET中很容易做到。 对于非托管代码来说,这并不容易做到,它需要一个知道方法参数类型的预言机,以替代缺少的元数据。

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

顺便说一句,这是不寻常的,使用"两者"的线程模型的COM对象模型几乎总是支持封送处理。 只是不适用于您尝试使用的特定版本。 DirectShow在过去的10年中已被弃用,取而代之的是Media Foundation。 你找到了Microsoft决定退休的一个很好的理由。

所以这只是你需要知道的事情。 这个细节与必须知道 Random 类不是线程安全的没有太大区别。 它在 MSDN 中没有很好的文档,但如前所述,您自己很容易发现它。

最新更新