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.
lua_integer2stris an alias tol_sprintfwhich 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 u32unsigned?
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:
- 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_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.
- tcc; here’s a random function I was looking at recently. Fabrice Bellard is the greatest programmer of all time.
- sokol; IMO, the perfector of the single header library. I could read this source all day.
- 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.
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() source code ↩︎
sp_hash_table and sp_dyn_array source code ↩︎
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. ↩︎
gs.h’s dyn_array, which itself is mostly aped from Sean Barrett and from which I took a few tweaks. ↩︎