COM 简介 - 它是什么以及如何使用它。






4.91/5 (506投票s)
2000年7月2日

2338802

15733
一篇为 COM 新手程序员准备的教程,解释了如何重用现有的 COM 组件,例如 Windows shell 中的组件。
本文目的
我为刚开始接触 COM 并需要一些帮助来理解基础知识的程序员写了这篇教程。本文简要介绍了 COM 规范,然后解释了一些 COM 术语,并描述了如何重用现有的 COM 组件。本文不涉及编写您自己的 COM 对象或接口。
更新
2000 年 7 月 22 日:增加了几段关于使用 Release()
和卸载 DLL 的内容。还增加了关于 HRESULT 的章节。
引言
COM(Component Object Model,组件对象模型)是如今在 Windows 世界中似乎无处不在的流行三字母缩写词(TLA)。不断有大量新技术涌现,它们都基于 COM。文档中充斥着诸如COM 对象、接口、服务器等术语,但都假设您已经熟悉 COM 的工作原理和使用方法。
本文从头开始介绍 COM,描述其底层机制,并展示如何使用由他人(特别是 Windows shell)提供的 COM 对象。读完本文后,您将能够使用 Windows 内置以及第三方提供的 COM 对象。
本文假设您精通 C++。我在示例代码中使用了一点 MFC 和 ATL,但我会详细解释代码,所以即使您不熟悉 MFC 或 ATL,也应该能跟上。本文的章节包括:
COM - 它究竟是什么? - 快速介绍 COM 标准,以及它旨在解决的问题。您不需要了解这些才能使用 COM,但我仍建议阅读它,以理解为什么 COM 中的事情要这样做。
基本元素的定义 - COM 术语以及这些术语所代表内容的描述。
使用 COM 对象 - 关于如何创建、使用和销毁 COM 对象的概述。
基础接口 - IUnknown - 对基础接口 IUnknown
中方法的描述。
请密切注意 - 字符串处理 - 如何在 COM 代码中处理字符串。
融会贯通 - 示例代码 - 两组示例代码,用以说明本文讨论的所有概念。
处理 HRESULT - 对 HRESULT
类型的描述,以及如何测试错误和成功代码。
参考资料 - 如果您的雇主允许,您应该报销的那些书。:)
COM - 它究竟是什么?
简而言之,COM 是一种在不同应用程序和语言之间共享二进制代码的方法。这与 C++ 的方法不同,C++ 提倡重用源代码。ATL 就是一个完美的例子。虽然源代码级别的重用效果很好,但它只适用于 C++。它还可能引入命名冲突的问题,更不用说项目中存在多份代码副本导致的臃肿。
Windows 允许您使用 DLL 在二进制级别共享代码。毕竟,Windows 应用程序就是这样运作的——重用 kernel32.dll、user32.dll 等。但是由于这些 DLL 是按照 C 接口编写的,它们只能被 C 或理解 C 调用约定的语言使用。这将共享的负担放在了编程语言实现者身上,而不是 DLL 本身。
MFC 引入了另一种二进制共享机制,即 MFC 扩展 DLL。但这些限制更多——您只能从 MFC 应用程序中使用它们。
COM 通过定义一个二进制标准解决了所有这些问题,这意味着 COM 规定二进制模块(DLL 和 EXE)必须被编译成符合特定结构的格式。该标准还精确规定了 COM 对象在内存中必须如何组织。这些二进制文件也必须不依赖于任何编程语言的特性(例如 C++ 中的名称修饰)。一旦做到这一点,这些模块就可以从任何编程语言中轻松访问。二进制标准将兼容性的负担放在了生成二进制文件的编译器上,这使得后来需要使用这些二进制文件的人们变得容易得多。
COM 对象在内存中的结构恰好与 C++ 虚函数使用的结构相同,这就是为什么很多 COM 代码使用 C++ 编写的原因。但请记住,模块是用什么语言编写的并不重要,因为最终生成的二进制文件对所有语言都是可用的。
顺便说一下,COM 并非 Win32 特有。理论上,它可以被移植到 Unix 或任何其他操作系统。然而,我从未在 Windows 世界之外见过 COM 的提及。
基本元素的定义
让我们从底层向上看。一个接口(interface)只是一组函数。这些函数被称为方法(method)。接口名称以 I 开头,例如 IShellLink
。在 C++ 中,接口被编写为一个只包含纯虚函数的抽象基类。
接口可以从其他接口继承(inherit)。继承的工作方式就像 C++ 中的单继承。接口不允许进行多重继承。
一个 coclass(component object class 的缩写)包含在 DLL 或 EXE 中,并包含一个或多个接口背后的代码。这个 coclass 被称为实现(implement)了那些接口。一个 COM 对象(COM object)是 coclass 在内存中的一个实例。请注意,COM 中的“class”与 C++ 中的“class”不同,尽管通常 COM 类的实现就是一个 C++ 类。
一个 COM 服务器(COM server)是一个包含一个或多个 coclass 的二进制文件(DLL 或 EXE)。
注册(Registration)是创建注册表条目的过程,这些条目告诉 Windows COM 服务器位于何处。反注册(Unregistration)则相反——移除那些注册表条目。
一个 GUID(发音与 "fluid" 押韵,代表globally unique identifier,全局唯一标识符)是一个 128 位的数字。GUID 是 COM 用来标识事物的、与语言无关的方式。每个接口和 coclass 都有一个 GUID。由于 GUID 在全世界范围内都是唯一的,因此避免了名称冲突(只要您使用 COM API 来创建它们)。您有时也会看到术语 UUID(代表universally unique identifier,通用唯一标识符)。UUID 和 GUID 在所有实际用途上是相同的。
一个类 ID(class ID),或称 CLSID,是命名一个 coclass 的 GUID。一个接口 ID(interface ID),或称 IID,是命名一个接口的 GUID。
COM 广泛使用 GUID 有两个原因:
- GUID 在底层只是数字,任何编程语言都可以处理它们。
- 任何人在任何机器上正确创建的每个 GUID 都是唯一的。因此,COM 开发者可以自行创建 GUID,而不会有两个开发者选择相同 GUID 的风险。这消除了需要一个中央机构来颁发 GUID 的必要性。
HRESULT 是 COM 用来返回错误和成功代码的一种整型类型。尽管有 H 前缀,但它并不是任何东西的“句柄”(handle)。稍后我将详细介绍 HRESULT 以及如何测试它们。
最后,COM 库(COM library)是操作系统的一部分,当您处理 COM 相关事务时,您会与之交互。通常,COM 库被简称为“COM”,但为了避免混淆,我在这里不会这样做。
使用 COM 对象
每种语言都有自己处理对象的方式。例如,在 C++ 中,您可以在栈上创建它们,或者使用 new
来动态分配它们。由于 COM 必须是语言中立的,COM 库提供了自己的对象管理例程。下面是 COM 和 C++ 对象管理的比较:
创建一个新对象
- 在 C++ 中,使用
operator new
或在栈上创建一个对象。 - 在 COM 中,调用 COM 库中的一个 API。
删除对象
- 在 C++ 中,使用
operator delete
或让栈对象离开作用域。 - 在 COM 中,所有对象都维护自己的引用计数。调用者必须在用完对象后告知该对象。当引用计数达到 0 时,COM 对象会自行从内存中释放。
现在,在创建和销毁对象的这两个阶段之间,您实际上需要使用它。当您创建一个 COM 对象时,您告诉 COM 库您需要哪个接口。如果对象创建成功,COM 库会返回一个指向所请求接口的指针。然后您可以通过该指针调用方法,就像它是一个指向常规 C++ 对象的指针一样。
创建 COM 对象
要创建一个 COM 对象并从该对象获取一个接口,您需要调用 COM 库的 API CoCreateInstance()
。CoCreateInstance()
的原型是:
HRESULT CoCreateInstance ( REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID* ppv );
参数如下:
rclsid
- coclass 的 CLSID。例如,您可以传入
CLSID_ShellLink
来创建一个用于创建快捷方式的 COM 对象。 pUnkOuter
- 这仅在聚合 COM 对象时使用,这是一种获取现有 coclass 并为其添加新方法的方式。对于我们的目的,我们只需传递 NULL,表示不使用聚合。
dwClsContext
- 指示我们想要使用哪种类型的 COM 服务器。在本文中,我们将始终使用最简单的服务器类型,即进程内 DLL,因此我们将传递
CLSCTX_INPROC_SERVER
。一个告诫:您不应使用CLSCTX_ALL
(这是 ATL 中的默认值),因为它在未安装 DCOM 的 Windows 95 系统上会失败。 riid
- 您希望返回的接口的 IID。例如,您可以传递
IID_IShellLink
来获取一个指向IShellLink
接口的指针。 ppv
- 一个接口指针的地址。COM 库通过此参数返回所请求的接口。
当您调用 CoCreateInstance()
时,它会处理在注册表中查找 CLSID、读取服务器位置、将服务器加载到内存中,以及创建您请求的 coclass 的实例。
这里有一个示例调用,它实例化一个 CLSID_ShellLink
对象,并请求一个指向该 COM 对象的 IShellLink
接口指针。
HRESULT hr; IShellLink* pISL; hr = CoCreateInstance ( CLSID_ShellLink, // CLSID of coclass NULL, // not used - aggregation CLSCTX_INPROC_SERVER, // type of server IID_IShellLink, // IID of interface (void**) &pISL ); // Pointer to our interface pointer if ( SUCCEEDED ( hr ) ) { // Call methods using pISL here. } else { // Couldn't create the COM object. hr holds the error code. }
首先,我们声明一个 HRESULT
来保存 CoCreateInstance()
的返回值和一个 IShellLink
指针。我们调用 CoCreateInstance()
来创建一个新的 COM 对象。如果 hr
持有一个表示成功的代码,SUCCEEDED
宏返回 TRUE;如果 hr
表示失败,则返回 FALSE。还有一个对应的宏 FAILED
用于测试失败代码。
删除 COM 对象
如前所述,您不会释放 COM 对象,您只是告诉它们您已经用完了。每个 COM 对象都实现的 IUnknown
接口有一个方法 Release()
。您调用此方法来告诉 COM 对象您不再需要它。一旦调用了 Release()
,您就不能再使用该接口指针了,因为 COM 对象可能随时从内存中消失。
如果您的应用程序使用许多不同的 COM 对象,那么在每次用完接口后调用 Release()
是至关重要的。如果您不释放接口,COM 对象(以及包含代码的 DLL)将保留在内存中,不必要地增加您应用程序的工作集。如果您的应用程序将长时间运行,您应该在空闲处理期间调用 CoFreeUnusedLibraries()
API。此 API 会卸载所有没有未完成引用的 COM 服务器,从而也减少了您应用程序的内存使用。
继续上面的例子,这里是如何使用 Release()
的:
// Create COM object as above. Then... if ( SUCCEEDED ( hr ) ) { // Call methods using pISL here. // Tell the COM object that we're done with it. pISL->Release(); }
下一节将完整解释 IUnknown
接口。
基础接口 - IUnknown
每个 COM 接口都派生自 IUnknown
。这个名字有点误导,因为它并不是一个未知的接口。这个名字的含义是,如果您有一个指向某个 COM 对象的 IUnknown
指针,您并不知道底层的对象是什么,因为每个 COM 对象都实现了 IUnknown
。
IUnknown
有三个方法:
AddRef()
- 告诉 COM 对象增加其引用计数。如果您创建了一个接口指针的副本,并且原始指针和副本都仍在使用,您就会使用此方法。在本文中,我们不需要使用AddRef()
。Release()
- 告诉 COM 对象减少其引用计数。请参考前面的例子中的代码片段来演示Release()
的用法。QueryInterface()
- 从 COM 对象请求一个接口指针。当一个 coclass 实现多个接口时,您会使用此方法。
我们已经见识了 Release()
的用法,那么 QueryInterface()
呢?当您用 CoCreateInstance()
创建一个 COM 对象时,您会得到一个接口指针。如果这个 COM 对象实现了多个接口(不包括 IUnknown
),您可以使用 QueryInterface()
来获取您需要的任何其他接口指针。QueryInterface()
的原型是:
HRESULT IUnknown::QueryInterface (
REFIID iid,
void** ppv );
参数如下:
iid
- 您正在请求的接口的 IID。
ppv
- 接口指针的地址。如果成功,
QueryInterface()
通过此参数返回接口。
让我们继续我们的 shell link 例子。用于创建 shell link 的 coclass 实现了 IShellLink
和 IPersistFile
。如果您已经有了一个 IShellLink
指针 pISL
,您可以用如下代码从 COM 对象请求一个 IPersistFile
接口:
HRESULT hr;
IPersistFile* pIPF;
hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );
然后,您使用 SUCCEEDED
宏测试 hr
来判断 QueryInterface()
是否成功。如果成功,您就可以像使用其他任何接口一样使用新的接口指针 pIPF
。您还必须调用 pIPF->Release()
来告诉 COM 对象您已经用完这个接口了。
请密切注意 - 字符串处理
我需要暂时绕个弯,讨论一下如何在 COM 代码中处理字符串。如果您熟悉 Unicode 和 ANSI 字符串的工作原理,并且知道如何在两者之间进行转换,那么您可以跳过本节。否则,请继续阅读。
每当 COM 方法返回一个字符串时,该字符串都将是 Unicode 格式。(嗯,所有遵循 COM 规范编写的方法都是如此!)Unicode 是一种像 ASCII 一样的字符编码方案,只是所有字符都是 2 字节长。如果您想将字符串转换成更易于管理的状态,您应该将其转换为 TCHAR
字符串。
TCHAR
和 _t
函数(例如 _tcscpy()
)旨在让您用同一套源代码处理 Unicode 和 ANSI 字符串。在大多数情况下,您将编写使用 ANSI 字符串和 ANSI Windows API 的代码,因此为了简单起见,本文的其余部分我将使用 char
而不是 TCHAR
。不过,您绝对应该阅读有关 TCHAR
类型的内容,以便在遇到他人编写的代码时有所了解。
当您从 COM 方法获得一个 Unicode 字符串时,您可以通过以下几种方式将其转换为 char
字符串:
- 调用
WideCharToMultiByte()
API。 - 调用 CRT 函数
wcstombs()
。 - 使用
CString
构造函数或赋值运算符(仅限 MFC)。 - 使用 ATL 字符串转换宏。
WideCharToMultiByte()
您可以使用 WideCharToMultiByte()
API 将 Unicode 字符串转换为 ANSI 字符串。这个 API 的原型是:
int WideCharToMultiByte ( UINT CodePage, DWORD dwFlags, LPCWSTR lpWideCharStr, int cchWideChar, LPSTR lpMultiByteStr, int cbMultiByte, LPCSTR lpDefaultChar, LPBOOL lpUsedDefaultChar );
参数如下:
CodePage
- 用于转换 Unicode 字符的代码页。您可以传递
CP_ACP
来使用当前的 ANSI 代码页。代码页是包含 256 个字符的集合。字符 0-127 始终与 ASCII 编码相同。字符 128-255 则不同,可能包含图形或带变音符号的字母。每种语言或地区都有自己的代码页,因此使用正确的代码页以正确显示重音字符非常重要。 dwFlags
dwFlags
决定了 Windows 如何处理“组合”Unicode 字符,即一个字母后跟一个变音符号。组合字符的一个例子是è
。如果这个字符在CodePage
中指定的代码页里,那么什么特别的事情都不会发生。但是,如果它不在代码页里,Windows 就必须将其转换为其他东西。
传递WC_COMPOSITECHECK
会使 API 检查无法映射的组合字符。传递WC_SEPCHARS
会使 Windows 将字符分解为两部分,即字母后跟变音符号,例如e`
。传递WC_DISCARDNS
会使 Windows 丢弃变音符号。传递WC_DEFAULTCHAR
会使 Windows 用lpDefaultChar
参数中指定的“默认”字符替换组合字符。默认行为是WC_SEPCHARS
。lpWideCharStr
- 要转换的 Unicode 字符串。
cchWideChar
lpWideCharStr
的长度,以 Unicode 字符为单位。通常您会传递 -1,表示该字符串是以零结尾的。lpMultiByteStr
- 一个
char
缓冲区,将存放转换后的字符串。 cbMultiByte
lpMultiByteStr
的大小,以字节为单位。lpDefaultChar
- 可选 - 一个包含单个字符的 ANSI 字符串,当
dwFlags
包含WC_COMPOSITECHECK | WC_DEFAULTCHAR
且某个 Unicode 字符无法映射到等效的 ANSI 字符时,将插入此“默认”字符。您可以传递 NULL,让 API 使用系统默认字符(截至本文撰写时,是一个问号)。 lpUsedDefaultChar
- 可选 - 指向一个
BOOL
变量的指针,该变量将被设置为指示默认字符是否曾被插入到 ANSI 字符串中。如果您不关心此信息,可以传递 NULL。
呼,好多无聊的细节!和往常一样,文档让它看起来比实际复杂得多。这里有一个例子,展示如何使用这个 API:
// Assuming we already have a Unicode string wszSomeString... char szANSIString [MAX_PATH]; WideCharToMultiByte ( CP_ACP, // ANSI code page WC_COMPOSITECHECK, // Check for accented characters wszSomeString, // Source Unicode string -1, // -1 means string is zero-terminated szANSIString, // Destination char string sizeof(szANSIString), // Size of buffer NULL, // No default character NULL ); // Don't care about this flag
此调用之后,szANSIString
将包含该 Unicode 字符串的 ANSI 版本。
wcstombs()
CRT 函数 wcstombs()
更简单一些,但它最终只是调用了 WideCharToMultiByte()
,所以结果是一样的。wcstombs()
的原型是:
size_t wcstombs ( char* mbstr, const wchar_t* wcstr, size_t count );
参数如下:
mbstr
- 一个用于存放结果 ANSI 字符串的
char
缓冲区。 wcstr
- 要转换的 Unicode 字符串。
计数
mbstr
缓冲区的大小,以字节为单位。
wcstombs()
在其对 WideCharToMultiByte()
的调用中使用了 WC_COMPOSITECHECK | WC_SEPCHARS
标志。重用前面的例子,您可以用这样的代码来转换一个 Unicode 字符串:
wcstombs ( szANSIString, wszSomeString, sizeof(szANSIString) );
CString
MFC 的 CString
类包含接受 Unicode 字符串的构造函数和赋值运算符,所以您可以让 CString
为您完成转换工作。例如:
// Assuming we already have wszSomeString... CString str1 ( wszSomeString ); // Convert with a constructor. CString str2; str2 = wszSomeString; // Convert with an assignment operator.
ATL 宏
ATL 有一套方便的宏用于字符串转换。要将 Unicode 字符串转换为 ANSI,请使用 W2A()
宏(助记符为“wide to ANSI”)。实际上,更准确地说,您应该使用 OLE2A()
,其中“OLE”表示该字符串来自 COM 或 OLE 源。不管怎样,这里有一个如何使用这些宏的例子。
#include <atlconv.h> // Again assuming we have wszSomeString... { char szANSIString [MAX_PATH]; USES_CONVERSION; // Declare local variable used by the macros. lstrcpy ( szANSIString, OLE2A(wszSomeString) ); }
OLE2A()
宏“返回”一个指向转换后字符串的指针,但转换后的字符串存储在一个临时的栈变量中,所以我们需要用 lstrcpy()
创建我们自己的副本。您应该研究的其他宏包括 W2T()
(Unicode 到 TCHAR
)和 W2CT()
(Unicode 字符串到 const TCHAR
字符串)。
还有一个 OLE2CA()
宏(Unicode 字符串转为 const char
字符串),我们可以在上面的代码片段中使用它。OLE2CA()
实际上是那种情况下的正确宏,因为 lstrcpy()
的第二个参数是 const char*
,但我不想一次性给您灌输太多东西。
坚持使用 Unicode
另一方面,如果您不会对字符串进行任何复杂的操作,也可以直接保持字符串为 Unicode 格式。如果您正在编写一个控制台应用程序,您可以使用 std::wcout
全局变量来打印 Unicode 字符串,例如:
wcout << wszSomeString;
但请记住,wcout
期望所有字符串都是 Unicode 格式,所以如果您有任何“普通”字符串,您仍然需要使用 std::cout
来输出它们。如果您有字符串字面量,请在它们前面加上 L
使其成为 Unicode,例如:
wcout << L"The Oracle says..." << endl << wszOracleResponse;
如果您保持字符串为 Unicode 格式,有几条限制:
- 您必须对 Unicode 字符串使用
wcsXXX()
字符串函数,例如wcslen()
。 - 除了极少数例外,您不能在 Windows 9x 上将 Unicode 字符串传递给 Windows API。要编写能在 9x 和 NT 上无需修改即可运行的代码,您需要使用
TCHAR
类型,如 MSDN 中所述。
融会贯通 - 示例代码
以下是两个例子,用来说明本文中涉及的 COM 概念。代码也包含在文章的示例项目中。
使用具有单个接口的 COM 对象
第一个例子展示了如何使用一个公开单个接口的 COM 对象。这是您会遇到的最简单的情况。代码使用 shell 中包含的 Active Desktop coclass 来检索当前壁纸的文件名。您需要安装 Active Desktop 才能使此代码工作。
涉及的步骤是:
- 初始化 COM 库。
- 创建一个用于与 Active Desktop 交互的 COM 对象,并获取一个
IActiveDesktop
接口。 - 调用 COM 对象的
GetWallpaper()
方法。 - 如果
GetWallpaper()
成功,打印壁纸的文件名。 - 释放接口。
- 反初始化 COM 库。
WCHAR wszWallpaper [MAX_PATH]; CString strPath; HRESULT hr; IActiveDesktop* pIAD; // 1. Initialize the COM library (make Windows load the DLLs). Normally you would // call this in your InitInstance() or other startup code. In MFC apps, use // AfxOleInit() instead. CoInitialize ( NULL ); // 2. Create a COM object, using the Active Desktop coclass provided by the shell. // The 4th parameter tells COM what interface we want (IActiveDesktop). hr = CoCreateInstance ( CLSID_ActiveDesktop, NULL, CLSCTX_INPROC_SERVER, IID_IActiveDesktop, (void**) &pIAD ); if ( SUCCEEDED(hr) ) { // 3. If the COM object was created, call its GetWallpaper() method. hr = pIAD->GetWallpaper ( wszWallpaper, MAX_PATH, 0 ); if ( SUCCEEDED(hr) ) { // 4. If GetWallpaper() succeeded, print the filename it returned. // Note that I'm using wcout to display the Unicode string wszWallpaper. // wcout is the Unicode equivalent of cout. wcout << L"Wallpaper path is:\n " << wszWallpaper << endl << endl; } else { cout << _T("GetWallpaper() failed.") << endl << endl; } // 5. Release the interface. pIAD->Release(); } else { cout << _T("CoCreateInstance() failed.") << endl << endl; } // 6. Uninit the COM library. In MFC apps, this is not necessary since MFC does // it for us. CoUninitialize();
在这个示例中,我使用了 std::wcout
来显示 Unicode 字符串 wszWallpaper
。
使用具有多个接口的 COM 对象
第二个例子展示了如何在一个公开多个接口的 COM 对象上使用 QueryInterface()
。代码使用 shell 中包含的 Shell Link coclass,为我们在上一个例子中检索到的壁纸文件创建一个快捷方式。
涉及的步骤是:
- 初始化 COM 库。
- 创建一个用于创建快捷方式的 COM 对象,并获取一个
IShellLink
接口。 - 调用
IShellLink
接口的SetPath()
方法。 - 在 COM 对象上调用
QueryInterface()
并获取一个IPersistFile
接口。 - 调用
IPersistFile
接口的Save()
方法。 - 释放接口。
- 反初始化 COM 库。
CString sWallpaper = wszWallpaper; // Convert the wallpaper path to ANSI IShellLink* pISL; IPersistFile* pIPF; // 1. Initialize the COM library (make Windows load the DLLs). Normally you would // call this in your InitInstance() or other startup code. In MFC apps, use // AfxOleInit() instead. CoInitialize ( NULL ); 2. Create a COM object, using the Shell Link coclass provided by the shell. // The 4th parameter tells COM what interface we want (IShellLink). hr = CoCreateInstance ( CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (void**) &pISL ); if ( SUCCEEDED(hr) ) { // 3. Set the path of the shortcut's target (the wallpaper file). hr = pISL->SetPath ( sWallpaper ); if ( SUCCEEDED(hr) ) { // 4. Get a second interface (IPersistFile) from the COM object. hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF ); if ( SUCCEEDED(hr) ) { // 5. Call the Save() method to save the shortcut to a file. The // first parameter is a Unicode string. hr = pIPF->Save ( L"C:\\wallpaper.lnk", FALSE ); // 6a. Release the IPersistFile interface. pIPF->Release(); } } // 6b. Release the IShellLink interface. pISL->Release(); } // Printing of error messages omitted here. // 7. Uninit the COM library. In MFC apps, this is not necessary since MFC // does it for us. CoUninitialize();
处理 HRESULT
我已经展示了一些简单的错误处理,使用了 SUCCEEDED
和 FAILED
宏。现在我将提供更多关于如何处理从 COM 方法返回的 HRESULT
的细节。
一个 HRESULT
是一个 32 位的有符号整数,非负值表示成功,负值表示失败。一个 HRESULT
有三个字段:严重性位(用于指示成功或失败)、功能代码和状态代码。“功能”(facility)指示 HRESULT
来自哪个组件或程序。微软为各种组件分配功能代码,例如 COM 有一个,任务计划程序有一个,等等。“代码”是一个 16 位的字段,本身没有内在含义;这些代码只是数字和意义之间的任意关联,就像 GetLastError()
返回的值一样。
如果您在 winerror.h
文件中查找错误代码,您会看到列出了很多 HRESULT
,其命名约定为 [功能]_[严重性]_[描述]。任何组件都可能返回的通用 HRESULT
(如 E_OUTOFMEMORY
)的名称中没有功能部分。例子:
REGDB_E_READREGDB
:功能 = REGDB,代表“注册表数据库”;E = 错误;READREGDB 是对错误的描述(无法读取数据库)。S_OK
:功能 = 通用;S = 成功;OK 是对状态的描述(一切正常)。
幸运的是,有比翻阅 winerror.h
更简单的方法来确定一个 HRESULT
的含义。对于内置功能,可以使用错误查找(Error Lookup)工具来查找 HRESULT
。例如,假设您在调用 CoCreateInstance()
之前忘记了调用 CoInitialize()
。CoCreateInstance()
将返回一个值 0x800401F0。您可以将该值输入到错误查找工具中,您将看到描述:“CoInitialize has not been called.”(CoInitialize 尚未被调用。)
您也可以在调试器中查找 HRESULT
的描述。如果您有一个名为 hres
的 HRESULT
变量,您可以在监视(Watch)窗口中通过输入“hres,hr”作为要监视的值来查看其描述。“,hr”告诉 VC 将该值显示为 HRESULT
的描述。
参考文献
《Essential COM》作者 Don Box,ISBN 0-201-63446-5。关于 COM 规范和 IDL(接口定义语言),你想知道的一切都在这里。前两章深入探讨了 COM 规范及其旨在解决的问题。
《MFC Internals》作者 George Shepherd 和 Scot Wingo,ISBN 0-201-40721-3。深入探讨了 MFC 对 COM 的支持。
《Beginning ATL 3 COM Programming》作者 Richard Grimes 等人,ISBN 1-861001-20-7。这本书深入讲解了如何使用 ATL 编写自己的 COM 组件。