This Ain't Yer Granddaddy's C The article presents a collection of modern coding tricks and ergonomic improvements for the C programming language, aimed at making it feel more contemporary and user-friendly in 2025. The author advocates for practices such as using short numeric type aliases (e.g., `u8`, `f32`) and designated initializers to simplify syntax and improve code readability. The piece also critiques outdated C conventions by contrasting them with modern language features, emphasizing that these tricks can be directly copied and pasted into existing codebases. I’ve dragged myself out of the rubble of broken glass and half-juiced lemons that is the C programming language to bring you some fun tricks. These aren’t tricks in a clever or brilliant sense. I’d consider APE https://justine.lol/cosmopolitan/ binaries a brilliant trick. These are more about ergonomics. These go a long way in making C feel like a modern language. I took care to make sure that you can just cut-n-paste most and put them directly in your code 1. If you just want the tricks, jump to them ergonomic-tricks-in-c . Otherwise, a short preamble. a short preamble: ergonomics matter Really. I spent roughly ninety seconds looking through the Lua source code before I found this disgusting function: js unsigned luaO tostringbuff const TValue obj, char buff { int len; lua assert ttisnumber obj ; if ttisinteger obj len = lua integer2str buff, LUA N2SBUFFSZ, ivalue obj ; else len = tostringbuffFloat fltvalue obj , buff ; lua assert len < LUA N2SBUFFSZ ; return cast uint len ; } Please, don’t misunderstand me. I love Lua. I think the codebase is great. I have no problem with the above function. But you’ve got to admit that it’s full of whacky shit that you would never see or do if you were writing Lua today. lua integer2str is an alias to l sprintf which uses a couple layers of macro indirection to build an appropriate format specifier.- This should just be write buff, "{}", i e.g. in Rust - Why do I have to figure out a fucking format specifier to convert an integer to a strinbg? - This should just be - We’d use pattern matching instead of the bit twiddling ttisinteger unsigned ? Can you think of any modern language that just calls their u32 unsigned ? And this is good code. This is rock solid code. If it were 1974, this code would get promoted from floor manager to front office and raise a family, obtain a generous pension, etc. But 1974 is is not. Jerry Garcia’s long gone; Lowell George, too, and the Blind Owl, and drinking beer by the river, popping a few black beauties and cruising around all night. It’s 2025. No one wants to write code that looks like that. Some people still want to write C without writing code that looks like that. Without further delay, here’s the tricks. Quite a few of them. All of them are prefixed with sp , since that’s my catch all namespace. » shill Subscribe to the mailing list. Come, now. Who else is writing about C like this? » short numeric types typedef int8 t s8; typedef int16 t s16; typedef int32 t s32; typedef int64 t s64; typedef uint8 t u8; typedef uint16 t u16; typedef uint32 t u32; typedef uint64 t u64; typedef float f32; typedef double f64; typedef char c8; If you drop this in a header and program with your left hand, it feels like you’re writing Rust. » designated initializers The biggest one. Without these, and this is not hyperbole, C would be borderline unusable. Seriously, these things are criminally underrated. Tell me that this API is out of place in any modern language: sp ps output t result = sp ps run sp ps config t { .command = "git", .args = { "clone", "--quiet", url, path }, .cwd = build- paths.source, .io = { .in.mode = SP PS IO MODE NULL, .out = { .mode = SP PS IO MODE EXISTING, .stream = build- log }, } } ; return sp str trim result.out ; That’s legitimately nice . Compare to the equivalent Python code; it’s the same thing, except statically typed. »» fixed size c arrays The astute reader may be wondering how args works; if it’s of type const c8 an array of C strings , the user has to either: - Specify how many strings she passed error prone, not sugary - Finish with a sentinel error prone, not sugary The trick is to used fixed size C arrays. Designated initializers always zero initialize anything not explicitly initialized, so you get your sentinel for free. The downside is that: - Your structs become needlessly large - The maximum is fixed at compile time - The friction of modifying the array outside of the designated initializer is extreme But in practice, none of these matter; this is a trick for descriptor structs that configure subsystems or many-parametered APIs. This isn’t your hot loop. And for the last two, if I really need an unknown number, I’ll just add an e.g. sp da const c8 and provide sp ps config add arg . These structs immediately get converted into good types sp str t /sp-001/ , sp da , etc , so anything at all that’s pretty at the public API level is fair game. » SP FATAL do { SP LOG "{:color red}: {}", SP FMT CSTR "SP FATAL " , SP FMT STR sp format FMT , VA ARGS ; ; SP EXIT FAILURE ; } while 0 // ...e.g. u32 best dead year = 1972; if year = best dead year { SP FATAL "The best year for the Dead is {:fg cyan}", SP FMT U32 best dead year ; } This one uses my own sp format , but just drop in printf if you’d like. Syntax highlighting is busted with define , but the macro part’s just SP FATAL FMT, ... . Cut-n-paste friendly reproduction in the footnotes 2. This doesn’t have to be a macro; it’s just nice to get line numbers with line sometimes » sp dyn array + sp hash table These are a little too hefty to drop in as entire snippets 3. By far the most clever of the bunch, they are also almost entirely stolen from the legendary stb ds.h , plus John Jackson’s excellent and spiritual kin to 4 fn:4 sp.h single header library, gunslinger . 5 fn:5 In lieu of explaining their implementation, I’ll just show you their API. It’s nice . And even though they’re both implemented as macros, if you can handle a \ at the end of each line I promise it’s very similar to reading simple templates. »» sp hash table sp ht s32, u32 hash table = SP ZERO INITIALIZE; sp ht insert hash table, 69, 420 ; s32 foo = sp ht get hash table, 69 ; s32 foo ptr = sp ht getp hash table, 69 ; sp ht for hash table, it { sp ht it get hash table, it ; // returns a regular s32 sp ht it getp hash table, it ; } // the macro just expands to this for sp ht it it = sp ht it init ht ; sp ht it valid ht, it ; sp ht it advance ht, it { // ... } String keys require you to set the hash function; this is one place where real templates are unquestionably better. But API wise, I prefer this to »» sp dyn array sp da s32 dyn array = SP ZERO INITIALIZE; sp da push dyn array, 69 ; sp da push dyn array, 420 ; sp da for dyn array, it { s32 value = dyn array it ; // yes, this works dyn array is just a plain s32 } This one’s the real beaut. By storing the array’s metadata before the array itself, you can store the array as a plain T and know that your metadata is a fixed number of bytes before that pointer. Ergonomically, that’s everything It feels sketchy for sp da s32 and s32 to be the same; the compiler will happy pass an arbitrary pointer to one of these functions. But that’s why we use sp da s32 when we declare. It signals to the reader the true type, even if the macro is just this: define sp dyn array T T define sp da T T » iterators Use iterators to make your for loops way, way easier to understand. Not always applicable; it’s annoying that C doesn’t have closures, so they have to be defined away from the call site for quick one-off cases. And still: // define omitted for syntax highlighting sp carr len CARR sizeof CARR / sizeof CARR 0 sp carr for CARR, IT for u32 IT = 0; IT < sp carr len CARR ; IT++ sp str t paths = { sp os join path path, sp str lit "spn.toml" , sp os join path path, sp str lit "spn.c" , }; sp carr for paths, it { sp str t path = paths it ; } Equally useful for whatever structs you have. Stop writing a bunch of index arithmetic. typedef struct { s32 index; bool reverse; sp ring buffer t buffer; } sp rb it t; define sp ring buffer for rb, it for sp rb it t it = sp rb it new & rb ; sp rb it done & it ; sp rb it next & it sp ring buffer for rb, it { // ... } » SP NULLPTR and other C++ smoothness ifdef SP CPP define SP NULLPTR nullptr define SP THREAD LOCAL thread local define SP BEGIN EXTERN C extern "C" { define SP END EXTERN C } define SP ZERO INITIALIZE {} else define SP NULLPTR void 0 define SP THREAD LOCAL Thread local define SP BEGIN EXTERN C define SP END EXTERN C define SP ZERO INITIALIZE {0} endif If you intend to use your C in C++ sometimes, this is really nice. Even if you don’t, it’s pretty sweet to read SP NULLPTR instead of NULL for argument 5 of some dusty POSIX function that you remember only hazily. » x macros X macros are the humble pack camel of C; they trudge through the desert, all we have against her death winds, and without them we would be stranded, utterly helpless, impotent, in a sea of formless flat and dune. They are the only tool we have to iterate over a list of things with the preprocessor. Without them we’d be pretty well fucked as far as preprocessor coercion. All that an X macro is is a macro which accepts another macro as its argument, and then applies that macro to a list of things. The beauty is that the argument, being an argument , could be anything at the time of invocation. define omitted because they break syntax highlighting and I’m lazy SP X NAMED ENUM DEFINE ID, NAME ID, SP X NAMED ENUM CASE TO CSTR ID, NAME case ID: { return NAME ; } SPN TOOL SUBCOMMAND X \ X SPN TOOL INSTALL, "install" \ X SPN TOOL UNINSTALL, "uninstall" \ X SPN TOOL RUN, "run" \ X SPN TOOL LIST, "list" \ X SPN TOOL UPDATE, "update" // "invoke" the macro to define each enumerated value typedef enum { SPN TOOL SUBCOMMAND SP X NAMED ENUM DEFINE } spn tool cmd t; // "invoke" the macro to return a string literal of e.g. SPN TOOL INSTALL spn tool cmd t spn tool subcommand from str sp str t str { SPN TOOL SUBCOMMAND SP X NAMED ENUM STR TO ENUM } // "invoke" the macro to make a case statement for each value sp str t spn tool subcommand to str spn tool cmd t cmd { switch cmd { SPN TOOL SUBCOMMAND SP X NAMED ENUM CASE TO STRING LOWER } } congrats If you made it this far. C’s gorgeous. Some recommendations for truly pretty C code; some of it’s closer to this style, some of it’s not particularly, all are legitimately beautiful code. Clear, imperative, precise use of indirection and abstraction. tcc https://github.com/TinyCC/tinycc ; here’s a random function https://github.com/TinyCC/tinycc/blob/3e8f1da9c559f05b046e6cbe87405125c983b15a/libtcc.c L1053 I was looking at recently. Fabrice Bellard is the greatest programmer of all time. sokol github.com/floooh/sokol ; IMO, the perfector of the single header library. I could read this source all day. gunslinger https://github.com/MrFrenik/gunslinger ; a little cheekier, a little more out there, more macros, more sugar. But very much in the tradition of Sokol, and similarly very tidy. I don’t actually recommend reading STB sources; not as a statement of quality. It’s some of the highest quality code ever written over some measure of ease-of-use times number of users times simplicity times usefulness. But it’s very much in the style of old C. It’s not pretty , even if it’s elegant. Here’s a slightly longer function I wrote recently that puts some of this stuff in a bow. It’s always better to see code in context. php void spn app prepare dep builds spn app t app { sp ht for app- package.deps, it { spn dep req t request = sp ht it getp app- package.deps, it ; spn semver t version = sp ht getp app- resolver.versions, request.name ; spn pkg t package = spn app find package app, request ; SP ASSERT package ; spn metadata t metadata = sp ht getp package- metadata, version ; SP ASSERT metadata ; // add a new build context for this dep spn pkg build t dep = { .name = request.name, .mode = SPN DEP BUILD MODE DEBUG, .metadata = metadata, .profile = app- profile, }; spn dep options t options = sp ht getp app- package.config, request.name ; if options { spn dep option t kind = sp ht getp options, sp str lit "kind" ; if kind { dep.kind = spn lib kind from str kind- str ; } } if dep.kind { spn lib kind t kinds = { SPN LIB KIND SOURCE, SPN LIB KIND STATIC, SPN LIB KIND SHARED }; sp carr for kinds, it { if sp ht getp package- lib.enabled, kinds it { dep.kind = kinds it ; } } } sp dyn array sp hash t hashes = SP NULLPTR; sp dyn array push hashes, sp hash str dep.metadata.commit ; sp dyn array push hashes, dep.profile.linkage ; sp dyn array push hashes, dep.profile.libc ; sp dyn array push hashes, dep.profile.standard ; sp dyn array push hashes, dep.profile.mode ; sp dyn array push hashes, dep.metadata.version.major ; sp dyn array push hashes, dep.metadata.version.minor ; sp dyn array push hashes, dep.metadata.version.patch ; dep.build id = sp hash combine hashes, sp dyn array size hashes ; sp str t build id = sp format "{}", SP FMT SHORT HASH dep.build id ; switch request.kind { case SPN PACKAGE KIND INDEX: { sp str t work = sp os join path spn.paths.build, package- name ; sp str t store = sp os join path spn.paths.store, package- name ; dep.paths.work = sp os join path work, build id ; dep.paths.store = sp os join path store, build id ; dep.paths.source = sp os join path spn.paths.source, package- name ; break; } case SPN PACKAGE KIND FILE: case SPN PACKAGE KIND WORKSPACE: case SPN PACKAGE KIND REMOTE: { SP FATAL "Tried to prepare {:fg brightcyan}, but kind was {:fg brightyellow}", SP FMT STR dep.name , SP FMT STR spn dep req to str request ; SP BROKEN ; break; } case SPN PACKAGE KIND NONE: { SP UNREACHABLE CASE ; } } spn app prepare build io &dep ; sp ht insert app- deps, request.name, dep ; } sp ht for app- package.deps, it { spn dep req t request = sp ht it getp app- package.deps, it ; spn pkg build t dep = sp ht getp app- deps, request.name ; dep- package = spn app find package app, request ; } } SP FATAL https://github.com/tspader/sp/blob/9966494a49d7c3452f9e0b65ceed5bedacc5747d/sp.h L197 source code ↩︎ fnref:2 sp hash table and sp dyn array https://github.com/tspader/sp/blob/9966494a49d7c3452f9e0b65ceed5bedacc5747d/sp.h L465 source code ↩︎ fnref:3 stb ds.h github.com/nothings/stb/blob/master/stb ds.h . There she is. Venerable, stately. Old, some might say, but more Helen Mirren than your Aunt Helen. She was never quite the same after the strokes. ↩︎ fnref:4 gs.h’s dyn array https://github.com/MrFrenik/gunslinger/blob/9e96f9c6bfa879cf122cd75a18bfc63644af53b6/gs.h L239 , which itself is mostly aped from Sean Barrett and from which I took a few tweaks. ↩︎ fnref:5