This article assumes that you are familiar with Windows SDK programming (in C or C++), C calling conventions including va_list for variable-length argument lists, use of resources for internationalisation, Windows’ safe string handling routines, and the Windows FormatMessage function in particular.
The Microsoft documentation for FormatMessage clearly
states that the floating-point format specifiers (e, E, f and g) you are familiar
with from printf() are not supported, and that you should use StringCchPrintf if
you want to take advantage of these specifiers.
StringCchPrintf is a buffer-overflow safe wsprintf() replacement, itself the
Windows provided implementation for the sprintf() from the C Run-Time Library
(RTL).
It is more than just trivia that the wsprintf function has
always been the only Windows -exported function that uses the C calling
convention, because it has to use it - the very similar wsprintf function does
not need to do so and therefore does not. The presence of wsprintf() in Windows
made implementing a C RTL for Windows that much easier.
With the introduction of the String… function, the wsprintf function has become
deprecated. The Microsoft documentation does not use that word, but Microsoft
did add a Security Alert to the wsprintf documentation, which recommends that
you use the String… functions instead.
Still , the advice to use StringCchPrintf advice is a bit surprising, as StringCchPrintf takes traditional printf-style format specifiers, not the FormatMessage-style specifiers. This mismatch between StringCchPrintf and FormatMessage does not make it easier to use them together.
One clearly documented limitation is that on Windows 95, Windows 98 and Windows ME, no single insertion string may exceed 1023 characters in length. Apparently, the implementation uses a null-terminated 1024-bytes buffer. Now, that is enough for most strings, but actually making sure that all your strings remain below that limit can be a PITA.
It is yet another reason to use a real operating system, one based on a NT kernel instead of MS-DOS. Not even Windows Mobile shares this limitation with Windows-on-DOS.
Still, note that the Windows Mobile documentation for FormatMessage (when I checked, last updated on 2007-04-09), still suggests using sprintf, despite that fact that sprintf is a unsafe function. The very practical reasons for this is that even Windows Mobile 6 does not support the String… functions yet.
Apparently, safety is less of an issue for Windows Mobile? Crackers take note: Microsoft is hardening the Windows desktop, but Windows Mobile is still wide open…
An oft-heard complaint about the FormatMessage documentation is that the few examples provided always pass NULL for va_list. As many Windows developer are not familiar on how to work with variable-argument lists, this continues to be a source of frustration.
I am not going to reiterate the documentation for va_list, va_start and va_end, but the working example below should help you make sense of the documentation.
Note the cast from LPCWSTR to LPWSTR in the code below. FormatMessage is one of the many functions in the Microsoft headers for which Microsoft did not bother to specify that a string parameter is constant. The not so practical upshot of that is the compiler complains that you did something wrong...
Old versions of the FormatMessage documentation were, cough, a little scarce on details about the format specifiers. You had to look through the message compiler documentation to find what little documentation there was for the FormatMessage specifiers.
Alas, even that documentation was not crystal clear on how to print a 64-bit integer using FormatMessage. The current documentation for FormatMessage merely mentions the existence of the I64 flag, and does not provide an example.
There are warnings about limitations using I64:
Inserts that use the I64 prefix are treated as two 32-bit arguments. They must
be used before subsequent arguments are used.
That are two limitations! First of all, your 64-bit integer is not really treated as 64-bit, but as two separate 32-bit integers. That will not do.
The second limitation is just as ridiculous.
It basically says that you may do
FormatMessage( "%1!I64u!,
%2!lu!", LONGLONG, WORD ),
but not
FormatMessage( "%1!lu!, %2!I64u!",WORD,
LONGLONG ).
That’s silly.
The documentation does not stress this, but note that both ll (double ell) and I64 are modifiers for a base type specifier, and are not meant to be used on their own. Results are somewhat unpredictable if you do.
The code below demonstrates the FormatMessage behaviour for 64-bit values with actual code. Contrary to what the documentation would lead you to believe, the output looks just fine when I use I64. It does not even matter whether I put I64 first or not, FormatMessage is not confused by it at all.
Perhaps the documentation still warns against something that has been fixed now? But surely if it had been fixed, the MSDN documentation would not that it was safe to from say Service Pack 2 onwards. It does not say that, yet it works fine. Hm…
You could use the modifier ll (for the 64-bit type LONGONG) instead of I64, and when you do so, things do go wrong, along the lines suggested by the FormatMessage documentation. When you use ll, the 64-bit value is indeed interpreted as two 32-bit values. Worse, FormatMessage does not print two 32-bit values, but only processes the first one, and the second one is treated as the argument for the next specifier…
Looking at the actual behaviour, it seems that it would be wise to put any ll modifier last, not first, to minimise its impact.
As the documentation mentions the I64 modifier but does not
mention ll at all, it seems to me that the documentation is wrong, confused
these two cases, and is confused about the actual effect of the defect as
well.
It seems that someone communicated the limitation, someone else wrote it down,
and no one ever bothered to check it.
A rather fundamental and somewhat surprising limitation is that FormatMessage does not format integers. Sure, it will print them decimally or hexadecimally, with or without leading zeroes or spaces, but it will not insert thousand separators.
This is somewhat surprising for two reasons. Long numbers
without thousand separators are hard to read. We can manage without the
separators for a 16-bit integer (at most 65.535), but not for a 32-bit (at most
4.294.967.295), and certainly not for a 64-bit integer (at most
18.446.744.073.709.551.615).
The preceding sentence would be considerably easier to read if the numbers
contained thousand separators. The messages formatted by FormatMessage are
generally meant for human consumption, so supporting thousand separators would
make sense.
The second reason may have you perform a double-take. forget the programming details of FormatMessage function for a moment and considers its purpose instead. FormatMessage exists in support of application internationalisation.
You can make multiple messages strings for the same message in multiple languages, and FormatMessage will load the right one. It actually already takes a locale parameter, which it all it needs to figure out the proper thousands separator and format the integer readably, but if you want to have it done, you must do it yourself. How sensible is that?
It does not matter much that FormatMessage support 64-bit integers if the result is a barely readable 123456789012345678 instead of a user-friendly 123.456.789.012.345.678.
It is time for Microsoft to clean up the FormatMessage mess. Provide sufficient examples, correct and double-check the documentation. Microsoft should provide FormatMessageEx, a routine that does not just support picking the right string, but actually support truly international formatting.
Of course, Microsoft will have to provide a GetNumberFormat() that fully supports 64-bit integers first.
void
FmtMsg
(
LPWSTR lpwszBuff
,DWORD dwNumElem
,LPCWSTR lpcwszFmt
,...
)
{
va_list val ;
va_start( val, lpcwszFmt ) ;
const DWORD dwMsgId = 0L ; // ignored for FORMAT_MESSAGE_FROM_STRING
const DWORD dwLangId = 0L ; // ignored for FORMAT_MESSAGE_FROM_STRING
FormatMessage( FORMAT_MESSAGE_FROM_STRING, lpcwszFmt, dwMsgId, dwLangId, lpwszBuff, dwNumElem, & val ) ;
va_end( val ) ;
}
void
DemoFmtMsg
(
void
)
{
WCHAR wszBuffer[ 1024 ] ;
LONGLONG llBigValue = 123456789012345678 ; // 0x01B6 9B4B A630 F34E
// 0x01B6 9B4B == 28744523
// 0xA630 F34E == 2788225870
DWORD dwValue = 123456789 ; // 0x075B CD15
HANDLE hCon = GetStdHandle( STD_OUTPUT_HANDLE ) ;
DWORD dwWritten = 0L ;
FmtMsg( wszBuffer, NELEM( wszBuffer ), L"str: %1!s!%n", L"hi" ) ;
WriteConsole( hCon, wszBuffer, lstrlen( wszBuffer ), & dwWritten, NULL ) ;
FmtMsg( wszBuffer, NELEM( wszBuffer ), L"dw: %1!lu!%n", dwValue ) ;
WriteConsole( hCon, wszBuffer, lstrlen( wszBuffer ), & dwWritten, NULL ) ;
FmtMsg( wszBuffer, NELEM( wszBuffer ), L"ll: %1!llu!%n", llBigValue ) ;
WriteConsole( hCon, wszBuffer, lstrlen( wszBuffer ), & dwWritten, NULL ) ;
FmtMsg( wszBuffer, NELEM( wszBuffer ), L"ll: %1!llu! dw: %2!lu!%n", llBigValue, dwValue ) ;
WriteConsole( hCon, wszBuffer, lstrlen( wszBuffer ), & dwWritten, NULL ) ;
FmtMsg( wszBuffer, NELEM( wszBuffer ), L"dw: %1!lu! ll: %2!llu!%n", dwValue, llBigValue ) ;
WriteConsole( hCon, wszBuffer, lstrlen( wszBuffer ), & dwWritten, NULL ) ;
FmtMsg( wszBuffer, NELEM( wszBuffer ), L"I64: %1!I64u!%n", llBigValue ) ;
WriteConsole( hCon, wszBuffer, lstrlen( wszBuffer ), & dwWritten, NULL ) ;
FmtMsg( wszBuffer, NELEM( wszBuffer ), L"I64: %1!I64u! dw: %2!lu!%n", llBigValue, dwValue ) ;
WriteConsole( hCon, wszBuffer, lstrlen( wszBuffer ), & dwWritten, NULL ) ;
FmtMsg( wszBuffer, NELEM( wszBuffer ), L"dw: %1!lu! I64: %2!I64u!%n", dwValue, llBigValue ) ;
WriteConsole( hCon, wszBuffer, lstrlen( wszBuffer ), & dwWritten, NULL ) ;
}
Copyright © Tamura Jones. All Rights reserved.