{"slug": "this-ain-t-yer-granddaddy-s-c", "title": "This Ain't Yer Granddaddy's C", "summary": "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.", "body_md": "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.\n\nThese 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.\n\nIf you just want the tricks, [jump to them](#ergonomic-tricks-in-c). Otherwise, a short preamble.\n\n# a short preamble: ergonomics matter\n\nReally. I spent roughly ninety seconds looking through the Lua source code before I found this disgusting function:\n\n``` js\nunsigned luaO_tostringbuff (const TValue *obj, char *buff) {\n  int len;\n  lua_assert(ttisnumber(obj));\n  if (ttisinteger(obj))\n    len = lua_integer2str(buff, LUA_N2SBUFFSZ, ivalue(obj));\n  else\n    len = tostringbuffFloat(fltvalue(obj), buff);\n  lua_assert(len < LUA_N2SBUFFSZ);\n  return cast_uint(len);\n}\n```\n\nPlease, 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.\n\n`lua_integer2str`\n\nis an alias to`l_sprintf`\n\nwhich uses a couple layers of macro indirection to build an appropriate format specifier.- This should just be\n`write!(buff, \"{}\", i)`\n\n(e.g. in Rust) - Why do I have to figure out a fucking format specifier to convert an integer to a strinbg?\n\n- This should just be\n- We’d use pattern matching instead of the bit twiddling\n`ttisinteger()`\n\n`unsigned`\n\n? Can you think of*any*modern language that just calls their u32`unsigned`\n\n?\n\nAnd 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.\n\nIt’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`\n\n, since that’s my catch all namespace.\n\n# » *shill*\n\nSubscribe to the mailing list. Come, now. Who else is writing about C like this?\n\n# » *short numeric types*\n\n```\ntypedef int8_t   s8;\ntypedef int16_t  s16;\ntypedef int32_t  s32;\ntypedef int64_t  s64;\ntypedef uint8_t  u8;\ntypedef uint16_t u16;\ntypedef uint32_t u32;\ntypedef uint64_t u64;\ntypedef float    f32;\ntypedef double   f64;\ntypedef char     c8;\n```\n\nIf you drop this in a header and program with your left hand, it feels like you’re writing Rust.\n\n# » *designated initializers*\n\nThe 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:\n\n```\nsp_ps_output_t result = sp_ps_run((sp_ps_config_t) {\n  .command = \"git\",\n  .args = {\n    \"clone\", \"--quiet\", url, path\n  },\n  .cwd = build->paths.source,\n  .io = {\n    .in.mode = SP_PS_IO_MODE_NULL,\n    .out = { .mode = SP_PS_IO_MODE_EXISTING, .stream = build->log },\n  }\n});\n\nreturn sp_str_trim(result.out);\n```\n\nThat’s legitimately *nice*. Compare to the equivalent Python code; it’s the same thing, except statically typed.\n\n## »» *fixed size c arrays*\n\nThe astute reader may be wondering how `args`\n\nworks; if it’s of type `const c8**`\n\n(an array of C strings), the user has to either:\n\n- Specify how many strings she passed (error prone, not sugary)\n- Finish with a sentinel (error prone, not sugary)\n\nThe 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:\n\n- Your structs become needlessly large\n- The maximum is fixed at compile time\n- The friction of modifying the array\n*outside*of the designated initializer is extreme\n\nBut 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*)`\n\nand provide `sp_ps_config_add_arg()`\n\n.\n\nThese structs immediately get converted into good types ([sp_str_t](/sp-001/), `sp_da`\n\n, etc), so anything at all that’s pretty at the public API level is fair game.\n\n## » *SP_FATAL()*\n\n```\ndo {\n  SP_LOG(\n    \"{:color red}: {}\",\n    SP_FMT_CSTR(\"SP_FATAL()\"),\n    SP_FMT_STR(sp_format((FMT), ##__VA_ARGS__);)\n  );\n  SP_EXIT_FAILURE();\n} while (0)\n\n// ...e.g.\nu32 best_dead_year = 1972;\nif (year != best_dead_year) {\n  SP_FATAL(\"The best year for the Dead is {:fg cyan}\", SP_FMT_U32(best_dead_year));\n}\n```\n\nThis one uses my own `sp_format()`\n\n, but just drop in `printf()`\n\nif you’d like. Syntax highlighting is busted with `#define`\n\n, but the macro part’s just `SP_FATAL(FMT, ...)`\n\n. Cut-n-paste friendly reproduction in the footnotes 2.\n\n(This doesn’t have to be a macro; it’s just nice to get line numbers with `__line__`\n\nsometimes)\n\n# » *sp_dyn_array + sp_hash_table*\n\nThese 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\n\n`stb_ds.h`\n\n, plus John Jackson’s excellent (and spiritual kin to\n\n[4](#fn:4)`sp.h`\n\n) single header library, `gunslinger`\n\n.\n\n[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 `\\`\n\nat the end of each line I promise it’s *very* similar to reading simple templates.\n\n## »» *sp_hash_table*\n\n```\nsp_ht(s32, u32) hash_table = SP_ZERO_INITIALIZE;\nsp_ht_insert(hash_table, 69, 420);\n\ns32 foo = sp_ht_get(hash_table, 69);\ns32 foo_ptr = sp_ht_getp(hash_table, 69);\n\nsp_ht_for(hash_table, it) {\n  sp_ht_it_get(hash_table, it);  // returns a regular s32\n  sp_ht_it_getp(hash_table, it);\n}\n\n// the macro just expands to this\nfor (sp_ht_it it = sp_ht_it_init(ht); sp_ht_it_valid(ht, it); sp_ht_it_advance(ht, it)) {\n  // ...\n}\n```\n\nString 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\n\n## »» *sp_dyn_array*\n\n```\nsp_da(s32) dyn_array = SP_ZERO_INITIALIZE;\nsp_da_push(dyn_array, 69);\nsp_da_push(dyn_array, 420);\n\nsp_da_for(dyn_array, it) {\n  s32 value = dyn_array[it]; // yes, this works! dyn_array is just a plain s32*\n}\n```\n\nThis one’s the real beaut. By storing the array’s metadata before the array itself, you can store the array as a plain `T*`\n\nand know that your metadata is a fixed number of bytes before that pointer. Ergonomically, that’s everything!\n\nIt feels sketchy for `sp_da(s32)`\n\nand `s32*`\n\nto 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)`\n\nwhen we declare. It signals to the reader the true type, even if the macro is just this:\n\n```\n#define sp_dyn_array(T) T*\n#define sp_da(T) T*\n```\n\n# » iterators\n\nUse iterators to make your `for`\n\nloops 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:\n\n```\n// #define omitted for syntax highlighting\nsp_carr_len(CARR) (sizeof((CARR)) / sizeof((CARR)[0]))\nsp_carr_for(CARR, IT) for (u32 IT = 0; IT < sp_carr_len(CARR); IT++)\n\nsp_str_t paths [] = {\n  sp_os_join_path(path, sp_str_lit(\"spn.toml\")),\n  sp_os_join_path(path, sp_str_lit(\"spn.c\")),\n};\nsp_carr_for(paths, it) {\n  sp_str_t path = paths[it];\n}\n```\n\nEqually useful for whatever structs you have. Stop writing a bunch of index arithmetic.\n\n```\ntypedef struct {\n  s32 index;\n  bool reverse;\n  sp_ring_buffer_t* buffer;\n} sp_rb_it_t;\n\n#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)))\n\nsp_ring_buffer_for(rb, it) {\n  // ...\n}\n```\n\n# » *SP_NULLPTR and other C++ smoothness*\n\n```\n#ifdef SP_CPP\n  #define SP_NULLPTR nullptr\n  #define SP_THREAD_LOCAL thread_local\n  #define SP_BEGIN_EXTERN_C() extern \"C\" {\n  #define SP_END_EXTERN_C() }\n  #define SP_ZERO_INITIALIZE() {}\n#else\n  #define SP_NULLPTR ((void*)0)\n  #define SP_THREAD_LOCAL _Thread_local\n  #define SP_BEGIN_EXTERN_C()\n  #define SP_END_EXTERN_C()\n  #define SP_ZERO_INITIALIZE() {0}\n#endif\n```\n\nIf 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`\n\ninstead of `NULL`\n\nfor argument 5 of some dusty POSIX function that you remember only hazily.\n\n# » *x macros*\n\nX 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.\n\nAll 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.\n\n`#define`\n\nomitted because they break syntax highlighting and I’m lazy\n\n```\nSP_X_NAMED_ENUM_DEFINE(ID, NAME) ID,\nSP_X_NAMED_ENUM_CASE_TO_CSTR(ID, NAME) case ID: { return (NAME); }\n\nSPN_TOOL_SUBCOMMAND(X) \\\n  X(SPN_TOOL_INSTALL, \"install\") \\\n  X(SPN_TOOL_UNINSTALL, \"uninstall\") \\\n  X(SPN_TOOL_RUN, \"run\") \\\n  X(SPN_TOOL_LIST, \"list\") \\\n  X(SPN_TOOL_UPDATE, \"update\")\n\n// \"invoke\" the macro to define each enumerated value\ntypedef enum {\n  SPN_TOOL_SUBCOMMAND(SP_X_NAMED_ENUM_DEFINE)\n} spn_tool_cmd_t;\n\n// \"invoke\" the macro to return a string literal of e.g. SPN_TOOL_INSTALL\nspn_tool_cmd_t spn_tool_subcommand_from_str(sp_str_t str) {\n  SPN_TOOL_SUBCOMMAND(SP_X_NAMED_ENUM_STR_TO_ENUM)\n}\n\n// \"invoke\" the macro to make a case statement for each value\nsp_str_t spn_tool_subcommand_to_str(spn_tool_cmd_t cmd) {\n  switch (cmd) {\n    SPN_TOOL_SUBCOMMAND(SP_X_NAMED_ENUM_CASE_TO_STRING_LOWER)\n  }\n}\n```\n\n# congrats\n\nIf 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.\n\n[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.\n\nI 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.\n\nHere’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.\n\n``` php\nvoid spn_app_prepare_dep_builds(spn_app_t* app) {\n  sp_ht_for(app->package.deps, it) {\n    spn_dep_req_t request = *sp_ht_it_getp(app->package.deps, it);\n    spn_semver_t version = *sp_ht_getp(app->resolver.versions, request.name);\n\n    spn_pkg_t* package = spn_app_find_package(app, request);\n    SP_ASSERT(package);\n\n    spn_metadata_t* metadata = sp_ht_getp(package->metadata, version);\n    SP_ASSERT(metadata);\n\n    // add a new build context for this dep\n    spn_pkg_build_t dep = {\n      .name = request.name,\n      .mode = SPN_DEP_BUILD_MODE_DEBUG,\n      .metadata = *metadata,\n      .profile = app->profile,\n    };\n\n    spn_dep_options_t* options = sp_ht_getp(app->package.config, request.name);\n    if (options) {\n      spn_dep_option_t* kind = sp_ht_getp(*options, sp_str_lit(\"kind\"));\n      if (kind) {\n        dep.kind = spn_lib_kind_from_str(kind->str);\n      }\n    }\n\n    if (!dep.kind) {\n      spn_lib_kind_t kinds [] = {\n        SPN_LIB_KIND_SOURCE, SPN_LIB_KIND_STATIC, SPN_LIB_KIND_SHARED\n      };\n      sp_carr_for(kinds, it) {\n        if (sp_ht_getp(package->lib.enabled, kinds[it])) {\n          dep.kind = kinds[it];\n        }\n      }\n    }\n\n    sp_dyn_array(sp_hash_t) hashes = SP_NULLPTR;\n    sp_dyn_array_push(hashes, sp_hash_str(dep.metadata.commit));\n    sp_dyn_array_push(hashes, dep.profile.linkage);\n    sp_dyn_array_push(hashes, dep.profile.libc);\n    sp_dyn_array_push(hashes, dep.profile.standard);\n    sp_dyn_array_push(hashes, dep.profile.mode);\n    sp_dyn_array_push(hashes, dep.metadata.version.major);\n    sp_dyn_array_push(hashes, dep.metadata.version.minor);\n    sp_dyn_array_push(hashes, dep.metadata.version.patch);\n    dep.build_id = sp_hash_combine(hashes, sp_dyn_array_size(hashes));\n    sp_str_t build_id = sp_format(\"{}\", SP_FMT_SHORT_HASH(dep.build_id));\n\n    switch (request.kind) {\n      case SPN_PACKAGE_KIND_INDEX: {\n        sp_str_t work = sp_os_join_path(spn.paths.build, package->name);\n        sp_str_t store = sp_os_join_path(spn.paths.store, package->name);\n\n        dep.paths.work = sp_os_join_path(work, build_id);\n        dep.paths.store = sp_os_join_path(store, build_id);\n        dep.paths.source = sp_os_join_path(spn.paths.source, package->name);\n\n        break;\n      }\n      case SPN_PACKAGE_KIND_FILE:\n      case SPN_PACKAGE_KIND_WORKSPACE:\n      case SPN_PACKAGE_KIND_REMOTE: {\n        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)));\n        SP_BROKEN();\n        break;\n      }\n      case SPN_PACKAGE_KIND_NONE: {\n        SP_UNREACHABLE_CASE();\n      }\n    }\n\n    spn_app_prepare_build_io(&dep);\n    sp_ht_insert(app->deps, request.name, dep);\n  }\n\n  sp_ht_for(app->package.deps, it) {\n    spn_dep_req_t request = *sp_ht_it_getp(app->package.deps, it);\n    spn_pkg_build_t* dep = sp_ht_getp(app->deps, request.name);\n    dep->package = spn_app_find_package(app, request);\n  }\n}\n```\n\n[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)", "url": "https://wpnews.pro/news/this-ain-t-yer-granddaddy-s-c", "canonical_source": "https://spader.zone/tricks/", "published_at": "2025-11-19 00:00:00+00:00", "updated_at": "2026-05-23 05:40:31.328697+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Lua"], "alternates": {"html": "https://wpnews.pro/news/this-ain-t-yer-granddaddy-s-c", "markdown": "https://wpnews.pro/news/this-ain-t-yer-granddaddy-s-c.md", "text": "https://wpnews.pro/news/this-ain-t-yer-granddaddy-s-c.txt", "jsonld": "https://wpnews.pro/news/this-ain-t-yer-granddaddy-s-c.jsonld"}}