前言
最近需要深入学习下COM,来解决工作上的问题。本文根据《COM原理与应用》这本书来写的。
COM简单介绍
COM是微软提出的组件标准,它定义了组件程序之间进行交互的标准,提供了组件程序运行所需的环境。
进程内组件:指的是dll
进程外组件:指的是exe
组件程序可能会包含多个组件对象,所以程序与程序进行通信时,通信双方为COM对象。
COM的历史发展
随着桌面程序之间的交互不断深入,在OLE技术发展过程中产生了COM。
大家都知道Windows操作系统,可以将一个应用程序里面写的文字复制到另一个程序中。
这其中需要程序与程序之间进行通信。
刚开始微软采用的是OLE规范,在OLE1中,进程间通信采用的是DDE技术,这种技术最大的缺点是效率低,稳定性不好,使用不方便。
OLE2中,放弃了DDE通信,采用COM方式进行通信。
既然提到了OLE,简单介绍下OLE是什么?
OLE原先是对象链接和嵌入,主要用于复合文档,但是自从OLE2之后,OLE变成了在桌面系统上进行程序通信的一个技术统称。
COM组件的组成
一个COM组件就是一个dll文件或者一个exe文件,一个组件可以包含多个COM对象,并且每个COM对象可以实现多个接口。
COM对象是通过接口来进行进程间通信的。
在进程通信中,COM规范采用的是客户/服务器模型。
一般而言,服务器为COM组件,调用方为客户。
COM对象用一个128位全局唯一标识符(GUID)来标识,称之为CLISID。
COM接口也是用一个128位的GUID来标识,称之为IID。
客户通过CLISID来创建COM对象,得到一个指向对象某个接口的指针之后(在此处可以得到该对象的所有接口指针),然后再调用该指针,就可以调用该接口提供的所有服务。
客户可以同时拥有两个相同CLISID的COM对象。
重要的IUnknown接口
所有的COM接口都要继承IUnknown接口,所有的COM对象都需要实现IUnknown接口。
IUnknown接口提供了非常重要的两个特性:生存期控制和接口查询。
客户端程序虽然只能通过接口与COM对象进行通信,但是也要控制COM对象的存在与否。
我们看一下IUnknown接口的定义:
class IUnknown
{
public:
virtual HRESULT _stdcall QueryInterface(const IID& iid,void ** ppv) = 0;
virtual ULONG _stdcall AddRef()=0;
virtual ULONG _stdcall Release()=0;
}
QueryInterface用于查询接口,AddRef用于增加引用计数,Release用于减少引用计数。
进程内组件
进程内组件和客户程序运行在同一个进程地址空间中,它是作为dll加载到客户程序的内存中。
dll本身是独立的,不依赖客户程序。
客户程序如果想要调用dll必须有标准的约定(二进制上的约定),dll程序包含一个引出函数表。
引出函数表中包含函数名称、函数序号以及函数地址。
在C++语言中,一般指定DLL的引出函数为_stdcall调用方式,如果使用了_cdecl,那么有些编程语言就不能使用dll。
因为C++会对dll的每个引出函数自动生成修饰名,但对于不同的编译器并不兼容,所以为了通用性,我们可以不要编译器使用修饰名,即在每个函数定义前面加上 extern “C"。
extern "C" int _stdcall MyFunction(int n);
光这个还不够,还需要描述DLL程序的模块信息。我们可以有两种方式去描述,一种是使用DEF文件,另一种是直接在函数说明时使用_declspec(dllexport)说明符:
//def 文件
LIBRARY "DictComp"
DESCRIPTION "描述语言"
EXPORTS
CreateObject @1
//直接在函数说明时使用说明符
extern "C" _declspec(dllexport) int _stdcall MyFunction(int n);
进程外组件
进程外组件是组件程序独占一个进程而不使用客户程序的进程空间。
这样就产生了两个问题
- 一个进程如何调用另一个进程中的函数
- 参数如何从一个进程被传递到另一个进程中
COM采用了本地过程调用(LPC)和远过程调用(RPC)的方法进行进程间通信。
LPC用于同一机器上的不同进程之间通信,RPC用于在不同机器上的进程间进行通信。
可以看一下普通程序如何通过LPC来调用另一个进程的:
COM进程外组件:
可以看到多出来代理dll和存根dll,它们是用来完成LPC调用,还会对参数和返回值进行翻译和传递。
所有的跨进程的操作被COM库封装了,实际使用过程和进程内组件差不多。
COM组件注册信息
com库会通过系统注册表所提供的信息进行组件的创建工作。
COM只用到了HKEY_CLASSES_ROOT,HKEY_CLASSES_ROOT下最主要的是CLISID子键,CLISID下面有很多组件项。
每个组件项底下有组件信息:
InprocServer32 ---------------进程内组件dll的全路径
LocalServer32 ---------------进程外组件的全路径
Version ---------------组件的版本
ProgID ---------------程序标识符
TypeLib ---------------当前系统中类型库信息
ProxyStubClsid ---------------代理dll
ProxyStubClsid32 ---------------代理dll
Implemented Categories ---------------COM类别,子键代表实现的接口
有关Implemented Categories列出的类型,可以在HKEY_CLASSES_ROOT键下有个子键Component Categories中找到。
微软也提供了OleView工具来列出机器上的所有类型以及组件对象列表。
进程内组件注册时会调用DllRegisterServer方法进行注册,卸载时会调用DllUnregisterServer进行卸载。
而进程外组件需要支持/RegServer和UnregServer来完成注册或卸载。
类厂
创建COM对象之前,我们先了解下类厂的概念。
类厂:创建类的工厂。
每个类厂只针对特定的COM类对象,类厂都会继承并实现IClassFactory:
class IClassFactory:public IUnkown
{
//用来创建COM对象
virtual HRESULT _stdcall CreateInstance(IUnknown* pUnknownOuter,const IID& iid,void ** ppv) =0;
//用来控制组件的生命周期
virtual HRESULT _stdcall LockServer(BOOL bLock);
}
类厂也是COM对象,类厂是由DllGetClassObject来创建的。
//原型
HRESULT DllGetClassObject(const CLSID& clsid,const IID& iid,(void**) ppv);
在COM库中,有三个API可用于创建对象:
CoGetClassObject
CoCreateInstance
CoCreateInstanceEx
HRESULT CoGetClassObject(const CLISID& clsid,
DWORD dwClsContext, //指定组件类型,进程内组件、进程外组件或者进程内控制对象
COSERVERINFO* pServerInfo, //用于DCOM时,不能为NULL
const IID& iid,
(void**) ppv);
如果是进程内组件,CoGetClassObject会调用dll中的DllGetClassObject函数,iid通常为接口IClassFactory的标识符IID_IClassFactory,然后返回类厂对象接口指针。
如果是进程外组件,那么就比较复杂。首先CoGetClassObject函数启动组件进程,等待组件进程把它支持的COM类对象的类厂注册到COM中,然后CoGetClassObject函数把COM中相应的类厂信息返回。
进程外组件被COM库启动时,它必须把所支持的COM类的类厂对象通过CoRegisterClassObject函数注册到COM中,以便COM库创建COM对象使用。
当进程退出时,必须调用CoRevokeClassObject函数以便通知COM所注册的类厂对象不再有效。
CoRegisterClassObject函数与CoRevokeClassObject函数必须配对。
这样一描述是挺麻烦的。。。
HRESULT CoCreateInstance(const CLSID& clsid,
IUnknown* pUnknownOuter,
DWORD dwClsContext,
const IID& iid,
(void**) ppv);
CoCreateInstance是一个包装的辅助函数,在它内部调用了CoGetClassObject函数。参数pUnknownOuter与类厂接口的CreateInstance中对应的参数一致。
使用这个函数,客户程序可以不用和类厂打交道,直接获取到了COM对象的接口指针。
CoCreateInstance不能创建远程机器的对象,因为服务器参数内部默认为NULL。
如果想要创建远程对象,可以使用CoCreateInstanceEx:
HRESULT CoCreateInstanceEx(const CLISID& clsid,
IUnknown* pUnknownOuter,
DWORD dwClsContext,
CONSERVERINFO* pServerInfo,
DWORD dwCount,
MULTI_QI* rgMultiQU)
pServerInfo用于指定服务器信息,dwCount和rgMultiQI指定了一个结构数组,可用于保存多个对象接口指针,其目的在于一次获取多个接口指针,减少网络交互。
COM库
如果应用程序需要使用COM库,那么必须先调用COM库的初始化函数:
HRESULT CoInitialize(IMalloc* pMalloc);//pMalloc为内存分配限制,默认为NULL,使用缺省的内存分配器
//返回S_OK,则表示初始化成功
//返回S_FALSE,则表示虽然初始化成功,但是本次调用不是本进程中首次调用。
//返回E_UNEXPECTED,则表示在初始化过程中发生了错误,应用程序不能使用COM库
DWORD CoBuildVersion();//获取COM库版本
void CoUninitialize(void);//释放COM库
因为COM是基于二进制标准建立起来的,COM库的内存分配与管理需要使用COM库中的内存分配与释放函数。
void* CoTaskMemAlloc(ULONG cb);//分配内存
void CoTaskMemFree(void* pv);//释放内存
void CoTaskMemRealloc(void* pv,ULONG cb);//相当于realloc
COM重用
COM重用有两种方式,包含和聚合。
看图说话:
我在工作中常用包含,A对象包含B对象,如果用C++表示:
class B
{
public:
void printB();
};
class A
{
public:
void printA();
void printB(){ m_b.printB();}
B m_b;
};
int main()
{
A a;
a.printA();
a.printB();
}
聚合比较复杂了,首先要了解什么是聚合?
聚合就是把一堆东西放在一块。
我们可以画一个图来表示COM聚合:
假如客户程序调用A对象,A对象聚合B对象,那么实现聚合需要依赖A对象的QueryInterface成员函数。
首先我们要了解QueryInterface会根据传入参数不同,返回不同的接口指针。
客户程序现在可以拥有两个接口指针,客户程序根据不同的接口指针,看到的世界不一样。
COM 规范
- 对于同一个对象的不同接口指针,查询得到的IUnknown接口必须完全相同
- 接口的对称性。对一个接口查询其自身总应该成功
- 接口自反性。如果从一个接口指针查询到另一个接口指针,则从第二个接口指针再回到第一个接口指针必定成功
- 接口传递性。如果从第一个接口指针查询到第二个接口指针,从第二个接口指针可以查询到第三个接口指针,则从第三个接口指针一定可以查询到第一个接口指针。
- 接口查询时间无关性。如果在某一时刻可以查询到某个接口指针,那么任何时刻都可以查询到。
根据图所示,客户程序是把整个COM组件当做对象A来调用。从外边看,对象A实现了两个接口。
但是根据COM规范,它违反了第1、3规范,第4规范更不用提了。
如何解决这个问题呢?
当客户程序通过IB接口指针查询IUnknown接口时,把IA的IUnknown接口传出去。这样做就不会违背COM第1规范。
如何才能做到这点呢?可以通过CoCreateInstance方法创建COM接口IA时候做到:
HRESULT CoCreateInstance(const CLSID& clsid,
IUnknown* pUnknownOuter,//将IB的IUnknown接口指针传进去,代表A聚合B
DWORD dwClsContext,
const IID& iid,
(void**) ppv);
IA接口需要实现两种IUnknown接口,一个是正常的IUnknown(非委托IUnknown),另一个是聚合的IUnknown(委托IUnknown)。
接下来我们就用代码实现下,因为C++不支持同时实现两个IUnknown,所以委托IUnknown和非委托IUnknown不能都使用IUnknown类,但是我们可以定义一个新的类。
class INondelegationUnknown
{
public:
virtual HRESULT _stdcall NondelegationQueryInterface(const IID& iid,void** ppv) = 0;
virtual ULONG _stdcall NondelegationAddRef()=0;
virtual ULONG _stdcall NondelegationRelease()=0;
};
因为对象B支持聚合,所以它要实现INondelegationUnknown
class B:public IB,public INondelegationUnknown
{
protected:
ULONG m_Ref;
public:
B(IUnknown* pUnknownOuter);
~B();
public:
//委托IUnknown接口函数
virtual HRESULT _stdcall QueryInterface(const IID& iid,void** ppv);
virtual ULONG _stdcall AddRef();
virtual ULONG _stdcall Release();
//非委托IUnknown接口函数
virtual HRESULT _stdcall NondelegationQueryInterface(const IID& iid,void** ppv);
virtual ULONG _stdcall NondelegationAddRef();
virtual ULONG _stdcall NondelegationRelease();
virtual HRESULT _stdcall printB();
private:
IUnknown* m_pUnknownOuter;
};
//非委托实现
ULONG B::NondelegationAddRef()
{
m_Ref++;
reurn (ULONG)m_Ref;
}
ULONG B::NondelegationRelease()
{
m_Ref--;
if(m_Ref == 0)
{
//g_CompANumber--;
delete this;
return 0;
}
reurn (ULONG)m_Ref;
}
HRESULT B::NondelegationQueryInterface(const IID& iid,void** ppv)
{
if(iid == IID_IUnknown)
{
*ppv = (INodelegationgUnknown*)this;
((IUnknown*)(*ppv))->AddRef();
}else if(iid == IID_IB)
{
*ppv = (IB*)this;
((IB*)(*ppv))->AddRef();
}else{
*ppv = NULL;
return E_NOINTERFACE;
}
return S_OK;
}
//委托实现
ULONG B::AddRef()
{
if(m_pUnknownOuter != NULL)
{
return m_pUnknownOuter->AddRef();
}
return NondelegationAddRef();
}
ULONG B::Release()
{
if(m_pUnknownOuter != NULL)
{
return m_pUnknownOuter->Release();
}
return NondelegationRelease();
}
HRESULT B::QueryInterface(const IID& iid,void** ppv)
{
if(m_pUnknownOuter != NULL)
{
return m_pUnknownOuter->QueryInterface(iid,ppv);
}else{
return NondelegationQueryInterface();
}
}
搞清楚了COM接口支持聚合要实现的具体逻辑,下面考虑下支持聚合后COM对象如何创建?
在创建COM A对象的时候同时创建COM B对象。
HRESULT A::Init()
{
IUnknown* pUnknownOuter = (IUnknown*) this;
//创建B对象,可以看到创建的时候把自身的指针传进去了。
//m_pUnknownInner为指向B对象的非委托IUnknown
HRESULT result = ::CoCreateInstance(CLSID_B,pUnknownOuter,CLSCTX_INPROC_SERVER,
IID_IUnknown,(void**) &m_pUnknownInner);
if(FAILED(result))
{
return E_FAIL;
}
return S_OK;
}
//析构的时候,要释放聚合对象B接口指针
A::~A()
{
if(m_pUnknownInner != NULL)
{
m_pUnknownInner->Release();
}
}
B对象被A创建的时候,需要注意两点:
第一点,B工厂创建B对象的时候
HRESULT BFactory::CreateInstance(IUnknown* pUnknownOuter,
const IID& iid,void ** ppv)
{
//如果pUnknownOuter参数不是NULL,则表明创建的B对象要被聚合
//聚合必须保证iid为IID_IUnknown
if(pUnknownOuter != NULL && iid != IID_IUnknown)
{
return CLASS_E_NOAGGREGATION;
}
* ppv = NULL;
HRESULT hr = E_OUTOFMEMORY;
B* b = new B(pUnknownOuter);
if(b == NULL)
{
return hr;
}
//此处调用B对象的非委托IUnknown接口
//只有非委托的IUnknown接口,才可以查询到IB接口
hr = b->NondelegationQueryInterface(iid,ppv);
return hr;
}
第二点,B对象构造函数
B::B(IUnknown* pUnknownOuter)
{
//把对象A的接口指针IUnknown给存起来了
m_pUnknownOuter = pUnknownOuter;
}
这些是聚合的细节,有些复杂。。。可以简单的从客户程序角度看,聚合就是把多个COM对象整成一个对象、多个接口,想要用哪个就调用哪个接口。
整体流程
到目前为止,大体流程已经介绍完毕,让我们整理下:
简单来看就三个步骤,下一篇让我们接着深入理解COM。