BREW与面向对象的比较

来源:互联网 发布:条形码数据库手机软件 编辑:程序博客网 时间:2024/05/16 13:45

BREW与面向对象的比较

毛晓冬 2007-6-13

一、   概述:

虽然BREW的整体框架是基于接口体系,是面向对象的。但是和C++,C#,Java不同,这些语言的面向对象是语言(编译器)层次上的支持,而BREW的面向对象是实现层次上的支持(BREW的框架是以纯C实现面向对象的体系)。由于支持的层次不一样,所以仍然存在着很大的差异性。

我们有必要了解这些差异性,不能直接拿传统面向对象语言的特性来同等的看待BREW,那样有时可能存在偏差,可能影响我们对BREW的理解。

二、   详细比较:

1、           类、接口的支持:

C++,C#,Java等面向对象语言在语言(编译器)层次上支持类,接口。即在传统的数据结构中引入了函数成员,形成了特殊的新类型:类。

BREW环境由C语言编写,C在语言层次上并不支持在结构中包含函数。BREW采用函数指针的方式将函数加入到结构中,形成了虚表-接口。而BREW中的类,只是将虚表作为第一成员的一个普通结构体而已:成员函数+私有数据成员。

由于BREW中的成员函数是以函数指针的方式引入的,所以调用者必须以函数指针的方式间接的进行访问,不过这一切BREW都为我们做好了。BREWIXXX_XXX(,,,)的宏就是完成这个目的的。

 

2、           封装性:

面向对象语言的封装性在语言(编译器)层次上支持,实现者可以将类的空间信息暴露给调用者,而只需要在类的成员前加以访问修饰符(PrivatePublicProtect)。就可以由编译器完成对调用者的权限限制。

BREW不支持语言层次上的封装性。BREW中的封装,其实是将类的空间信息完全向使用者隐藏,使用者唯一可见的只有虚表。由此,在编译器层次上达到对使用者的封装性。

类的空间信息对类的实现者开放,类的实现者需要访问类的空间信息(数据成员)以完成类的成员函数的实现。

在运行时,创建的是类的实体,返回给使用者的是虚表(接口)的实体,使用者被限制只允许访问虚表。而接口(虚表)和类的关系(首地址相同),保证了他们具有安全的转换特性,当转换发生后,访问的内存区域就变化了,封装的界限也同时发生了变化。

成员函数的实现中,将传入的接口(虚表)指针转换成类的指针,以完成对类空间的完全访问,从而完成了封装-》开放的转换,接口(虚表)和类的关系是BREW中联系封装与开放的桥梁。

从上面的分析看,BREW中的封装,其实更象C#,Java中的接口提供的封装。

 

3、           继承性:

面向对象语言中,类的继承是“实现”的继承,这是由编译器支持的,即,子类继承父类后,默认就包含了所有父类的成员函数,同时包含了这些成员函数的实现,除非子类重写覆盖了这些成员函数。

但是BREW中的继承却大不相同,其实,BREW中的继承叫做“代码重写”更加合适,因为那只是通过定义的宏来方便重写虚表,而没有继承任何的“实现”。

所以,BREW中的类必须对继承的所有接口的函数(虚表)一一实现。否则这些接口函数(虚表的函数指针)在运行时是未定义的。

值得注意的是,上面提到的“BREW中的类必须对继承的所有接口的函数(虚表)一一实现”,是一一“实现”,而并非一一“重写”。这是因为,在子类的Ctor函数中,我们有时会直接调用父类的Ctor初始化父类的虚表,然后再初始化子类的虚表。典型的比如BUIW中的WidgetContainer等。Anyway,这仅仅是人为的在实现层次上进行特殊处理以模拟面向对象中的对“实现”的继承。而真正的面向对象语言中的继承,是在语言(编译器)层次上支持的。即只要A类继承B类,那么A类不需要写任何一行代码,任何特殊处理,A类被编译器New的时候,就默认继承了B类的所有实现。所以,BREW中的继承,本质上只是对接口的继承,而非对实现的继承。

此外,在面向对象语言中,是有途径分别访问子类的实现和父类的实现的。但是在BREW中,永远只能访问子类的实现,而不管使用的是什么访问宏。例如:使用IBASE_Release可以统一的调用到任何接口的Release实现,因为任何接口都是继承至IBASE接口,而真正IBASE_Release(或者是父类宏)调用的实现,是由IBASE_Release的第一个参数(对象指针)来决定的。

从上面的分析看,BREW中的继承有时更像C#,Java中类对接口的继承。

 

4、           多态性:

面向对象语言的多态性是由编译器的晚绑定来实现的,需要在实现多态的成员函数前加入Virtual关键字,以告知编译器该函数采用晚绑定的方式,对于晚绑定的函数,编译器会转换为函数指针的间接调用,而该函数指针在运行时被绑定,我们称之为虚函数。

相比较而言,BREW中的多态就是天生具有的,这是由BREW的接口,类的结构所决定的。因为在BREW中,所有成员函数的调用,都是通过函数指针的方式间接调用的。而这些函数指针(虚表)的绑定,是在运行时完成的。因此,我们可以使用统一的父类宏,对一组继承至相同父类的子类实体进行操作,其缘由就是因为多态。几个例子:ITextCtlIMenuIStatic都继承至IControl接口,我们可以使用IControl_HandleEvent统一的访问具体的HandleEvent,这是在运行时由IControl_HandleEvent的第一个参数,对象指针所决定的。再比如,对于Jpegbmppnggif格式的图片,我们都是采用统一的IIMAGE方式访问,其实在运行时都会多态的映射到具体实体的实现中,IMedia也如此。

由上分析可见,BREW中的多态其实更象C#,Java中的接口。

 

5、           CISIDInterfaceID

这是COM中的概念。COM中的一个组件(类)继承至多个接口。客户只能通过接口间接的得到组件的服务。并且,通过不同的接口可以得到不同的服务。组件通过接口对外暴露服务。

CLSID即是唯一的标识这个组件,而Interface ID则是唯一的标识组件中的每个接口。当组件实体被创建后,客户就可以通过Interface ID查询出不同的接口,从而获得不同的服务。

BREW中,其实CLSID也是唯一的标识一个“类”,因为IShell_CreateInstance是对类进行创建。但是我们往往看不到Interface ID,这是因为在BREW中,大部分的类只继承至一个接口,也即只存在一份虚表,且该虚表和类的首地址一致。此时接口(虚表)和类之间是可以直接转换获得的。所以,这种情况下,Interface ID也就不需要了,只需要进行类到接口的指针转换就可以了。

但是,对于一小部分包含多个接口(多份虚表)的类,那么Interface ID仍然存在,此时Interface ID通常用于查询出后面的接口(虚表),因为该接口(虚表)和类不是首地址一致,所以通过转换是无法获得的。而对于第一个接口(第一份虚表),则是不需要Interface ID的,因为它可以直接和类指针转换获得。

其实,我们通过CLSID创建返回得到的接口实例,永远都是类的第一个接口(第一份虚表),对于类的其他接口(其他份虚表,如果存在),必须通过Interface ID进行查询获得,BREW中通过QueryInterface来查询接口,即,BREW中支持多接口的组件(类)必须支持QueryInterface成员函数,即,必须继承至IQueryInterface接口。

对于一些可能需要扩展的接口,往往都继承至IQueryInterface,比如IMedia,尽管当前的版本还不真正的支持多接口。

通过QueryInterface机制,可以使得在扩展功能的同时,保持向前的二进制层次兼容。

BREW中的QueryInterface机制满足COM中的QueryInterface的准则,比如A接口查询出B接口,那么B接口也可以查询出A接口 等等。

BUIW中的Container是一个很好的例子,其包含两个接口(两份虚表),一个是IContainerVTBL,一个是IWidgetVtblIWidget接口就是通过以AEEIID_WIDGETInterfaceID)方式查询(调用QueryInterface)得到。

 

6、           对象的创建和释放:

面向对象语言中,对象的创建和释放使用NEWDelete,以保证对象的额外初始化(构造函数),额外清理工作(析构函数)得以完成。

BREW接口体系中提供的对象,为了完成额外的初始化,实现者提供一个XXX_New函数,完成对象的创建和必要的初始化。同时IBASE具有的Release函数,保证了对象的释放和必要的清理工作。同时XXX_New函数对使用者屏蔽,以获得完全意义上的接口与实现分离。这是通过工厂模式实现的,即使用者调用IShell_CreatInstance(工厂方法)并传入CLSID以获得接口实例。

BREW中的接口创建和释放,更象COM中的接口创建和释放。

 

7、           引用计数:

COM中引入了引用计数的概念,那是为了解决组件(类)实例,被多个客户同时使用时的问题。COM中的组件以动态连接库的形式存在,所以只会被加载一次,也即只会存在组件的同一实例。那么多个客户同时使用时,何时真正释放实例(从内存中卸载)?如果采用传统的方式由客户决定释放,比如Delete,那么问题肯定是存在的,因为客户之间是信息屏蔽的,客户A不可能知道客户B也在使用,那么客户A释放组件就会造成对客户B的灾难!

所以,只能由组件自身来决定何时释放自己。引用计数应运而生。每个客户使用组件时,引用计数加1,释放组件时引用计数减1,当引用计数减到0时,表示当前没有任何客户再需要使用该组件,那么组件自身进行释放。

BREW中也引入了引用计数的概念。需要注意的是,BREW中大部分接口是多实例的,所以BREW中的引用计数是针对“同一接口实例”的!是同一接口实例在多个客户间使用的解决方案。不同的接口实例间的引用计数,没有任何关系。

BREW中的引用计数的使用规则,基本和COM类似,通常的准则是: 通过出口参数获得接口实例的,引用计数已经被加1,使用完毕后,客户需要显式的进行Release,通过返回值获得接口实例时,引用计数并没有加1,客户使用完毕后无需Release。通过入口参数输入接口实例时,接收方必须对输入的接口实例负责,即其会进行AddRef,以防止外部的Release造成的影响,所以客户输入实例后,任何不需要的时候都可以释放。

关于BREW引用计数的个别特殊情况,请查阅SDK中的某些接口的特殊说明。

由上分析,BREW借用了COM的引用计数,解决了同一实例多个客户使用中的问题。

 

8、           多重继承:

C++中支持多重继承,那是在语言(编译器)层次上支持,当接口间转换时,编译器自动完成接口(虚表)间的地址空间的转换。

BREW中的多接口继承是通过QueryInterface模式实现的。因为语言层次上不支持多个虚表间的直接转换,所以只能通过QueryInterface,根据Interface ID进行查询。

BREW中是通过AEEBase来巧妙的关联查询出的接口实例和组件(类)实例的,之所以需要关联,是因为查询出的接口实例的成员函数的实现中往往需要访问组件(类)实例的成员,因为查询出的接口从属于组件,是为组件向外提供服务的,所以其必须能访问组件的地址空间。

但是在BREW中,默认的对外接口其实就是一份虚表,而查询出的接口(虚表)默认和组件不共首地址,所以不可能进行转换。所以需要增加对组件实例的引用。

#define AEEBASE_INHERIT(abc,derived)      const AEEVTBL(abc) *pvt;/                                           derived       *pMe

typedef struct AEEBase {

  AEEBASE_INHERIT(IQueryInterface, IQueryInterface);

} AEEBase;

可见AEEBase其实就是vtable+ object pointer,我们再看ContainerBase的类定义:

struct ContainerBase

{

  AEEVTBL(IContainer) *   pvt;

  uint32                  nRefs;

  IModule *               piModule;

  IModel *                piModel;

  WExtent                 extent;

  IContainer  *           piParent;

  IWidget                 widget;

  AEEVTBL(IWidget)        vtWidget;

……

注意红色加粗的IWidget,该类型在ContainerBase中被重定义如下:

struct IWidget {

  AEEBASE_INHERIT(IWidget, ContainerBase);

};

现在清楚了吧,ContainerBase中包含的IWidget,其实是IWidgetVtbl+ContainerBase的引用。该引用在ContainerBase的实例化过程中被指向ContainerBase实例本身,而该IWidgetVtblContainerBase实例化过程中被实例化为ContainerBase_XXX,和WidgetBase无任何关系!

我们再看ContainerBase_QueryInterface

intContainerBase_QueryInterface(IContainer *po, AEECLSID id, void **ppo)

{

   BASE_FROM_CONTAINER;

 

  if ((id == AEECLSID_QUERYINTERFACE)

       || (id == AEEIID_CONTAINER)) {

     *ppo = po;

      ICONTAINER_AddRef(po);

      return SUCCESS;

  } else if ((id == AEEIID_WIDGET)

       || (id == AEEIID_HANDLER)) {

      *ppo = &me->widget;

      ICONTAINER_AddRef(po);

      return SUCCESS;

  } else {

      *ppo = 0;

      return ECLASSNOTSUPPORT;

  }

}

可见,对于AEEIID_WIDGET的查询,组件返回的即是AEEBase类型的widget成员,注意,该成员包含vtble+object point,但是对于使用者而言,其被限制只能访问首地址相同的vtble,即接口。而对于查询出的IWdiget接口的实现,则可以访问全部,如下:

voidContainerBase_GetExtent(IWidget *po, WExtent *pwe)

{

   BASE_FROM_WIDGET;

 

  *pwe = me->extent;

}

这是查询出的IWidget接口的GetExtent的实现,首先po从被限制为只能访问虚表的指针转换为访问AEEBase类型的指针(这是由于IWidget对于ContainerBase.c而言,被重定义了,定义为AEEBase类型)。BASE_FROM_WIDGET定义为:

#define BASE_FROM_WIDGET    ContainerBase *me = (ContainerBase*)po->pMe

这是获取组件实例的引用。接着 

 *pwe = me->extent; 就可以访问组件实例的全部地址空间了。

AEEBase类型被广泛的应用于BREW中支持多接口类的实现,这也几乎提供了一种模板,我们如果需要自己实现多接口的组件,就可以使用它。

最后需要提一点:QueryInterface出来的接口,从属于组件。其生命周期也从属于组件的生命周期,组件无效的话,其也就没有存在的价值了。所以,我们必须保证当从组件中查询出的接口还在使用时,组件不能被从内存中释放。为了保证这一点,QueryInterface的实现都是对原组件的引用计数加1,如下(红色加粗):

int ContainerBase_QueryInterface(IContainer*po, AEECLSID id, void **ppo)

{

   BASE_FROM_CONTAINER;

 

  if ((id == AEECLSID_QUERYINTERFACE)

       || (id == AEEIID_CONTAINER)) {

      *ppo = po;

      ICONTAINER_AddRef(po);

      return SUCCESS;

  } else if ((id == AEEIID_WIDGET)

       || (id == AEEIID_HANDLER)) {

      *ppo = &me->widget;

      ICONTAINER_AddRef(po);

      return SUCCESS;

  } else {

      *ppo = 0;

      return ECLASSNOTSUPPORT;

  }

}

同样的,查询得到的接口的AddRefRelease必须针对引用的组件的引用计数操作。

 

void ContainerBase_Ctor(ContainerBase*me, AEEVTBL(IContainer) *pvt,

                        IModule *piModule,PFNHANDLER pfnDefHandler,

                        PFNMKNODE pfnMkNode,PFNLAYOUT pfnLayout)

{

  me->pvt = pvt;

  me->nRefs = 1;

 

  me->piModule = piModule;

  ADDREFIF(piModule);

 

#define VT(name) pvt->name =ContainerBase_##name

  VT(AddRef);

  VT(Release);

  VT(QueryInterface);

  VT(Invalidate);

  VT(Insert);

  VT(Remove);

  VT(GetWidget);

  VT(Locate);

 

   AEEBASE_INIT(me, widget,&me->vtWidget);

 

#define WVT(name)me->vtWidget.name = ContainerBase_##name

  me->vtWidget.AddRef         = AEEBASE_AddRef(IWidget);

  me->vtWidget.Release        =AEEBASE_Release(IWidget);

  me->vtWidget.QueryInterface = AEEBASE_QueryInterface(IWidget);

如上可见,Container中查询得到的IWidgetAddRefReleaseQueryInterface实现分别是AEEBASE_AddRef(IWidget)AEEBASE_Release(IWidget)AEEBASE_QueryInterface(IWidget)。我们展开相应的宏:

 

#define AEEBASE_AddRef(abc)          ((uint32 (*)(abc *)) AEEBase_AddRef)

#define AEEBASE_Release(abc)         ((uint32 (*)(abc *)) AEEBase_Release)

#defineAEEBASE_QueryInterface(abc)  ((int(*)(abc *po, AEECLSID,void**)) AEEBase_QueryInterface)

再看AEEBase_AddRefAEEBASE_ReleaseAEEBase_QueryInterface的实现:

uint32 AEEBase_AddRef(AEEBase *po)

{

  return IQI_AddRef(po->pMe);

}

 

uint32 AEEBase_Release(AEEBase*po)

{

  return IQI_Release(po->pMe);

}

 

int AEEBase_QueryInterface(AEEBase*po, AEECLSID id, void **ppNew)

{

  return IQI_QueryInterface(po->pMe, id, ppNew);

}

可见,AddRefRelease都是针对引用的组件实例操作,并且QueryInterface也是间接的请求了组件的实现。

由上分析,BREW中多接口继承的实现其实采用了COMQueryInterface机制,并且提供了AEEBase的参考实现,方便用户的开发。

三、   总结:

本文详细的对比,阐述了BREW中一些核心的面向对象机制的特定。通过深入熟悉这些机制,可以加深对BREW的理解,加强利用现有机制快速开发的能力。