Just Let It Flow

January 25, 2013

Plug in to CL’s Kitchen

Filed under: Code,Windows — adeyblue @ 1:09 am

It’s well known that Visual Studio’s C compiler hasn’t progressed much beyond C89, save for things like variadic macros. What might not be quite as well known is that to rectify this a bit, somebody created a C99 to C89 converter. A decent tool to be sure, but it doesn’t integrate well into Visual Studio. Being a seperate program means you have to fudge things to run it instead of cl.exe, or you have to set a pre-build step, save the processed output and then compile those files instead of the ones in your project. It’s not terribly friendly for IDE purposes. Wouldn’t it be nice if there was a way to intercept the compilation and process the source files as it goes? What isn’t well known is that you can do that, with compiler plugins.

The good news about these plugins is that unlike the IDE ones like Visual AssistX, the support is directly in the compiler so you don’t have to have a pay-for version to use them. You see, not only does cl.exe have the plethora of options it displays as help, it has undocumented ones too. Ones which allow you to change or add compiler passes and pass your own arguments to them.

With none of these options in effect, cl.exe coordinates two compiler passes before calling the linker. Using them, you can replace either or both passes, add an intermediary one, and even change the linker itself!

The replacement switches and what they replace are [1]:
/B1: C front-end (originally c1.dll)
/BX: C++ front end (originally c1xx.dll)
/B1_5: In between pass (this isn’t used)
/B2: backend (originally c2.dll)
/Bl: linker (originally link.exe)

The plugin can be a dll or a standalone exe. Dll’s need to export two functions or cl will fail the compilation:

BOOL __stdcall InvokeCompilerPass(int argc, char** argv, int unk) //exported as _InvokeCompilerPass@12

OR

BOOL __stdcall InvokeCompilerPassW(int argc, wchar_t** argv, int unk, HMODULE* phCLUIMod) // exported as _InvokeCompilerPassW@16

AND

void WINAPI AbortCompilerPass(int how) // exported as _AbortCompilerPass@4

Which of the first two to implement depends on the version of Visual Studio in use. Older versions up to and including VS2005 used the narrow stringed version, versions since use the wide stringed version with a pointer to the clui.dll hmodule passed in.

The Invoke function is called once per input file, argc and argv are self-explanatory though they deliver lots more arguments than will be given to cl.exe [2]. The actual code file path is given after the -f argument. The ‘unk’ parameter is a boolean flag value, what is represents though is, as you could guess, unknown. The return value indicates whether compilation should stop. Non-zero stops it, zero continues.

The Abort function is called when you select Cancel Build from inside the IDE or Ctrl-C from the command line. The how parameter indicates the reason for the abort, it’s usually 2 for Ctrl-C.

Exe plugins, despite the emulation of an argc & argv for dlls, don’t receive their argmuents on the command line. The arguments are passed, space separated, through the MSC_CMD_FLAGS environment value.

As mentioned, you can also pass your own arguments to these plugins. What you can’t do is just list them on cl.exe’s command line, it rejects those it doesn’t know. Like with the plugins though, there are more switches cl.exe does recognize whose sole purpose is to pass the flags. They follow a similar pattern, and start with ‘d’ rather than ‘B’:

/d1: This one works for both front ends (e.g. /d1Name=Bob)
/d1_5
/d2

With all that known, there’s enough info to convert c99to89 to a plugin. All we need to know is how to continue the normal build procedure after the conversion is done. It’s not difficult, since we know those are just hardcoded plugins with the names c1.dll for C compiling and C1xx.dll for C++. LoadLibrary and GetProcAddress for the same function names our plugin should export is enough [3].

It should technically be even easier, since cl.exe supports a second family of switches to specify compilation ‘filters’ (/Bp1 /Bpx /Bp2 /Bpl). These are supposed to be equivalent to the replacement switches mentioned above except that the second entry in argv should be the path to the built-in plugin. Alas, the support code for these in the compiler was removed or broken between VS 2005 and 2008 and now CL.exe always reports a shouty INTERNAL COMPILER ERROR when specifying those switches.

The code for the bits required to turn c99to89 into a plugin is here. I would have had the full dll too but figuring out which of the 100+ libs in the clang/llvm sdk are required and in which order was too much for my batch file to comprehend.

Now you know how to make them, you can do all sorts of things with compiler plugins like, um…, no it is quite useless isn’t it. Oh well, maybe next time.


Notes:

[1]: Other undocumented switches (as of VS 2012) which may be useful to somebody:

/BC: Allows C programs to target the clr, not just C++ ones
/Bd: Shows the command line for each pass of the build
/Be: Shows a pseudo batch file containing the current directory, paths and command lines
/Bk: Keep intermediate compiler files
/Bt: Show the build duration
/Bt+: More verbose timing output
/By: Show the allocated address ranges and loaded modules
/Bv: Show the versions of the compiler pass modules
/Bz: Same as /Bd but doesn’t build
/Mdebug: When passed with /MP, displays progress and other messages for each compiling thread
/d1nodatetime: Disable the __TIME__, __DATE__ and __TIMESTAMP__ standard macros

You can also set the LOG_BUILD_COMMANDLINES environment variable to a file name and cl will append its and link.exe’s command line to that file.

See Geoff Chappell’s website for many other undocumented switches.

[2]: Here are all the arguments that are passed for the simplest invocation (with name here being umlib.dll)

cl /Bx <name> test.cpp

cl /Bx<name> test.cpp

cl /B2 <name> test.cpp

cl /B2<name> test.cpp

[3]: From only the arguments passed to the plugin, it’s impossible to tell whether the compiler should be operating in C or C++ mode. If you ever try developing one targetting both languages, you have to make do with scanning CL.exe’s command line (GetCommandLine()) for the forced language switches (/TP, /TC) and scanning the extension of the file passed in if one isn’t found.

No Comments

No comments yet.

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.

Powered by WordPress