convert-cpp-rust

aRustyDev's avatarfrom aRustyDev

Convert C++ code to idiomatic Rust. Use when migrating C++ projects to Rust, translating C++ patterns to idiomatic Rust, or refactoring C++ codebases. Extends meta-convert-dev with C++-to-Rust specific patterns, including FFI-based gradual migration.

⭐1stars🔀1forks📁View on GitHub🕐Updated Jan 4, 2026

When & Why to Use This Skill

This Claude skill automates the complex process of migrating C++ codebases to idiomatic Rust. It provides deep technical mappings for type systems, memory management (RAII to ownership), and design patterns, while offering specialized strategies for gradual migration using FFI and the cxx crate to ensure memory safety and performance.

Use Cases

  • Legacy Project Migration: Converting established C++ libraries or applications to Rust to eliminate memory-related vulnerabilities and leverage modern concurrency.
  • Pattern Translation: Mapping C++ specific constructs like templates, virtual functions, and smart pointers (unique_ptr/shared_ptr) to Rust generics, traits, and ownership models.
  • Incremental Refactoring: Setting up FFI bridges and interop layers to allow a phased transition where C++ and Rust modules coexist safely during the migration process.
  • Error Handling Modernization: Translating C++ exception-based logic into type-safe Rust Result and Option patterns for more predictable and robust code.
nameconvert-cpp-rust
descriptionConvert C++ code to idiomatic Rust. Use when migrating C++ projects to Rust, translating C++ patterns to idiomatic Rust, or refactoring C++ codebases. Extends meta-convert-dev with C++-to-Rust specific patterns, including FFI-based gradual migration.

Convert C++ to Rust

Convert C++ code to idiomatic Rust. This skill extends meta-convert-dev with C++-to-Rust specific type mappings, idiom translations, and FFI strategies for gradual migration.

This Skill Extends

  • meta-convert-dev - Foundational conversion patterns (APTV workflow, testing strategies, FFI patterns)

For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.

This Skill Adds

  • Type mappings: C++ types → Rust types (RAII → ownership, smart pointers → Box/Rc/Arc)
  • Idiom translations: C++ patterns → idiomatic Rust (templates → generics, virtual functions → traits)
  • Error handling: C++ exceptions → Rust Result<T, E>
  • Memory/Ownership: RAII/smart pointers → ownership/borrowing system
  • FFI Integration: cxx crate for safe C++/Rust interop during migration
  • Template patterns: C++ templates → Rust generics with trait bounds

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • C++ language fundamentals - see lang-cpp-dev
  • Rust language fundamentals - see lang-rust-dev
  • Reverse conversion (Rust → C++) - see convert-rust-cpp (if exists)
  • Advanced C++ metaprogramming (SFINAE, CRTP) - complex patterns require case-by-case analysis

Quick Reference

C++ Rust Notes
std::string String Owned, heap-allocated UTF-8
const char* / std::string_view &str Borrowed string slice
int / long i32 / i64 Specify size explicitly
unsigned int u32 Rust prefers explicit unsigned types
float / double f32 / f64 Direct mapping
bool bool Direct mapping
std::vector<T> Vec<T> Growable array
std::array<T, N> [T; N] Fixed-size array
std::map<K, V> HashMap<K, V> / BTreeMap<K, V> Unordered / ordered
std::unique_ptr<T> Box<T> Single ownership, heap allocation
std::shared_ptr<T> Rc<T> / Arc<T> Reference counting (single/multi-threaded)
std::optional<T> Option<T> Nullable type
try/catch Result<T, E> + ? Type-safe error handling
throw Err(...) or panic!() Errors vs unrecoverable failures
template<typename T> <T> with trait bounds Generics with constraints
class / struct struct + impl blocks Separation of data and behavior
virtual functions trait + dyn Trait Dynamic dispatch via trait objects
namespace mod Module system
nullptr None in Option<T> Explicit nullability

When Converting Code

  1. Analyze source thoroughly - Understand C++ object lifetimes, RAII patterns, and ownership semantics
  2. Map types first - Create type equivalence table, especially smart pointers → Rust ownership
  3. Preserve semantics - Maintain C++'s RAII cleanup guarantees in Rust's ownership system
  4. Adopt target idioms - Don't write "C++ code in Rust syntax" (avoid unnecessary Rc/Arc)
  5. Handle edge cases - nullptr checks, exception safety, move semantics, template instantiation
  6. Test equivalence - Same inputs → same outputs, verify memory safety
  7. Consider FFI - For large codebases, use cxx crate for gradual migration

Type System Mapping

Primitive Types

C++ Rust Notes
bool bool Direct mapping
char u8 C++ char is 1 byte, not Unicode
wchar_t / char16_t / char32_t char Rust char is Unicode scalar value (4 bytes)
int8_t i8 Guaranteed 8-bit signed
int16_t i16 Guaranteed 16-bit signed
int32_t i32 Guaranteed 32-bit signed
int64_t i64 Guaranteed 64-bit signed
uint8_t u8 Guaranteed 8-bit unsigned
uint16_t u16 Guaranteed 16-bit unsigned
uint32_t u32 Guaranteed 32-bit unsigned
uint64_t u64 Guaranteed 64-bit unsigned
size_t usize Platform-dependent unsigned
ptrdiff_t isize Platform-dependent signed
float f32 32-bit floating point
double f64 64-bit floating point
long double - No direct equivalent; use external crate if needed
void () Unit type

String Types

C++ Rust Notes
std::string String Owned, heap-allocated, UTF-8 enforced
const std::string& &str Borrowed string slice for parameters
std::string&& String Move semantics → ownership transfer
const char* &str / *const u8 Prefer &str; use raw pointer only for FFI
char* *mut u8 / &mut [u8] Mutable buffer or raw pointer
std::string_view (C++17) &str Non-owning string reference
std::u8string (C++20) String Rust String is always UTF-8

Collection Types

C++ Rust Notes
std::vector<T> Vec<T> Growable, owned array
std::vector<T>& &[T] / &mut [T] Borrowed slice for parameters
std::array<T, N> [T; N] Fixed-size array on stack
std::deque<T> VecDeque<T> Double-ended queue
std::list<T> - Use Vec or VecDeque; linked lists rare in Rust
std::map<K, V> BTreeMap<K, V> Ordered map, K must be Ord
std::unordered_map<K, V> HashMap<K, V> Hash table, K must be Hash + Eq
std::set<T> BTreeSet<T> Ordered set
std::unordered_set<T> HashSet<T> Hash set
std::pair<T, U> (T, U) Tuple
std::tuple<T, U, V> (T, U, V) Tuple
std::span<T> (C++20) &[T] / &mut [T] Non-owning view

Smart Pointer Types

C++ Rust Notes
std::unique_ptr<T> Box<T> Single ownership, heap allocation
std::unique_ptr<T[]> Vec<T> Owned dynamic array
std::shared_ptr<T> Rc<T> Reference counting (single-threaded)
std::shared_ptr<T> (thread-safe) Arc<T> Atomic reference counting (multi-threaded)
std::weak_ptr<T> Weak<T> / std::sync::Weak<T> Weak reference (Rc/Arc)
Raw pointer T* Box<T> / &T / &mut T Prefer owned/borrowed types; use raw only for FFI
T* const *const T Immutable raw pointer (unsafe)
T* (mutable) *mut T Mutable raw pointer (unsafe)

Optional and Variant Types

C++ Rust Notes
std::optional<T> (C++17) Option<T> Nullable type, compile-time safety
T* (nullable) Option<Box<T>> Heap-allocated nullable
std::variant<T, U> (C++17) enum Tagged union, type-safe variant
std::any (C++17) - Use generics or enums; avoid type erasure
void* - Use generics or trait objects; avoid in safe Rust

Function Types

C++ Rust Notes
void (*)(int) fn(i32) Function pointer
std::function<int(int)> Fn(i32) -> i32 Closure trait (or FnMut, FnOnce)
Lambda [](int x) { ... } |x| { ... } Closure syntax
Lambda [&](int x) { ... } |x| { ... } with captured refs Borrow checker enforces safety
Lambda [=](int x) { ... } move |x| { ... } Move closure (takes ownership)

Composite Types

C++ Rust Notes
struct { ... } struct { ... } Similar syntax, fields private by default in Rust modules
class { ... } struct { ... } + impl Separate data (struct) from methods (impl)
enum enum (fieldless) C-like enum
enum class (C++11) enum Rust enums are always scoped
Tagged union (manual) enum with variants Rust enums are sum types
union union (unsafe) Avoid; use enums instead
Inheritance hierarchy Composition + traits Rust favors composition over inheritance

Idiom Translation

Pattern 1: RAII and Resource Management

C++:

class FileHandle {
private:
    FILE* file;

public:
    FileHandle(const char* path, const char* mode)
        : file(fopen(path, mode)) {
        if (!file) {
            throw std::runtime_error("Failed to open file");
        }
    }

    ~FileHandle() {
        if (file) {
            fclose(file);
        }
    }

    // Delete copy, allow move
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    FileHandle(FileHandle&& other) noexcept : file(other.file) {
        other.file = nullptr;
    }

    FILE* get() { return file; }
};

// Usage - automatic cleanup
void processFile(const char* filename) {
    FileHandle file(filename, "r");
    // Use file.get()
    // Automatically closed when function exits
}

Rust:

use std::fs::File;
use std::io::{self, Read};

struct FileHandle {
    file: File,
}

impl FileHandle {
    fn new(path: &str) -> io::Result<Self> {
        let file = File::open(path)?;
        Ok(FileHandle { file })
    }
}

// Drop trait provides automatic cleanup (like C++ destructor)
impl Drop for FileHandle {
    fn drop(&mut self) {
        // File::drop is called automatically - no manual close needed
        println!("FileHandle dropped");
    }
}

// Usage - automatic cleanup via Drop
fn process_file(filename: &str) -> io::Result<()> {
    let mut file = FileHandle::new(filename)?;
    // Use file
    Ok(())
    // Automatically dropped when function exits
}

// Or more idiomatically, use std::fs::File directly
fn process_file_idiomatic(filename: &str) -> io::Result<()> {
    let file = File::open(filename)?;
    // Use file
    Ok(())
    // File implements Drop, automatic cleanup
}

Why this translation:

  • Rust's Drop trait is analogous to C++ destructors for RAII
  • Constructors that can fail use Result<T, E> instead of exceptions
  • Move semantics are default in Rust (no need for std::move)
  • Ownership system eliminates need for manual delete copy constructors
  • The ? operator propagates errors ergonomically

Pattern 2: Smart Pointers and Ownership

C++:

#include <memory>
#include <vector>

class Node {
public:
    int value;
    std::shared_ptr<Node> next;

    Node(int v) : value(v), next(nullptr) {}
};

class LinkedList {
private:
    std::shared_ptr<Node> head;

public:
    void push(int value) {
        auto new_node = std::make_shared<Node>(value);
        new_node->next = head;
        head = new_node;
    }

    std::optional<int> pop() {
        if (!head) {
            return std::nullopt;
        }
        int value = head->value;
        head = head->next;
        return value;
    }
};

Rust:

// Avoid shared_ptr pattern in Rust when possible
// Prefer Box for single ownership
struct Node {
    value: i32,
    next: Option<Box<Node>>,
}

struct LinkedList {
    head: Option<Box<Node>>,
}

impl LinkedList {
    fn new() -> Self {
        LinkedList { head: None }
    }

    fn push(&mut self, value: i32) {
        let new_node = Box::new(Node {
            value,
            next: self.head.take(),  // Moves ownership
        });
        self.head = Some(new_node);
    }

    fn pop(&mut self) -> Option<i32> {
        self.head.take().map(|node| {
            self.head = node.next;
            node.value
        })
    }
}

// If shared ownership is truly needed (rare), use Rc
use std::rc::Rc;

struct SharedNode {
    value: i32,
    next: Option<Rc<SharedNode>>,
}

// Note: Rc creates immutable shared ownership
// For interior mutability, use Rc<RefCell<T>>

Why this translation:

  • Rust prefers single ownership (Box) over shared ownership (Rc/Arc)
  • Option<Box<T>> replaces nullable pointers
  • .take() method moves ownership out of an Option, replacing with None
  • Shared ownership (Rc/Arc) should be used sparingly in Rust
  • Reference counting happens at compile-time via ownership tracking, not runtime

Pattern 3: Templates vs Generics

C++:

template<typename T>
class Container {
private:
    std::vector<T> data;

public:
    void add(const T& item) {
        data.push_back(item);
    }

    template<typename Predicate>
    std::vector<T> filter(Predicate pred) const {
        std::vector<T> result;
        for (const auto& item : data) {
            if (pred(item)) {
                result.push_back(item);
            }
        }
        return result;
    }

    size_t size() const { return data.size(); }
};

// Usage
Container<int> numbers;
numbers.add(1);
numbers.add(2);
auto evens = numbers.filter([](int x) { return x % 2 == 0; });

Rust:

struct Container<T> {
    data: Vec<T>,
}

impl<T> Container<T> {
    fn new() -> Self {
        Container { data: Vec::new() }
    }

    fn add(&mut self, item: T) {
        self.data.push(item);
    }

    fn size(&self) -> usize {
        self.data.len()
    }
}

// Conditional implementation for types that implement Clone
impl<T: Clone> Container<T> {
    fn filter<F>(&self, pred: F) -> Vec<T>
    where
        F: Fn(&T) -> bool,
    {
        self.data
            .iter()
            .filter(|item| pred(item))
            .cloned()
            .collect()
    }
}

// Usage
let mut numbers = Container::new();
numbers.add(1);
numbers.add(2);
let evens = numbers.filter(|x| x % 2 == 0);

// More idiomatic: use iterators directly
let numbers = vec![1, 2, 3, 4, 5];
let evens: Vec<_> = numbers.iter()
    .filter(|x| *x % 2 == 0)
    .copied()
    .collect();

Why this translation:

  • Rust generics require explicit trait bounds (e.g., T: Clone)
  • where clause provides cleaner syntax for complex bounds
  • Rust's iterator pattern is more idiomatic than manual collection
  • Generic functions use trait bounds instead of template parameter concepts
  • No implicit constraints like C++ templates (explicit is better)

Pattern 4: Inheritance vs Composition + Traits

C++:

class Animal {
public:
    virtual void make_sound() const = 0;  // Pure virtual
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void make_sound() const override {
        std::cout << "Woof!\n";
    }
};

class Cat : public Animal {
public:
    void make_sound() const override {
        std::cout << "Meow!\n";
    }
};

void animal_sounds(const std::vector<std::unique_ptr<Animal>>& animals) {
    for (const auto& animal : animals) {
        animal->make_sound();
    }
}

int main() {
    std::vector<std::unique_ptr<Animal>> animals;
    animals.push_back(std::make_unique<Dog>());
    animals.push_back(std::make_unique<Cat>());
    animal_sounds(animals);
}

Rust:

// Define behavior with a trait (like C++ pure virtual interface)
trait Animal {
    fn make_sound(&self);
}

// Implement trait for concrete types
struct Dog;
impl Animal for Dog {
    fn make_sound(&self) {
        println!("Woof!");
    }
}

struct Cat;
impl Animal for Cat {
    fn make_sound(&self) {
        println!("Meow!");
    }
}

// Option 1: Dynamic dispatch with trait objects (like C++ virtual)
fn animal_sounds_dyn(animals: &[Box<dyn Animal>]) {
    for animal in animals {
        animal.make_sound();
    }
}

// Option 2: Static dispatch with generics (no runtime overhead)
fn animal_sounds_generic<A: Animal>(animals: &[A]) {
    for animal in animals {
        animal.make_sound();
    }
}

fn main() {
    // Dynamic dispatch (runtime polymorphism)
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog),
        Box::new(Cat),
    ];
    animal_sounds_dyn(&animals);

    // Static dispatch (compile-time polymorphism)
    let dogs = vec![Dog, Dog];
    animal_sounds_generic(&dogs);
}

Why this translation:

  • Rust uses traits instead of inheritance for polymorphism
  • dyn Trait provides runtime polymorphism (like C++ virtual functions)
  • Generic bounds provide zero-cost compile-time polymorphism
  • No inheritance hierarchy - composition and traits are preferred
  • Trait objects require explicit Box<dyn Trait> or &dyn Trait

Pattern 5: Exception Handling to Result Types

C++:

#include <stdexcept>
#include <string>
#include <fstream>

class FileError : public std::runtime_error {
public:
    FileError(const std::string& msg) : std::runtime_error(msg) {}
};

std::string readFile(const std::string& path) {
    std::ifstream file(path);
    if (!file.is_open()) {
        throw FileError("Failed to open file: " + path);
    }

    std::string content;
    std::string line;
    while (std::getline(file, line)) {
        content += line + "\n";
    }

    if (file.bad()) {
        throw FileError("Error reading file: " + path);
    }

    return content;
}

void processFile(const std::string& path) {
    try {
        std::string content = readFile(path);
        // Process content
    } catch (const FileError& e) {
        std::cerr << "File error: " << e.what() << "\n";
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
    }
}

Rust:

use std::fs;
use std::io;
use std::path::Path;

// Custom error type
#[derive(Debug)]
enum FileError {
    Io(io::Error),
    InvalidContent(String),
}

impl From<io::Error> for FileError {
    fn from(err: io::Error) -> Self {
        FileError::Io(err)
    }
}

impl std::fmt::Display for FileError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            FileError::Io(e) => write!(f, "IO error: {}", e),
            FileError::InvalidContent(msg) => write!(f, "Invalid content: {}", msg),
        }
    }
}

impl std::error::Error for FileError {}

fn read_file(path: &Path) -> Result<String, FileError> {
    // ? operator propagates errors (like C++ exception unwinding)
    let content = fs::read_to_string(path)?;
    Ok(content)
}

fn process_file(path: &Path) {
    match read_file(path) {
        Ok(content) => {
            // Process content
            println!("Read {} bytes", content.len());
        }
        Err(FileError::Io(e)) => {
            eprintln!("File error: {}", e);
        }
        Err(FileError::InvalidContent(msg)) => {
            eprintln!("Invalid content: {}", msg);
        }
    }
}

// Or use the ? operator to propagate
fn process_file_propagate(path: &Path) -> Result<(), FileError> {
    let content = read_file(path)?;
    // Process content
    Ok(())
}

Why this translation:

  • Rust uses Result<T, E> instead of exceptions for recoverable errors
  • The ? operator replaces try/catch for error propagation
  • From trait enables automatic error conversion (like exception hierarchies)
  • Pattern matching on Result is explicit and type-safe
  • Unrecoverable errors use panic!() instead of exceptions

Memory & Ownership Translation

C++ RAII vs Rust Ownership

C++ Pattern Rust Equivalent Key Difference
Constructor acquires resource Constructor returns Result<T, E> Fallible construction explicit
Destructor releases resource Drop trait Automatic, deterministic cleanup
Copy constructor Clone trait Explicit, not automatic
Move constructor Default move semantics Moves are implicit, borrowing is explicit
const T& parameter &T parameter Borrow checker enforces lifetime
T&& parameter T parameter Takes ownership by default
std::unique_ptr<T> Box<T> Single ownership
std::shared_ptr<T> Rc<T> / Arc<T> Avoid when possible; prefer borrowing

Smart Pointer Translation Guide

// C++: unique_ptr for single ownership
std::unique_ptr<Widget> widget = std::make_unique<Widget>();
use_widget(*widget);  // Dereference
auto moved = std::move(widget);  // Explicit move
// Rust: Box for single ownership
let widget = Box::new(Widget::new());
use_widget(&widget);  // Automatic deref via Deref trait
let moved = widget;  // Implicit move (widget no longer usable)
// C++: shared_ptr for shared ownership
std::shared_ptr<Data> data = std::make_shared<Data>();
auto copy = data;  // Reference count increased
// Rust: Prefer borrowing over shared ownership
let data = Data::new();
use_data(&data);  // Borrow instead of clone
use_data_again(&data);  // Can borrow multiple times

// Only use Rc if truly needed (shared ownership)
use std::rc::Rc;
let data = Rc::new(Data::new());
let copy = Rc::clone(&data);  // Reference count increased

Lifetime Annotations (No C++ Equivalent)

Rust's borrow checker requires explicit lifetime annotations when relationships aren't clear:

// Rust: Lifetime ensures returned reference is valid
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Struct containing references
struct Parser<'a> {
    source: &'a str,
    position: usize,
}

impl<'a> Parser<'a> {
    fn new(source: &'a str) -> Self {
        Parser { source, position: 0 }
    }

    fn current(&self) -> &'a str {
        &self.source[self.position..]
    }
}

C++ has no equivalent to lifetimes - the compiler doesn't track reference validity at compile time. Rust's lifetimes prevent dangling references that would compile in C++ but cause runtime errors.


FFI & Interoperability (10th Pillar)

For large C++ codebases, gradual migration using FFI is often the best strategy. The cxx crate provides safe C++/Rust interop.

Why FFI Matters for C++ → Rust

Instead of rewriting everything at once:

  1. Convert performance-critical modules to Rust first
  2. Keep stable C++ code as-is during transition
  3. Test new Rust code against existing C++ test suite
  4. Gradually replace C++ modules over time
  5. Roll back easily if issues arise

The cxx Crate

The cxx crate provides safe, zero-overhead C++ interop:

# Cargo.toml
[dependencies]
cxx = "1.0"

[build-dependencies]
cxx-build = "1.0"

Basic FFI Example

C++ side (src/cpp/widget.h):

#pragma once
#include <memory>
#include <string>

class Widget {
private:
    int value_;

public:
    Widget(int value);
    int get_value() const;
    void set_value(int value);
    std::string to_string() const;
};

std::unique_ptr<Widget> create_widget(int value);

C++ implementation (src/cpp/widget.cpp):

#include "widget.h"
#include <sstream>

Widget::Widget(int value) : value_(value) {}

int Widget::get_value() const {
    return value_;
}

void Widget::set_value(int value) {
    value_ = value;
}

std::string Widget::to_string() const {
    std::ostringstream oss;
    oss << "Widget(" << value_ << ")";
    return oss.str();
}

std::unique_ptr<Widget> create_widget(int value) {
    return std::make_unique<Widget>(value);
}

Rust FFI bridge (src/bridge.rs):

#[cxx::bridge]
mod ffi {
    // Shared structs (visible to both C++ and Rust)
    struct Config {
        name: String,
        value: i32,
    }

    // C++ types and functions
    unsafe extern "C++" {
        include!("myproject/widget.h");

        // Opaque C++ type
        type Widget;

        // C++ functions
        fn create_widget(value: i32) -> UniquePtr<Widget>;
        fn get_value(self: &Widget) -> i32;
        fn set_value(self: Pin<&mut Widget>, value: i32);
        fn to_string(self: &Widget) -> String;
    }

    // Rust functions callable from C++
    extern "Rust" {
        fn process_widget(widget: &Widget) -> i32;
        fn create_config(name: String, value: i32) -> Config;
    }
}

// Implement Rust functions
fn process_widget(widget: &ffi::Widget) -> i32 {
    let current = widget.get_value();
    current * 2
}

fn create_config(name: String, value: i32) -> ffi::Config {
    ffi::Config { name, value }
}

// Use C++ from Rust
pub fn use_cpp_widget() {
    let widget = ffi::create_widget(42);
    println!("Widget: {}", widget.to_string());
    let doubled = process_widget(&widget);
    println!("Processed: {}", doubled);
}

Build script (build.rs):

fn main() {
    cxx_build::bridge("src/bridge.rs")
        .file("src/cpp/widget.cpp")
        .flag_if_supported("-std=c++17")
        .compile("myproject-cpp");

    println!("cargo:rerun-if-changed=src/bridge.rs");
    println!("cargo:rerun-if-changed=src/cpp/widget.h");
    println!("cargo:rerun-if-changed=src/cpp/widget.cpp");
}

Data Type Marshalling

C++ Type cxx Bridge Type Rust Type Notes
int32_t i32 i32 Direct pass by value
std::string String String Copied across boundary
&std::string &str &str Zero-copy borrow
std::unique_ptr<T> UniquePtr<T> UniquePtr<T> Ownership transfer
std::shared_ptr<T> SharedPtr<T> SharedPtr<T> Reference counted
&T &T &T Shared borrow
&mut T Pin<&mut T> Pin<&mut T> Exclusive borrow
std::vector<T> Vec<T> Vec<T> Copied across boundary
&std::vector<T> &CxxVector<T> &CxxVector<T> Zero-copy view

Gradual Migration Strategy

┌─────────────────────────────────────────────────────────────┐
│                  GRADUAL MIGRATION PHASES                    │
├─────────────────────────────────────────────────────────────┤
│  Phase 1: SETUP                                              │
│  • Add cxx to Cargo.toml                                     │
│  • Create FFI bridge module                                  │
│  • Set up build.rs to compile C++ code                       │
│  • Verify C++ and Rust can call each other                   │
├─────────────────────────────────────────────────────────────┤
│  Phase 2: IDENTIFY TARGET MODULES                            │
│  • Find performance bottlenecks (profile C++ code)           │
│  • Identify frequently-changing modules (benefit from Rust)  │
│  • Map dependencies between modules                          │
│  • Choose initial module with minimal dependencies           │
├─────────────────────────────────────────────────────────────┤
│  Phase 3: CONVERT FIRST MODULE                               │
│  • Translate C++ module to Rust                              │
│  • Expose Rust module via cxx bridge                         │
│  • Keep C++ interface unchanged (drop-in replacement)        │
│  • Test Rust implementation against C++ test suite           │
├─────────────────────────────────────────────────────────────┤
│  Phase 4: INTEGRATION                                        │
│  • Replace C++ module calls with Rust calls                  │
│  • Run full integration tests                                │
│  • Monitor for issues (memory leaks, performance)            │
│  • Rollback to C++ if needed                                 │
├─────────────────────────────────────────────────────────────┤
│  Phase 5: ITERATE                                            │
│  • Repeat for next module                                    │
│  • Gradually reduce C++ footprint                            │
│  • Eventually remove cxx bridge (all Rust)                   │
└─────────────────────────────────────────────────────────────┘

FFI Best Practices

  1. Keep FFI boundary thin - Convert types at the boundary, work with native types internally
  2. Avoid complex types - Prefer simple types (integers, strings) over complex structs
  3. Handle errors explicitly - C++ exceptions don't cross FFI boundary safely
  4. Test FFI thoroughly - Memory bugs can occur at language boundaries
  5. Document ownership - Be clear about who owns data (C++ or Rust)
  6. Measure overhead - Profile FFI calls if performance-critical

FFI Error Handling

#[cxx::bridge]
mod ffi {
    unsafe extern "C++" {
        include!("myproject/api.h");

        // C++ function that can throw
        fn risky_operation(value: i32) -> Result<String>;
    }
}

// Use from Rust
fn call_cpp() -> Result<(), Box<dyn std::error::Error>> {
    // cxx converts C++ exceptions to Rust Result
    let result = ffi::risky_operation(42)?;
    println!("Success: {}", result);
    Ok(())
}

C++ side:

// C++ function that throws
std::string risky_operation(int32_t value) {
    if (value < 0) {
        throw std::runtime_error("Negative value not allowed");
    }
    return "Success";
}

Common Pitfalls

1. Overusing Rc/Arc (Avoid C++ shared_ptr Mindset)

Problem: Translating every shared_ptr to Rc/Arc.

// Bad: Unnecessary shared ownership
use std::rc::Rc;

struct Node {
    value: i32,
    children: Vec<Rc<Node>>,  // Over-engineered
}

Solution: Prefer borrowing or single ownership:

// Good: Use Box for owned children
struct Node {
    value: i32,
    children: Vec<Box<Node>>,
}

// Or borrow when possible
fn process_nodes(nodes: &[Node]) {
    // Work with borrowed references
}

2. Fighting the Borrow Checker with Clones

Problem: Cloning everywhere to satisfy the borrow checker.

// Bad: Excessive cloning
fn process(data: &Vec<String>) -> Vec<String> {
    data.clone()  // Unnecessary full copy
        .into_iter()
        .filter(|s| s.len() > 5)
        .collect()
}

Solution: Use references properly:

// Good: Work with references
fn process(data: &[String]) -> Vec<&str> {
    data.iter()
        .filter(|s| s.len() > 5)
        .map(|s| s.as_str())
        .collect()
}

// Or if ownership is needed, be explicit
fn process_owned(data: Vec<String>) -> Vec<String> {
    data.into_iter()
        .filter(|s| s.len() > 5)
        .collect()
}

3. Null Pointer Mistakes

Problem: Treating Option<T> like nullable pointers without checking.

// Bad: Unwrapping without checking (panics at runtime)
fn get_value(opt: Option<i32>) -> i32 {
    opt.unwrap()  // Panics if None
}

Solution: Handle None explicitly:

// Good: Pattern matching
fn get_value(opt: Option<i32>) -> i32 {
    match opt {
        Some(v) => v,
        None => 0,  // Default value
    }
}

// Or use combinators
fn get_value(opt: Option<i32>) -> i32 {
    opt.unwrap_or(0)
}

4. Ignoring Lifetime Errors

Problem: Returning references that outlive their source.

// Bad: Compiler error - returning reference to local
fn create_string() -> &str {
    let s = String::from("hello");
    &s  // Error: s dropped at end of function
}

Solution: Return owned data or use proper lifetimes:

// Good: Return owned String
fn create_string() -> String {
    String::from("hello")
}

// Or if parameter-based:
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

5. Transliterating C++ Patterns

Problem: Writing "C++ code in Rust syntax" instead of idiomatic Rust.

// Bad: Transliterated C++ style
struct Container {
    data: Vec<i32>,
}

impl Container {
    fn get(&self, index: usize) -> Option<i32> {
        if index < self.data.len() {
            Some(self.data[index])
        } else {
            None
        }
    }
}

Solution: Use Rust idioms:

// Good: Idiomatic Rust
struct Container {
    data: Vec<i32>,
}

impl Container {
    fn get(&self, index: usize) -> Option<&i32> {
        self.data.get(index)  // Built-in method
    }
}

// Or even simpler - use Vec directly
fn get_item(data: &[i32], index: usize) -> Option<&i32> {
    data.get(index)
}

6. Manual Iterator Loops

Problem: Using C++-style loops instead of iterators.

// Bad: C++ style loop
let mut sum = 0;
for i in 0..numbers.len() {
    sum += numbers[i];
}

Solution: Use iterator methods:

// Good: Idiomatic iterator
let sum: i32 = numbers.iter().sum();

// Or for more complex operations
let sum: i32 = numbers.iter()
    .filter(|&&x| x > 0)
    .sum();

Tooling

Tool Purpose Notes
cxx crate Safe C++/Rust FFI Recommended for gradual migration
bindgen Generate Rust FFI bindings from C++ headers For C-compatible C++ APIs
cbindgen Generate C/C++ headers from Rust Expose Rust to C++
autocxx Automatically call C++ from Rust Higher-level than cxx
cpp crate Embed C++ directly in Rust For quick experiments
cargo expand Expand macros and generics Understand template translation
rust-analyzer IDE support Catch lifetime/borrow errors early
clippy Linter Suggests idiomatic Rust patterns

Examples

Example 1: Simple - String Processing

Before (C++):

#include <string>
#include <algorithm>

std::string to_uppercase(const std::string& input) {
    std::string result = input;
    std::transform(result.begin(), result.end(), result.begin(),
                   [](unsigned char c) { return std::toupper(c); });
    return result;
}

int main() {
    std::string text = "hello world";
    std::string upper = to_uppercase(text);
    // text is still valid (copied)
}

After (Rust):

fn to_uppercase(input: &str) -> String {
    input.to_uppercase()
}

fn main() {
    let text = "hello world";
    let upper = to_uppercase(&text);
    // text is still valid (borrowed, not moved)
}

Example 2: Medium - Optional Values and Error Handling

Before (C++):

#include <optional>
#include <stdexcept>
#include <map>

class UserDatabase {
private:
    std::map<int, std::string> users;

public:
    void add_user(int id, const std::string& name) {
        if (users.count(id) > 0) {
            throw std::runtime_error("User already exists");
        }
        users[id] = name;
    }

    std::optional<std::string> get_user(int id) const {
        auto it = users.find(id);
        if (it != users.end()) {
            return it->second;
        }
        return std::nullopt;
    }

    bool remove_user(int id) {
        return users.erase(id) > 0;
    }
};

int main() {
    UserDatabase db;

    try {
        db.add_user(1, "Alice");
        auto user = db.get_user(1);
        if (user) {
            std::cout << "Found: " << *user << "\n";
        }
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
    }
}

After (Rust):

use std::collections::HashMap;

#[derive(Debug)]
enum DbError {
    UserExists,
}

struct UserDatabase {
    users: HashMap<i32, String>,
}

impl UserDatabase {
    fn new() -> Self {
        UserDatabase {
            users: HashMap::new(),
        }
    }

    fn add_user(&mut self, id: i32, name: String) -> Result<(), DbError> {
        if self.users.contains_key(&id) {
            return Err(DbError::UserExists);
        }
        self.users.insert(id, name);
        Ok(())
    }

    fn get_user(&self, id: i32) -> Option<&String> {
        self.users.get(&id)
    }

    fn remove_user(&mut self, id: i32) -> bool {
        self.users.remove(&id).is_some()
    }
}

fn main() {
    let mut db = UserDatabase::new();

    match db.add_user(1, String::from("Alice")) {
        Ok(()) => {
            if let Some(user) = db.get_user(1) {
                println!("Found: {}", user);
            }
        }
        Err(DbError::UserExists) => {
            eprintln!("Error: User already exists");
        }
    }
}

Example 3: Complex - Polymorphism with Smart Pointers

Before (C++):

#include <memory>
#include <vector>
#include <iostream>

class Shape {
public:
    virtual double area() const = 0;
    virtual void describe() const = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    double area() const override {
        return 3.14159 * radius * radius;
    }

    void describe() const override {
        std::cout << "Circle with radius " << radius << "\n";
    }
};

class Rectangle : public Shape {
private:
    double width, height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const override {
        return width * height;
    }

    void describe() const override {
        std::cout << "Rectangle " << width << "x" << height << "\n";
    }
};

class ShapeCollection {
private:
    std::vector<std::unique_ptr<Shape>> shapes;

public:
    void add_shape(std::unique_ptr<Shape> shape) {
        shapes.push_back(std::move(shape));
    }

    double total_area() const {
        double total = 0;
        for (const auto& shape : shapes) {
            total += shape->area();
        }
        return total;
    }

    void describe_all() const {
        for (const auto& shape : shapes) {
            shape->describe();
            std::cout << "  Area: " << shape->area() << "\n";
        }
    }
};

int main() {
    ShapeCollection collection;
    collection.add_shape(std::make_unique<Circle>(5.0));
    collection.add_shape(std::make_unique<Rectangle>(4.0, 6.0));

    collection.describe_all();
    std::cout << "Total area: " << collection.total_area() << "\n";
}

After (Rust):

// Define trait (like C++ abstract base class)
trait Shape {
    fn area(&self) -> f64;
    fn describe(&self) -> String;
}

// Concrete implementations
struct Circle {
    radius: f64,
}

impl Circle {
    fn new(radius: f64) -> Self {
        Circle { radius }
    }
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        3.14159 * self.radius * self.radius
    }

    fn describe(&self) -> String {
        format!("Circle with radius {}", self.radius)
    }
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    fn new(width: f64, height: f64) -> Self {
        Rectangle { width, height }
    }
}

impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn describe(&self) -> String {
        format!("Rectangle {}x{}", self.width, self.height)
    }
}

// Collection using trait objects (dynamic dispatch)
struct ShapeCollection {
    shapes: Vec<Box<dyn Shape>>,
}

impl ShapeCollection {
    fn new() -> Self {
        ShapeCollection { shapes: Vec::new() }
    }

    fn add_shape(&mut self, shape: Box<dyn Shape>) {
        self.shapes.push(shape);
    }

    fn total_area(&self) -> f64 {
        self.shapes.iter().map(|s| s.area()).sum()
    }

    fn describe_all(&self) {
        for shape in &self.shapes {
            println!("{}", shape.describe());
            println!("  Area: {}", shape.area());
        }
    }
}

fn main() {
    let mut collection = ShapeCollection::new();
    collection.add_shape(Box::new(Circle::new(5.0)));
    collection.add_shape(Box::new(Rectangle::new(4.0, 6.0)));

    collection.describe_all();
    println!("Total area: {}", collection.total_area());
}

Limitations

This skill has limited coverage in some areas due to gaps in the foundation skills:

Coverage Gaps

Pillar lang-cpp-dev lang-rust-dev Mitigation
Module System ~ ✓ C++ namespaces → Rust modules documented via web research
Error Handling ~ ✓ C++ exception patterns researched from cppreference.com
Serialization ✗ ✓ Common C++ serialization libraries researched
FFI ~ ~ Extended via meta-convert-dev FFI pillar and cxx crate docs

Known Limitations

  1. C++ Module System: C++20 modules are new; this skill focuses on namespace translation
  2. Advanced Metaprogramming: SFINAE, CRTP, and template metaprogramming require case-by-case analysis
  3. Coroutines: C++20 coroutines have no direct Rust equivalent; use async/await patterns

External Resources Used

Resource What It Provided Reliability
cxx crate docs FFI patterns and examples High (official)
cppreference.com C++ exception model High (community standard)
Rust Book Ownership patterns High (official)

See Also

  • meta-convert-dev - Foundational patterns (APTV workflow, FFI pillar, testing strategies)
  • lang-cpp-dev - C++ development patterns
  • lang-rust-dev - Rust development patterns
  • convert-golang-rust - Similar modern language → Rust conversion

Cross-cutting pattern skills:

  • patterns-concurrency-dev - Async, threads, channels across languages
  • patterns-serialization-dev - JSON, validation, struct tags across languages
  • patterns-metaprogramming-dev - Templates, macros, generics across languages