MFC 的 COM 接口宏简介






4.49/5 (15投票s)
如何在 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_PART
和 END_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 \
这声明了一个名为 pThis
的 CWidgets
类型指针,并将其初始化为指向包含的 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_xWidgets
的 XWidgets
实例的指针。等等!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_PART
和 END_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
指针访问以这种方式定义的成员,就像在非嵌套类中一样。