Skip to content

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

abidw --out-file v1.xml liblib_v1.so
abidw --out-file v2.xml liblib_v2.so
abidiff v1.xml v2.xml

abidiff/abidw are not installed in this environment, so no output or exit code is reproduced here.

References


Source files

  • CMakeLists.txt
  • app.cpp
  • v1.cpp
  • v1.h
  • v2.cpp
  • v2.h

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.