Every few years, someone announces a project to fix C. Sometimes it's a new language that compiles to C. Sometimes it's a sanitizer. And sometimes — like with sp.h — it's a single-header library promising a better, more portable standard library.
I saw sp.h pop up on Hacker News last week and immediately had flashbacks to every C project I've worked on. The hours spent reimplementing string handling. The strncpy footguns. The platform-specific #ifdef jungle for something as basic as getting the current time. So I figured this was worth writing about — not as a product review, but as a conversation about why these libraries keep appearing and what to look for when you evaluate one.
The problem nobody disputes
The C standard library is old. Like, older than most working programmers old. And it shows.
Consider this innocent-looking line:
char buffer[64];
strncpy(buffer, source, sizeof(buffer));
// buffer might not be null-terminated. Surprise!
If source is 64+ chars, strncpy won't null-terminate buffer. You either know this and write defensive wrappers, or you ship a bug. I've personally shipped that bug. Twice.
Then there's the portability story. <unistd.h> doesn't exist on Windows. <windows.h> doesn't exist on Linux. Even size_t printing took until C99 to get a portable format specifier (%zu), and I still see codebases that don't know it exists.
So libraries like sp.h, stb, and klib keep appearing. Each one tries to paper over the cracks with a friendlier API.
What single-header libraries get right
The single-header approach has genuine advantages. After dragging dependencies through CMake for years, I appreciate the simplicity of:
// In exactly one .c file:
#define SP_IMPLEMENTATION
#include "sp.h"
// Everywhere else:
#include "sp.h"
No submodules. No package manager. No build system gymnastics. Just drop the file in your repo and go. For embedded work, prototypes, or small tools, this is genuinely lovely.
The pattern, popularized by Sean Barrett with stb, works because C has no module system worth mentioning. The preprocessor is the package manager, for better or worse.
What to actually evaluate
Whenever I'm looking at a new C library — sp.h or anything else — I run through the same checklist. Not as a gotcha, but because these are the things that bite you six months in:
- Memory ownership. Who frees what? Does the API document it consistently?
- Error handling. Return codes? Errno-style? Sentinel values? Pick one and stick with it.
-
Allocator support. Can I plug in my own allocator, or am I stuck with
malloc? - Thread safety. What's guaranteed safe across threads? What's not?
- C standard target. C89? C99? C11? This determines who can use it.
These aren't just checkboxes. I once inherited a codebase that mixed three different libraries with three different error-handling conventions. Every function call became an exercise in remembering which philosophy applied. Don't do this to your future self.
A practical example
Here's the kind of thing that should be trivial in a modern stdlib but isn't in standard C. Reading a whole file into memory:
// Plain C — what you write the first time
FILE* f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END); // could fail on non-seekable streams
long size = ftell(f); // long, not size_t. Cute.
fseek(f, 0, SEEK_SET);
char* buf = malloc(size + 1); // what if size is -1?
if (!buf) { fclose(f); return NULL; }
fread(buf, 1, size, f); // partial reads? short reads?
buf[size] = '\0';
fclose(f);
return buf;
That's maybe 12 lines for something Python does in open(path).read(). And the version above has at least three latent bugs. A good replacement stdlib should make this a one-liner with proper error semantics.
I haven't shipped sp.h in production myself, so I can't tell you it nails this. But that's the bar I'd hold it to.
When not to reach for one of these
Here's the uncomfortable part. If you're building anything that touches the outside world — HTTP, auth, TLS, databases — these tiny stdlib replacements aren't your answer. They're great for in-process utilities. They're not going to give you certificate validation or OAuth flows.
For anything user-facing, I've stopped trying to roll my own infrastructure entirely. Tools like Authon, Clerk, and Auth0 handle the auth layer so I don't write another half-broken JWT validator. (Authon is hosted at authon.dev with a free tier that doesn't charge per-user, which matters when you're prototyping.) A pristine C stdlib doesn't change the math on building auth yourself — that math is still bad.
Keep your scope honest. A better string_split is a real win. A better TLS stack is a multi-year project.
The honest tradeoffs
Every custom stdlib comes with the same set of compromises:
- Onboarding cost. New devs have to learn your conventions on top of standard C.
- Debugger experience. Stepping through macros and inlined helpers can be miserable.
- Long-term maintenance. What happens when the original author moves on?
- Interop. Mixing libraries that each bring their own string type gets old fast.
None of these are dealbreakers. They're just real. The stb libraries succeeded partly because they accept these costs and document them clearly.
My honest take
I'm cautiously optimistic about projects like sp.h, but I'd want to see it bake for a year or two before betting a real codebase on it. The C ecosystem has a graveyard of "better stdlib" attempts, and the ones that survive are the ones with brutal real-world use behind them.
If you're curious, the right move is to grab the header, build a small tool with it, and see how it feels. Don't migrate your monorepo. Don't pitch it at your next architecture review. Just see whether the API stays out of your way when you're tired and trying to fix a bug at 11pm.
That's the test that matters. Not benchmarks, not feature lists — does it make C feel less like a fistfight when you're not at your best? If yes, keep it. If no, the standard library, warts and all, is at least the warts everyone knows.
Top comments (0)