1  错误处理

本章内容包括:

1.1  定义自己的错误代码

1.2  ErrorShow示例程序

 

在深入讨论Microsoft Windows 提供的诸多特性之前,应该先理解各个Windows函数是如何进行错误处理的。

 

调用Windows函数时,它会先验证传入的参数,然后再开始执行任务。如果传入的参数无效,或者由于其他原因导致操作无法执行,则函数的返回值将指出函数因为某些原因失败了。表1-1展示了大多数Windows函数使用的返回值的数据类型。

 

1-1  常见的Windows函数返回值数据类型

数据类型

指出函数调用失败的值

VOID

这个函数不可能失败。只有极少数Windows函数的返回值类型为VOID

BOOL

如果函数失败,返回值为0;否则,返回值是一个非零值。应避免测试返回值是否为TRUE最稳妥的做法是检查它是否不为FALSE

HANDLE

如果函数失败,则返回值通常为NULL;否则,HANDLE标识一个你可以操纵的对象。请注意这种返回值,因为某些函数会返回为INVALID_HANDLE_VALUE的一个句柄值,它被定义为–1。函数的Platform SDK文档清楚说明了函数是返回NULL还是INVALID_HANDLE_VALUE来标识失败。

PVOID

如果函数调用失败,返回值为NULL;否则,PVOID标识一个数据块的内存地址。

LONG/DWORD

这种类型比较棘手。返回计数的函数通常会返回一个LONGDWORD。如果函数出于某种原因不能对我们想要计数的东西进行计数,它通常会返回0 –1具体取决于函数)。如果要调用一个返回LONG/DWORD的函数,务必仔细阅读Platform SDK文档,确保正确地检查可能出现的错误。

 

如果一个Windows函数能返回错误代码,通常有助于我们理解函数调用为什么会失败。Microsoft编辑了一个列表,其中列出了所有可能的错误代码,并为每个错误代码都分配了一个32位的编号。

 

在内部,当一个Windows函数检测到错误时,它会使用一种名为线程本地存储区[1]的机制将相应的错误代码与主调线程[2]关联到一起(线程本地存储区的详情将在第21章讨论)。这种机制使不同的线程能独立运行,不会影响到其他线程的错误代码。函数返回时,其返回值会指出已发生一个错误。要查看具体是什么错误,请调用GetLastError函数:

DWORD GetLastError();

它的作用很简单,就是返回由上一个函数调用设置的线程的32位错误代码。

有了32位错误代码之后,接着需要把它转换为更有用的信息。WinError.h头文件包含了Microsoft定义的错误代码列表。下面摘录了其中的一部分,便于大家了解错误代码是如何定义的:

 

// MessageId: ERROR_SUCCESS

//

// MessageText:

//

// The operation completed successfully.

//

#define ERROR_SUCCESS                                           0L

#define NO_ERROR 0L                                                                             // dderror

#define SEC_E_OK                                                        ((HRESULT)0x00000000L)

//

// MessageId: ERROR_INVALID_FUNCTION

//

// MessageText:

//

// Incorrect function.

//

#define ERROR_INVALID_FUNCTION                                  1L                      // dderror

//

// MessageId: ERROR_FILE_NOT_FOUND

//

// MessageText:

//

// The system cannot find the file specified.

//

#define ERROR_FILE_NOT_FOUND                                    2L

//

// MessageId: ERROR_PATH_NOT_FOUND

//

// MessageText:

//

// The system cannot find the path specified.

//

#define ERROR_PATH_NOT_FOUND                                    3L

//

// MessageId: ERROR_TOO_MANY_OPEN_FILES

//

// MessageText:

//

// The system cannot open the file.

//

#define ERROR_TOO_MANY_OPEN_FILES                               4L

//

// MessageId: ERROR_ACCESS_DENIED

//

// MessageText:

//

// Access is denied.

//

#define ERROR_ACCESS_DENIED                                     5L

 

可以看出,每个错误都有三种表示:一个消息ID(是一个宏定义,我们可以在源代码中用它来与GetLastError返回值进行比较)、消息文本(是一句用来对错误进行描述的英文文本)和一个数值(我们应该尽量使用消息ID避免使用这个数值。注意,整个WinError.h头文件长达39 000行,这里只摘录了其中的极小一部分!

 

一个Windows函数失败之后,应该马上调用GetLastError,因为假如又调用了另一个Windows函数,则此值很可能被改写。注意,成功调用的Windows函数可能用ERROR_SUCCESS改写此值。

 

一些Windows函数调用成功可能是缘于不同的原因。例如,创建一个具名事件内核对象时,以下两种情况均会成功:对象实际地完成创建,或者存在一个同名的事件内核对象。应用程序也许需要知道成功的原因。为返回这种信息,Microsoft选择采用“上一个错误代码[3]”机制。所以,当特定函数成功时,我们可以调用GetLastError来确定额外的信息。对于具有这种行为的函数,Platform SDK文档会清楚指明能以这种方式使用GetLastErrorCreateEvent函数就是这样一个例子,该函数的文档中明确说明如果一个具名事件已经存在,那么它会返回ERROR_ALREADY_EXISTS

 

调试程序时,我发现对线程的“上一个错误代码”进行监视是相当有用的。在Microsoft Visual Studio中,Microsoft的调试器支持一个很有用的功能——我们可以配置Watch窗口,让它始终显示线程的上一个错误代码和错误的文本描述。具体的做法是:在Watch窗口中选择一行,然后输入$err,hr。来看看图1-1的例子。在这个例子中,我已经调用了CreateFile函数。该函数返回值为INVALID_HANDLE_VALUE (–1)的一个HANDLE,指出它无法打开指定文件。但是Watch窗口指出,上一个错误代码(也就是调用GetLastError函数返回的错误代码)是0x00000002。多亏有了,hr限定符,Watch窗口进一步指出错误代码2是“The system cannot find the file specified”(系统找不到指定文件)。这就是在WinError.h头文件中为错误代码2列出的消息文本

 

1-1  Visual StudioWatch窗口中使用$err,hr来查看当前线程的“上一个错误代码”

 

Visual Studio还附带了一个很小的实用程序,名为Error Lookup。利用它,可以将错误代码编号转换为相应的文本描述。如下图所示:

 

如果我在自己写的程序中检测到一个错误,那么我可能希望把描述错误的文本信息显示给用户。Windows提供了一个函数,可以将错误代码转换为相应的文本描述。此函数名为FormatMessage,如下所示:

 

DWORD FormatMessage(

DWORD dwFlags,

LPCVOID pSource,

DWORD dwMessageId,

DWORD dwLanguageId,

PTSTR pszBuffer,

DWORD nSize,

va_list *Arguments);

FormatMessage功能实际上相当丰富,如果要生成要向用户显示的字符串,那么它是首选的方式。之所以说它好用,一个原因是它能轻松地支持多种语言[4]。它能获取一个语言标识符作为参数,并返回那种语言的文本。当然,我们首先必须翻译好字符串,并将翻译好的消息表[5]资源嵌入自己的.exeDLL模块中。但此后FormatMessage函数就能自动地从正确的语言中选择字符串。ErrorShow示例程序(参见后文)演示了如何调用这个函数将Microsoft定义的错误代码编号转换为相应的文本描述。

 

经常有人问我,Microsoft是否维护着一个主列表[6],其中完整地列出了每个Windows函数可能返回的所有错误代码。很遗憾,答案是否定的。而且,Microsoft决不可能提供这样的列表,因为随着新版本的操作系统的问世,构建和维护这样的列表是非常困难的。

 

这种列表的问题在于:我们可以调用一个Windows函数,但这个函数在内部可能会调用另一个函数,后者又可能调用其他函数……以此类推。出于众多原因,任何一个函数都可能失败。有时,当一个函数失败时,高层的函数也许能够恢复,并继续执行我们希望的操作。要创建这样一个主列表,Microsoft必须跟踪每个函数调用的路径,生成所有可能的错误代码的列表。这是非常难的。而且,随着新版本的操作系统的发布,这些函数的执行路径也可能发生改变。

1.1  定义自己的错误代码

前面讲述了Windows函数如何向其调用者指出错误。除此之外,Microsoft还允许我们在自己的函数中使用这种机制。假定我们要写一个供其他人调用的函数。由于这个函数可能会因为这样或那样的原因而失败,因此我们需要向调用者指出错误。

 

为了指出错误,只需设置线程的上一个错误代码,然后令自己的函数返回FALSE INVALID_HANDLE_VALUENULL或者其他合适的值。为了设置线程的上一个错误代码,只需调用以下函数,并传递我们认为合适的任何32位值:

 

VOID SetLastError(DWORD dwErrCode);

 

我会尽量使用WinError.h中现有的代码——只要代码能很好地反映我想报告的错误。如果WinError.h中的任何一个代码都不能准确反映一个错误,就可以创建自己的代码。错误代码是一个32位数,由表1-2描述的几个不同的字段组成。

 

1-2  错误代码的不同字段

位:    

31–30

29

28

27–16

15–0

内容

严重性

Microsoft/客户

保留

Facility代码

异常代码

含义

0 = 成功

1 = 信息(提示)

2 = 警告

3 = 错误

0 = Microsoft定义的代码

1 = 客户定义的代码

必须为0

256个值由Microsoft保留

Microsoft/客户定义的代码

 

这些字段将在第24章详细讨论。就目前来说,惟一需要注意的重要字段是第29Microsoft承诺,在它所生成的所有错误代码中,此位将始终为0。但是,如果要创建自己的错误代码,就必须在此位放入一个1。通过这种方式,我们可以保证自己的错误代码绝不会与Microsoft定义的错误代码冲突,这既包括当前已经存在的错误代码,也包括将来会创建的错误代码。注意,Facility字段非常大,足以容纳4096个可能的值。其中,前256个值是为Microsoft保留的,其余的值可由我们自己的应用程序来定义。

1.2  ErrorShow示例程序

ErrorShow应用程序(01-ErrorShow.exe,演示了如何得到一个错误代码的文本描述。此应用程序的源代码和资源文件可以在本书配套网页的01-ErrorShow目录中找到,网址是http://wintellect.com/Books.aspx

 

简单地说,这个应用程序展示了调试器的Watch窗口和Error Lookup程序是如何工作的(参见前面的两个屏幕截图)。启动程序时,将出现以下窗口。

 

 

可以在编辑控件中输入任何错误编号。单击Look Up按钮后,与该错误对应的文本描述将在对话框底部的滚动窗口中显示。对于这个应用程序,我们惟一感兴趣的是如何调用FormatMessage。下面展示了我如何使用这个函数:

 

// Get the error code

DWORD dwError = GetDlgItemInt(hwnd, IDC_ERRORCODE, NULL, FALSE);

 

HLOCAL hlocal = NULL; // Buffer that gets the error message string

 

// Use the default system locale since we look for Windows messages

// Note: this MAKELANGID combination has a value of 0

DWORD systemLocale = MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL);

 

// Get the error code's textual description

BOOL fOk = FormatMessage(

FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS |

FORMAT_MESSAGE_ALLOCATE_BUFFER,

NULL, dwError, systemLocale,

(PTSTR) &hlocal, 0, NULL);

 

if (!fOk) {

// Is it a network-related error?

HMODULE hDll = LoadLibraryEx(TEXT("netmsg.dll"), NULL,

DONT_RESOLVE_DLL_REFERENCES);

if (hDll != NULL) {

fOk = FormatMessage(

FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS |

FORMAT_MESSAGE_ALLOCATE_BUFFER,

hDll, dwError, systemLocale,

(PTSTR) &hlocal, 0, NULL);

FreeLibrary(hDll);

}

}

 

if (fOk && (hlocal != NULL)) {

SetDlgItemText(hwnd, IDC_ERRORTEXT, (PCTSTR) LocalLock(hlocal));

LocalFree(hlocal);

} else {

SetDlgItemText(hwnd, IDC_ERRORTEXT,

TEXT("No text found for this error number."));

 

上面这段代码的第一行从编辑控件获取错误代码。然后,定义了一个内存句柄并将它初始化为NULLFormatMessage函数会在内部分配一块内存,并将这块内存的句柄返回给我们。

 

在调用FormatMessage时,我们给它传入了FORMAT_MESSAGE_FROM_SYSTEM标志。该标志告诉FormatMessage:与我们希望获得的错误描述字符串相对应的,是一个由系统定义的错误代码。另外,我们还传入了FORMAT_MESSAGE_ALLOCATE_BUFFER标志,要求该函数分配一块足以容纳错误文本描述的内存。这块内存的句柄将在hlocal变量中返回。FORMAT_MESSAGE_IGNORE_INSERTS标志则允许我们获得含有%占位符的消息,这些占位符被Windows用来提供更多与上下文相关的信息,如下例所示:

 

 

如果不传递这个标志,那么我们必须在Arguments参数中提供这些占位符的值。但这对于Error Show程序来说是不可能的,因为我们事先并不知道消息的内容。

 

第三个参数指出想要查找的错误编号。第四个参数指出要用什么语言来显示文本描述。由于我们对Windows本身提供的消息感兴趣,因此我们根据两个特定的常量(即LANG_NEUTRALSUBLANG_NEUTRAL)来生成语言标识符,把这两个常量结合在一起得到的值为0——意味着操作系统的默认语言。在许多情况下,因为事先并不知道操作系统的安装语言是什么,所以我们不能在代码中明确指定一种特定的语言,这里就是一个例子。

 

如果FormatMessage成功,我会把内存块中的文本描述复制到对话框底部的滚动窗口中。如果FormatMessage失败,我会尝试在NetMsg.dll模块中查找消息代码,看错误是否与网络有关(有关如何在磁盘上搜索DLL的详情,请参见第20章)。利用NetMsg.dll模块的句柄,我再一次调用FormatMessage。我们可以看到,每个DLL.exe都可以有自己的一套错误代码。我们也可以向自己的模块中添加错误代码,只要使用Message CompilerMC.exe)来创建一个消息资源并将它添加到DLL(或.exe)模块中就可以了。Visual StudioError Lookup工具允许我们使用Modules对话框来载入新的模块,并在其中查找消息代码。

 



[1] 译注:thread-local storage

[2] 译注:calling thread

[3] 译注:last error-code

[4] 译注:这里的语言是自然语言,比如汉语、英语等等,而不是计算机编程语言

[5] 译注:message table

[6] 译注:master list