用C#编写COM服务器(答raffles同学的提问)
本文是为了回答raffles同学的提问(c# 程序间数据的相互调用 GetObject(工程名.Application))而写。
是qguai的原创作品,转载请注明出处。
因为raffles的标题不是很贴切,所以另起一题,以便有兴趣的同学查阅。
本文不是COM的教程,有关COM的资料,请查阅MSDN。
为了与raffles的问题相应,客户代码用VB(A)/VBScript写出。
1、COM的客户(client)是如何得到COM对象(COM object)的
COM客户要使用COM对象,就必须跟COM服务器要。
先看一个例子:
'VB Code
set ie = CreateObject("InternetExplorer.Application")
ie.Navigate2 "www.itpub.net"
ie.Visible = true
set ie = Nothing
我们叫CreateObject帮我们要一个ID为"InternetExplorer.Application"的COM对象,然后叫这个COM对象去为我们工作。
CreateObject找谁要呢?当然是IE了(IExplorer.exe)。IExplorer.exe就是一个COM服务器。
COM服务器是要生产COM对象的。在哪儿生产?在工厂里:)(联想一下设计模式中的Factory)
COM是通过接口(interface)与外界打交道的(联想一下OO设计中的“Code to an Interface, Not to an Implementation”原则)
负责生产COM对象的接口就是IClassFactory。其实CreateObject就是找IE要一个工厂,然后叫工厂生产一个对象。
如果是一个In-Process的COM服务器(DLL),就调用DLL输出的DllGetClassObject方法。如果是Out-of-Process的COM服务器(EXE),则这个EXE要负责向系统报告它的类工厂,然后系统转交给客户。
当然,这些都是在WIN32 API层面上的事情,C#开发员先不要着急。
2、用C#编写In-Process的COM服务器(DLL)
严格来说,用C#编写出来的DLL只是一个.NET程序集(assembly),不是COM服务器。
要使一个.NET DLL成为一个COM服务器,要借助中间人mscoree.dll。
mscoree.dll是谁呀?呵呵,就是大名鼎鼎的Microsoft .NET Runtime Execution Engine。
好了,先来看看我们的.NET DLL怎么写(这里假设是.NET 2.0以上,各位同学知道如何使用VS2003或2005或2008):
做一个C#的DLL工程Project1。
namespace qguai
{
public interface qgIApplication
{
string GetText();
}
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
public class qgApplication : qgIApplication
{
public string GetText()
{
Debug.WriteLine("GetText called.");
//你可以用各种方法取得数据
return ".NET class as coclass!";
}
}
}
编译成Project1.DLL。别忘了,要签发成为强名称的程序集(strong-named assembly)。
用RegAsm.exe工具注册我们的类: RegAsm Project1.DLL /codebase
用VB试试:(最简单的方法就是用记事本建一个test.vbs文件,把下面四行代码拷入保存。然后双击这个test.vbs就看到结果了。)
set app = createobject("qguai.qgApplication")
str = app.GetText()
msgbox str
set app = nothing
做In-Process COM服务器,就这么简单。当然,你如果想深入,则大有天地。
raffles同学请注意了,你如果是在IDE里调试,不要按F5。你要把你的调试程序编译成EXE,然后去运行那个EXE。
3、中间人mscoree.dll
为什么一个.NET类,加两个属性,就可以成为一个COM类了呢?
那是因为有了mscoree.dll的帮忙。mscoree.dll就是一个大的COM服务器。
先来看看它输出什么:
C:/WINDOWS/system32>dumpbin /exports mscoree.dll
...
58 18 00011032 DllCanUnloadNow = _DllCanUnloadNow@0
59 19 00003E1F DllGetClassObject = _DllGetClassObject@12
60 1A 00019DCB DllRegisterServer = _DllRegisterServer@0
61 1B 00019E61 DllUnregisterServer = _DllUnregisterServer@0
...
看到DllGetClassObject了没有?当我们set app = createobject("qguai.qgApplication")的时候,就是这个DllGetClassObject给我们提供类工厂。
理论上说,只要一个DLL输出DllCanUnloadNow和DllGetClassObject,就算是一个COM服务器了。
当然了,你不能光挂职不干活,对吧?人家要工厂的时候,你要有工厂才行。人家要产品的时候,你要能生产才行。
那么mscoree.dll提供的工厂怎么知道生产出我们要的"qguai.qgApplication"?
其实,当你用RegAsm.exe注册我们的.NET类的时候,相关信息就被写在注册表里面了。
请看:
[HKEY_CLASSES_ROOT/qguai.qgApplication]
@="qguai.qgApplication"
[HKEY_CLASSES_ROOT/qguai.qgApplication/CLSID]
@="{55345299-3217-3EA7-8FA3-84A1D5D6C642}"
[HKEY_CLASSES_ROOT/CLSID/{55345299-3217-3EA7-8FA3-84A1D5D6C642}]
@="qguai.qgApplication"
[HKEY_CLASSES_ROOT/CLSID/{55345299-3217-3EA7-8FA3-84A1D5D6C642}/InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="qguai.qgApplication"
"Assembly"="qguai, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5f281c1aa508859f"
"RuntimeVersion"="v2.0.50727"
"CodeBase"="file:///C:/Projects/qguai/bin/Debug/Project1.dll"
[HKEY_CLASSES_ROOT/CLSID/{55345299-3217-3EA7-8FA3-84A1D5D6C642}/ProgId]
@="qguai.qgApplication"
[HKEY_CLASSES_ROOT/CLSID/{55345299-3217-3EA7-8FA3-84A1D5D6C642}/Implemented Categories/{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}]
根据注册表信息,mscoree.dll知道去哪找我们的代码。"qguai.qgApplication"只是我们类的程序标识。
真正能唯一标识我们的类的,是这个叫CLSID的东西{55345299-3217-3EA7-8FA3-84A1D5D6C642}。
这一串数字,就是C#中的Guid结构中装的东西。除非你真的需要特定的CLSID(class id),否则还是让RegAsm.exe为你自动生成为好。
RegAsm.exe其实是调用了System.Runtime.InteropServices.RegistrationServices的RegisterAssembly方法来注册的。
你也可以自己写一个C#小程序调用这个方法,而不用RegAsm.exe。
System.Runtime.InteropServices.RegistrationServices这个类在我们编写Out-of-Process COM服务器的时候很有用。
请先预习一下,我们下面就会用到。
4、用C#编写Out-of-Process的COM服务器(EXE)
前面说过,一个DLL可以输出一个DllGetClassObject函数,让系统/客户调用,以便生产COM对象。
但是,一个可独立运行的EXE程序,是不输出DllGetClassObject的。那么它是如何让客户得到对象工厂的呢?
它必须在一开始运行时,调用CoRegisterClassObject,向系统注册它的工厂。而且必须快,在启动的一两分钟内要完成注册。
当程序退出运行时,它必须调用CoRevokeClassObject,向系统注销它的工厂。
幸运的是,我们的C#程序不用去做P/Invoke。我们只须利用RegistrationServices的服务就行了。
RegistrationServices有两个方法,对应于以上两个API函数:RegisterTypeForComClients和UnregisterTypeForComClients。
下面我们就来看看具体怎么做:
为了简便起见,咱们就以一个控制台程序为例。
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Diagnostics;
namespace qguai
{
public interface qgIApplication2
{
string GetText();
}
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
public class qgApplication2 : qgIApplication2
{
public string GetText()
{
Debug.WriteLine("GetText called.");
//你可以用各种方法取得数据
//比如象EXCEL一样,取某一例的数据,取一个文本框的内容,等等
return ".NET class as coclass from COM LocalServer!";
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Starting COM server ...");
//程序启动,咱们马上注册要当作COM对象的类
int cookie = 0;
RegistrationServices rs = new RegistrationServices();
cookie = rs.RegisterTypeForComClients(typeof(qgApplication2), RegistrationClassContext.LocalServer, RegistrationConnectionType.MultipleUse);
Debug.Assert(cookie != 0);
//现在可做你想做的事情了,如果是Windows程序,你就可以显示你的窗口
Console.WriteLine("COM server has started.");
Console.WriteLine("Press Enter to exit.");
Console.ReadLine();
//程序结束了,要注销我们已经注册过的类
if (cookie != 0)
{
rs.UnregisterTypeForComClients(cookie);
}
}
}
}
编译成一个可执行文件ConsoleApplication1.exe(也要strong-named的)。
如何注册这个ConsoleApplication1.exe呢?还是用RegAsm.exe吧;但是注册完之后,要手工修改一下。
因为RegAsm.exe会把它注册成InprocServer32,而我们的EXE应该是LocalServer32。
你可以用下面的结构注册,相应修改路径和CLSID值。
REGEDIT4
[HKEY_CLASSES_ROOT/qguai.qgApplication2]
@="qguai.qgApplication2"
[HKEY_CLASSES_ROOT/qguai.qgApplication2/CLSID]
@="{7690C214-8891-3DF8-BA1E-609A4754F9C1}"
[HKEY_CLASSES_ROOT/CLSID/{7690C214-8891-3DF8-BA1E-609A4754F9C1}]
@="qguai.qgApplication2"
[HKEY_CLASSES_ROOT/CLSID/{7690C214-8891-3DF8-BA1E-609A4754F9C1}/LocalServer32]
@="C:/tmp/ConsoleApplication1/bin/Debug/ConsoleApplication1.exe"
[HKEY_CLASSES_ROOT/CLSID/{7690C214-8891-3DF8-BA1E-609A4754F9C1}/ProgId]
@="qguai.qgApplication2"
测试一下test2.vbs:
set app = createobject("qguai.qgApplication2")
str = app.GetText()
msgbox str
set app = nothing
当你运行test2.vbs时,你会看到ConsoleApplication1.exe被系统启动,你也将得到你想要的COM对象。
当然了,一个完整的COM服务器,还要照顾到很多其它方面。
例如,如何让VB的GetObject得到我们的对象,等等。
有什么想法要交流的,欢迎给qguai@live.ca留言。
5、COM的类型库(Type Library)与.NET反射
将COM签发成具有强名称(Strong Name)的结果程序集(Assembly)
将COM签发成具有强名称(Strong Name)的结果程序集(Assembly) 现有COM文件d:/sample/MyCom.dll,将其添加引用(Add Reference)至.Net应用程序中编译时出现错误:
引用的程序集“MyCom”没有强名称
Referenced assembly ‘MyCom ' does not have a strong name
解决方法:
1. 首先生成强名称公/私密钥对文件(Assembly Key File)
在命令行下键入:
sn –k d:/sample/myAssemblyKey.snk
2. 签发COM成具有强名称的程序集
在命令行下键入:
tlbimp d:/sample/MyCom.dll /keyfile: d:/sample/myAssemblyKey.snk /out:d:/sample/myStrongNameCom.dll
注:
如有公用密钥对文件,则应使用公用文件而无须再自行生成