Skip to content

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.txt
  • app.c

See also: Examples overview ยท All COMPATIBLE cases ยท Category: Addition (Compatible).