如何处理 class 库中的 COM QueryInterface 失败
How to deal with COM QueryInterface failure in class library
我在执行某些 COM 互操作的实用程序库中遇到问题。它保留对在调用之间使用的 COM 对象的引用。
如果所有方法都是从使用相同 COM 线程模型的线程调用的,则 class 工作正常。
但是,如果创建 COM 对象的调用使用与后续调用不同的线程模型,QueryInterface 将失败并显示 E_NOINTERFACE
。
我们在向单元测试添加 async
分支时才发现这一点;在此之前,它 运行 在所有 MTA 应用程序和所有 STA 单元测试中都很好...
我想我理解失败的原因(通过 COM docs, Chris Brumme's blog)- 使用的 COM 对象支持 "both" 线程模型,这导致 C# 在 STA 和 MTA 创建的之间创建一个栅栏实例。
然而,从图书馆的角度来看,我唯一能想到的修复有点垃圾:
- 将此库仅用于 MTA 线程作为不成文的规定
- 更改库以检测来自 STA 线程的调用并失败(使用例如
CurrentThread.ApartmentState
)
- 更改库为所有 COM 互操作创建自己的 MTA 线程(或者可能只是当传入调用在 STA 线程上时)
有cleaner/easier选项吗?这是一个 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 class 更容易上手。几乎每个人都知道,比方说,List<> 或 Random class 不是线程安全的。知道如何以线程安全的方式使用它们的人并不多。 COM 设计者有更崇高的目标,并假设程序员一般不知道如何编写线程安全代码,聪明的人应该注意它。
确实需要处理 几个 细节。首先也是最重要的,您必须告诉 COM 您愿意为不是线程安全但无论如何都从工作线程使用的 coclasses 提供什么样的支持。你在那里犯下了可怕的罪行。当您使用 [STAThread] 时,您就做出了 承诺 。有两件事你必须做:你绝不能阻塞线程,你必须抽出一个消息循环(又名 Application.Run)。请注意您是如何违反 两个 要求的。永远不要撒谎,当你这样做时会发生非常糟糕的事情。但你还没有做到这一点。
您可以从正在使用的 coclass 中获得的线程支持很容易发现。启动 Regedit.exe 并导航至 HKLM\Software\Wow6432Node\Classes\CLSID。找到您使用的 {guid} 并查看您在 InProcServer32 键中看到的 ThreadingModel 值。您正在使用的是"Both"。意味着它是为在 STA 线程和完全不支持线程安全的线程以及 MTA 中的 运行s 而编写的。就像你的主线程和你的任务。正如您所发现的,两者都可以正常工作。请注意,这不是很常见,绝大多数 COM 服务器仅支持 "Apartment" 线程模型。 Microsoft 通常会不遗余力地支持这两者。
因此,您在 STA 线程上创建了枚举器对象,并在 MTA 中的线程上使用它。现在 COM 运行time 必须做一些非常重要的事情,它必须确保 可能 的任何回调(又名事件)从您调用的方法中调用 运行 在同一个 STA 线程上,以便回调中的任何代码也是线程安全的。换句话说,它必须将来自工作线程的调用编组回您的主线程。相当于 .NET 应用程序中的 Control.Invoke 或 Dispatcher.Invoke。在 COM 中完全自动完成。
这需要做一些在 .NET 中非常容易但在非托管代码中非常困难的事情。该方法的参数必须从一个堆栈帧复制到另一个堆栈帧,以便可以在另一个线程上进行调用。由于反射,在 .NET 中很容易做到。对于非托管代码,这并不容易,它需要一个知道方法参数类型的 oracle,以替代丢失的元数据。
该 oracle 也在注册表中找到。使用 Regedit 并导航到 HKLM\Software\Wow6432Node\Classes\Interface 键。在那里找到接口 guid,{29840822-5B84-11D0-BD3B-00A0C911CE86} 正如异常消息告诉您的那样。您会注意到问题:它不存在。是的,异常消息非常糟糕。报realE_NOINTERFACE是因为COM运行time也找不到别的方法,不支持IMarshal。如果它在那里,那么您将不得不处理 [STAThread] 谎言,您的线程将死锁。
这很不寻常,顺便说一句,使用 "Both" ThreadingModel 的 COM 对象模型几乎总是也支持封送处理。只是不适用于您尝试使用的 specific。 DirectShow 在过去 10 年已被弃用,取而代之的是 Media Foundation。您找到了 Microsoft 决定停用它的一个很好的理由。
所以这正是您需要知道的。一个细节与必须知道 Random class 不是线程安全的没有太大区别。它在 MSDN 中没有很好的记录,但如前所述,您很容易发现它。
我在执行某些 COM 互操作的实用程序库中遇到问题。它保留对在调用之间使用的 COM 对象的引用。
如果所有方法都是从使用相同 COM 线程模型的线程调用的,则 class 工作正常。
但是,如果创建 COM 对象的调用使用与后续调用不同的线程模型,QueryInterface 将失败并显示 E_NOINTERFACE
。
我们在向单元测试添加 async
分支时才发现这一点;在此之前,它 运行 在所有 MTA 应用程序和所有 STA 单元测试中都很好...
我想我理解失败的原因(通过 COM docs, Chris Brumme's blog)- 使用的 COM 对象支持 "both" 线程模型,这导致 C# 在 STA 和 MTA 创建的之间创建一个栅栏实例。
然而,从图书馆的角度来看,我唯一能想到的修复有点垃圾:
- 将此库仅用于 MTA 线程作为不成文的规定
- 更改库以检测来自 STA 线程的调用并失败(使用例如
CurrentThread.ApartmentState
) - 更改库为所有 COM 互操作创建自己的 MTA 线程(或者可能只是当传入调用在 STA 线程上时)
有cleaner/easier选项吗?这是一个 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 class 更容易上手。几乎每个人都知道,比方说,List<> 或 Random class 不是线程安全的。知道如何以线程安全的方式使用它们的人并不多。 COM 设计者有更崇高的目标,并假设程序员一般不知道如何编写线程安全代码,聪明的人应该注意它。
确实需要处理 几个 细节。首先也是最重要的,您必须告诉 COM 您愿意为不是线程安全但无论如何都从工作线程使用的 coclasses 提供什么样的支持。你在那里犯下了可怕的罪行。当您使用 [STAThread] 时,您就做出了 承诺 。有两件事你必须做:你绝不能阻塞线程,你必须抽出一个消息循环(又名 Application.Run)。请注意您是如何违反 两个 要求的。永远不要撒谎,当你这样做时会发生非常糟糕的事情。但你还没有做到这一点。
您可以从正在使用的 coclass 中获得的线程支持很容易发现。启动 Regedit.exe 并导航至 HKLM\Software\Wow6432Node\Classes\CLSID。找到您使用的 {guid} 并查看您在 InProcServer32 键中看到的 ThreadingModel 值。您正在使用的是"Both"。意味着它是为在 STA 线程和完全不支持线程安全的线程以及 MTA 中的 运行s 而编写的。就像你的主线程和你的任务。正如您所发现的,两者都可以正常工作。请注意,这不是很常见,绝大多数 COM 服务器仅支持 "Apartment" 线程模型。 Microsoft 通常会不遗余力地支持这两者。
因此,您在 STA 线程上创建了枚举器对象,并在 MTA 中的线程上使用它。现在 COM 运行time 必须做一些非常重要的事情,它必须确保 可能 的任何回调(又名事件)从您调用的方法中调用 运行 在同一个 STA 线程上,以便回调中的任何代码也是线程安全的。换句话说,它必须将来自工作线程的调用编组回您的主线程。相当于 .NET 应用程序中的 Control.Invoke 或 Dispatcher.Invoke。在 COM 中完全自动完成。
这需要做一些在 .NET 中非常容易但在非托管代码中非常困难的事情。该方法的参数必须从一个堆栈帧复制到另一个堆栈帧,以便可以在另一个线程上进行调用。由于反射,在 .NET 中很容易做到。对于非托管代码,这并不容易,它需要一个知道方法参数类型的 oracle,以替代丢失的元数据。
该 oracle 也在注册表中找到。使用 Regedit 并导航到 HKLM\Software\Wow6432Node\Classes\Interface 键。在那里找到接口 guid,{29840822-5B84-11D0-BD3B-00A0C911CE86} 正如异常消息告诉您的那样。您会注意到问题:它不存在。是的,异常消息非常糟糕。报realE_NOINTERFACE是因为COM运行time也找不到别的方法,不支持IMarshal。如果它在那里,那么您将不得不处理 [STAThread] 谎言,您的线程将死锁。
这很不寻常,顺便说一句,使用 "Both" ThreadingModel 的 COM 对象模型几乎总是也支持封送处理。只是不适用于您尝试使用的 specific。 DirectShow 在过去 10 年已被弃用,取而代之的是 Media Foundation。您找到了 Microsoft 决定停用它的一个很好的理由。
所以这正是您需要知道的。一个细节与必须知道 Random class 不是线程安全的没有太大区别。它在 MSDN 中没有很好的记录,但如前所述,您很容易发现它。