C++ and Rust Interoperability: FFI, C ABI, bindgen, cxx,
Why C++ and Rust Interoperability Matters
Problem: Hybrid Codebases
Problem: You have:
- Existing C++ codebase (millions of lines)
- Need Rust’s memory safety for new features
- Cannot rewrite everything in Rust Solution: FFI (Foreign Function Interface) via C ABI bridge.
flowchart LR
subgraph Rust
R[Rust Code]
end
subgraph C ABI
C["C ABI Bridge\nextern C"]
end
subgraph C++
CPP[C++ Code]
end
R <-->|FFI| C
C <-->|FFI| CPP
Table of Contents
- FFI Fundamentals
- C ABI Bridge
- bindgen: Generate Rust Bindings
- cxx: Safe C++/Rust Interop
- Memory Safety Patterns
- Ownership Transfer
- Error Handling
- Production Patterns
- Complete Example
1. FFI Fundamentals
What is FFI?
FFI (Foreign Function Interface): Mechanism to call functions written in one language from another.
C ABI: Universal Bridge
C ABI (Application Binary Interface): Standard calling convention and data layout.
- C++:
extern "C"disables name mangling - Rust:
extern "C"uses C calling convention
// C++ side
extern "C" {
int add(int a, int b) {
return a + b;
}
}
// Rust side
extern "C" {
fn add(a: i32, b: i32) -> i32;
}
fn main() {
unsafe {
let result = add(5, 3);
println!("5 + 3 = {}", result); // 8
}
}
Key: C ABI is the bridge between C++ and Rust.
2. C ABI Bridge
Calling C++ from Rust
C++ Library
// math.h
#ifndef MATH_H
#define MATH_H
#ifdef __cplusplus
extern "C" {
#endif
int add(int a, int b);
int multiply(int a, int b);
#ifdef __cplusplus
}
#endif
#endif
// math.cpp
#include "math.h"
extern "C" {
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
}
Rust Bindings
// build.rs
fn main() {
cc::Build::new()
.file("math.cpp")
.compile("math");
}
// lib.rs
extern "C" {
fn add(a: i32, b: i32) -> i32;
fn multiply(a: i32, b: i32) -> i32;
}
pub fn safe_add(a: i32, b: i32) -> i32 {
unsafe { add(a, b) }
}
pub fn safe_multiply(a: i32, b: i32) -> i32 {
unsafe { multiply(a, b) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(safe_add(2, 3), 5);
}
#[test]
fn test_multiply() {
assert_eq!(safe_multiply(4, 5), 20);
}
}
Key: Wrap unsafe FFI calls in safe Rust functions.
Calling Rust from C++
Rust Library
// lib.rs
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn rust_multiply(a: i32, b: i32) -> i32 {
a * b
}
C++ Usage
// main.cpp
#include <iostream>
extern "C" {
int rust_add(int a, int b);
int rust_multiply(int a, int b);
}
int main() {
std::cout << "5 + 3 = " << rust_add(5, 3) << "\n"; // 8
std::cout << "4 * 5 = " << rust_multiply(4, 5) << "\n"; // 20
}
Build:
# Build Rust library
cargo build --release
# Link with C++
g++ main.cpp -L target/release -lrust_math -o main
./main
Output:
5 + 3 = 8
4 * 5 = 20
Key: #[no_mangle] prevents Rust name mangling; extern "C" uses C ABI.
3. bindgen: Generate Rust Bindings
What is bindgen?
bindgen: Automatically generates Rust FFI bindings from C/C++ headers.
Installation
cargo install bindgen-cli
Example: Binding C++ Class
C++ Header
// vector.h
#ifndef VECTOR_H
#define VECTOR_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct Vector Vector;
Vector* vector_new();
void vector_delete(Vector* v);
void vector_push(Vector* v, int value);
int vector_get(const Vector* v, int index);
int vector_size(const Vector* v);
#ifdef __cplusplus
}
#endif
#endif
C++ Implementation
// vector.cpp
#include "vector.h"
#include <vector>
struct Vector {
std::vector<int> data;
};
extern "C" {
Vector* vector_new() {
return new Vector();
}
void vector_delete(Vector* v) {
delete v;
}
void vector_push(Vector* v, int value) {
v->data.push_back(value);
}
int vector_get(const Vector* v, int index) {
return v->data[index];
}
int vector_size(const Vector* v) {
return v->data.size();
}
}
Generate Bindings
bindgen vector.h -o bindings.rs
Rust Wrapper
// lib.rs
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
pub struct SafeVector {
ptr: *mut Vector,
}
impl SafeVector {
pub fn new() -> Self {
unsafe {
Self {
ptr: vector_new(),
}
}
}
pub fn push(&mut self, value: i32) {
unsafe {
vector_push(self.ptr, value);
}
}
pub fn get(&self, index: usize) -> i32 {
unsafe {
vector_get(self.ptr, index as i32)
}
}
pub fn len(&self) -> usize {
unsafe {
vector_size(self.ptr) as usize
}
}
}
impl Drop for SafeVector {
fn drop(&mut self) {
unsafe {
vector_delete(self.ptr);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vector() {
let mut vec = SafeVector::new();
vec.push(10);
vec.push(20);
vec.push(30);
assert_eq!(vec.len(), 3);
assert_eq!(vec.get(0), 10);
assert_eq!(vec.get(1), 20);
assert_eq!(vec.get(2), 30);
}
}
Key: bindgen generates unsafe bindings; wrap in safe Rust API.
4. cxx: Safe C++/Rust Interop
What is cxx?
cxx: Safe, zero-cost C++/Rust interop with compile-time checks.
Installation
# Cargo.toml
[dependencies]
cxx = "1.0"
[build-dependencies]
cxx-build = "1.0"
Example: Shared Types
Bridge Definition
// src/lib.rs
#[cxx::bridge]
mod ffi {
// Shared types
struct Point {
x: i32,
y: i32,
}
// C++ functions
extern "C++" {
include!("geometry.h");
fn distance(p1: &Point, p2: &Point) -> f64;
}
// Rust functions
extern "Rust" {
fn midpoint(p1: &Point, p2: &Point) -> Point;
}
}
// Rust implementation
fn midpoint(p1: &ffi::Point, p2: &ffi::Point) -> ffi::Point {
ffi::Point {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2,
}
}
C++ Implementation
// geometry.h
#pragma once
#include "lib.rs.h"
double distance(const Point& p1, const Point& p2);
// geometry.cpp
#include "geometry.h"
#include <cmath>
double distance(const Point& p1, const Point& p2) {
int dx = p2.x - p1.x;
int dy = p2.y - p1.y;
return std::sqrt(dx * dx + dy * dy);
}
Build Script
// build.rs
fn main() {
cxx_build::bridge("src/lib.rs")
.file("geometry.cpp")
.compile("geometry");
}
Usage
// main.rs
use crate::ffi::Point;
fn main() {
let p1 = Point { x: 0, y: 0 };
let p2 = Point { x: 3, y: 4 };
let dist = ffi::distance(&p1, &p2);
println!("Distance: {}", dist); // 5.0
let mid = midpoint(&p1, &p2);
println!("Midpoint: ({}, {})", mid.x, mid.y); // (1, 2)
}
Key: cxx provides type-safe, zero-cost interop with compile-time checks.
5. Memory Safety Patterns
Pattern 1: Opaque Pointers
// Rust side
pub struct OpaqueHandle {
_private: [u8; 0],
}
#[no_mangle]
pub extern "C" fn create_handle() -> *mut OpaqueHandle {
Box::into_raw(Box::new(MyStruct::new())) as *mut OpaqueHandle
}
#[no_mangle]
pub extern "C" fn destroy_handle(handle: *mut OpaqueHandle) {
if !handle.is_null() {
unsafe {
let _ = Box::from_raw(handle as *mut MyStruct);
}
}
}
Key: Hide implementation details behind opaque pointer.
Pattern 2: Lifetime Validation
// Rust side
#[repr(C)]
pub struct Buffer {
data: *const u8,
len: usize,
}
#[no_mangle]
pub extern "C" fn get_buffer(s: &str) -> Buffer {
Buffer {
data: s.as_ptr(),
len: s.len(),
}
}
// C++ side must not outlive Rust string
Key: Document lifetime constraints in FFI boundary.
Pattern 3: Null Pointer Checks
#[no_mangle]
pub extern "C" fn process_data(data: *const u8, len: usize) -> i32 {
if data.is_null() {
return -1; // Error code
}
unsafe {
let slice = std::slice::from_raw_parts(data, len);
// Process slice
0 // Success
}
}
Key: Always validate pointers at FFI boundary.
6. Ownership Transfer
C++ → Rust
// C++ side: transfer ownership
extern "C" {
void rust_take_ownership(int* data);
}
void transfer() {
int* data = new int(42);
rust_take_ownership(data); // Rust now owns data
// Do NOT delete data here
}
// Rust side: take ownership
#[no_mangle]
pub extern "C" fn rust_take_ownership(data: *mut i32) {
if !data.is_null() {
unsafe {
let boxed = Box::from_raw(data);
println!("Received: {}", *boxed);
// boxed dropped here, memory freed
}
}
}
Rust → C++
// Rust side: transfer ownership
#[no_mangle]
pub extern "C" fn rust_create_data() -> *mut i32 {
Box::into_raw(Box::new(42))
}
// C++ side: take ownership
extern "C" {
int* rust_create_data();
}
void receive() {
int* data = rust_create_data();
std::cout << "Received: " << *data << "\n";
delete data; // C++ must free
}
Key: Document ownership transfer clearly in API.
7. Error Handling
Pattern 1: Error Codes
#[repr(C)]
pub enum ErrorCode {
Success = 0,
InvalidInput = 1,
OutOfMemory = 2,
Unknown = 3,
}
#[no_mangle]
pub extern "C" fn process(input: *const u8, len: usize) -> ErrorCode {
if input.is_null() {
return ErrorCode::InvalidInput;
}
unsafe {
let slice = std::slice::from_raw_parts(input, len);
match do_process(slice) {
Ok(_) => ErrorCode::Success,
Err(_) => ErrorCode::Unknown,
}
}
}
Pattern 2: Out Parameters
#[no_mangle]
pub extern "C" fn divide(a: i32, b: i32, result: *mut i32) -> bool {
if b == 0 || result.is_null() {
return false;
}
unsafe {
*result = a / b;
}
true
}
int result;
if (divide(10, 2, &result)) {
std::cout << "Result: " << result << "\n"; // 5
} else {
std::cerr << "Error\n";
}
Pattern 3: Exception Boundary
// C++ side: catch exceptions at FFI boundary
extern "C" int cpp_function_that_may_throw(int x) {
try {
return risky_operation(x);
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << "\n";
return -1; // Error code
} catch (...) {
std::cerr << "Unknown exception\n";
return -1;
}
}
Key: Never let exceptions cross FFI boundary—catch and convert to error codes.
8. Production Patterns
Pattern 1: String Conversion
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn rust_process_string(s: *const c_char) -> *mut c_char {
if s.is_null() {
return std::ptr::null_mut();
}
unsafe {
let c_str = CStr::from_ptr(s);
let rust_str = match c_str.to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let result = rust_str.to_uppercase();
match CString::new(result) {
Ok(c_string) => c_string.into_raw(),
Err(_) => std::ptr::null_mut(),
}
}
}
#[no_mangle]
pub extern "C" fn rust_free_string(s: *mut c_char) {
if !s.is_null() {
unsafe {
let _ = CString::from_raw(s);
}
}
}
extern "C" {
char* rust_process_string(const char* s);
void rust_free_string(char* s);
}
void test() {
char* result = rust_process_string("hello");
if (result) {
std::cout << result << "\n"; // HELLO
rust_free_string(result);
}
}
Key: Use CString/CStr for C-compatible strings; provide free function.
Pattern 2: Callback Functions
type Callback = extern "C" fn(i32) -> i32;
#[no_mangle]
pub extern "C" fn rust_map(data: *const i32, len: usize, callback: Callback, out: *mut i32) {
if data.is_null() || out.is_null() {
return;
}
unsafe {
let input = std::slice::from_raw_parts(data, len);
let output = std::slice::from_raw_parts_mut(out, len);
for (i, &value) in input.iter().enumerate() {
output[i] = callback(value);
}
}
}
extern "C" {
void rust_map(const int* data, size_t len, int (*callback)(int), int* out);
}
extern "C" int double_value(int x) {
return x * 2;
}
void test() {
int data[] = {1, 2, 3, 4, 5};
int out[5];
rust_map(data, 5, double_value, out);
for (int i = 0; i < 5; ++i) {
std::cout << out[i] << " "; // 2 4 6 8 10
}
}
Key: Use function pointers for callbacks across FFI.
Pattern 3: Shared Memory
#[repr(C)]
pub struct SharedData {
pub counter: std::sync::atomic::AtomicI32,
pub flag: std::sync::atomic::AtomicBool,
}
#[no_mangle]
pub extern "C" fn create_shared_data() -> *mut SharedData {
Box::into_raw(Box::new(SharedData {
counter: std::sync::atomic::AtomicI32::new(0),
flag: std::sync::atomic::AtomicBool::new(false),
}))
}
#[no_mangle]
pub extern "C" fn increment_counter(data: *mut SharedData) {
if !data.is_null() {
unsafe {
(*data).counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
}
}
Key: Use atomic types for thread-safe shared memory.
9. Complete Example
Rust Library
// lib.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[repr(C)]
pub struct Image {
width: u32,
height: u32,
data: *mut u8,
}
#[no_mangle]
pub extern "C" fn image_create(width: u32, height: u32) -> *mut Image {
let size = (width * height * 4) as usize;
let mut data = vec![0u8; size];
let data_ptr = data.as_mut_ptr();
std::mem::forget(data);
Box::into_raw(Box::new(Image {
width,
height,
data: data_ptr,
}))
}
#[no_mangle]
pub extern "C" fn image_destroy(img: *mut Image) {
if !img.is_null() {
unsafe {
let img = Box::from_raw(img);
let size = (img.width * img.height * 4) as usize;
let _ = Vec::from_raw_parts(img.data, size, size);
}
}
}
#[no_mangle]
pub extern "C" fn image_fill(img: *mut Image, r: u8, g: u8, b: u8, a: u8) {
if img.is_null() {
return;
}
unsafe {
let img = &*img;
let size = (img.width * img.height * 4) as usize;
let data = std::slice::from_raw_parts_mut(img.data, size);
for i in (0..size).step_by(4) {
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
data[i + 3] = a;
}
}
}
#[no_mangle]
pub extern "C" fn image_save(img: *const Image, path: *const c_char) -> bool {
if img.is_null() || path.is_null() {
return false;
}
unsafe {
let img = &*img;
let c_str = CStr::from_ptr(path);
let path_str = match c_str.to_str() {
Ok(s) => s,
Err(_) => return false,
};
// Save image (simplified)
println!("Saving {}x{} image to {}", img.width, img.height, path_str);
true
}
}
C++ Usage
// main.cpp
#include <iostream>
extern "C" {
struct Image;
Image* image_create(uint32_t width, uint32_t height);
void image_destroy(Image* img);
void image_fill(Image* img, uint8_t r, uint8_t g, uint8_t b, uint8_t a);
bool image_save(const Image* img, const char* path);
}
int main() {
// Create 800x600 image
Image* img = image_create(800, 600);
// Fill with red
image_fill(img, 255, 0, 0, 255);
// Save
if (image_save(img, "output.png")) {
std::cout << "Image saved successfully\n";
} else {
std::cerr << "Failed to save image\n";
}
// Cleanup
image_destroy(img);
return 0;
}
Build:
# Build Rust library
cargo build --release
# Compile C++
g++ main.cpp -L target/release -limage_lib -o main
# Run
./main
Output:
Saving 800x600 image to output.png
Image saved successfully
Key: Complete FFI example with resource management and error handling.
Summary
Key Concepts
| Concept | Description |
|---|---|
| FFI | Foreign Function Interface |
| C ABI | Universal bridge between languages |
| bindgen | Generate Rust bindings from C/C++ |
| cxx | Safe, zero-cost C++/Rust interop |
| Memory safety | Validate pointers, document ownership |
| C++ and Rust interop requires C ABI bridge, careful memory management, and clear ownership semantics. |
Keywords
C++ Rust interop, FFI, C ABI, bindgen, cxx, memory safety, ownership transfer One-line summary: Master C++ and Rust interoperability using FFI, C ABI, bindgen, and cxx for safe, zero-cost hybrid codebases with clear ownership semantics.
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Master C++ and Rust interop: FFI fundamentals, C ABI bridge, bindgen for C++ bindings, cxx for safe interop, memory safe…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ 스마트 포인터와 순환 참조(Circular Reference) 해결법 [#33-3]
- C++ Lock-Free 프로그래밍 실전 | CAS·ABA·메모리 순서·고성능 큐 [#34-3]
- C++ RAII 패턴 | ‘리소스 관리’ 완벽 가이드
Keywords Covered in This Article (Related Search Terms)
This article covers C++, Rust, FFI, interop, bindgen, cxx, memory-safety.