上一篇说了怎么设计一个伪COM,现在我们来看看真正的COM库是怎么实现模块化的。
先提一个问题,上一节我们实现的伪COM有没有什么令你不爽的地方?对的,相信很多人对于需要自己加载dll和导出接口极为不爽,微软也很不爽,所以COM库是解决了这个问题的。
1.COM库-加载和导出接口
a).加载
先看看COM库怎么解决加载dll的问题。要加载dll,首先要找到它,上篇文章我们是假定dll在当前调用程序目录中的,但是COM标准要求的是具有位置透明性——不论dll在哪里都能发现和加载它。
COM实现的方法很简单,将dll的位置写入注册表。如下是一个典型的COM注册表,
其中GUID字符串{FFFD-...B52}就是上节说的标明具体组件对象的CLSID,
看看InprocServer32内容如下
InprocServer32意为进程内组件,ThreadingModel意为线程模型,这些之后再讲,现在知道即可。重点关注默认值是该组件dll的全路径,对了,当我们传入CLSID指定导出对应对象的接口时,COM库会帮我们查询注册表找到和加载这个dll的。
那么这个注册表是谁写的呢,答案是dll自己。COM要求dll必须导出注册函数DllUnregisterServer和取消注册函数DllUnregisterServer,他们会完成对应的注册表写入和删除操作。使用命令regsvr32 + dll路径即可调用对应的dll注册接口。
另外,这里ProgID是对应CLSID的一刻可读的组件对象名称,如下
可以使用函数CLSIDFromProgID完成ProgID到CLSID的转换。
b).导出接口
找到了dll,接下来就是导出对应接口,这里不像伪COM那样直接导出对应接口,全过程引用《COM原理与应用》图如下:
简单来说调用关系如下:
客户->CoCreateInstance
COM库->CoGetClassObject->找到dll->DllGetClassObject->得到工厂对象->调用工厂对象的CreateInstance方法创建组件对象
组件dll->实现DllGetClassObject导出函数,返回工厂对象
CoGetClassObject会帮我们完成根据CLSID找到对应DLL的工作,导出对应的接口并不是直接导出组件对象的接口,而是通过一个工厂对象来完成导出。之所以引入工厂对象而不是直接导出,是因为使用工厂对象可以作中间过渡,如判断不同场景下创建不同的组件对象。
DllGetClassObject根据传入的CLSID返回对应的工厂对象,工厂对象再进一步创建组件对象。
c).组件卸载
当我们使用LoadLibrary加载dll时,对应需要使用FreeLibrary完成对应dll卸载。那么在COM库中,dll加载由COM库托管了,怎么才知道何时卸载DLL呢。很简单,DLL导出一个函数DllCanUnloadNow,当COM库检测此函数返回值为TRUE的时候就FreeLibrary,类似java内存回收机制。
2.COM库接口的实现
a).注册和撤销注册DLL
对应两个函数实现如下:
//组件注册函数 STDAPI DllRegisterServer(void) { TCHAR szModule[MAX_PATH]; DWORD dwResult = ::GetModuleFileName(g_hModule, szModule, MAX_PATH); if (0 == dwResult) { return SELFREG_E_CLASS; } return CToolHelper::RegisterServer( CLSID_EasyComPeople, TEXT("EasyCom.Object"), szModule, TEXT("EasyCom Component Description")) ? S_OK : SELFREG_E_CLASS; } //组件取消注册函数 STDAPI DllUnregisterServer(void) { return CToolHelper::UnRegisterServer(CLSID_EasyComPeople, TEXT("EasyCom.Object")) ? S_OK : SELFREG_E_CLASS; }
注册时写入CLSID、ProgID、InprocServer32信息,反注册时删除对应CLSID项即可。
b).导出接口
//组件信息函数 STDAPI DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, LPVOID FAR* ppv) { if (rclsid == CLSID_EasyComPeople) { CPeopleFactory *pFactory = new CPeopleFactory; if (NULL == pFactory) { return E_FAIL; } HRESULT hr = pFactory->QueryInterface(riid, ppv); return hr; } else { return CLASS_E_CLASSNOTAVAILABLE; } }
这里创建返回指定CLSID的组件的工厂对象。
c).卸载组件
对应实现如下:
//组件卸载函数 STDAPI DllCanUnloadNow(void) { if(g_LockNumber == 0 && g_EasyComNumber==0) { return S_OK; } else { return S_FALSE; } }
可以看到这里判断g_LockNumber和g_EasyComNumber均为0时才告诉COM库当前组件dll可卸载了。g_LockNumber是工厂对象的锁,在需要持续用到工厂对象创建组件对象时,可以锁住工厂对象从而不让dll组件卸载;g_EasyComNumber是所有组件对象的引用计数和,当所有组件对象的所有接口都不再被引用时为0,可被卸载。
3.COM对象的实现
a).组件接口定义
// {2F8C8811-1D6D-4e1b-ABD0-686F2641F1C3} _declspec(selectany) GUID CLSID_EasyComPeople = { 0x2f8c8811, 0x1d6d, 0x4e1b, { 0xab, 0xd0, 0x68, 0x6f, 0x26, 0x41, 0xf1, 0xc3 } }; // {F4D72691-1361-4ece-B550-7C753874B880} _declspec(selectany) GUID IID_IAge = { 0xf4d72691, 0x1361, 0x4ece, { 0xb5, 0x50, 0x7c, 0x75, 0x38, 0x74, 0xb8, 0x80 } }; // {FDFCA635-07F6-4ac0-9978-3B7BF1A4840C} _declspec(selectany) GUID IID_IName = { 0xfdfca635, 0x7f6, 0x4ac0, { 0x99, 0x78, 0x3b, 0x7b, 0xf1, 0xa4, 0x84, 0xc } }; class IAge : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE PrintAge(int nAge)=0;//必须为虚函数 }; class IName : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE PrintName(PWCHAR szName)=0; };
这里我们实现一个打印个人信息的组件对象,有两个接口,分别负责打印年龄和打印姓名。
b).组件对象声明
class CPeople: IName, IAge { public: CPeople(void); ~CPeople(); //IUnknown HRESULT STDMETHODCALLTYPE QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject); ULONG STDMETHODCALLTYPE AddRef( void); ULONG STDMETHODCALLTYPE Release( void); //IName HRESULT STDMETHODCALLTYPE PrintName(PWCHAR szName); //IAge HRESULT STDMETHODCALLTYPE PrintAge(int nAge); private: ULONG m_nRef; };
c).组件对象实现
分别为构造函数、析构函数、接口查询、声明周期管理和各个接口的实现。其它的基本和上篇文章一致,不同 的是需要注意的是在构造和析构函数中增加和减少总的组件对象计数。
CPeople::CPeople(void) { g_EasyComNumber++;//组件引用计数 this->m_nRef = 0; } CPeople::~CPeople() { g_EasyComNumber--;//组件引用计数 } //IUnknown HRESULT STDMETHODCALLTYPE CPeople::QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject ) { if (riid == IID_IUnknown) { *ppvObject = (IAge*)this; ((IAge*)this)->AddRef(); } else if (riid == IID_IAge) { *ppvObject = (IAge*)this; ((IAge*)this)->AddRef(); } else if (riid == IID_IName) { *ppvObject = (IName*)this; ((IName*)this)->AddRef(); } else { *ppvObject = NULL; return E_NOINTERFACE; } return S_OK; } ULONG STDMETHODCALLTYPE CPeople::AddRef( void ) { m_nRef++; #ifdef _DEBUG cout << __FUNCTION__ << "\tDebugInfo-refcount:" << m_nRef << endl; #endif return (ULONG)m_nRef; } ULONG STDMETHODCALLTYPE CPeople::Release( void ) { m_nRef--; #ifdef _DEBUG cout << __FUNCTION__ << "\tDebugInfo-refcount:" << m_nRef << endl; #endif if (m_nRef == 0) { #ifdef _DEBUG cout << __FUNCTION__ << "\tRefcount=0=>Delete CPeople" << endl; #endif delete this; return 0; } return (ULONG)m_nRef; } //IName HRESULT STDMETHODCALLTYPE CPeople::PrintName( PWCHAR szName ) { wcout << L"CPeople->IName: My name is " << szName << endl; return S_OK; } //IAge HRESULT STDMETHODCALLTYPE CPeople::PrintAge( int nAge ) { cout << "CPeople->IAge: My age is " << nAge << endl; return S_OK; }
d).工厂对象的声明和实现
声明
class CPeopleFactory : public IClassFactory { public: CPeopleFactory(void); //IUnknown HRESULT STDMETHODCALLTYPE QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject); ULONG STDMETHODCALLTYPE AddRef( void); ULONG STDMETHODCALLTYPE Release( void); //IClassFactory HRESULT STDMETHODCALLTYPE CreateInstance(_In_ IUnknown *pUnkOuter, _In_ REFIID riid, _Out_ void **ppvObject); HRESULT STDMETHODCALLTYPE LockServer(_In_ BOOL fLock); protected: ULONG m_nRef; };
实现
CPeopleFactory::CPeopleFactory(void) { this->m_nRef = 0; } //IUnknown HRESULT STDMETHODCALLTYPE CPeopleFactory::QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject ) { if (riid == IID_IUnknown) { *ppvObject = (IUnknown*)this; ((IUnknown*)this)->AddRef(); } else if (riid == IID_IClassFactory) { *ppvObject = (IClassFactory*)this; ((IClassFactory*)this)->AddRef(); } else { *ppvObject = NULL; return E_NOINTERFACE; } return S_OK; } ULONG STDMETHODCALLTYPE CPeopleFactory::AddRef( void ) { m_nRef++; return (ULONG)m_nRef; } ULONG STDMETHODCALLTYPE CPeopleFactory::Release( void ) { m_nRef--; if (m_nRef == 0) { delete this; return 0; } return (ULONG)m_nRef; } //IClassFactory HRESULT STDMETHODCALLTYPE CPeopleFactory::CreateInstance( _In_ IUnknown *pUnkOuter, _In_ REFIID riid, _Out_ void **ppvObject ) { CPeople *pObj = NULL; HRESULT hr = S_FALSE; *ppvObject = NULL; //创建组件对象 pObj = new CPeople; if (pObj == NULL) { return hr; } //获得非托管第一个接口指针 hr = pObj->QueryInterface(riid, ppvObject); if (S_OK != hr) { delete pObj; } return hr; } HRESULT STDMETHODCALLTYPE CPeopleFactory::LockServer( _In_ BOOL fLock ) { fLock ? g_LockNumber++ : g_LockNumber--; return S_OK; }
可见,其实工厂对象也是一个COM对象,不同的只是他是给COM库调用的,相当于一个标准对象,是COM库和实际COM对象的桥梁。可以看工厂对象除了查询接口和声明周期管理外,还包含CreateInstance和LockServer函数,前者用于创建实际COM对象,后者传入参数TRUE时锁住组件dll,此时不会卸载。
还有一点是需要注意的,在工厂对象中是不需要操作全局组件对象计数g_EasyComNumber的,因为此时COM库正在加载dll导出接口是一定不会卸载dll的。
4.运行结果
如下调用
int _tmain(int argc, _TCHAR* argv[]) { HRESULT hr = S_FALSE; CLSID easycomCLSID; IUnknown *pUnknown = NULL; IAge *pAge = NULL; IName *pName = NULL; cout << "EasyCom Demo:" << endl; //初始化COM库 if (CoInitialize(NULL) != S_OK) { cout << "Fail to Initialize COM" << endl; return -1; } //由已知的ProgID找对应CLSID hr = ::CLSIDFromProgID(L"EasyCom.Object", &easycomCLSID); if (hr != S_OK) { cout << "Fail to Find CLSID" << endl; return -2; } //创建对应的接口实例 hr = CoCreateInstance(easycomCLSID, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnknown); if (hr != S_OK) { cout << "Fail to Create Object" << endl; return -2; } //查询接口 hr = pUnknown->QueryInterface(IID_IName, (void **)&pName); if (hr != S_OK) { cout << "Fail to Create IName" << endl; return -2; } pName->PrintName(L"wenzhou(http://www.jimwen.net)"); hr = pUnknown->QueryInterface(IID_IAge, (void **)&pAge); if (hr != S_OK) { cout << "Fail to Create IAge" << endl; return -2; } pAge->PrintAge(23); //清理工作 pAge->Release(); pName->Release(); pUnknown->Release(); CoUninitialize(); return 0; }
这里为了使COM库正常工作,需要调用CoInitialize初始化COM库,使用完了需要使用CoUninitialize卸载COM库。
结果显示如下
注意这里红框标明的调试信息。
先忽略红框内内容,CoCreateInstance导出接口IID_IUnknown时,引用计数为1,导出接口IID_IName时,引用计数为2,导出接口IID_IAge时,引用计数为3,符合逻辑。
那么这里红框的内容是怎么来的呢?
答案是这是CoCreateInstance内部调用函数时使用的,每次调用函数前先AddRef增加引用计数,传给函数,使用完再Release,这样可防止COM对象使用期间被卸载了。
本文完整演示代码下载链接
原创,转载请注明来自http://blog.csdn.net/wenzhou1219