Part 0 — Compatibility as a Product Contract¶
Series navigation: 0. Product Contract · 1. Foundations · 2. Symbol Contracts · 3. Type Layout · 4. C++ ABI · 5. Linker & ELF · 6. Transitive Breaks · 7. Designing for Stability · 8. Detecting Breaks
What you'll learn on this page
- Why ABI/API compatibility is a promise the product makes, not just a property a tool reads out of a binary.
- How to write down your public surface — the thing the promise is about — before you ever run a checker.
- How Semantic Versioning turns that promise into a version-number convention, and how abicheck's verdicts map onto SemVer decisions.
- Why the same technical change can be a release-blocking break for one product and a non-event for another.
This is the prologue to the seven-part series. The later parts teach the mechanisms (what bytes move, what the loader does). This part teaches the framing that makes those mechanisms matter: a change is only a "break" if it breaks something you promised.
New here? If you want the build/link/load mental model first, you can read Part 1 — Foundations and come back. But most of the confusion people have about ABI tools ("why did it flag this? why didn't it flag that?") dissolves once the contract is written down — so start here if you can.
1. The core idea: detection finds facts, the product decides breakage¶
abicheck — like every ABI/API tool — gathers evidence (symbols, type
layout, headers, dependencies) and reports facts: "function foo was
removed", "struct S grew by 8 bytes", "the SONAME changed". Whether a given
fact is a break is a separate question, and it is not a property of the
binary. It is a property of the contract the product published.
Detection finds facts. Policy decides whether those facts are breaking for this product.
A worked example: abicheck reports func_removed for a symbol that disappeared.
Is that a break?
- If the symbol was part of your promised public API → yes, existing consumers will fail to link or load. Breaking.
- If the symbol was an internal helper that merely happened to be exported (no visibility annotation, no version script) → it was never part of the contract. Removing it is housekeeping, not a break — even though the symbol table changed.
The tool sees the same fact in both cases. Only the contract distinguishes them. This is why abicheck has policy profiles and a public-surface scoping model: they are how you tell the tool what your contract actually is.
2. Define the public surface before you check¶
Before checking ABI/API stability, write down what is actually promised. The public surface is the union of:
| Surface element | What it pins | Where abicheck sees it |
|---|---|---|
| Public headers | The source-level API: function signatures, types, macros, default arguments | Header AST (CastXML), if you pass --old-header/--new-header |
| Exported symbols | The link/load-level ABI: which names a consumer can bind to | ELF .dynsym / PE export table / Mach-O export trie |
| Struct/class layout exposed in headers | Field offsets, sizes, alignment that consumers bake in | DWARF/PDB debug info |
Plugin / dlopen entry points |
The dynamic-loading contract between host and plugin | Plugin manifest |
| Supported platforms & architectures | Which ABIs you ship (x86-64, arm64, …) | Per-binary; compared per-platform |
| Supported compilers & standard-library ABI | e.g. the libstdc++ dual-ABI flag, MSVC version range | Build context / toolchain flags |
| Calling conventions & exception model | How calls and unwinding are wired | DWARF / mangling |
| SONAME / install-name policy | When the soname bumps (and consumers must relink) | ELF SONAME / Mach-O install name |
| Symbol-version policy | Which versioned symbols are promised stable | ELF symbol versions (GLIBC_2.x-style) |
| Source-compatibility promise | Whether recompiling against new headers must keep working | Policy choice (see verdicts) |
The single most useful sentence in your project's docs
"Our public API is everything declared in
include/foo/*.hand exported withFOO_PUBLIC. Everything underdetail/or not markedFOO_PUBLICis private and may change at any time."
With that sentence written down, most "is this a break?" arguments answer themselves — and you can tell abicheck the same thing via public-surface scoping and suppressions.
If you don't write this down, the default contract is brutal: everything you
export is part of the ABI, because some consumer somewhere may have bound to
it. That is exactly why accidental exports (missing -fvisibility=hidden, no
version script) are a recurring source of "we broke an ABI we didn't know we
had" — see Part 5 — Linker & ELF.
3. Semantic Versioning: turning the promise into a number¶
SemVer says a project must declare a public API, and then the version number communicates compatibility:
- MAJOR — incompatible API/ABI changes.
- MINOR — backward-compatible additions.
- PATCH — backward-compatible bug fixes.
That maps cleanly onto abicheck's verdicts — but only after the public API is declared (§2). abicheck detects the change and classifies it; you decide what the classification means for your version number and release.
abicheck verdict → SemVer action¶
| abicheck verdict / class | Product meaning | Typical SemVer action |
|---|---|---|
BREAKING |
Existing binary consumers may fail to link, load, or behave correctly | Major bump; SONAME/install-name bump, new symbol version, or block the release |
API_BREAK |
Source users may fail to recompile, but already-built binaries may still load | Major bump if source compatibility is promised; otherwise a documented source migration |
COMPATIBLE (addition) |
Existing users keep working; new public API added | Usually minor bump |
COMPATIBLE_WITH_RISK |
ABI likely intact, but a deployment/security/runtime assumption changed | Usually a release note + policy review; sometimes block |
NO_CHANGE |
No relevant public-contract change detected | Patch / implementation-only release |
| Internal / private change | No public-contract change if truly hidden | No SemVer impact |
abicheck's
comparemode is the only one with the full verdict vocabulary — in particular theAPI_BREAKdistinction between source breaks and binary breaks. Legacycompatmode and other tools generally collapse that distinction. See Verdicts and Tool Comparison.
The same change, two verdicts¶
Because breakage is contract-relative, the same technical change can land in different rows above depending on policy:
- Making a conversion constructor
explicitis anAPI_BREAK(old source that relied on the implicit conversion won't compile) but not a binary break (mangled names and layout are unchanged). Under a strict source-compatible SDK contract that's a major bump; under a binary-only plugin contract it may be acceptable. abicheck'ssdk_vendorvsplugin_abipolicies encode exactly this difference.
4. Name your contract shape¶
"Public surface" looks different for different kinds of products. Identify which shape you are before reasoning about breaks.
Traditional C shared library¶
The contract is typically: public headers + exported symbols + struct layout exposed in headers + SONAME/symbol-version policy + the supported platform ABI. Already-built consumers must keep linking, loading, and calling into the new binary using the old contract. This is the case abicheck models most directly — there is a real binary boundary to compare.
C++ SDK¶
Everything above, plus: supported compiler version range, standard library
ABI (e.g. the libstdc++ dual-ABI flag — see
case104), exception model,
RTTI, visibility rules, inline-namespace policy, template instantiation policy,
and toolchain flags. C++ contracts are wider and more fragile;
Part 4 — C++ ABI covers the mechanisms.
Plugin / SDK with dlopen¶
A two-sided ABI contract between host and plugin: fixed entry points,
dlopen/dlsym names, callback structs, registration functions, and
host/plugin ownership & lifetime rules. This is usually a manually declared
dynamic-loading contract, not ordinary link-time ABI — so abicheck checks it
against a plugin manifest.
Multi-library bundle / product release¶
The contract is product-level: not just whether each .so changed, but
whether the collection still satisfies all intra-bundle dependencies, provider
relationships, entry points, symbol versions, and manifest promises. Per-library
comparison is necessary but insufficient — see
Part 6 — Transitive Breaks and
Multi-Binary Releases.
Rule of thumb: For products that ship more than one public or semi-public library, per-library compatibility is necessary but not sufficient. The product contract is the bundle contract.
5. Where this leaves you¶
You now have the framing the rest of the series builds on:
A product declares a compatibility contract → abicheck gathers evidence from binaries, headers, debug info, applications, bundles, and manifests → policy maps the detected facts onto a release decision.
Carry these two questions into every later part:
- Was the thing that changed part of the promised public surface? (§2)
- What does my versioning policy say I must do about a change of this class? (§3)
Next: Part 1 — Foundations shows how a change becomes a break at the machine level. If you want to know which evidence abicheck (or any other tool) needs to even see a given change, read Evidence & Detectability.
See also: Verdicts · Policy Profiles · Evidence & Detectability · Examples Encyclopedia.