Case 97: API Depends on Consumer Environment¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break |
Detected ChangeKinds |
func_removed_elf_only |
| Source files | examples/case97_api_depends_on_consumer_env/ |
Category: Symbol API | Verdict: โ BREAKING
Verdict and consumer impact¶
v1.h declares lib::extended() only when the consumer defines
USE_FEATURE; the fixture's v1 build turns the macro on and exports it,
v2 builds without it. Any consumer that linked against v1's extended()
symbol โ because it (or the library's own default build) had the macro
set โ hits an unresolved symbol against v2's .so, with no header change
required to trigger it: the exported symbol simply disappeared.
Old/new diff¶
| v1.cpp | v2.cpp |
|---|---|
#define USE_FEATURE 1 before including the header โ extended() is declared and defined |
macro not defined โ extended() is neither declared nor defined |
Both v1.h/v2.h are textually identical (#ifdef USE_FEATURE guards
extended() in both); the exported surface differs purely because of
which macro state each build used.
abicheck command¶
g++ -std=c++17 -shared -fPIC -g v1.cpp -o liblib_v1.so
g++ -std=c++17 -shared -fPIC -g v2.cpp -o liblib_v2.so
abicheck compare liblib_v1.so liblib_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- func_removed_elf_only: Elf_only function removed: lib::extended()
> Exported function symbol removed from the binary; old binaries that
link or dlsym() it can fail even without header evidence.
Minimum evidence¶
min_evidence: L0 โ lib::extended() is present in v1's .dynsym and
absent from v2's; the exported-symbol table alone is enough. No headers
are passed here deliberately: the whole point of this case is that the
break is visible purely from what each build actually exported, not from
what the (identical) header declares.
Why abicheck catches it¶
The dynamic symbol table is authoritative L0 evidence โ _ZN3lib8extendedEv
is exported by liblib_v1.so and missing from liblib_v2.so. abicheck
diffs the two exported-symbol sets directly and reports the removal as
func_removed_elf_only (the "elf-only" variant fires because there's no
header/DWARF-declaration evidence backing the symbol, only the export
table itself).
A matrix-aware API_DEPENDS_ON_CONSUMER_ENV finding exists as a separate,
probe-harness-driven detector: when abicheck is invoked with a manifest
that builds and probes both macro states of the same library version, it
diffs the declared-name sets across configurations and flags the
macro-conditioned declaration directly. That harness mode is not exercised
by the plain compare invocation above โ this case's single-config build
already reproduces the underlying symbol-removal break that the matrix
finding is designed to explain.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: compile app against v1 (built with USE_FEATURE), swap in
v2's .so (built without it) without recompiling.
# Build old library + app
ln -sf liblib_v1.so liblib.so
g++ -std=c++17 app.cpp -L. -llib -Wl,-rpath,. -o app
./app
# โ exits 0 (lib::basic(); lib::extended(); both resolve)
# Swap in new library (no recompile)
ln -sf liblib_v2.so liblib.so
./app
# โ ./app: symbol lookup error: ./app: undefined symbol: _ZN3lib8extendedEv
Why CRITICAL: extended() is absent from v2's dynamic symbol table;
the runtime linker cannot resolve it and the process fails to start.
Safe redesign¶
Don't let a library's exported surface depend on which macro state the
library itself was built with while consumers assume a fixed API. Either
always build and ship extended() (documenting it as always-present), or
split it into a genuinely separate, versioned add-on library so its
presence/absence is an explicit linking decision rather than a silent
build-configuration difference. If a feature must remain conditional,
gate it behind a runtime capability check (dlsym + null check) instead
of an unconditional call.
Real-world example: libraries that expose optional accelerated code paths behind a build-time feature macro (e.g. an optional SIMD/GPU path) routinely hit this: a distro that builds without the feature flag silently drops a symbol that packages built elsewhere still expect.
Cross-tool comparison¶
abidiff/abidw are not installed in this environment, so no output or
exit code is reproduced here.
References¶
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.