Case 62: Type Field Added (Compatible โ Opaque Struct)¶
| Field | Value |
|---|---|
| Verdict | ๐ข COMPATIBLE |
| Category | Addition (Compatible) |
| Platforms | Linux, macOS |
| Flags | โ |
Detected ChangeKinds |
func_added |
| Source files | examples/case62_type_field_added_compatible/ |
Category: Addition | Verdict: ๐ข COMPATIBLE
Verdict and consumer impact¶
Session is an opaque handle โ callers only ever hold Session*, never
allocate, embed, or sizeof() the struct themselves. v2 grows the private
struct definition and adds session_get_priority(), but every existing
caller keeps working unmodified: allocation happens inside the library via
session_open(), so the caller-visible surface (the pointer and the
existing accessor functions) is untouched.
Old/new diff¶
| old/lib.c (private struct) | new/lib.c (private struct) |
|---|---|
char name[64]; int timeout; int _reserved0; |
char name[64]; int timeout; int priority; |
| (no accessor) | int session_get_priority(const Session *s); |
abicheck command¶
gcc -shared -fPIC -g -include old/lib.h old/lib.c -o libfoo_v1.so
gcc -shared -fPIC -g -include new/lib.h new/lib.c -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: COMPATIBLE (exit 0)
- func_added: New public function: session_get_priority
> New function available; existing binaries are unaffected.
Quality:
- used_reserved_field: Reserved field put into use: Session::_reserved0 -> priority
Minimum evidence¶
min_evidence: L0 โ session_get_priority appearing in v2's .dynsym is
enough to see the addition; Session is forward-declared only in the public
header, so its private layout is never part of the diffed surface at any
evidence tier, header-based or not.
Why abicheck catches it¶
The dynamic symbol table shows a new exported function name in v2's
.dynsym that isn't in v1's โ a pure L0 addition. Session's private
struct layout would only be visible to abicheck if it appeared in a public
header's type declaration; here it never does, so no type-level finding is
possible or needed to justify the COMPATIBLE verdict.
Runtime failure demonstration¶
No observable effect on existing binaries โ the opaque-pointer pattern is exactly what makes growing the private struct safe.
# Build old library + app (app.c only ever holds Session*)
gcc -shared -fPIC -g -include old/lib.h old/lib.c -o libfoo.so
gcc -g app.c -L. -lfoo -Wl,-rpath,. -o app
./app
# โ name = test
# โ timeout = 30
# Swap in new library (no recompile)
gcc -shared -fPIC -g -include new/lib.h new/lib.c -o libfoo.so
./app
# โ name = test
# โ timeout = 30 โ identical
Allocation and field access both happen inside the library, so growing
Session never affects app.c's stack/heap layout.
Safe redesign¶
This case is the safe redesign: expose only an opaque pointer plus
accessor functions, and do all allocation inside the library. Contrast with
case07, which adds a field to a non-opaque struct that callers sizeof
and embed directly โ that case is breaking precisely because the struct
layout is part of the public contract.
/* PUBLIC HEADER โ opaque pointer */
typedef struct Widget Widget;
Widget* widget_new(void);
void widget_free(Widget *w);
/* PRIVATE IMPLEMENTATION โ can grow freely */
struct Widget {
int x, y;
int new_field; /* safe to add */
};
Real-world examples: OpenSSL's SSL/EVP_MD_CTX types have been opaque
since 1.1.0; libcurl's CURL* handle is fully opaque; SQLite's sqlite3*
is opaque โ all three let the library grow internal state across releases
without breaking callers.
References¶
Source files¶
CMakeLists.txtapp.c
See also: Examples overview ยท All COMPATIBLE cases ยท Category: Addition (Compatible).