Over the last few days I’ve been slowly attacking the source code for 386MAX, trying to build the entire product. One of the many problems I ran into turned out to be quite interesting.
There are several (16-bit) Windows components in 386MAX, and many have some sort of GUI. As is common, dialogs etc. are built from templates stored in resource (.rc) files. But… trying to process many of the resource files with the standard Windows 3.1 resource compiler fails:
The problem is dialog text labels composed of multiple strings. Something like this:
LTEXT "Qualitas 386MAX\nVersion " "8" "." "03",-1, 12,7,112,17
For obvious reasons, the authors wanted to automatically update the strings with the current version numbers, and used macros to build those strings. Only that doesn’t quite work in the resource compiler.
But Why Not?
In C and C++, this is not a problem. String literals are merged together, thus the following are equivalent:
"Hello" " " "World" "Hel" "lo World" "Hello World"
But that does not happen in the resource compiler. We may wonder why not, but the fact is that it doesn’t.
The catch is that although the resource compiler (RC.EXE) uses a preprocessor (RCPP.EXE) which is essentially identical to a C compiler preprocessor (in fact almost certainly built from the same source code), a C preprocessor does not perform string literal merging. Again we may wonder why not, but the fact is that it doesn’t.
The upshot is that if the resource compiler expects a string, it must be supplied with a single string literal, because multiple consecutive string literals will not be merged.
A Quality Hack
For building 386MAX, Qualitas solved the problem in a manner that is as clever as it is dirty. Qualitas wrote a tool called RCPPP, called a “RCPP postprocessor”. The way it was used was as follows: The original resource compiler preprocessor (RCPP.EXE) had to be renamed to _RCPP.EXE, and the Qualitas RCPPP.EXE need to be copied to RCPP.EXE.
When the resource compiler (RC.EXE) was run, the Qualitas wrapper would pass its arguments the original preprocessor; after the preprocessor was finished, the wrapper would re-open the temporary file holding the preprocessor output, rewrite it to merge string literals, and then return control to the resource compiler. Voila, problem solved.
This was a very clever but nasty solution, because it required modifying the vendor tools (a big no-no). It was likely done that way because the Microsoft resource compiler does not offer any way to only run the preprocessor.
A Less Hacky Approach
There would have been a less hacky approach available, at the cost of makefile complication. The RCPP.EXE preprocessor can of course be executed as a standalone tool, and the preprocessed output could then be further rewritten to merge string literals.
Or, one might take advantage of the fact that the resource compiler preprocessor is a C preprocessor, and just use the C compiler to do the preprocessing.
Either approach requires multiple steps (preprocess, postprocess, run resource compiler) but does not need modifying the tools. Using the C compiler to preprocess additionally does not need relying on the internals of the resource compiler.
What Is Even RCPP.EXE?
Raymond Chen says: The Resource Compiler’s preprocessor is not the same as the C preprocessor, even though they superficially resemble each other.
I would rate that claim as misleading. In reality, the resource compiler’s preprocessor is very much a C preprocessor, with minor differences. I should add that the following applies to the Windows 3.1 Resource Compiler, which may not be quite like the newer NT based resource compilers.
A quick look at RCPP.EXE reveals that not only it very much is like a C preprocessor, it is more or less identical to the first phase of a Microsoft C compiler. Which, based on the strings included in it, does a lot more than just preprocessing.
Here is a screenshot of a handful of error messages from the Microsoft Windows 3.1 Resource Compiler preprocessor (RCPP.EXE):
For comparison, here’s a screenshot of error messages from the C1.ERR file corresponding to the first phase (C1.EXE) of the Microsoft C 5.1 compiler:
The similarity is not coincidental, and it is far more than superficial (even though the strings are not identical). Also note that most of the error messages apply to a C language compiler, not just a preprocessor.
I would guess that the Windows 3.1 RCPP.EXE is built from the source code for the first pass of the Microsoft C compiler, circa version 5.1 (other versions have noticeably different error messages, while version 5.1 is a close match). The similarities go far enough that, for example, the command line of the preprocessor child process (C1.EXE/RCPP.EXE) is in both cases passed in an environment variable called
MSC_CMD_FLAGS (bypassing the DOS 128 character command line length limit).
It should therefore not be surprising that RCPP.EXE and the C preprocessor behave almost identically. Consider the following:
rcpp -DRC_INVOKED -Ic:\msvc\include -E -g foo.i -f my.rc cl -DRC_INVOKED -Ic:\msvc\include -E my.rc > foo.i
Both produce nearly identical output, the only difference being slashes and backslashes in
As an aside, the RCPP.EXE shipped with the Windows 1.x and Windows 2.x SDKs seems to be a very close relative of the first phase of the Microsoft C 3.0 compiler (P1.EXE); RCPP.EXE is identical between the Windows 1.x and 2.x SDK versions. For Windows 3.0, the preprocessor was upgraded with the one from Microsoft C 5.1 (or something quite close), and stayed unchanged for Windows 3.1.
A Different Solution
Or… instead of massaging the preprocessor output, perhaps there is a way to avoid the problem entirely?
The stringize operator (
#) of the C preprocessor can be used to turn preprocessing tokens into a single character string literal. This approach requires separate machinery because the preprocessing tokens must not be string literals—otherwise extraneous double quotes end up in the output.
Using the C (or RC) preprocessor in this manner is not exactly intuitive, and understanding how and why it works requires a fairly deep understanding of the mechanics of the preprocessor. For all intents and purposes, the C preprocessor is a completely different language from C.
Suppose we want to produce a string like “386MAX Version 8.03”. In the original resource file, it was achieved as follows:
#define VER_MAJOR_STR "8" #define VER_MINOR_STR "03" #define VERSION VER_MAJOR_STR "." VER_MINOR_STR ... LTEXT "386MAX Version " VERSION, ...
It’s simple enough, except (as explained above), it doesn’t work because the resource compiler does not concatenate string literals.
The resource compiler compatible version is rather more involved, and the first attempt might look something like this:
#define VER_MAJOR 8 #define VER_MINOR 03 #define VER_MKSTR(s) #s #define VER_STR(s) VER_MKSTR(s) #define VER_PV_RC(m, n) VER_PRODVER_RC m.n #define VER_PVSTR_RC VER_STR(VER_PV_RC(VER_MAJOR, VER_MINOR)) ... #define VER_PRODVER_RC 386MAX Version ... LTEXT VER_PVSTR_RC, ...
Note that instead of VER_MAJOR_STR, we must use VER_MAJOR. Also note that if we want the version to be displayed as 8.03 rather than 8.3, VER_MINOR must be defined as 03 rather than 3.
Therein lies the first pitfall. If we wanted to set the version to 8.08, we might define VER_MINOR as 08. That would work nicely for the resource compiler, but not in C language arithmetic… because 08 is not a valid octal constant, and neither is 09. (If that does not make sense, you just do not know the C language well enough.) It is simple enough to define separate macros (say VER_MINOR_RC) for the preprocessor, should the need arise.
There are other pitfalls. Suppose we want to use the company name in the string:
#define VER_PRODVER_RC Qualitas, Inc. 386MAX Version
Now, it just so happens that the above works as users likely expect in the Windows 3.1 Resource Compiler preprocessor, but only because the Microsoft preprocessor is strange.
In more or less any other C preprocessor, the above doesn’t work. The comma causes too many arguments to be passed to the VER_STR macro when processing the VER_PVSTR_RC macro. Some compilers (e.g. Watcom, IBM) warn and throw away the comma and everything past it. Other compilers (Borland) error out and do not accept the input. Other compilers (gcc) behave yet differently, not expanding a function-like macro with too many arguments at all.
The C90 standard (relevant for the Windows 3.1 Resource Compiler) is clear:
The number of arguments in an invocation of a function-like macro shall agree with the number of parameters in the macro definition, and there shall exist aC90 Standard, section 6.8.3
)preprocessing token that terminates the invocation.
It is an error to use a comma in this context. Fortunately there is an easy workaround:
#define VER_PRODVER_RC Qualitas\x2C Inc. 386MAX Version
Instead of a comma character, we can use a hexadecimal escape sequence with the ASCII code for a comma (2Ch) to achieve the desired result.
This workaround takes advantage of the fact that to the preprocessor, \x2C is just a random sequence of four characters (backslash, x, 2, C). Only in a later phase of translation does the escape code get converted into a single character, together with classics like \n or \0.
At any rate, it is possible to use the preprocessor to produce a single string literal acceptable to the resource compiler. It is not exactly straightforward, primarily because the preprocessor is a rather different beast from the C language proper, but it is doable.
The bottom line is that it is no longer necessary to massage the preprocessor output, and it is certainly not necessary to hack the resource compiler itself to insert an extra processing stage. The unmodified resource compiler now produces the desired output, at the cost of a bit of extra baggage largely hidden away inside one header file.