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 browse on GitHub

Category: Type System | Verdict: ๐Ÿ”ด BREAKING (Scenario 1: typedef_base_changed) / ๐ŸŸก API_BREAK (Scenarios 2-3)

What changes

Symbol / Type v1 v2 Effect
dim_t typedef int dim_t typedef long dim_t Size 4 โ†’ 8 bytes (LP64)
handle_t typedef unsigned int handle_t (removed) Source break
struct Context Complete (id, flags, name[32]) Forward declaration only Opaque โ€” no stack alloc

Why this IS a source/ABI break (details depend on usage)

  1. TYPEDEF_BASE_CHANGED (dim_t): The return type of get_dimension() changes from int (4 bytes, returned in lower 32 bits of %eax) to long (8 bytes, full %rax). Callers compiled against v1 treat the return as int and may truncate or misinterpret the value. If dim_t is used in structs, their layout changes silently.

  2. TYPEDEF_REMOVED (handle_t): Code using handle_t will not compile against v2. At the binary level create_handle() still exists (returns unsigned int), so already-compiled binaries continue to link. This is a source-only break.

  3. TYPE_BECAME_OPAQUE (struct Context): v1 exposes the full struct definition, allowing stack allocation and direct field access. v2 provides only a forward declaration. Existing binaries that stack-allocate Context or access its fields inline will silently corrupt memory if the internal layout ever changes.

Code diff

-typedef int dim_t;
+typedef long dim_t;

-typedef unsigned int handle_t;
+/* removed */

-struct Context {
-    int id;
-    int flags;
-    char name[32];
-};
+struct Context;   /* opaque forward declaration */

Real Failure Demo

Severity: HIGH

Scenario: Compile app against v1 headers, then swap in the v2 .so without recompiling.

# Build v1 library + app
gcc -shared -fPIC -g v1.c -o libfoo.so
gcc -g app.c -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ Scenario 1 โ€” dim_t base type change:
# โ†’   sizeof(dim_t) at compile time = 4 (expected 4 for int)
# โ†’   get_dimension(7) = 7
# โ†’   If v2 lib loaded: dim_t is long (8 bytes) but caller expects int (4 bytes)
# โ†’
# โ†’ Scenario 2 โ€” handle_t typedef removed:
# โ†’   create_handle() = 42
# โ†’   Binary still works (function exists), but recompilation against v2.h fails
# โ†’
# โ†’ Scenario 3 โ€” struct Context became opaque:
# โ†’   sizeof(struct Context) at compile time = 40
# โ†’   Stack-allocated Context: id=99 flags=0x1 name="stack-ctx"
# โ†’   With v2 header this code would NOT compile (incomplete type)

# Swap in v2 library (no recompile of app)
gcc -shared -fPIC -g v2.c -o libfoo.so
./app
# โ†’ Output looks identical โ€” but dim_t return is now 8 bytes wide;
# โ†’ the caller only reads 4 bytes. On LP64 this happens to work for
# โ†’ small values but is technically undefined behavior.

Source break verification (recompilation against v2 fails):

gcc -g app.c -I. -include v2.h -L. -lfoo -Wl,-rpath,. -o app_v2 2>&1
# โ†’ error: 'handle_t' undeclared
# โ†’ error: invalid application of 'sizeof' to incomplete type 'struct Context'
# โ†’ error: variable 'local' has initializer but incomplete type

Reproduce with abicheck

gcc -shared -fPIC -g v1.c -o libfoo_v1.so
gcc -shared -fPIC -g v2.c -o libfoo_v2.so
abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml
echo "exit: $?"

How to fix

  • typedef base change: Never change the underlying type of a public typedef. Introduce a new typedef (dim64_t) and deprecate the old one.
  • typedef removal: Keep the old typedef as an alias (typedef unsigned int handle_t;) until the next major SONAME bump.
  • opaque transition: Provide the full struct definition in a separate "internal" header and only expose the opaque pointer in the public API from the start.

References


Source files

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