Case 79: Missing Template Instantiation in Shipped Binary¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS |
| Flags | ABI break, API break |
Detected ChangeKinds |
instantiation_missing_from_binary |
| Source files | examples/case79_missing_template_instantiation/ |
Category: Header-vs-Binary Parity | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
v1 ships two explicit template instantiations, descriptor<float> and
descriptor<double>, and the header advertises both via
extern template. v2's header is unchanged โ it still declares
extern template class descriptor<double> โ but the .cpp silently drops
the template class descriptor<double>; explicit-instantiation line.
Consumer source compiles cleanly against v2's header (no diagnostic at
all), but linking/loading against libv2.so fails with an undefined
symbol. This is the failure mode behind oneDAL's "ship one Float
combination only" build trims when a header isn't updated to match.
Old/new diff¶
| v1.cpp | v2.cpp |
|---|---|
template class descriptor<float>; |
template class descriptor<float>; |
template class descriptor<double>; |
(removed โ but v1.h/v2.h both still declare extern template class descriptor<double>) |
abicheck command¶
g++ -std=c++17 -shared -fPIC -g v1.cpp -o libfoo_v1.so
g++ -std=c++17 -shared -fPIC -g v2.cpp -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so -H old=v1.h -H new=v2.h --ast-frontend clang
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- instantiation_missing_from_binary: Template instantiation 'descriptor'
was exported by the old library but is missing from the new binary.
Other instantiations of 'mylib::descriptor::descriptor()' still exist,
so the public header very likely still advertises this one. Consumers
built against the old header link cleanly but fail at load time with an
undefined-symbol error. (mylib::descriptor<double>::descriptor())
- instantiation_missing_from_binary: ... mylib::descriptor<double>::threshold() const
- func_removed_elf_only: Elf_only function removed:
mylib::descriptor<double>::set_threshold(double)
- func_removed_elf_only: Elf_only function removed:
mylib::descriptor<double>::descriptor()
- func_removed_elf_only: Elf_only function removed:
mylib::descriptor<double>::threshold() const
Minimum evidence¶
min_evidence: L2 โ the public header AST is what tells abicheck the
header still declares extern template class descriptor<double> even
though the binary no longer exports its mangled symbols;
instantiation_missing_from_binary demangles each Function.mangled name
to recover the template argument (<double>) that the header/castxml
dumper's bare Function.name never carries, and scopes the check to
Visibility.PUBLIC functions so a stale extern template cross-reference
entry isn't mistaken for a surviving instantiation. castxml is the
documented default backend for this evidence layer; clang
(--ast-frontend clang) is a supported alternative AST frontend used
above.
Why abicheck catches it¶
Without header evidence, a plain symbol diff would just report
func_removed_elf_only for each of descriptor<double>'s three mangled
symbols โ already enough to conclude BREAKING. With the header AST,
abicheck additionally recognizes that the declaring header is unchanged
and still promises descriptor<double>, which is the specific signal that
distinguishes "the API author removed this on purpose" (an ordinary
func_removed, source-visible via a compile error) from "the API author
thinks this still ships but it doesn't" (instantiation_missing_from_binary
โ no source-level diagnostic fires at all, only a load-time failure).
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: compile app against v1, swap in v2 .so without recompile โ
the app only uses the header's extern template declaration, so nothing
about its own compilation changes.
# Build old library + app
g++ -std=c++17 -shared -fPIC -g v1.cpp -o libfoo.so
g++ -std=c++17 -g app.cpp -L. -lfoo -Wl,-rpath,. -o app
./app
# โ threshold = 0.500000
# Swap in new library (no recompile)
g++ -std=c++17 -shared -fPIC -g v2.cpp -o libfoo.so
./app
# โ app: symbol lookup error: app: undefined symbol:
# _ZN5mylib10descriptorIdE13set_thresholdEd
Why CRITICAL: the app's own source never changed and gave no warning
at compile time โ descriptor<double> is still declared extern template
in the header the app compiled against. The failure surfaces only at
dynamic-link time, in production, with no corresponding source-level
signal a developer could have caught earlier.
Safe redesign¶
Keep the shipped *_instantiation.cpp file's explicit-instantiation list
in lockstep with the header's extern template declarations โ ideally
generated from the same combinatorics list, so a build trim that drops an
instantiation is forced to also touch the header (turning a silent
load-time failure into a compile-time one).
Real-world example: oneDAL's algorithm modules each have an
*_instantiation.cpp file (one per Float ร Method ร Task combination).
If a build is trimmed without updating headers, the binary stops shipping
symbols the header still advertises โ exactly the scenario this case
reproduces with a minimal two-instantiation example.
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.