从子 COM 对象获取父 COM 对象

Get parent COM object from a child COM object

ID3D12GraphicsCommandList interface inherits from ID3D12CommandList。那么,如果我有一个 ID3D12GraphicsCommandList 对象,我如何获得相应的 ID3D12CommandList 对象?

  1. 类型转换有用吗?
    ID3D12GraphicsCommandList *gcl = ...;
    ID3D12CommandList *cl = (ID3D12CommandList*)gcl;
  1. QueryInterface 会工作吗?
    ID3D12GraphicsCommandList *gcl = ...;
    ID3D12CommandList *cl;
    HRESULT result = ID3D12GraphicsCommandList_QueryInterface(gcl,
                                                              &IID_ID3D12CommandList,
                                                              (void**)&cl);
  1. 我还需要做其他事情吗?

谢谢。

Will QueryInterface work?

是的。

Do I need to do something else?

没有

你的密码没问题。你也可以这样做:

ID3D12GraphicsCommandList *gcl = ...;
ID3D12CommandList *cl;
HRESULT result = gcl->lpVtbl->QueryInterface(gcl,
                                             &IID_ID3D12CommandList,
                                             (void**)&cl);
  1. Will typecasting work?

不,不是在 C 中。通过接口指针请求不同的接口可能需要调整指针。在这种情况下,简单地将指向一个接口的指针重新解释为指向另一个接口的指针将会中断(请参阅下文以获得更深入的探索)。

在 C++ 中,这可以通过提供一个 user-defined conversion function 来实现,尽管它非常脆弱,并且可以以微妙和不那么微妙的方式惊人地破坏。

  1. Will QueryInterface work?

是的。通过接口指针请求不同的接口是正确的方法。您提供的代码是正确的。

  1. Do I need to do something else?

不,不是真的,只要您遵守 COM 规则。一个细节经常被忽略:成功调用 QueryInterface 会增加接口上的引用计数,因此您必须 Release 从调用 QueryInterface.[= 返回的每个接口36=]


为什么转换不安全

那么,如果 IDerived 继承自 IBase 那么为什么明显的选择(从 IDerived* 转换为 IBase* 的指针)不是有效的? TL;DR 是,因为 COM 不提供使其有效的保证。

COM 的要求极简。 In fact,

The only language requirement for COM is that code is generated in a language that can create structures of pointers and, either explicitly or implicitly, call functions through pointers.

这允许使用范围广泛的编程语言来实现 COM 接口。不利的一面是 COM 对 ABI maps to language-level constructs. This is particularly true for interface inheritance:

的执行方式提供的保证很少

Inheritance in COM does not mean code reuse.

IDerived 的实现可以选择重用 IBase 的实现,或提供自己的实现。它还允许对 IBase 接口的调用具有不同的行为,具体取决于调用它的接口(IDerivedIBase)。这很灵活,但有一个缺陷,即不能保证通过指针转换来导航接口层次结构。

但还有更多! COM 还有另一个 rule,它非常容易理解,但经常被忽视:

From a COM client's perspective, reference counting is always done for each interface. Clients should never assume that an object uses the same counter for all interfaces.

同样,这为实现提供了很大的灵活性,但要求客户精心管理其接口指针。 QueryInterface 是实现用来跟踪未完成的接口引用的工具。转换指针回避了这个关键的管理任务,创造了一个机会,以一个引用计数为零的接口指针结束。

这些是规则和派生的保证。现在,在现实中,指针转换会出人意料地经常出现。因此,如果您是一名不太区分正确的代码和尚未失败的代码的开发人员,那么一定要继续前进,并指出您内心的喜悦。

另一方面,如果您是一名开发人员,以交付因正确而正常运行的软件为荣,那么 QueryInterface 总是 是必需的导航实现的界面表面。


好的,但 DirectX 实际上并不使用 COM!

没错。 DirectX 使用 COM 的一个小子集,通常称为 Nano-COM。虽然很多 COM 不适用,但 COM 的 ABI 方面适用。由于此答案仅讨论 ABI 方面,因此它同样适用于 COM 和 DirectX。

See Microsoft Docs.

如果 c++ 你已经有指向 ID3D12CommandList 的指针,因为 ID3D12GraphicsCommandList 继承自 ID3D12CommandList。如果 c 您需要正式转换 (ID3D12CommandList*)gcl 才能将其用作 ID3D12CommandList* 指针。在具体情况下不需要使用 QueryInterface - 当然可以使用,但这样做没有意义。再次 - 指向 ID3D12GraphicsCommandList 的指针 - 已经 也是指向 ID3D12CommandList 的有效指针。因为指向 ID3D12GraphicsCommandList 的指针 - 这是指向函数 (ID3D12GraphicsCommandListVtbl) 的 table 的指针,它与 ID3D12CommandListVtbl 布局兼容(包含在开头)。所以 ID3D12CommandList 的任何方法都可以通过 ID3D12GraphicsCommandList 指针按原样调用


如果我们有指向某个接口的指针 - 我们可以调用此接口指针的 any 方法。

特别是 - 如果我们有指向 ID3D12GraphicsCommandList 接口的指针 (gcl),我们可以调用 ID3D12GraphicsCommandListany 方法gcl) 指针。

结果我们可以调用 ID3D12CommandListany 方法 exactly 二进制值 gcl -因为 ID3D12CommandList 的方法也是 ID3D12GraphicsCommandList 的方法。

一般来说,如果某个对象实现了 2 个接口 I2I1 以及 I2 继承自 I1c++ 中的 I2 : I1 条款)我们总是可以将 I2* 指针转换为 I1* 指针。

以相反的方向执行此操作是错误的 - 如果我们有指向 I1* 的指针,我们不能将其转换为 I2*。这已经需要使用 QueryInterface。例如 - 让存在 I3 : I1 和对象实现所有 3 个接口 - I1I2I3I2I3 都继承自 I1I2I3 不是从另一个继承,有不同的布局。在这种情况下 I2I3 always 将有不同的指针。 2 个不同的 vtables。 I1* 可以指向 I3* vtable 而不是 I2* vtable.

所以

I2* p2;
I1* p1 = (I1*)p2;// cast need only for c, not need for c++

总是好的。但下一个代码是错误的

I1* p1;
I2* p2 = (I2*)p1;// wrong !!

但问题是关于 I2 -> I1 演员,而不是关于 I1 -> I2 演员

向上转换 COM 接口指针总是可以的

可以在此处找到实际规范,采用 MS-Word 格式(据我所知,Microsoft 不再托管它):

http://web.archive.org/web/20030201093710/http://www.microsoft.com/Com/resources/comdocs.asp

它非常容易阅读,并且澄清了很多关于 COM 的细节,这些细节目前在 MS 文档中并不明显。

根据规范,COM 接口指针定义为指向函数指针数组(称为 VTable)的指针。 VTable 中每个条目的第一个参数是接口指针本身。一个接口从另一个接口“继承”,首先列出另一个接口的功能,然后是它自己的。请注意,此要求意味着仅支持单继承,如 COM 规范第 2 章第 1.2 部分(强调我的)中所述:

Interfaces and Inheritance

COM separates class hierarchy (or indeed any other implementation technology) from interface hierarchy and both of those from any implementation hierarchy. Therefore, interface inheritance is only applied to reuse the definition of the contract associated with the base interface. There is no selective inheritance in COM: if one interface inherits from another, it includes all the functions that the other interface defines, for the same reason than an object must implement all interface functions it inherits. Inheritance is used sparingly in the COM interfaces. Most of the pre-defined interfaces inherit directly from IUnknown (to receive the fundamental functions like QueryInterface), rather than inheriting from another interface to add more functionality. Because COM interfaces are inherited from IUnknown, they tend to be small and distinct from one another. This keeps functionality in separate groups that can be independently updated from the other interfaces, and can be recombined with other interfaces in semantically useful ways. In addition, interfaces only use single inheritance, never multiple inheritance, to obtain functions from a base interface. Providing otherwise would significantly complicate the interface method call sequence, which is just an indirect function call, and, further, the utility of multiple inheritance is subsumed within the capabilities provided by QueryInterface.

请注意最后一部分:COM 设计使用单继承特别是 以便从子接口调用基接口很简单,即特别是这样您就可以转换接口指针。

接口 VTable 的结构(因此,指针转换的可行性)也在其他几个地方得到确认。请参阅 IDL 的描述中的继承描述,第 13 章,第 1.4 部分(强调我的):

Interface Inheritance

Single inheritance of interfaces is supported, using the C++ notation for same. Referring again to [CAE RPC], page 238:

<interface_header> ::= 
  <[> <interface_attributes> <]> interface <Identifier> [ <:> <Identifier> ]

For example:

[object, uuid(b5483f00-4f6c-101b-a1c7-00aa00389acb)]
  interface IBar : IWazoo {
      HRESULT Bar([in] short i, [in] IFoo * pIF);
      };

cases the first methods in IBar to be the methods of IWazoo.

在第 3 章第 1.3 部分(强调我的)中描述从 IUknown 的继承时,指针转换甚至被明确称为可以:

The IUnknown Interface

This specification has already mentioned the IUnknown interface many times. It is the fundamental interface in COM that contains basic operations of not only all objects, but all interfaces as well: reference counting and QueryInterface. All interfaces in COM are polymorphic with IUnknown, that is, if you look at the first three functions in any interface you see QueryInterface, AddRef, and Release. In other words, IUnknown is base interface from which all other interfaces inherit. Any single object usually only requires a single implementation of the IUnknown member functions. This means that by virtue of implementing any interface on an object you completely implement the IUnknown functions. You do not generally need to explicitly inherit from nor implement IUnknown as its own interface: when queried for it, simply typecast another interface pointer into an IUnknown* which is entirely legal with polymorphism.

我们还可以通过检查这些接口的头文件定义来确认接口布局,这些接口都包含一个包含基本 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)
                                                                                    ^^^^^^^^^

指针向上转换不只是巧合,它由规范保证,并且可以在您的头文件中验证。

解决其他人提出的问题:

What about pointer adjustments?

因为 COM 只支持单一的、纯虚拟的继承,所以在执行转换时永远不需要调整指针。这是故意的,因为转换时的指针调整是非常特定于 C++ 的行为,而 COM 试图独立于语言。您可以通过查看 C++ 编译器在转换 COM 接口指针时输出的机器代码来确认这一点。不会有任何调整。

What about different interface method implementations based on which interface the pointer actually points to?

这就是为什么您总是使用 vTable 调用函数的原因!这就是多态的魔力:一个函数可以根据对象的类型动态分派,而不需要调用者知道对象的类型。一个对象理论上可以根据您用来调用给定接口方法的接口指针做不同的事情,但这是它的业务,而不是客户的业务。 (编辑:我在下面添加了一些关于这个和引用计数的更多细节)

But what about separate reference counts for different interfaces?

同样,这就是为什么您总是使用 vTable 调用函数的原因。保证将呼叫发送到正确的位置。否则就没有 vTable 的意义(我们可以只使用静态分派)。请注意,无论何时复制接口指针,都应调用 AddRef,无论该副本是否使用强制转换。

决赛 Notes/Warnings:

  • 仅仅因为您总是可以转换为基本接口 而不是 意味着您获得的指针与您调用时 returned 的指针相同查询接口。这个细节在大多数时候并不重要,但如果您曾经检查过两个接口是否属于同一个 COM 对象,它就会发挥作用。您不能直接比较接口指针;您必须通过在两者上调用 QueryInterface(IID_IUnknown, ...) 来比较 returned 的值。
  • “向上”转换(隐式转换)在 C++ 中也完全有效,因为 C++ 编译器需要符合与 COM 布局类似的 ABI 要求(绝非巧合)。
  • 指针转换不适用于“交叉”转换或“向下”转换,只有“向上”转换(C++ 中的隐式转换)。如果 I1 不继承自 I2,则不能转换为 I2。
  • 接口方法的第一个参数应该是你从中得到lpVtbl的接口指针。这样的错误在 C 中应该很容易发现,而在 C++ 中甚至不可能。
  • 值得注意的是,托管在 MS-docs 上的当前 COM 文档对原始规范进行了轻微编辑,以删除大量具体细节和真实示例。具体来说,很难找到涉及 C 的 examples/explanations,这可能会误导 reader 相信围绕继承的规则只是遵循 C++ 继承规则,而不是它们的一个非常严格的子集。

简而言之:

您提供的类型转换示例将起作用,因为 ID3D12GraphicsCommandList 继承自 ID3D12CommandList。


编辑:关于界面与实现的更多说明:

如上所述,COM 对象 return 从 QueryInterface() 到通过向上转换获得的指针的不同指针是完全合法的。有人指出,这为接口 vTable 中的指针指向完全​​不同的函数打开了大门,因此在调用时会导致不同的对象行为。虽然 COM 对象以 API 消费者可见的方式执行此操作会令人难以置信的混乱和可怕,但根据 COM 规范,它仍然是合法的。不过,这 不是 意味着“始终使用 QueryInterface() 并且从不向上转换指针”是个好建议。

注意:为了减少混淆,我将从子接口获得的行为(与转换指针时获得的行为相同)称为“继承”行为,而接口的行为return由 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