上一篇说了怎么设计一个伪COM,现在我们来看看真正的COM库是怎么实现模块化的。
先提一个问题,上一节我们实现的伪COM有没有什么令你不爽的地方?对的,相信很多人对于
需要自己加载指定位置的dll和导出接口
极为不爽,微软也很不爽,所以COM库是解决了这个问题的。
1.COM库-定位和加载、导出接口
a).定位和加载
自己加载dll时,需要显示指定对应的dll位置来加载。在COM中简化了这一查找过程,
CoCreateInstance(Ex)
创建COM对象时传入标记guid,此函数内部调用
CoGetClassObjecti
就会自动帮我们找到对应的dll位置和加载。
COM库怎么查找到对应的dll呢,显然它自己是没法知道的,还是得我们告诉它,我们只需要告诉COM一次,它就记住了,这一过程就叫
COM的注册
。
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的转换
。
使用CLSID的好处在于满足COM标准要求的是
位置透明性
——不论dll在哪里都能发现和加载它,无论是进程内组件还是进程外组件都能使用同一方式来查找。
b.)导出接口
CoGetClassObject
内部找到dll并加载到进程地址空间后,会调用
DllGetClassObject
获得组件对象接口指针。
相比于直接创建组件对象,这里先获得一个工厂对象接口指针
IFactory*
,然后调用
IFactory*
的方法来创建对应的接口。
之所以引入工厂对象而不是直接导出,是因为使用工厂对象可以作中间过渡,如判断不同场景下创建不同的组件对象,
相当于一个中间代理,使得创建过程更加灵活,稍后介绍的包容和聚合都是依赖工厂对象来实现的
。
全过程引用《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
,当检测此函数返回值为TRUE的时候就可以FreeLibrary,类似java内存回收机制。
COM库提供CoFreeUnusedLibraries来检测当前进程中所有COM组件,发现某个组件的DllCanUnloadNow函数返回TRUE就调用FreeLibrary函数,
COM库不会主动调用CoFreeUnusedLibraries,推荐客户在空闲时刻调用(如单开一个线程处理)
。
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