This Ain't Yer Granddaddy's C (Tricks to Write Gorgeous C)

2025/11/19


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 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 code1.

If you just want the tricks, jump to them. 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:

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.

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.

» 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:

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:

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_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 footnotes2.

(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 snippets3. By far the most clever of the bunch, they are also almost entirely stolen from the legendary stb_ds.h4, plus John Jackson’s excellent (and spiritual kin to sp.h) single header library, gunslinger5.

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.

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.

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);
  }
}

  1. sp.h is my C standard library; find full sources there ↩︎

  2. SP_FATAL() source code ↩︎

  3. sp_hash_table and sp_dyn_array source code ↩︎

  4. 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. ↩︎

  5. gs.h’s dyn_array, which itself is mostly aped from Sean Barrett and from which I took a few tweaks. ↩︎