C-Interop and FFI
C-Interop and FFI
Section titled “C-Interop and FFI”C++ uses name mangling to encode type information into function symbols, enabling overloading. C Does not mangle names — each function has a single symbol matching its source name. Interoperating Between C and C++ requires careful management of linkage, data layouts, and exception boundaries.
5.1 extern "C" Linkage [N4950 §9.9]
Section titled “5.1 extern "C" Linkage [N4950 §9.9]”C++ uses name mangling to encode type information into function symbols, enabling overloading. C Does not mangle names — each function has a single symbol matching its source name. The extern "C" Linkage specification disables name mangling, making a C++ function callable from C (and vice Versa).
extern "C" {
// These functions are exported with unmangled C linkage:// Symbol names: "add" and "multiply" (no type encoding)int add(int a, int b) { return a + b;}
double multiply(double a, double b) { return a * b;}
} // extern "C"
// Without extern "C", these would have mangled names:// _Z3addii (add(int, int))// _Z8multiplydd (multiply(double, double))int subtract(int a, int b) { return a - b;}Formal Semantics of extern "C"
Section titled “Formal Semantics of extern "C"”By [N4950 §9.9], the extern "C" linkage specification has three effects:
- Name mangling is disabled. The symbol name in the object file is the literal function name, not an encoded representation of the signature.
- Language linkage is set to C. This affects how the function is called (C calling convention) and how entities are looked up.
- Overloading is prohibited. Within an
extern "C"block, you cannot have two functions with the same name — the linker would see duplicate symbols.
extern "C" and Function Overloading
Section titled “extern "C" and Function Overloading”Since name mangling is disabled, you cannot overload functions with extern "C" linkage:
extern "C" { int process(int x); // OK: symbol is "process" // int process(double); // ERROR: duplicate symbol "process" — no mangling to disambiguate}This is not a limitation of the linkage specification per se, but a consequence of the linker”s Requirement for unique symbol names within a translation unit.
extern "C" and Member Functions
Section titled “extern "C" and Member Functions”extern "C" cannot be applied to member functions. Only free functions and variables can have C Language linkage [N4950 §9.9.1]:
class Foo { // extern "C" void bar(); // ERROR: member functions cannot have C linkage static void bar(); // OK: but still has C++ linkage (name mangled)};extern "C" and Static Members
Section titled “extern "C" and Static Members”Static member functions have C++ linkage even if declared in a class. If you need a C-callable Static member function, you must provide a non-member wrapper:
extern "C" void foo_c_wrapper(void* self) { // reinterpret self to the actual class type // call the static or non-static member function}constexpr and extern "C"
Section titled “constexpr and extern "C"”A function declared with both constexpr and extern "C" linkage is valid since C++17. The Function can be used in constant expressions and also has C linkage for linking purposes [N4950 §9.9]:
extern "C" constexpr int square(int x) { return x * x;}
static_assert(square(5) == 25); // OK: constexpr evaluation// Symbol "square" has C linkage for linking purposesextern "C" and noexcept
Section titled “extern "C" and noexcept”Functions with extern "C" linkage are implicitly noexcept unless declared otherwise [N4950 §14.5]. This is because C has no exception mechanism, so a C-linkage function that throws violates The C ABI contract:
extern "C" void c_function(); // implicitly noexcept
extern "C" void throwing_c_function() noexcept(false); // explicitly non-noexcept — allowed // but dangerous: exceptions may cross // the C ABI boundary5.2 Calling C from C++
Section titled “5.2 Calling C from C++”The standard C library headers are wrapped with extern "C" by the C++ standard library headers. When you #include <cstring>The declarations are automatically given C linkage. For your own C Libraries, use extern "C":
// my_c_api.h — the C header#ifndef MY_C_API_H#define MY_C_API_H
#ifdef __cplusplusextern "C" {#endif
typedef struct { double x; double y;} Point;
Point point_create(double x, double y);double point_distance(const Point* a, const Point* b);void point_translate(Point* p, double dx, double dy);
#ifdef __cplusplus} // extern "C"#endif
#endif // MY_C_API_H// my_c_api.c — the C implementation#include "my_c_api.h"#include <math.h>
Point point_create(double x, double y) { Point p = {x, y}; return p;}
double point_distance(const Point* a, const Point* b) { double dx = a->x - b->x; double dy = a->y - b->y; return sqrt(dx * dx + dy * dy);}
void point_translate(Point* p, double dx, double dy) { p->x += dx; p->y += dy;}// main.cpp — calling C from C++#include "my_c_api.h"#include <cstdio>#include <memory>
int main() { auto deleter = [](Point* p) { std::printf("Destroying point\n"); delete p; }; std::unique_ptr<Point, decltype(deleter)> p( new Point(point_create(3.0, 4.0)), deleter );
Point origin = point_create(0.0, 0.0); double dist = point_distance(p.get(), &origin); std::printf("Distance from origin: %f\n", dist); // 5.000000
point_translate(p.get(), 1.0, 1.0); dist = point_distance(p.get(), &origin); std::printf("After translate: %f\n", dist); // ~4.242641}Memory Ownership Across the Boundary
Section titled “Memory Ownership Across the Boundary”When a C function returns a heap-allocated pointer, the C++ caller must know how to free it. If the C library uses mallocThe C++ code must use free (not delete):
extern "C" { // C function that allocates with malloc char* c_create_buffer(size_t size); void c_destroy_buffer(char* buf);}
// C++ code using the C allocatorstd::unique_ptr<char, decltype(&c_destroy_buffer)> buf( c_create_buffer(1024), c_destroy_buffer // uses the correct C deallocator);5.3 Calling C++ from C
Section titled “5.3 Calling C++ from C”Calling C++ functions from C requires a C-compatible entry point — a function with extern "C" Linkage that wraps the C++ implementation:
// widget.cpp — C++ implementation#include <string>#include <vector>
class Widget { std::string name_; std::vector<int> data_;public: Widget(const char* name) : name_(name) {} void add_value(int v) { data_.push_back(v); } const char* get_name() const { return name_.c_str(); } int get_value(int index) const { return (index < static_cast<int>(data_.size())) ? data_[index] : -1; }};
// C-compatible opaque handleextern "C" { // Opaque pointer type — C code never sees the full definition typedef struct WidgetOpaque* WidgetHandle;
WidgetHandle widget_create(const char* name) { return reinterpret_cast<WidgetHandle>(new Widget(name)); }
void widget_destroy(WidgetHandle h) { delete reinterpret_cast<Widget*>(h); }
void widget_add_value(WidgetHandle h, int v) { reinterpret_cast<Widget*>(h)->add_value(v); }
const char* widget_get_name(WidgetHandle h) { return reinterpret_cast<Widget*>(h)->get_name(); }
int widget_get_value(WidgetHandle h, int index) { return reinterpret_cast<Widget*>(h)->get_value(index); }}/* widget_user.c — calling C++ from C */#include <stdio.h>
/* Opaque type — only declared, never defined in C */typedef struct WidgetOpaque* WidgetHandle;
WidgetHandle widget_create(const char* name);void widget_destroy(WidgetHandle h);void widget_add_value(WidgetHandle h, int v);const char* widget_get_name(WidgetHandle h);int widget_get_value(WidgetHandle h, int index);
int main(void) { WidgetHandle w = widget_create("sensor-1"); widget_add_value(w, 10); widget_add_value(w, 20); widget_add_value(w, 30);
printf("Widget: %s\n", widget_get_name(w)); printf("Value[0] = %d\n", widget_get_value(w, 0)); printf("Value[1] = %d\n", widget_get_value(w, 1)); printf("Value[2] = %d\n", widget_get_value(w, 2));
widget_destroy(w); return 0;}void* Instead of reinterpret_cast
Section titled “void* Instead of reinterpret_cast”For maximum portability across platforms where C and C++ may have different pointer representations, Use void* handles and pass data through C-compatible types:
// More portable C API using void*extern "C" { typedef void* WidgetHandle;
WidgetHandle widget_create(const char* name) { return static_cast<void*>(new Widget(name)); }
void widget_destroy(WidgetHandle h) { delete static_cast<Widget*>(h); }}At a C/C++ boundary, several ABI properties must align:
| Property | C ABI | C++ ABI (Itanium, used on Linux/macOS) |
|---|---|---|
| Name mangling | None — symbol = function name | Encodes types, namespaces, templates |
| Calling convention | System V AMD64 (x86-64) | Same as C (on System V platforms) |
| Struct layout | Same as C++ POD | Same as C for POD; non-POD differs |
| Exception handling | N/A (no exceptions) | Zero-cost with unwind tables |
bool size | 1 byte (implementation-def) | Same as C (implementation-defined) |
Itanium C++ ABI vs MSVC C++ ABI
Section titled “Itanium C++ ABI vs MSVC C++ ABI”On Linux and macOS, the Itanium C++ ABI is used for name mangling, virtual table layout, and Exception handling. On Windows, MSVC uses a different C++ ABI. This means that C++ libraries Compiled with GCC/Clang cannot be linked with MSVC-compiled C++ code (even with extern "C" on the C-compatible parts). The C-compatible parts work fine across ABIs; only C++-specific features (classes, templates, exceptions) are incompatible.
Name Mangling Examples
Section titled “Name Mangling Examples”The Itanium C++ ABI encodes the full function signature into the symbol name:
// Symbol: _Z3addiivoid add(int, int);
// Symbol: _Z3addddvoid add(double, double);
// Symbol: _ZN3lib3addEiinamespace lib { void add(int, int); }
// Symbol: _ZNK4Base3fooEvstruct Base { virtual void foo() const; };
// Symbol: _Z3maxIiERKT_S2_template<typename T> const T& max(const T&, const T&);With extern "C"All of these become addLosing the type information. This is why Overloading is not possible within extern "C" blocks.
#include <cstddef>#include <cstdio>
// Verifying struct layout compatibility across the C boundaryextern "C" {struct CPoint { double x; double y; // No virtual functions, no non-POD members → layout is identical in C and C++};
// This struct has a C-compatible layoutstruct CPoint make_cpoint(double x, double y) { CPoint p{x, y}; return p;}
// C++-specific struct — NOT safe to pass across the boundarystruct ComplexPoint { double x, y; virtual double magnitude() const { /* ... */ return 0.0; } // vtable pointer changes the layout — first member is NOT at offset 0 // sizeof(ComplexPoint) >= 24 (8-byte vptr + 2*8 bytes)};}
int main() { static_assert(sizeof(CPoint) == 16); static_assert(offsetof(CPoint, x) == 0); static_assert(offsetof(CPoint, y) == 8);
std::printf("sizeof(CPoint) = %zu\n", sizeof(CPoint)); // 16 std::printf("sizeof(ComplexPoint) = %zu\n", sizeof(ComplexPoint)); // 24 (on x86-64)}5.5 Data Marshalling: Ensuring Compatible Layouts
Section titled “5.5 Data Marshalling: Ensuring Compatible Layouts”When passing data across a C/C++ boundary, ensure that:
- Structs are POD (Plain Old Data) or
standard-layout: no virtual functions, no base classes with virtual functions, no non-static data members of reference type, all non-static data members have the same access control. - Fixed-width types are used (
int32_tNotint). - No padding surprises: use
static_assertandoffsetofto verify layout, or#pragma pack/alignasto control it. - No C++ exceptions cross the boundary: exceptions thrown in C++ code called from C unwind through C frames, which have no unwind information — undefined behavior. Catch all exceptions before returning to C code.
Proof of Struct Layout Compatibility
Section titled “Proof of Struct Layout Compatibility”Claim: A standard-layout struct with only fundamental type members has identical layout in C and C++ on the same platform.
Proof:
- By [N4950 §7.7.2], a standard-layout class has the same layout as a corresponding C struct with the same members in the same order.
- By [N4950 §7.7.2.1], each non-static data member is allocated at an offset that satisfies its alignment requirement, and the alignment of the struct is the maximum alignment of its members.
- C struct layout follows the same rules (ISO C 6.2.5p20, 6.7.2.1p15): each member is placed at an offset satisfying its alignment, with padding inserted as needed.
- Since both C and C++ use the same alignment rules for fundamental types (
int``doubleEtc.) on the same platform, the resulting layout is byte-for-byte identical. QED.
What Breaks Layout Compatibility
Section titled “What Breaks Layout Compatibility”The following C++ features break layout compatibility with C:
| Feature | Effect on Layout |
|---|---|
| Virtual functions | Adds vtable pointer ( 8 bytes at offset 0) |
| Virtual base classes | Adds vtable pointer and virtual base offset |
| Multiple inheritance | May add pointer adjustments for base-to-derived |
| Non-standard-layout members | Reference members, std::stringEtc. |
| Different compiler flags | -fpack-struct``#pragma pack changes padding |
Different alignment (alignas) | Adds padding not present in the C struct |
#include <cstdint>#include <cstddef>#include <cstdio>
// Correct: POD struct with explicit layout guaranteesstruct PacketHeader { static_assert(offsetof(PacketHeader, magic) == 0); int32_t magic; int32_t version; int64_t timestamp; int32_t payload_size; int32_t checksum;};
static_assert(sizeof(PacketHeader) == 24);static_assert(alignof(PacketHeader) == 8);
// Incorrect: this struct would break C interopstruct BadPacket { std::string payload; // Non-POD: heap allocation, non-trivial destructor virtual void validate() {} // vptr changes layout};Controlling Layout with #pragma pack
Section titled “Controlling Layout with #pragma pack”When interfacing with a C library that uses non-default packing (common in network protocols and File formats), use #pragma pack to match the layout:
#pragma pack(push, 1) // 1-byte alignment — no paddingstruct NetworkHeader { uint8_t type; uint32_t length; uint16_t flags;};#pragma pack(pop)
static_assert(sizeof(NetworkHeader) == 7); // 1 + 4 + 2 = 7, no paddingPOSIX systems provide dlopen``dlsym``dlcloseAnd dlerror for loading shared libraries at Runtime. This enables plugin architectures and runtime code loading.
// plugin.cpp — compiled into libplugin.so// $ g++ -shared -fPIC -o libplugin.so plugin.cpp
#include <cstdint>
extern "C" {
int32_t plugin_version() { return 1;}
int32_t plugin_compute(int32_t x, int32_t y) { return x * x + y * y;}
const char* plugin_name() { return "quadratic_plugin";}
} // extern "C"// loader.cpp — dynamically loads and uses the plugin// $ g++ -std=c++17 -o loader loader.cpp -ldl#include <cstdint>#include <cstdio>#include <cstdlib>#include <dlfcn.h>#include <string>
struct PluginAPI { int32_t (*version)(); int32_t (*compute)(int32_t, int32_t); const char* (*name)();};
PluginAPI load_plugin(const char* path) { void* handle = dlopen(path, RTLD_NOW); if (!handle) { std::fprintf(stderr, "dlopen failed: %s\n", dlerror()); std::exit(1); }
// Clear any existing error dlerror();
auto load_sym = [&](const char* name) -> void* { void* sym = dlsym(handle, name); char* err = dlerror(); if (err) { std::fprintf(stderr, "dlsym(%s) failed: %s\n", name, err); std::exit(1); } return sym; };
PluginAPI api{}; api.version = reinterpret_cast<int32_t(*)()>(load_sym("plugin_version")); api.compute = reinterpret_cast<int32_t(*)(int32_t, int32_t)>(load_sym("plugin_compute")); api.name = reinterpret_cast<const char*(*)()>(load_sym("plugin_name")); return api;}
int main() { auto plugin = load_plugin("./libplugin.so");
std::printf("Plugin: %s v%d\n", plugin.name(), plugin.version()); std::printf("compute(3, 4) = %d\n", plugin.compute(3, 4)); // 25
// In production code, store handle and call dlclose(handle) when done}