C++ API vs ABI Compatibility: The Key Differences

When updating the C++ language standard, compilers, or third-party libraries, developers must navigate two distinct levels of interoperability: Application Programming Interface (API) compatibility and Application Binary Interface (ABI) compatibility. This article defines both concepts, outlines the technical differences between them in C++, and explains why maintaining or breaking them poses unique challenges for software maintenance and performance.

What is API Compatibility?

API compatibility concerns the source code level. An update is API-compatible (or source-compatible) if existing code can be recompiled against the updated library or compiler without generating compilation errors or changing program behavior.

API compatibility guarantees that:

  • Function names, signatures, and access qualifiers remain unchanged.
  • Classes, namespaces, and templates retain their expected interfaces.
  • Overload resolutions and type conversions resolve consistently with previous versions.

If a library maintainer removes a public method, renames a type, or changes a parameter from int to std::string, they break API compatibility. Developers must manually edit their source code to fix the resulting compile-time errors.

What is ABI Compatibility?

ABI compatibility concerns the binary level. An update is ABI-compatible (or binary-compatible) if existing pre-compiled binaries (such as .dll, .so, or static .a files) can link and run successfully with the updated components without recompiling the original source code.

ABI encompasses machine-level specifications, including:

  • Object Layout: The memory size, alignment, and internal byte offsets of non-static data members within a class or struct.
  • Name Mangling: How the compiler encodes function names, namespaces, and parameter types into symbol names for the linker.
  • Calling Conventions: How arguments are passed (via specific registers or the stack) and how return values are handled.
  • Vtable Layout: The structure and ordering of function pointers in the virtual table used for dynamic dispatch.
  • Exception Handling: The mechanisms and runtime metadata used to propagate and catch exceptions.

If a library update alters any of these binary structures, running an older binary with the new library leads to undefined behavior, memory corruption, or runtime crashes.

Key Differences Between API and ABI in C++

While API ensures code compiles, ABI ensures already-compiled binaries execute correctly together. In C++, it is remarkably easy to preserve the API while completely breaking the ABI.

Characteristic API Compatibility ABI Compatibility
Domain Source code Machine code / Binary files
Enforced By The Compiler The Linker and Dynamic Loader
Break Resolution Edit source code and recompile Recompile all dependent components
Visibility Compile-time failures Often silent runtime memory bugs or crashes

Breaking ABI Without Breaking API

A common scenario in C++ is breaking binary compatibility without touching the public interface. For example, adding a private data member to a class:

// Version 1.0
class Widget {
public:
    void process();
private:
    int id;
};

// Version 1.1
class Widget {
public:
    void process();
private:
    int id;
    int priority; // Added private member
};

This change does not break API compatibility; existing code using Widget::process() will compile cleanly. However, it breaks ABI compatibility because sizeof(Widget) has changed. A pre-compiled binary expecting an object of the original size will allocate insufficient memory or access members at incorrect offsets when interacting with code compiled against the updated definition.

Similarly, changing the order of virtual functions in a class preserves source compatibility but shuffles the vtable indexes, causing binaries to call the wrong functions at runtime.

Why ABI vs. API Matters in C++ Evolution

The C++ standard library maintainers and the ISO C++ Committee frequently face a trade-off between ABI stability and modern performance. Preserving ABI compatibility allows operating systems and large enterprise ecosystems to upgrade shared system libraries without forcing every application on the system to be recompiled.

However, strict ABI stability prevents the adoption of better data structures, optimizations (such as passing small structs in CPU registers), and bug fixes inside standard types like std::regex or std::string. Breaking ABI enables maximum runtime efficiency and modernized designs, but it requires a coordinated recompilation of all interacting binaries across the entire software stack.