Skip to content

Case 119: Internal Struct Loses a Field (Non-Public, Scoped)

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

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

Verdict and consumer impact

struct InternalStats drops a field (int errors;) between v1 and v2. Removing a field is normally an ABI break for that struct — but InternalStats is declared in the header only for other translation units inside the library; no exported function reaches it. The public API (Point, translate()) is unchanged, so no existing binary that uses only the public surface is affected.

Old/new diff

v1.h v2.h
struct InternalStats { int calls; int errors; }; struct InternalStats { int calls; };
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_removed: InternalStats (non-public-type)

Dropping --scope-public-headers reports the InternalStats field removal as an ordinary breaking change instead of filtering it — the right mode when auditing the full 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) has no notion of which structs are reachable from an exported declaration and which aren't. 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 resolves the public surface — exported symbols plus their reachable type closure — and evaluates InternalStats's field removal against that closure (ADR-024). Because InternalStats is never reached from translate() or any other exported declaration, the removal is routed to the filtered/audit ledger and the verdict stays NO_CHANGE. Internal-type leaks are never hidden this way — a type reachable from a public API stays on the surface and a field removal there is reported as breaking, same as case07's struct-size case.

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 lets them shrink or reshape freely without triggering false-positive ABI-break reports, as long as they truly stay unreachable from any exported declaration.

Cross-tool comparison

abidiff/ABICC have no equivalent of ADR-024 public-surface scoping — both 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 removal as a breaking change 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.