如何正确地实现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*
来返回。