I Once Appeared in the Old New Thing
12 comments
·September 17, 2025kevin_thibedeau
Arnavion
The ShowError() API they're talking about doesn't take a format string. It takes a resource ID which is an integer that's essentially an index in the resource table. So running the message compiler produces a C header that defines ERROR_BITLOCKER_PASSPHRASE_MINIMUM_TOO_LONG to an integer constant, and then ShowError(ERROR_BITLOCKER_PASSPHRASE_MINIMUM_TOO_LONG) looks up the string at that index in the resource table and displays it.
https://learn.microsoft.com/en-us/windows/win32/eventlog/mes...
(You'll note that that page mentions that FormatMessage() does support printf-style format specifiers in the string resource. That's why I'm saying that their ShowError() specifically is the one that doesn't.)
arcfour
The Microsoft way is to do things backwards, make the possible impossible, and wrap simple, well-understood APIs behind 16 layers of abstraction with awful names that will ever so slightly munge your arguments. Surely you know this :-)
m0llusk
In a way this is more like Perl's maketext or maybe Mozilla Fluent. Reasonable translations need to be constructed using variable inputs and correct messages can vary greatly between languages with grammar agreement and such. While gettext does have some extra support for getting numeric translations and agreements right it is kind of hacked in as an add on to a text lookup procedure rather than in terms of constructing the messages based on inputs.
wild_pointer
Very knowledgeable, helpful, and snarky - that's the vibe I'm getting from Raymond Chen too after reading many of his blog posts.
mtlynch
Author here. Happy to take any feedback or questions about this post.
chihuahua
It's kinda mind-boggling to me that after making a change to the build definition, there was no way to try it out and see if it works, other than committing the change and waiting until the next day to see if it broke the build.
My question is: how was anyone expected to make changes to the build definition if this was the only way? Wait a day to find out if it worked, and break the build for everyone if not?
quuxplusone
At one point you say "precompiler" when I guess you mean "preprocessor"?
Also I think the C preprocessor would be relatively unhelpful with the file format you explained in the post: As soon as you reached the first unmatched, unquoted apostrophe, cpp would assume it was inside a really long character literal and refuse to substitute any macros until the next apostrophe.
cpp is great, but it does basically require a grammar that assigns broadly the expected meaning to ' " # // /* */. Curly-brace languages fine, running English text not so much.
quuxplusone
Without running a preprocessor on the .mc file, and without adding "%d" support to `ShowError`, here's one other category of solution. These days, you could just write
if (minimumPassphraseLength > MAX_PASSPHRASE_MINIMUM) {
static_assert(MAX_PASSPHRASE_MINIMUM == 20);
ShowError(ERROR_BITLOCKER_PASSPHRASE_MINIMUM_LONGER_THAN_20);
}
so that if the value of MAX_PASSPHRASE_MINIMUM ever changed, you'd get a build failure right on this line and you'd be forced to fix it (part of which should involve updating the message to match).You could make that fancier by trying to craft the name of the error message itself via the preprocessor — something like:
SymbolicName=ERROR_BITLOCKER_PASSPHRASE_MINIMUM_LONGER_THAN_20
The BitLocker minimum passphrase length cannot exceed 20.
...
#define CONCAT(x, y) x##y
#define ERROR_BITLOCKER_PASSPHRASE_MINIMUM_LONGER_THAN(x) \
CONCAT(ERROR_BITLOCKER_PASSPHRASE_MINIMUM_LONGER_THAN_, x)
#define MAX_PASSPHRASE_MINIMUM 20
if (minimumPassphraseLength > MAX_PASSPHRASE_MINIMUM) {
ShowError(ERROR_BITLOCKER_PASSPHRASE_MINIMUM_LONGER_THAN(MAX_PASSPHRASE_MINIMUM));
}
But that would just make the compiler error (when MAX_PASSPHRASE_MINIMUM changed) a lot harder to read, without changing the essential task (go find the error message and update it), so it's not a good idea.mtlynch
>At one point you say "precompiler" when I guess you mean "preprocessor"?
Ah, you're right! Fixed, thanks!
>Also I think the C preprocessor would be relatively unhelpful with the file format you explained in the post: As soon as you reached the first unmatched, unquoted apostrophe, cpp would assume it was inside a really long character literal and refuse to substitute any macros until the next apostrophe.
Oh, that's a good point. I'm not sure how Visual C++ does with it, but I just tried with gcc, and it falls over on an apostrophe:
$ cat values.h
#define MAX_PASSPHRASE_MINIMUM 20
$ cat example.mcp
SymbolicName=ERROR_BITLOCKER_PASSPHRASE_MINIMUM_TOO_LONG
The BitLocker minimum passphrase length can't exceed MAX_PASSPHRASE_MINIMUM.
$ gcc --version | head -n 1
gcc (GCC) 14.3.0
$ gcc --preprocess --language=c --no-line-commands --include=values.h example.mcp
SymbolicName=ERROR_BITLOCKER_PASSPHRASE_MINIMUM_TOO_LONG
example.mcp:2:44: warning: missing terminating ' character
2 | The BitLocker minimum passphrase length can't exceed MAX_PASSPHRASE_MINIMUM.
| ^
The BitLocker minimum passphrase length can't exceed MAX_PASSPHRASE_MINIMUM.
card_zero
Somehow, for me, your code samples are appearing as black text on a black background.
charcircuit
How was this supposebit locker? Every time an admin changed the group policy it would trigger a recompile of bitlocker?
Everybody in gettext land uses (s)printf format specifiers. Nobody thought to do that at MS?