COM 编码做法

本主题介绍使 COM 代码更加有效和可靠的方法。

__uuidof运算符

生成程序时,可能会收到类似于以下内容的链接器错误:

unresolved external symbol "struct _GUID const IID_IDrawable"

此错误意味着使用外部链接声明了 GUID 常量 (外部) ,并且链接器找不到该常量的定义。 GUID 常量的值通常从静态库文件导出。 如果使用 Microsoft Visual C++,则无需使用 __uuidof 运算符链接静态库。 此运算符是 Microsoft 语言扩展。 它从表达式返回 GUID 值。 表达式可以是接口类型名称、类名或接口指针。 使用 __uuidof,可以按如下所示创建 Common Item Dialog 对象:

IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL, 
    __uuidof(pFileOpen), reinterpret_cast<void**>(&pFileOpen));

编译器从标头中提取 GUID 值,因此无需库导出。

注意

GUID 值通过在 标头中声明 __declspec(uuid( ... )) 来与类型名称相关联。 有关详细信息,请参阅 Visual C++ 文档中 __declspec 文档。

 

IID_PPV_ARGS宏

我们发现 CoCreateInstanceQueryInterface 都需要将最终参数强制转换为 void** 类型。 这会产生类型不匹配的可能性。 考虑以下代码片断:

// Wrong!

IFileOpenDialog *pFileOpen;

hr = CoCreateInstance(
    __uuidof(FileOpenDialog), 
    NULL, 
    CLSCTX_ALL, 
    __uuidof(IFileDialogCustomize),       // The IID does not match the pointer type!
    reinterpret_cast<void**>(&pFileOpen)  // Coerce to void**.
    );

此代码请求 IFileDialogCustomize 接口,但传入 IFileOpenDialog 指针。 reinterpret_cast表达式绕过 C++ 类型系统,因此编译器不会捕获此错误。 在最佳情况下,如果 对象未实现请求的接口,则调用只会失败。 在最坏的情况下,函数会成功,并且指针不匹配。 换句话说,指针类型与内存中的实际 vtable 不匹配。 正如你所能想象的,此时没有什么好事可以发生。

注意

vtable (虚拟方法表) 是函数指针表。 vtable 是 COM 在运行时将方法调用绑定到其实现的方式。 并非巧合的是,vtable 是大多数 C++ 编译器实现虚拟方法的方式。

 

IID_PPV_ARGS宏有助于避免此类错误。 若要使用此宏,请替换以下代码:

__uuidof(IFileDialogCustomize), reinterpret_cast<void**>(&pFileOpen)

替换为以下内容:

IID_PPV_ARGS(&pFileOpen)

宏会自动插入 __uuidof(IFileOpenDialog) 接口标识符,因此保证它与指针类型匹配。 下面是修改后的 (和正确的) 代码:

// Right.
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL, 
    IID_PPV_ARGS(&pFileOpen));

可以将同一个宏与 QueryInterface 配合使用

IFileDialogCustomize *pCustom;
hr = pFileOpen->QueryInterface(IID_PPV_ARGS(&pCustom));

SafeRelease 模式

引用计数是编程中基本容易但又乏味的事情之一,这很容易出错。 典型错误包括:

  • 使用完接口指针后,无法释放它。 此类 bug 将导致程序泄漏内存和其他资源,因为对象不会被销毁。
  • 使用无效指针调用 Release 。 例如,如果从未创建过对象,则可能发生此错误。 此类 bug 可能会导致程序崩溃。
  • 调用 Release 后取消引用接口指针。 此 bug 可能会导致程序崩溃。 更糟的是,它可能会导致程序在以后随机崩溃,从而难以跟踪原始错误。

避免这些 bug 的一种方法是通过安全释放指针的函数调用 Release 。 以下代码演示了一个执行此操作的函数:

template <class T> void SafeRelease(T **ppT)
{
    if (*ppT)
    {
        (*ppT)->Release();
        *ppT = NULL;
    }
}

此函数采用 COM 接口指针作为参数,并执行以下操作:

  1. 检查指针是否为 NULL
  2. 如果指针不是 NULL,则调用 Release
  3. 将指针设置为 NULL

下面是如何使用 SafeRelease的示例:

void UseSafeRelease()
{
    IFileOpenDialog *pFileOpen = NULL;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
    if (SUCCEEDED(hr))
    {
        // Use the object.
    }
    SafeRelease(&pFileOpen);
}

如果 CoCreateInstance 成功,则对 的 SafeRelease 调用将释放指针。 如果 CoCreateInstance 失败, pFileOpen 将保持 NULL。 函数 SafeRelease 会检查此情况,并跳过对 Release 的调用。

也可以安全地在同一指针上多次调用 SafeRelease ,如下所示:

// Redundant, but OK.
SafeRelease(&pFileOpen);
SafeRelease(&pFileOpen);

COM 智能指针

函数 SafeRelease 很有用,但需要记住两件事:

  • 初始化指向 NULL 的每个接口指针。
  • 在每个指针超出范围之前调用 SafeRelease

作为一名 C++ 程序员,你可能认为不必记住其中任一项。 毕竟,这就是 C++ 具有构造函数和析构函数的原因。 最好有一个类来包装基础接口指针并自动初始化和释放指针。 换句话说,我们需要如下所示的内容:

// Warning: This example is not complete.

template <class T>
class SmartPointer
{
    T* ptr;

 public:
    SmartPointer(T *p) : ptr(p) { }
    ~SmartPointer()
    {
        if (ptr) { ptr->Release(); }
    }
};

此处显示的类定义不完整,无法如图所示使用。 至少需要定义复制构造函数、赋值运算符和访问基础 COM 指针的方法。 幸运的是,无需执行任何此工作,因为 Microsoft Visual Studio 已提供智能指针类作为活动模板库 (ATL) 的一部分。

ATL 智能指针类名为 CComPtr。 (还有一个 CComQIPtr 类,此处未讨论此类。) 此处为使用 CComPtr 重写的打开对话框示例。

#include <windows.h>
#include <shobjidl.h> 
#include <atlbase.h> // Contains the declaration of CComPtr.

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
    HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | 
        COINIT_DISABLE_OLE1DDE);
    if (SUCCEEDED(hr))
    {
        CComPtr<IFileOpenDialog> pFileOpen;

        // Create the FileOpenDialog object.
        hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
        if (SUCCEEDED(hr))
        {
            // Show the Open dialog box.
            hr = pFileOpen->Show(NULL);

            // Get the file name from the dialog box.
            if (SUCCEEDED(hr))
            {
                CComPtr<IShellItem> pItem;
                hr = pFileOpen->GetResult(&pItem);
                if (SUCCEEDED(hr))
                {
                    PWSTR pszFilePath;
                    hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);

                    // Display the file name to the user.
                    if (SUCCEEDED(hr))
                    {
                        MessageBox(NULL, pszFilePath, L"File Path", MB_OK);
                        CoTaskMemFree(pszFilePath);
                    }
                }

                // pItem goes out of scope.
            }

            // pFileOpen goes out of scope.
        }
        CoUninitialize();
    }
    return 0;
}

此代码与原始示例之间的main区别在于此版本不显式调用 Release。 当 CComPtr 实例超出范围时,析构函数在基础指针上调用 Release

CComPtr 是类模板。 模板参数是 COM 接口类型。 在内部, CComPtr 保存该类型的指针。 CComPtr 重写 operator -> () 运算符& () ,使类的行为类似于基础指针。 例如,以下代码等效于直接调用 IFileOpenDialog::Show 方法:

hr = pFileOpen->Show(NULL);

CComPtr 还定义了 CComPtr::CoCreateInstance 方法,该方法使用一些默认参数值调用 COM CoCreateInstance 函数。 唯一必需的参数是类标识符,如以下示例所示:

hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));

CComPtr::CoCreateInstance 方法纯粹出于方便起见提供;如果愿意,仍可以调用 COM CoCreateInstance 函数。

下一步

COM 中的错误处理