Compatibility Direction¶
Most of this documentation set — and most of what abicheck compare checks
by default — assumes one specific scenario: an old, already-compiled
consumer running against a new version of the library. That is the
right default (it's the scenario every ordinary in-place upgrade creates),
but it is one of several distinct directions a compatibility question can
run in, and conflating them is a common source of a check passing when the
real deployment scenario it needed to cover was a different one.
Naming the directions¶
| Direction | The scenario | Typical trigger |
|---|---|---|
| Backward (the default) | Old consumer, new library | An in-place library upgrade; the consumer wasn't rebuilt |
| Forward | New consumer, old library | A downgrade/rollback; or a consumer built against a newer SDK than what's deployed |
| Host-forward | Old plugin, new host | A plugin ecosystem where hosts update faster than every plugin |
| Host-backward | New plugin, old host | A plugin built against a newer SDK loaded into an older, still-deployed host |
| Header/binary skew | Old headers, new binary (or the reverse) | A consumer's build system pins headers independently of the binary it links against |
| Coexistence | Two versions loaded in one process, or in communicating processes | A plugin ecosystem, a monorepo with mixed build times, a rolling deployment |
Each row is checking the same underlying contract from a different vantage
point, and — this is the part worth internalizing — a change can be safe
in one direction and unsafe in another. A newly added optional
parameter with a default is safe, for an ordinary call expression, in old
source recompiled against new headers — though not universally: it changes
the function's type, so source taking its address for a function pointer
(void (*p)(int) = &f; against f(int, int = 0)) stops compiling even in
that same direction. Note that this source-level safety is not the same
as backward compatibility in this table's sense (old binary, new library):
if the one-parameter overload is replaced rather than kept alongside the new
one, the new library no longer exports the mangled symbol an
already-compiled consumer references, and that is a hard backward binary
break — the recompile-safety and the binary contract are separate answers. In the
forward direction, the break isn't limited to source that names the new
argument — it's a link-time failure, not just a compile-time one:
f(int, int = 0) is a different function type from f(int), so it
normally mangles to a different symbol. New source compiled against the
new declaration — even an ordinary, old-form call like f(1), which the
compiler silently expands with the default argument — compiles cleanly,
but linking that new consumer against an old library that only exports
the one-parameter symbol fails outright, because the two-parameter symbol
the new consumer needs was never exported. Code that explicitly supplies
the new argument or takes the function's address fails earlier still, at
compile time, against genuinely old headers that never declared the
parameter at all (a distinct scenario from linking against an old
library with new headers, above). A newly
added exported symbol is invisible to an old, already-linked consumer
(the backward direction is unaffected — nothing calls a symbol that didn't
exist when it was built) but breaks the forward direction the moment a
new consumer, compiled against headers that declare it, gets linked
against an older library build that never exported it — exactly the
scenario a downgrade or a rollback creates.
Why the default direction is the right default, and when it isn't enough¶
Backward compatibility (old consumer, new library) is the right default
because it's what an ordinary "ship a point release, users update the
library" workflow needs, and it's the direction every ABI/API stability
convention (SONAME bumps, symbol versioning) is built around — see
Part 5 — Linker & ELF. abicheck compare
old.so new.so checks exactly this direction: can the old binary's
callers (whose expectations are frozen in the old snapshot) still be
satisfied by the new library.
It stops being enough the moment your deployment model allows any of these:
- Downgrades or rollbacks are a supported operation — an incident response that reverts a bad deploy needs the forward direction to also hold, or the rollback itself becomes a second incident.
- You ship a plugin ecosystem — third-party plugins are compiled at
different times against different SDK versions, and both host-forward and
host-backward compatibility matter depending on whether hosts or plugins
update first in practice. See Plugin Systems
for the two consumer-scoped checks that together cover one specific
plugin/host pair:
compare --used-byscopes the comparison to a consumer's actual, statically-linked imports, whilecompare --required-symbolchecks the host's own required entrypoints — the ones it resolves dynamically viadlopen/dlsymrather than importing at link time, which--used-byalone cannot see. A full plugin/host pair check needs both; either one run alone only covers its own half of the contract, in either direction. - Header and binary versions can diverge — a build system that vendors/pins headers separately from the binary it links (common in large, multi-team builds) needs both header→binary and binary→header checks, since a stale header can silently miscompile against a newer binary and vice versa.
- Multiple versions coexist in one process or communicating processes — a rolling deployment where old and new service instances talk to each other over a shared library's wire format needs simultaneous backward and forward compatibility for the whole rollout window, not just a single before/after comparison. (Where the shared contract is data crossing a boundary rather than binary ABI, see Data & Wire Compatibility — coexistence often turns an ABI question into a data-format one.)
Checking more than one direction¶
abicheck compare takes two snapshots positionally — which one is "old"
and which is "new" is a labeling choice, not a constraint the tool imposes.
Checking the forward direction for a given pair is the same command with
the arguments reversed: abicheck compare new.so old.so asks "can callers
compiled against the new contract be satisfied by the old library" —
useful for validating a rollback path is safe before you need it, not
after. For a --used-by/plugin scenario, reversing the two library
arguments is not by itself enough to check the opposite direction:
--used-by scopes the comparison to the supplied consumer's own actual
imports, so a forward consumer-scoped check needs a consumer binary that
was actually built against the new contract — reusing an old consumer
binary alongside reversed library arguments still only exercises what that
old consumer imports, never a symbol or version a new-SDK consumer would
use. The same applies to --required-symbol: the required-entrypoint set
must match the host contract for the direction under test. See
Plugin Systems for the consumer-scoped
mechanics compare --used-by/--required-symbol build on.
There is no single "check every direction" flag, because which directions
matter is a property of your deployment model, not something abicheck can
infer from two binaries — this is exactly the kind of contract-shape
decision Product Contract §5
asks you to make explicit before reasoning about breaks at all. Naming which
directions your project actually needs (usually: backward always, forward
if rollback is supported, both host directions if you ship plugins) is the
first step; running compare once per direction that matters is the
mechanical follow-through:
abicheck compare old.so new.so -H include/ # backward: old consumers on the new library
abicheck compare new.so old.so -H include/ # forward: the rollback path
See also: Product Contract §5 for naming your contract shape, and Plugin Systems for the consumer-scoped mechanics that make a specific direction checkable against a specific real consumer.
Ladder: ← Part 7 — Designing for Stability · Step 5 · Define Your Contract · Consumer Models →