Using FormatMessage |
|
The FormatMessage API call is very powerful, and particularly useful for issuing all kinds of messages. This is not intended to be a full tutorial on using FormatMessage, but simply an example of one of the most effective uses of the the call. There is nothing more offensive than being presented with a message like the one below:
This is incomprehensible. The file cannot be opened. Why? For that matter, what file? The name the user types in is only part of the filename context; the rest relies on the current directory, which may not be what the user thinks it is. Errors like this generate unnecessary support calls to tech support departments, and consume a lot of time of the technical support people trying to determine what has gone wrong. A much better example of an error message is the next one. It tells what the application-level error is, what file was being considered, and the description of the error code.
In this case, I had created a read-only file junk.txt to induce the error on a Save command.
For internal messages where the error is unexpected, I have no qualms about including the __FILE__ and __LINE__ parameters, or even __FUNCTION__. For example
void CSampleApplication::Lookup(const CString & name) { LPMYINFORMATION Somevalue; ...do some computation... if(Somevalue == NULL) { CString fmt; fmt.LoadString(IDS_INTERNAL_ERROR_23); CString msg; msg.Format(fmt, _T(__FILE__), __LINE__, _T(__FUNCTION__), name); AfxMessageBox(msg, MB_ERROR | MB_OK); return; } ... }
where the string is
IDS_INTERNAL_ERROR_23 |
some value |
Internal error 23\n%s(%d): %s\nLookup of name "%s" gave NULL pointer value |
Note that __FUNCTION__ is defined in VS.NET 2003 and later compilers.
To make this work, I use the FormatMessage call. I then convert the value to a CString. If no error message is found, I handle the error by formatting the message with a particular string, which is stored in the STRINGTABLE
IDS_UNKNOWN_ERROR |
some value |
Unknown error 0x%08x (%d) |
You will have to add this STRINGTABLE entry to your application. The string number doesn't matter. At the call site, you can do something like
CStdioFile f; if(!f.Open(dlg.GetPathName(), CFile::modeWrite | CFile::modeCreate)) { /* failed */ DWORD err = ::GetLastError(); CString errmsg = ErrorString(err); CString fmt; fmt.LoadString(IDS_OPEN_FAILED); CString s; s.Format(fmt, dlg.GetPathName(), errmsg); AfxMessageBox(s); return; } /* failed */
The message formatting string is stored as a resource in the STRINGTABLE. This allows for internationalization.
IDS_OPEN_FAILED |
some value |
File Open failed\n%s\n%s |
One of the peculiar features of FormatMessage, which makes absolutely no sense at all, is that it appends a newline sequence to the message. This is rarely, if ever, of value, and as far as I can tell, having worked in three other major operating systems that supported a FormatMessage equivalent, this seems to arise from some fundamental misunderstanding of the problem. In every single one of these systems, dating back to 1968, I have had to eliminate the newline sequence from the error string before it was usable! So I delete this newline sequence from the result of FormatMessage so the message can be placed in a MessageBox, CEdit, CListBox, or other such place and make sense. (The peculiar attitude seems to be that the designer of the error message should control this, when in fact it should either not be appended at all, or be an option flag that defaults to "off" in the call to retrieve the message).
Just for your information, if you want to get the error text while debugging, place in the watch window of the debugger the request
@ERR,hr
This is a screen snapshot of the watch window taken after the CStdioFile::Open call failed. As you can see, the error message is nicely explained to you, the programmer. When the error is something that needs to be displayed to the user, you can use my ErrorString function to get a nice CString. Note that even if you think the message code is of no use to the user, display it anyway. For example, a message of the form is actually very useful. Here is a simulated display of an AfxMessageBox. Your tech support people will be greatly aided.
Your Application Name Here |
Unable to
complete operation. Internal error. Unexpected error code from network support. ( An attempt was made to establish a session to a network server, but there are already too many sessions established to that server). |
Perhaps you did not expect this error and don't really know what to do when it occurs, but your tech support people could waste hours trying to track down a problem that is not really in your program, but an artifact of an overloaded server.
Here is the code of the function in its entirety: The download consists of a compilable .cpp file and the accompanying .h file.
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 = _tcschr(s, _T('\r')); if(p != NULL) { /* lose CRLF */ *p = _T('\0'); } /* lose CRLF */ Error = s; ::LocalFree(s); } /* success */ return Error; } // ErrorString
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. The sample applications and this code can be downloaded here.
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 eol = _tcsrchr(msg, _T('\r')); // [22] if(eol != NULL) *eol = _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
See also my essay on how to convert CInternetException errors to error strings. This is a simpler implementation than the code for WinSock, because the WinINet module is already loaded. However the WSOCK32.DLL is not the WinSock code, which is in WS2_32.DLL.