Skip to content

Case 26b: Union Field Added (No Size Change)

Field Value
Verdict ๐ŸŸข COMPATIBLE
Category Addition (Compatible)
Platforms Linux, macOS, Windows
Flags โ€”
Detected ChangeKinds union_field_added
Source files examples/case26b_union_field_added_compatible/

Category: Type Layout | Verdict: ๐ŸŸข COMPATIBLE

Verdict and consumer impact

union Value gains an int i member, but int (4 bytes) is smaller than the union's existing largest member, double d (8 bytes). sizeof(union Value) stays 8 bytes in both versions, every existing member keeps its offset-0 layout, and fill() still writes through v->l in both versions. A caller compiled against v1 needs no recompilation โ€” its allocations are already the right size for v2.

Old/new diff

old/lib.h new/lib.h
union Value { long l; double d; }; union Value { long l; double d; int i; };

abicheck command

gcc -shared -fPIC -g old/lib.c -Iold -o libval_v1.so
gcc -shared -fPIC -g new/lib.c -Inew -o libval_v2.so
abicheck compare libval_v1.so libval_v2.so

Expected abicheck finding

Verdict: COMPATIBLE (exit 0)

Additions:
- union_field_added: Union field added: Value::i (int)
  > Union size may grow; old code allocating with old sizeof gets
    truncated data.
  Affected symbols: fill

Minimum evidence

min_evidence: L1 โ€” DWARF's DW_TAG_union_type carries both the new member and the union's total byte size; abicheck sees the new member (via the member list) but reports no type_size_changed, because DWARF's size field is identical (8 bytes) in both versions. No public headers required.

Why abicheck catches it

abicheck diffs the two unions' member lists from debug info the same way it does in case26, finding i as a new member โ€” but it separately compares the DWARF-reported total size, which is unchanged here (max(8, 8, 4) == 8). A field addition alone, with no size change, cannot under-allocate a caller, so it's classified as a compatible addition rather than type_size_changed.

Runtime failure demonstration

No observable effect on existing binaries โ€” fill() still writes through v->l in both versions, and sizeof(union Value) is unchanged, so an app built against v1 behaves identically against v2 with no recompilation.

# Build old library + app
gcc -shared -fPIC -g old/lib.c -Iold -o libval.so
gcc -g app.c -Iold -L. -lval -Wl,-rpath,. -o app
./app
# โ†’ after fill: v.l = 42
# โ†’ OK: union field added (no size change) is ABI-compatible

# Swap in new library (no recompile)
gcc -shared -fPIC -g new/lib.c -Inew -o libval.so
./app
# โ†’ after fill: v.l = 42
# โ†’ OK: union field added (no size change) is ABI-compatible   (identical)

Safe redesign

This is already the safe way to extend a union: only add members whose size is no larger than the union's current largest member, so sizeof() never changes. Contrast with case26 (union_field_added, BREAKING), where the new member is the largest and grows the union.

Cross-tool comparison

abidw --out-file v1.xml libval_v1.so
abidw --out-file v2.xml libval_v2.so
abidiff v1.xml v2.xml

References


Source files

  • CMakeLists.txt
  • app.c

See also: Examples overview ยท All COMPATIBLE cases ยท Category: Addition (Compatible).