Com技术入门知识

Com技术入门知识

本文假设读者已经具备C++开发技能。

目录

1、背单词

2、Com技术两大核心思想

3、开发自己的Com组件

4、Com技术三板斧

1、背单词CoCreateInstance(创建组件对象);QueryInterface(查询接口);ComPtr(安全指针); HRESULT(接口函数返回值类型); CLSID(唯一编号)

 

2、Com技术的两大核心思想

  • 接口: 为什么要搞个接口这玩意出来?C++的类把数据和操作数据的函数组织到了一起,形成了“对象”这个概念;但在使用中,懒惰的人们又发现我们调用一个对象时,往往只关心对象的一小部分功能; 说是我们在调用一个对象的功能,其实我们只关心我们需要调用的功能。C++时代,在编写大型程序时,其实已经有技艺高超的架构设计师使用了自己的解决办法,即设计适量的“纯虚基类”做为模块间调用的协议。在Com体系中,则强化了这种做法,Com组件对于C++的对象,Com接口则对于C++的纯虚类;Com系统要求,组件之间的调用只能通过接口实现。一个接口往往是一组相关的功能函数的集合, 一个组件可以有支持多个接口,不同组件可以支持同一个接口,如,飞机组件支持IFLy接口,鸟儿组件也支持IFLy接口,则代码这两个组件对象都拥有飞的能力。 我们利用IFLy接口控制飞机对象时,只需要知道飞机飞行的功能(即IFLy中的接口函数),而不需要知道飞机对象的其他复杂功能(加油、维护等等),而且这些功能你要不懂最好不要操作,弄不好会机毁人亡的;

“你知道的越少越安全!--叉叉大佬语录”。

  • CLSID :CLSID是个128位的数值,利用了时间、硬件信息和随机算法,保证生成的每个CLSID都是全世界唯一的。Com充分利用了这种唯一特性,Com技术中给每个Com组件捆绑了一个CLSID,给每个接口捆绑了一个CLSID;这样,我们只要知道一个组件的CLSID就可以创建这个组件对象,只要知道了一个接口的CLSID,就可以拥有访问这个接口的能力。想想这会来什么样的变革? 组件ID,接口ID加接口(二进制通讯协议)就是我们和一个组件交互所需所有条件。发现没有,这三个条件和语言无关、和组件位置无关;这就是为什么Com可以实现多编程语言相互调用;为什么Com可以构建分布式系统。

    “你想给我介绍个女朋友?好啊,给我她的手机号就可以了,其他我自己搞定!”--叉叉语录

     

    3、 开发自己的Com组件

    用VC的向导做一个组件示例(具体怎么做,随便找本Com入门书照着点就可以了):

    组件名MathCom,组件CLSID为 CLSID_MathCom; A支持接口IMathCom,接口CLSID为IID_IMathCom; 支持接口IMathCom2,接口CLSID为IID_IMathCom2;

    为IMathCom增加一个函数func(); 为IMathCom2增加一个函数func2();

    下面的组件创建和调用都会以该组件为例。

     

    4、Com技术三板斧

    • 第一斧:创建Com组件对象;

    常用的组件创建函数 CoCreateInstance的定义:

    STDAPI CoCreateInstance(
     REFCLSID rclsid,//所创建的组件的CLSID(组件编号,128位全球唯一)
     LPUNKNOWN pUnkOuter,//=NULL(只有在组件聚合时,该参数才被利用,一般开发中很少使用聚合技术)
     DWORD dwClsContext,//=CLSCTX_INPROC_SERVER, (指定COM对象的运行环境,一般使用CLSCTX_INPROC_SERVER即可,表明是组件和调用者在一个进程内)
     REFIID riid,//希望获得到ppv接口指针的接口CLSID。
     LPVOID * ppv//获得创建组件对象的某个接口的指针;
    );
    

    创建MathCom组件代码:

    IMathCom *pMath;
    HRESULT hr;
    hr = CoCreateInstance(CLSID_MathCom, NULL, CLSCTX_INPROC_SERVER, IID_IMathCom, (LPVOID*)&pMath);
    

    看CoCreateInstance参数定义,第二第三个参数用那两个固定值就不管了,第一个参数就是组件的CLSID,第四个参数就是接口的CLSID,第五个是输出参数,用来传出第四个参数指定的接口的指针的;正如上面介绍CLSID时说的一样,创建一个组件只要知道组件的CLSID就行了,第四第五个参数是因为创建了一个组件以后总是要使用的,而使用组件的唯一办法就是通过它的某一个接口。所以,如果创建的组件有多个接口,你可以选择任何一个你用起来方便的接口的CLSID做为第四个参数。

     

    简化组件的创建? CComPtr的CoCreateInstance函数:

    CComPtr<IMathCom> pMathCom;
    pMathCom.CoCreateInstance( CLSID_MathCom );
    

    CComPtr的CoCreateInstance的定义:

    HRESULT CoCreateInstance(
      REFCLSID rclsid,
      LPUNKNOWN pUnkOuter = NULL,
      DWORD dwClsContext = CLSCTX_ALL 
    )
    

    考虑一下,为什么CComPtr的函数可以省掉两个第四第五个函数。

    • 第二斧:调用Com组件的接口功能;

    用VC给IMathCom接口增加函数:func1();
    给IMathCom2接口增加函数func2();

    要调用Com组件的接口,首先要掌握如何获取接口;在创建组件时,我们会获得一个组件的接口。通过这个接口当然可以访问该接口内的功能函数了。

    CComPtr<IMathCom> pMathCom;
    pMathCom->CoCreateInstance( CLSID_MathCom );
    pMathCom->func();

    如何调用另一个接口的函数?QueryInterface可以让我们从一个接口查询到同组件的其他任意接口。

    CComPtr<IMathCom2> pMathCom2;
    HRESULT hr;
    hr = pMathCom->QueryInterface(IID_IMathCom2,(LPVOID*)&pMathCom2);
    //或者简写为:hr = pMathCom->QueryInterface(&pMathCom2);   思考:为什么可以这样写?
    if( SUCCEED(hr) )
      pMathCom2->func2();
    

    QueryInterface函数哪里来的?所有的接口都继承于IUnknown ;

    class IUnknown
    {
    public:
      virtual HRESULT QueryInterface(REFIID iid, void** ppvObj) = 0;
      virtual ULONG AddRef() = 0;
      virtual ULONG Release() = 0;
    };
    
    • 第 三斧:组件释放控制

    组件使用了“引用计数”来实现组件释放的控制;引用计数记录了当前有几个使用者,当引用计数为0时,组件自动释放。

    接口提供者负责给引用计数加1,接口使用者在释放接口变量前负责给引用计数减1;

     

    IUnknown:AddRef()让组件的引用计数加一, Release() 让组件的引用计数减一。

    我们举个例子来分析引用计数是如何变化的:

    //调用前面做的组件为例:组件名MathCom,组件CLSID为 CLSID_MathCom; A支持接口IMathCom,接口CLSID为IID_IMathCom; 支持接口IMathCom2,接口CLSID为IID_IMathCom2
    //IMathCom有函数func(); IMathCom2有函数func2();
    IMathCom *pMath = NULL;
    HRESULT hr;
    hr = CoCreateInstance(CLSID_MathCom, NULL, CLSCTX_INPROC_SERVER, IID_IMathCom, (LPVOID*)&pMath);
    if(FAILED(hr))
    return E_FAIL;
    pMath->func();
    
    IMathCom  *pMathSame = NULL;
    pMathSame = pMath;
    pMathSame->AddRef();
    pMathSame->func();
    //...
    pMathSame->Release();
    pMathSame = NULL;
    
    IMathCom2 *pMath2 = NULL;
    hr = pMath->QueryInterface( IID_IMathCom2,(LPVOID*)&pMath2);
    if(FAILED(hr))
    return E_FAIL;
    pMath2->func2();
    //....
    pMath2->Release();
    pMath->Release();
    

     

    CComPtr: 通过析构函数和重载"=" 操作符,CComPtr较好的封装了引用计数的处理。

    下面看一下使用CComPtr写代码:

    CComPtr<IMathCom> pMath;
    HRESULT hr;
    hr = pMath->CoCreateInstance(CLSID_MathCom);
    if(FAILED(hr))
      return E_FAIL;
    pMath->func();
    
    CComPtr<IMathCom> pMathSame;
    pMathSame = pMath;
    pMathSame->func();
    
    CComPtr<IMathCom2> pMath2;
    hr = pMath->QueryInterface(&pMath2);
    if(FAILED(hr))
      return E_FAIL;
    pMath2->func2();
    

    最后,我们再看一下接口指针做为函数参数传递时,它的引用计数的控制。

    HRESULT CreateCom( IMathCom **ppMathOut )
    {
       CComPtr<IMathCom> pMath;
       HRESULT hr;
       hr = pMath->CoCreateInstance(CLSID_MathCom);
       if(FAILED(hr))
         return E_FAIL;   
       
       *ppMathOut = pMath;
    }
    
    //调用上面函数的代码
    CComPtr<IMathCom> pMath;
    HRESULT hr;
    hr = CreateCom( &pMath );
    if(FAILED(hr))
      return hr;
    pMath->func();

     

    仔细分析以上三块代码每条语句执行后,MathCom组件对象的引用计数的变化;代码结束后,引用计数是否回复为零了?谁是接口提供者,谁是接口使用者。

     

    “做为一个环保人士,我们一定时刻留意我们制造的东西、我们使用过的东西是如何被回收的! ”--叉叉语录