Skip to content

Case 158: Public Typedef Removed

Field Value
Verdict ๐ŸŸ  API_BREAK
Category API Break
Platforms Linux
Flags API break
Detected ChangeKinds public_typedef_removed
Source files examples/case158_public_typedef_removed/

Category: Build/Source Evidence (L4) | Verdict: ๐ŸŸ  API_BREAK

This case ships a hand-built pair of evidence-model fixtures (old.json + new.json โ€” SourceAbiSurface dumps) instead of compilable v1/v2 sources, so the corpus is also validated compiler-free by tests/test_l3l4l5_examples.py (which calls diff_source_abi on the same two files directly). See scripts/gen_l3l4l5_examples.py for how they were generated.

Verdict and consumer impact

The public typedef demo::handle_t (an alias for int) is present in v1 and removed in v2. A bare typedef emits no symbol and has no size or layout of its own, so the shipped .so is unaffected โ€” this is not a binary compatibility problem. It is a source compatibility problem: any consumer source that names the alias (a variable declaration, a cast, a template argument) fails to compile against the v2 headers.

Old/new diff

v1 source-surface fact v2 source-surface fact
include/demo/handle.h:8 โ€” typedef demo::handle_t = int, public header, type hash sha256:h1 (removed โ€” no matching types 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)

- public_typedef_removed: Public typedef 'demo::handle_t' removed
  (include/demo/handle.h:8 [L4_SOURCE_ABI])
  > Emitted no symbol of its own, so only source replay sees the
    removal; source that named the alias no longer compiles.

Minimum evidence

min_evidence: L4 โ€” a typedef contributes nothing to the exported-symbol table (L0) or DWARF (L1), and header-AST scoping alone (L2) records the public surface but isn't what this corpus's diff runs against. Only source replay's per-TU surface, built by a source extractor (clang, castxml, or the Android header-ABI dumper) over the real translation units, keeps typedefs as first-class facts โ€” which is what lets diff_source_abi see one vanish.

Why abicheck catches it

SourceAbiSurface.reachable_source_surface.types carries one entry per public type-level declaration reachable from the linked library โ€” including typedefs, keyed by qualified name with the aliased type recorded as value. diff_source_abi diffs the two surfaces' types sets directly and reports public_typedef_removed for a typedef entry present in v1 and absent in v2. (The sibling public_typedef_target_changed kind covers the case where the alias is kept but re-pointed to a different underlying type.)

Build/deployment scenario

This is what a CI job replaying two releases' public headers through a source extractor (or ingesting a captured abicheck_inputs/ pack from the build) would see the moment a public alias is dropped: the .so's symbol table and DWARF don't move at all, so a pipeline gating only on a binary/DWARF abicheck compare would ship this as clean. Only a scan that also collects L4 source-replay evidence over the public header tree catches that downstream code declaring a demo::handle_t variable, or casting to one, fails to compile against the new release.

Safe redesign

Keep the typedef as an alias for its replacement instead of deleting it outright โ€” using handle_t = new_handle_t; costs nothing at the ABI level and keeps existing consumer source compiling. Remove the alias only after a documented deprecation window, or provide a replacement name and update consumers first.

Real-world example: this is exactly how POSIX/glibc handle typedef churn โ€” e.g. width-sensitive _t aliases stay pointed at a new underlying type for years instead of being deleted outright, precisely to avoid this break.

Cross-tool comparison

Neither abidiff nor abi-compliance-checker have an operand here: both tools diff compiled binaries (plus optional DWARF/headers), and a bare typedef removal 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 it. There's no .so/abidw XML pair to hand either tool for this case.


Source files

  • new.json
  • old.json

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