Question

Using extern "C" and __cplusplus to Mix C and C++

ccppc-preprocessorextern-c

Question

In a project that contains legacy C code and new C++ code, how should C headers be written so that both C and C++ source files can include them correctly?

For example, C headers currently use this pattern after their include guards:

#ifdef __cplusplus
extern "C" {
#endif

/* includes, typedefs, and function declarations */

#ifdef __cplusplus
}
#endif

How does this behave when a C++ header includes multiple C headers? Does entering an extern "C" block make __cplusplus undefined for subsequently included headers? Is nesting linkage specifications such as extern "C" { extern "C" { ... } } valid?

Also, why is this wrapper needed in header files rather than .c implementation files? What happens if a C function has no declaration visible to the C++ caller? For third-party C headers that do not provide their own extern "C" guards, is wrapping the #include in extern "C" the correct approach? Finally, what other practices are important when maintaining a codebase that mixes C and C++?

Short Answer

You will learn the difference between preprocessing and language linkage, why __cplusplus remains defined inside an extern "C" block, and how to design C API headers that work from both C and C++.

The key rule is: compile C implementation files as C, and declare their public API with C linkage when that API is included by C++ code.

Concept

extern "C" is a C++ language-linkage specification. It tells a C++ compiler that declarations in the block use C language linkage.

For ordinary C functions, the practical effect is usually that a C++ compiler emits or looks up the same linker symbol name that a C compiler uses. Without it, a C++ compiler commonly mangles function names to support features such as function overloading.

Consider this C implementation:

/* math_utils.c */
int add(int left, int right) {
    return left + right;
}

A C compiler typically exports a linker symbol named add. If C++ sees this declaration without C linkage:

int add(int left, int right);

it may search for a C++-mangled name instead of the C symbol add. The code can compile but fail during linking with an undefined-reference error.

A portable C/C++ header gives the declaration C linkage only when it is compiled as C++:

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

#ifdef __cplusplus
extern "C" {
#endif

int add(int left, int right);

#ifdef __cplusplus
}
#endif

#endif

When a C compiler reads this header, __cplusplus is not defined, so it sees ordinary C declarations. When a C++ compiler reads it, it sees the declarations inside extern "C".

extern "C" affects linkage for applicable function declarations and names of variables with external linkage. It does not convert C code into C++, change the preprocessor, make __cplusplus disappear, or make arbitrary C++ constructs valid inside a C header.

This matters because a program can only link successfully when the declaration used by the caller and the compiled definition agree on the external symbol name and calling conventions expected by the platform.

Mental Model

Think of a header as a contact card for code in another compiled file.

  • A C compiler writes the contact name in the C naming format.
  • A C++ compiler normally writes contact names in a C++ naming format that includes extra details, such as parameter types.
  • extern "C" tells C++: “Use the C naming format for these contacts.”

__cplusplus is a label placed on the compilation by the C++ compiler. An extern "C" block changes how declarations are labelled for linking; it does not remove or alter that preprocessor label.

Nested extern "C" blocks are like placing a document in two envelopes both labelled “C.” The second label adds nothing, but it does not change the destination.

Take Quiz

Syntax and Examples

A C-compatible public header normally uses an include guard plus conditional C linkage:

#ifndef SENSOR_H
#define SENSOR_H

#ifdef __cplusplus
extern "C" {
#endif

typedef struct Sensor Sensor;

Sensor *sensor_create(int device_id);
int sensor_read(const Sensor *sensor, int *value);
void sensor_destroy(Sensor *sensor);

#ifdef __cplusplus
}
#endif

#endif

A C implementation defines the API normally. It does not need to repeat extern "C" when the file is compiled as C:

/* sensor.c */
#include "sensor.h"

struct Sensor {
    int device_id;
};

Sensor *sensor_create(int device_id) {
    static Sensor sensor;
    sensor.device_id = device_id;
    return &sensor;
}

int sensor_read(const Sensor *sensor, int *value) {
    if (sensor == 0 || value == 0) {
        return -1;
    }

    *value = sensor->device_id * 10;
    return 0;
}

void sensor_destroy(Sensor *sensor) {
    (void)sensor;
}

A C++ source file can include and call the C API:

#include "sensor.h"

#include <iostream>

int main() {
    Sensor *sensor = sensor_create(7);
    int value = 0;

    if (sensor_read(sensor, &value) == 0) {
        std::cout << value << '\n';
    }

    sensor_destroy(sensor);
}

The header is parsed in C++ mode, so sensor_create, sensor_read, and sensor_destroy receive C linkage. The C++ source can still use normal C++ features outside that C API declaration block.

For a third-party C header that lacks C++ guards, a local wrapper around the include is a reasonable compatibility technique:

extern "C" {
#include <vendor_library.h>
}

Prefer placing that wrapper in one project-owned adapter header, rather than repeating it throughout many .cpp files:

/* vendor_library_c_api.hpp */
#pragma once

extern "C" {
#include <vendor_library.h>
}

Then use #include "vendor_library_c_api.hpp" everywhere else.

Step by Step Execution

Trace this C++ translation unit:

#include "first.h"
#include "second.h"

Suppose both headers use the standard pattern:

/* first.h */
#ifdef __cplusplus
extern "C" {
#endif

void first_task(void);

#ifdef __cplusplus
}
#endif
/* second.h */
#ifdef __cplusplus
extern "C" {
#endif

void second_task(void);

#ifdef __cplusplus
}
#endif

What happens:

  1. The C++ compiler defines the preprocessor macro __cplusplus before preprocessing begins.
  2. It expands #include "first.h".
  3. In first.h, #ifdef __cplusplus is true, so the compiler processes extern "C" {.
  4. first_task is declared with C language linkage.
  5. The closing brace ends that linkage-specification block.
  6. __cplusplus is still defined. Nothing in extern "C" changes preprocessor macros.
  7. The compiler expands #include "second.h".
  8. #ifdef __cplusplus is true again, so second_task is also declared with C language linkage.

Even if one C header includes another while its extern "C" block is open, the nested declaration is valid:

extern "C" {
    extern "C" {
        void process(void);
    }
}

The inner declaration still has C language linkage. The extra nesting is redundant, not a mechanism that turns __cplusplus off.

If the C++ caller has no declaration at all:

int main() {
    process();
}

the C++ compiler reports an error because C++ requires a function to be declared before it is called. It does not guess whether process is a C or C++ function.

Real World Use Cases

  • Incremental modernization: A C++ application calls a stable C library while individual C modules are gradually replaced.
  • Operating-system and embedded APIs: Many platform SDKs expose a C ABI so their functions can be called from C, C++, and other languages.
  • Plugin interfaces: A C-style API is often used at dynamic-library boundaries because it has a simpler and more stable binary contract than C++ classes.
  • Foreign-function interfaces: Python, Rust, C#, and other languages frequently call libraries through a C ABI.
  • Vendor SDK integration: A C++ application consumes a device, database, graphics, or cryptography library whose headers were originally written for C.
  • Opaque handles: A C API can expose typedef struct Engine Engine; and functions operating on Engine *, while hiding its implementation. This works cleanly from C++ and avoids exposing C++ class layouts across a binary boundary.
Take Quiz

Real Codebase Usage

Keep the C/C++ boundary small and explicit.

A common directory arrangement is:

include/
  legacy_api.h          # Public C-compatible API
  vendor_api_bridge.hpp # Project-owned wrapper for a vendor C header
src/
  legacy_api.c          # Compiled as C
  application.cpp       # Compiled as C++

Useful practices:

  • Put linkage guards in every public C header. Consumers should not have to remember to add extern "C" around a normal project header.
  • Compile according to language. Compile .c files with a C compiler and .cpp files with a C++ compiler. Use the C++ compiler driver for the final link step when the program contains C++ objects, because it links the C++ runtime as needed.
  • Declare every exported function in a header. Include that header in the corresponding .c file. This lets the compiler compare the definition against the public declaration.
  • Use an adapter header for unguarded vendor headers. This centralizes compatibility code and prevents accidental inclusion of the raw header elsewhere.
  • Use C-friendly API types at the boundary. Prefer fixed-width integer types, pointers, struct types, enums used carefully, and explicit buffers. Avoid exposing C++ classes, overloaded functions, references, templates, exceptions, or std::string in a C API.
  • Convert errors at the boundary. A C API commonly returns a status code or null pointer. C++ wrapper code can turn that into exceptions or RAII objects if the application uses those patterns.

For example, a C++ wrapper may add safer ownership without changing the C API:

#include "sensor.h"

#include <memory>
#include <stdexcept>

struct SensorDeleter {
    void operator()(Sensor *sensor) const {
        sensor_destroy(sensor);
    }
};

using SensorPtr = std::unique_ptr<Sensor, SensorDeleter>;

SensorPtr open_sensor(int id) {
    SensorPtr sensor(sensor_create(id));
    if (!sensor) {
        throw std::runtime_error("Could not create sensor");
    }
    return sensor;
}

The C ABI remains simple; C++ convenience stays on the C++ side.

Common Mistakes

  • Believing that extern "C" undefines __cplusplus. It does not. __cplusplus is a preprocessor macro, while extern "C" is a C++ language feature.

  • Adding extern "C" to a .c file compiled as C. Plain C does not understand C++ linkage syntax. The function definition already gets C linkage because it is compiled as C.

    /* Broken if this file is compiled as C */
    extern "C" int add(int a, int b) {
        return a + b;
    }
    
  • Forgetting the closing conditional block. An unmatched extern "C" { can cause later declarations to receive C linkage unintentionally or produce a syntax error.

  • Putting C++-only declarations in a C header. A C compiler cannot parse templates, namespaces, classes, references, or std::vector.

    // Not valid in a header intended for C compilation.
    std::vector<int> load_values();
    
  • Calling an undeclared C function from C++. C++ does not support implicit function declarations. Include the correct header instead of manually guessing a declaration.

  • Writing a mismatched declaration manually. This can create undefined behavior even if linking succeeds. For example, a wrong parameter type, return type, or calling convention is dangerous. Keep one authoritative header.

  • Wrapping arbitrary C++ headers in extern "C". Do not do this:

    extern "C" {
    #include <vector>
    }
    

    C++ standard-library headers declare templates and C++ entities that cannot be given C linkage.

  • Using extern "C" to solve C/C++ data-layout problems. It solves linkage naming, not ABI compatibility for every type. Keep cross-language structures and ownership rules deliberately simple.

  • Compiling a .c file as C++ by accident. Build systems can override language choice. If a legacy C file is compiled as C++, it must be valid C++ and can behave differently from C.

Comparisons

TopicWhat it controlsTypical use
#ifdef __cplusplusPreprocessor conditional compilationMake a header valid in both C and C++
extern "C"C++ language linkage for declarationsLet C++ call functions implemented and exported as C
externStorage/linkage declaration in C or C++Declare a variable or function defined in another translation unit
staticInternal linkage or local lifetime, depending on contextHide file-local C functions or variables
Include guardsRepeated inclusion of a headerPrevent duplicate declarations in one translation unit

extern "C" is not the same as compiling source as C:

SituationResult
A .c file is compiled as CIts ordinary external functions use the platform's C ABI/name conventions.
A .cpp file declares extern "C" void run(void);The C++ caller looks for the C-linked run symbol.
A .cpp file defines extern "C" void run(void) {}It defines a C-linkage function, even though its body is compiled as C++.
A C++ declaration has no extern "C"It normally has C++ linkage and may be name-mangled.

A single-declaration form is useful for a small number of declarations:

extern "C" int vendor_initialize(void);

A braced form is usually clearer for a C API header with many declarations:

extern "C" {
    int vendor_initialize(void);
    void vendor_shutdown(void);
}
Take Quiz

Cheat Sheet

/* Put this in a public C API header. */
#ifndef MY_API_H
#define MY_API_H

#ifdef __cplusplus
extern "C" {
#endif

int my_api_start(void);
void my_api_stop(void);

#ifdef __cplusplus
}
#endif

#endif
  • __cplusplus is defined by C++ compilers, not by C compilers.
  • extern "C" is valid C++ syntax, not C syntax.
  • extern "C" does not undefine or change __cplusplus.
  • Nesting extern "C" blocks is valid when both specify C linkage; it is usually redundant.
  • Put guards around declarations in headers, because callers need to see C linkage.
  • Compile C implementation files as C; their normal external functions already use C linkage.
  • A C++ caller must see a declaration before calling a C function.
  • For an unguarded third-party C header, use one project-owned bridge header:
extern "C" {
#include <third_party_api.h>
}
  • Do not wrap C++ headers, templates, classes, overloads, or standard C++ library headers in extern "C".
  • Use the C++ linker driver for final executables that include C++ object files.

FAQ

Why is __cplusplus still defined inside an extern "C" block?

Because they are different mechanisms. __cplusplus is evaluated by the preprocessor, while extern "C" specifies linkage for C++ declarations. The linkage block does not modify macros.

Can C headers include other C headers that also use extern "C" guards?

Yes. The linkage specifications can be nested. Each guarded header works independently, and nested C linkage is harmless.

Does extern "C" make all code in the block compile as C?

No. The surrounding file is still compiled as C++. The block primarily affects language linkage for eligible declarations; it does not change C++ parsing rules into C parsing rules.

Should I put extern "C" around C function definitions in .c files?

No, not when the file is compiled as C. Use the conditional guard in the shared header, include that header from the .c file, and compile the implementation as C.

What if a C function has no prototype in the C++ source file?

The C++ compilation fails because the function is undeclared. Include the header that declares it with extern "C" linkage rather than creating an ad hoc declaration.

Is wrapping a third-party C header in extern "C" safe?

It is the usual solution if the header truly contains a C API and has no C++ guard. Put the wrapper in a dedicated adapter header. Do not do this for headers that contain C++ declarations.

Can C functions be overloaded in C++ when declared with extern "C"?

No. C linkage does not support C++ function overloading for the same external name. Give the functions distinct API names instead.

Does extern "C" guarantee binary compatibility everywhere?

No. It addresses language linkage, but binary compatibility can also depend on compiler, platform, calling convention, structure layout, alignment, and ownership rules.

Take Quiz

Mini Project

Description

Create a small C library that exposes a temperature conversion API, then call it from a C++ program. This demonstrates the normal production arrangement: a C-compatible header, a C implementation file, and a C++ consumer.

Goal

Build and run a C++ program that calls functions implemented in a separately compiled C source file.

Requirements

Create a C header with include guards and conditional extern "C" linkage guards. Implement two temperature conversion functions in a .c file. Include the C header from a .cpp file and print both conversion results. Compile the C source as C and the C++ source as C++. Link the final program with the C++ compiler driver.

Take Quiz

Keep learning

Related questions

View more