C# 如何处理类库中的COM查询接口故障
我在实用程序库中遇到了一个问题,它执行一些COM互操作。它保留对调用之间使用的COM对象的引用 如果使用相同的COM线程模型从线程调用所有方法,则该类工作正常 但是,如果创建COM对象的调用使用与后续调用不同的线程模型,QueryInterface将失败,并出现C# 如何处理类库中的COM查询接口故障,c#,com-interop,C#,Com Interop,我在实用程序库中遇到了一个问题,它执行一些COM互操作。它保留对调用之间使用的COM对象的引用 如果使用相同的COM线程模型从线程调用所有方法,则该类工作正常 但是,如果创建COM对象的调用使用与后续调用不同的线程模型,QueryInterface将失败,并出现E\u NOINTERFACE 我们只有在向单元测试添加async分支时才发现这一点;在此之前,它在所有MTA应用程序和所有STA单元测试中运行良好 我想我理解失败的原因(通过),正在使用的COM对象支持“两种”线程模型,这导致C#在ST
E\u NOINTERFACE
我们只有在向单元测试添加async
分支时才发现这一点;在此之前,它在所有MTA应用程序和所有STA单元测试中运行良好
我想我理解失败的原因(通过),正在使用的COM对象支持“两种”线程模型,这导致C#在STA和MTA创建的实例之间创建围栏
然而,从图书馆的角度来看,我能想到的唯一修复方法是一点垃圾:
- 将此库仅用于MTA线程作为一条不成文的规则
- 更改库以检测来自STA线程的调用并失败(例如使用
)CurrentThread.ApartmentState
- 更改库以为所有COM互操作创建自己的MTA线程(或者仅当传入呼叫位于STA线程上时)
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提供什么样的支持,但这些coclass是从工作线程使用的。你在那里犯下了可怕的罪行。当你使用[StatThread]时,你就做出了一个承诺。您必须做两件事:决不能阻塞线程,必须泵送消息循环(aka Application.Run)。注意你是如何打破这两个要求的。永远不要说谎,当你说谎的时候会发生非常糟糕的事情。但你还没走那么远 您所使用的coclass提供的线程支持很容易发现。启动Regedit.exe并导航到HKLM\Software\Wow6432Node\Classes\CLSID。找到您使用的{guid},并查看您在InProcServer32键中看到的ThreadingModel值。对于您正在使用的一个,它是“两者”。这意味着它是从STA线程和完全不支持线程安全并在MTA中运行的线程编写的。就像你的主线和任务一样。正如你所发现的,这两种方法都很好。请注意,这并不常见,绝大多数COM服务器只支持“单元”线程模型。微软通常会多花几千英里来支持两者 因此,您在STA线程上创建了枚举器对象,并在MTA中的线程上使用它。现在COM运行时必须做一些非常重要的事情,它必须确保从您调用的方法调用的任何回调(也称为事件)在同一个STA线程上运行,以便回调中的任何代码也是线程安全的。换句话说,它必须将工作线程的调用封送回主线程。与.NET应用程序中的Control.Invoke或Dispatcher.Invoke等效。在COM中完全自动完成 这需要做一些在.NET中非常简单但在非托管代码中非常困难的事情。方法的参数必须从一个堆栈帧复制到另一个堆栈帧,以便可以在另一个线程上进行调用。由于反射,在.NET中很容易实现。对于非托管代码来说,这并不容易,它需要一个知道方法参数类型的oracle来替代缺少的元数据 该oracle也可以在注册表中找到。使用Regedit并导航到HKLM\Software\Wow6432Node\Classes\Interface键。如异常消息所示,在此处找到接口guid{29840822-5B84-11D0-BD3B-00A0C911CE86}。您会注意到问题:它不在那里。是的,异常消息非常糟糕。报告真正的E_NOINTERFACE是因为COM运行时也找不到其他方法,不支持IMarshal。如果它在那里,那么您就要处理[StatThread]谎言,您的线程将死锁 顺便说一句,使用ThreadingModel“两者”的COM对象模型几乎总是支持封送处理。只是不适用于您尝试使用的特定对象。DirectShow在过去的10年中被贬低,被媒体基金会取代。你找到了微软决定让它退役的一个很好的理由 这是你需要知道的。这个细节与必须知道Random类不是线程安全的没有多大区别。MSDN中没有很好的文档记录,但正如所指出的,很容易自己发现