注册 登录
Office中国论坛/Access中国论坛 返回首页

ganlinlao的个人空间 http://www.office-cn.net/?230471 [收藏] [复制] [分享] [RSS]

日志

Freebasic编写com组件之超高难度地方———实现聚合

热度 1已有 2655 次阅读2015-6-4 16:44 |个人分类:FreeBasic| Freebasic, freebasic编写com, Freebasic

    正常情况下,无论你使用多少年的VBA,无论你使用vb写过多少类,写过多少ocx或dll。应该是很少听过com的聚合技术。事实上在Freebasic中编写com组件也不是一件容易的事,而要实现com的聚合只能说难上加难了,在这里先记下,以待以后不时的回味和细细揣摩。

一、基础知识和概念图:


COM不支持实现继承的原因在于这种继承方式将使得一个对象的实现同另外一个对象的实现紧紧地关联起来。在这种情况下,当基类的实现被修改后,派生类将无法正常运行而必须被修改。(这也是为什么vb没有实现继承,这是com决定它无法成为真正的oop)因此,为了保证以组件的修改不会影响应用程序的正常运行,COM并不支持实现继承。但我们可以用组件包容来完全模拟实现继承


聚合的概念
       聚合源自组件重用。当有两个组件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组件而不知道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 ( );
}

******************************************
Freebasic的代码例子:
这是一个comsample.dll的例子。这个comSample里面主要有两个组件,cMath和cSquare这两个com组件。其中cMath是聚合者,而cSquare是被聚合者。(如果从com的使用角度看,主动聚合者本质上是com的client端,被聚合者本质是com的server端,这里将通过类工厂,让server端把内部的方法暴露出来,供client端调用)
第一步,我们先看一下cMath的头文件BI是怎么声明的(注:tlb是vb的头文件,没有tlb的com组件vb无法调用,但FB照样可以使用没用tlb的com组件,这个例子就是没有额外写tlb)
cMath的BI声明:
#include Once "Win/objbase.bi"        '引用
#include "CSquare.bi"                         '引用cSquare的头文件
Type IMath extends IUnknown           'Imath接口声明为iunknown接口
    Declare abstract Sub SumSquare(Val1  As Integer,  Val2  As Integer, pResult As Integer ptr)      '声明静态方法
End Type
    extern IID_IMath As IID                  '导出IID,供外部调用

Type CMath extends IMath               '定义类cMath

public:
    ' 实现 IUnknown Interface.
    Declare virtual Function QueryInterface(iid As  REFIID , ppv As LPVOID Ptr ) As HRESULT                     '这三个涵数都是以virtual虚函数形式声明,接口在FB中都是这样
    Declare virtual Function  AddRef() As ULong
    Declare virtual Function  Release()As ULong

    '实现 IMath Interface.
    Declare virtual Sub  SumSquare(Val1  As Integer,  Val2  As Integer, pResult As Integer ptr)
   
    Declare Constructor     '声明构造函数
     Declare Destructor      '析构
     
     m_pISquare  As  ISquare Ptr       '声明被聚合者Csquare的iSquare的接口

private:                      '内部成员数据
    m_cRef  As Long      '这是用来记录引用的状态
End Type


发表评论 评论 (1 个评论)

回复 t小宝 2015-6-14 14:47
路过看看

facelist doodle 涂鸦板

您需要登录后才可以评论 登录 | 注册

QQ|站长邮箱|小黑屋|手机版|Office中国/Access中国 ( 粤ICP备10043721号-1 )  

GMT+8, 2024-4-18 22:43 , Processed in 0.060643 second(s), 18 queries .

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

返回顶部