2016-03-26

An IS_DEFINED() C macro to check whether another macro is defined

Every C programmer knows to use #defines and #ifdefs to disable compilation for a piece of code. But not everyone knows that there are better ways; the problem with this strategy is that the code becomes invisible to the compiler (and "smart" indexers), which means that it stops being checked for correctness. Also, if the rest of the code changes substantially, like variable renaming, function changing their signatures, etc, the disabled part falls out of sync and will need possibly serious work when re-enabled.

There are easy improvements to the #ifdef strategy, with the inconvenience that they add boilerplate and/or waste cycles on runtime (important on embedded environments). But there's also a way to remove both the boilerplate AND the runtime checks!



Basic


Let's start with some basic code like:
     int8_t bar;
#ifdef CONF_FEATURE
     bar = func();
#endif
 
Change the definition of bar or func and the compiler won't warn you that the disabled code is now broken.


Improved


The base of the improvements is to convert the #ifdef into a normal if. To do that, the macro must be #defined as 0 or 1 (or true/false):
#define CONF_FEATURE false  
....
    int8_t bar;
    if (CONF_FEATURE) {
        bar = func();
    }
This way, the compiler checks the code and forces us to keep it current (and the IDE pays attention to the code when refactoring). But since CONF_FEATURE still is a constant at compile_time, the compiler also knows that the code will never run, so the whole if will be optimized out.

This fixes the code-out-of-sync problem, but is inconvenient, because CONF_FEATURE now HAS to be #defined, be it false or true, or the code won't compile, whereas previously an undefined CONF_FEATURE was enough to disable the code. Also, any #ifdef CONF_FEATURE must be changed to #if CONF_FEATURE == true. Both can be jarring in a large codebase or a complicated build environment.


Better


A way to fix the inconvenience and minimize the changes to existing code and build systems is: prepare a #ifdef block "somewhere" which adapts the normal #defines into our *_BOOL #defines:
#ifdef CONF_FEATURE
    #define CONF_FEATURE_BOOL true
#else
    #define CONF_FEATURE_BOOL false
#endif
....
    int8_t bar;
    if (CONF_FEATURE) {
        bar = func();
    }
Now the original #defines stay as they were, but we can incrementally introduce the improvement. And this option is the one I have been using for a long time.


Best


But, what if there was a way to check from the C level (vs. the preprocessor level) whether a macro is defined or not? Like an IS_DEFINED(CONF_FEATURE) function callable in a normal if? Turns out, it can be done!

#define STRINGIFY(x) ""#x
#define IS_DEFINED(MACRO) ({ \
     (__builtin_strcmp("" #MACRO, STRINGIFY(MACRO))); \
     })
... 

if (IS_DEFINED(CONF_FEATURE)) { 
    bar = func();
}
That strcmp sounds crazy, right? But turns out that current GCC and Clang are able to do the string comparison at compile-time (given that its parameters are known at compile-time, logically). I have just confirmed that it works, both in GCC 5.3 (from -O0 even?!) and in clang 3.8 (from -O1).

What is happening is that we compare both the stringified name of the macro and the stringified expansion of the macro. If they differ, then the supposed macro was recognized and expanded as a macro – so it really was a macro. Thanks to Nominal Animal in Stack Overflow for the idea!

This all happens inside of an expression statement, which is a GNU extension supported by Clang. It behaves pretty much as a function, returning the value of the last expression.

Now, there are some caveats. One is that our IS_DEFINED() won't work with macros defined like #define CONF_FEATURE CONF_FEATURE – remember that our base supposition was that we distinguish a defined macro because it is expanded by the preprocessor to something different than its name, which of course won't work here.

Another problem is that there is no guarantee that the __builtin_strcmp will really be evaluated at compile-time (though it has done so in all my tests, apart from clang -O0); the GCC manual page on builtins only gives a noncommittal "many of these functions are only optimized in certain cases", so looks like the only way to be sure is checking the assembler (otool -rtV in Mac OS X).


So close...


For completeness, I have tried the following version, which tries to detect whether the strcmp was evaluated at compile-time; and it works when the parameter is a macro, but it always bails out when not:
#define IS_DEFINED(MACRO)    ({ \
    _Static_assert(__builtin_constant_p(_builtin_strcmp("" #MACRO, STRINGIFY(MACRO))) == 1, "Not compile-time!"); \
    strcmp("" #MACRO, STRINGIFY(MACRO)); \
    })

The idea is to use __builtin_constant_p(c) to check if c is a compile-time constant, and if not, then trigger a C11 static assertion – because in embedded I prefer to have the build break loudly than be running unexpected extra code. It's ugly as hell, but I was trying to help the compiler realize that the strcmp always should be doable at compile-time. Didn't work anyway, probably because, according to  the GCC manual, __builtin_constant_p does not say unequivocally whether an expression is NOT constant at compile-time; it only is unequivocal when it says that the expression IS constant. So, if you were thinking about trying this, know that as of GCC 5.2 and Clang 3.8 it won't work.

Also, it doesn't matter whether you use __builtin_strcmp or strcmp; the only difference is that the second one requires #include . But the generated assembler is the same. Interestingly, with clang -O0, __builtin_strcmp does generate a call to strcmp EVEN without the #include, which sounds like a bug to me...

2 comments

  1. Nice trick! Shouldn't it be possible to avoid the ugly strcmp call by just using !""#MACRO[0]? It is at least as ugly, but it does not depend on a compiler extension.

    ReplyDelete
  2. I couldn't make your suggestion work directly (I might be missing something?), but (!("" STRINGIFY(MACRO))[0]) does work. The linked discussion in Stack Overflow touched that kind of checks, but yours is more elegant!

    The thing is that it won't work with non-empty defined macros, but still it's appealingly clean... mhm.

    ReplyDelete