Article by Ayman Alheraki on February 11 2026 11:39 AM
Before comparing C, C++, and Rust, we must separate two frequently confused concepts:
Defines:
How arguments are passed (registers vs stack)
How return values are delivered
Which registers are caller/callee saved
Stack alignment requirements
Who cleans the stack
Much broader. Includes:
Calling convention
Type sizes and alignment
Struct/class layout
Name mangling rules
Exception handling model
Object file format and symbol visibility
Dynamic linking rules
Calling convention is a component of the ABI — not the whole ABI.
On modern systems, the platform ABI defines the real low-level rules:
Examples:
SysV AMD64 ABI (Linux/macOS/BSD on x86-64)
Windows x64 ABI
AAPCS64 (AArch64)
C, C++, and Rust compilers generally conform to the platform ABI when generating machine code.
So in practice:
The architecture + operating system define the low-level calling convention. The language defines how much additional ABI surface it adds.
C is often considered the "binary lingua franca" because:
No name mangling
No implicit runtime
No class layout rules
No exceptions crossing boundaries
Simple type system
A C function:
int add(int a, int b);Will:
Follow the platform calling convention
Export a predictable symbol name
Have stable parameter passing rules
Because:
It adds almost nothing beyond the platform ABI.
All major compilers target the same system ABI contracts.
Its type system maps cleanly to machine-level constructs.
LP64 vs LLP64 models (long differs)
Struct padding/alignment
Bitfields
Compiler-specific attributes
Variadic functions
But overall, C offers the narrowest and safest binary interface.
For simple free functions:
int add(int a, int b);The actual register usage and stack layout are typically identical to C on the same platform.
So at the lowest level:
C++ often shares the same calling convention as C.
But this is only part of the story.
C++ introduces additional binary obligations:
Encodes:
Function overloading
Namespaces
Templates
Different compilers may use different mangling schemes.
Includes:
Padding and alignment
Multiple inheritance layout
Empty base optimization
Virtual base offsets
Layout may differ across:
Compilers
Compiler versions
Flags
Virtual functions require:
vptr placement
vtable layout rules
Runtime type information metadata
These are ABI-defined — but not standardized across all toolchains.
C++ requires:
Stack unwinding metadata
Personality functions
Exception object format
Cross-compiler binary compatibility becomes fragile.
C++ often uses the same low-level calling convention as C, but:
The C++ ABI is much larger and more complex than C’s ABI.
Therefore:
Stable cross-compiler C++ binary interfaces are difficult.
Public C++ shared libraries must tightly control toolchains.
Rust functions without an extern declaration use the "Rust" ABI.
Example:
pub fn add(a: u64, b: u64) -> u64 { a + b }This does NOT guarantee:
Stable calling convention across compiler versions
Stable symbol naming
Stable layout
Rust does not promise a stable native ABI.
Rust forces you to be explicit:
extern "C" fn add(a: u64, b: u64) -> u64 { a + b}Now:
Uses platform C calling convention
Exports unmangled symbol
Suitable for FFI
struct Foo { x: u32, y: u64,}Ensures:
Field ordering matches C
Padding rules follow C ABI
Rust separates:
"C" ABI (no unwinding expected)
"C-unwind" ABI (explicit cross-language unwinding)
This is critical for safety.
Rust does not define a universal stable ABI.
Instead:
Rust delegates stable binary interfaces to the C ABI.
Rust’s strength is explicitness:
ABI is opt-in.
Layout is opt-in.
Unwinding is opt-in.
Even if you "use C everywhere", ABI mismatches can occur due to:
Windows x64 vs SysV AMD64
AArch64 differences
Large struct return conventions vary.
Incorrect prototypes can corrupt state.
C++ exceptions crossing into Rust
Rust panic crossing into C
Undefined behavior if not managed
Who allocates? Who frees? Which allocator?
This is part of ABI contract design — not just calling convention.
| Feature | C | C++ | Rust |
|---|---|---|---|
| Platform calling convention | Yes | Yes | Yes (via extern) |
| Name mangling | No | Yes | Yes (unless no_mangle) |
| Stable default ABI | Yes (practically) | No (complex) | No |
| Explicit ABI control | Limited | Partial | Strong |
| Exception model in ABI | None | Yes | Explicit and separated |
| Recommended for stable FFI | Yes | Only via extern "C" | Only via extern "C" |
Smallest ABI surface. Most stable for long-term binary compatibility.
Shares calling convention with C but adds:
Mangling
Vtables
Exceptions
Template instantiation complexity
Binary stability requires discipline.
No stable default ABI. Provides explicit, opt-in C ABI compatibility.
Best practice: Expose C ABI. Hide Rust internally.
If you want:
Cross-language compatibility
Cross-compiler stability
Long-term binary durability
Then:
Design a C ABI surface. Implement internally in C, C++, or Rust. Never expose language-specific ABI details.
Calling conventions may match at the CPU level — but ABI stability is determined by how much semantic complexity the language leaks into the binary boundary.