COM所有内容起于接口,终于接口。
定义COM接口,就是定义vtbl(虚拟函数表)结构。
COM规范对接口(vtbl结构)强制约定如下:
接口一旦公布,vtbl结构不应再变化;
接口在所有子孙级别上的所有派生类,以及每个派生类的所有实例,它们的vtbl结构必须和该接口定义的vtbl结构一致;
接口必须继承IUnknown,使vtbl结构的前三个函数固定为QueryInterface/AddRef/Release;
接口都不能以虚拟方式继承IUnknown(否则该接口的前三个函数指向的将不是IUnknown三函数)
IUnknown接口:
有且仅有三个成员函数QueryInterface/AddRef/Release
不是虚拟基类,也不应被虚拟继承。
Win32 SDK的UNKNWN.H头文件直接支持了IUnknown接口,其定义如下:
interface IUnknown
{
virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) = 0;
virtual ULONG __stdcall AddRef() = 0;
virtual ULONG __stdcall Release() = 0;
}
其他平台或函数库,也许未对IUnknown直接支持,此时需要程序员自行定义。
COM接口最自然的实践,就是在Windows平台利用C++的纯抽象基类进行实现。
客户和组件越“不熟”越好。
CreateInstance可建立组件并返回其IUnknown指针
QueryInterface的Win32 C++原型
HRESULT __stdcall QueryInterface(const IID& iid, void** ppv)
意义 | 注意事项 | |
参数1 | 标识所查询的接口,IID指“接口标识符”(Interface ID) | 组件支持的IID应事先与COM组件一起公开发布(如存放在头文件或类型库中) |
参数2 | 查询到的接口指针地址 | 查询不到接口时应为空 |
返回值 | 具有特定结构的32位值(并非handle句柄) | 须用SUCCEEDED和FAILED两个宏判断,避免直接与S_OK或E_NOINTERFACE比较。 |
QueryInterface满足对称、传递、自反三大特性,具体规则如下:
实现 QueryInterface 的规则 - Win32 apps | Microsoft Learn
从集合论与图论的角度看问题,可将接口看作满足关系QueryInterface的闭包,并把组件看成是多个这种闭包相连的图。由于关系QueryInterface即是对称的又是传递的还是自反的,那么每个闭包之间都可以直接相连。
这个结论从COM对象的角度来描述就是,对象的任意一个接口,对于同样的QueryInterface请求,回答都应该是相同的。
三大特性也意味着COM对象必须满足:
对象必须具有唯一标识
对象的接口集必须是静态的(不随时间变化)
实现层面,往往使用一个类实现组件提供的多个接口,此时会产生多重继承关系如下:
interface IX : IUnknown {/*...*/};
interface IY : IUnknown {/*...*/};
class CA : public IX, public IY {/*...*/};
注意到IX和IY都是继承自IUnknown,也就意味着CA同时继承了两个IUnknown接口(一个来自IX,一个来自IY),将CA强制转换成IUnknown接口时会报错:
// 这句话会报错
*ppv = static_case<IUnknown*>(this);
所以当QueryInterface收到IID_IUnknown查询时,必须返回确定的派生对象:
HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv)
{
// 其他代码...
if (iid == IID_IUnknown)
{
// 这里一般返回和基类更接近的派生类对象
*ppv = static_case<IX*>(this);
}
// 其他代码...
}
(未完待续)