Home C / C++ C/C++ Interop and Foreign Function Interface: A Practical Guide
Advanced 3 min · July 13, 2026

C/C++ Interop and Foreign Function Interface: A Practical Guide

Master C/C++ interop and FFI for seamless integration with C libraries, Python, and more.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Written from production experience, not tutorials.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of C and C++ syntax
  • Understanding of compilation and linking
  • Familiarity with shared libraries
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • C/C++ interop allows C++ code to call C functions and vice versa using extern "C" and proper linkage.
  • FFI (Foreign Function Interface) enables calling C/C++ functions from other languages like Python, Rust, or Java.
  • Key challenges include name mangling, calling conventions, and data type compatibility.
  • Use extern "C" to prevent C++ name mangling for C-accessible functions.
  • Tools like ctypes, cffi, and JNI simplify FFI integration.
✦ Definition~90s read
What is C/C++ Interop and Foreign Function Interface?

C/C++ interop and FFI is the technique of making C and C++ code callable from each other and from other programming languages, using linkage specifications and shared libraries.

Think of C/C++ interop like a bilingual translator between two cultures.
Plain-English First

Think of C/C++ interop like a bilingual translator between two cultures. C++ and C are similar but have different customs (like name mangling). FFI is like a universal adapter that lets your C++ code talk to other languages, such as Python, as if they were speaking the same language.

In the real world, C++ rarely exists in isolation. You might need to call a legacy C library for hardware access, expose your C++ engine to a Python script, or integrate with a system written in Rust. This is where C/C++ interop and Foreign Function Interface (FFI) come into play. Interop allows C and C++ code to coexist in the same project, while FFI enables calling C/C++ functions from other languages. Without proper understanding, you'll face linker errors, crashes, and undefined behavior. This tutorial covers the essentials: using extern "C" to prevent name mangling, handling calling conventions, and safely passing data across language boundaries. We'll also explore real-world debugging techniques and a production incident that cost a company hours of downtime. By the end, you'll be able to integrate C/C++ code with confidence, whether you're wrapping a library for Python or building a cross-language system.

Understanding C/C++ Interop Basics

C and C++ are closely related but have different linkage requirements. C++ compilers mangle function names to support overloading, while C does not. To call a C function from C++, you must tell the compiler to use C linkage via extern "C". This prevents name mangling and ensures the function is callable with C calling conventions. Conversely, to call a C++ function from C, you need to declare it with extern "C" and avoid C++-only features (like references) in the interface. Here's a simple example:

interop_basic.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// C header: clib.h
#ifndef CLIB_H
#define CLIB_H
void c_function(int x);
#endif

// C++ file: main.cpp
#include <iostream>

extern "C" {
    #include "clib.h"
}

int main() {
    c_function(42);
    return 0;
}

// C implementation: clib.c
#include <stdio.h>
void c_function(int x) {
    printf("C function called with %d\n", x);
}
Output
C function called with 42
💡Always use `extern "C"` for C headers
📊 Production Insight
In large codebases, missing extern "C" can cause subtle bugs that only appear when linking with specific compilers.
🎯 Key Takeaway
Use extern "C" to prevent C++ name mangling when calling C functions.

Calling C++ Functions from C

To expose C++ functions to C code, you need to declare them with extern "C" and ensure they have C-compatible signatures (no references, no overloads, no templates). You can also wrap C++ classes in C-style functions using opaque pointers. This is common when writing library bindings. Example:

cpp_to_c.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// C++ code: cpplib.cpp
#include <iostream>

class MyClass {
public:
    void greet(const char* name) {
        std::cout << "Hello, " << name << "!" << std::endl;
    }
};

// C-compatible wrapper
extern "C" {
    void* MyClass_create() {
        return new MyClass();
    }
    void MyClass_greet(void* obj, const char* name) {
        static_cast<MyClass*>(obj)->greet(name);
    }
    void MyClass_destroy(void* obj) {
        delete static_cast<MyClass*>(obj);
    }
}

// C caller: main.c
#include <stdio.h>

void* MyClass_create();
void MyClass_greet(void* obj, const char* name);
void MyClass_destroy(void* obj);

int main() {
    void* obj = MyClass_create();
    MyClass_greet(obj, "World");
    MyClass_destroy(obj);
    return 0;
}
Output
Hello, World!
⚠ Memory management across languages
📊 Production Insight
Always provide a destroy function to avoid memory leaks when the caller is in a different language.
🎯 Key Takeaway
Expose C++ classes to C using opaque pointers and C-style wrapper functions.

Foreign Function Interface (FFI) with Python

FFI allows calling C/C++ functions from languages like Python. The most common tools are ctypes and cffi. You need to compile your C/C++ code into a shared library (.so on Linux, .dll on Windows, .dylib on macOS) and then load it. Example using ctypes:

ffi_example.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// C++ code: math_ops.cpp
extern "C" {
    double add(double a, double b) {
        return a + b;
    }
    double multiply(double a, double b) {
        return a * b;
    }
}

// Compile: g++ -shared -o libmath.so math_ops.cpp

// Python code: use_math.py
import ctypes

lib = ctypes.CDLL('./libmath.so')
lib.add.restype = ctypes.c_double
lib.add.argtypes = [ctypes.c_double, ctypes.c_double]

result = lib.add(3.5, 2.7)
print(f"Result: {result}")
Output
Result: 6.2
🔥Setting argument and return types
📊 Production Insight
In production, consider using cffi for better performance and easier handling of complex types.
🎯 Key Takeaway
Use ctypes or cffi to call C/C++ shared libraries from Python, but always specify argument and return types.

Handling Complex Data Types Across Boundaries

Passing structs, arrays, and strings between C++ and other languages requires careful alignment and memory management. Use #pragma pack to control struct padding, and ensure that both sides agree on the layout. For strings, prefer null-terminated C strings. Example with struct:

struct_interop.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// C++ code: struct_ops.cpp
#include <cstdint>

#pragma pack(push, 1)
struct Point {
    int32_t x;
    int32_t y;
};
#pragma pack(pop)

extern "C" {
    int32_t sum_coords(Point p) {
        return p.x + p.y;
    }
}

// Python code: use_struct.py
import ctypes

class Point(ctypes.Structure):
    _fields_ = [("x", ctypes.c_int32),
                ("y", ctypes.c_int32)]

lib = ctypes.CDLL('./libstruct.so')
lib.sum_coords.argtypes = [Point]
lib.sum_coords.restype = ctypes.c_int32

p = Point(10, 20)
print(lib.sum_coords(p))
Output
30
⚠ Struct packing differences
📊 Production Insight
Use fixed-width integer types (e.g., int32_t) to ensure size consistency across platforms.
🎯 Key Takeaway
Control struct packing explicitly to avoid misalignment when passing structs across FFI boundaries.

Calling Conventions and Platform Differences

Calling conventions define how functions receive arguments and return values. Common conventions are cdecl (default in C/C++ on x86) and stdcall (used by Win32 API). On x86-64, there's a single convention (System V AMD64 on Unix, Microsoft x64 on Windows). When mixing languages, ensure both sides use the same convention. Example with __stdcall on Windows:

calling_conv.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Windows DLL: winlib.cpp
#include <windows.h>

extern "C" {
    __declspec(dllexport) int __stdcall add_stdcall(int a, int b) {
        return a + b;
    }
}

// Python ctypes with stdcall
import ctypes

lib = ctypes.WinDLL('./winlib.dll')
lib.add_stdcall.argtypes = [ctypes.c_int, ctypes.c_int]
lib.add_stdcall.restype = ctypes.c_int
print(lib.add_stdcall(3, 4))
Output
7
💡Use `CDLL` for cdecl, `WinDLL` for stdcall
📊 Production Insight
On x86-64, calling conventions are uniform, but on x86 (32-bit), you must be explicit.
🎯 Key Takeaway
Match calling conventions between caller and callee to prevent stack corruption.

Error Handling and Resource Management

C++ exceptions cannot cross language boundaries. Instead, use error codes or set a global error state. For resources, provide explicit create/destroy functions and consider using RAII wrappers on the C++ side. Example:

error_handling.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// C++ code: safe_ops.cpp
#include <cstring>

static char last_error[256] = {0};

extern "C" {
    int divide(int a, int b, int* result) {
        if (b == 0) {
            strncpy(last_error, "Division by zero", sizeof(last_error) - 1);
            return -1;
        }
        *result = a / b;
        return 0;
    }
    const char* get_last_error() {
        return last_error;
    }
}

// Python code
import ctypes

lib = ctypes.CDLL('./liberror.so')
lib.divide.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int)]
lib.divide.restype = ctypes.c_int
lib.get_last_error.restype = ctypes.c_char_p

result = ctypes.c_int()
ret = lib.divide(10, 0, ctypes.byref(result))
if ret != 0:
    print(f"Error: {lib.get_last_error().decode()}")
else:
    print(f"Result: {result.value}")
Output
Error: Division by zero
🔥Never let exceptions cross the boundary
📊 Production Insight
Consider using a thread-local error buffer for thread safety.
🎯 Key Takeaway
Use error codes instead of exceptions for FFI functions, and provide a way to retrieve error details.

Security Best Practices for FFI

FFI introduces security risks: buffer overflows, type confusion, and injection attacks. Always validate input sizes, use bounded string functions, and avoid passing user data directly to system calls. Example of safe string handling:

safe_string.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <cstring>
#include <cstdio>

extern "C" {
    void safe_print(const char* msg, size_t len) {
        // Use bounded copy to avoid overflow
        char buffer[256];
        if (len >= sizeof(buffer)) {
            len = sizeof(buffer) - 1;
        }
        strncpy(buffer, msg, len);
        buffer[len] = '\0';
        printf("%s\n", buffer);
    }
}
⚠ Never trust input from other languages
📊 Production Insight
Use static analysis tools to detect potential overflows in FFI wrappers.
🎯 Key Takeaway
Sanitize all inputs from FFI callers to prevent security vulnerabilities.
● Production incidentPOST-MORTEMseverity: high

The Silent Crash: A Name Mangling Disaster

Symptom
A C++ server crashed with a segmentation fault when calling a C library function.
Assumption
The developer assumed that C functions are automatically callable from C++ without any special syntax.
Root cause
The C library header was included directly in C++ code without wrapping in extern "C", causing C++ name mangling to produce a different symbol name than the C library exported.
Fix
Wrap the C header inclusion with extern "C" { #include "clib.h" } and ensure the library is linked with C linkage.
Key lesson
  • Always use extern "C" when including C headers in C++.
  • Verify symbol names using nm or objdump to ensure linkage matches.
  • Test interop with a minimal example before integrating into production.
  • Use #ifdef __cplusplus guards in C headers for automatic compatibility.
  • Document calling conventions (e.g., cdecl, stdcall) for cross-platform code.
Production debug guideSymptom to Action4 entries
Symptom · 01
Linker error: undefined reference to function
Fix
Check if function is declared with extern "C" and if the library is linked correctly.
Symptom · 02
Segmentation fault when calling C function from C++
Fix
Verify calling convention (e.g., __cdecl vs __stdcall) and data alignment.
Symptom · 03
Wrong values passed to C function
Fix
Ensure struct packing matches between C and C++ (use #pragma pack or __attribute__((packed))).
Symptom · 04
Python ctypes returns garbage
Fix
Check that the function signature in ctypes matches the C declaration, including return types and argument types.
★ Quick Debug Cheat SheetCommon interop issues and immediate fixes.
Undefined reference
Immediate action
Add `extern "C"`
Commands
nm -C object.o | grep function
objdump -T library.so | grep function
Fix now
Wrap header with extern "C" {}
Segfault on call+
Immediate action
Check calling convention
Commands
g++ -S -o - | grep call
readelf -s library.so | grep function
Fix now
Add __cdecl or __stdcall attribute
Wrong values+
Immediate action
Check struct packing
Commands
sizeof(struct) in both languages
g++ -E -dM | grep PACK
Fix now
Use #pragma pack(1) or __attribute__((packed))
FeatureC InteropC++ Interop
Name manglingNoneMangled by default; use extern "C" to disable
Calling conventioncdecl (default)cdecl or stdcall; can be specified
Exception handlingNot supportedMust be caught; cannot cross boundary
OverloadingNot supportedSupported; requires mangling
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
interop_basic.cppvoid c_function(int x);Understanding C/C++ Interop Basics
cpp_to_c.cppclass MyClass {Calling C++ Functions from C
ffi_example.cppextern "C" {Foreign Function Interface (FFI) with Python
struct_interop.cppstruct Point {Handling Complex Data Types Across Boundaries
calling_conv.cppextern "C" {Calling Conventions and Platform Differences
error_handling.cppstatic char last_error[256] = {0};Error Handling and Resource Management
safe_string.cppextern "C" {Security Best Practices for FFI

Key takeaways

1
Use extern "C" to bridge C and C++ code seamlessly.
2
Expose C++ classes via opaque pointers and C-style wrappers.
3
Always specify argument and return types in FFI calls.
4
Control struct packing and use fixed-width types for portability.
5
Handle errors with codes, not exceptions, across language boundaries.

Common mistakes to avoid

3 patterns
×

Forgetting `extern "C"` when including C headers in C++

×

Mixing `malloc`/`free` with `new`/`delete` across boundaries

×

Not setting `restype` and `argtypes` in ctypes

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain how `extern "C"` works and why it's needed.
Q02SENIOR
How would you expose a C++ class to a C program?
Q03SENIOR
What are the security risks of FFI and how do you mitigate them?
Q01 of 03JUNIOR

Explain how `extern "C"` works and why it's needed.

ANSWER
extern "C" disables C++ name mangling for the declared functions, allowing them to be linked with C code. It also ensures C calling conventions are used.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is the difference between `extern "C"` and `extern "C++"`?
02
Can I call C++ template functions from C?
03
How do I pass a C++ object to Python via FFI?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Written from production experience, not tutorials.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's C++ Advanced. Mark it forged?

3 min read · try the examples if you haven't

Previous
C++ Debugging: GDB, Valgrind, and Sanitizers
33 / 41 · C++ Advanced
Next
AddressSanitizer, UBSan, TSan, and MSan Deep-Dive