上古时代的COM组件

Win32 COM组件的调用规范指明了一个对象的虚函数表(vtable)的内存布局。如果语言/编译器希望支持某个COM组件,则它必须按照规范指明的布局来放置对象,否则就会失去COM组件的跨语言互操作性,也即:其他组件将不能正常的调用这个组件。

Win32 COM组件的内存布局和C++对象的内存布局具有高度的一致性,这并非巧合。
尽管COM最初被开发的时候,C语言还占据着主导地位,COM的设计者就看到了COM组件对即将到来的C++十分“友好”。

一个COM组件的内存布局被定义在接口定义的头文件中。举个例子,下图的代码来自objidl.h,它定义了IPersist接口。为了方便观看,我去掉了一些无关的宏。

打开网易新闻 查看精彩图片

我们来看它的内存布局:

打开网易新闻 查看精彩图片

这是什么意思呢?

一个COM接口指针,实际是一个指向上述结构体的指针,这个结构体里只定义了一个虚函数表,没有定义任何数据成员。虚函数表中包含了接口所支持的各种接口方法的指针。每个接口方法会将接口指针(上图中的p)作为它的第一个参数,也就是大家所熟悉的this指针。

黑魔法就在于,因为接口方法可以从它的第一个参数中得到接口指针,所以你可以在虚函数表中添加其他的东西,如下图所示:

打开网易新闻 查看精彩图片

位于虚函数表中的接口方法可以使用相对接口指针p的偏移来访问你所添加的东西,例如数据成员。

如果一个对象实现了多个接口,但是它们都继承自同一个祖先,则我们只需要一个单一的虚函数表就可以访问接口的所有成员。
举个例子,上面的对象可以被当作一个IUnknown接口或者一个IPersist接口,因为IUnknown是IPersist的父接口。

另一方面,如果一个对象实现了多个接口,但是不继承同一个祖先,则你就得到了一个多重继承,在这种情况下,对象的内存布局类似于下图:

打开网易新闻 查看精彩图片

我们来对上图做一个解析。
如果你使用来自于第一个虚表的接口方法,则接口指针为p。
但是,如果你使用来自第二个虚表的接口方法,则接口指针为q。

总结

我会在后面的文章中继续对上图进行一些额外的分析,例如神秘的“adjustor thunks”。
请期待。

最后

Raymond Chen的《The Old New Thing》是我非常喜欢的博客之一,里面有很多关于Windows的小知识,对于广大Windows平台开发者来说,确实十分有帮助。
本文来自:《The layout of a COM object》

打开网易新闻 查看精彩图片