Language Linkage
Controls how a symbol's name is encoded in the object file and which calling conventions apply, enabling C++ code to interoperate with C and other languages.
Language Linkagesince C++98Language linkage specifies the calling convention and name-encoding rules applied to a declaration, allowing C++ translation units to link against symbols produced by compilers for other languages β most commonly C.
Overview
C++ compilers encode rich type information into every exported symbol name β a process called name mangling. A function int square(int) becomes something like _Z6squarei under the Itanium ABI; double square(double) becomes _Z6squared. This encoding is what enables function overloading and type-safe linkage at the linker level, but it also makes C++ symbols completely opaque to C linkers, Fortran runtimes, assemblers, and anything else that expects flat, undecorated names.
Language linkage is the mechanism that controls this encoding. The C++ standard defines exactly two linkage specifications:
"C"β emits an undecorated, C-compatible symbol using the platform's C calling convention."C++"β the default; applies full C++ name mangling.
No other linkage strings are standardized. Some implementations support "Fortran", "stdcall", or vendor-specific strings, but those are extensions with no portability guarantee.
Language linkage is a property of a declaration, not a definition. It affects the symbol name seen by the linker and the calling convention used at every call site. It does not relax C++'s type-checking rules: a function declared extern "C" still undergoes full C++ argument-conversion checking at each call β only the ABI changes, not the type system.
Syntax
Apply linkage to a single declaration:
extern "C" int strlen(const char* s);Or to a brace-enclosed block of declarations:
extern "C" {
int open(const char* path, int flags, ...);
int close(int fd);
ssize_t read(int fd, void* buf, size_t count);
}Nesting is legal; the innermost specification wins:
extern "C" {
extern "C++" void cpp_callback(int); // C++ linkage, inside a C block
void c_function(int); // C linkage
}Linkage specifications may not appear in type aliases β this is ill-formed:
using CFn = extern "C" void(int); // errorTo typedef a C-linkage function pointer, declare it inside an extern "C" block:
extern "C" {
typedef int (*CComparator)(const void*, const void*);
}Cross-Language Headers
The canonical pattern for a header includable from both C and C++ uses the __cplusplus predefined macro:
// mylib.h
#ifndef MYLIB_H
#define MYLIB_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct MyHandle MyHandle;
MyHandle* mylib_create(void);
void mylib_destroy(MyHandle* h);
int mylib_process(MyHandle* h, const unsigned char* data, unsigned int len);
#ifdef __cplusplus
} // extern "C"
#endif
#endif // MYLIB_HWhen a C++ compiler processes this, the declarations receive C linkage and link cleanly against the C object file. A C compiler sees no extern "C" β C has no language linkage concept and needs none.
Examples
Exporting a C++ Implementation Behind a C Interface
// engine.cpp
#include "engine.h" // cross-language header above
#include <memory>
#include <stdexcept>
namespace engine {
struct State {
void update(float dt);
int process(const unsigned char* data, unsigned int len);
};
static std::unique_ptr<State> g_state; // C++11
}
extern "C" void engine_init() {
engine::g_state = std::make_unique<engine::State>(); // C++14
}
extern "C" void engine_tick(float dt) {
engine::g_state->update(dt); // full C++ inside the body
}
extern "C" void engine_shutdown() {
engine::g_state.reset();
}The extern "C" on the definition must match the declaration's linkage; omitting it on one side is undefined behaviour.
Function Pointers Across the ABI Boundary
Function pointer types carry linkage as part of their type. A pointer to a C function is a distinct type from a pointer to a C++ function even when their parameter lists are identical. This matters for callbacks:
extern "C" {
typedef int (*CComparator)(const void*, const void*);
void qsort(void* base, size_t n, size_t sz, CComparator cmp);
}
// This comparator has C linkage β types are compatible
extern "C" int compare_ints(const void* a, const void* b) {
return *static_cast<const int*>(a) - *static_cast<const int*>(b);
}
void sort_array(int* arr, std::size_t n) {
qsort(arr, n, sizeof(int), compare_ints); // well-formed
}Passing a plain C++ function (default "C++" linkage) where a CComparator is expected is technically ill-formed. Many implementations accept it when calling conventions happen to coincide, but it is not portable.
Exception Safety at C Boundaries
C has no exception-handling machinery. A C++ exception propagating through a C stack frame produces undefined behaviour on most targets. Guard every extern "C" entry point that can throw:
extern "C" int engine_process(MyHandle* h,
const unsigned char* data,
unsigned int len) noexcept { // C++11
try {
return h->impl->process(data, len);
} catch (...) {
return -1; // translate to an error code
}
}noexcept (C++11) and language linkage are orthogonal β you can combine them freely. If an exception would escape a noexcept function, std::terminate is called immediately.
Best Practices
- Always pair the
#ifdef __cplusplusguard in shared headers. A header consumed by C compilation must not expose C++-only syntax. - Repeat
extern "C"on the definition. The linkage specification on the declaration alone is not sufficient; defining a function without matching linkage is UB. - Keep the public C ABI surface minimal. Only the boundary functions need C linkage; all internal implementation can stay in idiomatic C++ namespaces, classes, and templates.
- Catch all exceptions before returning to C. Relying on calling-convention compatibility to absorb exceptions is unreliable and produces UB on most platforms.
- Combine with visibility attributes on shared libraries. On Linux with
-fvisibility=hidden,extern "C"does not export the symbol; add__attribute__((visibility("default")))(or[[gnu::visibility("default")]]as a non-standard extension) to the declaration.
Common Pitfalls
Overloading extern "C" functions is ill-formed. Overloading relies on mangled names; C linkage suppresses mangling, so two extern "C" declarations with the same name but different parameters produce duplicate symbols. The standard forbids it outright. A C++ function and an extern "C" function may share a name only when one has internal linkage (e.g., inside an unnamed namespace).
Primary templates cannot have C linkage. Explicit specializations may be given C linkage since C++11, but the primary template itself cannot:
extern "C" template <typename T> // ill-formed
T add(T a, T b);const at namespace scope has internal linkage by default in C++, unlike C where it defaults to external. If a constant must be visible across translation units with C linkage:
// C++ default: internal linkage, NOT exported
const int MAX_CONN = 100;
// Explicit external + C linkage
extern "C" extern const int MAX_CONN_EXPORTED;extern "C" does not disable C++ type rules. Every call to a C-linkage function still resolves argument types, implicit conversions, and overload sets according to C++ rules. The specifier affects only the emitted symbol and calling convention.
Name Mangling in Depth
C++ compilers must produce a unique linker symbol for every function, even when two functions share a name (overloading) or live in different namespaces or classes. They do this by encoding the full qualified name, parameter types, and qualifiers into the symbol string β a process called name mangling.
Under the Itanium C++ ABI (used by GCC, Clang, and every modern compiler except MSVC), mangled names always begin with _Z followed by a structured encoding. For example:
// Source
void print_data(int x);
void print_data(struct Point& pt);
namespace math { double square(double x); }# nm output (Linux/ELF)
$ nm libfoo.o | grep -E "print_data|square"
0000000000401186 T _Z10print_datai # print_data(int)
00000000004011b0 T _Z10print_dataR5Point # print_data(Point&)
0000000000401230 T _ZN4math6squareEd # math::square(double)The encoding rules are: _Z prefix, then N...E for qualified names, then an integer + identifier for each name component, then encoded parameter types (i = int, d = double, R = reference, a number + type name for class types, and so on). MSVC uses a completely different β and proprietary β scheme.
c++filt demangles symbols back to human-readable form:
$ nm libfoo.o | c++filt
0000000000401186 T print_data(int)
00000000004011b0 T print_data(Point&)
0000000000401230 T math::square(double)When extern "C" is applied, mangling is suppressed entirely and the symbol is emitted with the bare C identifier:
// Source
extern "C" void bt_cmp(const char* a, const char* b);
void cpp_fn(int x);
# nm output: bt_cmp is bare, cpp_fn is mangled
0000000000000020 T bt_cmp
0000000000401106 T _Z6cpp_fniThis is the root cause of every C/C++ link failure involving extern "C": without it, the C++ compiler references _Z6bt_cmpPKcS0_ at every call site, but the C object file exports only bt_cmp β the linker sees two different names and fails with "undefined reference."
Reading Mangled Names
The Itanium encoding is regular enough to decode by hand once you know the alphabet:
| Suffix | Meaning |
|---|---|
v | void |
i | int |
d | double |
c | char |
PKc | const char* (pointer to const char) |
Ri | int& |
Oi | int&& |
N...E | qualified name scope (N open, E close) |
I...E | template arguments |
So _Z6bt_cmpPKcS0_ decodes as: function bt_cmp (length 6, name "bt_cmp"), taking PKc (const char*) and S0_ (substitution back-reference for the first non-function type, also const char*).
Calling Conventions at the C Boundary
Language linkage is not only about symbol names. The C++ standard says it encompasses "calling convention, name mangling algorithm, etc." β meaning extern "C" also commits the function to the platform's C calling convention rather than any C++-specific ABI.
On most modern platforms (x86-64 System V, Windows x64, ARM64) the calling conventions for ordinary C and C++ free functions are identical, so in practice extern "C" rarely changes generated code for simple parameter types. However, the standard makes the difference normative: a pointer to a C function is a distinct type from a pointer to a C++ function with the same signature, and passing one where the other is expected is ill-formed.
What Cannot Cross the Boundary
Because C has no ABI for C++ constructs, the following must not be passed directly through an extern "C" interface:
Non-trivial C++ objects. Classes with constructors, destructors, virtual tables, or non-trivial copy/move semantics have no stable C layout. Passing std::string or std::vector by value across a C boundary is undefined behaviour.
References. C has no reference type. Declare reference parameters as pointers at the boundary:
// C-compatible boundary
extern "C" void process(MyHandle* h, const unsigned char* data, unsigned int len);
// Internal C++ implementation uses references freely
namespace impl {
void process(MyHandle& h, std::span<const unsigned char> data);
}Exceptions. C stack frames have no exception unwinding tables. If a C++ exception propagates through a C frame, the behaviour is undefined on all mainstream ABIs. Every extern "C" function that might throw must catch at the boundary (see the exception-safety example above).
C++ templates and overloads. You cannot expose a template instantiation directly. Wrap it in a concrete function:
// Can't export this directly
template<typename T>
T clamp(T val, T lo, T hi);
// Export a concrete wrapper instead
extern "C" double clamp_double(double val, double lo, double hi) {
return clamp(val, lo, hi);
}Function Pointer Calling Conventions in Practice
The standard requires that function pointer types carry their language linkage, so int (*)(int) (C++ linkage) and an extern "C" equivalent are distinct types. In practice, GCC, Clang, MSVC, and Intel compilers all treat them as the same type on platforms where the calling conventions coincide β but this is a known non-conformance accepted to avoid widespread ABI breakage (GCC bug 2316, Clang bug 6277). Portable code should always use a C-linkage typedef for callbacks passed to C APIs:
// Correct: typedef carries C linkage, type is unambiguous
extern "C" {
typedef int (*CCompareFn)(const void*, const void*);
void qsort(void* base, size_t n, size_t sz, CCompareFn cmp);
}
extern "C" int compare_ints(const void* a, const void* b) {
return *static_cast<const int*>(a) - *static_cast<const int*>(b);
}
// compare_ints has C linkage β matches CCompareFn exactly
qsort(arr, n, sizeof(int), compare_ints);Passing a plain C++ lambda or function pointer where a C-linkage callback is expected compiles on most implementations but is not portable and is technically undefined behaviour by the standard.
extern "C++": Restoring C++ Linkage
extern "C++" is the explicit spelling of the default linkage. It exists primarily to carve out a C++ island inside a large extern "C" block:
extern "C" {
// Everything in this block has C linkage by default
void c_open(const char* path);
void c_close(int fd);
extern "C++" {
// These two declarations escape back to C++ linkage
class CppLogger;
void log_event(CppLogger& logger, const char* msg);
}
void c_read(int fd, void* buf, size_t n);
}The innermost linkage specification always wins; extern "C++" inside extern "C" overrides the outer block for those declarations only.
When extern "C++" Is Actually Useful
In day-to-day code, you virtually never need to write extern "C++" explicitly β declarations at namespace scope already default to it. There are two real-world scenarios where it matters:
Selective C++ declarations inside a large C-wrapped header. Some projects auto-wrap an entire vendor C header in extern "C" { #include "vendor.h" }. If that header contains inline C++ helpers or C++ type aliases, tagging them extern "C++" preserves correct linkage without splitting the #include.
C++20 module attachment. With modules, extern "C++" is the only way to attach a declaration to the global module from within a module unit. This matters when you need ABI-stable headers consumed both by classic #include and by import:
// mylib.cppm β module interface unit
export module mylib;
extern "C++" {
// These stay in the global module, visible to non-module consumers
// that still #include the classic header
export class Widget;
export void process(Widget&);
}Outside of these two cases, extern "C++" is noise β omit it.
extern "C++" Does Not Re-Enable Mangling Selectively
A common misconception is that you can mix extern "C" and extern "C++" on the same declaration to get "some" mangling. The linkage of a declaration is set once; re-declaring the same entity with different linkage is ill-formed:
extern "C" int f();
extern "C++" int f(); // error: conflicting language linkagesA redeclaration that omits the linkage specification inherits the first one β so the extern "C++" is only meaningful when applied to declarations that have not yet been given any linkage, or when used as a scoped override inside an extern "C" block as shown above.
extern "C" for Callback APIs
Passing C++ callables to C libraries that expect raw function pointers requires C linkage. Many C APIs β signal handlers, pthread_create, qsort, GUI event loops β take a bare function pointer with a specific C type signature. A plain C++ function (or a lambda) has "C++" linkage by default, making it a distinct, incompatible type. The correct fix is to mark the callback extern "C".
// C++11
#include <csignal>
#include <cstdio>
extern "C" void handle_sigint(int sig) {
std::fputs("Caught SIGINT\n", stderr);
}
int main() {
// signal() expects a C-linkage function pointer; handle_sigint matches
std::signal(SIGINT, handle_sigint);
}When the callback must access C++ state β class members, captured variables, thread-local context β a free extern "C" function cannot do it alone. The canonical workaround is the static member function trick: a static member function has no implicit this, so its address has a regular function-pointer type; wrapping it in an extern "C" shim gives it C linkage while letting it reach the class's internals through a context pointer baked into the API.
// C++11
#include <cstdio>
// Hypothetical C API: void run_loop(void* ctx, void (*cb)(void* ctx, int event));
extern "C" void c_loop_callback(void* ctx, int event); // forward-declare
class EventHandler {
public:
void on_event(int event) {
++event_count_;
std::printf("event %d (total %d)\n", event, event_count_);
}
// Static: no implicit this β can be wrapped with C linkage
static void dispatch(void* ctx, int event) {
static_cast<EventHandler*>(ctx)->on_event(event);
}
private:
int event_count_ = 0;
};
extern "C" void c_loop_callback(void* ctx, int event) {
EventHandler::dispatch(ctx, event);
}
// Usage:
// EventHandler h;
// run_loop(&h, c_loop_callback);The void* context pointer is the standard idiom used by virtually every C callback API (pthread_create, libuv, libcurl, SDL_CreateThread, and so on) precisely to allow this pattern.
Name Mangling Inspection with nm, c++filt, and objdump
Linker errors involving extern "C" mismatches are easiest to diagnose by inspecting the actual symbol table of the compiled object file. Three standard tools cover this: nm lists symbols, c++filt demangles them, and objdump -t provides the full symbol table with section and size information.
// C++17
// symbols.cpp β compile with: g++ -c symbols.cpp -o symbols.o
#include <string>
void plain_fn(int x); // C++ linkage β mangled
extern "C" void c_fn(int x); // C linkage β bare name
namespace util { void helper(std::string& s); } // nested scope β mangledRunning nm on the resulting object file shows all three symbols:
$ nm --defined-only symbols.o
U _ZTVN10__cxxabiv117__class_type_infoE
0000000000000000 T _Z8plain_fni
0000000000000000 T c_fn
0000000000000000 T _ZN4util6helperERNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEPipe through c++filt to decode every mangled name in one pass:
$ nm --defined-only symbols.o | c++filt
0000000000000000 T plain_fn(int)
0000000000000000 T c_fn
0000000000000000 T util::helper(std::__cxx11::basic_string<char, ...>&)c++filt accepts a bare mangled name directly on the command line for quick spot-checks:
$ c++filt _ZN4util6helperERNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
util::helper(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)For shared libraries, objdump -T lists dynamic (exported) symbols; combine with c++filt to verify which C++ symbols are visible to the dynamic linker:
$ objdump -T libmylib.so | c++filt | grep " g "
0000000000001140 g DF .text 000000000000002a Base c_fn
0000000000001180 g DF .text 000000000000003e Base plain_fn(int)A symbol appearing without any _Z prefix in nm output is a reliable signal that it has C linkage (or C-compatible no-mangle linkage). A missing symbol in nm --defined-only but present in nm -u means the definition was not compiled in β the root cause of most "undefined reference" link errors.
See Also
reference/language/odr-linkageβ internal vs. external linkage and the One Definition Rulereference/language/attributesβ declaration modifiers including visibility and[[nodiscard]]