1.为什么设计COM
搞懂COM基本概念前,我们需要知道为什么设计COM,知道了为什么设计COM就知道怎么设计COM了。
COM是为了解决软件模块化和可重用化才出现的,Windows模块化和重用化依赖如下两方面:
1.语言层面使用面向对象封装、继承和多态来
2.二进制层面使用dll和exe文件
前者仅限特定语言中使用;后者导出函数多时可用函数混在一起,很难使用。能不能把这两者结合在一起设计一种语言无关且二进制层面支持封装、继承和多态的机制呢?对了,这就是COM机制。
语言无关性其实就是统一调用标准,详细探讨这个意义不大,想详细了解的可查询潘爱民的《COM原理与应用》。
COM分为进程内组件和进程外组件,我们先从进程内组件讲起。
本文先从COM模块化说说COM的封装和多态,可重用性(继承性)放在之后再讲。
2.怎么设计COM-模块化
先说模块化,我们如下设计COM:
1.将dll中功能相似的导出函数聚拢到一起,封装成一个组件接口
2.一个组件对象可包括多个接口
3.一个dll或exe可包括多个对象,称为组件
举个简单的例子,设计一个显示办公设备的组件如下:
如下:
1.
办公组件包括电脑组件对象(CComputer)和打印机组件对象(CPrinter)
2.
电脑组件对象包括USB组件接口(IInterfaceUsb)和Pci组件接口(IInterfacePci)
打印机组件对象包括USB组件接口(IInterfaceUsb)
3.
USB组件接口包含和USB相关的函数——显示当前USB上的键盘数量(ShowKeyboardCnt)和鼠标数量(ShowMouseCnt)
Pci组件接口包含和Pci相关的函数——显示当前Pci接口上内存条内存大小(ShowMemory)和显卡数量(ShowMonitorCnt)
这样一来想使用具体的接口相关操作时,不需要像之前一样在一大堆导出函数中翻来翻去找了,我们只需要导出指定对象和具体的接口即可,这就是封装。不同的对象,可导出相同的接口,同样的接口可包括同样的函数组合也可包含不同的函数组合,这就是多态。
3.怎么实现COM-模块化
前面说了,只需要指定对象和具体接口即可,具体实现可导出一个函数指明组件对象和相应的接口从而得到对应的接口指针,通过该指针即可完成对应功能操作。既然涉及到了指针和模块管理,自然想到C++智能指针的那一套,引入引用计数管理来确定对应组件接口的有效时间。(组件实现并不局限于c++语言,这里这样描述只是为了方便理解。)这就是分别对应COM实现最重要的两个概念——接口查询和生存周期管理。
为了指明组件对象和接口时唯一化,引入GUID,可使用VS自带工具guidgen.exe来产生对应的guid,也可使用COM函数CoCreateGuid,对应于组件对象的别名为CLSID,对应于接口别名为REFIID。
上面图中有个IUnkown接口,所有COM接口都必须继承该接口,所有的COM对象都必须实现该接口,接口定义在unknwn.h中,如下:
class IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject);
virtual ULONG STDMETHODCALLTYPE AddRef();
virtual ULONG STDMETHODCALLTYPE Release();
};
QueryInterface指明要查询的接口,返回的接口保存在ppvObject中。
AddRef和Release分别对应你于增加和减少引用计数。
下面我们先不用COM库的东西,自己仿照COM来实现之前描述的电脑组件对象:
1.先设计组件接口如下
// {9B8A9383-7830-463c-90F6-690184E5513D}
_declspec(selectany) GUID CLSID_COMPUTER =
{ 0x9b8a9383, 0x7830, 0x463c, { 0x90, 0xf6, 0x69, 0x1, 0x84, 0xe5, 0x51, 0x3d } };
// {79FB2CFE-B3EB-44C1-9EF7-97FB7DC017FC}
_declspec(selectany) GUID IID_PCI =
{ 0x79fb2cfe, 0xb3eb, 0x44c1, { 0x9e, 0xf7, 0x97, 0xfb, 0x7d, 0xc0, 0x17, 0xfc } };<pre name="code" class="cpp">class CComputer : public IInterfacePci, public IInterfaceUsb
{
public:
CComputer();
//IUnknown
virtual HRESULT STDMETHODCALLTYPE QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject);
virtual ULONG STDMETHODCALLTYPE AddRef();
virtual ULONG STDMETHODCALLTYPE Release();
//IInterfacePci
void STDMETHODCALLTYPE ShowMemory();
void STDMETHODCALLTYPE ShowMonitorCnt();
//IInterfaceUsb
virtual void STDMETHODCALLTYPE ShowKeyboardCnt();
virtual void STDMETHODCALLTYPE ShowMouseCnt();
private:
ULONG m_nRef;//引用计数
};
这里CLSID_COMPUTER用来标识电脑组件对象,IID_PCI和IID_USB分别用来标识Pci组件接口和Usb组件接口。IInterfacePci和IInterfaceUsb 分别为对应的组件接口,包含了对应的函数组合。
2.然后我们声明组件对象如下
class CComputer : public IInterfacePci, public IInterfaceUsb
{
public:
CComputer();
//IUnknown
virtual HRESULT STDMETHODCALLTYPE QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject);
virtual ULONG STDMETHODCALLTYPE AddRef();
virtual ULONG STDMETHODCALLTYPE Release();
//IInterfacePci
void STDMETHODCALLTYPE ShowMemory();
void STDMETHODCALLTYPE ShowMonitorCnt();
//IInterfaceUsb
virtual void STDMETHODCALLTYPE ShowKeyboardCnt();
virtual void STDMETHODCALLTYPE ShowMouseCnt();
private:
ULONG m_nRef;//引用计数
};
包含了IUnknown、IInterfacePci和IInterfaceUsb 的内容,面向对象设计的原则,
设计接口并在对象中设计在这里很好的体现了。
3.最后来实现组件对象
a).先看接口查询
HRESULT STDMETHODCALLTYPE CComputer::QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject )
{
if (riid == IID_IUnknown)
{
*ppvObject = (IInterfaceUsb*)(this);//存在二义性,所以指定转换类型
((IInterfaceUsb*)(this))->AddRef();
}
else if (riid == IID_USB)
{
*ppvObject = (IInterfaceUsb*)(this);
((IInterfaceUsb*)(this))->AddRef();
}
else if (riid == IID_PCI)
{
*ppvObject = (IInterfacePci*)(this);
((IInterfacePci*)(this))->AddRef();
}
else
{
*ppvObject = NULL;
return E_NOINTERFACE;
}
return S_OK;
}
简单吗?所有的实现都在组件对象中,查询时只需要按照指定的IID暴露指定的接口即可。这里的E_NOINTERFACE是标准COM错误,意为未实现的组件接口,后面再说。
b).然后是生存周期管理
CComputer::CComputer()
{
m_nRef = 0;
}
ULONG STDMETHODCALLTYPE CComputer::AddRef()
{
m_nRef++;
return m_nRef;
}
ULONG STDMETHODCALLTYPE CComputer::Release()
{
m_nRef--;
if (0 == m_nRef)
{
delete this;
return 0;
}
return m_nRef;
}
构造函数中,引用计数初始化为0。每次查询返回接口时,都会调用AddRef增加引用计数来保证接口有效。当不再需要使用接口时,调用Release,引用计数为0时就会删除组件对象,此时接口无效。
c).最后是各个接口函数
//IInterfacePci
void STDMETHODCALLTYPE CComputer::ShowMemory()
{
cout << "PCI-Memory: 512MB" << endl;
}
void STDMETHODCALLTYPE CComputer::ShowMonitorCnt()
{
cout << "PCI-ShowMonitorCnt: 2" << endl;
}
//IInterfaceUsb
void STDMETHODCALLTYPE CComputer::ShowKeyboardCnt()
{
cout << "USB-ShowKeyboardCnt: 2" << endl;
}
void STDMETHODCALLTYPE CComputer::ShowMouseCnt()
{
cout << "USB-ShowMouseCnt: 1" << endl;
}
到此一个完整组件对象就完成了。
4.组件导出使用函数
下面为了在组件外使用组件对象,我们需要在组件中导出一个函数来,设计如下
//导出接口
BOOL WINAPI CreateObject(const CLSID& clsid, const IID& iid, void **ppvObject)
{
if (clsid == CLSID_COMPUTER)
{
CComputer* pComputer = new CComputer;
if (pComputer && S_OK == pComputer->QueryInterface(iid, ppvObject))
{
return TRUE;
}
}
return FALSE;
}
这里clsid指明组件对象,iid指明组件接口,ppvObject返回对应接口指针,是不是完成了完整的接口查询功能。
在这这里我们只实现了一个组件对象,如果有多个组件对象,和之前的编写方法一样,然后在此加上if lese判断创建对应组件对象查询对应接口即可。虽然实际中很多COM组件看上去只包含一个组件对象,但是千万不要以为一个组件只能包含一个组件对象,这是很容易混淆的。
5.使用组件导出接口
为了使用组件功能,我们先加载组件,导出使用函数,如下
BOOL CreateObject(const CLSID& clsid, const IID& iid, void **ppvObject)
{
HMODULE hModule = LoadLibrary(L"LikeCom.dll");
if (NULL == hModule)
{
return FALSE;
}
typedef BOOL (WINAPI *PFUNC_CreateObject)(const CLSID& clsid, const IID& iid, void **ppvObject);
PFUNC_CreateObject pfnCreateObject;
pfnCreateObject = (PFUNC_CreateObject)GetProcAddress(hModule, "CreateObject");
if (NULL == pfnCreateObject)
{
FreeLibrary(hModule);
return FALSE;
}
return pfnCreateObject(clsid, iid, ppvObject);
}
然后查询指定组件对象的指定接口即可,如下
int _tmain(int argc, _TCHAR* argv[])
{
IInterfacePci *pPci = NULL;
IInterfaceUsb *pUsb = NULL;
if (CreateObject(CLSID_COMPUTER, IID_PCI, (LPVOID*)&pPci))
{
pPci->ShowMemory();
pPci->ShowMonitorCnt();
}
if (CreateObject(CLSID_COMPUTER, IID_USB, (LPVOID*)&pUsb))
{
pUsb->ShowKeyboardCnt();
pUsb->ShowMouseCnt();
}
if (pPci)
{
pPci->Release();
}
if(pUsb)
{
pUsb->Release();
}
return 0;
}
注意在使用COM时我们只需要在调用程序中包含COM接口定义,查询得到对应的接口即可完成操作,具体的实现都在COM组件中,COM组件只是导出了一个查询函数,所有的实现均对外不可见。某种程度上这 增加了软件逆向的难度,实际工程中很多公司没有使用COM,但是都或多或少使用了本文描述的这种 伪COM的封装手段。可以自己导出一些厂商的dll接口看看,带有Create***的很可能就是类似的实现,只是很多去掉了生存周期管理。
到此,希望借助这个伪COM的实现理解COM的两个基本概念——接口查询和生存周期管理。如果没有理解,请自己实现一遍。
本文完整演示代码下载链接
原创,转载请注明来自http://blog.csdn.net/wenzhou1219