Rust-style string interpolation for C -- use {variable} directly in format strings with automatic type detection via _Generic.
let(age, 26);
let(name, "Moss");
rprintf("hello {name}, you are {age} years old\n");C variable names are erased by the compiler. After compilation, a variable is just a stack offset like [rbp-4]. There is no runtime table mapping names to values unless you build one explicitly. To bridge this gap, rprintf provides two macros that use the # preprocessor stringification operator to capture variable names and snapshot their values into a central registry:
let(var, value): Declares a new variable using__typeof__, initializes it, and immediately registers it for interpolation.watch(var): Captures or updates an already existing variable in the registry. This must be called manually after mutating a variable to synchronize its new value withrprintf.
let(score, 100); // Declared and automatically registered
score = 150; // Mutated
watch(score); // Manually updated in the registryWhen working with local arrays, you must call watch immediately after their declaration:
int scores[] = { 10, 20, 30, 0 };
watch(scores); // Snapshots the array into the registryThis sequence is mandatory because:
- Scope and Existence:
watchdoes not allocate or declare memory. It requires the variable to already be fully declared and initialized in scope so it can extract its stringified name ("scores") and evaluate its address. - Alternative Initialization: Alternatively, arrays can be declared and automatically tracked in a single statement using the
letmacro with a compound literal:let(scores, ((int[]){ 10, 20, 30, 0 }));
- Type Detection:
_Genericdispatches each variable to the correctmake_*constructor at compile time, capturing both the value and its type tag into anArgtagged union. - Registry: A static
VarRegistrystores up to 256(name, Arg)pairs. The macros snapshot the current value on each call. - Interpolation:
rprintfwalks the format string, extracts names between{and}, looks them up in the registry, and prints them viaprint_arg.
- Array Pointer Decay & Sentinel Values: In C, passing an array to a function causes it to "decay" into a pointer, losing its compile-time size information. Because
MAKE_ARGpasses the array to functions likemake_int_arr(const int *x), the array's size cannot be determined viasizeof. Instead, arrays must be explicitly terminated with a0or0.0sentinel value so that their lengths can be dynamically scanned and calculated at runtime. - Manual Synchronization:
watchmust be called manually after every mutation to update the snapshot in the registry. - Fixed Scale: The registry is global, capped at 256 entries, and is not thread-safe.
- Supported Array Types: Native support is limited to
int[]anddouble[].