65.9K
CodeProject 正在变化。 阅读更多。
Home

MFC 的 COM 接口宏简介

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.49/5 (15投票s)

2004年4月16日

CPOL

6分钟阅读

viewsIcon

81576

如何在 MFC 类中定义和实现 COM 接口。

引言

在我正在进行的一个项目中,我需要实现多个 COM 接口。

如果我想实现 IWidget 接口和 IThingAMaBob 接口,它可能看起来像这样。

class CWidgets : public IWidgets
{
public:
    virtual HRESULT __stdcall QueryInterface(REFIID riid, 
                                     void __RPC_FAR *__RPC_FAR *ppvObject);
    virtual ULONG   __stdcall AddRef(void);
    virtual ULONG   __stdcall Release(void);

    virtual HRESULT __stdcall MyExtraFunction();
};
IThingAMaBob 接口也类似。这是 COM 101 的内容。

一直编写这些代码有点乏味。考虑到需要为每个接口正确实现 IUnknown 基方法(尤其是 QueryInterface),聪明的程序员会开始研究更好(更容易)的方法。

幸运的是,对于 MFC 程序员来说,MFC 提供了一种更容易的方式来不仅实现 COM 接口,而且还提供了一种在同一个类中定义多个接口的简单方法。

MFC COM 宏

我们可以使用一些 MFC 宏,将我们的 CWidgets 类重写如下。
class CWidgets : public CCmdTarget
{
public:
    DECLARE_INTERFACE_MAP()

    BEGIN_INTERFACE_PART(Widgets, IWidgets)
        STDMETHOD(MyExtraFunction)();
    END_INTERFACE_PART(Widgets)
};
您会注意到的第一件事是,该类现在派生自 CCmdTarget。它需要直接或间接派生自 CCmdTarget,原因我们稍后会看到。然后,该类声明一个接口映射,后跟一个接口部分。接口部分定义了一个嵌套类(在本例中)名为 XWidgets,它派生自 IWidgets 接口。(该宏在宏的第一个参数前加上一个 X,以给嵌套类一个独特的名称,不会与“C”或“I”名称冲突)。BEGIN_INTERFACE_PART 宏还为我们声明了从 IUnknown 继承的 3 个标准方法。此接口的任何其他方法都必须在 BEGIN_INTERFACE_PARTEND_INTERFACE_PART 宏之间定义。

在我们的实现文件中,我们会有类似这样的内容。

BEGIN_INTERFACE_MAP(CWidgets, CCmdTarget)
    INTERFACE_PART(CWidgets, IID_IWidgets, Widgets)
END_INTERFACE_MAP()
以及我们对 IWidgets 接口方法的实现。请注意,即使我们不必声明基本的 IUnknown 方法,我们_确实_必须实现它们。我们的 IWidgets 接口的四个方法可能看起来像这样。
STDMETHODIMP_(ULONG) CWidgets::XWidgets::AddRef()
{
    METHOD_PROLOGUE(CWidgets, Widgets);
    
    return pThis->ExternalAddRef();
}

STDMETHODIMP_(ULONG) CWidgets::XWidgets::Release()
{
    METHOD_PROLOGUE(CWidgets, Widgets)

    return pThis->ExternalRelease();
}

STDMETHODIMP CWidgets::XWidgets::QueryInterface(REFIID iid, LPVOID far* ppvObj)     
{
    METHOD_PROLOGUE(CWidgets, Widgets)

    return pThis->ExternalQueryInterface(&iid, ppvObj);
}

STDMETHODIMP CWidgets::XWidgets::MyExtraFunction()
{
    METHOD_PROLOGUE(CWidgets, Widgets)

    //  Do something
    .
    .
    .
    
    return S_OK;
}
在我们深入探讨之前,先回顾一下。我们的类派生自 CCmdTarget,并且通过 BEGIN_INTERFACE_PART 宏,包含一个名为 XWidgets 的嵌套类。

您会注意到嵌套类实现中的第一件事是一个新宏,METHOD_PROLOGUE。它接受两个参数。第一个是包含类的名称,第二个是内部类的名称(不带 X)。如果我们查看宏的定义,我们会看到这个

#define METHOD_PROLOGUE(theClass, localClass) \
    theClass* pThis = \
        ((theClass*)((BYTE*)this - offsetof(theClass, m_x##localClass))); \
    AFX_MANAGE_STATE(pThis->m_pModuleState) \
    pThis; // avoid warning from compiler \
预处理器将其替换为
    CWidgets* pThis = \
        ((CWidgets*)((BYTE*)this - offsetof(CWidgets, m_xWidgets:))); \
    AFX_MANAGE_STATE(pThis->m_pModuleState) \
    pThis; // avoid warning from compiler \
这声明了一个名为 pThisCWidgets 类型指针,并将其初始化为指向包含的 CWidgets 类。它通过使用 this 指针和 m_xWidgets 进行一些指针算术来实现这一点。现在,忽略 m_xWidgets 是如何出现的,只需接受它是一个 XWidgets 类的嵌入实例。

一旦我们获得了指向包含类的指针,我们就可以访问包含类中的数据或调用包含类上的成员函数。这就是 CCmdTarget 的派生发挥作用的地方。

MFC 为我们提供了 AddRef()Release() 的实现,我们可以通过 pThis 访问它们。因此,调用 ExternalAddRef() 等。如果我们的接口是聚合的,ExternalAddRef() 要么调用我们的外部 IUnknown,要么增加我们基类 CCmdTarget 对象中的引用计数器。由于本文中不涉及聚合,因此这将是您最后一次看到此主题。同样,调用 ExternalRelease() 会减少我们的引用计数。

更重要的是,MFC 为我们提供了一个正确的 QueryInterface() 实现。正确的含义是它遵循 QueryInterface 的语义。如果您有一个 IWidgets 接口,您可以使用它来获取一个 IUnknown 接口。反之亦然。这当然是它应该工作的方式,但令人惊讶的是,许多程序员(包括我自己)有时会忘记实现对 IUnknown 的查询。

嵌套类的实例

一旦我们定义了接口并编写了实现,我们需要一个类的实例来使用。人们可能会尝试在我们的外部类中这样做。
class CWidgets : public CCmdTarget
{
public:
    DECLARE_INTERFACE_MAP()

    BEGIN_INTERFACE_PART(Widgets, IWidgets)
        STDMETHOD(MyExtraFunction)();
    END_INTERFACE_PART(Widgets)
    
    XWidgets    m_myWidget;
};
它定义了 XWidgets 类,然后创建了一个嵌入在 CWidgets 对象中的实例。这当然会编译,但是当您的客户端代码调用 QueryInterface() 来获取 XWidgets 类的实例时,它不会获得指向 m_myWidget 的指针。它得到的是一个指向名为 m_xWidgetsXWidgets 实例的指针。等等!m_xWidgets 是从哪里来的?

隐藏在 END_INTERFACE_PART 宏内部的是嵌套类实例的声明。请注意这一点。通常,当您定义一个嵌套类时,您必须在包含类中声明该嵌套类的实例。MFC 宏假定您将始终在包含对象中拥有一个嵌入的嵌套类实例,因此它会为您创建一个实例,通过在嵌套类名称前加上 m_x 来命名该实例。

有人要牛排刀吗?

目前为止一切顺利。但等等,还有更多!这些宏还使得在一个对象中实现多个接口变得非常容易。让我们稍微扩展一下我们的类,以包含 IThingAMaBob 接口的实现。当然,只有当 Widget 和 ThingAMaBob 相关时,将两个接口组合在一个对象中才有意义,所以我们假设它们是相关的。我们的类现在看起来像这样。

class CWidgets : public CCmdTarget
{
public:
    DECLARE_INTERFACE_MAP()

    BEGIN_INTERFACE_PART(Widgets, IWidgets)
        STDMETHOD(MyExtraFunction)();
    END_INTERFACE_PART(Widgets)
    
    BEGIN_INTERFACE_PART(ThingAMaBob, IThingAMaBob)
        STDMETHOD(SomeOtherFunction)();
    END_INTERFACE_PART(ThingAMaBob)
};
它定义了第二个嵌套类,名为 XThingAMaBob。在我们的实现文件中,接口映射现在看起来像这样。
BEGIN_INTERFACE_MAP(CWidgets, CCmdTarget)
    INTERFACE_PART(CWidgets, IID_IWidgets, Widgets)
    INTERFACE_PART(CWidgets, IID_IThingAMaBob, ThingAMaBob)
END_INTERFACE_MAP()
我们还将添加 XThingAMaBob 类中的任何方法(不要忘记 IUnknown 方法)。作为这一切的一部分,我们在包含类中获得了一个名为 m_xThingAMaBob 的新成员变量,其类型为 XThingAMaBob

这样做真正好的地方在于,如果您的实现中的 QueryInterface() 调用 pThis->ExternalQueryInterface(),您将自动找到并返回与您(或某些其他程序)请求的 iid 关联的嵌入式 m_xSomething 实例,所有管道都由接口映射和 CCmdTarget 处理。您不必在每个对象的 QueryInterface() 方法中编写一堆代码来使其了解包含对象上实现的其他接口。更重要的是,如果您向类添加一个接口,您只需记住将其添加到实现文件中的接口映射中,MFC 就会处理其余部分。

引用计数

由于接口是在 MFC 对象中定义的,因此它们与包含对象具有相同的生命周期。因此,正如您已经看到的,可以在包含对象中进行引用计数,而不是让每个接口负责自己的计数。在类上实现的所有接口共享一个引用计数器。在调试版本中,MFC 在 CCmdTarget::~CCmdTarget() 析构函数期间对引用计数器执行 ASSERT(dwRef <= 1)。如果您遇到该断言,则意味着某处有人没有对一个或多个接口指针执行 Release()

嵌套类中的变量、方法和构造函数

除了为嵌套类定义 COM 方法外,还可以在嵌套类内部嵌入普通(非 COM)方法、构造函数和析构函数以及成员变量。您所要做的就是将它们添加到 BEGIN_INTERFACE_PARTEND_INTERFACE_PART 宏之间。您唯一需要记住的“特殊”之处是,构造函数/析构函数的名称是类名,前面加上一个“X”。
class CWidgets : public CCmdTarget
{
public:
    DECLARE_INTERFACE_MAP()

    BEGIN_INTERFACE_PART(Widgets, IWidgets)
        STDMETHOD(MyExtraFunction)();
    END_INTERFACE_PART(Widgets)
    
    BEGIN_INTERFACE_PART(ThingAMaBob, IThingAMaBob)
        STDMETHOD(SomeOtherFunction)();
        
        XThingAMaBob();
        ~XThingAMaBob();
        
    private:
        BOOL    m_bMyBool;
    END_INTERFACE_PART(ThingAMaBob)
};
它定义了一个名为 XThingAMaBob 的构造函数/析构函数对和一个名为 m_bMyBool 的私有 BOOL 变量。在您的嵌套类代码中,您可以通过隐式 this 指针访问以这种方式定义的成员,就像在非嵌套类中一样。

历史

2004 年 4 月 16 日 - 初始版本。
© . All rights reserved.