Case 66: Language Linkage Changed (extern "C" removed)¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
func_language_linkage_changed |
| Source files | browse on GitHub |
Category: Function ABI | Verdict: BREAKING
What breaks¶
The extern "C" wrapper is removed from the public header. In v1, functions are
exported with C linkage โ the symbol name in the .dynsym table is exactly
parse_config. In v2, functions use C++ linkage โ the exported symbol name
is mangled to something like _Z12parse_configPKc.
Any consumer (C or C++) that was linked against v1 has recorded parse_config
(unmangled) as the needed symbol. When v2 is loaded, that symbol doesn't exist โ
the dynamic linker fails with "undefined symbol".
Why this matters¶
extern "C" is the standard mechanism for C++ libraries to provide a C-compatible
ABI. It suppresses C++ name mangling, making symbols accessible from C code and
stable across different C++ compilers/versions. Removing it is equivalent to
renaming every affected function.
This break is particularly insidious because:
- The source code compiles fine with the new headers (C++ callers don't notice)
- The function signatures are identical โ same name, same parameters
- The break is only visible in the binary symbol table (nm -D)
- C consumers cannot call the function at all (mangled names aren't valid C identifiers)
Code diff¶
// v1.h โ C linkage (symbol: "parse_config")
extern "C" {
int parse_config(const char *path);
}
// v2.h โ C++ linkage (symbol: "_Z12parse_configPKc")
int parse_config(const char *path);
Real Failure Demo¶
Severity: CRITICAL
Scenario: compile C app against v1, swap in v2 .so without recompile.
# Build v1 (extern "C") and app
g++ -shared -fPIC -g v1.cpp -o libparser.so
gcc -g app.c -L. -lparser -Wl,-rpath,. -o app
./app
# โ parse_config = 1 (expected 1)
# โ validate_config = 1 (expected 1)
# Verify v1 exports unmangled names
nm -D libparser.so | grep parse_config
# โ T parse_config
# โ T validate_config
# Build v2 (no extern "C")
g++ -shared -fPIC -g v2.cpp -o libparser.so
# Verify v2 exports mangled names
nm -D libparser.so | grep parse_config
# โ T _Z12parse_configPKc
# โ T _Z15validate_configPKc
./app
# โ ./app: symbol lookup error: ./app: undefined symbol: parse_config
Why CRITICAL: The unmangled symbol parse_config no longer exists in v2's
dynamic symbol table. The C++ mangled version _Z12parse_configPKc is there,
but the pre-linked binary doesn't know about it. Process killed at load time.
How to fix¶
Always maintain extern "C" for public C-compatible APIs:
- Keep the extern "C" block: this is a public API contract, not an implementation detail
- Use a C header: keep the public header as pure C (
parser.h) and use a separate C++ header for C++ consumers - Enforce with CI: add a check that all public
.dynsymsymbols match expected names (e.g.,nm -D libparser.so | grep -v '^_Z'should list all public functions)
Real-world example¶
This commonly happens during "modernization" refactors when a C library is
rewritten in C++. The developer removes extern "C" thinking "we're C++ now"
without realizing that all downstream C consumers (and pre-built C++ binaries)
depend on the unmangled names.
libpng, zlib, and SQLite all maintain extern "C" blocks specifically to
ensure their C ABI contract is preserved even when compiled as C++.
abicheck detection¶
abicheck detects this as func_language_linkage_changed (BREAKING) by
comparing symbol names in the dynamic symbol table โ the unmangled name
disappears and a mangled name appears, which is flagged as a linkage change
rather than a simple removal + addition.
References¶
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.