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
#endifWhen 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).
- A forward-declared
structis declared in a C header without defining its body. - In the C++ implementation file, the struct is defined, or a
reinterpret_castis used to map it to a C++ class instance. - 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
}
}