본문으로 건너뛰기 C++ and Rust Interoperability: FFI, C ABI, bindgen, cxx,

C++ and Rust Interoperability: FFI, C ABI, bindgen, cxx,

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

  1. FFI Fundamentals
  2. C ABI Bridge
  3. bindgen: Generate Rust Bindings
  4. cxx: Safe C++/Rust Interop
  5. Memory Safety Patterns
  6. Ownership Transfer
  7. Error Handling
  8. Production Patterns
  9. 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

ConceptDescription
FFIForeign Function Interface
C ABIUniversal bridge between languages
bindgenGenerate Rust bindings from C/C++
cxxSafe, zero-cost C++/Rust interop
Memory safetyValidate 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.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, Rust, FFI, interop, bindgen, cxx, memory-safety.