--- 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 - **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` | `Vec` | Growable array | | `std::array` | `[T; N]` | Fixed-size array | | `std::map` | `HashMap` / `BTreeMap` | Unordered / ordered | | `std::unique_ptr` | `Box` | Single ownership, heap allocation | | `std::shared_ptr` | `Rc` / `Arc` | Reference counting (single/multi-threaded) | | `std::optional` | `Option` | Nullable type | | `try/catch` | `Result` + `?` | Type-safe error handling | | `throw` | `Err(...)` or `panic!()` | Errors vs unrecoverable failures | | `template` | `` 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` | 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` | `Vec` | Growable, owned array | | `std::vector&` | `&[T]` / `&mut [T]` | Borrowed slice for parameters | | `std::array` | `[T; N]` | Fixed-size array on stack | | `std::deque` | `VecDeque` | Double-ended queue | | `std::list` | - | Use Vec or VecDeque; linked lists rare in Rust | | `std::map` | `BTreeMap` | Ordered map, K must be Ord | | `std::unordered_map` | `HashMap` | Hash table, K must be Hash + Eq | | `std::set` | `BTreeSet` | Ordered set | | `std::unordered_set` | `HashSet` | Hash set | | `std::pair` | `(T, U)` | Tuple | | `std::tuple` | `(T, U, V)` | Tuple | | `std::span` (C++20) | `&[T]` / `&mut [T]` | Non-owning view | ### Smart Pointer Types | C++ | Rust | Notes | |----------|----------|-------| | `std::unique_ptr` | `Box` | Single ownership, heap allocation | | `std::unique_ptr` | `Vec` | Owned dynamic array | | `std::shared_ptr` | `Rc` | Reference counting (single-threaded) | | `std::shared_ptr` (thread-safe) | `Arc` | Atomic reference counting (multi-threaded) | | `std::weak_ptr` | `Weak` / `std::sync::Weak` | Weak reference (Rc/Arc) | | Raw pointer `T*` | `Box` / `&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` (C++17) | `Option` | Nullable type, compile-time safety | | `T*` (nullable) | `Option>` | Heap-allocated nullable | | `std::variant` (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` | `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++:** ```cpp 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:** ```rust use std::fs::File; use std::io::{self, Read}; struct FileHandle { file: File, } impl FileHandle { fn new(path: &str) -> io::Result { 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` 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++:** ```cpp #include #include class Node { public: int value; std::shared_ptr next; Node(int v) : value(v), next(nullptr) {} }; class LinkedList { private: std::shared_ptr head; public: void push(int value) { auto new_node = std::make_shared(value); new_node->next = head; head = new_node; } std::optional pop() { if (!head) { return std::nullopt; } int value = head->value; head = head->next; return value; } }; ``` **Rust:** ```rust // Avoid shared_ptr pattern in Rust when possible // Prefer Box for single ownership struct Node { value: i32, next: Option>, } struct LinkedList { head: Option>, } 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 { 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>, } // Note: Rc creates immutable shared ownership // For interior mutability, use Rc> ``` **Why this translation:** - Rust prefers single ownership (`Box`) over shared ownership (`Rc`/`Arc`) - `Option>` 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++:** ```cpp template class Container { private: std::vector data; public: void add(const T& item) { data.push_back(item); } template std::vector filter(Predicate pred) const { std::vector result; for (const auto& item : data) { if (pred(item)) { result.push_back(item); } } return result; } size_t size() const { return data.size(); } }; // Usage Container numbers; numbers.add(1); numbers.add(2); auto evens = numbers.filter([](int x) { return x % 2 == 0; }); ``` **Rust:** ```rust struct Container { data: Vec, } impl Container { 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 Container { fn filter(&self, pred: F) -> Vec 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++:** ```cpp 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>& animals) { for (const auto& animal : animals) { animal->make_sound(); } } int main() { std::vector> animals; animals.push_back(std::make_unique()); animals.push_back(std::make_unique()); animal_sounds(animals); } ``` **Rust:** ```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]) { for animal in animals { animal.make_sound(); } } // Option 2: Static dispatch with generics (no runtime overhead) fn animal_sounds_generic(animals: &[A]) { for animal in animals { animal.make_sound(); } } fn main() { // Dynamic dispatch (runtime polymorphism) let animals: Vec> = 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` or `&dyn Trait` ### Pattern 5: Exception Handling to Result Types **C++:** ```cpp #include #include #include 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:** ```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 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 { // ? 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` 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` | 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` | `Box` | Single ownership | | `std::shared_ptr` | `Rc` / `Arc` | Avoid when possible; prefer borrowing | ### Smart Pointer Translation Guide ```cpp // C++: unique_ptr for single ownership std::unique_ptr widget = std::make_unique(); use_widget(*widget); // Dereference auto moved = std::move(widget); // Explicit move ``` ```rust // 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) ``` ```cpp // C++: shared_ptr for shared ownership std::shared_ptr data = std::make_shared(); auto copy = data; // Reference count increased ``` ```rust // 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 // 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: ```toml # Cargo.toml [dependencies] cxx = "1.0" [build-dependencies] cxx-build = "1.0" ``` ### Basic FFI Example **C++ side (src/cpp/widget.h):** ```cpp #pragma once #include #include 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 create_widget(int value); ``` **C++ implementation (src/cpp/widget.cpp):** ```cpp #include "widget.h" #include 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 create_widget(int value) { return std::make_unique(value); } ``` **Rust FFI bridge (src/bridge.rs):** ```rust #[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; 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):** ```rust 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` | `UniquePtr` | `UniquePtr` | Ownership transfer | | `std::shared_ptr` | `SharedPtr` | `SharedPtr` | Reference counted | | `&T` | `&T` | `&T` | Shared borrow | | `&mut T` | `Pin<&mut T>` | `Pin<&mut T>` | Exclusive borrow | | `std::vector` | `Vec` | `Vec` | Copied across boundary | | `&std::vector` | `&CxxVector` | `&CxxVector` | 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 ```rust #[cxx::bridge] mod ffi { unsafe extern "C++" { include!("myproject/api.h"); // C++ function that can throw fn risky_operation(value: i32) -> Result; } } // Use from Rust fn call_cpp() -> Result<(), Box> { // cxx converts C++ exceptions to Rust Result let result = ffi::risky_operation(42)?; println!("Success: {}", result); Ok(()) } ``` **C++ side:** ```cpp // 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`. ```rust // Bad: Unnecessary shared ownership use std::rc::Rc; struct Node { value: i32, children: Vec>, // Over-engineered } ``` **Solution:** Prefer borrowing or single ownership: ```rust // Good: Use Box for owned children struct Node { value: i32, children: Vec>, } // 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. ```rust // Bad: Excessive cloning fn process(data: &Vec) -> Vec { data.clone() // Unnecessary full copy .into_iter() .filter(|s| s.len() > 5) .collect() } ``` **Solution:** Use references properly: ```rust // 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) -> Vec { data.into_iter() .filter(|s| s.len() > 5) .collect() } ``` ### 3. Null Pointer Mistakes **Problem:** Treating `Option` like nullable pointers without checking. ```rust // Bad: Unwrapping without checking (panics at runtime) fn get_value(opt: Option) -> i32 { opt.unwrap() // Panics if None } ``` **Solution:** Handle None explicitly: ```rust // Good: Pattern matching fn get_value(opt: Option) -> i32 { match opt { Some(v) => v, None => 0, // Default value } } // Or use combinators fn get_value(opt: Option) -> i32 { opt.unwrap_or(0) } ``` ### 4. Ignoring Lifetime Errors **Problem:** Returning references that outlive their source. ```rust // 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: ```rust // 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. ```rust // Bad: Transliterated C++ style struct Container { data: Vec, } impl Container { fn get(&self, index: usize) -> Option { if index < self.data.len() { Some(self.data[index]) } else { None } } } ``` **Solution:** Use Rust idioms: ```rust // Good: Idiomatic Rust struct Container { data: Vec, } 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. ```rust // Bad: C++ style loop let mut sum = 0; for i in 0..numbers.len() { sum += numbers[i]; } ``` **Solution:** Use iterator methods: ```rust // 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++):** ```cpp #include #include 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):** ```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++):** ```cpp #include #include #include class UserDatabase { private: std::map 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 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):** ```rust use std::collections::HashMap; #[derive(Debug)] enum DbError { UserExists, } struct UserDatabase { users: HashMap, } 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++):** ```cpp #include #include #include 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> shapes; public: void add_shape(std::unique_ptr 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(5.0)); collection.add_shape(std::make_unique(4.0, 6.0)); collection.describe_all(); std::cout << "Total area: " << collection.total_area() << "\n"; } ``` **After (Rust):** ```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>, } impl ShapeCollection { fn new() -> Self { ShapeCollection { shapes: Vec::new() } } fn add_shape(&mut self, shape: Box) { 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