如何正确地实现QueryInterface
在Windows编程中,在与系统组件打交道时,我们会经常用到COM接口,对于 QueryInterface() 函数不会感到陌生。相较之下,实现COM接口的需求少得多,一旦需要我们自己来实现COM接口,缺乏经验者很可能会在一些平时不会注意到的问题上碰壁。我最近就在实现 QueryInterface() 的时候遇到了一点麻烦,花了不少时间来排查,最终发现起因原来只是一个低级错误。
事情的背景是这样的:我要往RichEdit中插入一个OLE对象,需要实现一个自定义的类,这个类要实现 IOleObject 和 IViewObject 两个COM接口。我仿照从其它地方找到的示例,实现了这样的 QueryInterface() :
1 | class MyOLEObject : public IOleObject, public IViewObject { |
这个实现看上去没什么问题, MyOLEObject 仅支持 IUnknown 、 IOleObject 和 IViewObject 三个COM接口,所以判断 riid 是这三个接口的ID就返回this,很合理。
但是程序一运行起来就出现奇怪的问题,每当插入这个OLE对象的时候, IOleObject 的 SetClientSite() 函数会被调用,传进来的指针的值居然是1,导致我一用到这个指针,程序就崩溃了。
我百思不得其解,排查了很久之后才发现是 QueryInterface() 的实现错了,正确的实现应该是下面这样:
1 | class MyOLEObject : public IOleObject, public IViewObject { |
这个实现把 IViewObject 的判断单独分离出来,并且先把this转换成了 IViewObject* 再返回。
这背后的原因是:多重继承导致类对象的内存布局发生变化,this并不等于 IViewObject* ,所以不能直接将this作为 IViewObject* 返回。
我们先来看一下 MyOLEObject 的继承关系图:
这是一个菱形的继承结构,在这个结构下 MyOLEObject 对象的内存布局是这样的:
绿色部分是继承自 IOleObject 的数据,蓝色部分是继承自 IViewObject 的数据,白色部分是 MyOLEObject 自己的数据。由于基类都是COM接口,不包含任何成员变量,所以绿色和蓝色的数据实际上都只包含一个虚函数表指针(绿色的 IUnknown 和 IOleObject 共用一个虚函数表,蓝色的 IUnknown 和 IViewObject 共用一个虚函数表)。
从图中可以看出,this和 IOleObject* 都指向相同的绿色部分,所以在 QueryInterface() 内,如果 riid 是这个接口的ID,可以直接返回this。但是 IViewObject* 指向的是蓝色部分,跟this不一样,所以不能直接返回this。在一开始错误的实现中,我把this强制转型成 void* 返回,返回的是绿色部分的数据,即把 IOleObject 错当成了 IViewObject 返回。当外部调用 IViewObject 的函数时,就会错误地调用到了 IOleObject 的函数,所以参数的值都是错乱的。
而在正确的实现中,我通过 static_cast 将this转换成 IViewObject* ,编译器会正确地取出指向蓝色部分的指针。
最后再提一下 IUnknown 。由于 IOleObject 和 IViewObject 各自继承了 IUnknown ,所以在最终的对象内有两个 IUnknown 的虚函数表指针,它们其实都指向同一份函数实现,所以理论上在 QueryInterface() 内返回任意一个 IUnknown* 都是可以的。但是要注意以下写法会导致编译失败:
1 | *ppvObj = static_cast<IUnknown*>(this); |
因为编译器不知道应该使用哪一份 IUnknown 数据,我们必须显式地指定,要么用 IOleObject 的,要么用 IViewObject 的。
综上所述,当我们在实现 QueryInterface() 函数的时候,正确的做法是依次将this转型成对应的COM接口指针再返回,而不是一刀切地强制转型成 void* 来返回。