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.txtapp.cv1.cv1.hv2.cv2.h
See also: Examples overview · All NO_CHANGE cases · Category: No Change.