用C#编写一个进程外的COM组件示例代码讲解

 

代码的链接在《C#编写一个进程外的COM组件》,小技巧:如果你要同时看示例代码和讲解的话,可以用浏览器分别打开示例代码和这篇文章,然后使用Windows提供的纵向平铺窗口功能就可同时看两篇文章了。

 

TestComVisibleClass.cs里面定义了我们要发布给COM客户程序的.NET对象,由于我们的.NET进程外组件需要调用几个COM库的API,因此在ComHelperClass里面定义这些API.NET里面的声明方式,正确地声明P/Invoke函数的原型非常困难,要求程序员对Win32COM和托管代码都很熟悉才可以做,所以我写了另外一篇文章《使用Signature Tool自动生成P/Invoke调用Windows APIC#函数声明》简化P/Invoke函数声明的步骤。

 

ComHelperClass类里面CoRegisterClassObject函数的原型比较有意思,注意rclsid参数前面的[MarshalAs(UnmanagedType.LPStruct)]属性,这个属性告诉.NET,在从.NET一端传递rclsid参数值到非托管代码一端时,不要使用默认的列集(Marshal)规则,在P/Invoke里面,NET默认将结构体对象完整复制到非托管的内存里,使用UnmanagedType.LPStruct告诉NETGuid对象的指针传递给非托管函数,就省去了在调用的时候添加ref关键字的麻烦,UnmanagedType.LPStruct会有另外一篇文章来解释它,它有些特别。.NET默认将类实例对象列集成VARIANT拷贝到非托管的内存里,因此第二个参数我用[MarshalAs(UnmanagedType.IUnknown)]通知.NET需要将这个对象实例列集成IUnknow *指针。

 

我们的.NET进程外组件有可能被一些C++编写的客户端调用到,对于C++程序来说,使用前绑定(即通过接口指针调用接口成员函数)的方式会更加方便一些,否则使用延迟绑定技术(通过IDispatch接口调用接口成员函数)的方式C++代码会比较复杂一些。因此将TestComVisibleClass的一些方法和属性提取成一个接口,并且分别给ITestComVisible接口和TestComVisibleClass类分配了一个GUID,在COM世界里,前者就是我们熟知的IID,后者则是CLSID

 

同时,为了方便VB程序使用.NET进程外组件,我还特意给ITestComVisible接口的属性和函数指定了DispID,因为在OLE规范里,DispID0-1等几个特殊值的函数有特殊的意义,这一点我将会在后面的文章里讲到。

 

由于我们不能用mscoree.dll自己提供的类激活策略来在COM中激活我们的.NET对象,mscoree.dll默认提供的激活策略也会在后面的文章里讲到,我们只好显示地提供类厂,并且将我们的类厂在COM运行库里面注册一下。类厂(ClassFactory)的相关接口同样需要定义一个C#形式的原型,一个比较取巧的办法就是使用tlbimp生成一个dll,或者就是看看System.Runtime.InteropServices.ComTypes命名空间里面是否已经有定义好了的类型?IClassFactory最重要的一个函数就是CreateInstance,我们的实现就是看看客户端需要什么样子的接口,如果是IUnknown或者是我们发布的ITestComVisible102行到107行),否则就在109行的位置上抛出一个异常(碰到错误就抛异常的习惯在COM世界里不是一个友好的方式,我的代码为了简单就采取了抛异常的方式,更好的做法是返回错误码,由COM客户端决定如何处理这个错误。)

 

在程序启动的时候,我们将自己实现的类厂注册在COM运行库里面(Program.cs48行到53行),记得保留CoRegisterClassObject返回给我们的注册ID(第53行)。Program.cs里面的35行到44行可选,它们的目的是做一些安全检查,确保只有一些有权限的用户才能调用你的C# Dcom组件,如果你对安全性不关心的话,可以删除它们。在程序退出的时候(Program.cs26行到29行)扫除一些尾巴,释放一些资源。

 

那为什么我们还要一个注册表文件呢?这是因为在前绑定调用方式里,我们需要将指针在进程间传递,比如在客户端C++代码的第21行,实际上CoCreateInstanceEx需要启动我们的.NET进程(也即是进程外COM服务器),调用我们在Program.cs的第53行注册过了的IClassFactory接口来创建.NET对象实例,然后将实例的ITestComVisible接口指针从.NET进程传回C++客户端进程里来。可能有人会说,可以直接将指针的地址到C++客户端进程去嘛?这是不行的,因为Windows操作系统将进程与其它进程独立开来,简单说,一个进程里面的虚拟内存地址在另外一个进程里面可能指向一个垃圾,原理请参看操作系统书籍里面关于虚拟内存的描述。

 

为了在进程间传递ITestComVisible接口指针,COM库需要知道如何列集ITestComVisible指针,一般情况下,程序员需要提供另外一个DLL,这个DLL包含了列集ITestComVisible指针的代码。为什么要提供代码来列集指针的原因是,不同的接口包含不同的函数,例如在客户端C++代码的第25行和第26行,COM库需要列集远程函数调用(RPC),也就是需要一个方法将TestMethodRelease的调用区分开来。一般这个DLL,可以通过用msidl.exe分析IDL文件来生成列集函数的源代码编译生成。然而,这种方法比较麻烦,因此微软提供了OLEAUT32.dll,里面有一个通用的接口列集函数(但是这个函数不能列集所有的接口,可以列集的接口需要遵循一些规则,这个后面有时间再讲),可以通过分析TLB文件来列集指针,因为TLB文件相当于.NET Assembly里面的元数据,oleaut32.dll可以知道你的com组件里面有哪些接口,各个接口的声明又是怎样的,函数的参数类型是什么等等。但是oleaut32.dll需要查询注册表才能知道接口存在的Tlb文件的位置:

1.         因此注册表代码里面的第3行到第16行在注册表里面保存了类型库的存放路径,注意,在COM世界里,tlb文件也是用GUID来唯一标识的,你可以用oleview.exe打开regasm.exe或者tlbexp.exe生成的tlb文件,找到[custom(9903F14C-12CE-4c99-9986-2EE3D7D588A8)…]那一段文字来找到Tlb文件的GUID

2.         注册表代码里面的第18行到第28行,在注册表里面保存了列集ITestComVisible接口的信息,例如ProxyStubClsid指的是采用oleaut32.dll提供的通用接口列集函数,并且保存了该函数所使用tlb文件的信息(第26行到28行)。如果注册表里面没有列集接口的信息,CoCreateInstanceEx函数会返回E_NOINTERFACE(不支持此接口)错误一个让人稀里糊涂的错误代码。

3.         最后注册表代码里面的第30行到第41行向全世界声明(不好意思,不是中国人民从此站起来了的那种声明):我们的.NET对象不需要通过mscoree来在COM端激活,自己可以完成激活操作,因此我们删掉了由regasm.exe插入的InprocServer32键值,而是添加了LocalServer32键值。

 

附,上面注册表代码里面的第3行到第28行可以用RegisterTypeLib函数来完成,而RegisterTypeLib所需要的ptlib参数可以通过LoadTypeLib函数来拿到。
posted @ 2009-02-21 15:22  donjuan  阅读(4601)  评论(0编辑  收藏  举报