Skip to content

Case 16: Inline β†’ Non-inline (ODR / Symbol Appearance)

Field Value
Verdict 🟒 COMPATIBLE
Category Addition (Compatible)
Platforms Linux
Flags β€”
Detected ChangeKinds func_added
Source files examples/case16_inline_to_non_inline/

Category: Addition | Verdict: βœ… COMPATIBLE

Verdict and consumer impact

Existing binaries are unaffected: fast_hash was inline in old, so callers compiled against old's header already have their own copy of the function body baked in β€” they never referenced a .so symbol for it. new moves the implementation out of the header into the library and exports it as a real symbol. abicheck's binary-vs-binary comparison correctly sees this as a pure addition. The real risk is a build-coordination one, not a binary ABI break: new consumers compiled against new's header (declaration only, no inline body) require the symbol to exist in the library they link against β€” linking such a consumer against old's .so fails at link time, because old never exported fast_hash at all.

Old/new diff

old/lib.hpp new/lib.hpp
inline int fast_hash(int x) { return static_cast<int>(static_cast<unsigned>(x) * 2654435761U); } int fast_hash(int x); (declaration only)
old/lib.cpp: (empty β€” nothing to compile) new/lib.cpp: int fast_hash(int x) { return static_cast<int>(static_cast<unsigned>(x) * 2654435761U); }

abicheck command

g++ -shared -fPIC -std=c++17 -g old/lib.cpp -o libfoo_v1.so
g++ -shared -fPIC -std=c++17 -g new/lib.cpp -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so

Expected abicheck finding

Verdict: COMPATIBLE (exit 0)

Quality Issues:
- version_script_missing: Library exports 1 symbol(s) without a version
  script.

Additions:
- func_added: New public function: fast_hash
  > New function available; existing binaries are unaffected.

Minimum evidence

min_evidence: L0 β€” the exported-symbol table alone is enough: fast_hash is absent from old's .dynsym (it was header-only inline, nothing to export) and present in new's, with no removed or changed symbols. No debug info or headers needed.

Why abicheck catches it

The dynamic symbol table is authoritative L0 evidence β€” old's .so truly has no fast_hash symbol (nm --dynamic confirms it), so its appearance in new is classified func_added from pure ELF symbol-table evidence, same as any other new export. abicheck's binary comparison has no way to see (and correctly doesn't claim to) that the function existed all along as a header inline β€” that fact lives in source, not in either compiled .so.

Runtime failure demonstration

Severity: INFORMATIONAL for existing binaries β€” link-time risk for new consumers.

Scenario A (existing binary, unaffected): compile app against old's header (inline fast_hash), swap in new's .so without recompile.

g++ -shared -fPIC -std=c++17 -g old/lib.cpp -o libhash.so
g++ -std=c++17 -g app.cpp -I. -L. -lhash -Wl,-rpath,. -o app
./app
# β†’ fast_hash(42) = -182847734

# Swap in new library (no recompile)
g++ -shared -fPIC -std=c++17 -g new/lib.cpp -o libhash.so
./app
# β†’ fast_hash(42) = -182847734   (same β€” caller uses its own inlined copy)

Scenario B (new consumer, link failure): compile a fresh translation unit against new's header (declaration only), link against old's .so.

g++ -shared -fPIC -std=c++17 -g old/lib.cpp -o libhash.so
g++ -std=c++17 -g app.cpp -Inew -L. -lhash -Wl,-rpath,. -o app
# β†’ /usr/bin/ld: app.cpp:(.text+0x...): undefined reference to `fast_hash(int)'
# β†’ collect2: error: ld returned 1 exit status

Why the verdict is still COMPATIBLE: abicheck compares compiled binaries, and by that measure this is a strict addition β€” every symbol old exported is still present and unchanged in new. Scenario B's failure is a consequence of mixing a new header with an old library, which a coordinated release (ship the new header and the new .so together) naturally avoids.

Safe redesign

No fix needed β€” moving an implementation from header-inline to library-exported is a normal, compatible evolution as long as the header and library are always shipped as a matched pair. If old headers might still be distributed after the library moves on, keep a deprecated inline forwarder in the header for one release cycle to avoid Scenario B entirely.

Real-world example: in abseil-cpp, several string utility functions were moved from headers into the .so during a refactor; users who pinned to an old .so but updated their headers hit exactly Scenario B's linker error.

Cross-tool comparison

abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml || true
# β†’ reports fast_hash as a new addition, same conclusion as abicheck; it
#   has no visibility into the header-level inline→non-inline distinction
#   since it only ever compares the two .so files.

References


Source files

  • CMakeLists.txt
  • app.cpp

See also: Examples overview Β· All COMPATIBLE cases Β· Category: Addition (Compatible).