Presentation is loading. Please wait.

Presentation is loading. Please wait.

六 . COM接口的其他实现方法 基于表格驱动的接口查询 接口查询的本质 宏 应用 多重继承下的名字冲突 潜在的缺陷 临时的方案

Similar presentations


Presentation on theme: "六 . COM接口的其他实现方法 基于表格驱动的接口查询 接口查询的本质 宏 应用 多重继承下的名字冲突 潜在的缺陷 临时的方案"— Presentation transcript:

1 六 . COM接口的其他实现方法 基于表格驱动的接口查询 接口查询的本质 宏 应用 多重继承下的名字冲突 潜在的缺陷 临时的方案
使用前面的方法,我们已经可以顺利地实现接口与对象了.这些基本的技术是实现COM的基石.但是,人类对于完美与高效的追求是无止境的.本章介绍一些更加精细的技术,看这些技巧是如何提高COM的效率,如何使得COM的功能细致入微的. 基于表格驱动的接口查询 接口查询的本质 应用 多重继承下的名字冲突 潜在的缺陷 临时的方案 使用复合技术(嵌套类)实现接口 COM主对象和COM子对象 COM 主对象的实现 COM 子对象的实现 基于复合技术的COM对象的内存结构 针对接口的引用计数 需求 实现方法 多重继承与复合的结合 复合技术的表格驱动 动态复合接口 MFC对COM的支持

2 1.基于表格驱动的接口查询 1.1 接口查询的本质 前面我们使用继承的方式实现接口,使用多重继承的方式实现多个接口. 在这种方式下,接口的查询QueryInterface函数的实现非常的直接且直观. 在多重继承方式下,接口类是基类, IUnkown接口是最上层的基类. 对象类是接口类的派生的子类. 在内存中,子类比基类“大”. 因为子类除了包含基类的成员以外,还包含自己的成员.子类的一个实例中包含有基类的一个“subobject”,子对象. 如果这个基类还有基类,这个子对象中还含有一个更上层的子对象. QueryInterface函数的本质是: 使用statice_cast操作符在子类的对象中加上基类的偏移从而从而得到基类的子对象.转换到不同的基类时,要加上不同的偏移. 所以QueryInterface实际上是在不同的基类和不同的偏移中工作. 我们可以把基类和对应的偏移量抽象出来.作成表格.使得QueryInterface的工作更加形式化. 最终使得COM的编码更加形式化. (这里的工作为向MFC过渡而热身.我们看到,MFC中古怪的代码也是有其理性的由来的. )

3 1.2 宏 为了实现表格驱动的QueryInterface, 我们定义这样的一个结构:
typedef HRESULT (STDAPICALLTYPE *INTERFACE_FINDER)(void *pThis, DWORD dwData, REFIID riid, void **ppv); //这是一个查询接口的函数,暂时这里并没有用上. #define ENTRY_IS_OFFSET INTERFACE_FINDER(-1) //所以这里定义了一个伪函数ENTRY_IS_OFFSET typedef struct _INTERFACE_ENTRY { const IID * pIID; // 要寻找的接口的IID INTERFACE_FINDER pfnFinder; // finder function long dwData; // finder function所需的参数.这里指偏移量. } INTERFACE_ENTRY; //暂时,这里只使用了第一和第三个分量

4 这个宏用来计算基类BaseName到子类ClassName的偏移.(一个完整对象中的基类子对象的起始地址到完整对象的起始地址的偏移)
然后定义了几个宏: #define BASE_OFFSET(ClassName, BaseName) \ (DWORD(static_cast<BaseName*>(reinterpret_cast<ClassName*>(0x ))) - 0x ) 这个宏用来计算基类BaseName到子类ClassName的偏移.(一个完整对象中的基类子对象的起始地址到完整对象的起始地址的偏移) reinterpret_cast<ClassName*>(0x ) 把绝对内存地址0x 转换成为子类对象的地址. static_cast<BaseName*>(子类指针) 把子类指针转换为基类指针.这个工作由编译器计算出偏移.并且加上偏移值. DWORD(基类指针地址) - 0x 基类指针地址转换成绝对的数字后减去子类指针的绝对地址的数字. 宏BASE_OFFSET(ClassName, BaseName) 返回基类BaseName到子类ClassName的偏移量. 如下图所示: 在这个宏的基础上又定义了几个宏: 0x1000 0x1012 ClassName 子类 BaseName 基类 偏移

5 #define BEGIN_INTERFACE_TABLE(ClassName) \
typedef ClassName _InterfaceTableClassName;\ //申明了一个静态函数,返回接口表 static const INTERFACE_ENTRY *GetInterfaceTable(void) \ { static const INTERFACE_ENTRY table[] = {\ //函数体中, 定义一个静态的接口表, 定义了类,此表就分配了.对所有的对象而言是同一个. #define IMPLEMENTS_INTERFACE(ItfName) \ { &IID_##ItfName, ENTRY_IS_OFFSET, BASE_OFFSET(_InterfaceTableClassName, ItfName) }, //接口表项,对于接口ItfName,使用宏计算ItfName与子类的偏移. #define IMPLEMENTS_INTERFACE_AS(RequestedItfName, BaseClassName) \ { &IID_##RequestedItfName, ENTRY_IS_OFFSET , BASE_OFFSET(_InterfaceTableClassName, BaseClassName)}, //接口表项,对于请求的接口RequestedItfName,计算BaseClassName与子类的偏移.即请求RequestedItfName,返回给BaseClassName,(往往用于菱形多重继承的情形.为了避免歧义,请求祖父类接口,返回父亲类接口.) #define END_INTERFACE_TABLE() \ { 0, 0, 0 } }; return table;} //结尾项 我们还要定义一个函数:

6 HRESULT STDAPICALLTYPE InterfaceTableQueryInterface(void
HRESULT STDAPICALLTYPE InterfaceTableQueryInterface(void *pThis, const INTERFACE_ENTRY *pTable, REFIID riid, void **ppv) { if ( riid== IID_IUnknown) //对于IUnknown接口 { *ppv = pThis + pTable->dwData;//直接加上第一项的偏移量. ((IUnknown*)(*ppv))->AddRef(); //计数加1 return S_OK; } else //别的接口 { HRESULT hr = E_NOINTERFACE; while (pTable->pIID) //如果没有到达尾部则, (结尾项为空 ) { if (riid==pTable->pIID)) //比较ID { *ppv = pThis + pTable->dwData;//加上偏移量. hr = S_OK; break;} //找到了处理完则退出 pTable++;//否则循环遍历接口表. } if (hr != S_OK) *ppv = 0; //不支持此接口 return hr; 在此基础上, 以下宏实现IUnknown接口:

7 先定义一个结构用于引用计数: struct AUTO_LONG { LONG value; AUTO_LONG(void) : value(0) {}}; //构造函数使得其值为 #define IMPLEMENT_UNKNOWN(ClassName) \ AUTO_LONG m_cRef;\ //引用计数变量 STDMETHODIMP QueryInterface(REFIID riid, void **ppv) \ { return InterfaceTableQueryInterface(this, GetInterfaceTable(), riid, ppv); }\ //直接调用刚定义的InterfaceTableQueryInterface STDMETHODIMP_(ULONG) AddRef(void) \ {return InterlockedIncrement(&m_cRef.value); \ }\ //InterlockedIncrement能防止多线程同时访问 STDMETHODIMP_(ULONG) Release(void) \ {ULONG res = InterlockedDecrement(&m_cRef.value); if (res == 0) delete this; return res; }\ //实现IUnknown定义的三个函数

8 1.3 应用 现在来看应用. 对于第五章字典对象的例子:
class CDictionary : public IDictionary , public ISpellCheck {public : CDictionary(); ~CDictionary(); //构造函数,析构函数 virtual HRESULT __stdcall QueryInterface(const IID& iid, void **ppv) ; // IUnknown 成员函数 (在这里要实现,所以再次申明) virtual ULONG __stdcall AddRef() ; virtual ULONG __stdcall Release() ; //要对以上三个函数分别实现,而且对所有的COM对象都是雷同的. virtual BOOL __stdcall Initialize(); // IDictionary成员函数 ...... // ISpellCheck成员函数 virtual BOOL __stdcall CheckWord (String word, String *); private : struct DictWord *m_pData; char *m_DictFilename[128];……//私有与功能相关的数据. int m_Ref ;//用作引用计数 };

9 我们可以写成: class CDictionary : public IDictionary , public ISpellCheck { public : CDictionary(); ~CDictionary(); //构造函数,析构函数 // 使用宏来定义接口表,实现IUnknown 成员函数 IMPLEMENT_UNKNOWN(CDictionary) BEGIN_INTERFACE_TABLE(CDictionary) IMPLEMENT_INTERFACES(IDictionary) //请求IUnkown接口,则返回IDictionary.因为它是表的第一项. 见InterfaceTableQueryInterface的定义. IMPLEMENT_INTERFACES(ISpellCheck) END_INTERFACE_TABLE() virtual BOOL __stdcall Initialize(); // IDictionary成员函数 ...... virtual BOOL __stdcall CheckWord (String word, String *); // ISpellCheck成员函数 private : struct DictWord *m_pData; char *m_DictFilename[128];……//与功能相关的私有数据. // int m_Ref ;不再需要单独定义引用计数 }; //并没有引进新技术,但是,更加形式化.便于编程,提高效率.

10 2. 多重继承下的名字冲突 2.1 潜在的缺陷: 使用多重继承使用C++类来实现COM接口,是一种非常有效的技术.编码量小,直观.建立COM规范所要求的vptr和vtbl的工作大多由编译器完成了. 使用多重继承,一个虚函数出现在多个基类中,在子类中实现后,该子类的属于多个基类的虚表中的该虚函数的表项都指向这个实现. QueryInterface, AddRef, Release就是利用这样的方法实现的, 而且工作得很好. 这个特性对于基类中其他的函数也是成立的.但是有的时候,这就会成为一种限制.一种潜在的缺陷. 设想一种交通工具水上飞机boatplane.它既可以象飞机一样飞,也可以象船一样在水中航行.我们做一个COM对象BoatPlane,它使用多重继承的方式实现了接口IBoat,和接口IPlane. 这两个接口都派生自接口IVehicle. IVehicle有一个函数GetMaxSpeed以返回该交通工具的最大速度.IBoat继承了此函数.同样IPlane也继承了此函数. 注意接口是定义函数的地方,它不能够实现它,(IBoat和IPlane是抽象基类,GetMaxSpeed是纯虚函数). 在COM对象类BoatPlane中对此函数的实现,会导致BoatPlane中的属于IBoat和IPlane的GetMaxSpeed函数表项都指向该实现.这意味着客户得到COM对象的接口指针IBoat和IPlane时,调用GetMaxSpeed会得到同样的结果! 多重继承下的名字冲突. 潜在的缺陷爆发了

11 本章中用到的类的继承层次 使用多重继承的方式实现接口时的潜在缺陷 IUnknown IVehicle IBoat IPlane
BoatPlane 定义GetMaxSpeed 实现GetMaxSpeed IUnknown IVehicle IBoat IPlane 本章中用到的类的继承层次 使用多重继承的方式实现接口时的潜在缺陷 一种简单的解决方案是: IBoat不从IVehicle派生, 把GetMaxSpeed换成GetMasBoatSpeed, IPlane也不从IVehicle派生, 把GetMaxSpeed换成GetMaxPlaneSpeed. 换个名字而已.再在BoatPlane类中对这两个函数分别实现, 50KM/H, 800KM/H…… 问题似乎解决,但是, 我们要改变接口的继承结构吗? 而且, IBoat类的设计者十分不情愿把GetMaxSpeed换成GetMasBoatSpeed, 因为“…Boat…”对其而言是多余的,不雅观的.同样,IPlane的设计者也有同感. 毕竟, 人类对完美的追求是无止境的……

12 2.2临时的方案 一个别致的方案: 针对IBoat,设计一个中间类: struct IXBoat:public IBoat
{ virtual HRESULT GetMaxBoatSpeed(long *pV)=0; //新增加一个纯虚函数. 此函数在COM对象中实现 HRESULT GetMaxSpeed(long *pV) { return GetMaxBoatSpeed(pV);} //实现了接口IBoat的GetMaxSpeed,但是,只转交给另一个函数GetMaxBoatSpeed. COM对象不再改写此函数. } //此类既不是COM接口类,也不是COM实现类. 针对IPlane, 也作类似的处理,设计一个中间类IXPlane,增加一个纯虚函数GetMaxPlaneSpeed,并把其GetMaxSpeed转交给新函数处理.

13 class BoatPlane:public IXBoat, public IXPlane { public:
而COM对象类: class BoatPlane:public IXBoat, public IXPlane { public: // IUnknown的方法: QueryInterface,AddRef,Release …… //IBoat的方法: HRESULT swim(void); // IPlane的方法: HRESULT fly(void); //IXBoat的方法: HRESULT GetMaxBoatSpeed(long *pV){ *pV=50;} //IXPlane的方法: HRESULT GetMaxPlaneSpeed(long *pV) { *pV=800;} }//注意没有改写接口类的IBoat和IPlane的GetMaxSpeed. 只改写了中间类的两个函数. COM对象的内存结构如下:

14 多重继承中发生名字冲突的一种解决方案. IBoat接口指针 IUnknown IVehicle
IXBoat vptr BoatPlane::QueryInterface BoatPlane::AddRef BoatPlane::Release 对象状态数据 BoatPlane::Swim IXBoat::GetMaxSpeed BoatPlane::GetMaxBoatSpeed IXPlane vptr BoatPlane::Fly BoatPlane::GetMaxPlaneSpeed IPlane IUnknown IBoat接口指针 IVehicle 继承自接口类的虚表 (虚线框) 中间类的虚表 (实线框) 多重继承中发生名字冲突的一种解决方案.

15 IBoat->GetMaxSpeed(*pV)将在内部调用GetMaxBoatSpeed,返回50
IPlane->GetMaxSpeed(*pV)将在内部调用GetMaxPlaneSpeed,返回800 以上方案相当不错.注意,BoatPlane类没有改写接口类的GetMaxSpeed方法.实际上,一旦改写了,那么,费尽心机所加的中间类,以及中间函数都将失效. 客户使用IBoat接口或IPlane接口调用GetMaxSpeed时将调用BoatPlane的实现.所有的努力白费. 这是一个悖论! 接口定义了,实现却不能写它,客户还要调用. 称之为完美无缺,尚难服众. 在下面的章节中,我们有新的方法.

16 3.使用复合(COMPOSITE)技术实现接口
3.1 COM主对象和COM子对象 复合(COMPOSITE)指在一个类中包含了另一个类的实例作为其数据成员.有时也称为嵌套类. (当一个类A内嵌另一个类B时, 为方便起见,称A为B的父类,或包装类,称B为A的子类. 注意与继承关系带来的基类与子类的关系区别开来. ) COM规定了接口的结构,但是并没有规定其实现方法. 复合方式是使用C++语言实现COM接口的另一种方法. 使用复合技术可以很好地解决名字冲突问题. 方法是: 把发生名字冲突的接口以不同的C++的类实现,让COM对象类复合(即内嵌)这些类的实例. 为了让这些被内嵌(复合)进来的数据成员在客户的眼里(通过接口指针)看起来是一个对象, 通常我们在COM对象中做一个QueryInterface的主实现(master implementation), 其他数据成员的QueryInterface都委托给这个主实现. COM对象类的定义如下:

17 class BoatPlane { public: BoatPlane(void): m_Ref(0){} HRESULT _stdcall QueryInterface(REFIID iid,void **ppv); ULONG _stdcall AddRef(); //这三个函数不必是虚函数 ULONG _stdcall Release(); //嵌套类从接口类派生 struct XBoat : public IBoat{ inline BoatPlane *This(); //函数返回指向父类的指针 ULONG _stdcall AddRef(); //这几个是继承自IUnknown,虚的. HRESULT _stdcall Swim(); //具体的功能 HRESULT _stdcall GetMaxSpeed(long *pV); } m_xBoat; /* 嵌套类数据成员1, 实现了IBoat接口. 在使用继承的方式时,它就是COM对象. 但这里,它作为主对象的一个成员. 不妨称之为COM子对象,相应地BoatPlane的实例称为COM主对象. 子对象只使用了单继承. 同一主对象的不同子对象都使用单继承的方式. */

18 struct XPlane:public IPlane{ //嵌套类从接口类派生
inline BoatPlane *This(); //函数返回指向父类的指针 HRESULT _stdcall QueryInterface(REFIID iid,void **ppv); ULONG _stdcall AddRef(); ULONG _stdcall Release(); HRESULT _stdcall Fly(); HRESULT _stdcall GetMaxSpeed(long *pV); } m_xPlane; //嵌套类数据成员2 实现了IPlane接口,另一个COM子对象. int m_Ref ;// 用作引用计数 char * m_pTonsOfMemForBoat; //为实现Boat功能而需要的数据.见后文 ….//其他数据成员. } BoatPlane的QueryInterface等函数如下:

19 3.2 COM 主对象的实现 HRESULT BoatPlane::QueryInterface(const IID& iid, void **ppv) { if ( iid == IID_IUnknown) *ppv=static_cast<IUnknown*>(&m_xBoat) ; //对COM子对象进行转换后传出. else if ( iid == IID_IVehicle) *ppv=static_cast<IVehicle*>(&m_xBoat) ; else if ( iid == IID_IBoat) *ppv=static_cast<IBoat*>(&m_xBoat) ; else if ( iid == IID_IPlane) *ppv=static_cast<IPlane*>(&m_xPlane) ; else { *ppv=0; return E_NOINTERFACE;} ( (IUnknown*)(*ppv))->AddRef(); //注意只有在对IUnknown是单继承的情况下才能使用(IUnknown*)进行转换.否则编译出错.如果使用了多继承,要转换为相应的具体的接口. return S_OK; } BoatPlane 对QueryInterface的处理,并不是把指向自己的指针this进行static_cast转换, 而是对自己的成员变量即COM子对象进行static_cast转换. 再传出去.

20 ULONG CDictionary::AddRef()
{ m_Ref ++; //跟使用继承的方式实现接口时的引用计数方法完全一致 return (ULONG) m_Ref; } ULONG CDictionary::Release() { m_Ref --; if (m_Ref == 0 ) { delete this; return 0; }

21 3.3 COM 子对象的实现 3.4 基于复合技术的COM对象的内存结构:
为了维护对象的实体身份,(在这里为了实现IBoat和IPlane接口,我们使用了三个类, 其中两个派生自接口类,然后作为第三个的内嵌的数据成员) 我们不能让客户有所察觉,即“透明”地实现接口. 因此这里我们把COM子对象对IUnknown定义的函数的实现委托给COM主对象来完成. 因此有必要在COM子对象中访问COM主对象的成员. 它们的This内联函数就是这个目的: inline BoatPlane* BoatPlane::XBoat::This(void) { return (BoatPlane*)((char*)this-offsetof(BoatPlane,m_xBoat));} This函数把this指针减去类分量在类中的偏移得到父类指针. HRESULT BoatPlane::XBoat::QueryInterface(REFIID iid,void **ppv) { return This()->QueryInterface(iid,ppv); } //通过父类指针调用父类的实现 ULONG BoatPlane::XBoat:: AddRef() {return This()->AddRef(); } //通过父类指针调用父类的实现 ULONG BoatPlane::XBoat:: Release() {return This()->Release();} //通过父类指针调用父类的实现 //对于XPlane也采用类似的方法. 3.4 基于复合技术的COM对象的内存结构: COM对象的结构如下图所示:

22 基于复合技术实现COM接口 名称冲突消除了. IUnknown IVehicle BoatPlane的
XBoat COM子对象 XBoat::QueryInterface XBoat::AddRef XBoat::Release 对象状态数据 XBoat::Swim XBoat::GetMaxSpeed IBoat XPlane COM子对象 XPlane::QueryInterface XPlane::AddRef XPlane::Release XPlane::Fly XPlane::GetMaxSpeed IPlane IUnknown BoatPlane的 QueryInterface AddRef Release 等函数 XBoat的 Swim GetMaxSpeed XPlane的 Fly IVehicle 另一个COM子对象的虚表,单继承自其接口 COM子对象的虚表,单继承自其接口, 注意非COM主对象的虚表! 注意是子对象,而非虚表指针 基于复合技术实现COM接口 名称冲突消除了.

23 4. 针对接口的引用计数 4.1 需求 使用复合技术需要更多的编码,而且,复合技术产生的代码的质量也可能不如使用多重继承的方式好.
复合技术实现接口成功地消除了名字冲突. 复合技术之所以能够做到这一点是因为它没有多重继承所固有的“潜在的缺陷”, 如果这个缺陷会影响功能的话. 实际上,利用复合技术的这一个优势, 我们还能够实现针对接口的引用计数. 在此之前,我们所谓的引用计数都是针对对象的. 一个对象的所有接口都对同一个计数变量进行操作.对象无法区分是哪个接口对其进行操作的.而且,实际上,通常我们也不必区分它. 但是,存在这样的情况, 我们的COM对象越来越复杂,功能越来越多.(想一想我们的手机) COM对象为实现不同的接口准备了完全不同的资源,如果暂时不使用其中的某个接口的话,我们完全可以对其所需要的资源暂时不予分配.(我们打电话的时候一定要把摄像头打开吗?).而把分配工作放到必要时进行,而且,也要及时地释放.

24 仍然考虑水上飞机,在BoatPlane类中我们定义了一个成员变量:
char * m_pTonsOfMemForBoat; 这个成员变量只在在swim函数中要使用.也就是说,只有IBoat指针会使用它.而与IPlane指针无关. 假设m_pTonsOfMemForBoat需要分配一个很大的内存空间.我们当然希望只在必要的时候分配.然而,如果使用多重继承的方式实现COM接口,那么意味者所有的虚表中的AddRef和Release项都只指向同一个实现.也即我们无法从引用计数中区分出IBoat接口来.当然,对于分配过程我们还是有点办法: HRESULT BoatPlane ::QueryInterface(REFIID iid,void **ppv) { if(iid=IID_Boat) { if(m_pTonsOfMemForBoat==NULL) m_pTonsOfMemForBoat=new char[1024*1024*10]; //任务只完成了一半, 10M的内存只在最需要的时候分配. *ppv=static_cast<IBoat*>(&m_xBoat); } else if …… 但是,我们无法知道什么时候释放掉.因为我们不能鉴别IBoat和IPlane所发出的Release调用. 可见, 针对接口的引用计数有明确的应用需求. 实现方法如下:

25 4.2 实现方法 我们使用复合技术来实现针对接口的引用计数. COM 子对象XBoat的引用计数不再简单地委托给COM主对象实现
4.2 实现方法 我们使用复合技术来实现针对接口的引用计数. COM 子对象XBoat的引用计数不再简单地委托给COM主对象实现 在上一节的BoatPlane定义中增加一个成员变量int m_BoatRef; 在BoatPlane的构造函数中被赋值0. 除了XBoat的AddRef和Release函数变为: ULONG BoatPlane::XBoat:: AddRef() { ULONG res=InterLockedIncrement(&m_BoatRef); if(res==1) { This ->m_pTonsOfMemForBoat=new char[1024*1024*10]; // 只在必须分配内存的时候才分配. This->AddRef();} //只在第一次调用一次主对象的AddRef,在主对象中备案. return res; }

26 ULONG BoatPlane::XBoat:: Release()
{ULONG res=InterLockedDecrement(&m_BoatRef); if(res==0) { delete []This ->m_pTonsOfMemForBoat ; // 及时地释放内存. IPlane接口并不需要它. 打电话时关掉摄像头! This->Release();} //最后一次调用一次主对象的Release;通知主对象, 不必为此子对象而保持引用了 return res; } XPlane的引用计数函数保持不变. 客户的使用完全与以前一致.

27 为了使得此技术能正常工作,必须保证所有的接口指针用户遵从COM规范的要求, Release调用必须作用在它对应的AddRef指针上
为了使得此技术能正常工作,必须保证所有的接口指针用户遵从COM规范的要求, Release调用必须作用在它对应的AddRef指针上. 即AddRef和Release必须完全保持配对.因此在标准的QueryInterface中,在ppv指针赋值后,要使用 ((IUnkown*)(*ppv))->AddRef(); //使用新指针 而不能使用 AddRef(); //使用旧指针. 新旧指针指向同一个COM对象的接口, 它们有可能指的同一个对象.(在多重继承的情况下,同一个主对象),也有可能指的不同的对象(在复合技术下,不同的子对象). 在前者无论使用谁调用AddRef都是一样的,在后者则有可能不同.为了一致起见,应遵循COM规范,使用新指针. 以上方案可以实现针对接口的引用计数,从而实现资源的动态最优分配和释放.

28 5.多重继承与复合的结合 多重继承虽然存在“潜在的缺陷”,但是, 如果它正好满足我们的需求, 实际上,大多数时候是满足的,而且具有编码简单,代码质量高等优点. 而复合技术则能避免可能的名字冲突, 而且可以实现对接口的引用计数,提高程序的运行效率. 我们可以综合运用以上两种方法, 在一个COM对象中,以不同的方法实现不同要求的接口. 仍然考虑水上飞机, 我们可以这样来实现它的接口: 使用继承的方式实现IPlane接口, 使用复合的方式实现IBoat接口(IBoat不是有特殊的引用计数要求吗?). COM对象的定义如下:

29 class BoatPlane: public IPlane //IPlane接口通过继承的方式实现
{ public:BoatPlane(void): m_Ref(0){} HRESULT _stdcall QueryInterface(REFIID iid,void **ppv); ULONG _stdcall AddRef(); //这三个函数通过IPlane从IUnknown继承而来 ULONG _stdcall Release(); //这里改写虚函数,而不像前一节,是单纯的函数 HRESULT _stdcall Fly(); //IPlane的函数 HRESULT _stdcall GetMaxSpeed(long *pV);//IVehicle的函数 struct XBoat:public IBoat{ inline BoatPlane *This(); //函数返回指向父类的指针 ULONG _stdcall AddRef(); ULONG _stdcall Release(); HRESULT _stdcall Swim(); HRESULT _stdcall GetMaxSpeed(long *pV); //COM子对象的实现可以 与主对象的实现不一致. 通过IBoat接口和IPlane接口能得到不同的结果. } m_xBoat; //嵌套类数据成员,COM子对象 实现了IBoat接口. int m_Ref ; // 用作主对象的引用计数 int m_BoatRef; //用作COM子对象 IBoat接口的引用计数 char * m_pTonsOfMemForBoat; ….//其他数据成员. } 主对象的QueryInterface函数如下:

30 HRESULT BoatPlane::QueryInterface(const IID& iid, void **ppv)
{ if ( iid == IID_IUnknown) *ppv=static_cast<IUnknown*>(this) //也可以是 //*ppv=static_cast<IUnknown*>(&m_xBoat) ; else if ( iid == IID_IVehicle) *ppv=static_cast<IVehicle*>(this) //也可以是 // *ppv=static_cast<IVehicle*>(&m_xBoat) ; else if ( iid == IID_IBoat) *ppv=static_cast<IBoat*>(&m_xBoat) ; //把COM子对象传出 else if ( iid == IID_IPlane) *ppv=static_cast<IPlane*>(this); //把COM主对象传出 else { *ppv=0; return E_NOINTERFACE;} ( (IUnknown*)(*ppv))->AddRef(); //增加引用计数 return S_OK; } IPlane接口调用QueryInterface当然是上述函数,而IBoat的QueryInterface也应该委托给它. 其他的函数的实现方式都与以前一样. 客户得到的IPlane接口是指向COM主对象的,客户得到的IBoat接口是指向BoatPlane的成员变量m_xBoat即COM子对象的.接口的转换过程请自行分析.

31 6.复合技术的表格驱动 其实,我们已经看到,使用继承或使用复合来实现接口的差别并不大, 这两种技术可以和平共处. 我们曾利用表格驱动的方式实现了多重继承方式下的COM接口的QueryInterface函数.实际上,对于复合技术,也可以使用表格驱动的方式来实现它的QueryInterface函数. 表格驱动的本质是把接口的IID号和COM对象到这个IID号所代表的接口类子对象“subobject”的偏移联系起来.注意,我们的QueryInterface函数是在COM对象中实现的.而接口调用此函数,要么是通过虚函数改写的方式,要么是通过一个指向父类指针的方式进行委托调用. 无论哪种方式都是调用的COM对象定义的QueryInterface, 从这里(COM对象的this)出发, 如果有从COM对象到接口类子对象的偏移.(注意,这个偏移,有可能是从子类到基类的偏移,也有可能是从父类到嵌套子类的基类的偏移),我们当然可以得到接口类子对象.也就是接口指针. 上一段话的意思是说, 为了实现支持复合的表格驱动,只需要添加使用复合技术的接口的接口表项, 而QueryInterface函数,以及InterfaceTableQueryInterface函数,(见前面章节) 都不用更改.

32 BaseName 嵌套类的基类 即接口类 第二步
定义如下的宏: #define COMPOSITE_OFFSET(ClassName, BaseName, MemberType, MemberName) \ (DWORD(static_cast<BaseName*>(reinterpret_cast<MemberType*>(0x offsetof(ClassName, MemberName)))) - 0x ) reinterpret_cast<MemberType*>(0x offsetof(ClassName, MemberName)) 0x 加上父类到嵌套成员的偏移转换为嵌套类 (static_cast<BaseName*>(嵌套类指针))把嵌套类指针转换为它的基类指针即接口指针.这里由编译器进行计算偏移. (DWORD)接口指针- 0x 接口指针的绝对数字减去父类的起始地址0x 得到父类到嵌套类的基类的偏移. 0x1000 ClassName 父类 MemberType 嵌套类 第一步 偏移 第三步 BaseName 嵌套类的基类 即接口类 第二步 0x1012

33 #define BEGIN_INTERFACE_TABLE(ClassName) \
typedef ClassName _InterfaceTableClassName;\ #define IMPLEMENTS_INTERFACE_WITH_COMPOSITE(RequestedItfName, DataMemberType, DataMemberName) \ { &IID_##RequestedItfName, ENTRY_IS_OFFSET , COMPOSITE_OFFSET(_InterfaceTableClassName, RequestedItfName, DataMemberType, DataMemberName) }, 以上宏把复合的接口的偏移表项加入到接口表中去.

34 #define IMPLEMENT_COMPOSITE_UNKNOWN(OuterClassName, InnerClassName, DataMemberName) \
OuterClassName *This() { return (OuterClassName*)((char*)this - offsetof(OuterClassName, DataMemberName)); }\ STDMETHODIMP QueryInterface(REFIID riid, void **ppv) \ { return This()->QueryInterface(riid, ppv);\ }\ STDMETHODIMP_(ULONG) AddRef(void) \ { return This()->AddRef(); \ STDMETHODIMP_(ULONG) Release(void) \ { return This()->Release();\ 以上宏为复合的接口实现IUnknown定义的函数QueryInterface AddRef 和Release

35 使用这些宏来完成上一节的水上飞机(两种不同方式实现的接口):
class BoatPlane: public IPlane //IPlane接口通过继承的方式实现 { public: struct XBoat:public IBoat //IBoat接口使用复合方式实现 { IMPLEMENT_COMPOSITE_UNKNOWN (BoatPlane,XBoat,m_xBoat); //复合接口实现IUnknown接口 HRESULT _stdcall Swim(); HRESULT _stdcall GetMaxSpeed(long *pV); } m_xBoat; //嵌套类数据成员,实现了IBoat接口. IMPLEMENT_UNKNOWN(BoatPlane) // IPlane接口实现IUnknown BEGIN_INTERFACE_TABLE(BoatPlane) IMPLEMENT_INTERFACES(IPlane) IMPLEMENT_INTERFACES_AS(IVehicle,IPlane)//如果请求IVehicle则返回IPlane,当然也可以返回IBoat. IMPLEMENTS_INTERFACE_WITH_COMPOSITE (IBoat,XBoat,m_xBoat) //把复合接口加入到接口表中 END_INTERFACE_TABLE() HRESULT _stdcall Fly(); //IPlane的函数 HRESULT _stdcall GetMaxSpeed(long *pV);//IVehicle的函数 char * m_pTonsOfMemForBoat; } //注意,这里没有实现针对接口的引用技术,如果要实现不能使用IMPLEMENT_COMPOSITE_UNKNOWN宏

36 7.动态复合接口 事实上,除了使用单独的引用计数外,复合接口还可以进一步优化.一个COM子对象直到客户请求它的复合接口的时候才真正创建,可以进一步优化资源. 在特定的场合下有其重要的用途.这样的接口称为动态复合接口,也称为tearoff接口. class BoatPlane: public IPlane //IPlane接口通过继承的方式实现 { public: BoatPlane(void): m_Ref(0){} HRESULT _stdcall QueryInterface(REFIID iid,void **ppv); ULONG _stdcall AddRef(); ULONG _stdcall Release(); HRESULT _stdcall Fly(); //IVehicle的函数 HRESULT _stdcall GetMaxSpeed(long *pV); int m_Ref ; // 用作主对象的引用计数 char * m_pTonsOfMemForBoat; ….//其他数据成员. IBoat * m_pBoat; //指针用来保存动态创建的接口

37 struct XBoat:public:IBoat {//嵌套类实现了IBoat接口.
XBoat(BoatPlane *pThis); //构造函数 int m_BoatRef; // 自己负责自己的计数 BoatPlane *m_pThis; //指向父类的指针 inline BoatPlane *This(){return m_pThis;} //返回指向父类的指针 HRESULT _stdcall QueryInterface(REFIID iid,void **ppv); ULONG _stdcall AddRef(); ULONG _stdcall Release(); HRESULT _stdcall Swim(); HRESULT _stdcall GetMaxSpeed(long *pV); }//注意只定义了嵌套类,没有数据成员. 即只有COM子对象的类定义, 而没有COM子对象本身. } 主对象第一次接收到IBoat接口请求时,动态地创建一个新的复合接口. 这个工作当然是在QueryInterface中完成的:

38 HRESULT BoatPlane::QueryInterface(const IID& iid, void **ppv)
{ if ( iid == IID_IBoat) { if (m_pBoat==NULL) m_pBoat= new XBoat(this); / /动态创建对象,保存在m_pBoat中, 以供下一次查询时使用 *ppv= m_pBoat;} else if …… } 而XBoat的构造函数: XBoat::XBoat(BoatPlane *pThis) : m_BoatRef(0), m_pThis( pThis) //把父类的指针保存下来. { m_pThis->AddRef();} // 对主对象的引用加1. 以保证当COM子对象XBoat存在时,主对象不被删除. XBoat::~XBoat (void) { m_pThis->Release();} //子对象销毁时也通知主对象.

39 动态复合接口维护自己的引用计数. ULONG BoatPlane::XBoat:: AddRef() {return =InterLockedIncrement(&m_BoatRef); } ULONG BoatPlane::XBoat:: Release() { ULONG res=InterLockedDecrement(&m_BoatRef); if(res==0) delete this; return res;} 它的接口查询函数: HRESULT XBoat::QueryInterface(REFIID iid,void **ppv) { if(iid!=IID_Boat) return This()->QueryInterface(iid, ppv) //如果是别的接口,就让主对象去操心. *ppv=static_cast<IBoat*>(this); ((IUnknown*)(*ppv))->AddRef(); //内部的引用计数 }

40 8.MFC对COM的支持 MFC中使用CcmdTargert类采用了与以上示例类似的方法用复合嵌套类的方式提供对COM的支持。它使用接口映射表的机制来实现COM接口之间的查询转换功能。 接口映射表记录了每一个嵌套类的接口ID与其所代表的接口虚表与父类指针的偏移量。因为嵌套类的作用域在父类作用域的内部,嵌套类不能直接访问父类的成员,CCmdTarget利用嵌套类成员与父类this指针的偏移量使得嵌套类可以计算出父类的this指针,从而访问父类的成员。这种处理方法我们已经在前面的例子中看到多次了. 大部分乏味枯燥的操作都是使用宏替换的方式实现的. 这些宏的风格与我们已经见到的也相仿. MFC中大量使用宏来进行各种基础设施的构造工作,比如消息映射,比如类型识别,永久性支持等等.这些样式古怪的宏虽然功能强大,但是往往是初学者的拦路虎.

41 MFC使用了一组宏来简化操作 #define DECLARE_INTERFACE_MAP() \ private: static const AFX_INTERFACEMAP_ENTRY _interfaceEntries[]; \ protected: \ static AFX_DATA const AFX_INTERFACEMAP interfaceMap; \ static const AFX_INTERFACEMAP* PASCAL _GetBaseInterfaceMap(); \ virtual const AFX_INTERFACEMAP* GetInterfaceMap() const; \ 定义了静态的数据成员_interfaceEntries 和interfaceMap,静态成员函数_GetBaseInterfaceMap虚成员函数GetInterfaceMap。 其中:struct AFX_INTERFACEMAP_ENTRY { const void* piid; // the interface id (IID) (NULL for aggregate) size_t nOffset; // offset of the interface vtable from m_unknown }; struct AFX_INTERFACEMAP { #ifdef _AFXDLL const AFX_INTERFACEMAP* (PASCAL* pfnGetBaseMap)(); #else const AFX_INTERFACEMAP* pBaseMap; //指向基类的接口映射 #endif const AFX_INTERFACEMAP_ENTRY* pEntry; // map for this class };

42 DECLARE_INTERFACE_MAP()定义了一张接口表interfaceMap,表中第一项指向其基类的接口表pBaseMap,第二项指向接口表的入口pEntry,而接口表中的每一项包括接口的IID和接口vtable和父类指针之间的偏移。 使用这种结构,用户可以从CcmdTargert类派生自己的COM类,被派生的类可以继承其父类实现的接口,通过接口表结构的第一项获取其基类接口,从而形成接口链。 接口表的实现过程也是利用一组宏,以字典对象为例: BEGIN_INTERFACE_MAP(CDictionaryObj, CCmdTarget) INTERFACE_PART(CDictionaryObj, IID_Dictionary, Dictionary) INTERFACE_PART(CDictionaryObj, IID_SpellCheck, SpellCheck) END_INTERFACE_MAP()

43 #define BEGIN_INTERFACE_MAP(theClass, theBase) \
const AFX_INTERFACEMAP* PASCAL theClass::_GetBaseInterfaceMap() \ { return &theBase::interfaceMap; } \ const AFX_INTERFACEMAP* theClass::GetInterfaceMap() const \ { return &theClass::interfaceMap; } \ AFX_COMDAT const AFX_DATADEF AFX_INTERFACEMAP theClass::interfaceMap = \ { &theClass::_GetBaseInterfaceMap, &theClass::_interfaceEntries[0], }; \ AFX_COMDAT const AFX_DATADEF AFX_INTERFACEMAP_ENTRY theClass::_interfaceEntries[] = \ { \ #define INTERFACE_PART(theClass, iid, localClass) \ { &iid, offsetof(theClass, m_x##localClass) }, \ #define END_INTERFACE_MAP() \ { NULL, (size_t)-1 } \ }; \

44 BEGIN_INTERFACE_MAP给出了静态函数_GetBaseInterfaceMap和虚成员函数GetInterfaceMap的实现过程,它们分别返回基类和自己的接口表,并且定义了静态数据成员interfaceMap接口表和_interfaceEntries接口表项。 interfaceMap的第一个成员使用静态函数指针_GetBaseInterfaceMap填入,第二个成员使用接口表项的第一项作为其入口_interfaceEntries[0]。 INTERFACE_PART定义每一个接口项,END_INTERFACE_MAP() 在映射表中放入一个结束项。 注意INTERFACE_PART在定义接口项时使用了宏offset来计算成员与父类指针的偏移量: #define offsetof(s,m) (size_t)&(((s *)0)->m) 其中s表示父类,m表示s的一个成员。以上宏表示s结构正好落在地址0上时其成员m的地址。 再看嵌套类的定义:

45 class CDictionaryObj : public CCmdTarget
{ DECLARE_DYNCREATE(CDictionaryObj) CDictionaryObj(); public: BEGIN_INTERFACE_PART(Dictionary, IDictionary) //定义接口 INIT_INTERFACE_PART(CDictionary, Dictionary) STDMETHOD_(BOOL, Initialize)(); STDMETHOD_(BOOL, LoadLibrary)(LPOLESTR); ...... END_INTERFACE_PART_STATIC(Dictionary) BEGIN_INTERFACE_PART(SpellCheck, ISpellCheck) INIT_INTERFACE_PART(CDictionary, SpellCheck) STDMETHOD_(BOOL, CheckWord)(LPOLESTR, LPOLESTR *); END_INTERFACE_PART_STATIC(SpellCheck) protected: DECLARE_INTERFACE_MAP() //定义了接口映射表。 private : struct DictWord *m_pData; };

46 #define BEGIN_INTERFACE_PART(localClass, baseClass) \
class X##localClass : public baseClass \ { public: STDMETHOD_(ULONG, AddRef)(); \ STDMETHOD_(ULONG, Release)(); \ STDMETHOD(QueryInterface)(REFIID iid, LPVOID* ppvObj); \ 宏BEGIN_INTERFACE_PART封装了嵌套类的定义。同时声明了接口的前三个成员函数AddRef,Release和QueryInterface。 #define INIT_INTERFACE_PART(theClass, localClass) \ size_t m_nOffset; \ INIT_INTERFACE_PART_DERIVE(theClass, localClass) \ #define INIT_INTERFACE_PART_DERIVE(theClass, localClass) \ X##localClass() \ { m_nOffset = offsetof(theClass, m_x##localClass); } \ 宏INIT_INTERFACE_PART定义了记录偏移量的数据成员m_nOffset,这个成员也是通过宏offset来完成的。 #define END_INTERFACE_PART(localClass) \ } m_x##localClass; \ friend class X##localClass; \ 嵌套类的成员名字为m_x##localClass。 以上宏定义中出现的##用来告诉编译器将两个字符串连在一起。

47 嵌套类的成员的实现方法: STDMETHODIMP_(ULONG) CDictionaryObj::XDictionary::AddRef() { METHOD_PROLOGUE_EX_(CDictionaryObj, Dictionary) return (ULONG)pThis->ExternalAddRef(); } #define METHOD_PROLOGUE_EX(theClass, localClass) \ theClass* pThis = ((theClass*)((BYTE*)this - m_nOffset)); \ AFX_MANAGE_STATE(pThis->m_pModuleState) \ pThis; // avoid warning from compiler \ 宏METHOD_PROLOGUE_EX中定义了指向父类的成员变量指针pThis,pThis由当前指针减去与父类的this指针的偏移量而得出。 theClass* pThis = ((theClass*)((BYTE*)this - m_nOffset)); 然后通过调用父类的函数实现。

48 使用CcmdTarget实现COM的方法小结
1。在CcmdTarget类和派生类中使用宏DECLARE_INTERFACE_MAP().声明接口映射表使用的静态成员变量和函数。 2。在类的实现部分使用BEGIN_INTERFACE_MAP, INTERFACE_PART,和END_INTERFACE_MAP定义接口映射表。在这里指定嵌套类所实现的接口,以及接口的ID。 3。在类的声明部分使用BEGIN_INTERFACE_PART, INIT_INTERFACE_PART,和END_INTERFACE_PART为每一个接口定义嵌套类成员。 4。实现嵌套类。实现嵌套类的时候会使用到父类指针pThis,如果要使用它,要先使用宏METHOD_PROLOGUE_EX定义pThis指针并给它赋值。

49 MFC对类厂的支持。 MFC提供了类COleObjectFactory(由于历史原因没有称作CComObjectFactory)我们在COM对象的类定义中使用宏DECLARE_OLECREATE()来加入嵌套类的类厂成员,以使得对象可以被客户程序调用CoCreateInstance函数创建。 DECLARE_OLECREATE(CDictionaryObj) #define DECLARE_OLECREATE(class_name) \ public: static AFX_DATA COleObjectFactory factory; \ static AFX_DATA const GUID guid; \ 宏定义了一个静态的嵌套类类厂成员,和一个GUID。 然后使用IMPLEMENT_OLECREATE宏来对类厂成员进行初始化,并给GUID赋值。 IMPLEMENT_OLECREATE(CDictionaryObj, "Dictionary.Object", 0x54bf6567, 0x1007, 0x11d1, 0xb0, 0xaa, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00) #define IMPLEMENT_OLECREATE(class_name, external_name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ AFX_DATADEF COleObjectFactory class_name::factory(class_name::guid, RUNTIME_CLASS(class_name), FALSE, _T(external_name)); \ AFX_COMDAT const AFX_DATADEF GUID class_name::guid = \ { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } }; \ COM对象的CLSID和类型信息传递给类厂的构造函数。

50 小结:使用COLeObjectFactory实现类厂
1。在COM类的声明中加入嵌套类类厂成员。 DECLARE_OLECREATE(CDictionaryObj) 2。在COM类的实现中加入类厂的定义 IMPLEMENT_OLECREATE(CDictionaryObj, "Dictionary.Object", 0x54bf6567, 0x1007, 0x11d1, 0xb0, 0xaa, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00) 几个引出函数已经由AppWizard实现。


Download ppt "六 . COM接口的其他实现方法 基于表格驱动的接口查询 接口查询的本质 宏 应用 多重继承下的名字冲突 潜在的缺陷 临时的方案"

Similar presentations


Ads by Google