将 .NET 组件公开到 COM






4.86/5 (115投票s)
一种通过 COM 可调用包装器从支持 COM 的非 .NET 环境中调用 .NET 函数的方法
目录
引言
在 .NET Framework 出现之前作为一名开发人员工作,确实让人更加欣赏 Framework 开箱即用的丰富类。然而,很多时候,您可能需要在非托管环境中工作,并希望调用托管世界中提供的那些现成的类。我一直在玩 .NET 有一段时间了,我学到了许多 .NET Framework 中提供的很棒的类。SimonS 最近在 C# 论坛上发布了一个问题,促使我完成了些研究,并想与大家分享我的发现。我其实一直在琢磨这个想法一段时间了,但从未真正有足够的时间完成。现在它终于完成了。
问题是,假设我编写了一个很棒的库、一组实用函数等,这些都在 .NET Framework 下运行,但我希望在 .NET 之前的开发环境中使用它。对于 SimonS,他特别希望使用 VB6。这时就需要 COM 可调用包装器 (CCW) 来创建一个代理,该代理将通过接口指针提供对我们函数的访问。这可以通过使用那些有趣的特性标签(我每天都发现它们越来越有用)和接口来实现。
首先,您需要包含 `System.Runtime.InteropServices;` 命名空间。我们将做的第一件事是创建一个与类名同名但前缀为 `_`(下划线)的接口。在此接口中,我们将需要包含所有我们希望从 .NET 程序集中“导出”的函数。为我们声明的接口应用 `InterfaceType` 特性非常重要;这将把我们的接口公开给 COM。接下来,在我们的类声明上方,我们将包含一个 `ClassInterface` 特性,它公开 .NET 类中的所有 `public` 方法、属性、字段和事件。以前,我使用的是 `AutoDual`,但是 Heath Stewart[^] 指出这不是最佳方法,因为它可能会在长期内导致版本相关问题。在阅读了更多内容后,我将代码更改为使用 `ClassInterfaceType.None`,这强制我们的类只能通过我们的接口进行访问。这使得在未来类发生变化时一切都保持可行。唯一需要注意的其他事项是,您需要继承上面定义的接口。
C# 源代码
using System;
using System.Runtime.InteropServices;
namespace Tester
{
[Guid("D6F88E95-8A27-4ae6-B6DE-0542A0FC7039")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface _Numbers
{
[DispId(1)]
int GetDay();
[DispId(2)]
int GetMonth();
[DispId(3)]
int GetYear();
[DispId(4)]
int DayOfYear();
}
[Guid("13FE32AD-4BF8-495f-AB4D-6C61BD463EA4")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("Tester.Numbers")]
public class Numbers : _Numbers
{
public Numbers(){}
public int GetDay()
{
return(DateTime.Today.Day);
}
public int GetMonth()
{
return(DateTime.Today.Month);
}
public int GetYear()
{
return(DateTime.Today.Year);
}
public int DayOfYear()
{
return(DateTime.Now.DayOfYear);
}
}
}
VB.NET 源代码
Imports System
Imports System.Runtime.InteropServices
Namespace Tester
<Guid("89439AD1-756F-4f9c-BFB4-18236F63251E"), _
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)> _
Public Interface _Tester
<DispId(1)> Function GetMonth() As Integer
<DispId(2)> Function GetDay() As Integer
<DispId(3)> Function GetYear() As Integer
<DispId(4)> Function DayOfYear() As Integer
End Interface
<Guid("1376DE24-CC2D-46cb-8BF0-887A9CAF3014"), _
ClassInterface(ClassInterfaceType.None), _
ProgId("Tester.Numbers")> Public Class Tester
Implements _Tester
Public Tester()
Public Function GetMonth() As Integer Implements _Tester.GetMonth
GetMonth = DateTime.Now.Month
End Function
Public Function GetDay() As Integer Implements _Tester.GetDay
GetDay = DateTime.Now.Day
End Function
Public Function GetYear() As Integer Implements _Tester.GetYear
GetYear = DateTime.Now.Year
End Function
Public Function DayOfYear() As Integer Implements _Tester.DayOfYear
DayOfYear = DateTime.Now.DayOfYear
End Function
End Class
End Namespace
更多关于特性 - 自动化
您可能想知道所有这些不同的特性都用于什么。简短的回答是自动化,更确切地说,是自动化服务器。Visual Basic 是一个自动化客户端,这意味着它消耗通过 `IDispatch` 接口公开其功能的 COM 组件。Visual Basic 无法进行 vtable 查找以获取接口指针的地址,因此 `IDispatch` 在此起作用。`IDispatch` 标识了几个重要方法,我们将在此讨论其中两个:`GetIDOfNames` 和 `Invoke`。正如您可能猜到的,`GetIDOfNames` 返回接口中定义的 `DispId`,然后可以将其与客户端的“调用”函数的任何参数一起传递给 `Invoke` 方法。通过使用特性,我们可以定义所有 `DispId` 甚至类的 `ProgId`。这种方法允许像 VBScript 这样的某些脚本客户端与 COM 组件通信。
编译前
手动进行还是不进行……
您可能会发现将“Register for COM interop”选项设置为 True 有用。这将创建一个类型库并进行正确的注册表条目。您不必这样做,实际上,您可以使用 .NET SDK 附带的 程序集注册工具 (Regasm.exe)[^],但直接在属性窗口中勾选它要简单得多。一个警告是,如果您是手动操作,没有 IDE 进行 COM 互操作注册,在运行 `reasm.exe` 工具创建类型库之前,您需要使用 .NET SDK 附带的 强名称工具 (sn.exe)[^] 来签名您的程序集,从而允许程序集放置在 全局程序集缓存 (GAC)[^] 中。以下截图显示了通过命令行创建强命名密钥文件。
将程序集安装到 GAC 以外的其他位置的决定最终取决于您。如果您希望将程序集放置在 GAC 以外的其他位置,在使用命令行上的 `regasm.exe` 时,应包含 `/codebase` 标志。您选择的任一位置,无论是 GAC 还是使用 `/codebase` 标志的自己的目录,都需要您有一个强命名程序集。创建此文件后,只需编辑 `AssemblyInfo.cs` 文件并将 `AssemblyKeyFile` 属性更改为反映新的强名称密钥文件名。如果您决定仅使用 .NET SDK,以下命令行应该可以正常工作,创建您的类型库并进行注册表添加。
regasm Tester.dll /tlb:Tester.tlb
要将程序集复制到 GAC,您可以使用 全局程序集缓存工具 (Gacutil.exe)[^] 配合以下命令
gacutil /i tester.dll
VB 登场
打开 VB,创建一个新的 Standard EXE,选择 **Project** ---> **References**,您的类应该会列出。在您的类上打勾并在您的窗体上添加一个按钮。点击按钮并添加以下“代码”。
Private Sub Command1_Click()
Dim i As Tester.Numbers
Set i = New Tester.Numbers
MsgBox "The date is: " & i.GetMonth & "/" & i.GetDay & "/" & i.GetYear
End Sub
再加一点 MFC
好的,我知道这里的大多数人,或者至少很多人定期使用 MFC。在这里,我们可以看到 MFC 的实现相当简单。为了缩短篇幅,我删除了初始化时关于“关于”框的代码。您需要在头文件中包含 `#import "Tester.tlb"` 语句以及 `using namespace Tester;`,假设您想要示例中所有内容的范围。
BOOL CNickDlg::OnInitDialog()
{
CDialog::OnInitDialog();
CString strMessage;
Tester::_Numbers *com_ptr;
CoInitialize(NULL);
Tester::_NumbersPtr p(__uuidof(Tester::Numbers));
com_ptr = p;
int m_iDay = com_ptr->GetDay();
int m_iMonth = com_ptr->GetMonth();
int m_iYear = com_ptr->GetYear();
strMessage.Format("Today is: %d/%d/%d", m_iMonth, m_iDay, m_iYear);
MessageBox(strMessage, "Today's Date", MB_ICONASTERISK |
MB_ICONINFORMATION);
SetIcon(m_hIcon, TRUE);
SetIcon(m_hIcon, FALSE);
return TRUE;
}
最后的想法
关于本文所应用的这个概念,已经有很多问题被提出来了。再说一遍,创建 COM 可调用包装器 (CCW) 的目的是严格提供 .NET 和 COM 之间的桥接机制。要运行,.NET Framework 仍然需要在客户端机器或服务器上安装。我们正在 .NET 中标识一个接口,该接口通过我们的特性公开为 COM 的 `IDispatch` 接口。`IDispatch` 接口是 VB 等自动化客户端用来启用 COM 支持的。因此,只需在正确的位置应用几个特性,我们就能快速创建一个程序集,该程序集将其方法公开给支持 COM 的非 .NET 应用程序。即使 COM 和 CLR 在不同的架构结构下工作,它们在集成时仍然能很好地协同工作。希望这对那些思考类似问题的人有所帮助。如果其他人有任何反馈,我很乐意接受,请在下方发布主题。
更新历史
- 2003/1/15 - 初始发布
- 2003/1/16 - 将 `ClassInterfaceType` 从 `AutoDual` 更改为 `None` 以纠正版本问题
- 2003/1/16 - 应要求添加了 VB.NET 源代码
- 2003/1/18 - 添加了 MFC 示例以增加趣味性
- 2003/2/10 - 包含更多关于过程的信息
- 2003/9/15 - 包含更多关于在没有 IDE 的情况下进行手动注册的信息
- 2003/10/28 - 在“最后的想法”部分包含了一个更全面的详细解释。
- 2003/11/26 - 更新包括了关于 `ProgId`、`Guid` 和 `DispId` 的覆盖