Claude Code Plugins

Community-maintained marketplace

Feedback

convert-cpp-rust

@aRustyDev/ai
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name convert-cpp-rust
description 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.

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