深入理解COM(一)

前言

最近需要深入学习下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);

进程外组件

进程外组件是组件程序独占一个进程而不使用客户程序的进程空间。

这样就产生了两个问题

  1. 一个进程如何调用另一个进程中的函数
  2. 参数如何从一个进程被传递到另一个进程中

COM采用了本地过程调用(LPC)和远过程调用(RPC)的方法进行进程间通信。

LPC用于同一机器上的不同进程之间通信,RPC用于在不同机器上的进程间进行通信。

可以看一下普通程序如何通过LPC来调用另一个进程的:

COM进程外组件:

可以看到多出来代理dll和存根dll,它们是用来完成LPC调用,还会对参数和返回值进行翻译和传递。

所有的跨进程的操作被COM库封装了,实际使用过程和进程内组件差不多。

COM组件注册信息

com库会通过系统注册表所提供的信息进行组件的创建工作。

COM只用到了HKEY_CLASSES_ROOTHKEY_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 规范

  1. 对于同一个对象的不同接口指针,查询得到的IUnknown接口必须完全相同
  2. 接口的对称性。对一个接口查询其自身总应该成功
  3. 接口自反性。如果从一个接口指针查询到另一个接口指针,则从第二个接口指针再回到第一个接口指针必定成功
  4. 接口传递性。如果从第一个接口指针查询到第二个接口指针,从第二个接口指针可以查询到第三个接口指针,则从第三个接口指针一定可以查询到第一个接口指针。
  5. 接口查询时间无关性。如果在某一时刻可以查询到某个接口指针,那么任何时刻都可以查询到。

根据图所示,客户程序是把整个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。

  • 6
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值