Article by Ayman Alheraki on January 29 2026 03:17 PM
An ABI (Application Binary Interface) defines how compiled binaries interact without recompilation. It governs low-level, binary-level rules such as:
Function calling conventions
Symbol names exported by libraries
Object layout, size, and alignment
Exception propagation and stack unwinding
Runtime memory allocation compatibility
If an ABI changes, existing binaries may break, even if the source code is still valid.
A critical fact:
The C++ standard does not define an ABI. ABI behavior is left entirely to compiler and platform implementations.
C++ is standardized at the source language level, not the binary level.
That means critical ABI details are implementation-defined:
Name mangling format
Class layout rules
Virtual table (vtable) organization
RTTI representation
Exception handling metadata
As a result:
Different compilers use different ABIs
Even the same compiler may change ABI across versions
Binary compatibility is not guaranteed unless explicitly promised by a vendor
This is not a flaw — it is a design decision that allows C++ to evolve aggressively.
C++ supports:
Function overloading
Namespaces
Member functions
Templates
Operator overloading
To support this, compilers encode extra information into symbol names (“name mangling”).
Because the mangling scheme is not standardized, different compilers — or different compiler versions — may emit different symbols for the same source declaration.
Result: binary linkage across toolchains becomes unreliable.
C++ classes may involve:
Multiple inheritance
Virtual inheritance
Virtual functions
Hidden compiler-generated members
These introduce:
vtables
pointer adjustments
padding and alignment differences
Any small change — adding a virtual function, changing inheritance, reordering members — can alter:
object size
memory offsets
vtable layout
Result: binaries compiled against an older version may crash or misbehave.
Templates and inline functions are usually instantiated in the consumer’s binary, not in the library.
Changing:
a template definition
an inline function body
a header-only implementation
can silently change the generated machine code.
Result: binary compatibility is broken even when function signatures appear unchanged.
Throwing exceptions across module boundaries depends on:
exception object layout
stack unwinding format
personality routines
These are ABI details, not language semantics.
Different compilers or runtimes may implement them differently.
Result: mixing binaries can cause:
memory leaks
skipped destructors
undefined behavior
Exposing types such as:
std::string
std::vector
std::filesystem::path
in public interfaces ties your ABI to:
a specific standard library implementation
its allocator model
its internal layout decisions
Even minor standard library updates can invalidate binary compatibility.
Important clarification:
The C standard also does not define an ABI.
So why is C practically ABI-stable?
Most operating systems define their ABI using C calling conventions as the baseline:
argument passing
return values
stack layout
Other languages often target “the C ABI” to interoperate.
C exposes only:
plain functions
unmangled symbol names
simple data structures
explicit pointers
There are:
no templates
no exceptions
no virtual dispatch
no implicit object lifetime rules
This simplicity dramatically reduces ABI breakage risk.
C enforces:
well-defined member order
predictable alignment rules
no hidden compiler-generated members
As long as struct definitions remain unchanged, binaries remain compatible.
extern "C" Is the Industry’s ABI Escape HatchWhen C++ developers need stable linkage, they explicitly request C linkage.
This is an implicit acknowledgment that:
C linkage is the universal, stable ABI contract
Small changes can break ABI:
adding a virtual function
changing inheritance
switching standard library types
modifying inline code
Binary consumers may crash without warning.
If you keep:
function signatures stable
struct layouts stable
opaque handles instead of exposed objects
Then binaries often remain compatible for decades.
Large runtimes prioritize:
plugin ecosystems
long-term binary compatibility
multi-compiler support
ease of extension
They almost universally adopt this pattern:
C for the external ABI Any other language internally
This allows internals to evolve without breaking extensions.
C++ ABI instability is not a weakness — it is a trade-off for:
expressive power
zero-cost abstractions
aggressive optimization
C’s ABI stability is not magic — it comes from deliberate minimalism.
C remains ABI-stable because it exposes very little at the binary level. C++ is ABI-fragile because it exposes powerful abstractions whose binary meaning must evolve.
That is why:
operating systems
language runtimes
plugin systems
use C as the binary boundary, even when the implementation language is C++, Rust, or something else.
C is the stable contract. C++ is the powerful implementation.