4

我需要在非托管 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# COM 客户端不起作用。它是一个参考“Interop.Hello.dll”的 C# 控制台应用程序。我使用以下命令制作互操作 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# 代码不起作用?

4

2 回答 2

17

不支持这样的接口

错误消息不明确。每个人都会认为是他们的接口不受支持,在你的情况下是 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 Remoting 和 WCF 的工作方式。除了 .NET 可以自动创建代理和存根,这要归功于反射。在 COM 中,它们需要由您创建。两种基本方式完成,一般方式是用一种称为 IDL 的语言描述 COM 接口,然后使用 midl.exe 工具对其进行编译。它可以从 IDL 中的接口描述中自动生成代理和存根代码。或者,当您的 COM 服务器将自身限制为自动化子集并可以生成类型库时,可以使用一种简单的方法。在这种情况下,您可以使用 Windows 中内置的代理/存根实现,它使用类型库来确定参数的外观。这真的很像反射。

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


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

所以线程必须告诉 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.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 客户端代码中出现故障。这是一个危险的假设,可以随时字节,但你很可能会侥幸逃脱。

于 2013-04-25T12:47:06.397 回答
3

正确的 C# 代码:

 [STAThread]
 static void Main(string[] args)
 {
     Interop.Hello.IHello Hello = new Interop.Hello.CHello();
     Hello.Print();
 }
于 2012-09-20T07:37:03.003 回答