让 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。然后我们针对注册到INICOM写一个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读入的CLSIDDLL路径的对应关系,另一个是存储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,是给CoInitializeCoUninitialize做引用计数的,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读取CLSIDDLL路径的对应关系,代码如下:

 

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.rarhttp://pan.baidu.com/s/1qWJSeS0

 

在本文中,我们脱离了注册表依赖,将COM信息存到了本目录文件,这意味着我们在发布的时候可以事先生成这个文件,或者可以由安装程序生成。至此,我们给出了使用COM DLL作为替代普通DLL的一整套方案。如果能忍受COM接口讨厌的有限的变量类型,以及冗长的行文方式,COM DLL的形式将比普通DLL导出函数更具有优势,可以作为动态链接库实现形式的一般解决方案。

 

等等,有人可能会问:套间模型哪去了呢?

 

——套间模型是什么?这显然不影响我们将COM DLL用于替代普通DLL的整个过程。不需要这个概念。

 

posted on 2012-09-21 00:34  溪流  阅读(18)  评论(0编辑  收藏  举报