UML软件工程组织

(COM) 添加引用的背后:有人看见桥了吗?
Sam Gentile 2003 年 10 月 来源:Microsoft

适用于:
   COM Interop
   Microsoft® .NET Framework
   C# 语言
   Microsoft Visual Studio® .NET

摘要   Sam Gentile 解释了 COM Interop 和 Microsoft .NET Framework 之间为什么需要桥,以及如何在 .NET Framework 中实现这些桥。

前提条件:

  • 具有 Microsoft .NET 核心概念的基本知识(程序集、特性、反射、类、属性和事件)
  • 能够在 Visual Studio .NET 中用 C# 创建 Windows 窗体应用程序
  • 能够使用语言编译器来生成和管理应用程序

下载相关的代码示例(212 KB)。(请注意,在示例文件中,程序员的注释使用的是英文,本文中将其译为中文是为了便于读者理解。)

目录

简介
有人看见桥了吗?
桥的作用
COM Callable Wrapper (CCW)
求平方示例
生成 Interop 程序集
托管的平方程序
本文小结及下一篇文章的内容

简介

Interop 是 .NET Framework 中的一种非常有用且必需的技术。为什么是这样呢?坦白地讲,许多公司现在确实使用大量的现有 COM 代码。虽然这些公司知道托管代码的众多好处,但是他们非常希望能够利用在 COM 中的现有巨大投入来实现托管代码,而无需重新从头编写其全部应用程序。一个好消息是公共语言运行库 (CLR) 包含 COM Interop,从而可以借助 .NET Framework 从托管代码中使用此功能。一个不太好的消息是通常这并不是一件简单的事情。

简单地讲,COM Interop 是 COM 和 .NET 之间的一座“桥”。许多开发人员注意到 Visual Studio .NET 包含神奇的 (COM) Add Reference(添加引用)向导(Add Reference | COM Wizard [添加引用 | COM 向导])时很高兴。该向导使开发人员可以选择 COM 组件并执行某些“神奇”的操作来处理 .NET 应用程序。常见的问题是,开发人员不得不深入到向导背后,才能为其应用程序提供实际的 COM Interop 技术。这就是困难所在。

System.Runtime.InteropServices 命名空间包含近 300 个类。.NET Framework SDK 中还有各种 Interop 命令行工具。如果这些还不足以令人感到恐惧的话,此外任何复杂的 Interop 项目中都会出现许多问题,这些问题是由 COM 的细微差别以及 COM 和 .NET 之间的巨大差异造成的。根据我个人的经验,开发人员经常需要深入到 (COM) 添加引用向导的背后并使用这些命令行工具,同时还要较深入地了解所发生的情况,以使各方面能够按预期工作(或者甚至是仅仅能够工作)。

本文从“添加引用”后开始。我并不准备花时间向您显示 Visual Studio .NET 向导的屏幕快照。MSDN 已经为其提供了大量信息(请参阅 Calling COM Components from .NET Clients [英文])。本文是一系列文章中的第一篇,目的是深入探究编程人员在使用 COM Interop 过程中将要遇到的各种问题,这些问题不太好理解或者现有资料不够充分,但完成工作时又不可避免会遇上。

本文以及后续文章的代码示例都是用 C# 编写的,这只是个人的偏好。但是,这里要强调一下,CLR 的操作方式比以前的 Microsoft 技术更为抽象。对于 CLR 来说,只有一个类程序库和一个类型系统,而各种语言以不同的语法提供了各种公共服务。可以将 .NET 语言视为覆盖在基类程序库 (BCL)、公共类型系统 (CTS) 和 CLR 上的语法糖衣,而使用的语法(语言)只是一个个人口味问题。

可以开始了吗?那就探讨进一步的内容。

有人看见桥了吗?

每个人都了解桥的作用。桥使您能够在由无法通行的河流、海湾等隔开的两个区域之间穿行,可以类似地将 COM Interop 视为桥。这里有两个存在巨大差异的世界,即 COM 世界和 CLR 世界,它们之间界限明显。需要“桥接”这些差异,从而使 COM 世界能够与 CLR 托管世界一起工作。要想使 COM Interop 确实发挥作用,它必须是一座很好的桥,并隐藏掉两个世界各自的一些细节。.NET 编程人员希望的是他们能够像新建和使用其他任何 .NET 组件一样处理 COM 组件。.NET 编程人员不应当被卷入到 COM 世界中并调用 CoCreateInstance (Ex) 来创建 COM 组件以及处理引用计数以及类似的概念。他们应当使用操作符 new 来创建对象并调用相关方法,就像处理任何其他 .NET 组件一样。

对于另一方也应当是这种情况。如果您将一个 .NET 组件提供给一个 COM 客户端,则该组件看上去应当与其他任何 COM 组件一样。这样,通过 COM,开发人员便可以执行 QueryInterface 以及多年以来他们所进行的所有有趣的事情。我的意思是什么呢?也就是基础组件不应当更改,编程模型也不应当更改。我们将看到,对于大多数情况都是这样的,Interop 在桥接这些差异方面是非常成功的。但是,这两个世界的差异非常大,某些情况下会导致问题十分棘手。我们将在以后的文章中讨论这些问题。

究竟为什么需要桥?简单地讲,虽然这两种技术在 Interop 组件方面具有共同的目标,但这两个世界的差异实在是太大了。人们不应当对这一点感到十分惊奇。CLR 是一个托管执行的世界,其中包含内存回收器、通用编程模型以及其他许多内容。COM 存在于一个非托管的、引用计数的世界中,其中包含具有很大差异的编程模型。虽然 COM 具有一个二进制标准,但是仍然存在许多不同的编程模型,例如,Visual Basic®、Microsoft 基础类程序库 (MFC) 和活动模板库 (ATL)(这只是其中的几个例子),这些模型的抽象程度各不相同。了解这一点后,我们将简短介绍直接影响 Interop 的某些差异。这些差异包括标识、定位组件、对象的生存期、编程模型、类型信息、版本控制以及错误处理等方面。下一节将简短讨论这些方面的差异,请注意,每个主题的完整详细说明已经在许多书和文章中进行了专门论述,本文不包含这方面的内容。

标识

所有 COM 编程人员都非常熟悉全局唯一标识符 (GUID),它用于在 COM 中唯一地标识各种对象。在全局范围内,这些 128 位的数字无论在任何时候任何地点都是唯一的(除非您重复使用他人的标识符)。在 COM 中,任何位置都可以使用 GUID,可以作为 CLSID(类标识符)、IID(接口标识符)、LIBID(程序库标识符)和 AppID(应用程序标识符)等,这只是其中的几个例子。它们的作用都相同:赋予 COM 中的对象一个全局唯一的标识。

大多数人都无法记住 128 位数字,因此类可以采用便于用户记忆的名称,这些名称与 128 位数字相对应。

CLR 没有使用这种系统。类型只是由其名称来标识,并进一步由其所在的命名空间来限定。这称为“简单名称”。但是,任何 CLR 类型的完整类型标识都包括类型名称、命名空间及其包含程序集。也就是说,程序集也作为部署和打包的类型范围单元和逻辑单元。

定位组件

COM 和 .NET 定位组件的方式存在很大差异。COM 组件的物理位置可以任意,但是有关如何找到和加载它们的信息则是放在一个中心位置:注册表。而 CLR 组件根本就不使用注册表。所有托管程序集都将此信息以元数据的形式存储在程序集内。此外,.NET 组件还可以作为私有组件与其应用程序存在于相同的目录中,也可以作为全局共享组件存在于全局程序集缓存 (GAC) 中。

要用 CoCreateInstance 实例化某个 COM 组件,COM 将在注册表中查找 CLSID 项及其关联值。通过这些值,COM 就会知道实现您要加载的 COM 公共类的 DLL 或 EXE 的名称和位置。COM 具有引人注目的众多特色,其中之一就是位置的透明性。简单地讲,COM 客户端将以相同的方式调用对象,不管对象是与客户端位于同一个进程、位于同一台本地计算机上的不同进程,还是运行在根本不同的计算机上,注册表都会将对象的位置告知 COM。这种系统很容易遭到破坏。如果文件更改了位置但没有更改其注册表设置,程序将完全破坏。这会导致出现糟糕的“DLL Hell”问题。

由于这一点以及其他许多原因,.NET 组件采用了一种完全不同的方法。CLR 将查找三个位置之一:GAC、本地目录或由配置文件指定的某个其他位置。.NET 的目标之一就是从根本上简化部署。对于大多数应用程序,可以将组件部署到应用程序所在的本地目录中,一切都将正常工作。这称为 x-copy 部署。共享的程序集可以放在 GAC 中。有关这方面的详细信息,请参阅 Jeffrey Richter 编写的《Applied Microsoft .NET Framework Programming》(英文)。

对象的生存期

存在最大差异的一个方面可能是 COM 和 .NET 对以下问题的处理:对象在内存中应当存在多长时间以及如何确定该时间。

COM 使用引用计数系统来确定对象的生存期。这增加了对象以及编程人员的负担,因为这需要对象维护自己的生存期并确定应当在何时删除自己。COM 规范清楚地说明了此模型的规则。整个方案的关键是 IUnknown 接口,所有 COM 组件都必须实现该接口,所有 COM 接口都是从该接口派生的。IUnknown 包含两个直接负责处理引用计数的方法。Add 方法用于递增引用数,而 Release 方法用于递减引用数。当计数到达零时,对象可能被破坏。此方案会产生各种细微的差异,并且可能会创建对象循环。此外,此方案很容易出现错误,并且是造成困扰 COM 编程人员多年的许多问题的原因。

CLR 将编程人员从这一责任中完全解脱出来。CLR 通过使用内存回收来管理所有对象引用。内存回收器确定对象不再被使用后释放该对象。关键差异在于这是一个非确定性的过程。与 COM 不同,在最后一个客户端使用完对象后,对象不会立即释放,而是在内存回收器回收内存时才会释放。释放发生的时间是将来的某个不可预知的时间。大多数情况下,这不会导致问题。但是,在以后的文章中将看到,如果您的 COM 设计在某个时间点明确调用了释放操作,则这可能成为一个大问题。.NET Framework 确实在 System.Runtime.InteropServices 命名空间中提供了一个 ReleaseComObject 系统调用,用于要求立即释放,但是这会导致进一步的问题(我们稍后将对此加以说明)。

编程模型

COM 编程耗费编程人员大量的精力。虽然有许多可以大大简化 COM 编程的抽象编程模型(例如 Microsoft Visual Basic),但事实是,COM 需要严格遵守一组规则并需要大量低级、晦涩的详细信息才能有效地工作。此外,用于 COM 的编程工具也有很多,包括从 Delphi 到 MFC、ATL,再到 Visual Basic。虽然其中的每种工具都能够生成有效的 COM 组件(这些组件符合内存中的 COM 的二进制 v 表布局),但是它们的编程模型却存在很大差异。了解一种工具和模型的编程人员在选择使用另一种工具时,不得不面对一个全新的编程模型。鉴于这一点以及其他许多原因,.NET Framework 进行了很大的简化,将编程模型简化为一个。.NET Framework 具有一个一致的面向对象的基类程序库 (BCL) 框架,它独立于编程语言和工具。

在 COM 编程中,编程人员从来不会真正获得一个对实际对象的引用。而是 COM 客户端获得一个接口引用并通过它调用对象的方法(当然,Visual Basic 给人的感觉是对类进行编程,但实际上使用的是 COM 接口)。另外,COM 还没有实现继承。

.NET Framework 与此截然不同,它是一个完全面向对象的平台,在其中编程人员可以充分使用各种类。虽然编程人员可以使用接口,但是该模型并不要求他们这样做。

类型信息

在基于组件的系统中,有一点很重要:用某种方法说明组件的接口或约定,以及说明如何在组件及其客户端或使用者之间交换信息。COM 规范没有强制规定这样一种交换格式,因而它不属于该规范中的内容。这样就出现了两种不同的格式,而不是一种。

第一种是 Microsoft 接口定义语言 (MIDL),它实际上源于 OSF DCE RPC,而后者使用 IDL 并以一种独立于语言的方式编写远程过程调用 (RPC) 的说明。Microsoft IDL 提供了 DCE IDL 扩展,以支持 COM 接口、公共类定义、类型定义以及其他内容。使用 MIDL 编译器编译 IDL 时,将生成一组 C/C++ 头文件,从而使网络代码能够通过各种网络协议进行远程 RPC 调用。

更为常见的情况是使用类型库(TLB 文件)。类型库是用 IDL 术语说明的,被编译成一种二进制资源,然后类型库浏览器就可以读取和呈现该资源。其中的一个问题是,类型库完全是可选的并且不完整。而且,COM 没有强制要求其中的信息的正确性。此外,该格式是不可扩展的,也不试图说明组件的相关性。

为了创建一个真正一流的组件环境,类型信息以元数据的形式遍布于 CLR 的各个层次。除了 MSIL 以外,还需要 CLR 编译器来发布标准的元数据。类型信息始终是最新的、完整的和精确的。这赋予了 .NET 组件“自我描述”特性。

通过本节,您应当开始注意到我们需要通过某种转换过程来将 COM 类型定义转换为 .NET 元数据。

版本控制

在组件工程中,版本控制是一个重要的且难以解决的问题。接口可能会随着时间发生变化,而这可能导致客户端被破坏。COM 接口不存在版本控制问题。COM 接口被认为是不可改变的;在定义和公开后,它就不会再更改。任何更改(例如添加成员或更改方法中参数的顺序)都会导致客户端被破坏。因此,在 COM 中,如果对现有接口进行任何更改,我们都将定义一个全新的接口。如果定义并发布了一个 COM 接口 IFoo,并且希望进行更改或添加成员,则将定义一个新接口 IFoo2。

COM 的二进制对象模型及其在内存中的表示实际定义了 COM 接口。COM 接口是内存中的一个 v 表,接口指针是一个指向它的 v 指针。这种非常精确的模型是很脆弱的。对 v 表顺序或字段排列的任何更改都将导致客户端被破坏。

.NET Framework 从一开始就被设计为完全支持组件的版本控制。每个 .NET 程序集在被赋予一个严格的名称后,都可以包含一个由四部分组成的版本号,其形式为 Major.Minor.Build.Revision,该版本号存储在其清单中。CLR 完全支持内存中同时存在同一程序集的多个版本,这些版本彼此之间是独立的。CLR 还支持一个完全的版本控制策略,该策略可以由管理员在 XML 配置文件中应用,而配置文件可以基于计算机或应用程序来应用,从而将客户端绑定到特定版本的程序集。

错误处理

COM 和 .NET 处理错误报告的方式有很大差异。COM 具有多种处理错误的方式,但主要的方式是让方法返回类型为 HRESULT 的错误代码。HRESULT 是 32 位的状态代码,它可以告知调用者发生的错误的类型。HRESULTS 由三部分组成:设备代码、信息代码和严重度位。严重度由最显著的位指示,它说明是成功还是失败。设备代码指示错误源。信息代码位于最低的 16 位中,它包含错误或警告说明。

本文不想花费更多时间介绍 HRESULT,只是想说明没有强制客户端来检查它们,在 COM 运行库中也没有强制方法返回它们。可以将它们忽略。除了 HRESULT 以外,COM 公共类还可以支持附加接口 ISupportErrorInfo,该接口提供了丰富的错误信息。当然,由于它是一个接口,因此客户端必需专门查询它,检查是否支持它,然后再处理错误。许多客户端并不执行此操作。

.NET Framework 强制采用一种一致的报告和处理错误的方法:异常。异常不能被忽略。此外,异常还将处理错误的代码与实现逻辑的代码分离开来。

桥的作用

正如已经看到的那样,这两个系统存在很大差别。要在这两种模型之间实现 Interop,需要某种“桥”或包装程序。对于 COM Interop,有两种这样的桥。一种是 Runtime Callable Wrapper(运行时可调用包装程序,RCW),它获取 COM 组件,将其包装起来,并且使 .NET 客户端能够使用它。另一种是 COM Callable Wrapper(COM 可调用包装程序,CCW),它包装 .NET 对象以供 COM 客户端使用。

您可能已经注意到上述术语中的“运行时”一词。这些桥或包装程序是由 CLR 在运行时动态创建的。对于 RCW,CLR 将通过包含在已经生成的 Interop 程序集中的元数据来生成它。对于 CCW,不需要 Interop 程序集;COM 类型库的生成完全是可选的。但是,需要在 Windows 注册表中注册程序集,以便 COM 可以调用它(我们将在系列文章中的下一篇中讨论这方面的问题)。

这些包装程序将完全处理并掩盖 COM 和 .NET 之间的转换,并处理前面提到的所有差异:数据的封送处理、对象的生存期问题、错误处理以及其他许多问题。正如您所希望的那样,通过桥,应当能够安全地从一侧到达另一侧,而无需处理细节。使用对象创建语义(在 .NET 中为 new,在 COM 中为 CoCreateInstance)来创建包装程序,可以在任意一侧使用相应的语义,在内部创建实际的对象。然后只需调用该包装程序,而包装程序将进一步调用实际的对象。

在较高的层次上,它如图 1 所示:

图 1:使用对象创建语义创建包装程序并调用它们

包装程序取决于类型信息。正如介绍的那样,将 COM 类型数据与 CLR 元数据互换需要某种转换过程或工具。.NET Framework 软件开发工具包 (SDK) 提供了这些工具,我们将对它们进行简短的介绍。

我们已经讨论了桥的总体思想,下面将详细介绍 Runtime Callable Wrapper (RCW)。

Runtime Callable Wrapper (RCW)

.NET 客户端从不与 COM 对象直接联系,而是由托管代码与调用 Runtime Callable Wrapper (RCW) 的包装程序联系。RCW 是由 CLR 根据 Interop 程序集中包含的元数据信息在运行时动态创建的代理。对于 .NET 客户端,RCW 看上去与其他任何 CLR 对象一样。同时,RCW 还作为对 .NET 客户端和 COM 对象之间的调用进行封送处理的代理。不管每个 COM 对象具有多少托管引用,都只有一个 RCW。RCW 的工作是维护 COM 对象标识,它通过以下方式来完成此工作:内部调用 IUnknown->QueryInterface() 并缓存接口指针,在适当的时间调用 AddRefRelease

RCW 可以执行以下功能:

  • 调用封送方法
  • 代理 COM 接口
  • 保留对象标识
  • 维护 COM 对象生存期
  • 使用默认的 COM 接口,例如 IUnknownIDispatch

该过程如图 2 所示。

图 2:RCW 过程

COM Callable Wrapper (CCW)

COM Callable Wrapper (CCW) 在另一个方向上扮演类似的角色。它也是作为桥或代理,但用于当 COM 客户端想要联系 .NET 对象时。CCW 的主要工作是将来自 COM 客户端的调用转发给 .NET 对象,这些客户端处在这样一种假象下,即,它们是在与另一个 COM 对象联系。根据这种情况,CCW 实现了标准的 COM 接口,例如 IUnknownIDispatch 以及其他许多接口。某种 .NET 类型的多个 COM 客户端可以共享一个 CCW。

CCW 可以执行以下功能:

  • 将 COM 数据类型转换为相应的 CLR 类型(封送处理)
  • 模拟 COM 引用计数
  • 提供封装的标准 COM 接口实现

类型库导入程序 (TLBIMP.EXE)

在给出最后的示例之前,需要简单介绍一下类型库导入程序工具。在我的下一篇文章中,将详细讨论类型库导入程序,但在这里只给出简短介绍。

正如前面所述,CLR 无法用 COM 类型信息做任何事情。CLR 要求类型信息采用 CLR 元数据的形式存储在程序集中。显然,我们需要某种机制来读取 COM 类型信息并将其转换为程序集中的 CLR 元数据。这些程序集(称为 Interop 程序集)可以通过三种不同的方式创建。

第一种方式是使用 Visual Studio .NET 中的“添加 COM 引用”向导。我发现这种方式对于 Interop 具有很大的限制,因为选项中没有提供任何灵活性。由于此原因以及已经有许多 MSDN 文档论述了如何使用此向导,我将不会在本系列文章中进一步讨论这方面的内容。第二种方式是使用类型库导入程序工具 (TBLIMP.EXE)。最后一种方式是使用 System.Runtime.InteropServices.TypeLibConverter 类进行编程。前两种方式实际上是调用此类来完成各自的工作。这里,我们将着重介绍 TLBIMP 工具。

TLBIMP 是一个命令行工具,.NET Framework SDK(英文)和 Visual Studio .NET 中提供了该工具。它读取 COM 类型信息文件(通常为 .tlb、*.dll 或 *.exe 文件)并将其转换为 Interop 程序集中的 CLR 类型。此工具具有一整套选项,我们将在下一篇文章中深入讨论。现在,只介绍其中的一个重要选项。您当然可以按照最简单的形式来使用 TLBIMP,即,仅指定要转换的 COM 文件的名称。但遗憾的是,如果这样做,TLBIMP 将用 Interop 程序集覆盖该特定文件且不会发出警告。为避免出现这种情况,您可以指定 /out 选项。这使您可以指定所需的输出文件的名称。您的公司可能对此有特定的标准,但是我所喜欢采用的一个约定是在文件名前面加上“Interop.”。这样,“foo.dll”将变为“Interop.foo.dll”。采用此约定,我们的 TLBIMP 的最简单形式将变为:

TLBIMP foo.dll /out:Interop.foo.dll

现在,让我们继续考虑一个非常简单的示例。

求平方示例

作为第一个示例,我选择了实现一个简单的 COM 组件,它具有一个接口 IMSDNComServer 和一个方法 SquareIt。您可以下载示例代码。请注意,为使示例简单,示例代码没有执行任何形式的错误检查。在您开发的代码中,显然您会希望这样做。这种令人惊奇的方法将获取一个双精度数作为输入,并返回该数值的平方。我使用了 Visual C++® 6.0 来实现它。相关 IDL 如下所示:

interface IMSDNComServer : IDispatch
{
   [id(1), helpstring("method SquareIt")] HRESULT SquareIt([in] double dNum, 
      [out] double *dTheSquare);
};

coclass MSDNComServer
{
      [default] interface IMSDNComServer;
};

In the file MSDNComServer.cpp, the SquareIt method looks like the following:

STDMETHODIMP CMSDNComServer::SquareIt(double dNum, double *dTheSquare)
{

   *dTheSquare = dNum * dNum;

   return S_OK;
}

下载内容中还包括一个 Visual Basic 6.0 测试客户端,它将实例化 COM 服务器并调用 SquareIt 方法。该代码非常简单:

     Dim oSquare As MSDNComServer
    Set oSquare = New MSDNComServer
    Dim dIn As Double
    Dim dTheSquare As Double
    
    
    dIn = 3
    Call oSquare.SquareIt(dIn, dTheSquare)
    MsgBox Str(dTheSquare)

当运行该 Visual Basic 6.0 测试应用程序时,我们将获得预期的结果:

图 3:SquareIt 测试应用程序

生成 Interop 程序集

我们的目标是通过我们的 .NET 代码使用此 COM 组件。可以使用 Visual Studio .NET (COM) 添加引用向导(Add Reference | COM Wizard [添加引用 | COM 向导]);在这种非常简单的 COM 组件中,TLBIMP 并没有提供特别的优势,但为了演示,我们将使用 TLBIMP 命令。要使用此命令,请在“程序”菜单中,单击 Visual Studio .NET Tools | Visual Studio .NET 2003 Command Line Prompt(Visual Studio .NET 工具 | Visual Studio .NET 2003 命令行提示)。通过这种方式可以设置正确的路径和环境变量。

可以通过键入以下内容来查看 TLBIMP 提供的众多选项:

C:\Code\MSDN\MSDNManagedSquare>tlbimp /?

将会列出大量选项。在本系列的第二篇文章中,我们将介绍其中的许多选项,以及它们对所生成的 Interop 程序集的影响。在本简单示例中,将只指定输出文件的名称。如前面所述,如果没有指定此选项,TLBIMP 将用生成的程序集覆盖指定的文件。使用的命令行如下所示:

C:\Code\MSDN\MSDNManagedSquare>tlbimp /out:Interop.MSDNCom.dll MSDNCom.dll

此特定 TLBIMP 变换将获取我们的 COM 服务器(位于 MSDNCom.dll 中)并生成一个名为 Interop.MSDNCom.dll 的 Interop 程序集。有一点非常重要,即,基础 COM 组件保持不变;它在任何方式下都不会更改。我们所做的是为它创建另一个“视图”,一个从 CLR 角度看到的视图。

要查看该“托管视图”,我们可以使用 ILDASM.exe。此工具也是 .NET Framework SDK 和 Visual Studio .NET 附带的,它使我们可以查看包含在托管程序集中的元数据和 IL。当自定义类型库导入和导出过程时,您会发现此工具在 Interop 工作中是不可或缺的。通过对我们的 Interop 程序集调用 ILDASM,顶层视图将如下所示:

图 4:ILDASM 提供的托管视图

我们的 Interop 程序集包含两项内容:清单 (MANIFEST) 和称为 Interop.MSDNCom 的命名空间。展开该命名空间,我们发现类型库导入进程已经生成了三项内容!

图 5:类型库导入程序进程的结果

类型库导入程序生成了一个抽象接口 (IMSDNComServer) 和两个类(MSDNComServer 与 MSDNComServerClass)。这里面的原因有些复杂,我的下一篇文章将以此为主题,对导入进程进行详细讨论。此外,注意到这一点就够了:这是由于编程模型存在巨大差异以及组件的版本控制方式所引起的。

要注意的一件事是,Interop 程序集通常情况下包含的都是元数据。方法通常是将调用转发给基础 COM 组件,突出了桥或代理的作用。查看 SquareIt 方法的反汇编就可以说明这一点。

.method public hidebysig newslot virtual 
        instance void  SquareIt([in] float64 dNum,
                                [out] float64& dTheSquare) runtime managed internalcall
{
  .custom instance void 
[mscorlib]System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) = ( 01 00 01 00 00 00 00 00 ) 
  .override Interop.MSDNCom.IMSDNComServer::SquareIt
} // 方法 MSDNComServerClass::SquareIt 的结尾

托管的平方程序

由于已经生成了 Interop 程序集,我们可以通过 .NET 客户端使用它。为了通过示例进行说明,用 C# 生成了一个 Windows 窗体应用程序。假设您知道如何使用 Visual Studio .NET 来创建这样一个项目。代码是作为 MSDNManagedSquare 项目提供的。该项目引用了 Interop 程序集 Interop.MSDNCom。引用完成后,可以用 C# 的 using 语句来使用该程序集的元数据:

using Interop.MSDNCom;

要通过托管代码调用 COM 服务器,只需实例化服务器类并
调用 SquareIt 方法。

private void button1_Click(object sender, System.EventArgs e)
{
double numToSquare = System.Convert.ToDouble(textBox1.Text);
   double squaredNumber;

   MSDNComServerClass squareServer = new MSDNComServerClass();
   squareServer.SquareIt(numToSquare, out squaredNumber);

   textBox2.Text = squaredNumber.ToString();
}

请注意,该代码在实例化对象和调用其方法方面与其他任何 .NET 代码类似。要与 COM 一起工作,不必编写任何特殊代码,也不必使用 GUID、CoCreateInstanceEx 和其他 COM 编程结构。当运行我们的应用程序时,它会像预期的那样工作。在内部,CLR 将动态创建一个 RCW,通过它来调用 SquareIt 方法并返回结果。但是,这对于执行应用程序是完全透明的。

本文小结及下一篇文章的内容

本文介绍了由于 COM 和 .NET 在标识、定位组件、对象的生存期、编程模型、类型信息、版本控制以及错误处理等方面存在巨大差异,因此两者之间需要建立桥。讨论了两种用于 COM 和 .NET 的桥:Runtime Callable Wrapper (RCW) 和 COM Callable Wrapper (CCW)。由于其中的一个重要差异是类型信息(这是因为这两个系统使用了不兼容的类型系统),因而我们探讨了使用类型库导入程序 (TLBIMP) 将 COM 数据类型转换为元数据形式的 CLR 类型。

第一个示例是一个求某个数值的平方的 COM 组件,我们使用 TLBIMP 生成了一个 Interop 程序集,然后通过一个基于 C# 的 Windows 窗体应用程序来使用它。

在下一篇文章中,将进一步讨论 TLBIMP 和类型库导入程序进程,同时详细介绍封送处理进程以及如何使用 System.Runtime.InteropServices 命名空间中的属性和类来构造特定的导入进程。


版权所有:UML软件工程组织