在 C# 中加载 COM 对象会引发异常"无法将类型'System.__ComObject'的 COM 对象强制转换为接口类型...",但C++或 VB 不会



我需要在非托管C++中制作COM服务器,在C#中制作COM客户端。我在C++中找到了COM Hello World教程(http://antonio.cz/static/com/5.html)。页面为捷克语。COM服务器在从IHello接口调用函数Print()后显示带有文本"Hello world"的MessageBox。源代码在这里:http://antonio.cz/static/com/Hello.zip。该档案包含C++中COM服务器和COM客户端的源代码,并且可以正常工作。

但是我的C#客户端不起作用。这是一个参考"Interop.Hello.dll"的C#控制台应用程序。我用命令制作Interop.dll

tlbimp Hello.tlb /out:Interop.Hello.dll

C#代码:

static void Main(string[] args)
{
Interop.Hello.IHello Hello = new Interop.Hello.CHello();
Hello.Print();
}

但C#客户端抛出异常:

Unable to cast COM object of type 'System.__ComObject' to interface type
'Interop.Hello.CHello'. This operation failed because the QueryInterface call on the
COM component for the interface with IID '{B58DF060-EAD9-11D7-BB81-000475BB5B75}' 
failed due to the following error: No such interface supported (Exception from 
HRESULT: 0x80004002 (E_NOINTERFACE)).

我也尝试从Visual Basic加载COM服务器。它是有效的。我参考"Interop.Hello.dll"在VB中制作了控制台应用程序。

VB代码:

Module Module1
Sub Main()
Dim ic As Interop.Hello.CHello
ic = CreateObject("MyCorporation.Hello")
ic.Print()
End Sub
End Module

从C#客户端加载时,我调试了COM服务器。当变量"riid"为IHello接口guid时,QueryInterface()方法返回S_OK。

有什么想法为什么C#代码不起作用吗?

不支持

错误消息不明确。每个人都会认为不支持他们的接口,在您的情况下是IHello。但事实并非如此,错误信息也不够清楚。不支持IMarshal接口。

COM处理.NET没有的编程细节,它不会忽略线程。众所周知,线程很难正确处理,有很多代码不是线程安全的。NET允许您在工作线程中使用这样的代码,并且不会反对您出错,通常会产生非常难以诊断的错误。COM设计者最初认为线程处理太难了,应该由聪明的人来处理。并且内置在基础结构中,使在工作线程中使用不安全的代码无论如何都是线程安全的。它工作得很好,可以处理95%的典型线程问题。然而,最后5%往往会让你头疼不已。就像这个。

像您的COM组件一样,可以发布从注册表中的线程使用它是否安全。注册表值名称为"ThreadingModel"。一个非常的常用值,也是缺失时的默认值,是"Apartment"。解释公寓有点超出了这个答案的范围,它实际上意味着"我不安全"。COM基础结构确保对对象的任何调用都是从创建对象的同一线程进行的,从而确保线程安全。

然而,这需要一些魔法。将调用从一个线程编组到特定于另一个线程是一件非常简单的事情。.NET使用Dispatcher.BeginInvoke和Control.BeginInvoke等方法使其看起来很简单,但这隐藏了一个相当大的代码冰山,99%的代码都被淹没了。COM很难做到这一点,它缺少一个.NET功能,使其更容易实现,它不直接支持反射。

首先,所需要的是在目标线程上构建一个堆栈框架,以便可以进行调用。这需要知道该方法的参数是什么样子的。COM需要帮助,它不知道它们是什么样子的,因为它不能依赖反射。所需要的是两段代码,称为代理和存根。代理确实知道参数的样子,并将方法的参数序列化为RPC数据包。该代码由COM自动调用,使用一个看起来与原始接口完全相似的伪接口,但每个方法都进行代理调用。在目标线程上,存根代码接收RPC数据包,构建堆栈帧并进行调用。

在.NET术语中,这听起来可能很熟悉,这正是.NET远程处理和WCF的工作方式。除了.NET可以通过Reflection自动创建代理和存根之外。在COM中,它们需要由您创建。有两种基本的方法,一般的方法是用一种名为IDL的语言描述COM接口,并使用midl.exe工具进行编译。它可以根据IDL中的接口描述自动生成代理和存根代码。或者有一种简单的方法,当COM服务器将自己限制为Automation子集并可以生成类型库时,该方法可用。在这种情况下,您可以使用内置在Windows中的代理/存根实现,它使用类型库来计算参数的样子。这真的很像Reflection。额外的一步是必须在注册表HKCR\Interfaces键中注册,这样COM就可以找到它需要的代码。

因此,异常消息的真正含义是COM找不到封送调用的方法。它在注册表中查找,但找不到代理/存根的注册表项。然后,它通过查询IMarshal来询问COM对象"你知道如何封送自己吗?"。答案是否定的!这就结束了,给您留下了一个很难解释的异常消息。错误报告是COM的致命弱点。


接下来,我需要关注为什么COM决定将调用封送到您的COM服务器,这是您没有预料到的。对调用COM对象的线程的一个基本要求是,它需要告诉COM它对封送处理调用提供了什么样的支持。这是除了构建堆栈框架之外很难做的第二件事,需要在一个非常特定的线程上进行调用,即创建COM对象的线程。实现线程的代码需要实现这一点,这不是一件小事。它需要解决一般的生产者/消费者问题,这是软件工程中的一个一般问题。其中"生产者"是进行调用的线程,"消费者"是创建对象的线程。

因此,线程必须告诉COM的是"我实现了生产者/消费者问题的解决方案,继续并随意生产"。大多数Windows程序员都知道这个问题的通用解决方案,它是GUI线程实现的"消息循环"。

你很早就告诉COM了,每个进行COM调用的线程都必须调用CoInitializeEx()。您可以指定两个选项中的一个,也可以指定COINIT_PARTMENTTHREADED(也称为STA)来保证为非线程安全的COM对象提供一个安全的home。又出现了"公寓"这个词。或者,您可以指定COINIT_MULTITHREADED(又名MTA),这基本上意味着您不做任何事情来帮助COM,并将其留给COM基础设施来解决。

A。NET程序不直接调用CoInitializeEx(),CLR会为您进行调用。它还需要知道您的线程是STA还是MTA。您可以使用程序主线程的Main()方法上的一个属性来执行此操作,该属性指定[STAThread]或[MTAThread]。MTA是默认选项,也是线程池线程的默认选项和唯一选项。或者,当您创建自己的线程时,您可以通过调用Thread来指定它。SetApartmentState()。

MTA和COM对象的组合不是线程安全的,或者换句话说,"我什么都不帮COM"的情况是这里问题的一部分。您强制COM为对象提供一个安全的家。COM基础结构将自动创建一个新线程STA线程。这是必须的,没有其他方法可以确保对对象的调用是线程安全的,因为您选择了退出帮助。因此,对对象进行的任何调用都将被封送处理。这是非常低效的,创建自己的STA线程可以避免封送处理成本。但最重要的是,COM将需要代理和存根来进行调用。你没有实施它们,所以这是一个笑话。

这在C++客户端代码中起作用,因为它可能调用了CoInitialize()。选择STA。它在你的VB.NET代码中起作用,因为VB.NET运行时支持自动选择STA,这是该语言的典型特征,它自动做了很多事情来帮助程序员陷入成功的深渊。

但这不是C#的方式,它很少自动执行一些事情。您得到kaboom是因为Main()方法没有[STAThread]属性,所以它默认为MTA。

但是,请注意,这实际上不是技术上正确的解决方案。当你向STA承诺时,你也必须履行这个承诺。它说你解决了生产者/消费者的问题。这需要你泵一个消息循环,应用程序。在中运行()。NET。你没有。

违背承诺可能会产生不愉快的后果。COM将依赖您的承诺,并在需要时尝试封送调用,期望它能正常工作。它将不起作用,调用不会在线程上调度,因为您没有调用GetMessage()。你没有消费。使用调试器可以很容易地看到这一点,线程将死锁,调用永远不会完成。单元线程COM服务器通常也很容易假设STA线程泵送消息循环,并将使用它来实现自己的线程间封送处理,通常是通过从工作线程调用PostMessage()。WebBrowser控件就是一个很好的例子。PostMessage()消息不去任何地方的副作用通常是,组件不会引发事件或不执行职责。例如,在WebBrowser的情况下,您永远不会得到DocumentCompleted事件。

听起来你的COM服务器没有做出这些假设,否则你也不会在工作线程上进行调用。或者您可能已经注意到它在您的C++或VB.NET客户端代码中出现故障。这是一个危险的假设,可以在任何时候字节,但你可能会逃脱惩罚

右C#代码:

[STAThread]
static void Main(string[] args)
{
Interop.Hello.IHello Hello = new Interop.Hello.CHello();
Hello.Print();
}

相关内容

最新更新