Using ::FormatMessage to report errors in MFC |
|
This code actually appears in nearly all my projects. However, a number of readers have missed this, so I'm putting this code here (there's no download, you can copy-and-paste from this Web page).
This shows a way of interfacing to ::FormatMessage from MFC and use it in an MFC-compatible fashion.
One of the common problems in handling errors is delivering complete, informative information to the end user. All too often, the problem is reported by one of these truly informative message boxes:
It is almost as useful as the one that says
These are truly egregious examples which are all too common in far too many programs. The problem with this is that the user has no idea what to do in response to this error. Therefore, the user has to call tech support. What does this poor victim tell tech support? The contents of the dialog? You had better hope your tech support people don't know where your cubicle is, or you might meet with an unfortunate "accident" some day.
There is absolutely no excuse for such pitiful examples of error messages. None whatsoever.
Now this is an informative message:
It says there is a file open error (if I hard more than one kind of file, even that line would be more precise), it says what file had the problem, and it says why. The user looks at this, realizes that the file is open in some other program, and closes that program. Or checks that the protections are properly set on the file. But does not feel compelled to call tech support.
How do you get that nice result? The answer is a remarkably few number of lines of code, which is why there is never an excuse for not producing a decent error message.
For example, to produce this message, I did
BOOL CMyClass::ReadFile(const CString & filename) { CFile f; if(!f.Open(filename, CFile::modeRead)) { /* Failed */ DWORD err = ::GetLastError(); // [1] CString fmt; // [2] fmt.LoadString(IDS_FILE_OPEN_ERROR); // [3] // File open error\n // %s\n // %s CString msg; // [4] msg.Format(fmt, filename, ErrorString(err)); // [5] AfxMessageBox(msg, MB_ICONERROR | MB_OK); return FALSE; } /* Failed */ ... read file here f.Close(); return TRUE; } // CMyClass::ReadFile
This takes only five lines of code to do the job right. Therefore, there is never a justification for the completely incompetent example given as the first example.
Note one important point here. The very first line that is executed after the API fails is ::GetLastError. Do not put an ASSERT before it; do not do anything else; capture that error immediately.
To accomplish this, the ErrorString function must be written. However, given how trivial this function is to write, and that it only needs to be written once for your entire lifetime, there is little reason not to use it everywhere. To allow for localization, the string "Unknown error code %08x (%d)" is stored in the STRINGTABLE as the string IDS_UNKNOWN_ERROR.
I have never understood this phenomenon, but in the first operating system that had an integrated error handling system, IBM's TSS/360, in 1968, would add a CRLF at the end of every error message string. This makes no sense. If I want a CRLF, I can add it. Nonetheless, Microsoft has continued this insanity nearly 20 years later. Each message ends with a CRLF. Why? There is no sensible explanation. So I have to remove the terminal CRLF.
In addition, if you plan to use a message in some context that cannot understand newline sequences, such as a ListBox, you may wish to additionally do a CString::Replace(_T("\r\n"), _T(" ")) to get rid of gratuitous internal CRLF sequences.
ErrorString.h:
CString ErrorString(DWORD err);
ErrorString.cpp:
#include "stdafx.h" #include "resource.h" #include "ErrorString.h" /**************************************************************************** * ErrorString * Inputs: * DWORD err: Error code * Result: CString * String message ****************************************************************************/ CString ErrorString(DWORD err) { CString Error; LPTSTR s; if(::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, err, 0, (LPTSTR)&s, 0, NULL) == 0) { /* failed */ // Unknown error code %08x (%d) CString fmt; CString t; fmt.LoadString(IDS_UNKNOWN_ERROR); t.Format(fmt, err, LOWORD(err)); Error = t; } /* failed */ else { /* success */ LPTSTR p = _tcsrchr(s, _T('\r')); if(p != NULL) { /* lose CRLF */ *p = _T('\0'); } /* lose CRLF */ Error = s; ::LocalFree(s); } /* success */ return Error; } // ErrorString
There is no place, in any program I write, in which there are two places that issue the same MessageBox/AfxMessageBox text. Just by looking at the text of the message, I can immediately tell what line of my program issued it. This is important. This makes it possible to determine the exact line of my program that issued the message.
Sometimes, if I have a generic error handler, I will pass in an extra CString parameter which appears in the MessageBox/AfxMessageBox so that the caller of the handler routine can be identified. There is no alternative to making sure that every user-visible error condition has a 1:1 correspondence with the site in the program that issues it.
If you are doing network programs, you probably want to report network errors in a meaningful way. This is the version of ErrorString I used in a network-based application.
This is a global function based on the above code. However, this function additionally can produce text messages for network errors.
CString ErrorString(DWORD err) // [1] { DWORD flags = 0; // [2] CString path; HMODULE lib = NULL; // [3] LPTSTR p = path.GetBuffer(MAX_PATH); // [4] HRESULT hr = ::SHGetFolderPath(NULL, CSIDL_SYSTEM, NULL, SHGFP_TYPE_CURRENT, p); // [5] if(SUCCEEDED(hr)) // [6] { /* succeeded */ path.ReleaseBuffer(); // [7] if(path.Right(1) != _T("\\")) path += _T("\\"); // [8] path += _T("WSOCK32.DLL"); // [9] lib = ::LoadLibrary(path); // [10] if(lib != NULL) // [11] flags |= FORMAT_MESSAGE_FROM_HMODULE; // [12] } /* succeeded */ else { /* failed */ path.ReleaseBuffer(); // [13] } /* failed */ LPTSTR msg; // [14] if(::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | // [15] FORMAT_MESSAGE_FROM_SYSTEM | // [16] flags, // [17] (LPCVOID)lib, // [18] err, // [19] 0, // language ID (LPTSTR)&msg, // [20] 0, // size ignored NULL) // arglist == 0) { /* not found */ return DefaultError(err); // [21] } /* not found */ LPTSTR p = _tcsrchr(msg, _T('\r')); // [22] if(p != NULL) *p = _T('\0'); // [23] CString s = msg; // [24] if(lib != NULL) ::FreeLibrary(lib); // [25] LocalFree(msg); // [26] return s; } // ErrorString
[1] | This function takes a DWORD error code and returns a CString. Radical OO types do not approve of global functions but want methods of singleton classes, but I think that is largely overkill. |
[2] | The FormatMessage call takes a set of flags, one of which will be optional. This variable will hold that optional flag, and if the flag is not valid, the variable is initialized to 0. |
[3] | We are going to load a DLL which has the WinSock MESSAGETABLE and this HANDLE will hold the handle to the library. It is initialized to NULL. |
[4] | We are going to call a system API which requires a character buffer of size MAX_PATH. This creates the temporary buffer in a CString. |
[5] | The ::SHGetFolderPath API replaces the now-obsolete GetSystemDirectory API. On a standard installation, it is likely that this will return the string "c:\Windows\System32". |
[6] | The ::SHGetFolderPath API returns a type HRESULT, and the way this is tested for success is with the SUCCEEDED macro. |
[7] | If the call succeeds, we must call ReleaseBuffer before using the CString. |
[8] | We want to make sure the path ends with "\", so if it doesn't, one is added. |
[9] | The DLL that holds the MESSAGETABLE is
called "WSOCK32.DLL". Now what seems
odd about this is that for a 64-bit app, you might expect that you should be
trying to load "WSOCK64.DLL", but it doesn't
work that way. The names, no matter how tasteless they were when they
are chosen, seem to remain the same. I verified this by using the
dumpbin utility on WSOCK32.DLL, and got the
following:Microsoft (R) COFF/PE Dumper Version 8.00.50727.42 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file c:\windows\system32\wsock32.dll PE signature found File Type: DLL FILE HEADER VALUES 8664 machine (x64) 5 number of sections 42438B57 time date stamp Thu Mar 24 23:53:59 2005 0 file pointer to symbol table 0 number of symbols F0 size of optional header 2022 characteristics Executable Application can handle large (>2GB) addresses DLL |
[10] | The ::LoadLibrary API will load the DLL. The handle for this DLL will be used in the FormatMessage call. |
[11] | The handle will be NULL if there was any failure. |
[12] | If the ::LoadLibrary succeeds, we can use the module handle, so the FORMAT_MESSAGE_FROM_HMODULE flag will be added to the set of flags used by FormatMessage. |
[13] | If the ::SHGetFolderPath fails, it is still necessary to do a ReleaseBuffer because otherwise the CString is in an undefined state and will probably cause some serious malfunction when its destructor is called. |
[14] | This will be a pointer to the buffer which will be allocated by the FormatMessage call. |
[15] | The FormatMessage call will retrieve the error string in the local language. The FORMAT_MESSAGE_ALLOCATE_BUFFER tells the API that it should allocate a buffer to hold the message. |
[16] | The FORMAT_MESSAGE_FROM_SYSTEM flag indicates that the error code should be interpreted according to the system error table. |
[17] | If there was any failure trying to load the DLL, the flags value is 0 so only the system message table will be searched. However, if the flags has been set to FORMAT_MESSAGE_FROM_HMODULE, then the specified module will be searched first; if the error code is not found in that module, then the system error table is searched. |
[18] | The meaning of this parameter varies with the flags. The parameter is officially specified as an LPCVOID, and its interpretation when the FORMAT_MESSAGE_FROM_HMODULE flag is specified, this is interpreted as being the handle to a DLL that contains a MESSAGETABLE. But to sneak this by the compiler, an explicit (LPCVOID) cast must be written. |
[19] | This is the 32-bit error code that will be checked against the error table(s). |
[20] | This parameter is officially specified as an LPTSTR pointer to a buffer into which the characters of the message will be written. But if the FORMAT_MESSAGE_ALLOCATE_BUFFER flag is specified, it is actually a pointer to a pointer to a character buffer. The API call will allocate the buffer, and store the pointer to this buffer in the location pointed to by this parameter. So it must, when that flag is specified, by a pointer-to-an-LPTSTR, and to sneak this past the compiler, the explicit (LPTSTR) cast is required. |
[21] | The ::FormatMessage API call returns the count of the number of characters it returned, and if this is 0, the API call failed. This means that it was unable to find the error code in any of the sources specified. In this case, we revert to formatting a simple string. |
[22] | Due to what is poor design, every message gets a \r\n (CRLF) appended to the end of the message. This is at best useless and at worst harmful. This code checks to see if there is a terminal CRLF... |
[23] | ...and if so, overwrites the \r with a NUL character so the CRLF is truncated. |
[24] | The function is specified as returning a CString, so this variable represents that CString value that will be returned. The assignment causes a copy of the returned buffer to be made in the CString. |
[25] | If the DLL had been loaded, it must be freed. |
[26] | The buffer that was allocated by the FormatMessage API must be freed. The API that does this is LocalFree. |
static CString DefaultError(DWORD err) { CString fmt; fmt.LoadString(IDS_DEFAULT_ERROR); CString s; s.Format(fmt, err, err); return s; } // DefaultError
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.