在 DLL 中加入第二个 COM 类

引言

在前面几篇文章里,我们已经成功脱离ATL写了一个COM组件,并且实现了自动化。今天,我们来加入第二个类,并且为加入第二个类做一些整理工作。

 

DLL建立一个Module

在前面,我们为了使得DllCanUnloadNow能正确工作而放了一个全局变量LONG g_nModuleCount,并且在SampleClass的构造函数和析构函数里对它进行自增和自减。另外还有个ITypeLib,也是全局的。为了将这些零散的东西收集在一起,我们建立一个ComModule类,地位类似MFCCWinApp,作为这个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。在之前的代码里,我们直接写死一个ClassFactoryClassFactory里面对应写死的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的自动建立》

The Object Map

 

它的手法是挺巧妙,可是太编译器相关了,相关得我不想抄……可是又暂时想不出其他办法,只好抄了……

 

由于数据里无法存储“类型”,只能存储数据,于是我们为Factory类定义FactoryFactory

 

template <typename T>

class ClassFactory : public ComClass<ClassFactory<T>>,

                     public IClassFactoryImpl<>

{

public:

    static IClassFactory *CreateFactory()

    {

        return new ClassFactory;

    }

 

    // ...

};

 

这样,对于每个COMT,我们只需要存储__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,用工具查看编译出的文件结构,结果是这样的:

 

clip_image001

 

新增的自定义的段为“XL_COM”。

 

实际上编译器在处理段名的时候,只读取到$符号前面的字符,后面的附加后缀只存在于编译期间。目前知道的用途,便是排序,__a会在__m之前,__m会在__z之前。

 

一开始我们就在 XL_COM$__aXL_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找到对应的类厂了。

 

只是不知道ATLOBJECT_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

};

 

其中的lpszProgIDVersionIndependentProgID,带VersionProgID使用最后一个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;

}

 

这里只管CLSIDProgIDTypeLibAppID啥的不管了。

 

单用户注册

现在我们来面对之前一直置之不理的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且指定了/nregsvr32仅仅调用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)”据说会把DllRegisterServerDllUnregisterServer的注册表位置由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.rarhttp://pan.baidu.com/s/1hqtJX6c)。

 

我们走到哪里了?

最近发的比较勤,究竟做了些什么事呢?小小的理一下:

 

首先,在第一篇《裸写一个含内嵌IE控件的窗口》中,练习了一下如何手工写一个COM类,此时整个模块还不是COM组件。

第二篇《学习下 ATL 的继承链处理》则是个小铺垫,揭示了COM类继承处理上的一个小手法。

第三篇《山寨一下ATLCOM_INTERFACE是比较全面地学习ATL关于单个COM类的实现上的简化技巧。

第四篇《写个含 Windows Media Player 的窗口》是个整理,验证下前一篇实践代码的合理性和可用性。

第五篇《裸写一个进程内 COM 组件》,是对COM DLL的一个整体实践,不再是单个COM类了。

第六篇《让COM组件可被跨语言调用》是实现自动化,在后面它将作为COM组件的基本功能存在。

第七篇,也就是本文,是对COM DLL的一个整理,把共性整合到框架,简化COM类中的代码,让加入第二个、第三个COM类更方便。

 

经过这几天的练习,攒下了一批框架性代码,借助于它们,已经可以较为方便地写出一个COM DLL了,就如……使用ATL一样方便。换句话说,我们已经实现了ATL……的一小部分。这是造轮子吗?非也!这是学习过程中的自然积累。

 

前面的有些做法是抄ATL的,这并不可耻,好方法就拿来用嘛。目前的讨论还仅局限于进程内组件,进程外的并未涉及,COM的加载过程也没有涉及。这些以后再慢慢学。到本文为之,算一个小系列吧,故此总结。

 

posted on 2012-09-12 00:23  溪流  阅读(30)  评论(0编辑  收藏  举报