如何在纯 C 语言中使用 .NET C# COM 对象
逐步演示如何将 C 类与 C# 中创建的 COM 对象进行通信。
前言
我最近遇到了一个问题:如何在 C 项目中调用 C# 类中的属性或方法? 谷歌给我的信息对于这类问题并不准确。我花了一些时间研究,当最终弄清楚该怎么做时,我决定将这些知识分享给其他人——也许我可以为别人节省一些时间。
我想声明——我不是 C/C++ 程序员,我更常使用 C#,所以如果你知道如何做得更好,请给我发电子邮件或在评论中提出建议。我在这里想展示的是一个直接的解决方案,适用于不熟悉 C/C++/COM 编程的人。
引言
在开始之前,我们应该回答一个基本问题
我们为什么需要这个?我们可以使用 CLR 并且以更小的努力达到相同的结果,不是吗?这是真的,但只有在你需要从头开始创建这种通信时才成立。本文主要面向那些已经创建了程序(用 C 编写),并且由于可能带来的问题而无法将编译器从 C 更改为 CLR 的人。
本文涵盖了在 .NET 对象(通过 COM)和纯 C(无需 CLR 和/或额外框架的任何帮助)之间建立通信的基本设置。这使您能够直接从纯 C 项目中调用 .NET(用 C# 或 VB 创建)方法、属性等。
我们的目标是从 C# 类库获取一些数据到 C Win32 控制台应用程序。听起来很简单,但需要几个步骤才能完成。让我们开始吧!
1. 设置正确的编译器选项
1.1 首先,我们需要在 Visual Studio 中创建一个解决方案(我使用 VS2008 Pro)。选择:文件 -> 新建 -> 项目,然后选择 其他语言 -> Visual C++ -> Win32 控制台应用程序。我将其命名为“plainC”。点击“确定”后,项目向导应该会弹出。点击“下一步”后,我建议取消选中 预编译头文件 选项,除非你熟悉它。最后点击“完成”。
1.2 如你所见,IDE 会生成 *.cpp 类,确实这是一个 C++ 项目。让我们将其更改为 C。我们需要将主文件从“plainC.cpp”重命名为“plainC.c”。
1.3 接下来我们需要更改编译语言选项:右键点击项目 -> 属性,然后选择 配置属性 -> C/C++ -> 高级 -> 编译为,并选择:编译为 C 代码 (/TC)。
其余选项可以(或应该为了此解决方案)保持不变。特别是在常规配置属性选项卡中
1.4 好的,现在我们可以创建 C# 类(为了方便,让我们在当前解决方案中完成)。在 Visual C# -> Windows 中创建新项目,并选择:类库。我将项目命名为“CSharpLib”。
1.5 在项目属性中,我们可以设置自动注册我们的程序集以进行 COM 互操作。我们可以在以下位置找到此选项:生成 -> 注册 COM 互操作。如果出于某种原因您不想自动执行此操作,则可以使用 regasm.exe(有关如何使用的更多详细信息和信息,您可以查看 msdn)。
2. 准备 .NET 代码
2.1 现在我们终于可以向 C# 项目添加新类了——例如:NetClass,它带有一些属性和显式默认构造函数。我们还需要一个接口(INetClassComVisible),因为稍后我们将通过它调用类成员。
using System;
using System.Runtime.InteropServices;
namespace CSharpLib
{
[ComVisible(true)]
[Guid("1A741C67-8D57-4a83-8E0B-834D6D526D0C")]
[ClassInterface(ClassInterfaceType.None)]
public class NetClass : INetClassComVisible
{
public string StringValue
{
get
{
return "Example string";
}
}
public string CreationTime
{
get;
internal set;
}
public int IntegerValue
{
get
{
return 50;
}
}
public string CustomValue
{
get;
internal set;
}
public int Add(int x, int y)
{
return x + y;
}
public NetClass() { }
}
[ComVisible(true)]
[Guid("C27A0124-BA12-4323-B83C-68C27536F989")]
public interface INetClassComVisible
{
string StringValue { get; }
string CreationTime { get; }
int IntegerValue { get; }
string CustomValue { get; }
int Add(int x, int y);
}
}
简短的代码解释
[ComVisible(true)]
此属性控制 COM 在本地的可见性和可访问性。这意味着,只有带有此属性的类型才可见,而其他类型则不可见。如果你想反转此行为(所有可见的都不可见,标记的都不可见),那么你可以在项目属性中选中“应用程序->程序集信息->使程序集 COM 可见”,或者在 AssemblyInfo.cs 中更改条目,从
[assembly: ComVisible(false)]
to
[assembly: ComVisible(true)]
当然,用 `[ComVisible(false)]` 属性标记你想要不可见的类型。
--------------------------------------------------------
[Guid("1A741C67-8D57-4a83-8E0B-834D6D526D0C")]
这是必要的,因为 GUID 参与连接我们的 COM 和 C 世界。它必须显式声明,因为我们需要在两边使用相同的 GUID。您可以使用 GUID 生成器工具 (guidgen.exe),它可以帮助创建类/接口的 GUID,或者在 VisualStudio->工具->创建 GUID 中选择:注册表格式。
--------------------------------------------------------
[ClassInterface(ClassInterfaceType.None)]
此属性可以帮助我们通过我们 COM 可见类实现的显式定义接口来访问类型成员。
--------------------------------------------------------
一切都清楚了吗?很好,那我们继续。
3. 生成 C 和 COM 世界之间的桥梁
3.1 打开 OLE/COM 对象查看器 (C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\OleView.exe),然后单击“查看类型库”选项(三个红色三角形按钮),选择 CSharpLib.tlb——如果你选中了“注册 COM 互操作”,这个文件应该会与 dll 一起创建。你也可以从 regasm 创建 tlb(我之前提到过)。
3.2 之后会弹出一个新窗口,其中包含一些内容。这些内容实际上是生成的接口定义语言代码,它是独立于语言的接口描述。遗憾的是,我不知道为什么 ITypeLib 查看器中的“另存为”选项对我来说不起作用。以这种方式保存的文件内容会被截断,这将导致后续错误(天哪……我花了多少时间才找出这些错误的原因……)。解决方案很简单——我们需要手动创建文件。从查看器中选择所有内容,然后复制并粘贴到新文件中。将其保存为 CSharpLib.IDL
3.3 现在我们需要 MIDL 编译器来执行操作。
"Microsoft 接口定义语言 (MIDL) 定义了客户端和服务器程序之间的接口。MIDL 可用于所有基于 Windows 操作系统的客户端/服务器应用程序。它也可用于为异构网络环境创建客户端和服务器程序。"
那么,让我们利用它。运行 Visual Studio 2008 命令提示符(开始 -> Microsoft Visual Studio 2008 -> Visual Studio Tools -> Visual Studio 2008 命令提示符)。然后将当前目录更改为我们已编译库所在的位置
cd C:\PlainC\CSharpLib\bin\Debug
并使用以下参数运行 midl 编译器
midl /Oicf CSharpLib.IDL /h CSharpLib.h
它应该创建两个文件:CSharpLib.h 和 CSharpLib_i.c
一点点解释
/Oicf
指定无代码代理封送方法,该方法包含 /Oi 和 /Oic 提供的所有功能,但使用一种新的解释器(快速格式字符串),其性能优于 /Oi 或 /Oic。此开关包含最近的 RPC 增强功能,建议用于现代 RPC 场景。
/h
/h 开关指定 filename 作为头文件的名称,该头文件包含 IDL 文件中包含的所有定义,而没有 IDL 语法。此文件可以用作 C 或 C++ 头文件。
更多详情:MIDL 命令行参考
4. 准备 C 代码
4.1 现在我们必须将编译器生成的文件包含到我们的 C 项目中。为此,将它们移动到 PlainC 项目目录,并从 VS 中使用“添加现有文件”选项。最简单的代码可能如下所示
#include "stdafx.h"
#include "CSharpLib.h"
int _tmain(int argc, _TCHAR* argv[])
{
BSTR bstrVal = SysAllocString(L"");
HRESULT hr = S_OK;
INetClassComVisible* dotNetClass = NULL;
hr = CoInitialize(NULL);
hr = CoCreateInstance(&CLSID_NetClass, NULL, CLSCTX_INPROC_SERVER,
&IID_INetClassComVisible, (void**) &dotNetClass);
hr = dotNetClass->lpVtbl->get_StringValue(dotNetClass,&bstrVal);
CoUninitialize();
return 0;
}
调试代码将显示 `dotNetClass` 被正确实例化,并且调用 `get_StringValue` 方法也正常工作。`bstrVal` 成功填充了正确的 getter 值(“Example string”)。示例代码中提供了更复杂的示例,请查看。
CoCreateInstance 方法的小解释
HRESULT CoCreateInstance( _In_ REFCLSID rclsid, - CLSID associated with type which we want to create _In_ LPUNKNOWN pUnkOuter, - if not NULL it should point to object's IUnknown interface _In_ DWORD dwClsContext, - indicate the context to manage newly created object _In_ REFIID riid, - IID to interface to communicate through it _Out_ LPVOID *ppv - address of pointer variable that receives return value );
MSDN 参考:CoCreateInstance 函数
请记住,您不需要将 CSharpLib.dll 放在与 PlainC.exe 相同的位置。COM 模型处理所有与库位置相关的事务。当然,如果您将其从注册位置移动,它将无法工作(您将收到 HRESULT = 系统找不到指定的文件)。但是,如果 COM 未使用 'codebase' 标志注册,则库必须位于与调用它的可执行文件相同的位置。
关注点
如您所见,将纯 C 环境与 COM 合并确实很痛苦,但它是可以做到的。在下一篇文章中,我将尝试展示它在 C++ 中的样子(幸运的是,在那里它更容易)。