Fun Extending the C Preprocessor

Hacker News Top Tools

Summary

The author describes fun extensions added to the C preprocessor in their DrC C2Y compiler project, including debugging builtins like __print()/__where() and a functional-style __map() macro for transforming argument lists.

No content available
Original Article
View Cached Full Text

Cached at: 10/18/26, 07:53 AM

# Fun-Extending-the-C-Preprocessor Source: [https://www.davidpriver.com/Fun-Extending-the-C-Preprocessor.html](https://www.davidpriver.com/Fun-Extending-the-C-Preprocessor.html) As I've mentioned in[previous](https://www.davidpriver.com/fixing-c-biggest-mistake.html)[posts](https://www.davidpriver.com/From-Tree-Walker-to-Bytecode.html), I've been writing my own[C2Y Compiler / Interpreter / REPL](https://github.com/drpriver/DrC)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](https://github.com/drpriver/DrC/blob/master/EXTENSIONS.md#preprocessor), 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 withcpp\. ``` $ 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](https://www.davidpriver.com/fixing-c-biggest-mistake.html), 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\!

Similar Articles

On C extensions, portability, and alternative compilers

Lobsters Hottest

This article discusses the practical challenges of writing portable C code due to reliance on non-standard compiler extensions and glibc's conditional headers, illustrating with examples from building a C compiler.

Getting silly with C, part &((int*)-8)[3]

Lobsters Hottest

A humorous educational article covering C programming fundamentals such as forward declarations, operator precedence, unconditional jumps, and basic arithmetic with intentionally silly code examples.

C for Rust Programmers

Lobsters Hottest

A blog post by a Rust programmer detailing quirky C language behaviors learned while self-teaching C, including booleans via <stdbool.h>, null-terminated strings versus Rust's length-prefixed &str, and related memory trade-offs. It serves as a practical companion guide for developers transitioning from Rust to C.

a bunch of stuff i used to not know about K&R C

Lobsters Hottest

The article explores obscure details about pre-ANSI C (K&R C) including the absence of void, different floating point types, and simplified type specifier rules. It explains how the language's context-sensitive grammar was justified by single-pass compiler constraints.