A C library's 1.0 is a binary contract, not a feature freeze. Hide the layout, or old programs keep loading and start meaning the wrong thing.
Semver is a label in a changelog. The loader does not read it. A caller that compiled against a public struct already baked your field offsets into its instructions.
libghostty-vt 1.0 is the example worth stealing. Mitchell Hashimoto pulled the terminal emulator out of Ghostty and marked the C ABI stable. The terminal is the excuse. The contract is the release.
Every object is an opaque handle. Callers never see the fields, so the library can reshuffle them. Hundreds of knobs go through a few enum-keyed getters and setters. A new knob is a new enum value, not a new exported function.
Newer headers against an older libghostty return GHOSTTY_UNSUPPORTED. That is a result. A crash means the old library met a value it does not have. A quiet success means it pretended.
The few structs that stay public carry their own size, so a field can be appended. Some keep reserved padding instead. Enums are pinned to int. "The compilers will probably agree" is not an ABI.
On ELF, exported symbols are versioned. The dynamic loader refuses a mismatched library by name when the process starts. You hear about it immediately, not as a mystery failure later.
CI is the part that makes the promise real. Linux diffs the binary against a committed baseline with libabigail. macOS and WebAssembly check golden type manifests. Reorder a field and the build fails.
Insert one field in the middle of a public config struct. Every caller that stack-allocated the old shape now lines its bytes up with your new fields. Opaque, or size-prefixed and append-only, keeps the old binary meaning what it meant. The new field exists for callers who pass the new size, or who call the getter.
Skip these:
- A public struct and a 1.1, because you "only added one field."
- A new function per knob. The symbol table becomes the changelog, and old loaders still do not know the names.
- Success when the running library is older than the headers the caller compiled with.
- A semver badge and no ABI check in CI.
- One unversioned SONAME, with a bug report as the compatibility test.
If you cannot add a field without relinking the world, you do not have a stable C library. You have a header people copied.
This post was generated automatically from public Hacker News data.