1

ID3D12GraphicsCommandList接口继承自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. 我需要做其他事情吗?

谢谢。

4

4 回答 4

5
  1. 类型转换会起作用吗?

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

在 C++ 中,这可以通过提供用户定义的转换函数来实现,尽管它非常脆弱,并且可以以微妙和不那么微妙的方式惊人地破坏。

  1. QueryInterface 会起作用吗?

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

  1. 我需要做其他事情吗?

不,不是真的,只要你遵守 COM 规则。一个细节经常被忽略:一个成功的调用QueryInterface增加了接口的引用计数,所以你必须对Release调用返回的每个接口QueryInterface


为什么铸造不安全

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

COM 的要求极简。事实上

COM 的唯一语言要求是代码以一种可以创建指针结构的语言生成,并且可以显式或隐式地通过指针调用函数。

这允许在实现 COM 接口时使用多种编程语言。另一方面,COM 在ABI如何映射到语言级结构方面提供的保证很少。对于接口继承尤其如此:

COM 中的继承并不意味着代码重用。

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

但还有更多!COM 有另一个规则很容易理解,但经常被忽视:

从 COM 客户端的角度来看,总是对每个接口进行引用计数。客户永远不应假定一个对象对所有接口使用相同的计数器

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

这些是规则和派生的保证。现在,实际上,指针强制转换会令人惊讶地经常出现。因此,如果您是一名开发人员,对正确的代码和尚未失败的代码没有太大的区别,那么请务必继续前进,并为您的内心感到高兴。

另一方面,如果您是一个以交付正确的软件而自豪的开发人员,那么总是QueryInterface需要导航实现的界面表面。


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

真的。DirectX 使用 COM 的一小部分,通常称为Nano-COM。虽然大部分 COM 不适用,但 COM 的 ABI 方面适用。由于此答案仅涉及 ABI 方面,因此它适用于 COM 和 DirectX。

请参阅Microsoft 文档

于 2021-07-01T08:04:50.757 回答
2

向上转换 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

于 2021-08-08T15:15:15.863 回答
0

QueryInterface 会起作用吗?

是的。

我需要做其他事情吗?

不。

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

ID3D12GraphicsCommandList *gcl = ...;
ID3D12CommandList *cl;
HRESULT result = gcl->lpVtbl->QueryInterface(gcl,
                                             &IID_ID3D12CommandList,
                                             (void**)&cl);
于 2021-07-01T07:41:44.513 回答
-4

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


如果我们有指向某个接口的指针——我们可以调用这个接口指针的任何方法。

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

结果,我们可以按原样调用具有完全二进制值的任何方法- 因为方法也是方法。ID3D12CommandListgclID3D12CommandListID3D12GraphicsCommandList

一般来说,如果某个对象实现了 2 个接口I2并继承自(用c++术语),我们总是可以将指针转换为指针。I1I2I1I2 : I1I2*I1*

在相反的方向这样做是错误的——如果我们有指针,I1*我们就不能投射它I2*。这已经需要使用了QueryInterface。例如 - 让存在和I3 : I1对象实现所有 3 个接口 -I1和. 两者都继承,但不继承另一个,具有不同的布局。在这种情况下,总是会有不同的指针。2个不同的虚表。并且可以指向vtable 而不是vtable。I2I3I2I3I1I2I3I2I3 I1*I3*I2*

所以

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演员表

于 2021-07-01T08:26:06.287 回答