我需要在非托管C ++中使用COM服务器,在C#中使用COM客户端。我在C ++中找到了COM Hello World世界(http://antonio.cz/static/com/5.html)。 Page是捷克语。 COM服务器在IHello接口调用函数Print()之后显示带有文本“Hello world”的MessageBox。源代码在这里:http://antonio.cz/static/com/Hello.zip。该存档包含C ++中COM服务器和COM客户端的源代码,它可以工作。
但我的C#COM客户端不起作用。它是一个C#控制台应用程序,引用了“Interop.Hello.dll”。我用命令编写了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服务器。它有效。我在VB中使用“Interop.Hello.dll”进行了控制台应用程序。
VB代码:
Module Module1
Sub Main()
Dim ic As Interop.Hello.CHello
ic = CreateObject("MyCorporation.Hello")
ic.Print()
End Sub
End Module
我从C#客户端加载时调试了COM服务器。 QueryInterface()方法在变量“riid”中返回S_OK是IHello接口guid。
为什么C#代码不起作用?
答案 0 :(得分:15)
不支持此类界面
错误消息不明确。每个人都会认为他们的界面是不受支持的,IHello就是你的情况。但事实并非如此,而且错误信息并不能说清楚。不支持IMarshal interface。
COM处理.NET没有的编程细节,它不会忽略线程。众所周知,线程很难正确,有很多代码不是线程安全的。 .NET允许您在工作线程中使用此类代码,并且不会反对您的错误,通常会产生非常难以诊断的错误。 COM设计者最初认为线程太难以正确,应由聪明人照顾。并且构建在基础结构中以使用在工作线程安全中不是线程安全的代码。这很好用,它可以解决95%的典型线程问题。然而,最后5%往往会给你一个相当重要的头痛。喜欢这个。
与您的COM组件一样,可以发布从注册表中的线程使用是否安全。注册表值名称为" ThreadingModel"。 非常公共值,也是缺失时的默认值,是#34; Apartment"。解释公寓有点超出了这个答案的范围,它的确意味着"我不是线程安全的"。 COM基础结构确保对象的任何调用都是从创建对象的同一个线程中进行的,从而确保线程安全。
然而,这需要一些魔力。将来自一个线程的调用编组到特定的其他线程是一件非常重要的事情。 .NET使用Dispatcher.BeginInvoke和Control.BeginInvoke等方法使其看起来很简单,但它隐藏了一个相当大的代码冰山,99%的水下代码。 COM很难做到这一点,它缺少.NET功能,这使得它更容易实现,它不直接支持Reflection。一方面,需要在目标线程上构建堆栈帧,以便进行调用。这需要知道该方法的参数是什么样的。 COM需要帮助,它不知道它们的样子,因为它不能依赖于Reflection。需要的是两段代码,称为代理和存根。代理 知道参数的外观,并将方法的参数序列化为RPC数据包。该代码由COM自动调用,使用虚拟接口,与原始接口一样完全,但每个方法都进行代理调用。在目标线程上,存根代码接收RPC数据包,构建堆栈帧并进行调用。
这可能听起来很熟悉.NET术语,这正是.NET Remoting和WCF的工作方式。除了.NET可以通过Reflection自动创建代理和存根。在COM中,它们需要由您创建。完成的两种基本方法,一般方法是使用名为IDL的语言描述COM接口,并使用midl.exe工具进行编译。哪个可以从IDL中的接口描述自动生成代理和存根代码。或者有一种简单的方法,当您的COM服务器将自身限制为自动化子集并可以生成类型库时可用。在这种情况下,您可以使用Windows内置的代理/存根实现,它使用类型库来确定参数的外观。这真的很像反射。有了额外的步骤,必须在注册表中注册HKCR \ Interfaces键,所以COM可以找到它需要的代码。
那么异常消息真正意味着COM无法找到编组调用的方法。它在注册表中查找并找不到代理/存根的注册表项。然后它问你的COM对象"你知道如何编组自己吗?"通过查询IMarshal。答案是否定的!这就是它的结束,给你留下一个很难解释的异常消息。错误报告是COM的Achilles脚跟。
接下来,我需要关注为什么 COM决定它应该封送你的COM服务器的调用,这是你不希望发生的事情。调用COM对象的线程的一个基本要求是它需要告诉COM它为编组调用提供了什么样的支持。除了构建堆栈框架之外,第二件事情很难做,调用需要在一个非常特定的线程上进行,即创建COM对象的线程。实现线程的代码需要使这成为可能,这不是一件容易的事情。它需要解决一般producer/consumer problem,这是软件工程中的一般问题。 "生产者"是打电话的线程和"消费者"是创建对象的线程。
那么线程告诉COM的是"我实现了生产者/消费者问题的解决方案,继续并随意生产"。这个问题的通用解决方案是大多数Windows程序员所熟知的,它是一个消息循环" GUI线程实现的。
你很早就告诉COM,每个进行COM调用的线程都必须调用CoInitializeEx()。您可以指定两个选项之一,您可以指定COINIT_APARTMENTTHREADED(也称为STA)以保证为非线程安全的COM对象提供安全的主页。有"公寓"再次。或者您可以指定COINIT_MULTITHREADED(又名MTA),它基本上表示您 nothing 来帮助COM并将其留给COM基础架构进行排序。
.NET程序不直接调用CoInitializeEx(),CLR会为您调用。它还需要知道您的线程是STA还是MTA。您可以使用Main()方法的属性为程序的主线程指定,指定[STAThread]或[MTAThread]。 MTA是默认设置,也是线程池线程的默认选项。或者,当您创建自己的Thread时,可以通过调用Thread.SetApartmentState()来指定它。
MTA和COM对象的组合,它不是线程安全的,换句话说就是"我什么都不帮助COM"场景是这里问题的一部分。你强迫COM给对象一个安全的家。 COM基础架构将自动创建一个新线程,一个STA线程。它必须,没有其他方法来确保对象的调用将是线程安全的,因为你选择了不帮助。因此,您对该对象进行的任何调用都将被封送。这非常低效,创建自己的STA线程可以避免编组成本。但最重要的是,COM将需要代理和存根来进行调用。你没有实现它们,所以它是一个kaboom。
这适用于您的C ++客户端代码,因为它可能称为CoInitialize()。选择STA。它在您的VB.NET代码中起作用,因为vb.net运行时支持自动选择STA,这是该语言的典型代表,它会自动执行大量操作以帮助程序员陷入成功之中。
但那不是C#方式,它会自动完成很少的事情。你得到了kaboom,因为你的Main()方法没有[STAThread]属性所以它默认为MTA。
但请注意,这实际上并不是技术上正确的解决方案。当你向STA承诺时,你也必须履行这一承诺。这说明你解决了生产者/消费者的问题。这要求您在.NET中使用消息循环Application.Run()。你没有。
打破这种承诺可能会产生令人不快的后果。 COM将依赖您的承诺,并会在需要时尝试编组呼叫,期望它能够正常工作。它不起作用,因为你没有调用GetMessage(),所以调用不会在你的线程上调度。你没有消耗。您可以通过调试器轻松看到这一点,线程将死锁,调用永远不会完成。公寓线程COM服务器通常也很容易假设您的STA线程泵送消息循环并将使用它来实现自己的线程间编组,通常是通过从工作线程调用PostMessage()。 WebBrowser控件就是一个很好的例子。 PostMessage()消息不会出现在任何地方的副作用通常是组件不会引发事件或者不执行任务。对于WebBrowser,您将永远不会获得DocumentCompleted事件。
听起来你的COM服务器没有做出这些假设,否则你不会在工作线程上进行调用。或者你会注意到你的C ++或VB.NET客户端代码出现故障。这是一个危险的假设,可以随时写字,但你可能会侥幸逃脱。
答案 1 :(得分:3)
正确的C#代码:
[STAThread]
static void Main(string[] args)
{
Interop.Hello.IHello Hello = new Interop.Hello.CHello();
Hello.Print();
}