启动COM组件的三种机制
这里的COM组件可以是一个进程内服务器(In-Process Server),也可以是一个进程外服务器(Out-Of-Process Server)。一般情况下,我们在使用这些COM组件的时候,只要保证COM是正确注册了,根本不用关心DLL是怎么被load进来的,或者Exe的进程是被怎么创建的,一切都交给系统提供的COM启动机制,而之中用的最多的就是:
STDAPI CoCreateInstance(REFCLSID rclsid,LPUNKNOWN pUnkOuter,DWORD dwClsContext,REFIID riid,LPVOID * ppv);
它会帮你找到需要的DLL/Exe,加载或者启动它们,然后创建你需要的那个COM对象。
一些智能指针,如CComPtr, CComQIPtr, _com_ptr_t(XXXPtr)也提供了诸如CreateInstance的方法,归根结底也是调用到此函数。
其实,启动COM并不只有这么一种方法,为了解决不同的问题,我们至少有三种启动COM组件的方式,下面逐一介绍每种方式的启动机理。
注册表方式(Registry)
这是激活COM最早出现的方式,过程是完全封装在CoCreateInstance函数里面的,其大致过程如下:
- 根据传入的CLSID在注册表中查找其所在DLL或Exe
- 加载DLL或启动Exe
- 创建类厂对象
- 根据类厂创建COM对象
这里,第一步中注册表的信息是在HKEY_CLASSES_ROOT\CLSID下面,根据下图,我们不难得到某个CLSID所对应的组件。
Manifest文件方式(Registry Free)
第一种方式要求COM组件注册在注册表中,所以其是全局共享的。可很多时候,我们不希望这样,比如我们不想操作注册表,因为这涉及到权限问题;比如我不想全局共享,而是让不同的应用程序能够独立的使用它所需要的版本。这就需要用到Registry-Free COM,我在关于registry-free COM的几点局限中介绍了这方面的知识。它和Registry COM最大的区别在于用manifest文件代替了注册表。其实就是把原来放在注册表里的信息放到了manifest文件中,manifest文件可以和你的应用程序在同一目录下,也可以作为resource嵌入到应用程序中。
一般情况下,你要为你的COM组件提供一个manifest,说明这个COM组件的文件名,支持的COM对象,接口和typelib等等, 这叫Assembly Manifest,如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
manifestVersion="1.0">
<assemblyIdentity type="win32" name="ComDLL" version="1.0.0.0" /><file name = "ComDLL.dll">
<comClass clsid="{A1A70915-98B9-429F-A985-353452C664CE}" threadingModel = "Apartment" />
<typelib tlbid="{7B0B4D95-AF97-4D2A-8BA3-2CAABAA22E8A}" version="1.0" helpdir=""/>
</file><comInterfaceExternalProxyStub
name="IDLLTestObject"
iid="{A3F5D53C-3DC6-430B-89D8-5BED54B67718}"
proxyStubClsid32="{00020424-0000-0000-C000-000000000046}"
baseInterface="{00000000-0000-0000-C000-000000000046}"
tlbid = "{7B0B4D95-AF97-4D2A-8BA3-2CAABAA22E8A}" />
</assembly>
而应用程序则要提供一个manifest说明用到了哪些COM组件,这叫Application Manifest。如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity type = "win32" name = "RegistryFreeWay" version = "1.0.0.0" />
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="COMDLL"
version="1.0.0.0" />
</dependentAssembly>
</dependency>
</assembly>
Registry Free COM的启动也是封装在CoCreateInstance函数中,其过程为:
- 根据传入的CLSID在manifest文件中查找其所在的DLL
- 加载DLL
- 创建类厂对象
- 根据类厂创建COM对象
除了第一步,其过程与Registry COM的方式基本一致,但要注意的是,Registry Free COM是不支持进程外服务器的。这是一个很大的遗憾,但是没办法,这是其内在缺陷,因为Registry Free COM是基于Activation Context的,而Activation Context是一种进程内的机制。
程序方式(Customized)
Registry Free COM是解决了进程内服务器(dll)的side-by-side的问题,可是进程外服务器呢(exe)?微软并没有提供这样的机制,但是这并不是说我们只能把进程外服务器作为全局共享的组件注册在注册表中了。既然知道CoCreateInstance的工作机制和exe的路径,我们完全可以自己完成这个过程来创建COM组件。其过程如下:
- 确定进程外服务器Exe的路径,一般我们可以将其放在应用程序同目录下
- 调用CreateProcess启动进程
- 调用CoGetClassObject得到所要创建COM对象对应的类厂对象
- 利用类厂对象创建COM对象
这里CoGetClassObject之所以能成功,是因为在用CreateProcess启动进程时,该进程外服务器已经调用CoRegisterClassObject在class table中保存了所有的类厂对象。当然,为了保证其side-by-side性,CoRegisterClassObject应该以REGCLS_SINGLEUSE的方式注册对象:
REGCLS_SINGLEUSE After an application is connected to a class object with CoGetClassObject, the class object is removed from public view so that no other applications can connect to it.
到了这里,我们不难发现进程内服务器其实也可以用这种方式来启动:
- 确定进程内服务器DLL的路径,一般我们可以将其放在应用程序同目录下。
- 调用LoadLibrary 加载DLL
- 调用GetProcAddress得到该DLL中的DllGetClassObject函数并调用得到类厂对象
- 利用类厂对象创建COM对象
具体代码可以参考Demo:COMActivation\CustomizedWay\CustomizedWay.cpp
总结一下,有些比较常用的COM组件,如MSXML,DirectX等,都是采取全局注册的方式,这样可以有效的节省磁盘和内存,但这些组件必须有非常好的后向兼容性,而这是非常痛苦的一件事情。所以很多时候,我们会采用Registry Free的方式来side-by-side。而对于进程外服务器,因为Registry Free COM不支持,我们可以自己模拟整个COM组件启动的过程来实现side-by-side的目的。这三种方式各有所长,在一些大型的应用的,你可能会同时用到这三种方式。
更新:
最近在实现一个registry free的exe,本来想写篇文章。后来觉得在这里补充一下会比较好:
1. 你可以只需要一个Assembly Manifest, 用来声明相关的文件与接口,类等,并将其link
到目标app中,application manifest其实并不是必须的。
2. COM并不默认支持registry free的exe,所以你需要显式的CreateProcess来启动exe,并且你还需要提供一个manifest文件来描述支持的接口和tlb文件的位置: 因为涉及到两个进程间的通信,用tlb来做接口的marshalling是必须的, 如:
< file name="Server.tlb">
< typelib tlbid="{D98A091D-3A0F-4C3E-B36E-61F62068D488}" version="1.0" helpdir=""/>
< /file>
< comInterfaceExternalProxyStub name="IServer" iid="{4CA6F74F-B927-4694-806B-59E16C3FFA55}" tlbid="{D98A091D-3A0F-4C3E-B36E-61F62068D488}" proxyStubClsid32="{00020424-0000-0000-C000-000000000046}">
< /comInterfaceExternalProxyStub>
注意,你需要将所有的接口都列上,因为在marshalling某个接口的时候,需要通过这里的说明找到对应的tlb文件。 可以利用SDK提供的mt.exe来提取tlb的内容先。
3. 如果是进程内服务器,你只需在Client端嵌入此manifest,但是如果是进程外服务器,Client和Server都需要嵌入此manifest文件,因为双方都需要知道如何来做marshalling。
在测试过程中一定要保证你的exe和tlb都是没有注册的,不然会干扰你本机的结果,等拿到客户那边就比较危险了。
具体参考更新文章:进程外COM组件的一个实例
下载