Skip to content

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

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