Skip to content

Case 28: Typedef and Opaque Type Changes

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds type_became_opaque
Source files examples/case28_typedef_opaque/

Category: Type System | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

Three related changes ship together in this header: dim_t's underlying type changes (int โ†’ long), the handle_t typedef is removed (so create_handle()'s return type changes from handle_t to plain unsigned int), and struct Context's full definition is hidden behind a forward declaration. Any caller compiled against v1 that treats get_dimension()'s result as a 4-byte int reads a truncated/misinterpreted value once the library is upgraded โ€” the underlying representation grew to 8 bytes. Recompilation is required either way: the typedef and return-type changes are binary-incompatible, and code using the now-removed handle_t name or stack-allocating Context won't even compile against v2.

Old/new diff

v1.h v2.h
typedef int dim_t; typedef long dim_t;
typedef unsigned int handle_t; handle_t create_handle(void); (typedef removed) unsigned int create_handle(void);
struct Context { int id; int flags; char name[32]; }; struct Context; (forward declaration only)

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

Expected abicheck finding

Verdict: BREAKING (exit 4)

- typedef_base_changed: Typedef base type changed: dim_t (int -> long int)
  > Underlying type changed; old code using the typedef operates on wrong
    representation.
  Affected symbols: get_dimension
- func_return_changed: Return type changed: create_handle (handle_t -> unsigned int)
  > Callers expect the old return type layout in registers/stack;
    misinterpretation causes data corruption.
- typedef_removed: Typedef removed: handle_t (unsigned int)
  > Old code using the typedef name won't compile; binary impact depends
    on usage.

Quality issues:
- imported_symbol_removed: Imported symbol no longer required: memset@GLIBC_2.2.5

Minimum evidence

min_evidence: L1 โ€” DWARF already carries enough to reach BREAKING here: dim_t's DW_AT_type changes from int to long in v2's debug info, and create_handle's DW_TAG_subprogram return type changes accordingly, both visible without public headers. The struct Context opacity transition itself (full definition โ†’ forward declaration) is a header-surface fact rather than a binary-layout one โ€” the internal struct definition still exists inside v2.c and is fully described in DWARF โ€” so confirming it specifically needs header/AST evidence (abicheck's default AST backend is castxml; clang is a supported alternative frontend via --ast-frontend clang). The typedef/return-type changes above are enough on their own to reach the correct BREAKING verdict at L1.

Why abicheck catches it

DWARF's type-die graph links dim_t's typedef DIE to its underlying-type DIE for both binaries; abicheck compares the resolved underlying type directly and reports a size/representation change. The same graph exposes each function's return-type DIE, catching the handle_t โ†’ unsigned int change on create_handle, and the disappearance of the handle_t typedef DIE itself between the two snapshots.

Runtime failure demonstration

Severity: HIGH

Scenario: compile app against v1 headers (dim_t = int), swap in the v2 .so without recompiling.

# Build old library + app
gcc -shared -fPIC -g v1.c -o libfoo.so
gcc -g app.c -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ get_dimension(5) = 5

# Swap in new library (no recompile)
gcc -shared -fPIC -g v2.c -o libfoo.so
./app
# โ†’ get_dimension(5) = 6
# โ†’ WRONG RESULT: typedef underlying type changed (int -> long)

Why HIGH: v2's get_dimension returns a long (axis + 1, deliberately offset here to make the mismatch visible) but the app still reads the result as a 4-byte int; on this LP64 platform the truncated value happens to differ from the caller's expectation, demonstrating that the ABI contract for the return value's width and representation no longer matches what the caller was compiled against.

Source break verification (recompiling against v2 fails outright, for the two purely source-level issues):

gcc -g app.c -I. -include v2.h -L. -lfoo -Wl,-rpath,. -o app_v2
# โ†’ error: use of undeclared identifier 'handle_t' (wherever it's referenced)
# โ†’ error: variable has incomplete type 'struct Context' (stack allocation)

Safe redesign

  • Typedef base change: never change the underlying type of a public typedef; introduce a new typedef (e.g. dim64_t) and deprecate the old one instead.
  • Typedef removal: keep the old typedef as an alias (typedef unsigned int handle_t;) until the next major SONAME bump.
  • Opaque transition: design the public API with an opaque pointer from the start (full struct definition lives only in an internal header), so there's never a compatible-looking "complete struct" period for callers to depend on.

Real-world example: dimension/size typedefs in numeric and graphics libraries (e.g. widening an index type from 32-bit to 64-bit for large datasets) are a recurring source of this exact break โ€” silent truncation instead of a compile error, because the symbol name and calling convention otherwise look unchanged.

Cross-tool comparison

abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml
echo "exit: $?"

References


Source files

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

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.