如何正确地实现QueryInterface

在Windows编程中,在与系统组件打交道时,我们会经常用到COM接口,对于 QueryInterface() 函数不会感到陌生。相较之下,实现COM接口的需求少得多,一旦需要我们自己来实现COM接口,缺乏经验者很可能会在一些平时不会注意到的问题上碰壁。我最近就在实现 QueryInterface() 的时候遇到了一点麻烦,花了不少时间来排查,最终发现起因原来只是一个低级错误。

事情的背景是这样的:我要往RichEdit中插入一个OLE对象,需要实现一个自定义的类,这个类要实现 IOleObjectIViewObject 两个COM接口。我仿照从其它地方找到的示例,实现了这样的 QueryInterface()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyOLEObject : public IOleObject, public IViewObject {
public:
HRESULT QueryInterface(REFIID riid, LPVOID* ppvObj) override {

if (!ppvObj) {
return E_INVALIDARG;
}

if (riid == IID_IUnknown ||
riid == IID_IOleObject ||
riid == IID_IViewObject) {

*ppvObj = (void*)this;
AddRef();
return S_OK;
}

*ppvObj = nullptr;
return E_NOINTERFACE;
}
};

这个实现看上去没什么问题, MyOLEObject 仅支持 IUnknownIOleObjectIViewObject 三个COM接口,所以判断 riid 是这三个接口的ID就返回this,很合理。

但是程序一运行起来就出现奇怪的问题,每当插入这个OLE对象的时候, IOleObjectSetClientSite() 函数会被调用,传进来的指针的值居然是1,导致我一用到这个指针,程序就崩溃了。

我百思不得其解,排查了很久之后才发现是 QueryInterface() 的实现错了,正确的实现应该是下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyOLEObject : public IOleObject, public IViewObject {
public:
HRESULT QueryInterface(REFIID riid, LPVOID* ppvObj) override {

if (!ppvObj) {
return E_INVALIDARG;
}

if (riid == IID_IUnknown || riid == IID_IOleObject) {
*ppvObj = static_cast<IOleObject*>(this);
AddRef();
return S_OK;
}

if (riid == IID_IViewObject) {
*ppvObj = static_cast<IViewObject*>(this);
AddRef();
return S_OK;
}

*ppvObj = nullptr;
return E_NOINTERFACE;
}
};

这个实现把 IViewObject 的判断单独分离出来,并且先把this转换成了 IViewObject* 再返回。

这背后的原因是:多重继承导致类对象的内存布局发生变化,this并不等于 IViewObject* ,所以不能直接将this作为 IViewObject* 返回。

我们先来看一下 MyOLEObject 的继承关系图:

这是一个菱形的继承结构,在这个结构下 MyOLEObject 对象的内存布局是这样的:

绿色部分是继承自 IOleObject 的数据,蓝色部分是继承自 IViewObject 的数据,白色部分是 MyOLEObject 自己的数据。由于基类都是COM接口,不包含任何成员变量,所以绿色和蓝色的数据实际上都只包含一个虚函数表指针(绿色的 IUnknownIOleObject 共用一个虚函数表,蓝色的 IUnknownIViewObject 共用一个虚函数表)。

从图中可以看出,this和 IOleObject* 都指向相同的绿色部分,所以在 QueryInterface() 内,如果 riid 是这个接口的ID,可以直接返回this。但是 IViewObject* 指向的是蓝色部分,跟this不一样,所以不能直接返回this。在一开始错误的实现中,我把this强制转型成 void* 返回,返回的是绿色部分的数据,即把 IOleObject 错当成了 IViewObject 返回。当外部调用 IViewObject 的函数时,就会错误地调用到了 IOleObject 的函数,所以参数的值都是错乱的。

而在正确的实现中,我通过 static_cast 将this转换成 IViewObject* ,编译器会正确地取出指向蓝色部分的指针。

最后再提一下 IUnknown 。由于 IOleObjectIViewObject 各自继承了 IUnknown ,所以在最终的对象内有两个 IUnknown 的虚函数表指针,它们其实都指向同一份函数实现,所以理论上在 QueryInterface() 内返回任意一个 IUnknown* 都是可以的。但是要注意以下写法会导致编译失败:

1
*ppvObj = static_cast<IUnknown*>(this);

因为编译器不知道应该使用哪一份 IUnknown 数据,我们必须显式地指定,要么用 IOleObject 的,要么用 IViewObject 的。

综上所述,当我们在实现 QueryInterface() 函数的时候,正确的做法是依次将this转型成对应的COM接口指针再返回,而不是一刀切地强制转型成 void* 来返回。