让 COM 脱离注册表
站长推荐:NSetup一键部署软件
一键式完成美化安装包制作,自动增量升级,数据统计,数字签名。应对各种复杂场景,脚本模块化拆分,常规复杂的脚本代码,图形化设置。无需专业的研发经验,轻松完成项目部署。(www.nsetup.cn)
引言
在上一篇《在 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如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
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://sdrv.ms/SF3Ebm)
在本文中,我们脱离了注册表依赖,将COM信息存到了本目录文件,这意味着我们在发布的时候可以事先生成这个文件,或者可以由安装程序生成。至此,我们给出了使用COM DLL作为替代普通DLL的一整套方案。如果能忍受COM接口讨厌的有限的变量类型,以及冗长的行文方式,COM DLL的形式将比普通DLL导出函数更具有优势,可以作为动态链接库实现形式的一般解决方案。
等等,有人可能会问:套间模型哪去了呢?
——套间模型是什么?这显然不影响我们将COM DLL用于替代普通DLL的整个过程。不需要这个概念。
学习日记,兼职软件设计,软件修改,毕业设计。
本文出自 学习日记,转载时请注明出处及相应链接。
本文永久链接: https://www.softwareace.cn/?p=751