向上转换 COM 接口指针总是可以的
可以在此处以 MS-Word 格式找到实际规范(据我了解,Microsoft 不再托管它):
http://web.archive.org/web/20030201093710/http://www.microsoft.com/Com/resources/comdocs.asp
它非常容易阅读,并阐明了许多关于 COM 的细节,这些细节目前在 MS-docs 上并不明显。
根据规范,COM 接口指针被定义为指向函数指针数组(称为 VTable)的指针。VTable 中每个条目的第一个参数是接口指针本身。一个接口“继承”另一个接口,首先列出另一个接口的功能,然后是它自己的功能。请注意,此要求意味着仅支持单继承,如 COM 规范第 2 章第 1.2 部分(强调我的)中所述:
接口和继承
COM 将类层次结构(或实际上任何其他实现技术)与接口层次结构以及任何实现层次结构中的两者分开。因此,接口继承仅适用于重用与基本接口相关联的契约的定义。COM 中没有选择性继承:如果一个接口从另一个接口继承,它包含了另一个接口定义的所有功能,出于同样的原因,一个对象必须实现它继承的所有接口函数。在 COM 接口中很少使用继承。大多数预定义接口直接从 IUnknown 继承(接收基本功能,如 QueryInterface),而不是从另一个接口继承以添加更多功能。因为 COM 接口继承自 IUnknown,所以它们往往很小并且彼此不同。这将功能保留在单独的组中,这些组可以独立于其他接口进行更新,并且可以以语义上有用的方式与其他接口重新组合。另外,接口只使用单继承,从不多重继承,从基接口获取函数。否则会显着使接口方法调用序列复杂化,这只是一个间接函数调用,此外,多重继承的实用性包含在 QueryInterface 提供的功能中。
请注意最后一部分:COM 设计专门使用单继承,以便从子接口调用基本接口很简单,即专门这样您就可以强制转换接口指针。
接口 VTable 的结构(因此,指针转换的可行性)也在其他几个地方得到证实。请参阅 IDL 描述中的继承描述,第 13 章,第 1.4 部分(强调我的):
接口继承
支持接口的单一继承,使用 C++ 表示法。再次参考 [CAE RPC],第 238 页:
<interface_header> ::=
<[> <interface_attributes> <]> interface <Identifier> [ <:> <Identifier> ]
例如:
[object, uuid(b5483f00-4f6c-101b-a1c7-00aa00389acb)]
interface IBar : IWazoo {
HRESULT Bar([in] short i, [in] IFoo * pIF);
};
案例IBar 中的第一个方法是 IWazoo 的方法。
在第 3 章第 1.3 部分(强调我的)中描述从 IUknown 的继承时,甚至明确指出指针转换是可以的:
IUnknown 接口
该规范已经多次提及 IUnknown 接口。它是 COM 中的基本接口,不仅包含所有对象的基本操作,还包含所有接口的基本操作:引用计数和 QueryInterface。COM 中的所有接口都是 IUnknown 多态的,也就是说,如果您查看任何接口中的前三个函数,您会看到 QueryInterface、AddRef 和 Release。换句话说,IUnknown 是所有其他接口继承的基本接口。任何单个对象通常只需要 IUnknown 成员函数的单个实现。这意味着通过在对象上实现任何接口,您就可以完全实现 IUnknown 函数。您通常不需要显式继承或实现 IUnknown 作为它自己的接口:当查询它时,只需将另一个接口指针类型转换为 IUnknown* ,这对于多态是完全合法的。
我们还可以通过检查这些接口的头文件定义来确认接口布局,这些接口都包含一个 vTable,其中包含基本 vTable 作为第一个元素(以 d2d1.h 为例):
typedef struct ID2D1ResourceVtbl {
IUnknownVtbl Base;
STDMETHOD_(void, GetFactory)(ID2D1Resource *This, ID2D1Factory **factory) PURE;
} ID2D1ResourceVtbl;
事实上,在调用基接口方法时可能用来避免强制转换的函数实际上是执行强制转换的宏!
#define ID2D1Resource_QueryInterface(this,A,B) (this)->lpVtbl->Base.QueryInterface((IUnknown*)(this),A,B)
^^^^^^^^^
指针向上转换不仅仅是巧合,它既受规范保证,又可在您的头文件中验证。
解决其他人提出的问题:
指针调整呢?
因为 COM 只支持单一的纯虚拟继承,所以在执行强制转换时永远不需要调整指针。这是经过深思熟虑的,因为转换时的指针调整是非常 C++ 特定的行为,并且 COM 试图独立于语言。您可以通过在转换 COM 接口指针时查看 C++ 编译器输出的机器代码来确认这一点。不会有任何调整。
基于指针实际指向哪个接口的不同接口方法实现呢?
这就是为什么您总是使用 vTable 调用函数的原因!这就是多态的魔力:一个函数可以根据对象的类型动态地调度,而调用者不需要知道对象的类型。理论上,一个对象可以根据您用来调用给定接口方法的接口指针做不同的事情,但这是它的业务,而不是客户端的业务。(编辑:我在下面添加了一些关于这个和引用计数的更多细节)
但是不同接口的单独引用计数呢?
同样,这就是为什么您总是使用 vTable 调用函数的原因。保证呼叫被分配到正确的地方。否则,有一个 vTable 是没有意义的(我们可以只使用静态调度)。请注意,每当您制作接口指针的副本时,都应该调用 AddRef,无论该副本是否使用强制转换。
最后说明/警告:
- 仅仅因为您总是可以转换为基本接口并不意味着您获得的指针与调用 QueryInterface 时返回的指针相同。这个细节在大多数时候并不重要,但如果你曾经检查过两个接口是否属于同一个 COM 对象,它就会发挥作用。您不能直接比较接口指针;您必须比较通过在两者上调用 QueryInterface(IID_IUnknown, ...) 返回的值。
- “向上”转换(隐式转换)在 C++ 中也是完全有效的,因为 C++ 编译器需要符合与 COM 布局类似的 ABI 要求(并非巧合)。
- 指针转换不适用于“交叉”转换或“向下”转换,只能用于“向上”转换(C++ 中的隐式转换)。如果 I1 不从 I2 继承,则不能强制转换为 I2。
- 接口方法的第一个参数只能是您从中获得 lpVtbl 的接口指针。这样的错误在 C 中应该很容易发现,在 C++ 中甚至是不可能的。
- 值得注意的是,在 MS-docs 上托管的有关 COM 的当前文档在原始规范的基础上进行了轻微编辑,以删除许多具体细节和现实生活中的示例。具体来说,很难找到涉及 C 的示例/解释,这可能会误导读者相信围绕继承的规则只是遵循 C++ 继承规则,而不是其中非常严格的子集。
简而言之:
您使用类型转换给出的示例将起作用,因为 ID3D12GraphicsCommandList 继承自 ID3D12CommandList。
编辑:关于接口与实现的更多说明:
如上所述,COM 对象将不同的指针从 QueryInterface() 返回到通过向上转换获得的指针是完全合法的。有人指出,这为接口 vTable 中的指针指向完全不同的函数打开了大门,因此在调用时会导致不同的对象行为。虽然 COM 对象以 API 使用者可见的方式执行此操作会令人难以置信的混乱和可怕,但根据 COM 规范,它仍然是合法的。不过,这并不意味着“始终使用 QueryInterface() 并且从不向上转换指针”是个好建议。
注意:为了减少混淆,我将从子接口获得的行为(与投射指针时获得的行为相同)称为“继承”行为,而 QueryInterface 返回的接口行为将是“查询”的行为。
首先:不同的“继承”和“查询”行为将是一件很奇怪的事情,任何合理的设计者都必须将该信息放入对象的文档中。API 使用者总是试图解释这种怪异的可能性是没有意义的,就像仅仅在星期二调用 COM 方法是没有意义的,因为它不符合规范一个 COM 对象,以根据星期几更改其行为。
其次:与“继承”行为相比,不能保证“查询”行为是您想要的。事实上,如果我们的理论对象被刻意设计为具有不同的每个接口行为,您可能会想要“继承”行为,因为它可能更适合您一开始对子接口所做的任何事情。
最后:引用计数将继续正常工作,即使不同的接口对应不同的引用计数。我在上面讨论了这个问题,但在这里我会更具体。无论其类型如何(相同类型!= 相同指针,即使它们来自同一个对象),您都应该始终在用于调用 AddRef 的同一指针上调用 Release。因为对 AddRef 和 Release 的调用是动态分派的(拥有 vTable 的全部意义!),在正确的指针上调用 Release 将减少正确的引用计数。如果处理不当,避免指针向上转换将无法挽救您。
顺便说一句:您的代码不应出于任何原因使用引用计数的确切值。COM 公开此值仅用于调试目的(例如,当您跟踪内存泄漏时),它不适合一般程序使用。客户端代码唯一关心的是在正确的时间调用“AddRef”和“Release”,其余的由服务器负责。
以下是有关参考计数的 MS-Docs 上的一些信息,可能会有所帮助:
https://docs.microsoft.com/en-us/windows/win32/learnwin32/managing-the-lifetime-of-an-object