有一个名叫Animal的游戏程序,它内部维护了一个二叉树,可以通过询问玩家问题来沿着一个分支来向答案推荐,以此猜测玩家心中所想的动物(Animal),同此程序并不知道用户所假想的动物是什么一样,COM客户也并不知道一个组件所支持的接口是什么。为知道某个组件是否支持某个特定的接口,客户可以在运行时询问组件。
即使最后猜到玩家心中的答案,但“Animal”程序对于此动物的了解也是停留在几个表象特征,他并不能像玩家一样对他所猜到的答案有一个完整的认知,COM客户同理,客户可以询问组件是否支持某个特定的接口。在进行多次这种询问之后,客户对于组件的认识将越来越清晰。但不管怎样,客户决不可能对组件有一个完整的了解。不过这并不是一个负面影响,因为我们本来就希望客户对于组件尽可能少了解一些。
Animal 对于动物所知有限的原因在于其实现方式的负面影响,但客户对于组件的不甚了解却不是一种负面效果。因为我们本来就希望客户对于组件要尽可能地少了解一些。事实上客户对于组件知道得越少,那么在不影响客户正常运行的情况下组件就可能最大限度地发生变化。客户只是在它需要使用某种接口时才询问组件,那么只是在改变这些接口时才会影响到客户。在不改变与客户接口的情况下,可以用新的组件来代替已有的组件而不会对客户造成任何影响。对于客户不使用的接口,新组件用不着一定要支持。
在本章中我们将讨论客户如何向组件询问关于它所支持的接口、组件如何回答、以及这种请求应答方式的结果。另外我们还将看到这种使客户请求接口的方式如何提供了一个可以无缝地处理组件版本变化的强壮系统。
后面将会看到,即使组件不支持客户所需要的某个接口,客户也可以在请求失败时很好地处理这种情形。在本章中我们将讨论客户如何向组件询问关于它所支持的接口、组件如何回答、以及这种请求应答方式的结果。另外我们还将看到这种使客户请求接口的方式如何提供了一个可以无缝地处理组件版本变化的强壮系统。
接口查询
由于COM中的所有内容最终都起于接口、又最终归于接口,因此首先我们讨论一下可以通过一种什么样的接口来查询其他接口。客户同组件的交互都是通过一个接口完成的。在客户查询组件的其他接口时,也是通过接口完成的。这个接口就是IUnknown
。 IUnknown
接口的定义包含在Win32 SDK
中的UNKNWN.H
头文件中。为清楚起见,在此引用如下:
interface IUnknown
{
virtual HRESULT_stdcall QueryInterface(const IID& iid, void ** ppv) = 0;
virtual ULONG_stdcall AddRef() = 0;
virtual ULONG_stdcall Release()
}
在IUnknown中定义了一个名为QueryInterface
的函数。客户可以调用QueryInterface
来决定组件是否支持某个特定的接口。本章将主要讨论Querylnterface
。 下一章将讨论AddRef和Release,这两个函数可以用来控制接口的生命期。
关于IUnknown
所有的COM接口都需要继承IUnkonw,因此某个客户拥有IUnknown接口的指针,它并不需要知道它所拥有的接口指针到底是什么类型的,而只需知道此接口可以用来查询其他接口就行了。
由于所有的COM接口都继承了IUnknown,每个接口的vtbl中的前三个函数都是QueryInterface
、AddRef
和Release
(如图3-1所示)。这使得所有的COM接口都可以被当成IUnknown接口来处理。
- 若某个接口的vtbl中的前三个函数不是这三个,那么它将不是一个COM接口。
- 由于所有的接口都是从IUnknown继承的,因此所有的接口都支持QueryInterface。 因此组件的任何个接口都可以被客户用来获取它所支持的其他接口。
- 由于所有的接口指针同时也将是IUnknown指针,客户并不需要单独维护一个代表组件的指针,他所关心的仅仅是这个接口的指针。
IUnknown 指针的获取
那么客户如何获取一个指向IUnknown接口的指针呢?这里我们将用到一个名为Createlnstance的函数,它可以建立一个组件并返回一个IUnkown指针。
IUnkown * CreateInstanceQ;
在创建组件时,客户可以使用Createlnstance
而不必再使用new操作符。在本章中我们将实现此函数的一个简单版本,然后在后续的各章中将根据需要对之进行修改,第6章和第7章将介绍建立COM组件的正式方法。
在了解了客户如何获取一一个IUnknown指针之后,下面我们来看一下客户如何使用Querylnterface 来获取其他接口,然后再实际实现QueryInterface。
关于QueryInterface
IUnknown 中包含一个名为QueryInterface 的成员函数,客户可以通过此函数来查询某个组件是否支持某个特定的接口。
HRESULT_stdcall QueryInterface(const 1ID& iid, void ** ppv) ;
返回值:
- 若支持将返回一个指向此接口的指针
- 否则返回值将是一个错误代
参数:
const 1ID& iid
:标识客户所需的接口:此参数是一个“接口标识符”(IID)结构。
在第6章“关于HRESULT、GUID、登记簿及其他细节”中我们将详细讨论IID。在本章中读者只需将其当成是一个标识所需接口的常量。void ** ppv
:另外一个指针是QueryInterface存放所请求接口指引的地址。
QueryInterface返回的是一个HRESULT
值。此值实际上并不像其名称所表示的那样
是标识某个结果的 句柄 。相反,他是一个32位数。QueryInterface 可以返回S_OK
或E_NOINTERFACE
。客户不应将 QueryInterface 的返回值直接同这两个值进行比较,而应使用SUCCEEDED宏或FAILED宏。在第6章中我们将详细讨论HRESULT。
下面我们来看一下QueryInterface是如何使用的,然后再来介绍它是如何实现的。
QueryInterface 的使用
假定客户有了一个指向IUnknown的指针pI,为知道相应的组件是否支持某个特定的接口,可以调用QueryInterface,并传给它一个接口标识符。若QueryInterface成功返回,那么就可以使用它返回的指针了。这个过程可以用如下的代码表示:
void foo(IUnknown * pI)
{
// Define a pointer for the interface.为接口定义一个指针。
IX * pIX = NULL ;
// Ask for interface IX.请求接口IX。
//查询pI它是否支持由IID_IX所标识的接口
HRESULT hr = pI-> QueryInterface(IID_IX, (void ** ) &pIX) ;
// Check return value.检查返回值。
if(SUCCEEDED(hr))
{
// Use interface.使用界面
pIX -> Fx();
}
}
在上面的代码段中,我们查询了pI它是否支持由IID_IX所标识的接口。IID_IX的
定义在与此组件相应的头文件中,或它也可能是在某种类型的库中定义的。在第13章中我们将看到这一点。
注意,在调用Querylnterface之前,我们将pIX初始化为NULL,这是一种比较好的编程
方法。在后面的实现中我们将会看到QueryInterface在失败时将把返回的接口指针置为NULL。但由于QueryInterface是由程序员而不是由系统实现的,因此某些组件可能并不会在查询失败时将此指针置为NULL。因此为安全起见,在程序中还是我们自己将其置为NULL 比较好。·
关于如何使用QueryInterface的一些基础知识就介绍这么多了,后面我们将介绍一些QueryInterface使用的更为高级的技巧。但首先我们来看一下如何在我们的组件中实现QueryInterface。
Querylnterface的实现
Querylnterface需要根据某个给定的IID返回指向相应接口的指针。
- 若组件支持客户指定的接口,那么应返回S_OK以及相应的指针。
- 若不支持,返回值应是E_NOINTERFACE,并将相应的指针返回值置成NULL。
实现类CA中所实现的组件中的QueryInterface函数。
interface IX : IUnknown {/* ... */};
interface IY : IUnknown {/* ... */ł};
class CA : public IX, public IY {/ *...* /};
非虚拟继承:
注意 IUnkrown并不是虚拟基类。IX和IY并不能按虚拟方式继承IUnknown,这是由
于会导致与COM不兼容的vtbl。若IX和IY按虚拟方式继承IUnknown,那么IX和IY的vtbl 中的头三个函数指向的将不是IUnknown的三个成员函数。
下面实现的就是类CA中的QueryInterface。此处QueryInterface的实现可以返回三种不同接口的指针:IUnknown
、IX
及IY
。不过要注意的是返回的IUnknown指针都是同一个,即使CA继承了两个IUnknown接口(其中的一个来自于IX,另一个来自于IY)。
HRESULT_stdcall CA :: QueryInterface(const IID& iid, void ** ppv)
{
if (iid == IID_IUnknown)
{
// The client wants the IUnknown interface.客户端需要IUnknown接口。
* ppv = static_cast < IX* >(this) ;
}
else if (iid == IID_IX)
{
// The client wants the IX interface.客户端需要IX接口。
* ppv = static_cast < IX* >(this) ;
}
else if (iid == IID_IY)
{
// The client wants the IY interface.客户端需要IY接口。
* ppv = static_cast< IY* >(this) ;
}
else
{
// we don't support the interface the client wants. 我们不支持客户想要的接口。
//Be sure to set the resulting pointer to NULL.确保将结果指针设置为NULL。
* ppv = NULL ;
return E_NOINTERFACE ;
}
static_cast < IUnknown* >(* ppv) -> AddRef()// See Chapter 4.参见第四章
return S.OK ;
}
在上例中,QuertyInterface是用一个简单的if-then-else语句实现的。当然用其他任何
一种可以将一种类型映射成另外一种类型的结构也是可以实现的。例如可以使用数组
、哈希表
或者是树
来实现QueryInterface。在组件所实现的接口很多的情况下,使用这些结构可能会更好一些。但case语句是无法用的,因为接口标识符是一个结构而不是一个数。
在客户所查询的接口不被支持时,QueryInterface将*ppv
置为NULL
。这一点不但是COM规范所要求的,而且从其本身而言也是一种好的作法。将接口指针置为NULL
将会使不检查返回值的客户崩溃。这一点比起返回一个随机的代码位所造成的损害可能会小得多。另外在QueryInterface的末尾调用AddRef
实际上没有任何作用,在第4章中我们将实现 AddRef。
关于类型转换
可以注意到在上面的代码中,在将this指针保存在ppv中时,我们对之进行了类型的
转换。保存在ppv中的值将会根据所用的转换不同而不同。
将this指针转换成一个IX指针所得到的地址与将其转换成一个IY指针所得的地址是不同的,例如:
static_cast <IX* >(this) ! = static_cast < IY * >(this)
ctatic_cast <void *>(this) ! = static_cast < IY * >(this)
某些程序员更常用以下旧方式的类型转换:
(IX * )this ! = (IY * ) this
(void * )this ! = (IY* ) this
这样将this指针进行类型转换将会导致其值的改变,我们不要这样实现,这一点主要是由于C++的实现方式。为搞清楚究竟是为什么,可参阅附栏“多生继承及类型转换”。
一般在将this指针值赋给某个void指针时,应先将其转换成合适的类型。一个有趣
的例子是返回IUnknown指针的情形。某些程序员可能会用:
* ppv = static_cast < IUnknown * > (this) ; // Ambiguous 模棱两可
这样将this指针转换成IUnknown*是不明确的。这是由于IX和IY都是从IUnknown
继承得到的。因此在这种情况下,返回值应该是
static_cast<IUnknown*>(static_cast<IX*>(this))
或
static_cast<IUnknown*>(static_cast<IY*>(this))
只不过在上例中选择哪一个是无关紧要的,因为它们使用的都是同一实现。但是在代码中要保持一致,因这两个指针实际上是不一样的,并且COM要求对IUnknown接口返回相同的指针。本章的后面我们将讨论这种需求。
多重继承及类型转换
通常将一种类型的指针转换成另外一种类型并不会改变它的值。但为了支持多重继承,在某些情况下,C++必须改变类指针的值。许多C++程序员并不清楚多重继承的此种负面效果。例如假定有一个类CA定义如下,
class CA :public IX, public IY{……}
由于CA同时继承了IX和IY,因此在可以使用IX或IY指针的地方均可以使用指向CA的指针。例如可以将指向CA的指针传给接收IX或IY指针的函数,这样此函数仍将能够正常工作。例如:
void foo(IX* pIX);
void bar(IY* pIY);
int main()
{
CA * pA=new CA
foo(pA);
bar(pA);
delete pA ;
return 0 ;
}
foo需要一个指向合法的IX的虚拟函数表的指针,而bar则需要一个指向IY虚拟函
数表的指针。当然IX和IY的虚拟函数表中的内容是不一样的。因此在一个IX vtbl传
给bar时,此函数将不能正常工作。因此编译器将同一指针传给foo和ber是不可能的,它必须对 CA的指针进行修改以便它指向一个合适的vabl指针。图3-3显示了类CA对象的内存结构:
从图3-3可以看到,CA的this指针指向IX的虚拟函数表。因此可以在不改变CA的
this 指针的值的情况下用它来代替IX指针。但是从图中可以很明显地看出,类CA的this指针并没有指向IY的虚拟函数表指针。因此在将指向类CA的指针传给一个接收IY指针的函数之前,其值必须修改,为完成这种修改,编译器将把IY虚拟函数表指针的偏移量(△IY)加到CA的this指针上,因此编译器将把代码
IY* pC = pA
转换成
IY* pC = (char* )pA + AIY;
有关更为详细的讨论请参阅 Margaret A. Ellia 和Bjarne Stoustnp 所著《The Armotated C++ Reference Manual》中的 Sochion 10.3c,“Multiple Inheritance adCating”。但应说明的是,C++编译实现如图3-3所示的多重维承虚拟函数表。
一个完整的例子
下面我们将把前面所提到过和各代码段组合起来,以构成一个说明QueryInterface实
现及使用的完整例子。
此例子的代码总的来说可以将这些代码分成三部分:
- 第一部分是接口IX、IY和LZ的定义部分:
- 接口IUnknown的定义在Win32 SDK的头文件UNKNWN.H中。
- _stdcall:标记了的函数将在返回到调用者之间将参数从栈中删除。我们在常规的c/c++调用约定中,栈的清理工作则是由调用者完成。
// Interfaces 接口
interface IX : IUnknown
{
virtual void _stdcall Fx() = 0;
};
interface IY : IUnknown
{
virtual void _stdcall Fy() = 0;
};
interface IZ : IUnknown
{
virtual void _stdcall Fz() = 0;
};
- 第二部分是组件的实现:
- 类CA实现了一个支持IX和IY接口的组件。
// Component.组件
class CA : public IX, public IY
{
//IUnknown implementation IUnknown实现
virtual HRESULT _stdcall QueryInterface(const IID & iid, void** ppv);
virtual ULONG _stdcall AddRef()
{
return 0;
}
virtual ULONG _stdcall Release()
{
return 0;
}
// Interface IX implementation 接口IX实现
virtual void _stdcall Fx()
{
cout << "Fx" << endl;
}
// Interface IY implementation 接口IY实现
virtual void _stdcall Fy()
{
cout << "Fy" << endl;
}
};
- QueryInterface根据某个给定的IID返回指向相应接口的指针。
HRESULT _stdcall CA::QueryInterface(const IID& iid, void** ppv)
{
if (iid == IID_IUnknown)
{
trace("QueryInterface: Return pointer to IUnknown.(返回指向IUnknown的指针)");
*ppv = static_cast <IX*>(this);
}
else if (iid == IID_IX)
{
trace("QueryInterface: Return pointer to IX.(指向IX的返回指针)");
*ppv = static_cast<IX*>(this);
}
else if (iid == IID_IY)
{
trace("QueryInterface: Return pointer to IY.(指向IY的返回指针)");
*ppv = static_cast<IY*>(this);
}
else
{
trace("QueryInterface: Interface not supported.(不支持接口)");
*ppv = NULL;
return E_NOINTERFACE;
}
reinterpret_cast <IUnknown*> (*ppv)->AddRef(); // See Chapter 4.
return S_OK;
}
- 在类CA的末尾给出了Createlnstance的定义:
客户可以使用此函数来创建类CA所代表的组件并返回一个指向其IUnknown接口的指针。
// Creation function 创建函数
IUnknown* CreateInstance()
{
IUnknown* pI = static_cast <IX*> (new CA);
pI->AddRef();
return pI;
}
- 在定义好CreateInstance函数之后,下面定义的是各接口的IID结构。
// IIDS
//{32bb8320-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IX =
{ 0x32bb8320, 0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82} };
//{32bb8321-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IY =
{ 0x32bb8321,0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xe7, 0xb2, 0xd6, 0x82} };
//{32bb8322-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IZ =
{0x32bb8322,0xb41b,0x11cf,
{ 0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82 }};
从这些定义可以看出IID结构是一个相当大的结构。在第7章中我们还将再讨论这些结构。在链接此示例程序时,需要将其同UUID.LIB一块链接以获取IUnknown的接口标识符IID_IUnknown的定义。
- 第三部分是main函数:
它表示示例程序中的客户。
- 客户程序启动之后它将调用Createlnstance函数。此函数将返回一个指向组件的IUnknown接口的指针。
int main()
{
HRESULT hr;
trace("Client: Get an IUnknown pointer.(获取一个IUnknown指针)") ;
IUnknown* pIUnknown = CreateInstance();
- 客户将通过此IUnknown接口的QueryInterface函数而查询一个IX接口的指针。在决定查询是否成功时我们使用了SUCCEEDED宏。若客户成功地获取了IX接口的指针,它将使用返回的指针调用相应接口中的Fx函数。
trace("Client: Get interface IX.(获取接口IX)");
IX* pIX = NULL;
hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX);
if (SUCCEEDED(hr))
{
trace("Client: Succeeded getting IX.(成功获取IX)") ;
pIX->Fx();// Use interface IX. 使用接口IX。
}
- 接下来客户将使用IUnknown指针来获取一个IY接口的指针。若成功,它将使用返回的指针。
trace("Client: Get interface IY.(获取接口IY)") ;
IY * pIY = NULL;
hr = pIUnknown->QueryInterface(IID_IY, (void**)&pIY);
if (SUCCEEDED(hr))
{
trace("Client: Succeeded getting IY.(成功获取IY)");
pIY->Fy();// Use interface IY. 使用接口IY
}
- 由于类CA同时实现了IX和IY,因此在查询这些接口时肯定会成功。但是类CA并没有实现接口IZ,因此当客户查询IZ接口时,Querylnterface将会失败,此时它将返回E_NOINTERFACE。此时SUCCEEED宏的返回值将为FALSE,这种情况下客户将不会用plZ来访问IZ的成员函数。
trace("Client:Ask for an unsupported interface.(请求不支持的接口)");
IZ* pIZ = NULL;
hr = pIUnknown->QueryInterface(IID_IZ, (void**)&pIZ);
if (SUCCEEDED(hr))
{
trace("Client:Succeeded in getting interface IZ.(成功获取接口IZ)");
pIZ->Fz();
}
else
{
trace("Client:Could not get interface IZ.(无法获取接口IZ)");
}
- 与此同时,客户也可以通过一个IX接口指针plX来查询IY接口的指针。若组件支持IY接口,此查询将会成功,客户将可以像使用第一个指针那样使用返回的IY接口指针。
trace("Client:Get interface IY from interface IX.(从接口IX获取接口IY)") ;
IY* pIYfromIX = NULL;
hr = pIX->QueryInterface(IID_IY, (void**)&pIYfromIX);
if (SUCCEEDED(hr))
{
trace("Client:Succeeded getting IY.(成功获得IY)");
pIYfromIX->Fy();
}
- 最后客户通过IY接口指针来查询IUnknown接口。由于所有的COM组件都是从IUnknown接口继承的,因此,此查询请求也将成功。
trace("Client:Get interface IUnknown from IY.(从IY中获取接口IUnknown)");
IUnknown* pIUnknownFromIY = NULL;
hr = pIY->QueryInterface(IID_IUnknown, (void**)&pIUnknownFromIY);
- 通过IY查询返回的IUnknown接口指针pIUnknownFromIY同IUnknown接口指针pIUnknown是相同的
if (SUCCEEDED(hr))
{
cout << "Are the IUnknown pointers equal? (两个IUnknown指针相等吗?)"<<endl;
if (pIUnknownFromIY == pIUnknown)
{
cout << "Yes, pIUnknownFromIY == pIUnknown." << endl;
}
else
{
cout << "No, pIUnknownFromIY ! = pIUnknown." << endl;
}
}
// Delete the component. 删除组件。
delete pIUnknown;
return 0;
}
这就是COM的需求之一:Querylnterface对所有的IUnknown接口查询请求都必须返回相同的指针。这个例子表明,从任何其他的接口都可以使用OueryInterface来获取CA所实现的任意接口。这是指导Querylnterface接口实现的重要规则之一。下面我们将具体讨论一下QueryInterface的实现有一些什么样的规则。
- 完整代码
在使用Microsoft Visual C++编译此程序时可以使用命令 cl IUnknown.cpp UUID.lib。
// IUnknown.cpp
// To compile use: cl IUnknown.cpp UUID.lib
#include <iostream>
#include <objbase.h>
using namespace std;
void trace(const char* msg)
{
cout << msg << endl;
}
// Interfaces 接口
interface IX : IUnknown
{
virtual void _stdcall Fx() = 0;
};
interface IY : IUnknown
{
virtual void _stdcall Fy() = 0;
};
interface IZ : IUnknown
{
virtual void _stdcall Fz() = 0;
};
// Forward references for GUTDs 向前引用GUTDs
extern const IID IID_IX;
extern const IID IID_IY;
extern const IID IID_IZ;
// Component.组件
class CA : public IX, public IY
{
//IUnknown implementation IUnknown实现
virtual HRESULT _stdcall QueryInterface(const IID & iid, void** ppv);
virtual ULONG _stdcall AddRef()
{
return 0;
}
virtual ULONG _stdcall Release()
{
return 0;
}
// Interface IX implementation 接口IX实现
virtual void _stdcall Fx()
{
cout << "Fx" << endl;
}
// Interface IY implementation 接口IY实现
virtual void _stdcall Fy()
{
cout << "Fy" << endl;
}
};
HRESULT _stdcall CA::QueryInterface(const IID& iid, void** ppv)
{
if (iid == IID_IUnknown)
{
trace("QueryInterface: Return pointer to IUnknown.(返回指向IUnknown的指针)");
*ppv = static_cast <IX*>(this);
}
else if (iid == IID_IX)
{
trace("QueryInterface: Return pointer to IX.(指向IX的返回指针)");
*ppv = static_cast<IX*>(this);
}
else if (iid == IID_IY)
{
trace("QueryInterface: Return pointer to IY.(指向IY的返回指针)");
*ppv = static_cast<IY*>(this);
}
else
{
trace("QueryInterface: Interface not supported.(不支持接口)");
*ppv = NULL;
return E_NOINTERFACE;
}
reinterpret_cast <IUnknown*> (*ppv)->AddRef(); // See Chapter 4.
return S_OK;
}
// Creation function 创建函数
IUnknown* CreateInstance()
{
IUnknown* pI = static_cast <IX*> (new CA);
pI->AddRef();
return pI;
}
// IIDS
//{32bb8320-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IX =
{ 0x32bb8320, 0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82} };
//{32bb8321-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IY =
{ 0x32bb8321,0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xe7, 0xb2, 0xd6, 0x82} };
//{32bb8322-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IZ =
{0x32bb8322,0xb41b,0x11cf,
{ 0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82 }};
// Client
//
int main()
{
HRESULT hr;
trace("Client: Get an IUnknown pointer.(获取一个IUnknown指针)") ;
IUnknown* pIUnknown = CreateInstance();
trace("Client: Get interface IX.(获取接口IX)");
IX* pIX = NULL;
hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX);
if (SUCCEEDED(hr))
{
trace("Client: Succeeded getting IX.(成功获取IX)") ;
pIX->Fx();// Use interface IX. 使用接口IX。
}
trace("Client: Get interface IY.(获取接口IY)") ;
IY * pIY = NULL;
hr = pIUnknown->QueryInterface(IID_IY, (void**)&pIY);
if (SUCCEEDED(hr))
{
trace("Client: Succeeded getting IY.(成功获取IY)");
pIY->Fy();// Use interface IY. 使用接口IY
}
trace("Client: Ask for an unsupported interface.(请求不支持的接口)");
IZ* pIZ = NULL;
hr = pIUnknown->QueryInterface(IID_IZ, (void**)&pIZ);
if (SUCCEEDED(hr))
{
trace("Client: Succeeded in getting interface IZ.(成功获取接口IZ)");
pIZ->Fz();
}
else
{
trace("Client: Could not get interface IZ.(无法获取接口IZ)");
}
trace("Client: Get interface IY from interface IX.(从接口IX获取接口IY)") ;
IY* pIYfromIX = NULL;
hr = pIX->QueryInterface(IID_IY, (void**)&pIYfromIX);
if (SUCCEEDED(hr))
{
trace("Client: Succeeded getting IY.(成功获得IY)");
pIYfromIX->Fy();
}
trace("Client: Get interface IUnknown from IY.(从IY中获取接口IUnknown)");
IUnknown* pIUnknownFromIY = NULL;
hr = pIY->QueryInterface(IID_IUnknown, (void**)&pIUnknownFromIY);
if (SUCCEEDED(hr))
{
cout << "Are the IUnknown pointers equal? (两个IUnknown指针相等吗?)"<<endl;
if (pIUnknownFromIY == pIUnknown)
{
cout << "Yes, pIUnknownFromIY == pIUnknown." << endl;
}
else
{
cout << "No, pIUnknownFromIY ! = pIUnknown." << endl;
}
}
// Delete the component. 删除组件。
delete pIUnknown;
return 0;
}
运行结果
Client: Get an IUnknown pointer.(获取一个IUnknown指针)
Client: Get interface IX.(获取接口IX)
QueryInterface: Return pointer to IX.(指向IX的返回指针)
Client: Succeeded getting IX.(成功获取IX)
Implementing IX interface(实现IX接口)
Client: Get interface IY.(获取接口IY)
QueryInterface: Return pointer to IY.(指向IY的返回指针)
Client: Succeeded getting IY.(成功获取IY)
Implementing IY interface(实现IY接口)
Client: Ask for an unsupported interface.(请求不支持的接口)
QueryInterface: Interface not supported.(不支持接口)
Client: Could not get interface IZ.(无法获取接口IZ)
Client: Get interface IY from interface IX.(从接口IX获取接口IY)
QueryInterface: Return pointer to IY.(指向IY的返回指针)
Client: Succeeded getting IY.(成功获得IY)
Implementing IY interface(实现IY接口)
Client: Get interface IUnknown from IY.(从IY中获取接口IUnknown)
QueryInterface: Return pointer to IUnknown.(返回指向IUnknown的指针)
Are the IUnknown pointers equal? (两个IUnknown指针相等吗?)
Yes, pIUnknownFromIY == pIUnknown.
关于QueryInterface的实现规则
本节将给出一些Querylnterface的所有实现都必须遵循的一些规则,以便客户能够获取关于组件的足够多的知识并对之施实一些控制和其他有用的处理。如果没有这些规则,是不可能编写出组件的,因为在这种情况下,QueryInterface的行为将是不确定的。具体来讲,这些规则是:
- QueryInterface 返回的总是同一IUnknown指针。
- 若客户曾经获取过某个接口,那么它将总能获取此接口。
- 客户可以再次获取已经拥有的接口。
- 客户可以返回到起始接口。
- 若能够从某个接口获取某特定接口,那么可以从任意接口都将可以获取此接口。
下面我们将详细讨论这些规则。
同一IUnknown
组件的实例只有一个IUnknown接口。因为当查询组件实例的IUnknown接口时,不论通过哪个接口,所得到的均将是同一指针值。为确定两个接口是否指向同一组件,可以通过这两个接口查询IUnknown接口,然后将返回值进行比较。例如,下面代码中的Sarne-Components 函数可以决定pIX和plY是否指向同一组件中的接口:
BOOL SameComponents(IX * pIX, IY* pIY)
{
IUnknown * pIl = NULL ;
IUnknown * pI2 = NULL;
// Get IUnknown pointer from pIX.从pIX获得IUnknown。
pIX->QueryInterface(IID_IUnknown,(void ** )&pIl);
// Get IUnknown pointer from pIY.从pIY获得IUnknown。
pIY->QueryInterface(IID_IUnknown,(void ** )&pI2) ;
// Are the two IUnknown pointers equal?这两个IUnknown相等吗?
return pIl == pI2;
}
这条规则是极为重要的。如果Querylnterface的实现不遵循这条规则的话,则将没法决定两个接口是否指向同一组件。
客户可以获取曾经得到过的接口
若对于某个给定的接口,QueryInterface 曾经成功过,那么对于同一组件的后续QueryInterface将总是成功的。QueryInterface调用是失败的,那么后续的调用也将会失败。这一规则适用于组件的某个特定实例。当创建组件的一个新实例时,这条规则并不适用。
为什么需要这条规则呢?若组件所支持的接口会不时的发生变化客户代码的编写将是极为困难的。例如,客户应该在何时发出查询请求呢?应该按何种频率发出此种请求呢?当客户不再能获取它曾经使用过的某个接口时会发生什么情况呢?因此,若组件实现的接口集不是固定的,客户将无法通过编程的方法来决定一个组件到底具有一些什么样的功能。
可以再次获取已经拥有的接口
若客户拥有一个IX接口,则可以通过它来查询IX接口指针,并且一定可以成功。
例:
void f(IX* pIX)
{
IX* pIX2 = NULL ;
// Query IX for IX.
HRESULT hr = pIX -> QueeryInterface(IID_IX, (void ** )&pIX2) ;
assert(SUCCEEDED(hr)); //Query must succeed.
}
这条规则看起来可能会显得有些奇怪。客户为什么需要再次获取它已经拥有的接口呢?记住,所有的接口都继承了IUnknown,而许多函数都需要一个IUnknown指针作为参数。它们能够使用任何IUnknown指针来获取任何接口。
如下所示:
void f(IUnknown *pI)
{
HRESULT hr ;
IX * pIX = NULL ;
// Query pI for IX.
hr =[pI -> QueryInterface(IID_IX,(void ** )&pIX) ;
// Do something creative here.
}
void main()
{
// Get an IX pointer for somewhere.找一个IX指针。
IX* pIX= GetIX() ;
// Pass it to a function.
f(pIX);
}
函数f将能够使用传给它的接口指针来获取一个IX指针,即使此指针已经是一个IX指针。
客户可以从任何接口返回到起始接口
若客户拥有一个IX接口指针并成功地使用它来查询了一个IY接口,那么它将可以使用此IY接口来查询一个IX接口。不论客户所拥有的接口是什么,它都可以返回起始时所用的接口。如下面的代码所示。
void f(IX *pIX)
{
HRESULT hr ;
IX* pIX2= NULL;
IY* pIY = NULL ;
// Get IY from IX.
hr =pIX-> QueryInterface(IID_IY,(void ** )&pIY);
if (SUCCEEDED(hr))
{
// Get an IX from IY.
hr = pIY->QueryInterface(IID_IX,(void ** )&pIX2) ;
// QueryInterface must succeed.
assert(SUCCEEDED(hr));
}
}
若能够从某接口获取某特定接口.则从任意接口都将能够获取此接口
若能够从某个组件获取某特定接口,那么客户将可以通过此组件所支持的任意接口获取此接口。例如,若可以通过接口IX得到接口IY,通过IY可以得到Z,那么通过IX也将可以得到KZ。如下面的代码所示:
void f(IX*pIX)
{
HRESULT hr ;
IY * pIY = NULL;
// Query IX for IY.
hr = pIX-> QueryInterface(IID_IY, (void ** )&pIY) ;
if (SUCCEEDED(hr))
{
IZ * pIZ = NULL ;
// Query IY for IZ.
hr = pIY->QueryInterface(IID_IZ,(void ** )&pIz))
if (SUCCEEDED(hr))
{
// Query IX for IZ.
hr = pIX->QueryInterface(IID_IZ,(void ** )&pIz) ;
// This must succeed.
assert(SUCCEEDED(hr));
}
}
}
这条规则使得QueryInterface是可用的。可以想象一下若在获取某个特定接口的指针时,必须依赖于所使用的到底是哪个接口会发生什么情况。例如,当修改客户的代码并将其中的某个函数移到另外一个函数之前,那么整个系统可能就无法运行了。这样一来想开发出能够同此组件一块使用的客户几乎是不可能的。
制定上述规则的目的完全是为了使QueryInterface使用起来更为简单、更富有逻辑性、更一致以及更具确定性。幸运的是在实现组件的QueryInterface时遵循这些规则时并不是什么困难的事情。并且只有当组件按照这些规则正确地实现了Querylnterface时,客户才不用为这些规则担心。值得注意的是QueryInterface使用和实现上的简单性并不会减少QueryInterface 对于COM的重要性。事实上再没有其他什么像Querylnterface对于 COM那样重要了。
QueryInterface 定义了组件
我们说 QueryInterface是COM最为重要的部分,这主要是因为一个组件实际上就是由QueryInterface定义了。组件所支持的接口集就是QueryInterface能够为之返回接口指针的那些接口。这一点是由QueryInterface的实现决定的,而不是由实现组件的C++类决定的。实现组件的类的继承层次关系也不能决定组件。一个组件仅仅是由QueryInterface的实现决定的。
由于客户并不知道 QueryInterface的实现,因此,它将无法知道一个组件所支持的所有接口。客户了解组件所支持接口的唯一方法是进行查询。这一点同C++有着很大的不同。在那里,某个类的客户可以知道该类的所有成员,这是因为它拥有此类的头文件。从某种意义上,COM更类似于在某次社交聚会上同某人会面,而与对他们进行工作面试有很大的不同。当进行工作面试时,被试者将提交一份介绍他们情况的个人简历。这份个人简历是类似于C++类的定义。而当在社交聚会上会面时,没有人会给对方提供个人简历。为了解对方的情况,必须向他们提问。这一点是类似于COM组件的。
接口集
作者开始学习COM时所考虑的第一个问题是为什么不能向组件询问它所支持的所有接口呢?关于这个问题作者得到的回答是:你想对组件所支持的接口列表进行什么处理呢?虽然这一回答并没有从正面回答问题,但却不失为一个好的回答。
例如对于前面例子中的组件,它能够支持接口IX和IY。假定客户的编码是在此组件的编码之前完成的,并且此客户并不知道接口IY。当客户创建此组件并查询其所有接口,然后组件返回IX和IY。由于客户并不能识别IY,因此,对于此接口不能进行任何处理。为使客户能够对它所不能识别的接口进行一些有意义的处理,客户需要重新阅读关于组件的文档,然后编写处理此接口的代码。从目前的技术水平而言,这一点是不可能的。因此,一个组件所能支持的接口只是那些接口程序员所知道的接口。与此类似,客户所支持的接口只是其程序员所知道的那些接口。
但COM也确实提供了一个名为类型库的手段,供客户在运行时确定组件所提供的接口。客户可以使用类型库来决定接口中某个函数的参数,但客户仍然不可能知道如何编写出能够使用这些函数的代码,这项工作仍然需要程序员来完成。关于类型库我们将在第11章中讨论。
在许多情况下,客户可以使用只实现某个特定接口集的组件。创建一个组件,然后一个个地查询其组件,以最终找出此组件是否支持某个所需的接口是非常浪费时间的。为节省时间开销,某个特定的接口集可以用一个组件类别来标识。各组件可以声明它是否属于某个特定的组件类别。客户可以在不创建组件的情况下获取此种信息。这些内容将在第6章详细讨论。
QueryMultipleInterfaces
分布式COM(DCOM)定义了一个新接口IMultiQl。此接口有一个新的成员函数QuieryMultiplelnterfaces。使用此函数,客户可以通过一次调用而查询组件的多个接口。这主要是为了减少数据在网络土来回传输的次数,以提高应用程序的效率。
下面我们来看一下QueryInterface非常规的用途之一,即处理新版本的组件。
新版本组件的处理
在COM中接口是不会发生变化的,当组件发布一个接口并被某个客户使用之后,此接口将决不会发生任何变化,而将永远保持不变。所以每一个接口都有一个唯一的接口标识符(IID)。一般情况下,我们不会改变接口,而可以建立一个新接口并为之指定一个新的IID。Querylnterface接收到对老IID的查询时,它将返回老的接口。而当它收到对新的IID的查询时,它将返回新的接口。就QueryInterface而言,一个IID就是一个接口。
所以同某个IID相应的接口将绝不会发生变化。新接口可以继承老接口,它也可以同老接口完全不同。由于老的接口仍然保持不变,已有客户的运行将不会受到任何影响。而新客户则可以自行决定是使用老接口还是新接口,因他可以自由决定到底是查询哪个接口。
这种处理多个版本的方法最有效的地方在于它是无缝的。客户不需要做任何附加的工作即可确信它使用的是接口的正确版本。若它能够查找到某个接口,那么这个接口一定是正确的。接口的标识同其版本是完全绑在一块的。若版本发生了变化,那么接口也将发生变化。这里不会发生混淆。
下面有一个例子:
有一个名为Pilot的飞行模拟程序。它使用了由许多不同厂商开发的组件所实现的模拟飞行器。为同Pilot一块工作,飞行器组件必须实现一个名为IFly的接口。假定某个公司开发了一个名为Bronco的飞行器组件,并且它支持IFly接口。随后我们决定将Pilot升级成一个名为FastPilot的新版本。在FastPilot中,通过提供另外一个名为IFlyFast的接口来扩展一架飞机所能表演出的动作。与此相应,销售Bronco的公司在其中加入了IFlayFast接口,从而得到一个新版本的组件FastBronco。
由于 FastPilot仍然支持IFly,所以如果用户有一份Bronco,FastPilot也将能够使用它。此种情况下,FastPilot将首先查询IFly,若被查询的组件不支持此接口,那么它将继续查询IFly。同样,由于FastBronco仍然支持IFly,因此当某些用户仍然在使用原有的Pilot时,FastBronco也将能够同它一块运行。图3-4显示出了各种可能的互连关系。
可以看到不论按何种组合方式,客户和组件都将能够正常工作。在某些情况下,新版本的组件或客户要保持后向的兼容或许是不可能的,诸如需要付出极大的工作量,或具有极高的技术难度。但COM处理多个版本的手段在保持后向兼容不可能的情况下同样是有效的。因为接口的IID决定了它的版本。当客户获取某个接口时,由于不同版本的接口实际上是不同的接口,它们各自具有不同的ID,因此客户仍将能够得取正确版本的接口。
何时需要建立一个新版本
为使COM处理多个版本的机制能够起作用,程序员在给某个已有接口的新版本指定新的ID时应非常严格。当改变了下列条件中的任何一个时,就应给新接口指定新的ID:
- 接口中函数的数目。
- 接口中函数的顺序。
- 某个函数的参数。
- 某个函数参数的顺序。
- 某个函数参数的类型。
- 函数可能的返回值。
- 函数返回值的类型。
- 函数参数的含义。
- 接口中函数的含义。
总之,只要是所做的修改将会导致已有客户的正常运行,都应为接口指定新的ID。(当然若能够同时修改客户和组件,则对于上述规则可以灵活掌握。)
不同版本接口的命名
在建立了某个接口的新版本时,也应相应地修改其名称。**COM关于新版本名称的约定是在老名称的后面加上一个数字。**在此种约定下,IFly的新版本名称将是IFIy2而不是IFlyFast。当然对于用户自己建立的接口,可以按自己的喜好指定名称。若老接口是其他人建立的,那么在建立其新版本或指定一个新名称之前应先询问一下别人是否允许。
隐含合约
仅保证函数名称及参数不变并不足以保证对组件的修改不会妨碍客户的正常运行。因为客户将按照一定的方式或次序来使用接口中的函数。若组件的实现发生变化之后,此种调用方式或次序不再能正常工作,则客户也将因此而不能正常运行。
关于这个问题我们可以从另外一个角度来考虑。法律合约一般被认为是非常精确的文档,它精确地说明了所涉及到的合约各方的责任。但不论合同怎么短小或平常,其中总会有一些破绽。这些破绽就是那些当事人在签约时没有仔细考虑,而后又因此损失成千上万财富的句子。在这里合约所使用的字体大小是无关紧要的——合同的法律约束力并不会因此减少什么。
接口就是客户同组件之间的一个合约。同所有合约一样,接口也会有破绽。只不过对于接口而言,容易出破绽的地方在于接口被使用的方式。客户使用接口中函数的方式定义了它同实现此接口的组件之间的合约。若组件对接口的实现被修改了,它应保证客户仍然能够按同一方式来使用此接口中的函数。否则客户将因无法正常工作而不得不重新编译。例如,若客户按如下次序调用函数Foo1、Foo2及Foo3。若组件被修改之后,Foo3必须首先被调用,那么此组件实际上就违反了指定接口中函数可以被如何以及按何种次序被使用的隐含合约。
所有的接口都有一些隐含合约。这种合约只是在想用一种将会妨碍对接口已有使用的方法来实现它时才会引起问题。为避免违反隐含合约,可以使用两种方法。第一种方法是使得接口不论在其成员函数怎么被调用都能正常工作。第二种方法是强制客户按一定的方式来使用此接口并在文档中将这一点说明清楚。这样一来当因组件的变化而使客户无法正常运行时,所违反的将不再是隐含规则而是那些明确的规则。不论使用哪种方法,都需要开发人员具有高度的洞察力与仔细的规划。
本章小结
QueryInterface 是将编写COM组件的过程同编写C++类的过程区分开的一种特性。COM组件大部分的灵活性及其封装的能力都是由Querylnterface提供的。它使得客户能够在运行时决定一个组件所能提供的功能,并最大限度地利用动态链接。通过将组件的功能完全地向客户隐藏来,QueryInterface 可以尽可能地防止组件实现的变化对客户造成的影响。同时QueryInterface也是一种极好的实现对组件版本无缝处理的机制。这种机制使得组件的新旧不同版本可以互操作,从而能够一块工作。
在这一章中我们也讨论了IUnknown这一所有其他接口都支持的根接口。而QueryInterface 只不过是IUnknown三个成员函数之一。在下一章中我们将讨论如何使用IUnknown的另外两个成员函数AddRef和Release 来代替在我们的例子中一直在使用的delete操作符。但在开始阅读下一章的内容之前,可以再看一下游戏Animal。
你需要完成将组件从内存中释放之类的处理吗?
是
你需要处理引用计数吗?
是
你是AddRef吗?
否
你是谁?
Release
你同AddRef有什么区别呢?
减少引用计数值