COM聚合

长平狐 发布于 2012/08/28 16:38
阅读 124
收藏 0

【Gopher China万字分享】华为云的Go语言云原生实战经验!>>>

 
 
聚合源自组件重用。当有两个组件A和B,他们分别实现了自己的接口IA和IB。如果有一个客户程序创建了A对象使得自己可以调用IA的方法,但同时又想获得IB的接口,调用IB的方法。这时候有两种做法:一种是客户程序创建B对象,还有一种方法是A组件内部创建B组件,然后客户通过某种途径调用B的接口方法。
第一种方法,使得客户必须知道有独立的B组件的存在,第二种方法客户可以认为只有一个组件A,组件A实现了两个接口IA和IB。第二种方法可以制造出一种假象,让客户程序编写更加简单。从组件A如何管理组件B的方法上,第二种方法还可以分为两种:包容和聚合。
包容很简单,如果组件IB接口拥有一个方法F(),那么A组件就要实现一个自己的IBEx接口,并实现IBEx::F( )方法,内部调用IB::F()方法。这样,客户也就可以通过调用IBEx::F()来调用IB::F。在这种情况下,客户只知道有IA和IBEx接口,不知道还存在另一个B组件和IB接口。IBEx::F()增加一些代码从而修改IB::F()方法的功能,甚至可以完全丢弃IB::F()方法。
聚合通常用于IB接口的功能完全不需要做任何的修改,就可以直接交给用户使用的情况。这时候,如果IB接口的方法很多,包容就显得很笨拙。因为它不得不对每一个方法作一次包装,尽管什么都不做。COM+对象池就是通过聚合我们的组件,来把我们组件的接口暴露给客户的。聚合方式下,A组件直接将IB接口交给客户,客户就可以调用,但是客户仍然以为是A组件实现了IB接口。如下图:
                              组件A
                                      IUnknown

       组件B
IB
 

 
 
 

客户直接使用
 
 

IA
 


客户直接使用
 
       客户程序只知道A组件而不知道B组件,并且认为A组件实现了IA和IB接口。因此,当客户创建了A组件(通过CoCreateInstance函数),获取到IUnknown接口时,应该获得的是A组件实现的IUnknown接口。       问题在于B组件有自己的IUnknown的接口实现,如果B组件还是采用一般的方法实现IUnknown接口的话,当客户调用IB::QueryInterface函数,就不会得到IA接口,这当然是不允许的。所以一个支持聚合的组件,它的IUnknown实现必然要有别于普通组件。
       B组件应该拥有一个成员变量IUnknown* m_pUnknownOuter;该指针可以指向A组件的IUnknown接口。当客户调用IB::QueryInterface请求时,如果IB接口已经被聚合了,就调用m_pUnknownOuter->QueryInterface方法,这实际上就是调用了A组件的QueryInterface方法。那么,m_pUnknownOuter指针是什么时候被初始化的呢?CoCreateInstance函数和IClassFactory::CreateInstance方法都接受一个IUnknown参数。如果A组件内部想聚合B组件的IB接口,他就会将自己的IUnknown指针传递进去,如果A组件并不想聚合B组件,那么简单的传递一个NULL就行了。
       B组件可以根据m_pUnknownOuter是否为NULL,来判断是否被聚合。
       B组件实现的具体步骤如下:
1) 声明一个INondelegationUnknown接口,该接口拥有IUnknown接口一样的纯虚函数,当然函数名前面均加上Nondelegation前缀。
2) CB类(假设CB类为组件类)继承并实现INondelegationUnknown接口,实现代码和普通组件的IUnknown一样,这用于非聚合的情况下。
3) CB类的构造函数接受IUnknown指针,如果传递进来的是NULL,说明B组件并不被用作聚合。m_pUnknownOuter变量就指向B组件自身的IUnknown接口。如果传递进来的不是NULL,说明被用作聚合。m_pUnknownOuter变量值等于参数值,指向外部组件的IUnknown接口指针。
4) CB类也要继承并实现IUnknown接口。这个接口的QueryInterface函数实现只是调用m_pUnknownOuter->QueryInterfac方法。m_pUnknownOuter究竟代表什么取决于创建组件时是否传递了外部组件的IUnknown指针。
5) B组件的类厂的CreateInstance方法内部创建CB类时,将IUnknown* pUnkownOuter参数传递给构造函数。创建成功后,调用B组件自身的(而不是外部的)NondelegationQueryInterface方法,将自身的INondelegationUnknown传递出去给组件A。
 
 
       外部组件A要创建组件B的实例,并保存B组件的INondelegationUnknown接口指针。该接口指针是通过上一节5)由B的类厂返回的。m_pUnknownInner变量保存了B接口的INondelegationUnknown接口指针。外部组件调用CoCreateInstance函数创建聚合组件时,iid必须等于IID_IUnknown。
       外部组件要修改自己的QueryInterface,当客户程序请求IB接口的时候,将调用m_pUnknownInner->QueryInterface方法。
       外部组件可以通过QueryInterface的代码来控制是否聚合B组件所有的接口。如果不想聚合B组件实现的IC接口。可以在输入参数iid等于IID_IC的时候,返回E_NOINTERFACE。
       聚合B组件的所有接口的方式称为盲聚合。盲聚合的缺点是:如果B组件实现了IPersist接口,客户调用IPersist::GetClassID方法时,获得的是CLSID_CB,这就暴露了内部的B组件;还有就是B组件实现的接口不了解A组件的状态,如果B组件实现了ISave接口,客户调用了它,却不能正确完成保存组件状态的功能。所以,一般我们要避免使用盲聚合,而要有选择的聚合。
       外部组件还有一个重要的任务就是控制内部组件的生命周期。因为内部组件拥有外部组件的IUnknown指针,这时候当调用内部组件的AddRef/Release,改变的是外部组件的引用计数。所以如果需要减少引用计数,应该调用pUnknowOuter(pUnknowOuter其实就是this指针的强制转换)->Release( )。CA类析构时,要采用以下特殊的做法保证不会被过早的析构和多次释放。
       m_cRef=1;
       IUnknown* pUnknownOuter = this;
       pUnknownOuter->AddRef( );
       m_pIB->Release( );
需要注意的是CA的析构函数是在A组件自身的引用计数为0时才会被调用,如果没有m_cRef=1这行代码,m_pIB->Release( )会导致析构函数再次被调用。
`       最后,析沟函数将释放组件B。通过调用B的INondelegationUnknown::Release( )方法。
代码如下:
       if(m_pUnknownInner!=NULL)
{
       m_pUnknownInner->Release ( );
}
 
以上总结主要来自于<<COM技术内幕>>。但是在ATL中,一切都被隐藏的很多。
ATL7对内部组件的支持
        使用ATL7.1创建ATL简单对象(传统COM对象时)向导中可以选择是否支持聚合,默认是支持的。我们现在开始创建内部组件。
 
 
   
 
 
      
 
      
 
      
 
       按照上图步骤创建组件B,并且实现了IB接口。现在我们为IB增加方法F( )。F()方法很简单,只是弹出一个消息框。
       STDMETHODIMP CB::F(void)
{
          MessageBox(NULL,"OK","OK",MB_OK);
          return S_OK;
}
 
DECLARE_CLASSFACTORY()-------------
ATL中,类厂是由CComCoClass类的宏DECLARE_CLASSFACTORY()实现的。该宏的定义为: #define DECLARE_CLASSFACTORY() DECLARE_CLASSFACTORY_EX(ATL::CComClassFactory)
CComClassFactory实现了 IClassFactory接口。最主要的就是实现了创建组件的函数。
// IClassFactory
     STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid, void** ppvObj)
     {
         ATLASSERT(m_pfnCreateInstance != NULL);
         HRESULT hRes = E_POINTER;
         if (ppvObj != NULL)
         {
              *ppvObj = NULL;
              // can't ask for anything other than IUnknown when aggregating
              if ((pUnkOuter != NULL) && !InlineIsEqualUnknown(riid))
              {
                   ATLTRACE(atlTraceCOM, 0, _T("CComClassFactory: asked for non IUnknown interface while creating an aggregated object"));
                   hRes = CLASS_E_NOAGGREGATION;
              }
              else
                   hRes = m_pfnCreateInstance(pUnkOuter, riid, ppvObj);
         }
         return hRes;
}
m_pfnCreateInstance是创建 COM对象的函数的指针。创建CB类的函数指针是被保存到对象映射表中的用于创建CB类的函数指针。对象映射表是一个_ATL_OBJMAP_ENTRY结构数组,该结构数组还保存了创建类厂的函数指针。该结构数组的初始化是通过B.h中的宏OBJECT_ENTRY_AUTO(__uuidof(B), CB)来完成的。该宏取代了ATL3中的BEGIN_OBJECT_MAP()/OBJECT_ENTRY()/END_OBJECT_MAP()宏。
 
DECLARE_CLASSFACTORY_EX定义为 #define DECLARE_CLASSFACTORY_EX(cf) typedef ATL::CComCreator< ATL::CComObjectCached< cf > > _ClassFactoryCreatorClass;
CComObjectCached类派生自CComClassFactory,该类的作用是使得组件类的生命周期和服务器生命周期相同,所以适用于进程内组件。CComCreator类只提供了一个CreateInstance函数,用来帮助我们创建一个组件类,这里就帮助我们创建类厂。
所以宏DECLARE_CLASSFACTORY()的功能就是:
1) 通过从 CComClassFactory类派生实现了IClassFactory接口
2) 使用 CComObjectCached类指定了类厂对象的生命周期和服务器生命周期相同
3) 定义了 _ClassFactoryCreatorClass类型,该类型的CreateInstance函数可以创建类厂对象,而且是采用多步构造的方式,防止了构造期间的意外析构。
 
 
DECLARE_AGGREGATABLE(T)---------------------
该宏的定义如下:
#define DECLARE_AGGREGATABLE(x) public:/
     typedef ATL::CComCreator2< ATL::CComCreator< ATL::CComObject< x > >, ATL::CComCreator< ATL::CComAggObject< x > > > _CreatorClass;
    _CreatorClass类型的 CreateInstance函数即可以创建独立组件,也可以创建聚合组件,取决于传入的pOuterIUnknown参数是否为NULL。
值得一提的是 CComAggObject类通过两次从CComObjectRootEx继承,实现了内部组件的两个IUnknown接口,一个是IUnknown*,一个是INondelegationUnknown*(其实ATL中不使用INondelegationUnknown名字,但是作用一样和INondelegationUnknown一样)。
如果我们在向导中选择不支持聚合,向导将会在我们的CB类内加上宏 #define DECLARE_NOT_AGGREGATABLE(x)
#define DECLARE_NOT_AGGREGATABLE(x) public:/
     typedef ATL::CComCreator2< ATL::CComCreator< ATL::CComObject< x > >, ATL::CComFailCreator<CLASS_E_NOAGGREGATION> > _CreatorClass;
这样 _CreatorClass类型的CreateInstance函数当接收到不为NULL的pOuterIUnknown参数时会失败。
 
所以,当我们有了 ATL的支持后,我们创建内部组件的一切工作只是在向导中选择支持聚合,轻松惬意!
ATL7对外部组件的支持
       现在我们另创建一个名为OuterComponent的ATL项目,创建A组件并实现IA接口。创建步骤就不再叙述。A组件是否支持聚合无关紧要,我们这里就默认支持吧。在OuterComponent项目中导入InnerComponent.dll,然后将 CComPtr<IUnknown> m_pIB作为CA的成员变量。请参考下面的代码的注释
// A.h : CA 的声明
 
#pragma once
#include "resource.h"       // 主符号
 
#include "OuterComponent.h"
 
 
// CA
 
class ATL_NO_VTABLE CA :
     public CComObjectRootEx<CComSingleThreadModel>,
     public CComCoClass<CA, &CLSID_A>,
     public IDispatchImpl<IA, &IID_IA, &LIBID_OuterComponentLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public :
     CA()
     {
     }
 
DECLARE_REGISTRY_RESOURCEID(IDR_A)
 
BEGIN_COM_MAP(CA)
     COM_INTERFACE_ENTRY(IA)
     COM_INTERFACE_ENTRY(IDispatch)
     COM_INTERFACE_ENTRY_AUTOAGGREGATE(IID_IB,m_spunkIB.p,CLSID_B)// 自动聚合IB接口,有了该宏就不需要在FinalConstruct函数中创建B对象了
END_COM_MAP()
 
     DECLARE_GET_CONTROLLING_UNKNOWN()// 创建虚函数IUnknown* GetControllingUnknown(),CComContainedObject类会继承并实现该函数,然后将返回最外层组件的IUnknown指针
     DECLARE_PROTECT_FINAL_CONSTRUCT()
     HRESULT FinalConstruct()
     {
         return S_OK;
     }
    
     void FinalRelease()
     {
         m_spunkIB.Release();// 避免两次析构内部组件
     }
private :
     CComPtr<IUnknown> m_spunkIB;// 保存被聚合的内部组件的IUnknown接口
};
OBJECT_ENTRY_AUTO(__uuidof(A), CA)
 
   
    现在我们来用一下,创建一个客户程序,它导入 A和B的组件类型库,然后创建A组件,然后查询IB接口并调用IB::F()方法。
 
// Client.cpp : 定义控制台应用程序的入口点。
//
 
#include "stdafx.h"
#import "../../OuterComponent/OuterComponent/debug/OuterComponent.tlb" no_namespace named_guids raw_interfaces_only
#import "../../InnerComponent/InnerComponent/debug/InnerComponent.tlb" no_namespace named_guids raw_interfaces_only
 
 
int _tmain(int argc, _TCHAR* argv[])
{
     ::CoInitialize(NULL);
     CComPtr<IA> pA;
     HRESULT hr=pA.CoCreateInstance(CLSID_A);
     CComPtr<IB> pB;
     hr=pA->QueryInterface(&pB);
     pB->F();
     pB.Release();
     pA.Release();
     ::CoUninitialize();
     return 0;
}
 
一切成功。 ATL大大简化了我们对于聚合的使用。
 
陈抒
2005-9-15
 
 
原文链接:http://blog.csdn.net/sheismylife/article/details/1154730
加载中
返回顶部
顶部