C and C++ Interoperability Mechanisms

Seamless interoperability between C and C++ relies on a shared ancestry, a common Application Binary Interface (ABI) foundation, and language-level linkage directives. This article outlines the core mechanisms that allow both languages to call each other's functions and share data structures without runtime overhead. Key concepts covered include the extern "C" linkage specification, the __cplusplus preprocessor macro, standard-layout data types, calling conventions, and foreign function interface patterns such as opaque pointers.

The extern "C" Linkage Directive

The primary barrier to linking C and C++ code is name mangling (or name decoration). C++ compilers alter function names in the compiled object file to encode parameter types, enabling function overloading. In contrast, C compilers retain the exact symbol name defined in the source code.

The extern "C" directive instructs the C++ compiler to use C-style linkage for the specified functions, disabling name mangling.

extern "C" {
    void c_function(int a, double b);
    int another_c_call(void);
}

When this directive is applied, the linker can match calls from C++ code to the raw symbol names generated by a C compiler.

The __cplusplus Header Guard Pattern

To allow a single header file to be included in both C and C++ source files, developers use the built-in __cplusplus macro. Because pure C compilers do not recognize the extern "C" keyword, conditional compilation is required:

#ifndef MY_API_H
#define MY_API_H

#ifdef __cplusplus
extern "C" {
#endif

void execute_task(int task_id);
int calculate_result(void);

#ifdef __cplusplus
}
#endif

#endif

When parsed by a C++ compiler, extern "C" is evaluated; when parsed by a C compiler, it is skipped entirely, maintaining strict C syntax validity.

Calling Conventions and ABI Compatibility

For two languages to communicate, they must agree on an Application Binary Interface (ABI). This dictates how functions receive parameters, return values, and preserve register states.

Most platforms provide standardized calling conventions (such as cdecl on x86, or the System V AMD64 ABI) that both C and C++ conform to by default. Because C++ was designed as a superset of C, standard C++ compilers automatically adhere to the host platform's C calling conventions when extern "C" is applied.

Data Layout and Type Compatibility

Data exchange requires identical in-memory representations. C++ supports this via the concept of "Standard Layout" types (formerly referred to as Plain Old Data or POD).

A C++ struct that contains no virtual functions, no virtual base classes, identical access control across all non-static data members, and standard primitive types possesses the same memory alignment, size, and field offsets as its C counterpart:

/* Compatible in both C and C++ */
typedef struct {
    int id;
    float value;
    char name[32];
} DataPayload;

As long as alignment rules and padding are identical—often enforced using fixed-width integers from <stdint.h>—pointers to these structures can be passed across the language boundary safely.

Function Pointers and Callbacks

C and C++ share identical semantics for basic function pointers. A C API can accept a function pointer as a callback, and a C++ program can supply a regular function or a static member function, provided the target has extern "C" linkage:

// C++ side providing a callback to a C library
extern "C" void my_callback(int event_code) {
    // Handle the event
}

// Calling the C API
register_listener(&my_callback);

Non-static member functions cannot be passed directly because they require an implicit this pointer, which is not supported by standard C function pointers.

Encapsulation with Opaque Pointers

To expose modern C++ features (such as classes, templates, and standard library containers) to a C codebase, developers use opaque pointers (the Opaque Type Pattern).

  1. A forward-declared struct is declared in a C header without defining its body.
  2. In the C++ implementation file, the struct is defined, or a reinterpret_cast is used to map it to a C++ class instance.
  3. C code handles the object strictly through pointers, manipulating it via wrapper functions with extern "C" linkage.
/* In shared C/C++ header */
typedef struct EngineWrapper EngineWrapper;

EngineWrapper* engine_create(void);
void engine_start(EngineWrapper* engine);
void engine_destroy(EngineWrapper* engine);

Exception Safety at the Boundary

C lacks an exception mechanism and does not support the stack-unwinding models used by C++. If a C++ exception crosses an extern "C" boundary into C code, the behavior is undefined and generally leads to an immediate process crash (std::terminate).

To maintain seamless interoperability, C++ functions exposed to C must catch all exceptions internally and translate them into C-compatible return values or error codes:

extern "C" int safe_wrapper(void) {
    try {
        cpp_operation_that_may_throw();
        return 0; // Success
    } catch (...) {
        return -1; // Error code returned to C
    }
}