Skip to content

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.

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).

mathlib.cpp
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;
}

By [N4950 §9.9], the extern "C" linkage specification has three effects:

  1. Name mangling is disabled. The symbol name in the object file is the literal function name, not an encoded representation of the signature.
  2. Language linkage is set to C. This affects how the function is called (C calling convention) and how entities are looked up.
  3. Overloading is prohibited. Within an extern "C" block, you cannot have two functions with the same name — the linker would see duplicate symbols.

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" 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)
};

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
}

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 purposes

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 boundary

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 __cplusplus
extern "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
}

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 allocator
std::unique_ptr<char, decltype(&c_destroy_buffer)> buf(
c_create_buffer(1024),
c_destroy_buffer // uses the correct C deallocator
);

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 handle
extern "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;
}

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);
}
}
## 5.4 ABI Boundaries: Name Mangling and Layout

At a C/C++ boundary, several ABI properties must align:

PropertyC ABIC++ ABI (Itanium, used on Linux/macOS)
Name manglingNone — symbol = function nameEncodes types, namespaces, templates
Calling conventionSystem V AMD64 (x86-64)Same as C (on System V platforms)
Struct layoutSame as C++ PODSame as C for POD; non-POD differs
Exception handlingN/A (no exceptions)Zero-cost with unwind tables
bool size1 byte (implementation-def)Same as C (implementation-defined)

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.

The Itanium C++ ABI encodes the full function signature into the symbol name:

// Symbol: _Z3addii
void add(int, int);
// Symbol: _Z3adddd
void add(double, double);
// Symbol: _ZN3lib3addEii
namespace lib { void add(int, int); }
// Symbol: _ZNK4Base3fooEv
struct 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 boundary
extern "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 layout
struct CPoint make_cpoint(double x, double y) {
CPoint p{x, y};
return p;
}
// C++-specific struct — NOT safe to pass across the boundary
struct 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:

  1. 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.
  2. Fixed-width types are used (int32_tNot int).
  3. No padding surprises: use static_assert and offsetof to verify layout, or #pragma pack / alignas to control it.
  4. 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.

Claim: A standard-layout struct with only fundamental type members has identical layout in C and C++ on the same platform.

Proof:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

The following C++ features break layout compatibility with C:

FeatureEffect on Layout
Virtual functionsAdds vtable pointer ( 8 bytes at offset 0)
Virtual base classesAdds vtable pointer and virtual base offset
Multiple inheritanceMay add pointer adjustments for base-to-derived
Non-standard-layout membersReference 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 guarantees
struct 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 interop
struct BadPacket {
std::string payload; // Non-POD: heap allocation, non-trivial destructor
virtual void validate() {} // vptr changes layout
};

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 padding
struct NetworkHeader {
uint8_t type;
uint32_t length;
uint16_t flags;
};
#pragma pack(pop)
static_assert(sizeof(NetworkHeader) == 7); // 1 + 4 + 2 = 7, no padding
## 5.6 Dynamic Library Loading with `dlfcn.h`

POSIX 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
}