C Interop

By the end of this lesson you'll be able to call C libraries from C++ and expose C++ functions to C using extern "C" , write a header both languages can include, pass structs and pointers across the boundary safely, and avoid the traps — name mangling, escaping exceptions, and handing C++ objects to C.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Think of a function name as a label on a parcel . C++ writes a detailed label that encodes the argument types — add(int,int) becomes something like _Z3addii — so it can tell two functions called add apart (that's overloading). C writes a plain label: just add . When a C courier goes looking for the parcel labelled add but C++ filed it under _Z3addii , delivery fails — that's a linker error . extern "C" tells the C++ side to use the plain label, so both couriers agree on the address. Everything in this lesson is about keeping the labels matched.

Rule of thumb: anything C understands is plain data and plain functions . The C++ features that need name mangling (overloading, templates, namespaces) are exactly the ones that can't cross.

1. Name Mangling & extern "C"

Name mangling is how C++ encodes a function's argument types into its symbol name so that print(int) and print(double) can coexist — that's what makes overloading possible. C doesn't mangle: a function is just its bare name. So if C++ and C try to link to the same function, the names won't match. extern "C" is the fix: it tells the C++ compiler to give a function C linkage — the plain, unmangled name and C calling convention — so both languages can find it. Run this and notice the functions behave normally; the difference is invisible until link time.

In a real project the declarations live in a header shared by both languages. The trick is the __cplusplus macro, which only a C++ compiler defines. You wrap the prototypes in extern "C" only when C++ is reading the header, and a C compiler skips those lines entirely. This is the interop pattern — memorise its shape.

Your turn. The program below won't link cleanly for a C caller because two functions are still mangled. Add C linkage where the comments point — fill in the two ___ blanks, then run it.

2. Passing Structs & Pointers Across the Boundary

Once the names line up, you still have to pass data C understands. C has no references and no std::string , so you use pointers and POD structs — "plain old data", a struct of fields with no methods or constructors, laid out the same way in both languages. To return a value you write through a pointer (an out-parameter), and arrays travel as a pointer plus a length, because a raw array carries no size of its own.

3. Exposing C++ to C (Safely)

Going the other way — letting C call your C++ code — adds one hard rule: no C++ exception may escape into C . A C stack frame has no idea how to unwind one, so an escaping throw means undefined behaviour (usually a crash). The pattern is a thin extern "C" wrapper that try / catch es everything and reports problems the C way: a return code plus an out-parameter for the result. The rich C++ logic stays safely behind the wrapper.

Now you try. Below is a C++ helper and a half-finished C-facing wrapper. Give the wrapper C linkage and make it report bad input with a return value instead of throwing — fill in the two blanks:

4. Calling Classic C APIs from C++

You'll use C libraries constantly — SQLite, OpenSSL, zlib, and POSIX are all C. The C++ standard library even ships the C headers for you: becomes , becomes , and they already wrap their declarations in extern "C" . A classic example is qsort , which takes a function pointer as a callback. Note a quirk: a capturing lambda has hidden state and cannot become a plain function pointer; a stateless lambda (no captures) can.

extern "C" affects linkage only — the symbol name and calling convention. It does not turn off C++ inside the function body: you can still use std::string , classes, and the STL in there, as long as none of it leaks across the boundary as a parameter, return type, or escaping exception.

Because the name is unmangled, you lose the features that depend on mangling: no overloading (two functions would share one symbol), no namespaces in the symbol, no templates. Each C-facing function needs one unique, bare name.

No blanks this time — just a brief and a blank canvas (with an outline to keep you on track). Build a POD struct and an extern "C" function that fills it in through a pointer, then run it and check your output against the example in the comments.

Practice quiz

What does extern "C" actually change about a function?

  • It rewrites the body in C
  • It makes the function faster
  • It gives the function C linkage: a plain, unmangled symbol name
  • It disables the C++ standard library inside it

Answer: It gives the function C linkage: a plain, unmangled symbol name. extern "C" affects linkage only — the symbol name and calling convention — so C and C++ agree on the name.

Why does a missing extern "C" cause an error only at link time, not while compiling?

  • Compiling checks the declaration exists; linking checks the matching definition's symbol
  • The compiler ignores C functions
  • C++ never compiles C calls
  • Linking happens first

Answer: Compiling checks the declaration exists; linking checks the matching definition's symbol. The C++ caller compiles fine against a declaration but the linker can't find the mangled name the C object never exported.

Can you overload a function declared extern "C"?

  • Yes, always
  • Only with different return types
  • Only inside a namespace
  • No — C has no mangling, so two overloads would share one symbol and collide

Answer: No — C has no mangling, so two overloads would share one symbol and collide. Overloading needs mangling to make distinct symbols; with C linkage the names collide, so each C-facing function needs a unique name.

What happens if a C++ exception escapes into C code?

  • C catches it normally
  • Behaviour is undefined — typically std::terminate and a crash
  • It is silently ignored
  • It converts to a return code automatically

Answer: Behaviour is undefined — typically std::terminate and a crash. A C frame can't unwind a C++ exception, so a C-facing wrapper must catch everything and report errors via a return code.

In the dual-language header pattern, what is the role of the __cplusplus macro?

  • It is defined only by C++ compilers, so only C++ wraps the prototypes in extern "C"
  • It enables optimizations
  • It is defined only by C compilers
  • It includes the C standard library

Answer: It is defined only by C++ compilers, so only C++ wraps the prototypes in extern "C". A C compiler skips the extern "C" lines entirely; a C++ compiler sees __cplusplus and wraps the declarations.

How should you pass a std::string's text to a C function?

  • Pass the std::string object directly
  • Cast it to void*
  • Pass str.c_str() — a const char*
  • Pass &str

Answer: Pass str.c_str() — a const char*. C doesn't know std::string's layout; pass plain data: str.c_str() for the text, vec.data()/vec.size() for arrays.

Across the C boundary, how does a C-style API return a value into a struct?

  • By reference (Point&)
  • By writing through a pointer (an out-parameter)
  • By throwing it
  • By returning a std::string

Answer: By writing through a pointer (an out-parameter). C has no references, so you pass a pointer and write through it, e.g. void makePoint(Point* p, ...).

What kind of struct is safe to pass across the C boundary?

  • Any C++ class
  • A class with virtual functions
  • A template struct
  • A POD struct: plain fields, no methods, no constructors

Answer: A POD struct: plain fields, no methods, no constructors. Only plain-old-data structs are laid out identically in both languages and safe to share byte-for-byte.

When calling C's qsort from C++, the comparator must be:

  • A capturing lambda
  • A plain function pointer (a stateless/no-capture lambda is acceptable)
  • A std::function
  • A member function

Answer: A plain function pointer (a stateless/no-capture lambda is acceptable). qsort takes a function pointer; a capturing lambda has hidden state and can't convert, but a stateless lambda can.

Memory allocated with C's malloc must be released with:

  • delete

Always match allocators: malloc/free and new/delete. Mixing them is undefined behaviour.