Skip to content

case156_public_macro_removed โ€” Public macro removed

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

Category: API Break | Verdict: ๐ŸŸ  API_BREAK

Verdict and consumer impact

The public header macro DEMO_MAX_ITEMS is present in v1's source-replay surface and gone from v2. Macros are a preprocessor construct โ€” they never reach the binary โ€” so removing one changes nothing an artifact-only check can see, but any consumer source that referenced DEMO_MAX_ITEMS (a constant, a feature guard, a function-like macro invocation) fails to compile against v2's headers. This is a source/API break: recompilation against v2 is not just required, it's impossible without a code change.

Old/new diff

include/demo/config.h โ€” v1 include/demo/config.h โ€” v2
#define DEMO_MAX_ITEMS 64 (removed)

The exported binary symbol (demo::keep) is unchanged between v1 and v2; only the header-level macro surface differs.

abicheck command

The case ships old.json/new.json as hand-built SourceAbiSurface fixtures (the normalized L4 model dump --sources would produce from a real source-replay pass) rather than compiled binaries, so reproducing the finding means embedding each side's fixture into a snapshot's build_source field โ€” exactly what dump --sources does internally โ€” and comparing the two:

python3 - <<'PY'
import json
from pathlib import Path
from abicheck.model import AbiSnapshot
from abicheck.buildsource.pack import BuildSourcePack
from abicheck.buildsource.source_abi import SourceAbiSurface
from abicheck.serialization import save_snapshot

for side in ("old", "new"):
    d = json.loads(Path(f"{side}.json").read_text())
    snap = AbiSnapshot(library="libdemo.so", version="1")
    snap.build_source = BuildSourcePack(root=Path(""), source_abi=SourceAbiSurface.from_dict(d))
    save_snapshot(snap, f"{side}.abi.json")
PY

abicheck compare old.abi.json new.abi.json

Expected abicheck finding

Verdict: API_BREAK (exit 2)

Source-Level Breaks:
- public_macro_removed: Public macro 'DEMO_MAX_ITEMS' was removed from the headers.
  ('64' -> '') โ€” include/demo/config.h:12 [L4_SOURCE_ABI]
  > Source that referenced it no longer compiles; there is no binary
    footprint, so only source replay sees it.

Minimum evidence

min_evidence: L4 โ€” only the per-TU source-replay surface records public macros. Macros never reach the binary, so no artifact layer (L0 symbols, L1 DWARF, L2 header AST as parsed for types/decls) sees the removal; L4 source replay is the floor.

Why abicheck catches it

abicheck compare reads each side's normalized SourceAbiSurface's reachable_source_surface.macros (as embedded by dump --sources, or supplied out-of-band via --old/new-sources) and diffs the macro set directly โ€” the same diff_source_abi() routine tests/test_l3l4l5_examples.py exercises against the committed fixtures. Per ADR-028 D3 a source-evidence finding never decides a shipped-ABI break on its own, but public_macro_removed is registered as an API_BREAK_KINDS default verdict (a source-level break), not a risk signal โ€” the compile failure it causes is deterministic, not contingent on a further artifact diff.

Runtime failure demonstration

There's no compiled app.c consumer for this case โ€” the failure happens at compile time, not runtime, so a "swap the .so and observe a crash" demo doesn't apply here. Picture a downstream package that does #if DEMO_MAX_ITEMS > 32 or simply uses DEMO_MAX_ITEMS as an array bound; after upgrading to v2's headers, that translation unit fails to compile with 'DEMO_MAX_ITEMS' undeclared. This is exactly what a CI job running source replay (L4) over a consumer's include path โ€” or over the library's own public headers on each PR โ€” would catch before the header ships, well before any consumer attempts to build against it.

Safe redesign

Keep a compatible macro in place (optionally #define-forwarding to a replacement constant or enum), or document the removal with a migration path and a deprecation window before dropping it โ€” the same discipline applied to removing a public symbol, since the compile-time contract is just as binding on consumers.

Cross-tool comparison

abidiff/abi-compliance-checker operate on built binaries and their debug info; macros are stripped by the preprocessor before either format exists, so neither tool can see this removal under any invocation โ€” there is nothing to run. Only the L4 source-replay layer that abicheck reads directly (or the header AST as parsed by a full C/C++ frontend) preserves the macro-removal fact at all.


Source files

  • new.json
  • old.json

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