COM编程入门Part Ⅰ- 什么是COM和如何使用COM [译]

本篇文章为翻译文章,适合像我一样,之前从来没有接触过COM编程的人,如果翻译的有什么不足之处,希望大家多多指出。

原文链接:
https://www.codeproject.com/Articles/633/Introduction-to-COM-What-It-Is-and-How-to-Use-It

以下为译文部分:


这是一篇面向COM编程新手程序员的教程,解释了COM服务器的内部原理,以及如何使用C++编写你自己的接口。

1. 本文的目的

我写这篇文章的目的是为了那些开始使用COM并且需要一些帮助来理解基础知识的程序员。本文简要介绍了COM规范,然后解释了一些COM术语,并描述了如何重用现有的COM组件。本文不讨论如何编写自己的COM对象或接口。


2. 介绍

COM(Component Object Model)是最近WIndows世界中最流行的TLA(three-letter acronym)。一些新技术的出现都是基于COM的。并且这些技术文档中抛出了很多术语, 比如 COM对象接口服务器 等等,但都假设您已熟悉COM的基本工作原理和使用方法。

本文由浅入深描述COM的内在运行机制,并展示了如何使用其他人提供的COM对象(特别是Windows sheel)。阅读完本片文章,您将具备能够使用Windows和第三方提供的COM对象的能力。

本文假设您熟练使用C++。在我的示例代码中使用了一点MFC和ATL,但我会详细解释这些代码,因此如果您不熟悉MFC或者ATL也能够跟得上。本篇文章将包括如下内容:

  • COM到底是什么? - 对COM标准的快速介绍,以及创建该标准所要解决的问题。在使用COM时,你不需要知道这些,但我仍然建议你阅读它,以理解为什么在COM中事情是这样做的。
  • 基本元素的定义 - COM术语和这些术语所表示的内容的描述。
  • 使用COM对象 - 概述如何创建、使用和销毁COM对象。
  • 基本接口-IUnknown - 基本接口中方法的描述,IUnknown
  • 字符串处理 - 在COM中如何处理字符串
  • COM编程示例 - 两组示例代码演示了本文中讨论的所有概念。
  • 处理 HRESULTs - 对HRESULT类型以及如何测试错误和成功代码的描述。

3. COM到底是什么?

简单地说,COM是一种跨不同应用程序和语言共享二进制代码的方法。这与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或任何其他操作系统。然而,我似乎从来没有提到过COM以外的Windows世界。


4. 基本元素的定义

让我们从下往上。接口只是一组函数。这些函数被称为方法。接口名以I开头,例如IShellLink。在c++中,接口被编写为只有纯虚函数的抽象基类。

接口可以从其他接口继承。继承的工作方式就像c++中的单继承。接口不允许多重继承。

coclass (component object class的缩写)包含在DLL或EXE中,背后包含了一个或多个接口的代码。coclass被称为实现这些接口。COM对象是coclass在内存中的实例。请注意,COM“类”与c++“类”是不同的,尽管COM类的实现通常是c++类。

COM服务器 是一个二进制文件(DLL或EXE),其中包含一个或多个coclass。

注册 (Registration)是创建注册表项的过程,这些注册表项告诉Windows COM服务器的位置。
取消注册 (Unregistration) 是相反的——删除那些注册表项。

GUID (与“fluid”押韵,表示全球唯一标示符 – globally unique identifier)是一个128位的数字。 guid是COM的独立于语言的识别事物的方式。每个接口和coclass都有一个GUID。由于GUID在全世界都是唯一的,因此可以避免名称冲突 (只要您使用COM API创建它们)。您还会不时看到术语 UUID (它代表通用唯一识别符 – universally unique identifier)。实际上, uuid和guid是相同的。

IDCLSID 是一个coclass的GUID名。接口 IDIID 是一个接口(interface)的GUID的名字。

在COM中GUID使用如此的广泛,有两个原因:

  1. GUID只是底层的数字,任何编程语言都可以处理它们。
  2. 任何人在任何机器上创建的每个GUID在正确创建时都是惟一的。因此,COM开发人员可以自己创建GUID,而不会出现两个开发人员选择相同GUID的情况。这就消除了发布guid的中间授权的需要。

HRESULT 是一个整数类型,是COM用来返回错误和成功码的。它不是任何东西的“句柄”,尽管有H前缀。稍后我将对HRESULT以及如何测试它们进行更多的说明。

最后,COM库(COM library) 是操作系统的一部分,当你做与COM相关的事情时,它会与你交互。 通常, COM库(COM library) 被称为“COM”,但是为了避免混淆,我在这里不这么做。


5. 使用COM对象

每种语言都有自己处理对象的方式。例如,在c++中,您可以在栈上创建它们,或者使用new动态地分配它们。因为COM必须是语言无关的,所以COM库提供了自己的对象管理例程。COM和c++对象管理的比较如下:

创建一个对象
  • C++中,使用 operator new 或者在栈中创建一个对象
  • COM中,调用COM库的API创建
删除一个对象
  • C++中,使用 operator delete 或者栈中的对象作用域消失时,释放这个对象
  • COM中,所有对象都保留自己的引用计数。调用者必须在调用者使用对象完成时告诉对象。当引用计数达到0时,COM对象从内存中释放自己。

由此可见,对象的创建和销毁这两个阶段缺一不可。当你创建一个COM对象时,你告诉COM库你需要什么接口。如果对象被成功创建,COM库返回一个指向所请求接口的指针。然后,您可以通过该指针调用方法,就像它是一个普通c++对象的指针一样。


6. 创建COM对象

要创建COM对象并从该对象获取接口,需要调用COM库API CoCreateInstance() , 函数原型如下:

HRESULT CoCreateInstance (
    REFCLSID  rclsid,
    LPUNKNOWN pUnkOuter,
    DWORD     dwClsContext,
    REFIID    riid,
    LPVOID*   ppv );

参数:

  • rclsid : coClassCLSID 。例如,您可以传递 CLSID_ShellLink 来创建一个COM对象,用它来创建快捷方式。
  • pUnkOuter : 这只在聚合COM对象时使用,它可以获取现有的coclass并向其添加新方法。出于我们的目的,我们可以传递NULL来表示我们没有使用聚合。
  • dwClsContext : 指明我们要使用的COM服务器类型。在本文中,我们将始终使用最简单的服务器类型,即进程内DLL(in-process 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 用于测试失败代码。


7. 删除一个COM对象

如前所述,你不能释放COM对象,你只是告诉它们你已经用完了。每个COM对象实现的 IUnknown 接口都有一个 Release() 方法。调用此方法是为了告诉COM对象您不再需要它。一旦调用 Release() ,就不能再使用接口指针了,因为COM对象随时可能从内存中消失。

如果你的应用程序使用了很多不同的COM对象,那么在你使用完接口的时候调用 Release() 是非常重要的。 如果你不释放(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 接口将在下一节中详细解释。


8. 基本接口 - IUnknown

每个COM接口都是从 IUnknown 派生出来的。这个名称有点误导人,因为它不是一个未知的接口。 这个名字意味着如果你有一个指向COM对象的 IUnknown 指针,你不知道底层的对象是什么, 因为每个COM对象都实现 IUnknown

IUnknown 有三个方法:

  1. AddRef() - 通知COM对象增加它的引用计数。如果您复制了接口指针,并且原始指针和副本仍然会被使用,那么您将使用此方法。在本文中,我们不需要使用 AddRef()
  2. Release() - 通知COM对象减小它的引用计数。参见前面的示例以获得演示 Release() 的代码片段。
  3. QueryInterface() - 从COM对象请求接口指针。当coclass实现多个接口时使用此方法。

我们已经看到了 Release() 的作用,但是QueryInterface()呢?当您使用 CoCreateInstance() 创建COM对象时,您将返回一个接口指针。如果COM对象实现了多个接口(不包括IUnknown), 您可以使用 QueryInterface() 来获得您需要的任何其他接口指针。QueryInterface()的原型为:

HRESULT IUnknown::QueryInterface (
    REFIID iid,
    void** ppv );

参数说明:

  • iid :你所请求的接口的IID。
  • ppv : 接口指针的地址。如果成功的话,QueryInterface()通过这个参数返回接口。

让我们继续我们的shell链接示例。生成shell links的coclass实现了 IShellLinkIPersistFile 。如果你已经有了一个 IShellLink 指针 pISL ,你可以用如下代码从COM对象请求 IPersistFile 接口:

HRESULT hr;
IPersistFile* pIPF;

hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );

然后使用宏 SUCCEEDED 测试 hr ,以确定 QueryInterface() 是否工作。如果成功,就可以像使用其他接口一样使用新的接口指针 pIPF 。您还必须调用 pIPF->Release() 来告诉COM对象您已经使用完了这个接口。


9. 密切关注 - 字符串处理

译者注:此部分我是直接使用有道词典翻译的,没有一句一句进行校验

我需要做一个暂时的弯路,并讨论如何处理字符串在COM代码。如果您熟悉Unicode和ANSI字符串的工作方式,并且知道如何在两者之间进行转换,那么您可以跳过这一节。否则,请继续阅读。

每当COM方法返回一个字符串时,该字符串将使用Unicode格式。(也就是说,所有写进COM规范的方法都是这样的)Unicode是一个像ASCII一样的字符编码方案,它所有字符只有2字节长。如果您希望将字符串转换为更易于管理的状态,那么应该将其转换为 TCHAR

TCHAR_t 函数(例如, _tcscpy() )被设计用来让您使用相同的源代码处理Unicode和ANSI字符串。在大多数情况下, 您将编写使用ANSI字符串和ANSI Windows API的代码,因此在本文的其余部分中,为了简单起见,我将使用 chars 而不是 TCHARs 。不过,您一定要了解TCHAR类型, 以免在其他人编写的代码中遇到它们。

当你从一个COM方法得到一个Unicode字符串时,你可以用以下几种方法将它转换成一个 char 字符串:

  1. 调用 WideCharToMultiByte() API。
  2. 调用 CTR函数 wcstombs()
  3. 使用 CString 构造函数或者赋值操作符(仅限MFC)
  4. 使用ATL字符串转化宏
(1) WideCharToMultiByte()

您可以将Unicode字符串转化为ANSI字符串使用 WideCharToMultiByte() API。这个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字符,即字母后跟变音符号。复合字符的一个例子是e。如果此字符位于代码页中指定的代码页中,则不会发生任何特殊情况。但是,如果它不在代码页中,Windows必须将其转换为其他内容。
    传递WC_COMPOSITECHECK使API检查非映射的复合字符。传递WC_SEPCHARS使Windows将字符分成两个部分,字母后跟变音符号,例如e '。传递WC_DISCARDNS会使Windows放弃变音符号。传递WC_DEFAULTCHAR将使Windows用lpDefaultChar参数中指定的“默认”字符替换复合字符。默认行为是WC_SEPCHARS。
  • lpWideCharStr : 要转换的Unicode字符串。
  • cchWideChar :lpWideCharStr在Unicode字符中的长度。通常传递-1,这表示字符串以零结尾。
  • lpMultiByteStr :一个字符缓冲区,用于保存转换后的字符串。
  • cbMultiByte : lpMultiByteStr的大小,单位为字节。
  • lpDefaultChar :可选——当dwFlags包含WC_COMPOSITECHECK | WC_DEFAULTCHAR和Unicode字符时,一个包含“默认”字符的单字符ANSI字符串不能映射到等效的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版本。

(2) wcstombs()

CRT函数wcstombs()稍微简单一些,但它最终只是调用了WideCharToMultiByte(),因此最终结果是相同的。wcstombs()的原型为:

size_t wcstombs (
    char*          mbstr,
    const wchar_t* wcstr,
    size_t         count );

参数如下:

  • mbstr : 一个字符缓冲区,用于保存生成的ANSI字符串。
  • wcstr : 要转换的Unicode字符串。
  • count : mbstr缓冲区的大小,以字节为单位。

wcstombs()在它对WideCharToMultiByte()的调用中使用WC_COMPOSITECHECK | WC_SEPCHARS标志。为了重用前面的例子,你可以用如下代码转换Unicode字符串:

wcstombs ( szANSIString, wszSomeString, sizeof(szANSIString) );
(3) 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.
(4) 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 to TCHAR)和W2CT() (Unicode string to const TCHAR string)。

有一个OLE2CA()宏(Unicode字符串到const char字符串),我们可以在上面的代码片段中使用它。对于这种情况,OLE2CA()实际上是正确的宏,因为lstrcpy()的第二个参数是const char*,但我不想一次抛出太多。

(5) Sticking with Unicode

【这个我不知道怎么翻译,应该是控制台输出相关的部分内容】

另一方面,如果您不需要对字符串做任何复杂的操作,那么您可以在Unicode中保留该字符串。如果你在写一个控制台应用程序,你可以打印Unicode字符串与 std::wcout 的全局变量,例如:

wcout << wszSomeString;

但是请记住,wcout要求所有字符串都是Unicode的,所以如果您有任何“正常”字符串,您仍然需要使用std::cout输出它们。如果你有字符串,在它们前面加上L使它们成为Unicode,例如:

wcout << L"The Oracle says..." << endl << wszOracleResponse;

如果你在Unicode中保存一个字符串,有几个限制:

  • 对于Unicode字符串,您必须使用 wcsXXX() 字符串函数,例如 wcslen()
  • 除了极少数例外,您不能将Unicode字符串传递给Windows 9x上的Windows API。要编写在9x和NT上不加更改地运行的代码,您需要使用TCHAR类型,如MSDN中所述。
(6) Qt中Unicode转换

【因为我个人更加擅长使用Qt开发,因此充了这个部分】

  • 函数 QString::fromWCharArray() WCHAR转为QString
  • 函数 QString::toWCharArray() QString转为WCHAR

10. 将一切结合在一起 - 示例代码

下面是两个示例,演示了本文中涉及的COM概念。代码也包含在本文的示例项目中。

(1) 使用带有单个接口的COM对象

第一个示例展示了如何使用公开单个接口的COM对象。这是将是你遇到的最简单的例子。该代码使用包含在sheel中的Active Desktop coclass来检索当前壁纸的文件名。要使此代码工作,您需要安装Active Desktop。

涉及的步骤为:

  1. 初始化COM库。
  2. 创建一个用于与 Active Desktop 交互的COM对象,并获得一个 IActiveDesktop 接口。
  3. 调用COM对象的 GetWallpaper() 方法。
  4. 如果 GetWallpaper() 成功,打印壁纸文件名。
  5. 释放接口
  6. 取消初始化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.</FONT>
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

(2) 使用具有多个接口的COM对象

第二个示例展示了如何对公开单个接口的COM对象使用 QueryInterface() 。代码使用包含在Shell中的Shell Link coclass为我们在上一个示例中检索到的墙纸文件创建快捷方式。

涉及的步骤有:

  1. 初始化COM库。
  2. 创建一个用于创建快捷方式的COM对象,并获得一个 IShellLink 接口。
  3. 调用 IShellLink 接口的 SetPath() 方法。
  4. 在COM对象上调用 QueryInterface() 并获得 IPersistFile 接口。
  5. 调用 IPersistFile 接口的Save() 方法。
  6. 释放接口
  7. 取消初始化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();

11. 处理HRESULTs

我已经展示了一些使用 SUCCEEDEDFAILED 宏的简单错误处理。现在, 我将提供一些关于如何处理从COM方法返回的 HRESULT s的更多细节。

HRESULT 是一个32位有符号整数,非负值表示成功,负值表示失败。 HRESULT 有三个位域:程度位(指示成功或失败)、功能码和状态码。“功能码”指示 HRESULT 来自哪个组件或程序。Microsoft将功能代码分配给各个组件,例如COM有一个,任务调度器有一个,等等。 “代码”是一个没有内在含义的16位字段;这些代码只是数字和含义之间的任意关联,就像 GetLastError() 返回的值一样。

如果您在 winerror.h 文件中查找错误代码,您会看到列出了许多 HRESULTs ,命名约定为[功能][程度][描述]。任何组件(如E_OUTOFMEMORY)都可以返回通用的 HRESULTs ,但在它们的名称中没有任何功能。例如:

  • REGDB_E_READREGDB : 功能= REGDB,表示“注册表数据库”;E =错误;READREGDB是对错误(无法读取数据库)的描述。
  • S_OK : 功能 =通用;S =成功;OK是对状态的描述(everything’s OK)。

幸运的是,有比查看 winerror.h 更容易的方法来确定 HRESULT 的含义。 内置工具的 HRESULTs 可以用错误查找工具查找。例如,假设您忘记在 CoCreateInstance() 之前调用 CoInitialize()CoCreateInstance() 将返回一个值0x800401F0。您可以在错误查找中输入该值,然后您将看到描述: “ CoInitialize 未被调用。”

您还可以在调试器中查找 HRESULT 描述。如果您有一个名为hres的 HRESULT 变量,那么您可以通过输入“hres,hr”作为要监视的值来查看监视窗口中的描述。“,hr”告诉VC将值显示为 HRESULT 描述。

不会飞的纸飞机
扫一扫二维码,了解我的更多动态。

下一篇文章:COM编程入门Part Ⅱ - 深入理解COM服务器[译]