# This Ain't Yer Granddaddy's C

> Source: <https://spader.zone/tricks/>
> Published: 2025-11-19 00:00:00+00:00

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)
