Rust for C/C++ Programmers
Case Study Overview: C++ to Rust Translation
What you'll learn: Lessons from a real-world translation of ~100K lines of C++ to ~90K lines of Rust across ~20 crates. Five key transformation patterns and the architectural decisions behind them.
- We translated a large C++ diagnostic system (~100K lines of C++) into a Rust implementation (~20 Rust crates, ~90K lines)
- This section shows the actual patterns used — not toy examples, but real production code
- The five key transformations:
| # | C++ Pattern | Rust Pattern | Impact |
|---|---|---|---|
| 1 | Class hierarchy + dynamic_cast | Enum dispatch + match | ~400 → 0 dynamic_casts |
| 2 | shared_ptr / enable_shared_from_this tree | Arena + index linkage | No reference cycles |
| 3 | Framework* raw pointer in every module | DiagContext<'a> with lifetime borrowing | Compile-time validity |
| 4 | God object | Composable state structs | Testable, modular |
| 5 | vector<unique_ptr<Base>> everywhere | Trait objects only where needed (~25 uses) | Static dispatch default |
Before and After Metrics
| Metric | C++ (Original) | Rust (Rewrite) |
|---|---|---|
dynamic_cast / type downcasts | ~400 | 0 |
virtual / override methods | ~900 | ~25 (Box<dyn Trait>) |
Raw new allocations | ~200 | 0 (all owned types) |
shared_ptr / reference counting | ~10 (topology lib) | 0 (Arc only at FFI boundary) |
enum class definitions | ~60 | ~190 pub enum |
| Pattern matching expressions | N/A | ~750 match |
| God objects (>5K lines) | 2 | 0 |
Case Study 1: Inheritance hierarchy → Enum dispatch
The C++ Pattern: Event Class Hierarchy
// C++ original: Every GPU event type is a class inheriting from GpuEventBase
class GpuEventBase {
public:
virtual ~GpuEventBase() = default;
virtual void Process(DiagFramework* fw) = 0;
uint16_t m_recordId;
uint8_t m_sensorType;
// ... common fields
};
class GpuPcieDegradeEvent : public GpuEventBase {
public:
void Process(DiagFramework* fw) override;
uint8_t m_linkSpeed;
uint8_t m_linkWidth;
};
class GpuPcieFatalEvent : public GpuEventBase { /* ... */ };
class GpuBootEvent : public GpuEventBase { /* ... */ };
// ... 10+ event classes inheriting from GpuEventBase
// Processing requires dynamic_cast:
void ProcessEvents(std::vector<std::unique_ptr<GpuEventBase>>& events,
DiagFramework* fw) {
for (auto& event : events) {
if (auto* degrade = dynamic_cast<GpuPcieDegradeEvent*>(event.get())) {
// handle degrade...
} else if (auto* fatal = dynamic_cast<GpuPcieFatalEvent*>(event.get())) {
// handle fatal...
}
// ... 10 more branches
}
}The Rust Solution: Enum Dispatch
// Example: types.rs — No inheritance, no vtable, no dynamic_cast
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GpuEventKind {
PcieDegrade,
PcieFatal,
PcieUncorr,
Boot,
BaseboardState,
EccError,
OverTemp,
PowerRail,
ErotStatus,
Unknown,
}// Example: manager.rs — Separate typed Vecs, no downcasting needed
pub struct GpuEventManager {
sku: SkuVariant,
degrade_events: Vec<GpuPcieDegradeEvent>, // Concrete type, not Box<dyn>
fatal_events: Vec<GpuPcieFatalEvent>,
uncorr_events: Vec<GpuPcieUncorrEvent>,
boot_events: Vec<GpuBootEvent>,
baseboard_events: Vec<GpuBaseboardEvent>,
ecc_events: Vec<GpuEccEvent>,
// ... each event type gets its own Vec
}
// Accessors return typed slices — zero ambiguity
impl GpuEventManager {
pub fn degrade_events(&self) -> &[GpuPcieDegradeEvent] {
&self.degrade_events
}
pub fn fatal_events(&self) -> &[GpuPcieFatalEvent] {
&self.fatal_events
}
}Why Not Vec<Box<dyn GpuEvent>>?
- The Wrong Approach (literal translation): Put all events in one heterogeneous collection, then downcast — this is what C++ does with
vector<unique_ptr<Base>> - The Right Approach: Separate typed Vecs eliminate all downcasting. Each consumer asks for exactly the event type it needs
- Performance: Separate Vecs give better cache locality (all degrade events are contiguous in memory)
Case Study 2: shared_ptr tree → Arena/index pattern
The C++ Pattern: Reference-Counted Tree
// C++ topology library: PcieDevice uses enable_shared_from_this
// because parent and child nodes both need to reference each other
class PcieDevice : public std::enable_shared_from_this<PcieDevice> {
public:
std::shared_ptr<PcieDevice> m_upstream;
std::vector<std::shared_ptr<PcieDevice>> m_downstream;
// ... device data
void AddChild(std::shared_ptr<PcieDevice> child) {
child->m_upstream = shared_from_this(); // Parent ↔ child cycle!
m_downstream.push_back(child);
}
};
// Problem: parent→child and child→parent create reference cycles
// Need weak_ptr to break cycles, but easy to forgetThe Rust Solution: Arena with Index Linkage
// Example: components.rs — Flat Vec owns all devices
pub struct PcieDevice {
pub base: PcieDeviceBase,
pub kind: PcieDeviceKind,
// Tree linkage via indices — no reference counting, no cycles
pub upstream_idx: Option<usize>, // Index into the arena Vec
pub downstream_idxs: Vec<usize>, // Indices into the arena Vec
}
// The "arena" is simply a Vec<PcieDevice> owned by the tree:
pub struct DeviceTree {
devices: Vec<PcieDevice>, // Flat ownership — one Vec owns everything
}
impl DeviceTree {
pub fn parent(&self, device_idx: usize) -> Option<&PcieDevice> {
self.devices[device_idx].upstream_idx
.map(|idx| &self.devices[idx])
}
pub fn children(&self, device_idx: usize) -> Vec<&PcieDevice> {
self.devices[device_idx].downstream_idxs
.iter()
.map(|&idx| &self.devices[idx])
.collect()
}
}Key Insight
- No
shared_ptr, noweak_ptr, noenable_shared_from_this - No reference cycles possible — indices are just
usizevalues - Better cache performance — all devices in contiguous memory
- Simpler reasoning — one owner (the Vec), many viewers (indices)