在 DLL 中加入第二个 COM 类
引言
在前面几篇文章里,我们已经成功脱离ATL写了一个COM组件,并且实现了自动化。今天,我们来加入第二个类,并且为加入第二个类做一些整理工作。
为DLL建立一个Module类
在前面,我们为了使得DllCanUnloadNow能正确工作而放了一个全局变量LONG g_nModuleCount,并且在SampleClass的构造函数和析构函数里对它进行自增和自减。另外还有个ITypeLib,也是全局的。为了将这些零散的东西收集在一起,我们建立一个ComModule类,地位类似MFC的CWinApp,作为这个DLL中的唯一的全局对象。
class ComModule { public: ComModule(HMODULE hModule = nullptr) : m_hModule(hModule), m_nGlobalRefCount(0), m_pTypeLib(nullptr) { TCHAR szModulePath[MAX_PATH] = {}; GetModuleFileName(m_hModule, szModulePath, ARRAYSIZE(szModulePath));
m_strModulePath = szModulePath;
LoadTypeLib(szModulePath, &m_pTypeLib); }
~ComModule() { if (m_pTypeLib != nullptr) { m_pTypeLib->Release(); m_pTypeLib = nullptr; } }
public: ULONG GlobalAddRef() { return (ULONG)InterlockedIncrement(&m_nGlobalRefCount); }
ULONG GlobalRelease() { return (ULONG)InterlockedDecrement(&m_nGlobalRefCount); }
private: HMODULE m_hModule; String m_strModulePath; LONG m_nGlobalRefCount; ITypeLib *m_pTypeLib; }; |
然后,定义一个全局指针g_pComModule:
__declspec(selectany) ComModule *g_pComModule = nullptr; |
要求在DllMain里面new/delete一个ComModule:
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: g_pComModule = new ComModule(hModule); break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: delete g_pComModule; break; default: break; }
return TRUE; } |
再把全局对象计数放到所有COM类的公共基类ComClass里。
template <typename T> class ComClass { public: ComClass() { if (g_pComModule != nullptr) { g_pComModule->GlobalAddRef(); } }
~ComClass() { if (g_pComModule != nullptr) { g_pComModule->GlobalRelease(); } }
// ... }; |
这样,每个COM对象会自动向Module类报告引用计数,这边解决了DllCanUnloadNow的问题。
同时,为了方便建立一个新的DLL,我们把需要导出的那几个函数也都做在ComModule里面,使得DLL那边只需要写下面这些就够了:
STDAPI DllCanUnloadNow() { return xl::g_pComModule->DllCanUnloadNow(); }
STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID *ppv) { return xl::g_pComModule->DllGetClassObject(rclsid, riid, ppv); }
STDAPI DllRegisterServer() { return xl::g_pComModule->DllRegisterServer(); }
STDAPI DllUnregisterServer() { return xl::g_pComModule->DllUnregisterServer(); }
STDAPI DllInstall(BOOL bInstall, _In_opt_ LPCTSTR lpszCmdLine) { return xl::g_pComModule->DllInstall(bInstall, lpszCmdLine); } |
ComModule::DllCanUnloadNow的实现代码为:
HRESULT DllCanUnloadNow() { return m_nGlobalRefCount > 0 ? S_FALSE : S_OK; } |
COM类的全局映射
现在需要解决的是ComModule::DllGetClassObject。在之前的代码里,我们直接写死一个ClassFactory,ClassFactory里面对应写死的SampleClass。现在要假设有好多COM类的情形。
类厂我们可以搞个模版,ClassFactory<T>,T就是对应的COM类,在类厂里面 new T 就可以了。问题是DllGetClassObject只有CLSID,也就是__uuidof(T),没有T本身。从__uuidof(T)得到T,是件不容易的事情,__uuidof(T)是数据不是类型,没法采用类似萃取的方法。
我们的名言是,不会做了,于就抄ATL的……
不知大家有木有发现,ATL生成的COM类的头文件的最后有一句这样子的话:
OBJECT_ENTRY_AUTO(__uuidof(CTheClass), TheClass) |
(惭愧的是,我曾经有一次把它当成垃圾代码删除掉了,导致对象怎么也创建不出来,还为此调试了好久……)
这个宏将分散在各个头文件的类定义搜集在一起,形成内存连续的全局常量,以便我们在DllGetClassObject之中查表。具体的手法可以参考下面两篇文章:
《巧妙的Section — — 剖析ATL OBJECT_MAP的自动建立》
它的手法是挺巧妙,可是太编译器相关了,相关得我不想抄……可是又暂时想不出其他办法,只好抄了……
由于数据里无法存储“类型”,只能存储数据,于是我们为Factory类定义Factory的Factory:
template <typename T> class ClassFactory : public ComClass<ClassFactory<T>>, public IClassFactoryImpl<> { public: static IClassFactory *CreateFactory() { return new ClassFactory; }
// ... }; |
这样,对于每个COM类T,我们只需要存储__uuidof(T)以及ClassFactory<T>::CreateFactory,便可以在DllGetClassObject中查表,由CLSID查到类厂创建函数,从而得到类厂实例。
表结构定义:
typedef IClassFactory *(*ClassFactoryCreator)();
struct ClassEntry { const CLSID *pClsid; ClassFactoryCreator pfnCreator; }; |
然后抄ATL的手法:
#pragma section("XL_COM$__a", read) #pragma section("XL_COM$__m", read) #pragma section("XL_COM$__z", read)
extern "C" { __declspec(selectany) __declspec(allocate("XL_COM$__a")) const ClassEntry *LP_CLASS_BEGIN = nullptr; __declspec(selectany) __declspec(allocate("XL_COM$__z")) const ClassEntry *LP_CLASS_END = nullptr; }
#if !defined(_M_IA64) #pragma comment(linker, "/merge:XL_COM=.rdata") #endif
#if defined(_M_IX86) #define XL_CLASS_MAP_PRAGMA(class) __pragma(comment(linker, "/include:_LP_CLASS_ENTRY_" # class)); #elif defined(_M_IA64) || defined(_M_AMD64) #define XL_CLASS_MAP_PRAGMA(class) __pragma(comment(linker, "/include:LP_CLASS_ENTRY_" # class)); #else #error Unknown Platform. define XL_CLASS_MAP_PRAGMA #endif
#define XL_DECLARE_COM_CLASS(class) \ \ const ClassEntry CLASS_ENTRY_##class = \ { \ &__uuidof(class), \ &ClassFactory<class>::CreateFactory \ }; \ extern "C" __declspec(allocate("XL_COM$__m")) __declspec(selectany) \ const ClassEntry * LP_CLASS_ENTRY_##class = &CLASS_ENTRY_##class; \ XL_CLASS_MAP_PRAGMA(class) \
|
呃……虽然上面给出了两篇文章,还是忍不住亲自讲一下。首先是:
#pragma section("XL_COM$__a", read)
#pragma section("XL_COM$__m", read)
#pragma section("XL_COM$__z", read)
看上去是定义了三个段,实际上最后出现在PE文件里的只有一个段。如果没有后面的merge,用工具查看编译出的文件结构,结果是这样的:
新增的自定义的段为“XL_COM”。
实际上编译器在处理段名的时候,只读取到$符号前面的字符,后面的附加后缀只存在于编译期间。目前知道的用途,便是排序,__a会在__m之前,__m会在__z之前。
一开始我们就在 XL_COM$__a和XL_COM$__z分别保存了一个空指针,之后每次调用宏XL_DECLARE_COM_CLASS,就在XL_COM$__m放一个指向真实ClassEntry的指针。
最后的一句,#pragma comment(linker, "/merge:XL_COM=.rdata")用于把段XL_COM合并到.rdata,并保持原来在XL_COM里头的数据不变。
经过以上一系列处理,我们便将一张COM类表保存在了全局数据中。
__pragma(comment(linker, "/include: ..."))的作用不知道,MSDN解释看不懂,有人知道吗?
最后,ComModule::DllGetClassEntry就可以这样写了:
HRESULT DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID *ppv) { for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry) { if (*ppEntry == nullptr) { continue; }
if (rclsid == *(*ppEntry)->pClsid) { IClassFactory *pClassFactory = (*ppEntry)->pfnCreator(); return pClassFactory->QueryInterface(riid, ppv); } }
return CLASS_E_CLASSNOTAVAILABLE; } |
之所以要判断*ppEntry是否为空,是因为Debug编译的时候,各个指针之间会被插入大量的零数据……ATL也是这么搞的。
这样,我们在每个COM类声明后,也可以像ATL的一样,写一句
XL_DECLARE_COM_CLASS(SampleClass);
就可以让DllGetClassObject找到对应的类厂了。
只是不知道ATL的OBJECT_ENTRY_AUTO(__uuidof(CTheClass), TheClass)为什么需要我们提供两个参数,第一个参数是CLSID,明显它可以根据第二个参数自己去拿到的嘛……
注册与反注册
注册和反注册部分,我们之前也是写死的,现在要换成活的。
我一开始的方案是,从ComModule里的ITypeLib出发,找到每一个coclass,然后注册CLSID,以及注册TypeLib。但是问题是ProgID,原始IDL文件里面并没有ProgID,因此ITypeLib里面也不可能拿到。我觉得ProgID不能牺牲,所以这个方案不行。
ATL的方案是使用RGS文件,然后注册的时候解析RGS文件并写注册表。我比较讨厌RGS文件……原因是文件格式找不到官方说明,很坑爹的是,字符串值的写法“s ‘SomeString’”的“s”和单引号之间必须有个空格!多少个漆黑的夜里,把rgs改了一下,就再也无法正确注册,然后把原始的拷过来,小心翼翼的一个一个修改、检验,才能发现问题所在……
我决定借用上一节的表,把ClassEntry改为:
struct ClassEntry { const CLSID *pClsid; ClassFactoryCreator pfnCreator; LPCTSTR lpszClassDesc; // SampleClass Class LPCTSTR lpszProgID; // COMProvider.SampleClass LPCTSTR lpszVersion; // 1 }; |
其中的lpszProgID为VersionIndependentProgID,带Version的ProgID使用最后一个lpVersion拼到lpszProgID的后面。
这样,注册Class的函数便可写成类似下面这样子:
bool RegisterComClasses(HKEY hRootKey) { for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry) { if (*ppEntry == nullptr) { continue; }
// ... }
return true; } |
同时可以定义其他三个函数:
UnregisterComClasses
RegisterTypeLib
UnregisterTypeLib
代码不贴了。其中TypeLib的信息从ITypeLib中读取。
最后,对外开放的那个两个函数可以简单地实现为:
HRESULT DllRegisterServer() { if (!RegisterTypeLib(HKEY_LOCAL_MACHINE)) { return E_FAIL; }
if (!RegisterComClasses(HKEY_LOCAL_MACHINE)) { return E_FAIL; }
return S_OK; }
HRESULT DllUnregisterServer() { if (!UnregisterComClasses(HKEY_LOCAL_MACHINE)) { return E_FAIL; }
if (!UnregisterTypeLib(HKEY_LOCAL_MACHINE)) { return E_FAIL; }
return S_OK; } |
这里只管CLSID、ProgID、TypeLib,AppID啥的不管了。
单用户注册
现在我们来面对之前一直置之不理的DllInstall。函数原型为:
STDAPI DllInstall(BOOL bInstall, _In_opt_ LPCWSTR pszCmdLine)
MSDN文档页面:
http://msdn.microsoft.com/en-us/library/windows/desktop/bb759846.aspx
第一个参数,bInstall,当我们在调用regsvr32的时候指定参数/u的时候,它是FALSE,不指定/u的时候,它是TRUE。
第二个参数,当我们在调用regsvr32指定/i:XXX的时候,pszCmdLine为字符串XXX。当直接使用/i或者/i:的时候,pszCmdLine为空字符串。
只有使用了/i参数,regsvr32才会调用DllInstall。
如果指定了/i,且未指定/n,注册的时候regsvr32会先调用DllRegisterServer,后调用DllInstall(TRUE, …),反注册的时候regsvr32会先调用DllInstall(FALSE, …),后调用DllRegisterServer。
如果指定了/i且指定了/n,regsvr32仅仅调用DllInstall。
好了,文档解释到这里。ATL默认代码为:
STDAPI DllInstall(BOOL bInstall, _In_opt_ LPCWSTR pszCmdLine) { HRESULT hr = E_FAIL; static const wchar_t szUserSwitch[] = L"user";
if (pszCmdLine != NULL) { if (_wcsnicmp(pszCmdLine, szUserSwitch, _countof(szUserSwitch)) == 0) { ATL::AtlSetPerUserRegistration(true); } }
if (bInstall) { hr = DllRegisterServer(); if (FAILED(hr)) { DllUnregisterServer(); } } else { hr = DllUnregisterServer(); }
return hr; }
|
其中的“ATL::AtlSetPerUserRegistration(true)”据说会把DllRegisterServer和DllUnregisterServer的注册表位置由HKLM重定向到HKCU。也就是说,当使用regsvr32 /i:user的时候,支持注册到当前用户。
既然ATL默认生成的COM DLL支持/i:user参数,我们也要模拟一下,装得正式一点:
HRESULT DllInstall(BOOL bInstall, _In_opt_ LPCTSTR lpszCmdLine) { if (lpszCmdLine == nullptr) { return E_INVALIDARG; }
if (_tcsicmp(lpszCmdLine, _T("User")) == 0) { if (bInstall) { if (!RegisterTypeLib(HKEY_CURRENT_USER)) { return E_FAIL; }
if (!RegisterComClasses(HKEY_CURRENT_USER)) { return E_FAIL; }
return S_OK; } else { if (!UnregisterComClasses(HKEY_CURRENT_USER)) { return E_FAIL; }
if (!UnregisterTypeLib(HKEY_CURRENT_USER)) { return E_FAIL; }
return S_OK; } }
return E_FAIL; } |
好了,到目前为止我们已经支持了所有标准的注册方式了。除了/i:user,其实我们可以在这里做一些扩展,支持一些自定义的注册方式,来脱离注册表依赖,此为后话。
第二个COM类
准备工作做完了,现在着手加入第二个COM类。
且慢,还有一点点荣誉。在上一篇,为了实现自动化,我们在对象类加入了IDispatch的实现代码。但这些代码是机械的、可抄的,因此可以写成一个独立的东西,IDispatchImpl已经被占用了,就叫Dispatcher吧。
现在SampleClass干净了,代码清单为:
SampleClass.h
#include "COMProvider_h.h" #include <xl/Win32/COM/xlDispatcher.h>
class SampleClass : public xl::ComClass<SampleClass>, public xl::Dispatcher<ISampleInterface> { public: STDMETHOD(SampleMethod)();
public: XL_COM_INTERFACE_BEGIN(SampleClass) XL_COM_INTERFACE(ISampleInterface) XL_COM_INTERFACE(IDispatch) XL_COM_INTERFACE_END() };
XL_DECLARE_COM_CLASS(SampleClass, _T("Streamlet COMProvider Sample Class"), _T("Streamlet.COMProvider.SampleClass"), _T("1")); |
SampleClass.cpp
#include "SampleClass.h"
STDMETHODIMP SampleClass::SampleMethod() { MessageBox(NULL, _T("SampleMethod called."), _T("Info"), MB_OK | MB_ICONINFORMATION); return S_OK; } |
挺清晰的吧?
第二个COM类的IDL:
import "oaidl.idl"; import "ocidl.idl";
[ object, uuid(83C783E3-F989-4E0D-BFC5-631273EDFFDA), ] interface ISampleInterface : IDispatch { [id(1)] HRESULT SampleMethod(); }; [ object, uuid(AD6AD24D-0E31-44A6-A2B3-7B180437541D), ] interface ISampleInterface2 : IDispatch { [id(1)] HRESULT SampleMethod2(); };
[ uuid(22935FC2-282E-4727-B40F-E55128EA1072), version(1.0), ] library COMProviderLib { importlib("stdole2.tlb"); [ uuid(0DECBFF5-A8A5-49E8-9962-3D18AAC6088E) ] coclass SampleClass { [default] interface ISampleInterface; }; [ uuid(85431E6A-28C1-483D-A3DE-CEA640899E0E) ] coclass SampleClass2 { [default] interface ISampleInterface2; }; };
import "shobjidl.idl"; |
注意,我这里加“2”并不表示同一ProgID的升级版,我只是为了和第一个区分。
SampleClass2的实现代码:
SampleClass2.h
#include "COMProvider_h.h" #include <xl/Win32/COM/xlDispatcher.h>
class SampleClass2 : public xl::ComClass<SampleClass2>, public xl::Dispatcher<ISampleInterface2> { public: STDMETHOD(SampleMethod2)();
public: XL_COM_INTERFACE_BEGIN(SampleClass2) XL_COM_INTERFACE(ISampleInterface2) XL_COM_INTERFACE(IDispatch) XL_COM_INTERFACE_END() };
XL_DECLARE_COM_CLASS(SampleClass2, _T("Streamlet COMProvider Sample Class 2"), _T("Streamlet.COMProvider.SampleClass2"), _T("1"));
|
SampleClass2.cpp
STDMETHODIMP SampleClass2::SampleMethod2() { MessageBox(NULL, _T("SampleMethod2 called."), _T("Info"), MB_OK | MB_ICONINFORMATION); return S_OK; } |
完毕。相关框架代码见:
http://xllib.codeplex.com/SourceControl/changeset/view/19794#318174
例子见COMProtocol3.rar(http://pan.baidu.com/s/1hqtJX6c)。
我们走到哪里了?
最近发的比较勤,究竟做了些什么事呢?小小的理一下:
首先,在第一篇《裸写一个含内嵌IE控件的窗口》中,练习了一下如何手工写一个COM类,此时整个模块还不是COM组件。
第二篇《学习下 ATL 的继承链处理》则是个小铺垫,揭示了COM类继承处理上的一个小手法。
第三篇《山寨一下ATL的COM_INTERFACE》是比较全面地学习ATL关于单个COM类的实现上的简化技巧。
第四篇《写个含 Windows Media Player 的窗口》是个整理,验证下前一篇实践代码的合理性和可用性。
第五篇《裸写一个进程内 COM 组件》,是对COM DLL的一个整体实践,不再是单个COM类了。
第六篇《让COM组件可被跨语言调用》是实现自动化,在后面它将作为COM组件的基本功能存在。
第七篇,也就是本文,是对COM DLL的一个整理,把共性整合到框架,简化COM类中的代码,让加入第二个、第三个COM类更方便。
经过这几天的练习,攒下了一批框架性代码,借助于它们,已经可以较为方便地写出一个COM DLL了,就如……使用ATL一样方便。换句话说,我们已经实现了ATL……的一小部分。这是造轮子吗?非也!这是学习过程中的自然积累。
前面的有些做法是抄ATL的,这并不可耻,好方法就拿来用嘛。目前的讨论还仅局限于进程内组件,进程外的并未涉及,COM的加载过程也没有涉及。这些以后再慢慢学。到本文为之,算一个小系列吧,故此总结。