Modern Software Experience

2007-05-15

assumptions

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.

documented limitations

no floating point support

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).

buffer-overflow safe

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.

specifier style mismatch

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.

Windows 9X buffer limitation

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.

Windows Mobile not safe

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…

va_list

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.

constant cast

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...

FormatMessage I64

message compiler documentation

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.

no example

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.

warnings

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.

two I64 limitation

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.

modifiers

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.

I64 works fine

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…

ll is messed up

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…

observation

Looking at the actual behaviour, it seems that it would be wise to put any ll modifier last, not first, to minimise its impact.

documentation wrong?

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.

no thousand separators

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.

hard to read

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.

internationalisation

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.

wanted: FormatMessageEx

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.

example code


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 ) ;
}