As I've mentioned in previous posts, I've been writing my own C2Y Compiler / Interpreter / REPL for fun. One of the first things one has to do when writing a C Compiler is to first write a C Preprocessor (also known as the CPP). Counter to what most people think of the C Preprocessor, it is not a text-to-text translator. Instead it is a source to C token translator, with most implementations offering a text output to see what it does. Historically, it might have been implemented as a text-to-text translator, but the C standard specifies it as a series of phases that ends up with C tokens that are consumed by the C parser until you eventually end up with a C translation unit.
Others have probably written about how to write a C Preprocessor before, so I won't focus on that. Instead I'll focus on some fun ways I've extended the C Preprocessor, either to make it easier to use or just because I thought it was goofy. I won't list all of them, but just the particularly interesting ones.
__print()/__where()
The most straightforward extensions: builtin debugging macros.
__print(...) acts as a preprocessor print function, expanding its arg(s) and writing it to stderr, useful for debugging what a macro expands to. As you only get exactly what you __print(), it can be much easier to see what the preprocessor expands to without dumping the entire translation unit like with cpp.
$ printf "#define X 1\n__print(X X)" | drcpp (stdin):2:1: 1 1
Ever have trouble figuring out where the heck a macro was defined that is expanding in some weird way? Maybe some baroque glibc macro is ruining your life? __where() is your friend. It writes to stderr where the macro was defined.
$ printf "#define X 1\n\n\n\n__where(X)" | drcpp (stdin):1:9: info: X defined
__map()
Something that occasionally comes up is the need to apply some transformation to each argument in a list. Perhaps you want to print a sequence of values you have in a preprocessor sequence. The standard functional way to implement this would be to write a recursive function (in the preprocessor a function-like macro), but the C standard blocks direct recursion as the tokens get "painted blue" and have their expansion suppressed. However, you generally don't need arbitrarily long recursion, so you can fake it by pumping the recursion yourself for a fixed number of iterations. If you have an aversion to disgusting preprocessor abuse, then don't look at this example:
I warned you
// map.c
#define CAT(a, b) CAT_(a, b)
#define CAT_(a, b) a##b
#define NARG(...) \
NARG_(__VA_ARGS__ __VA_OPT__(,) 8,7,6,5,4,3,2,1,0)
#define NARG_(_1,_2,_3,_4,_5,_6,_7,_8,N,...) N
#define MAP_0(m)
#define MAP_1(m, x) m(x)
#define MAP_2(m, x, ...) m(x) MAP_1(m, __VA_ARGS__)
#define MAP_3(m, x, ...) m(x) MAP_2(m, __VA_ARGS__)
#define MAP_4(m, x, ...) m(x) MAP_3(m, __VA_ARGS__)
#define MAP_5(m, x, ...) m(x) MAP_4(m, __VA_ARGS__)
#define MAP_6(m, x, ...) m(x) MAP_5(m, __VA_ARGS__)
#define MAP_7(m, x, ...) m(x) MAP_6(m, __VA_ARGS__)
#define MAP_8(m, x, ...) m(x) MAP_7(m, __VA_ARGS__)
#define __map(m, ...) \
CAT(MAP_, NARG(__VA_ARGS__))(m __VA_OPT__(,) __VA_ARGS__)
int x = 1;
int y = 2;
int z = 3;
#define PRINTINT(x) printf(#x " = %d\n", x);
int printf(const char*, ...);
int main(){
__map(PRINTINT, 1, 2, 3, 4, x, y, z)
}
Which expands to (whitespace inserted for readability):
// map.i
int x = 1;
int y = 2;
int z = 3;
int printf(const char*, ...);
int main(){
printf("1" " = %d\n", 1); // 1 = 1
printf("2" " = %d\n", 2); // 2 = 2
printf("3" " = %d\n", 3); // 3 = 3
printf("4" " = %d\n", 4); // 4 = 4
printf("x" " = %d\n", x); // x = 1
printf("y" " = %d\n", y); // y = 2
printf("z" " = %d\n", z); // z = 3
}
This is useful, but obviously pretty disgusting and is generally quite slow as you are abusing preprocessor expansion, which has to scan and expand each argument of each function-like macro over and over again. So I added __map(macro, ...) to my C preprocessor:
With a builtin __map(), the above example simplifies to just:
int x = 1;
int y = 2;
int z = 3;
#define PRINTINT(x) printf(#x " = %d\n", x);
int printf(const char*, ...);
int main(){
__map(PRINTINT, 1, 2, 3, 4, x, y, z)
}
Now you don't have to mess around with faking recursion: it can apply the macro to each argument once in a linear scan. And you have no arbitrary limit on how many items you can map!
__VA_COUNT__
C23 made working with variadic macros more convenient and powerful with __VA_OPT__(), but some use-cases remain difficult. As demonstrated in the previous section, it can be useful to "overload" a macro by its arity, but this requires gross manual preprocessor pumping.
Another use case is passing a count + pointer to a function if your compiler still hasn't fixed C's biggest mistake, or a param count to a variadic function.
// sum.c
#define CAT(a, b) CAT_(a, b)
#define CAT_(a, b) a##b
#define NARG(...) \
NARG_(__VA_ARGS__ __VA_OPT__(,) 8,7,6,5,4,3,2,1,0)
#define NARG_(_1,_2,_3,_4,_5,_6,_7,_8,N,...) N
int sum(size_t count, int* data){
int result = 0;
for(size_t i = 0; i < count; i++){
result += data[i];
}
return result;
}
#define sum(...) sum(NARG(__VA_ARGS__), (int[]){__VA_ARGS__})
int printf(const char*, ...);
int main(){
printf("%d\n", sum(1,2,3)); // 6
printf("%d\n", sum(4,5)); // 9
}
Instead of going through all of this nonsense to just get the number of variadic arguments (which the cpp already has!), I added a builtin macro __VA_COUNT__, so you can just get the count of variadic arguments directly.
// sum-drc.c
int sum(size_t count, int* data){
int result = 0;
for(size_t i = 0; i < count; i++){
result += data[i];
}
return result;
}
#define sum(...) sum(__VA_COUNT__, (int[]){__VA_ARGS__})
int printf(const char*, ...);
int main(){
printf("%d\n", sum(1,2,3)); // 6
printf("%d\n", sum(4,5)); // 9
}
Nicer and faster!
Conclusion
The C Preprocessor is a powerful tool, but requires some unreadable dark magic to really take advantage of it. This isn't inherent to what you want to do with it though - it is merely a consequence of its underdeveloped machinery. With some extensions, we can make the CPP more powerful while also improving readability and making it more enjoyable to use.
I have more to talk about, including some true dark magic like mutable preprocessor state, but that's it for this post!