裸写一个进程外 COM 组件
引言
前面九月份的八篇关于COM的文章,说的都是进程内COM。那时,我们从一个含内嵌IE控件的窗口说起,根据COM协议手工书写了进程内COM组件,并由此积累了一些类似ATL的框架性代码。
今天开始,我们把脚步迈向进程外组件。同样是从最基础的开始,本篇我们将根据进程外COM组件的加载规范手工编写一个EXE,然后用标准的COM调用方法来使用它。之前积累的框架性代码不属于第三方库,所以这里不会避免去使用,相反地,会把一些通用性较强的代码直接扩充到框架中。
本文仅限于常规EXE。NT服务程序暂时不在讨论之列。
命令行规范
进程外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键结构:
第二个,TypeLib,跟CLSID的TypeLib一样。
第一个,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. 使用CoRegisterClassObject向COM库注册类厂。
3. 跑一个消息循环,并自己控制退出。
4. 调用CoRevokeClassObject向COM库注消类厂。(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里完成1、2,在ComModule::ExeUnregisterClassObject里完成4、5。
ComModule::ExeRegisterClassObject
之前为了注册COM类,我们已经保存了对象表,里面有每一个对外暴露的类的CLSID、类厂创建函数指针等。我们找到每一个要注册的类,创建类厂,然后调用CoRegisterClassObject。CoRegisterClassObject将返回一个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; } |
运行结果:
还有一个输出参数值:
一切正常。
但是此时,调用方运行结束后,COM组件还在运行中,没有退出。退出是需要我们手工控制的,下面我们来做这件事。
进程外COM的退出
观察了下ATL的实现,它是在最后一个对象被释放后,触发AtlExeModuleT的Unlock,在其中向主线程发送了一个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; } |
其中CanUnloadNow跟DllCanUnloadNow的判断是一致的,于是乎合并起来:
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 |
运行结果:
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 |
运行结果:
网页中的Javascript
JS代码如下:
<script type='text/javascript'> var objCom = new ActiveXObject("Streamlet.COMProvider.SampleClass.1"); objCom.SampleMethod("Hello! Calling from JavaScript.", 0); </script> |
运行结果:
遗憾的是,从网页调用后,COM组件似乎没法退出。查了下,貌似是JS释放对象机制的问题。