Skip to content
C++
Language
since C++98
Intermediate

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++98

Language 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:

cpp
extern "C" int strlen(const char* s);

Or to a brace-enclosed block of declarations:

cpp
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:

cpp
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:

cpp
using CFn = extern "C" void(int);  // error

To typedef a C-linkage function pointer, declare it inside an extern "C" block:

cpp
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:

cpp
// 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_H

When 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

cpp
// 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:

cpp
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:

cpp
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 __cplusplus guard 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:

cpp
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:

cpp
// 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:

cpp
// Source
void print_data(int x);
void print_data(struct Point& pt);
namespace math { double square(double x); }
cpp
# 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:

cpp
$ 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:

cpp
// 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_fni

This 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:

SuffixMeaning
vvoid
iint
ddouble
cchar
PKcconst char* (pointer to const char)
Riint&
Oiint&&
N...Equalified name scope (N open, E close)
I...Etemplate 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:

cpp
// 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:

cpp
// 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:

cpp
// 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:

cpp
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:

cpp
// 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:

cpp
extern "C"   int f();
extern "C++" int f();  // error: conflicting language linkages

A 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".

cpp
// 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.

cpp
// 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.

cpp
// 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 β†’ mangled

Running nm on the resulting object file shows all three symbols:

cpp
$ nm --defined-only symbols.o
                 U _ZTVN10__cxxabiv117__class_type_infoE
0000000000000000 T _Z8plain_fni
0000000000000000 T c_fn
0000000000000000 T _ZN4util6helperERNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE

Pipe through c++filt to decode every mangled name in one pass:

cpp
$ 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:

cpp
$ 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:

cpp
$ 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 Rule
  • reference/language/attributes β€” declaration modifiers including visibility and [[nodiscard]]