裸写一个进程内 COM 组件
引言
前几天山寨了ATL的COM_INTERFACE,了解了一个COM类的如何进行通用的组织。今天再来学习下COM协议,看看如何实现一个COM组件——当然,也是不能用ATL的,不然就学不到什么了。
COM DLL说简单简单,说复杂也很复杂。说简单呢,其实貌似只要导出下面这五个函数就可以了:
DllCanUnloadNow
DllGetClassObject
DllRegisterServer
DllUnregisterServer
DllInstall
(我有点怀疑但不确定DllInstall是不是后来加的,本文中我们先不理它。)
前四个函数中,后两个是注册与反注册,就是写写注册表的事情,简单。前面两个,特别是DllGetClassObject,比较关键。
先不研究这些,我们先按前两天的方法写个COM类吧。
准备一个COM类
首先,我们定义一个接口 ISampleInterface,以及一个类CSampleClass
Interface.h
#include <Unknwn.h>
struct __declspec(uuid("{83C783E3-F989-4E0D-BFC5-631273EDFFDA}")) ISampleInterface : public IUnknown { STDMETHOD(SampleMethod)() PURE; };
class __declspec(uuid("{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}")) SampleClass; |
与前面不同的是,这里两个声明都写上了一个UUID,前一个是接口ID(IID),后一个是类ID(CLSID)。这个文件将是提供给COM的使用者的。
SampleClass.h
class SampleClass : public xl::ComClass<SampleClass>, xl::IUnknownImpl<ISampleInterface> { public: SampleClass(); ~SampleClass();
public: STDMETHOD(SampleMethod)();
public: XL_COM_INTERFACE_BEGIN(SampleClass) XL_COM_INTERFACE(ISampleInterface) XL_COM_INTERFACE_END() }; |
SampleClass.cpp
SampleClass::SampleClass() { InterlockedIncrement(&g_nModuleCount); }
SampleClass::~SampleClass() { InterlockedDecrement(&g_nModuleCount); }
STDMETHODIMP SampleClass::SampleMethod() { MessageBox(NULL, _T("SampleMethod called."), _T("Info"), MB_OK | MB_ICONINFORMATION); return S_OK; }
|
SampleMethod 就简单调用一个MessageBox意思一下。构造函数和析构函数中两行先不看,后面解释。
实现DllCanUnloadNow
这个函数的MSDN文档见:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms690368.aspx
函数原型为:
HRESULT __stdcall DllCanUnloadNow(void);
函数实现的要求是,当此DLL内的所有COM对象都消亡了的时候,返回S_OK;如果还有COM对象存在,就返回S_FALSE。
现在我们有一个COM类SampleClass,它可能被创建一次,然后引用计数加加减减;也可能被创建多次,每个实例的引用计数同样会被加加减减。当引用计数被减为0的时候,对象将消亡(析构函数被调用)。因此,我们在对象的构造和析构的地方埋点就可以了。
定义一个全局变量:
LONG g_nModuleCount = 0;
然后看到刚才的SampleClass的构造函数和析构函数中的灰色代码,就可以存对象的创建/销毁计数了。对于多个COM对象的情况,也可以这么搞。
然后,DllCanUnloadNow的实现就很简单了:
STDAPI DllCanUnloadNow() { return g_nModuleCount > 0 ? S_FALSE : S_OK; } |
实现DllGetClassObject
终于到关键的地方了。以前一直弄不懂这个函数。它的MSDN文档见:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms680760.aspx
函数原型为:
HRESULT __stdcall DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID *ppv);
第一个参数是要创建的对象的CLSID,很明确。第二个参数有点迷惑,MSDN原文是:
A reference to the identifier of the interface that the caller is to use to communicate with the class object. Usually, this is IID_IClassFactory (defined in the OLE headers as the interface identifier for IClassFactory).
“Usually”,它是IID_IClassFactory。我不知道有没有不“Usually”的情况,也不知道这个接口原先的设计意图是什么。如果可能,其实完全可以绕开类厂机制,直接用想要使用的那个接口的IID,貌似整套机制也能运转……有木有达人解释下它的渊源?
不过呢,目前我就把它当作IID_IClassFactory,其他一律不支持。
我们先要实现一个“类厂”——一个继承于IClassFactory的COM类:
ClassFactory.h
class ClassFactory : public xl::ComClass<ClassFactory>, public xl::IClassFactoryImpl<> { public: ClassFactory(REFCLSID rclsid); ~ClassFactory();
public: STDMETHOD(CreateInstance)(_In_opt_ IUnknown *pUnkOuter, _In_ REFIID riid, _COM_Outptr_ void **ppvObject);
public: XL_COM_INTERFACE_BEGIN(ClassFactory) XL_COM_INTERFACE(IClassFactory) XL_COM_INTERFACE_END()
private: CLSID m_clsid; }; |
IClassFactory有两个方法,CreateInstance和LockServer。我们只实现前者,它用于创建一个对象,第一个参数不理它,第二个参数是IID,第三个参数用于输出。CLSID由构造函数传入,保存在m_clsid中——类厂是与COM类一一对应的。
ClassFactory.cpp
ClassFactory::ClassFactory(REFCLSID rclsid) { m_clsid = rclsid; }
ClassFactory::~ClassFactory() {
}
STDMETHODIMP ClassFactory::CreateInstance(_In_opt_ IUnknown *pUnkOuter, _In_ REFIID riid, _COM_Outptr_ void **ppvObject) { if (riid == __uuidof(ISampleInterface) && m_clsid == __uuidof(SampleClass)) { ISampleInterface *p = new SampleClass; p->QueryInterface(riid, ppvObject);
return S_OK; }
return CLASS_E_CLASSNOTAVAILABLE; } |
这里只对IID为__uuidof(ISampleInterface)且CLSID为__uuidof(SampleClass))的情况作了响应,创建一个SampleClass对象。注意到创建对象之后有一次QueryInterface,在这里面会做一次AddRef操作,因此引用计数此时为1。其实AddRef可以不出现在代码中,需要的时候就用QueryInterface代替。
这些都准备好了,最后来实现DllGetClassObject:
STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID *ppv) { if (riid == __uuidof(IClassFactory) && rclsid == __uuidof(SampleClass)) { IClassFactory *p = new ClassFactory(rclsid); p->QueryInterface(riid, ppv);
return S_OK; }
return CLASS_E_CLASSNOTAVAILABLE; }
|
实现DllRegisterServer和DllUnregisterServer
这个我就直接贴代码了:
STDAPI DllRegisterServer(void) { TCHAR szModulePath[MAX_PATH] = {}; GetModuleFileName(g_hModule, szModulePath, ARRAYSIZE(szModulePath));
xl::Registry::SetString(HKEY_CLASSES_ROOT, _T("CLSID\\{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}\\InprocServer32"), _T(""), szModulePath);
xl::Registry::SetString(HKEY_CLASSES_ROOT, _T("CLSID\\{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}\\ProgID"), _T(""), _T("Streamlet.COMProvider.SampleClass.1"));
xl::Registry::SetString(HKEY_CLASSES_ROOT, _T("Streamlet.COMProvider.SampleClass.1"), _T(""), _T("SampleClass Class"));
xl::Registry::SetString(HKEY_CLASSES_ROOT, _T("Streamlet.COMProvider.SampleClass.1\\CLSID"), _T(""), _T("{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}"));
return S_OK; }
STDAPI DllUnregisterServer(void) { xl::Registry::DeleteKeyRecursion(HKEY_CLASSES_ROOT, _T("CLSID\\{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}"));
xl::Registry::DeleteKeyRecursion(HKEY_CLASSES_ROOT, _T("Streamlet.COMProvider.SampleClass.1"));
return S_OK; } |
注意,我这里并没有写得很全,只是注册几项必要的。至此,我们的COM组件实现完毕。
调用COM组件
编译刚才的DLL,并使用regsvr32注册(注意管理员权限)。然后写一个小程序来调用之:
#include <tchar.h> #include <Objbase.h> #include "../COMProvider/Interface.h"
int _tmain(int argc, TCHAR *argv[]) { HRESULT hr = CoInitialize(NULL);
ISampleInterface *pSampleInterface = nullptr; hr = CoCreateInstance(__uuidof(SampleClass), nullptr, CLSCTX_INPROC_SERVER, __uuidof(ISampleInterface), (LPVOID *)&pSampleInterface);
if (SUCCEEDED(hr)) { pSampleInterface->SampleMethod(); pSampleInterface->Release(); }
CoUninitialize();
return 0; } |
运行结果:
上述例子代码见COMProtocol.rar(http://pan.baidu.com/s/1c0GSI7u),库依赖见http://xllib.codeplex.com/。当然,现在只是简单地迎合了一下CoCreateInstance,还有许多其他事情要做,且听下回分解。