replicated.wiki
Recently, “to C or not to C” became a topic on HN, which is a nice
excuse to spend couple hours on ABC retrospective. The decision
to work in C was rather natural: the author is a C/Go, not C++/Rust
kind of person, so once go runtime became a problem, C was the
most straightforward answer. The dirty secret of both C++ and C is
that these two are like IKEA or LEGO languages. Languages to create
other languages. For example, virtually any serious C++ user has
some sort of alternative standard library (Abseil, QT, there are
many). You don’t use C or C++ as-is, normally. C standard library
is small by design, so that is inevitable for most use cases.
C++/C standard libraries are sort of a very mixed bag, effectively
a chronicle of CS ideas for the last 40-50 years. If C standard lib
is kind of a manuscript chamber in a faraway monastery, C++ std lib
is more like the Library of Congress. Nobody knows it all, and most
of the ideas written are definitely not recommended today.
Abstractionless C resulted from many frustrations with C++ and its
endless quirks. I needed generics, STL-like containers, disk and
network serialization, some standard algorithms, with no pointer
arithmetics and no malloc/free headaches. Coming from Go, I clearly
needed slices. That was the pragmatic problem statement. On the
higher level, I wanted to avoid the tower-of-abstractions trap that
I felt quite sharply in C++. There, same bytes packaged differently
become an entirely different incompatible story (like std::string
vs std::vector<char> vs std::vector<uint8_t> etc). The fact
that C++ char is neither signed nor unsigned and all those quirks
that sound like a really strange religion – those drive me mad.
So the set of architectural choices was:
u32, i64, sha256, etc).typedef u8* u8s[2]; and a slice is non-owning.void SHA1Sum(sha1* hash, u8csc from) declared in SHA1.h,
implemented in SHA1.c, tested in test/SHA1.c, etc.Slices and generics are a bit unexpected in C, the rest is just another C style, nothing out of the ordinary. The obvious issue here is that C does not support slices in any of its standard APIs. But, the C standard library is not that huge, and its usable part is even less, so unless a function is a syscall or somehow preferentially treated by the compiler, what is the value of it? Diminishingly zero. Especially in the LLM era. What has a lot of value is the toolchain that understands C and the OS kernel. Those are true megaprojects.
So, I sketched some skeleton of my (un)standard lib and started working with it. The “meat” slowly grew, the thing saw one or two refactors along the way, but it mainly remains a collection of small and focused modules with slice-based APIs and increasingly rare malloc use. The cases for malloc go down for the following reasons:
u8cs (two-pointer slice) and the bytes
live in the arena,malloc or something else.Out of remaining burning questions one may mention package and dependendency management. Obviously, for C that is RPM, APT, apk, Brew and so on. I am not going to bring along second copies of CURL, libsodium, and all the other usual suspects.
So for my purposes, it worked out fine. As L.Torvalds once said: “Standards are paper. Buy some and write your own.” Or something like that.