在C#中加载COM对象抛出异常"无法将类型为'System .__ ComObject'的COM对象转换为接口类型...",但C++或VB没有

dli*_*dli 4 c# c++ com com-interop

我需要在非托管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
Run Code Online (Sandbox Code Playgroud)

C#代码:

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

但是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)).
Run Code Online (Sandbox Code Playgroud)

我也试过从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
Run Code Online (Sandbox Code Playgroud)

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

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

Han*_*ant 15

没有这样的界面支持

错误消息不明确.每个人都会认为这是他们不受支持界面,IHello就是你的情况.但情况并非如此,错误信息并不能说清楚.它是不受支持的IMarshal接口.

COM负责.NET不支持的编程细节,它不会忽略线程.众所周知,线程很难正确,有很多代码不是线程安全的..NET允许您在工作线程中使用此类代码,并且不会反对您弄错,通常会产生非常难以诊断的错误.COM设计者最初认为线程太难以正确,应由聪明人照顾.并构建在基础结构中,以便在工作线程安全中使用非线程安全的代码.这很好用,它可以解决95%的典型线程问题.然而,最后5%往往会给你一个相当重要的头痛.像这个.

与您的COM组件一样,COM组件可以发布从注册表中的线程使用是否安全.注册表值名称为"ThreadingModel".一个非常常见的值,也就是缺失时的默认值,是"公寓".解释公寓有点超出了这个答案的范围,它的确意味着"我不是线程安全的".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的致命弱点.


接下来,我需要关注为什么 COM决定它应该封送你的COM服务器的调用,这是你不期望发生的事情.调用COM对象的线程的一个基本要求是它需要告诉COM它为编组调用提供了什么样的支持.除了构建堆栈框架之外,第二件事情很难做,调用需要在一个非常特定的线程上进行,即创建COM对象的线程.实现线程的代码需要使这成为可能,这不是一件容易的事情.它需要解决一般的生产者/消费者问题,这是软件工程中的一般问题.其中"producer"是进行调用的线程,"consumer"是创建对象的线程.

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

你很早就告诉COM,每个进行COM调用的线程都必须调用CoInitializeEx().您可以指定两个选项之一,您可以指定COINIT_APARTMENTTHREADED(也称为STA)以保证为非线程安全的COM对象提供安全的主页.还有"公寓"这个词.或者你也可以指定COINIT_MULTITHREADED(又名MTA),基本上说,你做什么来帮助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客户端代码出现故障.这是一个危险的假设,可以随时发送,但你可能会侥幸逃脱.

  • 写的棒棒哒 那很有启发性。 (2认同)