本文介绍如何使用C#编写进程外的COM组件。文章中所使用的example为All-In-One Code Framework 的CSExeCOMServer示例:
为了帮助读者更快体会到什么是进程外的COM组件,我建议您从上诉链接中下载最新的release,并按照如下步骤搭一个简单的测试环境:
Step1. 使用Visual Studio 2008打开All-In-One Code Framework(AIO)的solution文件(Visual Studio需要run as admin)。
Step2. 直接build其中的CSExeCOMServer项目。该COM组件的注册将自动完成(我稍后再解释它是如何被注册的)。
Step3. 编写一个简单的客户端以测试该进程外COM组件:打开notepad,并输入如下vbscript代码。将文件保存为client.vbs,并双击运行。此时,您将看到一个HelloWorld的message box被弹出。“HelloWorld”其实是由我们的COM组件的HelloWorld方法返回的一个字符串。
Set obj = CreateObject("CSExeCOMServer.CSSimpleObject")
MsgBox obj.HelloWorld
Step4. 在点击确定按钮关闭message box之前,打开Windows的任务管理器,并找到CSExeCOMServer.exe进程。我们CreateObject("CSExeCOMServer.CSSimpleObject")时创建的COM组件对象就是运行在这个进程里的,而非客户端进程。这也就是所谓的“进程外”COM server。
Step5. 点击确定按钮以关闭message box。观察Windows任务管理器中的CSExeCOMServer进程。您会发现几秒钟之后,该进程自动退出了。这是因为我们的client端释放了那个COM对象。当COM server中不再含有活着的COM对象时,出于性能考虑,COM server会自动shut down。
简单的测试到此结束。下面我们来看看编写一个C#进程外COM组件的一般方法:
方法一:先用C#编写一个进程内的COM组件(例见AIO/CSDllCOMServer),然后使用DCOMCNFG将这个DLL包装在一个COM+ Application里。
方法二:使用C#编写一个Service Component,并使用regsvcs.exe将其直接注册成一个COM+的组件。(例见AIO/CSServicedComponent)
方法三:仿造ATL out-of-process的template,使用C#编写一个真正意义上的out-of-process COM local server。
方法一和方法二的缺点是,部署人员不得不struggle with 复杂的COM+属性,尤其是其中和安全性相关的设置。如果设置不妥,客户端将抛出类似于permission denied的错误。但好处也是很明显的:你可以享受到COM+ transaction,isolation等特点。
方法三的缺点是,它需要P/Invoke CoRegisterClassObject API,而微软在这篇MSDN文档中已明确指出P/Invoke CoRegisterClassObject是不受support的。
在AIO/CSExeCOMServer的ReadMe.txt中,对方法三的具体操作步骤已有详细的说明。其中值得注意的几点是:
1. Out-of-proc .NET COM 组件的注册
Regasm.exe只适用于进程内.NET COM组件的注册。为了注册进程外.NET COM组件,我们需要customize一下Regasm的注册逻辑,以改变其中的部分注册表键值。.NET通过ComRegisterFunctionAttribute和ComUnregisterFunctionAttribute提供了修改Regasm注册逻辑的机会。被这两个属性修饰的方法,将在Regasm完成对ComVisible type默认的注册行为之后被call到。在CSExeCOMServer中,我将HKCR"CLSID"<CLSID of CSSimpleObject>"InprocServer32 key替换成了LocalServer32 key。同时这个key的default value被设置成了CSExeCOMServer.exe的文件路径。
2. CSExeCOMServer.exe进程的shutdown
CSExeCOMServer.exe进程的shutdown取决于该COM Server中活着的COM object数量(lock count)。借助于ReferenceCountedObject 这个class,当有一个新的COM object被创建时, lock count自增1。当该COM object被release时(GC/Finalize), lock count自减1。当lock count变为0时(意味着此时没有活的COM 对象了)我们就post WM_QUIT message到COM Server的message loop上以quit这个message loop, 并shutdown COM server。
考虑到GC触发的时机,我使用了一个timer去定时地触发GC,从而及时地Finalize那些已经可以被释放的.NET COM objects。
讨论
在.NET中,如果我们要做RPC,.NET Remoting和WCF都是很不错的选择,另外进程内的COM组件也比进程外的好些很多。为什么还需要用.NET写“进程外”的“COM组件”?
COM有机会优于.NET Remoting和WCF很重要的一点就是,客户端可以使用简单灵活的解释型语言vbscript去编写。另外,使用.NET来写COM,开发人员可以收益与.NET强大的基础类库,轻松写出VC++可能很困难做到的一些事情。
之所以要写进程外而非进程内COM组件,我想到的原因一是进程外的COM直接支持RPC,二是一些COM应用可能需要进程范围的全局变量,比如session。如果写成进程内的COM组件,这些全局变量将常驻客户端进程。而如果写成进程外COM组件,我们只要适时地shut down那个COM进程,就能灵活地控制这些全局变量了。