Skip to content

Case 124: Header Constant Value Changed

Field Value
Verdict ๐ŸŸ  API_BREAK
Category API Break
Platforms Linux, macOS, Windows
Flags API break
Detected ChangeKinds constant_changed
Source files examples/case124_header_constant_value_changed/

Category: API Break | Verdict: ๐ŸŸ  API_BREAK

Verdict and consumer impact

audio::kMaxChannels is a namespace-scope constexpr int, so it has no exported symbol โ€” the value is baked into every consumer at compile time (float buf[kMaxChannels]). A binary built against v1 carries 8; one built against v2 carries 16. Mixing them โ€” an old consumer loaded against the new library, or two translation units built against different header snapshots โ€” produces disagreeing buffer sizes. Because there is no symbol, the .so files themselves are byte-identical, so recompiling against the new header is the only way to pick up the new value; this is a pure source/API break, not a runtime ABI break.

Old/new diff

v1.h v2.h
constexpr int kMaxChannels = 8; constexpr int kMaxChannels = 16;

abicheck command

g++ -shared -fPIC -g v1.cpp -o libaudio_v1.so
g++ -shared -fPIC -g v2.cpp -o libaudio_v2.so
ABICHECK_AST_FRONTEND=clang abicheck compare libaudio_v1.so libaudio_v2.so \
    --header old=v1.h --header new=v2.h

Expected abicheck finding

Verdict: API_BREAK (exit 2)

- constant_changed: Preprocessor constant value changed:
  audio::kMaxChannels ('8' -> '16')
  > A public const/constexpr value baked into every consumer at compile
    time changed; mixing binaries built against different header
    snapshots disagrees on the value.

Minimum evidence

min_evidence: L2 โ€” the constant has internal linkage and emits no ELF symbol or DWARF entry, so the object files carry no trace of it at all (abicheck compare with no headers reports NO_CHANGE). Only the public header's AST records the constexpr initializer, so this case needs L2 header evidence to detect at all. castxml is abicheck's default L2 backend; here the run above uses the clang AST frontend (ABICHECK_AST_FRONTEND=clang) as a supported alternative โ€” either backend surfaces the initializer.

Why abicheck catches it

The dumper extracts const/constexpr initializers declared in the provided public headers (not ones pulled in transitively), from the init attribute the AST backend records for the declaration โ€” so the finding is scoped to the public contract by construction. Plain #define macro constants remain invisible to this path (the AST backends emit no macro table).

Runtime failure demonstration

Severity: not a runtime crash โ€” a silent baked-in disagreement

Because kMaxChannels has no symbol, the two .so files are ABI-identical: swapping the library binary alone changes nothing observable.

# Build old library + app (baked-in value: 8)
g++ -shared -fPIC -g v1.cpp -o libaudio.so
g++ -g app.cpp -I. -L. -laudio -Wl,-rpath,. -o app
./app
# -> channels=8 mixed=8

# Swap in new library (no recompile) -- byte-identical ABI, no crash:
g++ -shared -fPIC -g v2.cpp -o libaudio.so
./app
# -> channels=8 mixed=8   (unchanged: the value was baked into app, not the .so)

# Recompile app against the new header instead:
g++ -g -DUSE_V2 app.cpp -I. -L. -laudio -Wl,-rpath,. -o app
./app
# -> channels=16 mixed=16

Why this matters anyway: a library swap alone is silent, which is what makes this dangerous. The real failure shows up when two components in the same process (or two ends of a shared-memory/serialization boundary) are compiled against different header snapshots โ€” one baked-in 8, the other 16 โ€” and disagree about a buffer's size with no compiler or linker error to catch it.

Safe redesign

Treat public compile-time constants as part of the ABI contract: avoid changing their values across compatible releases, or expose the value through an exported accessor function (int max_channels();) so consumers read it at runtime instead of baking it in.

Cross-tool comparison

abidiff/abidw operate on the compiled ELF and DWARF debug info only โ€” they have no header/AST layer, so a constexpr value with no emitted symbol is invisible to them the same way it is to abicheck's own object-only mode. Only a header-aware comparison (abicheck's L2 mode) sees this class of change at all.


Source files

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

See also: Examples overview ยท All API_BREAK cases ยท Category: API Break.