COM与ATL(一):COM对象与接口

作者:yurunsun@gmail.com 新浪微博@孙雨润 新浪博客 CSDN博客日期:2012年12月10日

1. COM概述

COM即组件对象模型,是一种已组建为发布单元的对象模型,使各软件组件可以用一种统一的方式进行交互。COM既规定了组件之间进行交互的规范,也提供了实现交互的环境,可以作为不同语言协作开发的一种标准。

1.1 面向对象的组件模型 —— COM

  • 组件的产生

    计算机软件发展的早期,一个app往往是一个单独的应用程序,从软件模型角度来考虑,进化的方法就是把一个庞大的应用程序分成多个模块,每个模块保持一定的功能独立性。协同工作时通过相互之间的接口完成实际任务,这样的模块称为组件。

  • 面向对象的组件

    组件之间的接口是组件软件的关键,需要遵守统一的标准。如果不考虑与其他软件通信,则可以使用自定义的接口标准,否则需要使用一些公用的标准,COM就是Windows系统上这样一个组建标准。

    COM不仅提供了组件之间接口标准,还引入了面向对象的思想:组件模块为COM对象提供活动的空间,COM对象以接口的方式提供服务。

  • 组件、对象、接口

    Windows下一个COM组件可以是dll或exe,一个组件程序可以包含多个COM对象,每个COM对象可以实现多个接口。普通程序调用组件的功能时先创建一个COM对象,或获取一个COM对象,然后通过该对象实现的COM接口调用提供的服务。

1.2 COM结构

COM标准包括规范和实现两部分,规范部分定义了组件之间通信的机制,实现部分就是COM库,为COM规范的具体实现提供了一些核心服务。

1.2.1 对象与接口
  • COM对象

    COM对象类似于C++中对象的概念,是某个类的实例,而类则是一组相关的数据和功能组合在一起的一个定义。接口是一组逻辑上相关的函数集合,对象通过接口成员函数为客户提供服务。

  • COM接口

    COM模型中对象本身对客户是不可见的,客户只能通过接口请求服务。每一个接口都由一个128位GUID标识,客户通过GUI获得接口的指针,通过接口指针调用响应的成员函数。

  • COM接口指针

    客户如何标识COM对象呢?与接口类似,每个对象也用128位GUID来标识,称为CLSID。只要系统中含有这类COM对象的信息,并包括COM对象所在的模块文件dll/exe以及COM对象在代码中的入口点,客户程序就可以由CLSID来创建COM对象。创建成功后客户得到一个指向对象某个接口的指针,就可以调用该对象所有接口提供的服务了。

COM对象本身可以是有状态的也可以是无状态的,对用户透明。

1.2.2 C/S模式

C/S模式的优点是稳定性好,但COM不仅仅是一种简单的C/S模式,客户也可以反过来提供服务。

1.2.3 COM库

COM本身除了规范之外还有实现的部分,包括一些核心的OS级代码。这些库以dll形式存在,包括:

  • 提供少量API实现C/S端COM应用的创建过程
  • COM通过注册表查找本地服务,progname与CLSID的转换
  • 提供一种标准的内存控制方法

COM库在OS层实现,一个OS只有一个COM库实现,保证所有组件按照统一的方式进行交互操作,使我们在编写COM应用时不用编写COM通信相关的大量基础代码。

1.3 COM特性

  • 语言无关性:二进制层次的对象,C++/java等都可以调用
  • 进程透明性:不需要关心dll/exe/远程等多种形式
  • 可重用性:包容、聚合

2. COM对象与接口

2.1 COM对象

2.1.1 COM对象的标识 —— CLSID

COM组件的位置对客户来说是透明的,因为客户不直接访问COM组件,而是通过一个CLSID进行对象的创建和初始化。CLSID实际上就是GUID,只不过是专门用于COM的GUID。VS提供了两个工具来随机生成GUID:uuidgen.exe/guidgen.exe. COM库提供以下API:

HRESULT CoCreateGuid(GUID* pguid);
2.1.2 COM对象 vs C++对象
  • 封装特性:

    COM对象中数据是完全封装在对象内部的,外部不可能直接访问对象的数据属性;C++使用者可以直接访问对象中的public数据成员。

  • 可重用性:

    COM对象A要使用COM对象B的功能,可以通过包容和聚合实现,不需要重新编译;C++则表现在源码一级上,需要重新编译。

2.2 COM接口

2.2.1 从API到COM接口

假如我们要实现一个字处理应用程序,需要一个查字典的功能:

BOOL EXPORT Initialize();
BOOL EXPORT LoadLibrary(char*);
BOOL EXPORT InsertWord(char*, char*);
BOOL EXPORT DeleteWord(char*);
BOOL EXPORT LookupWord(char*, char**);
BOOL EXPORT RestoreLibrary(char*);
BOOL EXPORT FreeLibrary();
2.2.2 接口定义与标识


接口指针又指向另一个叫做vptr的指针,vptr再指向vtable。对一个接口来说,vtable是确定的、接口的成员函数个数是确定的、先后顺序是确定的、每个函数的参数和返回值也是确定的,只要在二进制一级确定这些信息,任何语言都可以实现。

C++中的定义如下:

class IDictionary
{
    public : 
        virtual BOOL __stdcall Initialize() = 0;
        virtual BOOL __stdcall LoadLibrary(String) = 0;
        virtual BOOL __stdcall InsertWord(String, String) = 0;
        virtual void __stdcall DeleteWord(String) = 0;
        virtual BOOL __stdcall LookupWord(String, String *) = 0;
        virtual BOOL __stdcall RestoreLibrary(String) = 0;
        virtual void __stdcall FreeLibrary() = 0;
};
2.2.3 描述语言IDL

使用MIDL工具能把IDL转化成C/C++的头文件。

interface IDictionary
{
    public : 
        HRESULT Initialize();
        HRESULT LoadLibrary([in] string);
        HRESULT InsertWord([in] string, [in] string);
        HRESULT DeleteWord([in] string);
        HRESULT LookupWord([in] string, [out] string *);
        HRESULT RestoreLibrary([in] string);
        HRESULT FreeLibrary();
};
2.2.4 接口的内存模型

我们实现刚刚的接口类,并添加属性:

class CDictionary : public IDictionary
{
    public :
        CDictionary();
        ~CDictionary();

        // IDictionary member function
        virtual BOOL __stdcall Initialize();
        virtual BOOL __stdcall LoadLibrary(String);
        virtual BOOL __stdcall InsertWord(String, String);
        virtual void __stdcall DeleteWord(String);
        virtual BOOL __stdcall LookupWord(String, String *);
        virtual BOOL __stdcall RestoreLibrary(String);
        virtual void __stdcall FreeLibrary();

    private :
        struct  DictWord **m_Data;
        char    *m_DictFilename[128];
};

如果两个CDictionary对象:

如果一个使用CDictionary的实现,另一个不是CDictionary的实现,那么:

2.3 IUnknown 接口

COM规范规定每一个接口都必须从IUnKnown继承,因为IUnknown提供了两个重要特性:生存期控制和接口查询:

class IMyUnknown 
{
public:
    virtual HRESULT __stdcall  QueryInterface(const IID& iid, void **ppv) = 0 ;
    virtual ULONG   __stdcall  AddRef() = 0; 
    virtual ULONG   __stdcall  Release() = 0;
};
2.3.1 引用计数

这部分与一般的智能指针原理一样,不再详细介绍。需要注意的是一般采用在COM对象一级使用引用计数,因此需要为每个COM对象实现IUnKnown的AddRef和Release方法:

ULONG     CDictionary::AddRef()
{
    m_Ref ++;
    return  (ULONG) m_Ref;
}

ULONG     CDictionary::Release()
{
    m_Ref --;
    if (m_Ref == 0 ) {
        delete this;
        return 0;
    }
    return  (ULONG) m_Ref;
}
2.3.2 接口查询

一个COM对象可以实现多个接口,客户程序可以在运行时刻对COM对象的接口进行查询,如果对象实现了该接口则提供服务,否则拒绝服务。这一切是通过QueryInterface实现的。

输入参数iid为接口标识符IID,输出参数ppv为查询得到的结果的接口指针,NULL表示没有实现该接口。下面是使用方式:

BOOL __stdcall CreateObject(const CLSID& clsid, const IID& iid, void **ppv)
{
    if (clsid == CLSID_Dictionary ) {
        CDictionary *pObject = new CDictionary;
        HRESULT result = pObject->QueryInterface(iid, ppv);
        return (result == S_OK) ? TRUE : FALSE;
    }
    return FALSE;
}
2.3.3 接口查询的原则
  • 同一个对象的不同接口指针,查询得到的IUnknown接口必须相同
  • 接口的自反性:查询自己总能成功
  • 接口的对称性:A->B成功则B->A一定成功
  • 接口的传递性:A->B B->C C->D 那么D->A一定成功
  • 接口查询时间无关性
2.3.4 实现多个接口时的内存模型

【注意】不能使用虚继承,因为一定要保证两个接口对应的IUnKnown是不同的。

2.3.4 QueryInterface函数实现
HRESULT  CDictionary::QueryInterface(const IID& iid, void **ppv)
{
    if ( iid == IID_IUnknown ) {
        *ppv = (IDictionary *) this ;
        ((IDictionary *)(*ppv))->AddRef() ;
    } else if ( iid == IID_Dictionary )  {
        *ppv = (IDictionary *) this ;
        ((IDictionary *)(*ppv))->AddRef() ;
    } else if ( iid == IID_SpellCheck )  {
        *ppv = (ISpellCheck *) this ;
        ((ISpellCheck *)(*ppv))->AddRef() ;
    } else {
        *ppv = NULL;
        return E_NOINTERFACE ;
    }
    return S_OK;
}

【注意】在第一个if中没有把this直接转换成IUnKnown指针,因为CDictory有两个IUnKnown节点,会存在二义性。因此先转成实现的接口中任意一个,然后输出时会二次转换成IUnKnown。

对应的CDictionary的COM服务器与客户端程序,猛击此处下载


  • 如果这篇文章对您有帮助,请到CSDN博客留言;
  • 转载请注明:来自[雨润的技术博客]http://(blog.csdn.net/sunyurun) http://blog.csdn.net/sunyurun
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值