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宏
我们发现 CoCreateInstance 和 QueryInterface 都需要将最终参数强制转换为 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 接口指针作为参数,并执行以下操作:
- 检查指针是否为 NULL。
- 如果指针不是 NULL,则调用 Release。
- 将指针设置为 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 函数。
下一步