Case 59: Function Became Inline (outlined โ inline)¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS |
| Flags | ABI break |
Detected ChangeKinds |
func_removed_elf_only, func_visibility_changed |
| Source files | examples/case59_func_became_inline/ |
Category: Symbol API | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
v1 exports fast_abs and fast_max as regular outlined functions from the
shared library. v2 moves both to bad.h's successor, good.h, as static
inline โ the definitions still exist and are still callable from source,
but the exported symbols disappear from .dynsym. This is the inverse of
case47 (inline_to_outlined, which is compatible): existing binaries that
resolve fast_abs/fast_max against the shared library get undefined
symbol at load time, even though a fresh compile against v2's header works
fine. Recompilation is mandatory for every already-linked consumer.
Old/new diff¶
| bad.h (v1) | good.h (v2) |
|---|---|
int fast_abs(int x); โ declared, defined (and exported) in bad.c |
static inline int fast_abs(int x) { ... } โ defined in the header, not exported |
int fast_max(int a, int b); โ same |
static inline int fast_max(int a, int b) { ... } โ same |
abicheck command¶
gcc -shared -fPIC -g -I. bad.c -o libfoo_v1.so
gcc -shared -fPIC -g -I. good.c -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so \
--header old=bad.h --header new=good.h --ast-frontend clang
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- func_visibility_changed: Function visibility changed to hidden: fast_abs (public -> hidden)
> Symbol hidden from dynamic linking; old binaries can't find it at load time.
- func_visibility_changed: Function visibility changed to hidden: fast_max (public -> hidden)
> Symbol hidden from dynamic linking; old binaries can't find it at load time.
- func_removed_elf_only: Elf_only function removed: fast_abs
> Exported function symbol removed from the binary; old binaries that
link or dlsym() it can fail even without header evidence.
- func_removed_elf_only: Elf_only function removed: fast_max
> Exported function symbol removed from the binary; old binaries that
link or dlsym() it can fail even without header evidence.
Minimum evidence¶
min_evidence: L0 โ the underlying fact (fast_abs/fast_max vanish from
.dynsym) is fully visible from the exported-symbol table alone: a bare
abicheck compare libfoo_v1.so libfoo_v2.so with no headers already reports
BREAKING via the plain func_removed kind. Adding good.h's header AST (as
above) sharpens the diagnosis from "symbol removed" to "removed from the
binary but still declared static inline in the header" โ surfacing the
more precise func_visibility_changed/func_removed_elf_only pair โ but
that extra precision isn't needed to reach the correct BREAKING verdict.
Why abicheck catches it¶
The dynamic symbol table is authoritative L0 evidence: fast_abs and
fast_max are T-type symbols in v1's .dynsym and absent from v2's, so
abicheck flags a plain removal without any further evidence. When public
headers are also supplied, abicheck cross-references the header AST and
sees both functions are still declared (as static inline) on the new
side โ reclassifying the finding as a visibility change plus an ELF-only
hard removal, rather than treating it as if the API vanished from source
too.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: compile app against v1 (functions resolved from the shared
library), swap in v2 .so without recompile.
# Build old library + app
gcc -shared -fPIC -g -I. bad.c -o libfoo.so
gcc -g app.c -L. -lfoo -Wl,-rpath,. -o app
./app
# โ abs(-7) = 7
# โ max(3, 9) = 9
# Swap in new library (no recompile)
gcc -shared -fPIC -g -I. good.c -o libfoo.so
./app
# โ ./app: symbol lookup error: ./app: undefined symbol: fast_abs
Why CRITICAL: fast_abs is no longer in v2's dynamic symbol table
because it was inlined into the header; the runtime linker cannot resolve
the app's reference to it and the process is killed immediately on startup.
Safe redesign¶
Keep an outlined, exported definition alongside the inline one so both old binaries (linking against the symbol) and new compiles (using the inline fast path) keep working:
/* header */
static inline int fast_abs(int x) { return x < 0 ? -x : x; }
/* .c file โ still provide an exported symbol for backward compat */
int fast_abs(int x) { return x < 0 ? -x : x; }
Or mark the outlined definition __attribute__((weak)) so callers that
still bind it dynamically keep resolving.
Real-world example: header-only refactors that convert small "hot path"
helper functions to static inline for performance are a common source of
this exact break โ the API still compiles fine from source, which is why
the symbol-table regression is easy to miss in review.
References¶
Source files¶
CMakeLists.txtapp.cbad.cbad.hgood.cgood.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.