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)¶
-
TYPEDEF_BASE_CHANGED (
dim_t): The return type ofget_dimension()changes fromint(4 bytes, returned in lower 32 bits of%eax) tolong(8 bytes, full%rax). Callers compiled against v1 treat the return asintand may truncate or misinterpret the value. Ifdim_tis used in structs, their layout changes silently. -
TYPEDEF_REMOVED (
handle_t): Code usinghandle_twill not compile against v2. At the binary levelcreate_handle()still exists (returnsunsigned int), so already-compiled binaries continue to link. This is a source-only break. -
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-allocateContextor 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.