让 COM 脱离注册表
引言
在上一篇《在 DLL 中加入第二个 COM 类》的“单用户注册”一节中,我们曾提到脱离注册表依赖一事,现在我们来把这事儿给办了。
注册
我们在之前支持了“regsvr32 /n /i:user COMProvider.dll”这一注册命令。这一注册命令给了我们一定的扩展余地。从ATL默认的代码来看,对于DllInstall,目前已定义的命令行参数似乎只有user,于是我们可以定义自己的。
本文中,我们将从一个INI文件读入COM的相关信息,同时,也提供注册选项注册到INI文件。注册命令定义为:
regsvr32 /n /i:INI COMProvider.dll
regsvr32 /n /i:INI:FileName.ini COMProvider.dll
其中,第一条命令注册到工作目录的一个默认文件名,第二条命令注册到FileName.ini,可以带路径(相对于工作目录)。
因此,首先改造ComModule::DllInstall如下:
STDMETHODIMP DllInstall(BOOL bInstall, _In_opt_ LPCTSTR lpszCmdLine) { if (lpszCmdLine == nullptr) { return E_INVALIDARG; }
String strCmdLine = lpszCmdLine; String strCmdLineLower = strCmdLine.ToLower();
if (strCmdLineLower == _T("user")) { 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; } }
if (strCmdLineLower == _T("ini") || strCmdLineLower.IndexOf(_T("ini:")) == 0) { LPCTSTR DEFAULT_INI_FILENAME = _T("xlComReg.ini"); String strIniFileName = DEFAULT_INI_FILENAME;
if (strCmdLine.Length() > 4) { strIniFileName = strCmdLine.SubString(4);
if (strIniFileName[strIniFileName.Length() - 1] == _T('\\')) { strIniFileName += DEFAULT_INI_FILENAME; } }
if (bInstall) { if (!RegisterTypeLibToIni(strIniFileName)) { return E_FAIL; }
if (!RegisterComClassesToIni(strIniFileName)) { return E_FAIL; }
return S_OK; } else { if (!UnregisterComClassesFromIni(strIniFileName)) { return E_FAIL; }
if (!UnregisterTypeLibFromIni(strIniFileName)) { return E_FAIL; }
return S_OK; } }
return E_FAIL; } |
默认INI名字定为xlComReg.ini。这里调用了四个函数:
RegisterTypeLibToIni
UnregisterComClassesFromIni
RegisterComClassesToIni
UnregisterTypeLibFromIni
与之前写注册表的四个函数并列。这四个函数的实现比较简单,就是将之前写注册表的那几个函数换成写INI的,就可以了。代码如下:
RegisterTypeLibToIni
bool RegisterTypeLibToIni(const String &strIniFileName) { if (!IniFile::SetValue(strIniFileName, m_strLibID, _T("TypeLib"), m_strLibName)) { return false; }
if (!IniFile::SetValue(strIniFileName, m_strLibID, _T("Version"), m_strLibVersion)) { return false; }
String strModulePath = GetModuleRelativePathToIni(strIniFileName);
#ifdef _WIN64 if (!IniFile::SetValue(strIniFileName, m_strLibID, _T("Win64"), strModulePath)) { return false; } #else if (!IniFile::SetValue(strIniFileName, m_strLibID, _T("Win32"), strModulePath)) { return false; } #endif return true; } |
UnregisterComClassesFromIni
bool UnregisterTypeLibFromIni(const String &strIniFileName) { if (!IniFile::DeleteSection(strIniFileName, m_strLibID)) { return false; }
return true; } |
RegisterComClassesToIni
bool RegisterComClassesToIni(const String &strIniFileName) { for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry) { if (*ppEntry == nullptr) { continue; }
TCHAR szClassID[40] = {}; StringFromGUID2(*(*ppEntry)->pClsid, szClassID, ARRAYSIZE(szClassID));
String strVersionIndependentProgID = (*ppEntry)->lpszProgID; String strProgID = strVersionIndependentProgID + _T(".") + (*ppEntry)->lpszVersion;
if (!IniFile::SetValue(strIniFileName, szClassID, _T("Class"), (*ppEntry)->lpszClassDesc)) { return false; }
String strModulePath = GetModuleRelativePathToIni(strIniFileName);
#ifdef _WIN64 if (!IniFile::SetValue(strIniFileName, szClassID, _T("InprocServer64"), strModulePath)) { return false; } #else if (!IniFile::SetValue(strIniFileName, szClassID, _T("InprocServer32"), strModulePath)) { return false; } #endif
if (!m_strLibID.Empty()) { if (!IniFile::SetValue(strIniFileName, szClassID, _T("TypeLib"), m_strLibID)) { return false; } }
if (!strProgID.Empty()) { if (!IniFile::SetValue(strIniFileName, szClassID, _T("ProgID"), strProgID)) { return false; }
if (!IniFile::SetValue(strIniFileName, strProgID, _T("Class"), (*ppEntry)->lpszClassDesc)) { return false; }
if (!IniFile::SetValue(strIniFileName, strProgID, _T("CLSID"), szClassID)) { return false; } }
if (!strVersionIndependentProgID.Empty()) { if (!IniFile::SetValue(strIniFileName, strVersionIndependentProgID, _T("Class"), (*ppEntry)->lpszClassDesc)) { return false; }
if (!IniFile::SetValue(strIniFileName, strVersionIndependentProgID, _T("CurVer"), strProgID)) { return false; }
if (!IniFile::SetValue(strIniFileName, strVersionIndependentProgID, _T("CLSID"), szClassID)) { return false; } } }
return true; } |
UnregisterTypeLibFromIni
bool UnregisterComClassesFromIni(const String &strIniFileName) { for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry) { if (*ppEntry == nullptr) { continue; }
TCHAR szClassID[40] = {}; StringFromGUID2(*(*ppEntry)->pClsid, szClassID, ARRAYSIZE(szClassID));
String strVersionIndependentProgID = (*ppEntry)->lpszProgID; String strProgID = strVersionIndependentProgID + _T(".") + (*ppEntry)->lpszVersion;
if (!IniFile::DeleteSection(strIniFileName, szClassID)) { return false; }
if (!strProgID.Empty()) { if (!IniFile::DeleteSection(strIniFileName, strProgID)) { return false; } }
if (!strVersionIndependentProgID.Empty()) { if (!IniFile::DeleteSection(strIniFileName, strVersionIndependentProgID)) { return false; } } }
return true; } |
其中DLL路径用的是DLL相对于INI的相对路径,用函数GetModuleRelativePathToIni获取,该函数的实现如下:
String GetModuleRelativePathToIni(const String &strIniFileName) { TCHAR szIniPathAbsolute[MAX_PATH] = {};
if (GetFullPathName(strIniFileName.GetAddress(), ARRAYSIZE(szIniPathAbsolute), szIniPathAbsolute, nullptr) == 0) { return m_strModulePath; }
TCHAR szModuleRelativePath[MAX_PATH] = {};
if (!PathRelativePathTo(szModuleRelativePath, szIniPathAbsolute, 0, m_strModulePath.GetAddress(), 0)) { return m_strModulePath; }
return szModuleRelativePath; }
|
上面代码将一个COM在注册表中的所有信息全部写到了INI。其实这是不必要的,对于C++程序来说,要使用这个COM,可只需要知道CLSID对应到哪个DLL就可以了。因此,上面划线的代码可以去掉不用,不影响后续使用。
好了,运行“regsvr32 /n /i:ini COMProvider.dll”,生成xlComReg.ini,内容如下:
[{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}] Class=Streamlet COMProvider Sample Class InprocServer32=.\COMProvider.dll |
加载
注册好了,该使用了。这就涉及COM的加载过程了。简单的说,我们一般先CoInitialize,然后CoCreateInstance拿到对象去使用,完了之后CoUninitialize使用完毕。现在我们就来模拟这个过程。除了这三个函数以外,我们还将模拟CoGetClassObject以及CoFreeUnusedLibraries。
于是加载器接口定义为:
struct __declspec(uuid("FE52639A-5B41-49B0-9A50-7A1C4FBC83E2")) IComLoader : public IDispatch { virtual HRESULT CoInitialize(_In_opt_ LPVOID pvReserved) PURE; virtual void CoUninitialize() PURE; virtual void CoFreeUnusedLibraries() PURE; virtual HRESULT CoGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv) PURE; virtual HRESULT CoCreateInstance(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv) PURE; }; |
ComLoader本身将作为一个Com类实现,因此我在IComLoader的声明中加上了UUID。然后我们针对注册到INI的COM写一个Loader。
相关数据结构定义如下:
typedef HRESULT (__stdcall *FnDllCanUnloadNow)(); typedef HRESULT (__stdcall *FnDllGetClassObject)(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID *ppv);
struct ComDllModule { String strFileName; HMODULE hModule; FnDllCanUnloadNow fnDllCanUnloadNow; FnDllGetClassObject fnDllGetClassObject;
ComDllModule() : hModule(nullptr), fnDllCanUnloadNow(nullptr), fnDllGetClassObject(nullptr) {
} };
typedef Map<String, String> ClassIDPathMap; typedef Map<String, ComDllModule> PathModuleMap;
|
前面两行定义函数指针,这两个函数是COM DLL导出的。对于每个被加载的DLL,我们将查找这两个函数入口。ComDllModule结构用于保存一个已加载的COM DLL的信息。各分量意义很明白了,不解释。最后两个Map,一个是用于存储从INI读入的CLSID到DLL路径的对应关系,另一个是存储DLL加载后,DLL路径到ComDllModule结构的对应关系。
下面是ComLoaderFromIni的框架性定义:
class ComLoaderFromIni : public ComClass<ComLoaderFromIni>, public Dispatcher<IComLoader> { public: ComLoaderFromIni(const String &strIniFile =_T("xlComReg.ini")) : m_strIniFile(strIniFile), m_lInitializeCount(0) {
}
~ComLoaderFromIni() { CoUninitialize(); }
public: HRESULT CoInitialize(_In_opt_ LPVOID pvReserved) { return S_OK; }
void CoUninitialize() {
}
void CoFreeUnusedLibraries() {
}
HRESULT CoGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv) { return S_OK; }
private: HRESULT FindComDllModule(REFCLSID rclsid, const ComDllModule **ppModule) { return S_OK; }
bool LoadComDll(const String &strFileName) { return true; }
private: String m_strIniFile; LONG m_lInitializeCount; ClassIDPathMap m_mapClassIDToPath; PathModuleMap m_mapPathToModule; CriticalSection m_cs;
public: XL_COM_INTERFACE_BEGIN(ComLoaderFromIni) XL_COM_INTERFACE(IComLoader) XL_COM_INTERFACE(IDispatch) XL_COM_INTERFACE_END() }; |
其中主要函数目前还没实现。成员变量中有个m_lInitializeCount,是给CoInitialize和CoUninitialize做引用计数的,CriticalSection是给两个Map加锁用的。其余变量的意义很明白,也不介绍了。
LoadComDll
首先看最后一个函数,LoadComDll。它用于加载指定的COM DLL,并将模块信息存入Map。实现如下:
bool LoadComDll(const String &strFileName) { XL_SCOPED_CRITICAL_SECTION(m_cs);
HMODULE hModule = LoadLibrary(strFileName.GetAddress());
if (hModule == nullptr) { return false; }
ScopeGuard sgFreeLibrary = MakeGuard(Bind(FreeLibrary, hModule));
FnDllCanUnloadNow fnDllCanUnloadNow = (FnDllCanUnloadNow)GetProcAddress(hModule, "DllCanUnloadNow");
if (fnDllCanUnloadNow == nullptr) { return false; }
FnDllGetClassObject fnDllGetClassObject = (FnDllGetClassObject)GetProcAddress(hModule, "DllGetClassObject");
if (fnDllGetClassObject == nullptr) { return false; }
ComDllModule &module = m_mapPathToModule[strFileName]; module.strFileName = strFileName; module.hModule = hModule; module.fnDllCanUnloadNow = fnDllCanUnloadNow; module.fnDllGetClassObject = fnDllGetClassObject;
sgFreeLibrary.Dismiss();
return true; } |
FindComDllModule
倒数第二个函数,FindComDllModule,定义为从CLSID找到ComDllModule。首先从m_mapClassIDToPath找到路径,再尝试从m_mapPathToModule找到ComModule。如果未找到,那就尝试使用上面的LoadComDll加载它。代码如下:
HRESULT FindComDllModule(REFCLSID rclsid, const ComDllModule **ppModule) { XL_SCOPED_CRITICAL_SECTION(m_cs);
if (ppModule == nullptr) { return E_INVALIDARG; }
*ppModule = nullptr;
TCHAR szClassID[40] = {}; StringFromGUID2(rclsid, szClassID, ARRAYSIZE(szClassID));
auto itPath = m_mapClassIDToPath.Find(szClassID);
if (itPath == m_mapClassIDToPath.End()) { return REGDB_E_CLASSNOTREG; }
auto itModule = m_mapPathToModule.Find(itPath->Value);
if (itModule == m_mapPathToModule.End()) { if (!LoadComDll(itPath->Value)) { return E_FAIL; }
itModule = m_mapPathToModule.Find(itPath->Value); }
if (itModule == m_mapPathToModule.End()) { return E_FAIL; }
*ppModule = &itModule->Value;
return S_OK; } |
CoInitialize
下面按使用流程分别介绍五个标准函数。首先是CoInitialize,它主要就是从INI读取CLSID到DLL路径的对应关系,代码如下:
HRESULT CoInitialize(_In_opt_ LPVOID pvReserved) { XL_SCOPED_CRITICAL_SECTION(m_cs);
if (m_lInitializeCount > 0) { InterlockedIncrement(&m_lInitializeCount); return S_FALSE; }
Array<String> arrSections;
if (!IniFile::EnumSections(m_strIniFile, &arrSections)) { return E_FAIL; }
for (auto it = arrSections.Begin(); it != arrSections.End(); ++it) { String strClass;
if (!IniFile::GetValue(m_strIniFile, *it, _T("Class"), &strClass)) { continue; }
String strPath;
#ifdef _WIN64 if (!IniFile::GetValue(m_strIniFile, *it, _T("InprocServer64"), &strPath)) { continue; } #else if (!IniFile::GetValue(m_strIniFile, *it, _T("InprocServer32"), &strPath)) { continue; } #endif
m_mapClassIDToPath.Insert(*it, strPath); }
InterlockedIncrement(&m_lInitializeCount);
return S_OK; } |
需要留意的就是引用计数处理,这使得多次调用CoInitialze也是安全的。
CoGetClassObject
这个函数用于取得类厂。由于上面已经准备了FindComDllModule,直接调用获取到ComDllModule信息,然后调用COM DLL导出的DllGetClassObject就可以了:
HRESULT CoGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv) { XL_SCOPED_CRITICAL_SECTION(m_cs);
const ComDllModule *pModule = nullptr; HRESULT hr = FindComDllModule(rclsid, &pModule);
if (FAILED(hr)) { return hr; }
return pModule->fnDllGetClassObject(rclsid, riid, ppv); } |
CoCreateInstance
由于上面已经可以拿到类厂了,这里直接调用,获取类厂后调用类厂的CreateInstance创建对象:
HRESULT CoCreateInstance(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv) { IClassFactory *pClassFactory = nullptr; HRESULT hr = CoGetClassObject(rclsid, __uuidof(IClassFactory), (LPVOID *)&pClassFactory);
if (FAILED(hr)) { return hr; }
hr = pClassFactory->CreateInstance(NULL, riid, ppv); pClassFactory->Release();
return hr; } |
CoFreeUnusedLibraries
这个函数用于清理不再使用的COM DLL。实现思路就是遍历已加载的模块信息,调用DllCanUnloadNow,如果DLL返回S_OK,将其卸载:
void CoFreeUnusedLibraries() { XL_SCOPED_CRITICAL_SECTION(m_cs);
for (auto it = m_mapPathToModule.Begin(); it != m_mapPathToModule.End(); ) { if (it->Value.fnDllCanUnloadNow() == S_OK) { FreeLibrary(it->Value.hModule); it = m_mapPathToModule.Delete(it); } else { ++it; } } } |
CoUninitialize
这是最终的卸载函数,卸载所有已加载的DLL,清除所有信息:
void CoUninitialize() { XL_SCOPED_CRITICAL_SECTION(m_cs);
if (m_lInitializeCount == 0) { return; }
InterlockedDecrement(&m_lInitializeCount);;
if (m_lInitializeCount > 0) { return; }
for (auto it = m_mapPathToModule.Begin(); it != m_mapPathToModule.End(); ++it) { FreeLibrary(it->Value.hModule); }
m_mapClassIDToPath.Clear(); m_mapPathToModule.Clear(); } |
需要注意的是,这里也有引用计数的处理,与CoInitialize重的对应。
使用
使用之前,先为刚才的加载器创建一个工厂函数吧(当然,直接使用也没关系):
enum ComLoadType { CLT_FROM_INI, };
inline IComLoader *CreateComLoader(ComLoadType type, const String &strData = _T("xlComReg.ini")) { IComLoader *pLoader = nullptr;
switch (type) { case CLT_FROM_INI: pLoader = new ComLoaderFromIni(strData); pLoader->AddRef(); break; default: break; }
return pLoader; } |
目前只有INI加载器。
使用方式如下:
int _tmain(int argc, TCHAR *argv[]) { xl::IComLoader *pComLoader = xl::CreateComLoader(xl::CLT_FROM_INI); HRESULT hr = pComLoader->CoInitialize(NULL);
ISampleInterface *pSampleInterface = nullptr; hr = pComLoader->CoCreateInstance(__uuidof(SampleClass), __uuidof(ISampleInterface), (LPVOID *)&pSampleInterface);
if (SUCCEEDED(hr)) { pSampleInterface->SampleMethod(); pSampleInterface->Release(); }
pComLoader->Release();
return 0; } |
运行结果:
以上,框架代码见:
http://xllib.codeplex.com/SourceControl/changeset/view/20034#319450
例子代码见COMProtocol4.rar(http://pan.baidu.com/s/1qWJSeS0)
在本文中,我们脱离了注册表依赖,将COM信息存到了本目录文件,这意味着我们在发布的时候可以事先生成这个文件,或者可以由安装程序生成。至此,我们给出了使用COM DLL作为替代普通DLL的一整套方案。如果能忍受COM接口讨厌的有限的变量类型,以及冗长的行文方式,COM DLL的形式将比普通DLL导出函数更具有优势,可以作为动态链接库实现形式的一般解决方案。
等等,有人可能会问:套间模型哪去了呢?
——套间模型是什么?这显然不影响我们将COM DLL用于替代普通DLL的整个过程。不需要这个概念。