ABI & Calling Conventions

For separately compiled pieces of code to correctly call each other — your code calling a library function, one language calling into another — they must agree on far more than just a shared source-level API. That deeper, binary-level agreement is the ABI.

An ABI (Application Binary Interface) is the set of low-level rules that let separately compiled pieces of binary code work together correctly — exactly how function arguments are passed, where a function's return value goes, how data types are laid out in memory, and more, all agreed upon at the binary level rather than just the source level.

An API tells you what a function is called and what arguments it logically takes, expressed in source code. But once everything is compiled to machine code, the actual calling code and the actual called code need to agree, in concrete binary terms, on exactly which registers or stack locations hold which argument — that binary-level agreement is what makes it possible to link together code that was compiled completely separately, sometimes even by different compilers or in different languages, and have it still work correctly.

Every function call between separately compiled units (your program calling a system library, one language's code calling into a library written in another language via a foreign-function interface, or a plugin system loading independently compiled shared libraries at runtime) fundamentally depends on both sides agreeing on the same ABI.

Think of an electrical outlet standard. Two countries might both agree, at a high level, that "this device plugs into that outlet to get power" (the API-level agreement) — but if one uses 120 volts with a two-prong plug and the other uses 240 volts with a three-prong plug (different ABIs), physically connecting the two without an adapter will not work, and might actively damage something. An ABI is this low-level, physical/binary compatibility layer beneath the higher-level, conceptual agreement of an API.

Any code calling add — regardless of what compiler or even what source language produced it — must place a in edi and b in esi, and any correctly compiled implementation of add must read its arguments from exactly those registers and put its result in eax, purely because that's what this specific platform's ABI mandates for functions with this argument-type signature.

Different platforms (and sometimes different compilers on the same platform, historically) define different ABIs — the System V AMD64 ABI used by Linux and macOS on x86-64 differs from the Microsoft x64 calling convention used by Windows on the very same CPU architecture, meaning binary code compiled for one generally cannot be directly linked with binary code compiled for the other, despite targeting identical hardware.

Caller-saved (also called volatile) registers are ones a called function is free to overwrite without restoring, so the calling code must itself save any values it still needs in those registers before making the call. Callee-saved (non-volatile) registers are ones a called function must restore to their original values before returning, letting the calling code trust their contents remain unchanged across the call.

Data-layout rules (how a struct's fields are arranged and padded in memory, discussed earlier in the Structures & Enumerations fundamentals topic) are also part of the ABI — two pieces of separately compiled code need to agree not just on how to call functions, but on the exact memory layout of any shared data structures they both access.

Name mangling is how a compiler encodes additional information (like a function's parameter types, needed to support function overloading) into the actual symbol name emitted for the linker — which is exactly why C++ code generally can't directly link against code compiled as plain C without an explicit extern "C" declaration telling the C++ compiler to use plain, unmangled C-style naming for that specific function instead.

Assuming any two compiled pieces of code for the "same" CPU architecture can always be linked together directly — differing ABIs between operating systems (or even sometimes between compiler versions or specific compiler flags, for less strictly specified aspects) can make binaries incompatible despite targeting identical hardware.

Forgetting extern "C" when writing C++ code meant to be called from C or from another language's foreign-function interface, leading to confusing "undefined reference" linker errors caused specifically by C++ name mangling producing a different symbol name than the caller expects.

Assuming a struct's in-memory layout is guaranteed to be identical across every compiler and platform — while common conventions exist and are usually followed for simple cases, subtle ABI differences in padding, alignment, or bit-field layout can cause real incompatibility when sharing binary data structures across a language or platform boundary.

Wrapping C++ functions intended to be called from C (or from another language via foreign-function interfaces, many of which assume C-compatible ABI conventions) in an extern "C" block, to force plain, predictable C-style symbol naming.

Being deliberate and explicit about a shared library's ABI stability when it's meant to be used by other, separately compiled and separately updated programs — an ABI-breaking change (even one that looks like a small, source-compatible tweak) can silently break every program built against the previous version without a recompile.

Consulting a specific platform's documented calling convention when writing hand-tuned assembly, or when debugging a subtle cross-language or cross-compiler interoperability bug, rather than assuming a convention based on a different, possibly incompatible platform.

ABI stability as a long-term engineering commitment Much like the API stability discussed in the earlier Libraries & APIs fundamentals topic, maintaining a stable ABI for a widely used shared library (so that programs compiled against an older version keep working with a newer version, without needing to be recompiled) is a serious, deliberate engineering discipline — changing a public struct's layout, a function's calling convention, or even just adding a new field in the wrong place can silently break every already-compiled program depending on the old ABI, which is exactly why heavily used system libraries evolve their ABIs so carefully and conservatively.

Stack alignment requirements Most modern ABIs require the stack pointer to be aligned to a specific boundary (commonly 16 bytes on x86-64) at the point a function call is made, since certain instructions (including some SIMD/vectorized instructions used heavily by both hand-written performance code and compiler auto-vectorization) require correctly aligned memory access to function correctly, or to perform well — violating this alignment requirement, most often in hand-written or incorrectly generated assembly, can cause a crash or a significant performance penalty on the affected instructions.

Foreign Function Interfaces (FFIs) live entirely on top of ABI agreement

When one programming language calls into code written in another language (Python calling a C library, Rust calling into a C library, and so on), the entire mechanism rests on both sides agreeing on the same underlying platform ABI — which is exactly why FFIs overwhelmingly standardize on the C ABI as the common, most universally supported target, even when neither side of the actual call is itself written in C.

What's the difference between an API and an ABI?

An API is a source-level contract — function signatures and behavior as seen by code being compiled against them. An ABI is a binary-level contract — the exact register usage, stack layout, and data representation needed for already-compiled binary code to correctly call and interoperate with other already-compiled binary code, without needing to be recompiled together.

What's the difference between caller-saved and callee-saved registers?

Caller-saved (volatile) registers may be freely overwritten by a called function, so the calling code must save any values it still needs before the call. Callee-saved (non-volatile) registers must be restored to their original values by the called function before it returns, so the calling code can trust their contents survive the call unchanged.

Why does C++ code need extern "C" to be called from C?

Because C++ compilers mangle function names (encoding parameter type information into the symbol name, to support features like function overloading), producing a different symbol name than plain C code expects. extern "C" tells the C++ compiler to emit that specific function using plain, unmangled C-style naming instead.

Why do foreign function interfaces between different programming languages so often standardize on the C ABI specifically?

Because the C ABI is the most universally supported, stable, and simple binary calling convention across virtually every platform and toolchain — making it the practical common ground two otherwise very different language runtimes can both target, even when neither side of a given cross-language call is actually written in C.

int add(int a, int b);

// Under the System V AMD64 ABI (used by Linux/macOS on x86-64):
// - first integer argument (a) goes in register edi
// - second integer argument (b) goes in register esi
// - the return value comes back in register eax