裸写一个进程外 COM 组件

引言

前面九月份的八篇关于COM的文章,说的都是进程内COM。那时,我们从一个含内嵌IE控件的窗口说起,根据COM协议手工书写了进程内COM组件,并由此积累了一些类似ATL的框架性代码。

 

今天开始,我们把脚步迈向进程外组件。同样是从最基础的开始,本篇我们将根据进程外COM组件的加载规范手工编写一个EXE,然后用标准的COM调用方法来使用它。之前积累的框架性代码不属于第三方库,所以这里不会避免去使用,相反地,会把一些通用性较强的代码直接扩充到框架中。

 

本文仅限于常规EXENT服务程序暂时不在讨论之列。

 

命令行规范

进程外COM不像DLL,不需要实现四个导出函数。取而代之地,需要实现一些命令行参数:

 

1.         /RegServer

2.         /UnregServer

3.         /RegServerPerUser

4.         /UnRegServerPerUser

 

我没有找到官方文档,只是从ATL的实现来找到了上述四个参数。从名字来看,很容易理解。前两个相当于进程内组件的 DllRegisterServer DllUnregisterServer,后两个针对当前用户,相当于 DllInstall(“user”)

 

ATL的实现代码来看,命令的前导符号不必是“/”,也可以是“-”。

 

另外,从测试情况来看,当COM库加载进程外组件的时候,会带上参数“-Embedding”,这可以用于区分用户主动运行还是被COM库加载。

 

注册和反注册

为了快速达到运行目的,今天我们只实现/RegServer/UnregServer,后两个先不管了。

 

进程外COM和进程内COM的注册表结构大体一致,“粗看”发现,唯一的区别是,CLSID下的InprocServer32变成了LocalServer32。另外一个关键点是,我们自定义的每一个接口都需要注册到Interface下。Interface键结构:

image

 

第二个,TypeLib,跟CLSIDTypeLib一样。

第一个,ProxyStubClsid32,是要注册该接口的代理存根对象,用于序列化/反序列化参数和返回值。序列化/反序列化在COM中的术语是列集/散集,超不喜欢这名字。我们这里不实现自定义的代理存根,直接写死“{00020424-0000-0000-C000-000000000046}”,用系统的。不过使用这个代理存根有个局限,接口必须符合下列两种情况之一:

 

1.         实现了IDispatch接口,并在IDL中把接口属性标记为dual

2.         只使用VARIANT兼容的数据类型,并在IDL中把接口属性标记为oleautomation

 

另外说一点,ATL /RegServer,仅仅注册 dual 的接口。这点我们这里不学。

 

下面修改以前的ComModule::RegisterTypeLib,增加注册Interface的代码:

 

bool RegisterTypeLib(HKEY hRootKey)

{

    String strPath;

    strPath += _T("Software\\Classes\\TypeLib\\");

    strPath += m_strLibID;

    strPath += _T("\\");

    strPath += m_strLibVersion;

 

    if (!Registry::SetString(hRootKey, strPath, _T(""), m_strLibName))

    {

        return false;

    }

 

    strPath += _T("\\0\\");

#ifdef _WIN64

    strPath += _T("Win64");

#else

    strPath += _T("Win32");

#endif

 

    if (!Registry::SetString(hRootKey, strPath, _T(""), m_strModulePath))

    {

        return false;

    }

 

    for (UINT i = 0; i < m_pTypeLib->GetTypeInfoCount(); ++i)

    {

        TYPEKIND type = TKIND_MAX;

        HRESULT hr = m_pTypeLib->GetTypeInfoType(i, &type);

 

        if (FAILED(hr))

        {

            return false;

        }

 

        if (type != TKIND_INTERFACE && type != TKIND_DISPATCH)

        {

            continue;

        }

 

        ITypeInfo *pTypeInfo = nullptr;

        hr = m_pTypeLib->GetTypeInfo(i, &pTypeInfo);

 

        if (FAILED(hr))

        {

            return false;

        }

 

        XL_ON_BLOCK_EXIT(pTypeInfo, &ITypeInfo::Release);

 

        TYPEATTR *pAttr = nullptr;

        pTypeInfo->GetTypeAttr(&pAttr);

 

        if (FAILED(hr))

        {

            return false;

        }

 

        XL_ON_BLOCK_EXIT(pTypeInfo, &ITypeInfo::ReleaseTypeAttr, pAttr);

 

        TCHAR szInterfaceID[40] = {};

        StringFromGUID2(pAttr->guid, szInterfaceID, ARRAYSIZE(szInterfaceID));

 

        String strInterfacePath;

        strInterfacePath += _T("Software\\Classes\\Interface\\");

        strInterfacePath += szInterfaceID;

 

        if (!Registry::SetString(hRootKey, strInterfacePath + _T("\\ProxyStubClsid32"), _T(""), _T("{00020424-0000-0000-C000-000000000046}")))

        {

            return false;

        }

 

        if (!Registry::SetString(hRootKey, strInterfacePath + _T("\\TypeLib"), _T(""), m_strLibID.GetAddress()))

        {

            return false;

        }

 

        if (!Registry::SetString(hRootKey, strInterfacePath + _T("\\TypeLib"), _T("Version"), m_strLibVersion.GetAddress()))

        {

            return false;

        }

    }

 

    return true;

}

 

反注册相应地增加删除Interface的代码:

 

bool UnregisterTypeLib(HKEY hRootKey)

{

    for (UINT i = 0; i < m_pTypeLib->GetTypeInfoCount(); ++i)

    {

        TYPEKIND type = TKIND_MAX;

        HRESULT hr = m_pTypeLib->GetTypeInfoType(i, &type);

 

        if (FAILED(hr))

        {

            return false;

        }

 

        if (type != TKIND_INTERFACE && type != TKIND_DISPATCH)

        {

            continue;

        }

 

        ITypeInfo *pTypeInfo = nullptr;

        hr = m_pTypeLib->GetTypeInfo(i, &pTypeInfo);

 

        if (FAILED(hr))

        {

            return false;

        }

 

        XL_ON_BLOCK_EXIT(pTypeInfo, &ITypeInfo::Release);

 

        TYPEATTR *pAttr = nullptr;

        pTypeInfo->GetTypeAttr(&pAttr);

 

        if (FAILED(hr))

        {

            return false;

        }

 

        XL_ON_BLOCK_EXIT(pTypeInfo, &ITypeInfo::ReleaseTypeAttr, pAttr);

 

        TCHAR szInterfaceID[40] = {};

        StringFromGUID2(pAttr->guid, szInterfaceID, ARRAYSIZE(szInterfaceID));

 

        String strInterfacePath;

        strInterfacePath += _T("Software\\Classes\\Interface\\");

        strInterfacePath += szInterfaceID;

 

        if (!Registry::DeleteKeyRecursion(hRootKey, strInterfacePath))

        {

            return false;

        }

    }

 

    String strPath;

    strPath += _T("Software\\Classes\\TypeLib\\");

    strPath += m_strLibID;

 

    if (!Registry::DeleteKeyRecursion(hRootKey, strPath))

    {

        return false;

    }

 

    return true;       

}

 

再给RegisterComClasses新增个参数:

 

bool RegisterComClasses(HKEY hRootKey, bool bInprocServer = true)

 

相应注册逻辑修改如下:

 

if (bInprocServer)

{

    if (!Registry::SetString(hRootKey, strClassIDPath + _T("\\InprocServer32"), _T(""), m_strModulePath))

    {

        return false;

    }

}

else

{

    if (!Registry::SetString(hRootKey, strClassIDPath + _T("\\LocalServer32"), _T(""), m_strModulePath))

    {

        return false;

    }

}

 

然后,汇总一下,写两个供调用的接口函数:

 

STDMETHODIMP ExeRegisterServer()

{

    if (!RegisterTypeLib(HKEY_LOCAL_MACHINE))

    {

        return E_FAIL;

    }

 

    if (!RegisterComClasses(HKEY_LOCAL_MACHINE, false))

    {

        return E_FAIL;

    }

 

    return S_OK;

}

       

STDMETHODIMP ExeUnregisterServer()

{

    if (!UnregisterComClasses(HKEY_LOCAL_MACHINE))

    {

        return E_FAIL;

    }

 

    if (!UnregisterTypeLib(HKEY_LOCAL_MACHINE))

    {

        return E_FAIL;

    }

 

    return S_OK;

}

 

对于PerUser的情形,只需要把四处HKEY_LOCAL_MACHINE换成HKEY_CURRENT_USER就好了,不过现在我们先不管这些。

 

然后我们转到入口函数WinMain,加上对/RegServer/UnregServer的处理:

 

int APIENTRY _tWinMain(_In_ HINSTANCE     hInstance,

                       _In_opt_ HINSTANCE hPrevInstance,

                       _In_ LPTSTR        lpCmdLine,

                       _In_ int           nCmdShow)

{

    xl::g_pComModule = new xl::ComModule(hInstance, _T("Streamlet COMProvider TypeLib 1.0"));

   

    if (_tcsicmp(lpCmdLine, _T("/RegServer")) == 0 || _tcsicmp(lpCmdLine, _T("-RegServer")) == 0)

    {

        xl::g_pComModule->ExeRegisterServer();

    }

    else if (_tcsicmp(lpCmdLine, _T("/UnregServer")) == 0 || _tcsicmp(lpCmdLine, _T("-UnregServer")) == 0)

    {

        xl::g_pComModule->ExeUnregisterServer();

    }

 

    delete xl::g_pComModule;

 

    return 0;

}

 

 

到目前为止,可以编译程序,把组件注册上了。

 

进程外COM的启动

进程外COM的启动大致有如下几步:

1.         初始化COM库。

2.         使用CoRegisterClassObjectCOM库注册类厂。

3.         跑一个消息循环,并自己控制退出。

4.         调用CoRevokeClassObjectCOM库注消类厂。(Revoke这个名字也不喜欢,一般与Register对应的都是Unregister。)

5.         反初始化COM库。

6.         退出。

 

我们将WinMain改成如下的样子:

 

int APIENTRY _tWinMain(_In_ HINSTANCE     hInstance,

                       _In_opt_ HINSTANCE hPrevInstance,

                       _In_ LPTSTR        lpCmdLine,

                       _In_ int           nCmdShow)

{

    xl::g_pComModule = new xl::ComModule(hInstance, _T("Streamlet COMProvider TypeLib 1.0"));

   

    if (_tcsicmp(lpCmdLine, _T("/RegServer")) == 0 || _tcsicmp(lpCmdLine, _T("-RegServer")) == 0)

    {

        xl::g_pComModule->ExeRegisterServer();

    }

    else if (_tcsicmp(lpCmdLine, _T("/UnregServer")) == 0 || _tcsicmp(lpCmdLine, _T("-UnregServer")) == 0)

    {

        xl::g_pComModule->ExeUnregisterServer();

    }

    else if (_tcsicmp(lpCmdLine, _T("/Embedding")) == 0 || _tcsicmp(lpCmdLine, _T("-Embedding")) == 0)

    {

        HRESULT hr = xl::g_pComModule->ExeRegisterClassObject();

 

        if (SUCCEEDED(hr))

        {

            MSG msg = {};

 

            while (GetMessage(&msg, nullptr, 0, 0))

            {

                TranslateMessage(&msg);

                DispatchMessage(&msg);

            }

        }

 

        xl::g_pComModule->ExeUnregisterClassObject();

    }

 

    delete xl::g_pComModule;

 

    return 0;

}

 

我们将在ComModule::ExeRegisterClassObject里完成12,在ComModule::ExeUnregisterClassObject里完成45

 

ComModule::ExeRegisterClassObject

之前为了注册COM类,我们已经保存了对象表,里面有每一个对外暴露的类的CLSID、类厂创建函数指针等。我们找到每一个要注册的类,创建类厂,然后调用CoRegisterClassObjectCoRegisterClassObject将返回一个DWORD值(Cookie),用于唯一确定所注册的类,在CoRevokeClassObject的时候要用到,所以要保存起来。CoRegisterClassObject内部会将我们传给它的类厂的引用计数加一,我们应该把自己产生的引用计数都释放掉,整个COM组件运行期间,类厂引用计数始终维持在1,就是COM库占用的那个。

 

代码如下:

 

STDMETHODIMP ExeRegisterClassObject()

{

    HRESULT hr = CoInitialize(nullptr);

 

    if (FAILED(hr))

    {

        return hr;

    }

 

    for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)

    {

        if (*ppEntry == nullptr)

        {

            continue;

        }

 

        IClassFactory *pClassFactory = (*ppEntry)->pfnCreator();

 

        if (pClassFactory == nullptr)

        {

            return E_FAIL;

        }

 

        IUnknown *pUnk = nullptr;

        HRESULT hr = pClassFactory->QueryInterface(__uuidof(IUnknown), (LPVOID *)&pUnk);

 

        if (FAILED(hr) || pUnk == nullptr)

        {

            return hr;

        }

 

        DWORD dwRegister = 0;

        hr = CoRegisterClassObject(*(*ppEntry)->pClsid,

                                    pUnk,

                                    CLSCTX_LOCAL_SERVER,

                                    REGCLS_MULTIPLEUSE,

                                    &dwRegister);

        pUnk->Release();

 

        if (FAILED(hr))

        {

            return hr;

        }

 

        m_arrRegClassObjects.PushBack(dwRegister);

    }

 

    return S_OK;

}

 

ComModule::ExeUnregisterClassObject

这个就比较简单了,针对上面保存的Cookie,调用CoRevokeClassObject即可。CoRevokeClassObject会调用类厂的Release来释放COM库占用的那个引用计数。

 

STDMETHODIMP ExeUnregisterClassObject()

{

    for (auto it = m_arrRegClassObjects.Begin(); it != m_arrRegClassObjects.End(); ++it)

    {

        CoRevokeClassObject(*it);

    }

 

    m_arrRegClassObjects.Clear();

 

    CoUninitialize();

 

    return S_OK;

}

 

进程外COM的使用

通过上面几个步骤,我们的进程外COM组件已经可以被使用了。为了检验参数的序列化/反序列化是否正确,我们稍稍地改变下接口ISampleInterface,加些参数:

 

[

    object,

    uuid(83C783E3-F989-4E0D-BFC5-631273EDFFDB),

    dual,

]

interface ISampleInterface : IDispatch

{

    [id(1)] HRESULT SampleMethod([in] BSTR bstrMessage, [out] LONG *pResult);

};

 

并实现如下:

 

STDMETHODIMP SampleClass::SampleMethod(BSTR bstrMessage, LONG *pResult)

{

    MessageBox(NULL, bstrMessage, _T("Info"), MB_OK | MB_ICONINFORMATION);

 

    if (pResult != nullptr)

    {

        *pResult = 12345678;

    }

 

    return S_OK;

}

 

另起一个EXE

 

int _tmain(int argc, TCHAR *argv[])

{

    HRESULT hr = CoInitialize(NULL);

 

    if (FAILED(hr))

    {

        return 0;

    }

   

    ISampleInterface *pSampleInterface = nullptr;

    hr = CoCreateInstance(__uuidof(SampleClass),

                          nullptr,

                          CLSCTX_LOCAL_SERVER,

                          __uuidof(ISampleInterface),

                          (LPVOID *)&pSampleInterface);

 

    if (SUCCEEDED(hr))

    {

        BSTR bstrMessage = SysAllocString(_T("COMProvider!SampleClass::SampleMethod Called From COMUser."));

        LONG nResult = 0;

        pSampleInterface->SampleMethod(bstrMessage, &nResult);

        SysFreeString(bstrMessage);

        pSampleInterface->Release();

    }

 

    CoUninitialize();

 

    return 0;

}

 

运行结果:

image

 

还有一个输出参数值:

image

 

一切正常。

 

但是此时,调用方运行结束后,COM组件还在运行中,没有退出。退出是需要我们手工控制的,下面我们来做这件事。

 

进程外COM的退出

观察了下ATL的实现,它是在最后一个对象被释放后,触发AtlExeModuleTUnlock,在其中向主线程发送了一个WM_QUIT,结束消息循环。

 

我们之前实现进程内组件的时候,也做过DllCanUnloadNow,这里面,有一个对象计数和类厂的锁计数。而由于xl::ComClass在构造的时候就对对象技术进行了加一,所以对象计数包含了类厂的计数,这在进程内组件里没问题,而且也是必须的。因为类厂存在,说明被使用,DLL不该被释放。

 

而在进程外组件中,由于消息循环结束之前,COM库肯定会占用一个类厂引用计数,如果对象计数包含类厂的话,我们就无法判断发WM_QUIT的时机了。因此,我们这里对xl::ComClass的构造函数和加一个参数,指定需不需要加引用计数,然后分别在DLL创建类厂和EXE创建类厂的时候传入不同的值。

 

xl::ComClass 构造析构函数修改如下:

 

ComClass(bool bAddObjRefCount = true) : m_nRefCount(0), m_bAddObjRefCount(bAddObjRefCount)

{

    if (g_pComModule != nullptr && m_bAddObjRefCount)

    {

        g_pComModule->ObjectAddRef();

    }

}

 

~ComClass()

{

    if (g_pComModule != nullptr && m_bAddObjRefCount)

    {

        g_pComModule->ObjectRelease();

    }

}

 

类厂构造函数和创建函数修改如下:

 

static IClassFactory *CreateFactory(bool bAddObjRefCount = true)

{

    return new ClassFactory(bAddObjRefCount);

}

 

ClassFactory(bool bAddObjRefCount = true) :

    ComClass<ClassFactory<T>>(bAddObjRefCount)

{

 

}

 

xl::ComModule::ExeRegisterClassObject中修改如下:

 

STDMETHODIMP ExeRegisterClassObject()

{

    HRESULT hr = CoInitialize(nullptr);

 

    if (FAILED(hr))

    {

        return hr;

    }

 

    for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)

    {

        if (*ppEntry == nullptr)

        {

            continue;

        }

 

        IClassFactory *pClassFactory = (*ppEntry)->pfnCreator(false);

 

        if (pClassFactory == nullptr)

        {

            return E_FAIL;

        }

 

        IUnknown *pUnk = nullptr;

        HRESULT hr = pClassFactory->QueryInterface(__uuidof(IUnknown), (LPVOID *)&pUnk);

 

        if (FAILED(hr) || pUnk == nullptr)

        {

            return hr;

        }

 

        DWORD dwRegister = 0;

        hr = CoRegisterClassObject(*(*ppEntry)->pClsid,

                                    pUnk,

                                    CLSCTX_LOCAL_SERVER,

                                    REGCLS_MULTIPLEUSE,

                                    &dwRegister);

        pUnk->Release();

 

        if (FAILED(hr))

        {

            return hr;

        }

 

        m_arrRegClassObjects.PushBack(dwRegister);

    }

 

    m_dwThreadId = GetCurrentThreadId();

 

    return S_OK;

}

 

注意在最后存了一个Thread ID,这个ID表明当前是处于EXE模式。

 

然后在xl::ComModule的对象引用计数释放、锁计数释放的函数中判断并发送WM_QUIT

 

ULONG STDMETHODCALLTYPE ObjectRelease()

{

    ULONG lResult = (ULONG)InterlockedDecrement(&m_nObjectRefCount);

 

    if (m_dwThreadId != 0 && CanUnloadNow())

    {

        PostThreadMessage(m_dwThreadId, WM_QUIT, 0, 0);

    }

 

    return lResult;

}

 

ULONG STDMETHODCALLTYPE LockRelease()

{

    ULONG lResult = (ULONG)InterlockedDecrement(&m_nLockRefCount);

 

    if (m_dwThreadId != 0 && CanUnloadNow())

    {

        PostThreadMessage(m_dwThreadId, WM_QUIT, 0, 0);

    }

 

    return lResult;

}

 

其中CanUnloadNowDllCanUnloadNow的判断是一致的,于是乎合并起来:

 

bool CanUnloadNow()

{

    if (m_nObjectRefCount > 0 || m_nLockRefCount > 0)

    {

        return false;

    }

 

    return true;

}

 

STDMETHODIMP DllCanUnloadNow()

{

    return CanUnloadNow() ? S_OK : S_FALSE;

}

 

好了,代码实现全部结束。

 

从不同语言调用

WSH+VBScript

和以前差不多,也写个VBS脚本:

Set obj = WScript.CreateObject("Streamlet.COMProvider.SampleClass.1")

obj.SampleMethod "Hello! Calling from VBScript.", 0

 

运行结果:

image

Visual Basic 6

VB6代码:

Private Sub Command1_Click()

 

    Dim obj As Object

    Set obj = CreateObject("Streamlet.COMProvider.SampleClass.1")

    obj.SampleMethod "Hello! Calling from VB6.", 0

    Set obj = Nothing

   

End Sub

 

运行结果:

image

网页中的Javascript

JS代码如下:

<script type='text/javascript'>

    var objCom = new ActiveXObject("Streamlet.COMProvider.SampleClass.1");

    objCom.SampleMethod("Hello! Calling from JavaScript.", 0);

</script>

 

运行结果:

image

 

遗憾的是,从网页调用后,COM组件似乎没法退出。查了下,貌似是JS释放对象机制的问题。

 

 

本文例子代码见:COMProtocol5.rarhttp://pan.baidu.com/s/1dD3ZzUD

posted on 2012-12-02 19:56  溪流  阅读(81)  评论(0编辑  收藏  举报