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.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All API_BREAK cases ยท Category: API Break.