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.txtapp.cv1.cv1.hv2.cv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.