扩展 MIDL 接口和 COM 对象设计

Posted

技术标签:

【中文标题】扩展 MIDL 接口和 COM 对象设计【英文标题】:Extending MIDL interfaces and COM object design 【发布时间】:2011-07-08 05:06:39 【问题描述】:

我已经阅读了COM Programmer's Cookbook 中详细介绍的各种 COM 设计模式以及一些相关的 SO 线程,尤其是 the thread discussing composition vs. multiple inheritance。可能是因为我对 C++ 和 COM 都太陌生,我可能会错过各种来源中的观点,所以这是我用一句话表达的问题:

我可以扩展 MIDL 生成的接口供 DLL 内部使用吗?如果可以,鉴于 MIDL/COM 限制,我如何正确处理菱形问题/并行层次结构?

肮脏的细节......

希望能帮助其他人找出我的困惑所在,以下是我的假设:

1) COM 不支持虚拟继承,只允许通过接口进行多重继承。

2) 即使COM 看不到它,只要我不希望它被COM 直接暴露,使用不受支持的C++ 继承对我来说应该不是非法的。

3) 因为 MIDL 只允许接口的单一继承,如果我有一个并行层次结构,我需要为 coclass 聚合它们。

4) MIDL 似乎并没有声明 coclass 本身,所以我需要编写一个 .h 文件来声明实际的类,在那里,我可以根据需要进行扩展,以了解 COM 消费者不能使用它(这没关系)。

我想要做的是有一个处理大部分实现细节并将一些特定功能委托给子类的基础对象(我还没有决定它是否是抽象的,尽管我认为现在是抽象的) .客户端通常会使用子类。所以,

project.idl

import "oaidl.idl"
import "ocidl.idl"

[
  object,
  uuid(...),
  dual,
  oleautomation
]
interface IBase : IDispatch 
   //stuff I want to show to COM
;

[
  object,
  uuid(...),
  dual,
  oleautomation
]
interface IChild1 : IBase 
   //stuff (in addition to base) I want to show to COM
;

[
  object,
  uuid(...),
  dual,
  oleautomation
]
interface IChild2 : IBase 
   //stuff (in addition to base) I want to show to COM
;

[
   uuid(...),
   version(...),
]
library myproject 
   importlib("stdole32.tlb");
   interface IBase;
   interface IChild1;
   interface IChild2;
   [
      uuid(...),
   ]
   coclass Base 
      [default]interface IBase;
      interface IOle*; //include other IOle* interfaces required for the functionality
   ;
   [
      uuid(...),
   ]
   coclass Child1 
      [default]interface IChild1;
      interface IOle*; //those are delegated to the base members
   ;
   [
      uuid(...),
   ]
   coclass Child2 
      [default]interface IChild2;
      interface IOle*; //those are delegated to the base members
   ;
;

base.h

#include base_h.h //interfaces generated by MIDL

// I assume I need to re-include IOle* because IBase has no relationship
// and C++ wouldn't know that I want the object base to also have those
// interfaces...
class base : public IBase,
             public IOle* 
    //handle all IUnknown, IDispatch and other IOle* stuff here
    //as well as the common implementations as appropriate
;

child1.h

#include base.h

//I'm not sure if I need to re-include the IOle* interfaces...
//I also assume that by inheriting base, child1 also inherits its interface
class Child1 : public Base,
               public IChild1 
  //specific details only, let base handle everything else.
;

child2.h

#include base.h

//I'm not sure if I need to re-include the IOle* interfaces...
class Child2 : public Base,
               public IChild2 
  //specific details only, let base handle everything else.
;

从概念上讲,创建一个新的 child* 对象总是意味着创建基础对象,因为需要基础来处理实现细节,所以我认为让基础处理 QueryInterface 和引用计数也是合适的,但是我对以下几点感到困惑:

1) 编译器抱怨由于并行层次结构导致成员不明确; IUnknown 从我的自定义界面和附加的 IOle* 界面重新实现了几次。文档表明,预计每个对象实际上只需要一个实现,但我不清楚我将如何解决编译器的问题,而且我觉得进行强制转换有点错误?我还想知道我是否应该虚拟继承所有接口,似乎对 C++ 有效,尽管 COM 不会有这样的理解,但它也不应该关心(?)。

2) 但是,如果我在 .h 文件中将所有继承的接口声明为虚拟接口,编译器就会抱怨当我尝试在 Base class.cpp 中实现 QueryInterface 时不允许继承的成员。我已经用谷歌搜索了那个错误,但不清楚它在这里试图告诉我什么。

编辑:我回答了我自己的问题。 Intel had documentation 这个错误我最初没有点击链接,假设它可能不适用于 Visual Studio。我希望我还是这样做了,但现在我明白了为什么我会收到这个错误,因为我试图在 Base:: 中而不是 IUnknown:: 或 IDispatch:: 中进行所有实现。现在,这引出了一个新问题,可以澄清我原来的主要问题——如果可能的话,我如何将实施从 IUnknown(和其他)推迟到 Base 并仅从 Base 工作?似乎如果我只使用 IUnknown::xxx,它就不能再访问 Base 的私有成员,这似乎是正常的事情,所以这可能不是我想要的。我尝试将除 base 自己的所有其他接口声明为虚拟接口,但这并没有真正采取。 (同样,我没有看到明显的解决方案可能是我的经验不足。)

3) Base 的 QueryInterface 不能将 base 强制转换为孩子,这是一个合理的抱怨,所以我认为无论如何我都必须为所有孩子重新实现 QI,但是一旦我确定请求的接口不是,我可以委托给 base 的 QI孩子的。奇怪的是,由于缺少 IUnknown 和 IDispatch 的成员,编译器坚持认为 child* 类是抽象的,但基础不是已经实现,因此孩子也应该拥有这些成员吗?

各种编译器错误让我担心我对语言和框架中的任何一个或两个都缺乏经验,这导致我对如何设计 COM 对象和继承层次结构以及实现细节做出有缺陷的假设,而我显然遗漏了一些东西这里。任何指点,甚至是一记耳光都将不胜感激。

谢谢!

【问题讨论】:

【参考方案1】:

您在这里的想法是正确的,您所缺少的只是在派生最多的类中捆绑一些松散的末端。作为 COM 开发人员,您希望类对象上的所有 AddRef/Release/QI impls 都是相同的;但是面向 C++ 的编译器不知道这一点,因此将它们视为可能是独立的。您在此处拥有的两个 impl 是 Base 中的一个和您添加的任何接口中的一个。

在此处直接设置编译器非常简单:在派生最多的类中,重新定义所有 IUnknown 方法,并将它们定向到适当的基类 - 例如。

class ChildX: public Base,
              public IChildA
              ... more COM interfaces if needed ...

    ...

    // Direct IUnknown methods to Base which does the refcounting for us...
    STDMETHODIMP_(ULONG) AddRef()  return Base::AddRef();  
    STDMETHODIMP_(ULONG) Release()  return Base::Release();  
    ... suggest implementing QI explicitly here.

这基本上是说所有称为 AddRef 的方法,无论它们在 ChildX 中如何结束,都将获得特定的实现。

在这里直接实现 QI 是最简单的,并且只将 AddRef/Release 委托给 Base。 (从技术上讲,Base 可以使用 static_cast 强制转换为 Child,但是您需要在 Child 完全定义后将代码放入函数中;但是,不建议这样做,因为基类很少有充分的理由知道派生自它的类。)

其他需要注意的事项:确保 Base 声明了一个虚拟 dtor - 即使只是空的,这样当 Base 在 ref 变为 0 时执行“删除这个”时,它将调用派生类中的 dtor他们分配的任何资源都会得到适当的清理。此外,如果需要,请确保正确计算 ref 计数和线程安全;查看任何好的 COM 介绍书籍(例如“Inside Distributed COM”,尽管名称如此,但以普通 COM 开头),看看其他人是如何做到这一点的。

这是 COM 中非常常见的习惯用法,许多框架使用#define 宏或派生更多的模板类在最派生的类中添加 AddRef/Release/QI(如 MFC 所做的那样),然后委托那些处理大部分家务的知名基类。

【讨论】:

投反对票的人是否愿意解释这里出了什么问题,或者他们会如何做不同的事情? +1 对抗巨魔/仇恨者。对此响应做出了很好的努力。如果不是 100% 完美,那也是一个很好的开始。

以上是关于扩展 MIDL 接口和 COM 对象设计的主要内容,如果未能解决你的问题,请参考以下文章

预定义的 IDL 文件中没有类型定义

ATL/COM:MIDL 编译器不输出分配接口的 UUID

Python基础(16)_面向对象程序设计(类继承派生组合接口)

面向对象设计的三个原则

面向对象设计的原则

设计模式——面向对象设计原则