本章内容包括:
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 |
这种类型比较棘手。返回计数的函数通常会返回一个LONG或DWORD。如果函数出于某种原因不能对我们想要计数的东西进行计数,它通常会返回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
#define NO_ERROR
#define SEC_E_OK ((HRESULT)0x
//
// MessageId: ERROR_INVALID_FUNCTION
//
// MessageText:
//
// Incorrect function.
//
#define ERROR_INVALID_FUNCTION
//
// MessageId: ERROR_FILE_NOT_FOUND
//
// MessageText:
//
// The system cannot find the file specified.
//
#define ERROR_FILE_NOT_FOUND
//
// MessageId: ERROR_PATH_NOT_FOUND
//
// MessageText:
//
// The system cannot find the path specified.
//
#define ERROR_PATH_NOT_FOUND
//
// MessageId: ERROR_TOO_MANY_OPEN_FILES
//
// MessageText:
//
// The system cannot open the file.
//
#define ERROR_TOO_MANY_OPEN_FILES
//
// MessageId: ERROR_ACCESS_DENIED
//
// MessageText:
//
// Access is denied.
//
#define ERROR_ACCESS_DENIED
可以看出,每个错误都有三种表示:一个消息ID(是一个宏定义,我们可以在源代码中用它来与GetLastError的返回值进行比较)、消息文本(是一句用来对错误进行描述的英文文本)和一个数值(我们应该尽量使用消息ID,避免使用这个数值)。注意,整个WinError.h头文件长达39 000多行,这里只摘录了其中的极小一部分!
一个Windows函数失败之后,应该马上调用GetLastError,因为假如又调用了另一个Windows函数,则此值很可能被改写。注意,成功调用的Windows函数可能用ERROR_SUCCESS改写此值。
一些Windows函数调用成功可能是缘于不同的原因。例如,创建一个具名事件内核对象时,以下两种情况均会成功:对象实际地完成创建,或者存在一个同名的事件内核对象。应用程序也许需要知道成功的原因。为返回这种信息,Microsoft选择采用“上一个错误代码[3]”机制。所以,当特定函数成功时,我们可以调用GetLastError来确定额外的信息。对于具有这种行为的函数,Platform SDK文档会清楚指明能以这种方式使用GetLastError。CreateEvent函数就是这样一个例子,该函数的文档中明确说明如果一个具名事件已经存在,那么它会返回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
Studio的Watch窗口中使用$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]资源嵌入自己的.exe或DLL模块中。但此后FormatMessage函数就能自动地从正确的语言中选择字符串。ErrorShow示例程序(参见后文)演示了如何调用这个函数将Microsoft定义的错误代码编号转换为相应的文本描述。
经常有人问我,Microsoft是否维护着一个主列表[6],其中完整地列出了每个Windows函数可能返回的所有错误代码。很遗憾,答案是否定的。而且,Microsoft决不可能提供这样的列表,因为随着新版本的操作系统的问世,构建和维护这样的列表是非常困难的。
这种列表的问题在于:我们可以调用一个Windows函数,但这个函数在内部可能会调用另一个函数,后者又可能调用其他函数……以此类推。出于众多原因,任何一个函数都可能失败。有时,当一个函数失败时,高层的函数也许能够恢复,并继续执行我们希望的操作。要创建这样一个主列表,Microsoft必须跟踪每个函数调用的路径,生成所有可能的错误代码的列表。这是非常难的。而且,随着新版本的操作系统的发布,这些函数的执行路径也可能发生改变。
前面讲述了Windows函数如何向其调用者指出错误。除此之外,Microsoft还允许我们在自己的函数中使用这种机制。假定我们要写一个供其他人调用的函数。由于这个函数可能会因为这样或那样的原因而失败,因此我们需要向调用者指出错误。
为了指出错误,只需设置线程的上一个错误代码,然后令自己的函数返回FALSE, INVALID_HANDLE_VALUE、NULL或者其他合适的值。为了设置线程的上一个错误代码,只需调用以下函数,并传递我们认为合适的任何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章详细讨论。就目前来说,惟一需要注意的重要字段是第29位。Microsoft承诺,在它所生成的所有错误代码中,此位将始终为0。但是,如果要创建自己的错误代码,就必须在此位放入一个1。通过这种方式,我们可以保证自己的错误代码绝不会与Microsoft定义的错误代码冲突,这既包括当前已经存在的错误代码,也包括将来会创建的错误代码。注意,Facility字段非常大,足以容纳4096个可能的值。其中,前256个值是为Microsoft保留的,其余的值可由我们自己的应用程序来定义。
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."));
上面这段代码的第一行从编辑控件获取错误代码。然后,定义了一个内存句柄并将它初始化为NULL。FormatMessage函数会在内部分配一块内存,并将这块内存的句柄返回给我们。
在调用FormatMessage时,我们给它传入了FORMAT_MESSAGE_FROM_SYSTEM标志。该标志告诉FormatMessage:与我们希望获得的错误描述字符串相对应的,是一个由系统定义的错误代码。另外,我们还传入了FORMAT_MESSAGE_ALLOCATE_BUFFER标志,要求该函数分配一块足以容纳错误文本描述的内存。这块内存的句柄将在hlocal变量中返回。FORMAT_MESSAGE_IGNORE_INSERTS标志则允许我们获得含有%占位符的消息,这些占位符被Windows用来提供更多与上下文相关的信息,如下例所示:

如果不传递这个标志,那么我们必须在Arguments参数中提供这些占位符的值。但这对于Error Show程序来说是不可能的,因为我们事先并不知道消息的内容。
第三个参数指出想要查找的错误编号。第四个参数指出要用什么语言来显示文本描述。由于我们对Windows本身提供的消息感兴趣,因此我们根据两个特定的常量(即LANG_NEUTRAL和SUBLANG_NEUTRAL)来生成语言标识符,把这两个常量结合在一起得到的值为0——意味着操作系统的默认语言。在许多情况下,因为事先并不知道操作系统的安装语言是什么,所以我们不能在代码中明确指定一种特定的语言,这里就是一个例子。
如果FormatMessage成功,我会把内存块中的文本描述复制到对话框底部的滚动窗口中。如果FormatMessage失败,我会尝试在NetMsg.dll模块中查找消息代码,看错误是否与网络有关(有关如何在磁盘上搜索DLL的详情,请参见第20章)。利用NetMsg.dll模块的句柄,我再一次调用FormatMessage。我们可以看到,每个DLL(或.exe)都可以有自己的一套错误代码。我们也可以向自己的模块中添加错误代码,只要使用Message Compiler(MC.exe)来创建一个消息资源并将它添加到DLL(或.exe)模块中就可以了。Visual Studio的Error Lookup工具允许我们使用Modules对话框来载入新的模块,并在其中查找消息代码。