Ferris in C++

Topics:

Converting C++ to Rust via @DavidHenderson

Slides: converting_cpp_to_rust.pdf

Converting C++ to Rust: Summary

Why Convert to Rust?

The primary driver for converting from C++ to Rust is memory safety. This has become a critical priority, with the US government actively advocating for memory-safe languages. Key benefits include:

  • Memory safety guarantees - Safe Rust eliminates undefined behavior
  • No garbage collector - Unlike other memory-safe languages, Rust provides safety without GC pauses
  • Data race prevention - Particularly valuable for multithreaded applications
  • Industry momentum - Major companies are transitioning: Microsoft Distinguished Engineer Galen Hunt has stated the goal to eliminate all C and C++ code from Microsoft by 2030, using AI and algorithmic assistance for translation

Safety-Critical Systems

Ferrocene, the qualified Rust compiler toolchain, meets stringent safety standards:

  • ISO 26262 (ASIL D for automotive)
  • IEC 61508 compliance
  • Suitable for ADAS (Advanced Driver-Assistance Systems) and other safety-critical applications

Conversion Approaches

1. Total Rewrite ("Big Bang")

  • Start from scratch in Rust
  • Cons: Long development time before having testable code; risk of "second system effect" (over-engineering with new language features)

2. Automatic Conversion Tools

C2Rust (https://c2rust.com/)

  • Translates C99-compliant C code to semantically equivalent Rust
  • Provides translator, refactoring tools, and cross-checking utilities
  • Pros: Good starting point
  • Cons: C-only (not C++), produces unsafe and non-idiomatic Rust code, may carry over bugs from original code

bindgen (FFI approach)

  • Generates Foreign Function Interfaces from C/C++ headers
  • Creates Rust crate that calls C/C++ code
  • Common pattern: "sys-" prefixed crates for bindgen wrappers, with clean Rust API built on top
  • Typically used in build scripts
  • Pros: Enables piecewise conversion, heavily used in practice
  • Cons: Produces unsafe, non-idiomatic code

cbindgen

  • Reverse direction: produces C headers from Rust code
  • Allows calling Rust from existing C code
  • Good for introducing Rust early in conversion

This incremental approach is recommended for both schedule and cost reasons:

  • Always maintain a working codebase
  • Convert critical sections first
  • Make use of Rust's built-in test framework
  • Can result in either pure Rust or pragmatic C++/Rust hybrid

Gradual Conversion Process

Planning Phase

  1. Understand the existing C++ codebase

    • Review algorithms and data structures
    • For embedded systems, identify platform-specific elements:
      • Inline assembler code
      • Hardware-specific drivers and low-level interfaces
      • Direct memory and device access
  2. Prioritize conversion targets

    • Security-critical functionality
    • Foundational modules, libraries, and frameworks
    • Drivers and interfaces requiring heavy modification

Implementation Best Practices

  • Incremental rewriting along clear boundaries

  • Continuous testing:

    • Compare byte-for-byte results between old and new implementations
    • Use "Memory Access" trait to examine memory modifications
    • Add tests for newly discovered bugs (porting may reveal latent defects)
    • Integrate into CI/CD workflow
  • Performance monitoring (TPMs - Technical Performance Measures):

    • Add instrumentation for memory usage, execution time, and image size
    • Periodically compare against original C++ metrics
    • Focus optimization efforts where needed
    • Set up profiling with automated regression testing

Language Comparison Highlights

Structure

  • C++: Namespaces, #include, separate header and source files, CMake/make
  • Rust: Modules (just name the file), .rs files serve as modules, cargo build system

Key Feature Differences

C++RustNotes
Preprocessor directives (#define, #ifdef)Attributes (#[test], #![feature])Used for traits, linting, conditional compilation
Various cast operatorsas, Into<>, From<> traitsRust disallows C++-style casting
std::vector, std::map, etc.Vec, HashMap, HashSetSimilar functionality
enum, unionenum, union (unsafe)Rust enums are more powerful and safer
try/catch, error codesResult<T, E>, Option, panic!More explicit error handling
switchmatchMore flexible, always requires default handler
Pointers (char *)Raw pointers, referencesRaw pointers are unsafe

String Handling

  • C++: char*, std::string, wchar_t*
  • Rust: str, String, OSString (for FFI), OSStr
  • Rust uses UTF-8 encoding by default

Embedded Systems Considerations

Memory Management

  • C++ embedded typically uses preallocated memory blocks (not preferred by the presenter)
  • Must allocate all memory at boot and carefully share buffers
  • Rust avoids garbage collection pauses while maintaining safety

Threading Model

  • Rust embedded often models interrupts as threaded code
  • Interrupt context treated as a thread with mutex switching
  • Hardware interrupts can occur millions of times during execution

FFI Patterns

  • Use #[no_mangle] attribute to maintain function names in linker (good for porting)
  • Common pattern: sys-prefixed bindgen crates with cleaner Rust wrapper on top
  • Some C++ code may remain indefinitely (like kernel code)

Tools and Resources

Rust Embedded Ecosystem

  • Embassy (https://embassy.dev/) - Framework for embedded applications, provides generalized HAL
  • c-types crate (https://crates.io/crates/rs-ctypes) - Provides C type definitions
  • cargo-make - Make-like functionality integrated with cargo.toml

Community Resources

  • Embedded Rust Matrix channel: https://matrix.to/#/#rust-embedded:matrix.org (where hardware people gather)
  • Embedded Working Group: https://github.com/rust-embedded/wg
  • Embedded Working Group Blog: https://blog.rust-embedded.org/
  • The Rustonomicon: https://doc.rust-lang.org/stable/nomicon/ (guide to unsafe Rust)

Additional References

  • Official Rust documentation and installation guides
  • GitHub: locka99/cpp-to-rust-book
  • Various practical porting guides available on Medium and elsewhere

Key Takeaways

  1. Memory safety is the compelling reason to switch, not necessarily productivity gains
  2. Gradual, incremental conversion is the most practical approach
  3. Maintain continuous testing and performance monitoring throughout conversion
  4. Use FFI tools (bindgen/cbindgen) to enable piecewise migration
  5. Accept that some code may remain in C++ indefinitely (pragmatism applies)
  6. Avoid over-optimization early; collect metrics to guide focused optimization efforts
  7. Be aware of the "second system effect" - resist the urge to over-engineer with new language features

Crates you should know