Skip to content

Case 118: Internal Struct Gains a Field (Non-Public, Scoped)

Field Value
Verdict NO_CHANGE
Category No Change
Platforms Linux, macOS, Windows
Flags
Detected ChangeKinds
Source files examples/case118_internal_struct_field_added_scoped/

Category: Public-Surface Scoping (ADR-024) | Verdict: ✅ NO_CHANGE

Verdict and consumer impact

struct InternalStats gains a new field (int errors;) between v1 and v2 — a real layout change to that struct. But InternalStats is declared in the header only for other translation units inside the library; no public, exported function (translate()) takes, returns, or otherwise reaches it. Nothing a consumer can legally depend on — Point/translate() — changed, so no existing binary is affected. Reporting the InternalStats layout change under public-surface scoping would be a false positive.

Old/new diff

v1.h v2.h
struct InternalStats { int calls; }; struct InternalStats { int calls; int errors; };
Point translate(Point p, int dx, int dy); (unchanged) Point translate(Point p, int dx, int dy); (unchanged)

abicheck command

gcc -shared -fPIC -g v1.c -o libfoo_v1.so
gcc -shared -fPIC -g v2.c -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so \
  --header old=v1.h --header new=v2.h \
  --ast-frontend clang --gcc-path "$(command -v clang)" \
  --scope-public-headers --show-filtered

Expected abicheck finding

Verdict: NO_CHANGE (exit 0)

_No ABI changes detected._

Filtered as non-public ABI surface (1 finding, --scope-public-headers):
  - type_field_added_compatible: InternalStats (non-public-type)

--show-filtered is what surfaces the filtered-ledger line above; without it the report shows only the clean NO_CHANGE verdict. Dropping --scope-public-headers entirely reports the InternalStats field addition as an ordinary (compatible) change instead of filtering it — useful when auditing the whole exported surface rather than just the public-header API.

Minimum evidence

min_evidence: L2 — telling InternalStats apart from a genuinely public type requires the public header AST: DWARF alone (L1) sees every struct the compiler emitted debug info for and can't tell which ones are reachable from an exported declaration. castxml is the documented default header/AST backend; this environment used the supported alternative Clang AST frontend (--ast-frontend clang) to produce that evidence, since castxml itself isn't installed here.

Why abicheck catches it (and doesn't report it)

With -H/--header the header AST parser records which declarations are public. abicheck then resolves the public surface — exported symbols plus their reachable type closure — and computes InternalStats's layout change against that closure (ADR-024). Because InternalStats is never reached from translate() or any other exported declaration, the layout change is routed to the filtered/audit ledger instead of the reported findings, and the verdict stays NO_CHANGE. Internal-type leaks (a private type reachable from a public API) are never hidden this way — only changes to types genuinely absent from the public closure are filtered.

Runtime failure demonstration

No observable effect on existing binaries — this is the intended, compatible outcome. Building app.c against v1 and swapping in the v2 .so without recompiling produces identical output both times:

gcc -shared -fPIC -g v1.c -o libfoo.so
gcc -g app.c -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# → translate -> (11, 22)

gcc -shared -fPIC -g v2.c -o libfoo.so   # swap, no recompile
./app
# → translate -> (11, 22)   (unchanged)

Safe redesign

N/A — this is the pattern to follow, not to avoid. Keeping internal bookkeeping types out of the reachable closure of the public API (or moving them out of the installed header entirely, into a private header) is exactly what lets them evolve freely without triggering false-positive ABI-break reports.

Cross-tool comparison

abidiff/ABICC have no equivalent of ADR-024 public-surface scoping — both tools diff every type present in the debug info regardless of reachability from an exported declaration, so a plain abidiff v1.xml v2.xml on these two .so files would report the InternalStats field addition (as a compatible/non-breaking change, since it's an append-only addition) rather than recognizing it as out-of-surface and NO_CHANGE. abidw/abidiff are not installed in this environment, so no such output is reproduced here.


Source files

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

See also: Examples overview · All NO_CHANGE cases · Category: No Change.