Case 157: Inline Function Removed¶
| Field | Value |
|---|---|
| Verdict | ๐ API_BREAK |
| Category | API Break |
| Platforms | Linux |
| Flags | API break |
Detected ChangeKinds |
inline_function_removed |
| Source files | examples/case157_inline_function_removed/ |
Category: Build/Source Evidence (L4) | Verdict: ๐ API_BREAK
This case ships a hand-built pair of evidence-model fixtures (
old.json+new.jsonโSourceAbiSurfacedumps) instead of compilablev1/v2sources, so the corpus is also validated compiler-free bytests/test_l3l4l5_examples.py(which callsdiff_source_abion the same two files directly). Seescripts/gen_l3l4l5_examples.pyfor how they were generated.
Verdict and consumer impact¶
The public header-only inline function demo::clamp is present in v1 and
removed in v2. Nothing about the shipped .so changes โ an inline function
never had an exported symbol to remove, so the binary is byte-for-byte
compatible at the ELF/DWARF level. The break is purely at the source level:
any translation unit that #includes the header and calls clamp() fails
to compile against the v2 headers, with no runtime signal at all โ recompiling
against v2 is what surfaces the problem, not deploying it.
Old/new diff¶
| v1 source-surface fact | v2 source-surface fact |
|---|---|
include/demo/math.h:20 โ inline demo::clamp(...), public header, body hash sha256:clampv1 |
(removed โ no inline_bodies entry) |
include/demo/keep.h:3 โ demo::keep(), public header (unrelated, unchanged) |
include/demo/keep.h:3 โ demo::keep(), unchanged |
abicheck command¶
# old.json / new.json here are this case's committed L4 source-ABI-replay
# fixtures (SourceAbiSurface dumps, as produced by source-replay extraction
# over a real source checkout) -- not AbiSnapshot files, so they're supplied
# to abicheck as an out-of-band --build-info pack rather than as compare's
# positional inputs.
mkdir -p pack_old/source pack_new/source
echo '{"build_source_pack_version": 1}' > pack_old/manifest.json
echo '{"build_source_pack_version": 1}' > pack_new/manifest.json
cp old.json pack_old/source/source_abi.json
cp new.json pack_new/source/source_abi.json
# Nothing changed at the binary/header level between v1 and v2, so a pair of
# otherwise-empty snapshots stands in for the (unchanged) artifact side.
python3 -c "
from abicheck.model import AbiSnapshot
from abicheck.serialization import save_snapshot
save_snapshot(AbiSnapshot(library='libdemo.so', version='1.0'), 'empty_old.json')
save_snapshot(AbiSnapshot(library='libdemo.so', version='2.0'), 'empty_new.json')
"
abicheck compare empty_old.json empty_new.json \
--build-info old=pack_old --build-info new=pack_new
Expected abicheck finding¶
Verdict: API_BREAK (exit 2)
- inline_function_removed: Public inline function 'demo::clamp' removed
(include/demo/math.h:20 [L4_SOURCE_ABI])
> It had no exported binary symbol, so only source replay sees the
loss; source that called it no longer compiles.
Minimum evidence¶
min_evidence: L4 โ the exported-symbol table (L0), DWARF (L1), and even the
public-header AST (L2) are all blind here: an inline function leaves no
symbol to remove and no size/layout to diff. Only source replay's per-TU
surface โ built by a source extractor (clang, castxml, or the Android
header-ABI dumper) walking the real translation units โ records inline
function bodies as first-class facts, which is what lets diff_source_abi
see one vanish between releases.
Why abicheck catches it¶
SourceAbiSurface.reachable_source_surface.inline_bodies carries one entry
per public inline function reachable from the linked library, keyed by
qualified name; diff_source_abi diffs the two surfaces' inline_bodies
sets directly and reports inline_function_removed for any entry present
in v1 and absent in v2 โ the same mechanism DWARF-based detectors use for
symbols, just at the source-fact layer instead of the binary layer.
Build/deployment scenario¶
This is what a CI job that replays two releases' public headers through a
source extractor (or ingests a captured abicheck_inputs/ pack from the
build) would see the moment a header-only inline utility is dropped: .so
symbol tables and DWARF are identical release over release, so a pipeline
gating only on a binary/DWARF abicheck compare would wave this through as
clean. Only a scan that also collects L4 source-replay evidence over the
public header tree catches that downstream code including <demo/math.h>
and calling clamp() will fail to compile against the new release.
Safe redesign¶
Treat a header-only inline function as part of the public API contract, not
free churn โ deprecate it the same way as an exported symbol
([[deprecated("use new_clamp() instead")]]) for at least one release
before deleting the declaration, and keep the old inline body forwarding to
any replacement so existing translation units keep compiling.
Real-world example: header-only utility libraries commonly keep a deprecated inline shim around specifically to avoid a hard build break for consumers who haven't migrated to the replacement yet.
Cross-tool comparison¶
Neither abidiff nor abi-compliance-checker have an operand here: both
tools diff compiled binaries (plus optional DWARF/headers), and this finding
is โ by construction โ invisible at that layer; the whole reason it needs L4
source-replay evidence is that no built binary or header-AST-only diff can
see an inline function's body disappear. There's no .so/abidw XML pair
to hand either tool for this case.
Source files¶
new.jsonold.json
See also: Examples overview ยท All API_BREAK cases ยท Category: API Break.