Why I Stopped Writing New Systems Code in C and Switched to Zig
Why Zig over C for Systems Programming
C is still the author's favorite language, but not the one he wants to write new code in. Zig keeps C's small, close-to-the-metal, no-GC character while fixing the parts that cost time: errdefer replaces goto cleanup chains, slices carry their length, errors can't be ignored, and comptime replaces the preprocessor. He's honest about the tradeoff — Zig still allows use-after-free, and a 0.17 upgrade broke 35 of his book's 152 files.
I am not writing this post because I think C is bad; C is my favorite programming language! I am writing it because I do not want to write new code in C any more.
- uecker
Zig is a nice language, but those articles always compare new languages against old-style C without use of modern tools. Most of the listed advantages you can have in C as well if you are willing to adopt modern tooling and style: signed overflow checking: sanitizers, errors you can not ignore: [[nodiscard]], slice type which know their size: variably-modified types or span types.
Defer will also appear in C compilers soon, but personally I still think goto-error handling is clearer.
(Rust has an advantage for temporal memory safety, but this does apply to Zig.)
- variadix
I’ve settled on a dialect of C++ for systems programming after using C for a long time. It’s mostly C-like and I disable exceptions, RTTI, etc. but I do use templates, overloads, methods, lambdas, etc.
I have my own object system with constructor methods that all return a status code so you don’t need exceptions, my own stdlib for containers, buffered I/O, networking, hashing, math, threading, async, etc.
C++ gives you all the features to effectively build your own language, and you can use first class tooling since C++ is widely supported. LLMs also make it much easier to build out and test an entire stdlib on your own.
- asveikau
Just from the first example:
struct user *user_new(const char *name, size_t n_groups)
{
struct user *u = malloc(sizeof *u);
if (u == NULL)
goto fail;
u->name = strdup(name);
if (u->name == NULL)
goto fail_user;
u->groups = calloc(n_groups, sizeof *u->groups);
if (u->groups == NULL)
goto fail_name;
return u;
fail_name:
free(u->name);
fail_user:
free(u);
fail:
return NULL;
}
I think you can represent C better than this. I like to focus on one cleanup block, and exploit the fact that free(NULL) is a no-op.
struct user *user_new(const char *name, size_t n_groups)
{
struct user *u = calloc(sizeof *u, 1);
if (u == NULL)
goto fail;
u->name = strdup(name);
if (u->name == NULL)
goto fail;
u->groups = calloc(n_groups, sizeof *u->groups);
if (u->groups == NULL)
goto fail;
return u;
fail:
if (u)
{
free(u->name);
free(u->groups);
}
free(u);
return NULL;
}
Although note that this assumes calloc(sizeof *u, 1) initializes pointers to NULL, IIRC assuming the bit pattern of NULL is all zeroes is technically against the standard but every system you're likely to target will still work.
If you follow the pattern of "it's OK to free NULL pointers", you can also re-use a free function: […]
- pron
Also, Zig is as at least as expressive as C++ (but without the implicitness and general language complexity that makes reviewing some sensitive code, where you need to see exactly what operations are performed, hard), and it supports arenas better than C/C++/Rust.
- drnick1
All these new languages are unreadable relative to C. If, like me, you like explicit stuff like for (size_t i = 0; i <= n; i++) total += a[i]; all these new languages won't do it.
- kombine
D and Nim have had the same features for years btw.
- qweqwe14
Rust will take over all these niche languages anyway. Yes, Rust is a viable replacement for C and C++ in the long term. Zig is just trying to be a middle ground, which we've established doesn't work. Zig is particularly irrelevant compared to Rust in the LLM era.
- program
> Errors you cannot ignore
It's okay for me to declare a macro with `__attribute__((warn_unused_result))` for GCC and Clang, and `[[nodiscard]]` when the C code is later compiled with C++ (or nothing).