原创|其它|编辑:郝浩|2006-04-06 09:59:00.000|阅读 1678 次
概述:如果您正在编写托管代码,一定会希望使用现有的 COM 组件,但是您无法直接调用它们。相反,您必须将 COM 组件包装在一个运行时可调用的包装中,作为组件和托管代码之间的代理。虽然 CLR 为此提供了包装类,但有时您需要用自定义对象包装 COM 组件。要获得精确控制资源清理、传递安全性信息、访问 CLR 功能所需要的底层访问能力,办法之一就是用托管 C++ 编写自己的包装类。本文将告诉您怎样实现这一目的。
# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>
本文假设读者熟悉 .NET、C++ 和 COM
下载本文代码:DCOMSuds.exe (296KB)
摘要如果您正在编写托管代码,一定会希望使用现有的 COM 组件,但是您无法直接调用它们。相反,您必须将 COM 组件包装在一个运行时可调用的包装中,作为组件和托管代码之间的代理。虽然 CLR 为此提供了包装类,但有时您需要用自定义对象包装 COM 组件。要获得精确控制资源清理、传递安全性信息、访问 CLR 功能所需要的底层访问能力,办法之一就是用托管 C++ 编写自己的包装类。本文将告诉您怎样实现这一目的。
与现有的 COM/COM+ 对象互操作是开发人员使用托管代码时需要理解的 Microsoft .NET 框架的重要方面。虽然 .NET 框架以包装类的形式提供了丰富的 COM 互操作性支持,但有些时候,尤其是涉及到远程对象时,仍然需要您手工创建互操作代码。在本文中,我将阐述什么时候这种方式是合适的,以及怎样进行。注意,本文中的一些示例代码为了清晰起见进行了缩减。完整的源代码提供了更多在 COM 中进行异常处理和跨单元调用的可取做法。您可以从本文开始的链接中下载完整代码。但是在开始讲述之前,我们先来仔细了解一下公共语言运行时 (CLR) 提供的包装类。这些类使得托管和非托管对象互操作更加容易。
例如,一个托管客户端需要调用 COM 对象 ACOMObject.Test 公开的方法 Foo。托管客户端只需要与运行时可调用的包装 (RCW) 互操作,后者作为 COM 对象的代理负责管理 COM 对象的生命周期和来往于 COM 对象的数据的封送处理。这意味着调用 COM 对象的代码与调用托管对象的代码没有区别,如下所示:
ACOMObject.Test objTest = new ACOMObject(); objTest.Foo();
显然这是不进行数据封送处理的一个基本示例。如果需要更多有关数据封送处理和自定义 COM 包装类的信息,Don Box 在其 2001 年 5 月的 House of COM 专栏中讨论了如何迁移本机代码。
使用包装类看上去非常简单,因此为什么还要考虑在 RCW 之外迁移,以及手工编写自己的包装类呢?如果所讨论的 COM 对象 ACOMObject.Test 位于远程机器,情况会怎么样呢?您该怎么办?
看起来可以使用 System 命名空间中用于远程访问的Activator 类,因此前面的例子应该如下所示:
Type type = Type.GetTypeFromProgId ("ACOMObject.Test", "MachineName"); Object objTest = Activator.CreateInstance (type); objTest.Foo();
您可以看到,这里使用了 DCOM 与远程对象通信。糟糕的是,这个技巧自身存在一些局限。例如,无法在一个调用中请求多个接口(通过分别的往返行程来实例化接口代价很高)。同样,在实例化 COM 对象时不能传递安全性信息。换句话说,我们无法访问 API 函数 CoCreateInstanceEx 提供的所有功能。
现在让我们考虑远程调用 COM+ 组件的情形。为此您可以使用已经配置的 COM 对象或者 ServicedComponent 对象。这两种对象都可以通过宿主在 COM+ 中以使用 COM+ 服务。
为了使用 Activator.CreateInstance 远程实例化 COM+ 组件,您需要将客户端程序集宿主在 COM+ 应用程序中。您还可以通过使用导出的 COM+ 应用程序代理,远程地调用已经配置的对象。在客户端机器上安装了应用程序代理之后,就可以使用嵌入程序集调用远程对象了。这个技巧通常都可以奏效,但是它也有一个问题 — 托管客户端程序集要依赖不属于公共接口的一些类型。这意味着 CLR 将尝试加载依赖程序集,继而又需要将它们安装在客户端机器上。
例如,如果您有一个受服务的组件依赖数据访问程序集,就需要将这个程序集放在所有客户端机器上。您可以使用 ILDASM 工具打开托管客户端程序集并寻找 AssemblyRef 标记,从而查看程序集的依赖项。 AssemblyRef 标记定义了程序集的外部依赖项。您可以在公共语言基础结构规范第二部分(位于公共语言基础结构 (CLI) )中找到更多有关 AssemblyRef 标记的信息。
另一个选择是使用 .NET 远程处理代替 DCOM 来调用 COM+ 组件。您可以使用 ASP.NET 来宿主 COM+ 组件,这样托管客户端可以使用 .NET 远程处理或者 SOAP 与 ASP.NET 通信。如果您需要跨机器传递服务上下文(如事务),应该使用 DCOM 协议。
DCOM 和 .NET 远程处理之间的另一个区别是:.NET 远程处理需要代理进程(如 ASP.NET 或者自定义的套接字侦听器)宿主受服务的组件,而 DCOM 协议本身就受支持。虽然这些区别肯定会随着时间的推移(尤其是随着 WS-Transaction 的出现)而消失,但是 DCOM 协议目前仍然提供了最好的方法。
使用 COM 包装的另一个重要问题是对象生命周期和资源清理。前面已经讲过,COM 包装提取了托管和非托管环境之间的区别。这意味着托管客户端不用担心引用计数,RCW 将为它们负责这一任务。反过来 RCW 也会像其他托管对象那样被垃圾回收。这种非确定性的终止方式意味着 COM 对象的生命周期不是由托管客户端直接控制的,在处理资源密集的 COM 对象时这可能是一个问题。此问题的解决办法是调用 Marshal::ReleaseComObject 方法,它允许托管客户端显式地控制 COM 对象的生命周期。但是这种控制是以复杂性的增加为代价的。
运行时将为每个 COM 对象都严格的创建一个 RCW,无论存在多少个 COM 对象的实例。因此,底层 COM 对象的引用计数总是 1,即使有多个托管客户端与之相连接。托管客户端需要知道这一点,并确保它们不会在堆中留下 RCW 的虚实例。COM 对象释放的顺序也非常重要。
除了刚刚讲过的方法以外,还有另一个办法可以强制底层 COM 对象的释放:使用 System.GC.Collect 显式地调用垃圾回收可以奏效,但这是比较极端的做法,应该小心使用。强制使用垃圾回收可能代价很高,因为它影响了进程的所有应用程序域。
托管 C++ 包装
托管 C++ 是 C++ 语言的一种扩展,它允许开发人员在使用 Visual C++ 时利用 .NET 框架的各种功能。托管 C++ 最强大的功能是它能够使托管和非托管代码混合使用。Visual C++ 编译器通过自动地生成在托管和非托管上下文之间无缝流动所需要的代码,使这种混合更加容易。
因为托管和非托管代码可以共存,所以您可以简单地用托管包装包装一个非托管类。非托管类可以完全访问 Platform SDK API 函数(如 CoCreateInstanceEx),可以用来实例化 COM 对象。托管包装使任何 .NET 框架兼容的语言都可以使用这个功能。图 1 说明了这一概念。非托管 C++ 类使用 COM API 函数实例化 COM 组件,而托管 C++ 类包装非托管 C++ 类,从而使托管客户端能够使用 COM 对象。
图 1 使用托管 C++ 包装类
这个方式有许多优点。首先,您对 COM 对象如何实例化、如何释放可以进行更好的控制,而且在需要时,CoCreateInstanceEx 可以从非托管类调用。这使您可以在一次调用中获得多个接口,并提供适当的安全信息。您还可以通过调用 Release 方法显式地控制 COM 对象的生命周期。而且在特定情况下,您甚至能获得更好的封送处理性能。例如,可以缓存一个要封送处理的数据结构,用它来调用多个托管调用。另一个优点是,与导出的托管客户端程序集不同,您可以控制托管包装程序集的外部依赖项。最后,您甚至可以处理那些不总是遵守 COM 规则而且默认 COM 包装不能很好地进行处理的 COM 对象。您可以看到,托管包装有助于缓解我在本文前面讨论到的一些问题。
在您开始着手构建托管包装类之前,您应该了解到创建它与所带来的一些挑战是密不可分的。首先,您需要使用 #import 指令包含类型库中的信息。类型库的内容将自动地转换为 C++ 类。接下来,您需要编写托管包装类,它带有镜像底层 COM 对象方法的代理方法。代理方法简单的把从托管客户端传入的请求转发给底层 COM 对象。最后,您需要处理托管和非托管数据类型之间的转换。可以看到,这些重复性的步骤是可以自动化的。
自动生成托管包装的工具
我将创建的一个工具,称为 DCOMSuds,能够自动生成托管包装。它并不想成为商业品质的工具,那需要将所有 COM 的惯用法(如连接点、事件和自定义封送处理程序)都考虑进去。工具的当前版本只能寻找双重 COM 接口,而且数据封送处理的支持也只局限于基元类型和字符串。在您理解了创建托管包装的过程以后,能够通过改变所生成的文件添加其他数据类型。
顾名思义,这个工具是受了 Soapsuds 工具的启发,后者能够创建与基于 XML 的 Web 服务通信的运行时程序集。DCOMSuds 在功能上是类似的,只不过它创建的托管程序集与基于 DCOM 的服务器通信。其他区别还有:Soapsuds 工具读取来自 XML 架构的元数据,而 DCOMSuds 读取来自类型库的元数据。COM 服务器的描述可以从类型库文件或者宿主受服务组件的程序集文件读入,如图 2 所示。
图 2 DCOMSuds 工具
虽然 DCOMSuds 工具可以用于与 COM 对象进行本地和远程互操作,但是它特别适合托管客户端远程调用现有的已配置或者未配置 COM 服务器,或者通过 DCOM 协议远程调用受服务组件。
图 3提供了调用此工具的语法。前面提到过,它可以接受类型库或者程序集作为输入。还需要提供要为其生成托管包装程序集的 COM 对象的接口名称。默认情况下,将生成一个程序集。option -gc 可以用来生成托管 C++ 文件。更多细节,参见下载包中的 ReadMe.txt 文件。您生成了托管包装程序集以后,就可以调用来自远程机器的基于 DCOM 的服务器了。为此,将生成的程序集复制到客户端机器,然后将它添加为托管客户端应用程序的引用。在应用程序配置文件中添加如下项,以传递服务器的位置:
您稍候将看到非托管 C++ 类是如何依赖应用程序配置文件在运行时读入服务器位置等信息的。这比导出的 .msi 文件将服务器位置与生成应用程序代理的机器绑定起来更加具有 XCOPY 友好性。最后,确保 COM 对象或者正在调用的受服务组件使用 regsvr32 或者 regasmed 在本地进行注册。
DCOMSuds 的设计
现在我想讲一讲 DCOMSuds 工具。这个工具是用 C# 开发的,包含四个主要的类:Controller、TypeLibReader、CodeGenerator 和 AssemblyGenerator 。这些类都可以在代码下载包里的 eponymous .cs 文件中找到。图 4 列出了它们的方法和属性。让我们分别来看一看。
图 4 DCOMSuds 类、方法和属性
Controller 这个类负责验证输入参数并加载输入类型库。当程序集作为输入提供时,Controller 类使用 ConvertAssemblyToTypeLib 函数将程序集转换为类型库。DCOMSuds 对受服务组件程序集有一个限制 — 它要求使用接口实现 ServicedComponent 类。之所以这样是因为 TypeLibReader 类不支持类接口。类接口是一种 COM 可访问的接口,由 CLR 通过用 ClassInterfaceAttribute 修饰 .NET 类而自动生成。TypeLibReader 的当前版本要求显式地定义接口。
TypeLibReader 这个类在此应用程序中至关重要。它负责使用 System.Runtime.InteropServices 命名空间的 UCOMITypeInfo 接口遍历类型库。UCOMITypeInfo 是 ITypeInfo 接口的托管定义。因为我想用 C#(而不是 C++ )开发这个工具,所以首先来看 UCOMITypeInfo。Matt Pietrek 的文章 “Improve Your Debugging by Generating Symbols from COM Type Libraries” (MSJ ,1999 年 3 月)提供了有关 ITypeInfo 接口和遍历类型库相关步骤的详细介绍。所以,我将跳过关于 UCOMITypeInfo 类似的讨论,因为两者很像。但是,我将指出使用 UCOMITypeInfo 接口时您会遇到的一些挑战。
考虑摘自文件 TypeLibReader.cs 的如下代码段(为了简洁起见我对代码进行了编辑):
IntPtr pElemDesc = FuncDesc.lprgelemdescParam; for ( int cParams = 0; cParams < FuncDesc.cParams; cParams++ ) { ElemDesc = (ELEMDESC) Marshal.PtrToStructure (pElemDesc, typeof(ELEMDESC)); vt = GetValueTypeFromELEMDESC (ElemDesc); m_COMInterfaceDeclaration.AddParameter (vt, GetParamType (vt)); if (cParams != FuncDesc.cParams-1) { m_COMInterfaceDeclaration.AddParameterSeperator(); } pElemDesc = new IntPtr(pElemDesc.ToInt64() +Marshal.SizeOf(ElemDesc)); }
该代码中首先要注意的是 IntPtr 结构的使用。因为 C# 安全模式中没有指针,所以我使用了 IntPtr 结构,它是专门设计用来存放特定于平台的指针的。代码中的变量 pElemDesc 是 IntPtr 类型的,它指向 ELEMDESC 缓冲区的非托管数组。为了将这个数组的单一元素复制到一个托管对象,我使用了 Marshal.PtrToStructure 方法。这个代码另一个有趣之处与 IntPtr 变量的递增有关。这是通过传递 pElemDesc 的 64 位有符号表示形式(表示 ELEMDESC 的总大小),构造一个 IntPtr 的新实例实现的。注意使用 64 位表示形式是有好处的,因为这样它也能在 64 位版本的 Windows 上工作了。
CodeGenerator 这个类负责生成托管 C++ 包装代码的不同元素。您可能熟悉 ICodeGenerator 接口,它提供了根据代码文档对象模型 (CodeDOM) 图生成代码的方式。其中的思想是,CodeDOM 图的实例可以转换为 .NET 兼容的任何编程语言的源代码,只要这个语言实现了 ICodeGenerator 接口。因为 DCOMSuds 要生成的代码是 C++ 的(以及混合模式的),您不能使用 CodeDOM。但是要注意,在这方面 Visual Studio .NET 2003 中使用 CodeDOM 是可能的。我曾经试图根据这个接口为 CodeGenerator 类建模。
还值得一提的是写出 C++ 代码时使用了 IndentedTextWriter 类。这个类对于代码生成尤其有用,因为它提供了控制输出缩进层次的方法。
AssemblyGenerator 这个类负责从生成的头文件 (.h) 和源文件 (.cpp) 中生成一个程序集。CodeDOM 的功能还包括可以用来从 CodeDOM 图和源文件创建程序集的 CodeCompiler 类。糟糕的是,与代码生成功能相似,代码编译功能也不能用来编译 C++ 源文件。因此,必须调用批编译,如下所示:
//Declare and instantiate a new process component. ProcessStartInfo pStartInfo = new ProcessStartInfo(); pStartInfo.FileName = @"C:\Program Files\Microsoft Visual Studio .NET\Common7\IDE\devenv.exe"; pStartInfo.Arguments = @"/rebuild release .\gen\ManagedWrapper.sln"; pStartInfo.CreateNoWindow = true; pStartInfo.WorkingDirectory = Thread.GetDomain().BaseDirectory; pStartInfo.RedirectStandardOutput = true; pStartInfo.RedirectStandardError = true; pStartInfo.UseShellExecute = false; (Process.Start (pStartInfo)).WaitForExit();
代码生成
现在让我们来看生成的托管和非托管 C++ 代码。我从图 5 中所示接口定义语言 (IDL) 文件定义的 COM 对象开始。这里我将定义一个 COM 对象,它有一个名为 CTest 的组件类和 IDispatch 接口 ITest 。ITest 定义了 5 个方法(f1 到 f5),每个都有不同的数据封送处理需求。研究一下与每个参数相关的 IDL 属性。这些属性将决定托管包装类方法的签名。
图 6 显示了自动生成的头文件中的代码。代码定义了名为 ManagedCTest 的托管包装类,它包装了您前面看到的 CTest 组件类。这个类属于 MSDNMagNS 命名空间。ManagedCTest 类还定义了私有成员 m_pUnMngdObject0,它指向名为 UnManagedObject 的有两个参数的模板类。(您一会儿就会看到模板类的实现。)还要注意 CTest 和 ITest 结构是与组件类 (coclass) 和接口的 IDL 定义一一对应的。CTest 和 ITest 作为参数传递给模板类。ManagedCTest 类还为由 ITest 接口定义的每个方法定义了等价的包装方法。关键的区别在于,包装方法是使用公共类型系统 (CTS) 类型定义的。BSTR 数据类型是用 String* 表示的,而 BSTR* 数据类型是作为包装方法中的 StringBuilder* 反射的。
这个代码中要注意的最后一点是 ManagedCTest 类实现了 IDisposable 接口。IDisposable 定义了一个方法 Dispose,可以用来释放已经分配的资源。此方法的实现简单地调用 m_pUnMngdObject0 的 Release 方法。等一下您将看到 Dispose 方法如何允许托管客户端控制 COM 对象的生存。
图 7 显示了自动生成的源文件。它提供了头文件中 CManagedTest 包装方法的定义。要注意的是,数据封送处理是在非托管和托管数据类型之间进行的。Marshal 类提供了许多有助于该转换的函数。方法 ManagedCTest::f1 使用 Marshal::StringToBSTR 分配一个 BSTR 并将 String 的内容复制给它。这个 BSTR 实例被传递给 COM 对象。控制从 COM 方法调用返回后,以前分配的缓冲区需要使用 Marshal::FreeBSTR 释放。方法 f2 有不同的封送处理需求。如图 5 中所示,方法 f2 的参数有一个 [in, out] 属性。这意味着 COM 对象能够改变 BSTR 的内容。因为 String 类是不可变的,我在这里无法使用它。相反,我使用 StringBuilder 类,它代表可变的字符串。
您可以通过调整这些文件,使数据封送处理更加高效或者提供更多数据转换例程。例如,一个结构的封送处理的实例可能要跨越多个调用进行缓存和重用。同样,托管和非托管数据类型之间转换的关键也是 Marshal 类。它提供了一组用来进行实际转换的方法。
图 8显示了 UnManagedObject 模板类的实现。托管 C++ 一个功能非常强大的方面就在于,它允许一般类型与托管类型混合,尽管 .NET 框架目前还不支持参数的多态性。没有模板的支持,实现 UnManagedObject 类将非常困难。请注意 UnManagedObject 类的模板参数是如何与底层 COM 对象的 CLSID和IID 对应的。底层 COM 对象的位置是使用 ConfigurationSettings 类的 AppSettings 属性从配置文件中读入的。其他的代码就是简单地使用 CoCreateInstanceEx/CoCreateInstance API 函数实例化 COM 对象。Release 方法调用底层 COM 对象的 Release 方法。最后,我需要把复制构造函数和赋值运算符隐藏起来。
图 9显示了 DCOMSuds 工具生成的能够使用托管包装程序集的 C# 客户端。创建托管包装类只不过就是实例化 ManagedCTest 对象而已,这继而会导致底层 COM 对象实例化。底层 COM 对象将一直生存至程序退出 using 语句的作用域。到那时,Dispose 方法将自动地被调用。using 语句定义了一个作用域,对象将在作用域结尾被处理。您可以使用 using 语句,因为 ManagedCTest 类实现了 IDisposable 接口。Dispose 方法将使底层 COM 对象释放。这与 COM 包装恰成对比,对于后者,等价的代码需要标记一个对象以备删除。
小结
当 RCW 和 COM 可调用包装 (CCW) 类不够用的时候,托管 C++ 为与 COM 对象互操作提供了功能强大的选择。在本文中我讲到的 DCOMSuds 工具有助于托管 C++ 包装类的自动生成。您可以看到,这些自定义类为您提供了更多控制和灵活性,可以让您更好地应付各种情况,包括需要一些特殊处理的远程调用。
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com