1、C+类的动态组件化技术0640303401 蒋浩平关键词COM 组件 接口 生命周期 C+类 ATL 组件类 C+基类 ATL 模板基类 继承摘要在组件化编程的时代,如何复用历史累积的大量没有组件特性的 C+类?本文从工程的角度对这一问题进行探讨,利用现有组件技术,提出了一套将 C+类平滑过渡到 COM组件的完整解决方案。1. 问题的提出自从 Microsoft 公布了 COM(Component Object Model,组件对象模型,简称 COM)技术以后,Windows 平台上的开发模式发生了巨大的变化,以 COM 为基础的一系列组件技术将 Windows 编程带入了组件化时代,传统的面
2、向对象的软件开发方法已经逐渐被面向组件的方法所取代。COM 标准建立在二进制可执行代码级的基础上,不论何种工具、语言开发的组件,只要符合 COM 规范,就可复用于 VC、VB、Delphi、BC 等各种开发环境中。 COM 的语言无关性将软件复用的层次从源代码级推进到了二进制级,复用更方便,也更安全。然而,COM 技术带来全新的软件设计和开发模式的同时,也带来了新的问题。许多软件公司在开发自己的软件产品过程中,都累积了大量 C+类,这些代码设计精良,功能完备,以面向对象的标准来检验无可挑剔。然而,这些代码不支持 COM,将无法在 COM 时代继续被复用。如果它们在软件组件化的趋势中被淘汰,那对
3、软件公司和开发人员来说都是极大的损失。COM 专家 Don Box 曾说过, “COM is a super C+”。这给了我们一个启示,是否可以实现一种技术,能够动态的为普通 C+类加上一层 COM 的封装呢?这样,既可以保持这些代码自身的完整和特性,使它们能继续应用于原来的系统,也可以在需要作为组件使用的时候,把它们动态转变成组件,复用于新系统。一个自然而然的想法是,为每一个 C+类开发一个只暴露一个接口的 COM 组件,将原 C+类的每个 public 方法都对应于该接口的一个方法,接口方法的实现可以简单的调用相对应的 C+类方法即可。这样,程序逻辑由原有的 C+类控制,但 COM 层的
4、封装则由组件提供。基本思路如下图所示:调 用 调 用 ATL组 件 类 ( CATL) 调 用 调 用 暴 露 接 口 接 口 应 用 C+类 ( CImplent) Method1 Method2 public: Method1 public: Method2 本文就这一技术展开讨论,最终提供一套由普通 C+类平滑过渡到 COM 组件的完整解决方案。我们选用 ATL(Active Template Library,活动模板库,简称 ATL)作为 COM组件的开发工具,开发环境为 Visual Studio 6.0。如没有特殊说明,下文中的“C+类”指没有组件特性 C+类, “C+对象”指 C
5、+类的实例;“ ATL 组件类”指用于包装的 ATL类, “ATL 对象 ”指 ATL 组件类的实例。2. 用 ATL 包装 C+类按上述思路将 C+对象动态组件化后,所得的组件实际上由两部分组成:ATL 组件对象和绑定的 C+对象。两者的生命周期互相牵制,但要保持一致。生命周期的管理是 C+类动态组件化的首要难点。C+类分为两种,一种是简单的 C+类,一种是集合型的 C+类。集合型的 C+对象管理一组 C+对象,负责其创建和删除,维护它们的生命周期。下面,分别就简单 C+类和集合型 C+类的组件化技术进行说明,展示解决方案的核心技术。2.1.简单 C+类的组件化为使 ATL 组件类可以自由调
6、用 C+类的方法,需要: 为 ATL 组件类安插一个指针成员变量,指向 C+类 提供 ATL 对象和 C+对象的绑定机制我们可以在 ATL 组件类初始化时创建一个 C+类,用成员变量 m_pCPPObj 记录,在析构时删除,从而实现 ATL 组件类和 C+类的天然绑定。但出于灵活性考虑,使得 ATL组件对象可以绑定任意 C+类的对象,我们为 ATL 组件类添加一个绑定函数Link2CPPObj(CImplement* pObj) 。在 ATL 组件类的构造函数内,创建一个 C+对象,用 m_pCPPObj 记录。如果调用了 Link2CPPObj,则将 m_pCPPObj 指向的对象删除,改用
7、传入的 C+对象。在 ATL 组件类的的析构函数内,删除其绑定的 C+对象。由构造函数和 Link2CPPObj函数的定义可知,m_pCPPObj 指针总是有意义的。简单 C+类组件化的思想如下图所示:指 向 调 用 调 用 暴 露 接 口 ATL组 件 类 ( CATL) 接 口 ( Itf) Method1 Method2 CImplent* m_pCPObj; Link2CPObj(CImplent* pObj); 应 用 C+类 ( Implent) public: Method1 public: Method2 2.2.集合型 C+类的组件化集合型 C+类的情况有所不同。集合型 C+
8、类以数组(array ) 、列表(list) 、映射表(map)的形式管理其它 C+对象。集合对象和它管理的元素对象都被包装成组件后,集合型 ATL 对象可能调用一个“Destroy”方法,期望删除某一个元素 ATL 对象;这一操作的实质却是,集合型 C+对象的“Destroy ”方法被调用,将元素 C+对象删除了,而元素 ATL 对象却不知道。这一操作的结果导致了元素的 ATL 对象存在,而其绑定的 C+对象却被删除的情况,两者的生命周期出现了不一致。为了解决这个问题,我们需要在 C+对象被删除时,能将 ATL 对象同时删除;而在ATL 对象的引用计数为 0 需要删除自身时,也能把 C+对象
9、删除。可行的解决方案是: 在 C+类中保存一个接口指针,指向绑定在一起的 ATL 对象;为该接口指针赋值的最佳地点显然是提供绑定机制的 Link2CPPObj 函数内部,为此,还需要给Link2CPPObj 添加一个 IUnknown*参数 在 C+类的析构函数中,判断该接口指针是否为空,如果不为空,则 Release 对接口的引用,引发 ATL 对象自身的析构现在,技术方案如下图所示:指 向 指 向 调 用 调 用 暴 露 接 口 ATL组 件 类 ( CATL) 接 口 应 用 C+类( CImplent) Method1 Method2 public: Method1 public: M
10、ethod2 CImplent* m_pCPObj; Link2CPObj(CImplent* pObj, IUnkown* pUnk); IUnkown* m_pAsociATLUnk; CImplent(); 2.3.内部创建的组件和外部创建的组件集合型 C+类组件化后仍然是集合型 ATL 组件,它可以创建、删除自己管理的组件。这样,组件的创建就可能有两种情况: 由客户直接创建 由客户调用集合型组件的接口方法间接创建创建方式的不同导致了组件生命周期管理的复杂性。一般说来,组件的创建者负责维护组件的生命周期。上述两种情况下,分别由客户和集合型组件维护被创建组件的生命周期。然而,另有一种情况是
11、,客户创建了一个组件,然后送交一个集合型组件管理,现在维护组件生命周期的责任就由客户转交给了集合型组件。我们的解决方案必须提供这样的健壮性和灵活性,以维护各种情况下组件的生命周期。我们为 ATL 组件类添加一个 BOO 成员 m_bInnerManage,作为组件的维护标识。内部维护意味着组件的生命周期由其它组件(集合型组件)维护;外部维护则是由客户维护。指 向 指 向 调 用 调 用 暴 露 接 口 ATL组 件 类 ( CATL) 接 口 应 用 C+类( CImplent) Method1 Method2 public: Method1 public: Method2 CImplent*
12、 m_pCPObj; Link2CPObj(CImplent* pObj, IUnkown* pUnk); IUnkown* m_pAsociATLUnk; CImplent(); BOL m_bInerMange; 缺省情况下,组件是外部创建并维护的,在组件的构造函数内设置外部维护标识。集合型组件创建元素时,需要为元素分别创建一个 C+对象和一个 ATL 对象,然后调用ATL 对象的 Link2CPPObj 函数将两者绑定在一起,在 Link2CPPObj 函数内修改维护标识。对于第三种情况,可以在外部创建组件由客户转交给集合型组件时,在集合型组件相应方法内重新设置维护标识。2.4.C+基类
13、为了对现有 C+类的改动最小,我们设计一个基类封装需要为 C+类添加的功能。所有需要动态组件化的 C+类都必须从这个基类派生,以保证动态组件化中 C+对象与 ATL对象生命周期的一致。如下图示:指 向 指 向 调 用 调 用 派 生 C+基 类 ( CPBase) IUnkown* m_pAsociATLUnk; 暴 露 接 口 ATL组 件 类 ( CATL) 接 口 应 用 C+类( CImplent) Method1 Method2 public: Method1 public: Method2 BOL m_bInerMange; Link2CPObj(CImplent* pObj, I
14、Unkown* pUnk); CPBase(); CImplent* m_pCPObj; 实现代码如下所示:class CCPP2ATLObjBaseCCPP2ATLObjBase ();public:/ IUnknown 指针,反指向封装该 CPP 类的接口IUnknown* m_pAssociATLUnk;protected:virtual CCPP2ATLObjBase ();CCPP2ATLObjBase:CCPP2ATLObjBase()/ 将 IUnknown 指针初始化为 0m_pAssociATLUnk = NULL;CCPP2ATLObjBase:CCPP2ATLObjBas
15、e()/ CPP 类的对象析构时,Release 对接口的引用if (m_pAssociATLUnk)m_pAssociATLUnk-Release();然后,修改现有各个 C+类,使之从 CCPP2ATLObjBase 派生,如下面代码片断所示:class CImplement : public CCPP2ATLObjBase ;必须指出的是,在 CCPP2ATLObjBase 基类中,我们设置的 m_pAssociATLUnk 变量存在和现有 C+类成员命名冲突的问题。但是,考虑到原 C+类并没有组件特性,也应该不会有“IUnknown ”型指针,因此,只要各个类的变量命名都按照规范的命名
16、法,出现这种名字冲突的可能性是极小的。2.5.ATL 模板基类通过以上分析,我们发现,所有的 ATL 组件类都需要实现一些相同的功能: 保留一个指向其绑定 C+对象的指针 提供一个 Link2CPPObj 函数 在构造函数中创建一个绑定 C+类的对象为了减化编码,我们定义一个带参数的模板基类,实现上述公共功能,模板参数就是绑定的 C+类。然后,所有的 ATL 组件类都从模板基类中派生。现在的技术方案如下图所示: 指向 派 生 指向 调 用 派 生 模 板 参 数 C+基 类 ( CPBase) IUnkown* m_pAsociATLUnk; 暴 露 接 口 ATL组 件 类 ( CATL)
17、接 口 ( Itf) 应 用 C+类 ( Implent) Method public: Method CPBase(); ATL模 板 基 类 ( CP2ATLemplateBase) BOL m_bInerMange; Link2CPObj(CImplent* pObj, IUnkown* pUnk); CImplent* m_pCPObj; 实现代码如下所示:template class CCPP2ATLTemplateBase : protected:/ C+类指针T* m_pCPPObj;/ 标识继承该模板的 ATL 对象是否由内部维护BOOL m_bInnerManage;publ
18、ic:/*模板的构造函数,实现如下功能:1、new 一个 C+实现类对象2、缺省情况下,ATL 对象由外部维护,将内部维护标识设为 FALSE3、将 C+类中对 ATL 接口的反指指针设置为空*/CAtlCPP2ATLTemplateBase()m_pCPPObj = new T;m_bInnerManage = FALSE;m_pCPPObj-m_pAssociATLUnk = NULL;/*析构 ATL 对象时,如果该 ATL 对象是由外部创建的,则显式的删除 C+对象如果 ATL 对象由内部维护,那么什么事都不用做*/virtual CAtlCPP2ATLTemplateBase()if
19、 (!m_bInnerManage) if (m_pCPPObj)delete m_pCPPObj;/*Link2CPPObj 函数,负责绑定 C+对象和 ATL 接口1、删除构造函数中 new 的 C+对象,而使用外部传入的 C+对象2、将 ATL 对象的内部维护标识设为 TRUE3、设置 C+基类中的接口指针成员4、因为 ATL 接口传送给外部使用,需要增加引用计数*/virtual void Link2CPPObj(T* pObj, IUnknown* pUnk)ASSERT(pObj != NULL);ASSERT(pUnk != NULL);if (m_pCPPObj)delete
20、m_pCPPObj;m_pCPPObj = pObj;m_bInnerManage = TRUE;m_pCPPObj-m_pAssociATLUnk = pUnk;m_pCPPObj-m_pAssociATLUnk-AddRef();然后,每个 ATL 类都从该模板类派生,如下代码片断所示:class ATL_NO_VTABLE CATLXX : ,/ 添加 ATL 模板基类public CCPP2ATLTemplateBase3.C+参数类型的自动化包装在本文的技术方案中,C+类的 public 方法与 ATL 组件接口中的方法一一对应;相应的,C+类中方法的参数类型也要转换为 COM 规范
21、所允许的数据类型。在基于 COM 的自动化(Automation)技术中,Microsoft 提供了一套自动化兼容的数据类型 VARIANT,定义如下:typedef struct FARSTRUCT tagVARIANT VARIANT;typedef struct FARSTRUCT tagVARIANT VARIANTARG;typedef struct tagVARIANT VARTYPE vt;unsigned short wReserved1;unsigned short wReserved2;unsigned short wReserved3;union Byte bVal; /
22、 VT_UI1.Short iVal; / VT_I2.long lVal; / VT_I4.float fltVal; / VT_R4.double dblVal; / VT_R8.VARIANT_BOOL boolVal; / VT_BOOL.SCODE scode; / VT_ERROR.CY cyVal; / VT_CY.DATE date; / VT_DATE.BSTR bstrVal; / VT_BSTR.DECIMAL FAR* pdecVal; / VT_BYREF|VT_DECIMAL.IUnknown FAR* punkVal; / VT_UNKNOWN.IDispatch
23、 FAR* pdispVal; / VT_DISPATCH.SAFEARRAY FAR* parray; / VT_ARRAY|*.Byte FAR* pbVal; / VT_BYREF|VT_UI1.short FAR* piVal; / VT_BYREF|VT_I2.long FAR* plVal; / VT_BYREF|VT_I4.float FAR* pfltVal; / VT_BYREF|VT_R4.double FAR* pdblVal; / VT_BYREF|VT_R8.VARIANT_BOOL FAR* pboolVal; / VT_BYREF|VT_BOOL.SCODE FA
24、R* pscode; / VT_BYREF|VT_ERROR.CY FAR* pcyVal; / VT_BYREF|VT_CY.DATE FAR* pdate; / VT_BYREF|VT_DATE.BSTR FAR* pbstrVal; / VT_BYREF|VT_BSTR.IUnknown FAR* FAR* ppunkVal; / VT_BYREF|VT_UNKNOWN.IDispatch FAR* FAR* ppdispVal; / VT_BYREF|VT_DISPATCH.SAFEARRAY FAR* FAR* pparray / VT_ARRAY|*.VARIANT FAR* pv
25、arVal; / VT_BYREF|VT_VARIANT.void FAR* byref; / Generic ByRef.char cVal; / VT_I1.unsigned short uiVal; / VT_UI2.unsigned long ulVal; / VT_UI4.int intVal; / VT_INT.unsigned int uintVal; / VT_UINT.char FAR * pcVal; / VT_BYREF|VT_I1.unsigned short FAR * puiVal; / VT_BYREF|VT_UI2.unsigned long FAR * pul
26、Val; / VT_BYREF|VT_UI4.int FAR * pintVal; / VT_BYREF|VT_INT.unsigned int FAR * puintVal; / VT_BYREF|VT_UINT.;我们看到,所有简单数据类型都可以在 VARIANT 中找到对应的定义,但是,在多数的基于 C+的系统设计中,方法参数不会仅仅出现简单数据类型,类对象、对象引用、对象指针被频繁的作为参数来传递。以类对象、对象引用或对象指针形式存在的参数,我们称为复杂类型参数。在技术方案中,所有复杂类型参数在 ATL 接口方法中一律对应接口指针,我们需要提供 C+对象(或引用、指针)和 ATL 接口
27、指针之间的动态转换功能。下文就复杂类型作为传入、传出参数分别进行讨论。3.1.复杂类型的传入参数ATL 接口方法获取一个接口指针参数后,如何将此接口指针转变为 C+对象指针?对于 ATL 对象,可以直接取得 m_pCPPObj 变量,而接口指针却不能。所以,需要提供一种途径,从 ATL 接口指针获取 ATL 组件的 m_pCPPObj 变量值。我们的设计是,为每个 ATL 组件提供一个基接口 ICPPObjSeeker,实现对绑定 C+对象指针(即 m_pCPPObj)的查询方法 HandleCPPObj。任意 ATL 接口都从该基接口派生,都可以调用 HandleCPPObj 方法。在前文就生命周期管理进行讨论时,曾提到这样一种情况:客户创建了一个组件,然