Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Case Study Overview: C++ to Rust Translation
案例总览:从 C++ 迁移到 Rust

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.
本章将学到什么: 一个真实项目把约 10 万行 C++ 重写成约 9 万行 Rust、拆成约 20 个 crate 之后,总结出的经验教训。重点看五类核心转化模式,以及这些架构选择背后的原因。

  • We translated a large C++ diagnostic system (~100K lines of C++) into a Rust implementation (~20 Rust crates, ~90K lines)
    我们把一个大型 C++ 诊断系统从头翻成了 Rust 实现,大约从 10 万行 C++ 变成了 20 个左右 Rust crate、总计约 9 万行代码。
  • This section shows the actual patterns used — not toy examples, but real production code
    这一节讲的都是真实用过的模式,不是课堂玩具例子,而是生产代码里真刀真枪踩出来的做法。
  • The five key transformations:
    五类关键转换如下:
#C++ Pattern
C++ 模式
Rust Pattern
Rust 模式
Impact
效果
1Class hierarchy + dynamic_cast
类层级 + dynamic_cast
Enum dispatch + match
枚举分发 + match
~400 → 0 dynamic_casts
dynamic_cast 从约 400 处降到 0
2shared_ptr / enable_shared_from_this tree
shared_ptr / enable_shared_from_this 树结构
Arena + index linkage
Arena + 索引关联
No reference cycles
彻底避免引用环
3Framework* raw pointer in every module
每个模块里都塞一个 Framework* 裸指针
DiagContext<'a> with lifetime borrowing
带生命周期借用的 DiagContext<'a>
Compile-time validity
有效性在编译期校验
4God object
巨型上帝对象
Composable state structs
可组合的状态结构体
Testable, modular
更容易测试,也更模块化
5vector<unique_ptr<Base>> everywhere
到处都是 vector<unique_ptr<Base>>
Trait objects only where needed (~25 uses)
只在必要场景下使用 trait object,大约 25 处
Static dispatch default
默认走静态分发

Before and After Metrics
迁移前后指标对比

Metric
指标
C++ (Original)
C++ 原始实现
Rust (Rewrite)
Rust 重写实现
dynamic_cast / type downcasts
dynamic_cast / 类型向下转型
~4000
virtual / override methods
virtual / override 方法
~900~25 (Box<dyn Trait>)
Raw new allocations
new 分配
~2000 (all owned types)
0,全部改成显式所有权类型
shared_ptr / reference counting
shared_ptr / 引用计数
~10 (topology lib)
约 10 处,主要在拓扑库
0 (Arc only at FFI boundary)
0,只有 FFI 边界才用 Arc
enum class definitions
enum class 定义
~60~190 pub enum
Pattern matching expressions
模式匹配表达式
N/A~750 match
God objects (>5K lines)
上帝对象(超过 5000 行)
20

这些数字很能说明问题:Rust 重写不是“把 C++ 语法改成 Rust 语法”那么简单,而是顺手把一整批原本靠运行时兜底的设计,改造成了更静态、更可验证的结构。
也就是说,真正值钱的部分不是换了门语言,而是趁机把模型理顺了。否则只是把旧包袱换个皮接着背,纯属自讨苦吃。


Case Study 1: Inheritance hierarchy → Enum dispatch
案例一:继承层级改成枚举分发

The C++ Pattern: Event Class Hierarchy
C++ 的老路子:事件类层级

// 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
    }
}

这种设计在 C++ 里不算少见:先搞一棵继承树,再往一个 vector<unique_ptr<Base>> 里乱炖,最后消费端一边遍历一边 dynamic_cast。能跑,但读起来像拆炸弹,改起来像挖雷。
一旦事件种类越来越多,分支也会跟着爆炸,类型系统在这种结构里基本没帮上什么忙。

The Rust Solution: Enum Dispatch
Rust 的解法:枚举分发

#![allow(unused)]
fn main() {
// 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,
}
}
#![allow(unused)]
fn main() {
// 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
    }
}
}

Rust 这边没有照着 C++ 生搬硬套。真正有效的做法,是把“类型分发”前移到数据建模阶段。不同事件该分开存,就老老实实分开存。
这样一来,消费方根本不需要 downcast,也不需要猜“当前拿到的是不是这个子类”。拿到什么类型,就处理什么类型,代码一下就亮堂了。

Why Not Vec<Box<dyn GpuEvent>>?
为什么不写成 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>>
    错误做法:按字面直译,继续把所有事件塞进一个异构集合里,再去 downcast。这其实就是把 C++ 的毛病原封不动带进 Rust。
  • The Right Approach: Separate typed Vecs eliminate all downcasting. Each consumer asks for exactly the event type it needs
    更好的做法:按具体类型拆成独立 Vec,这样可以把 downcast 全部删掉。每个消费者只拿自己真正需要的那一类事件。
  • Performance: Separate Vecs give better cache locality (all degrade events are contiguous in memory)
    性能收益:拆开的 Vec 还会带来更好的缓存局部性,同类事件挨着存,遍历时更顺。

这一刀砍下去,往往是迁移里最提气的一步:类型语义终于从“运行时猜”变成了“编译期定”。
说得直白一点,就是少了很多“看着挺面向对象,其实全靠 if-else 补锅”的历史包袱。


Case Study 2: shared_ptr tree → Arena/index pattern
案例二:shared_ptr 树改成 arena 加索引

The C++ Pattern: Reference-Counted Tree
C++ 的老模式:引用计数树

// 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 forget

这种树结构在 C++ 里也很常见:为了让父节点和子节点都能互相引用,先上 shared_ptr,再靠 weak_ptr 去拆环。写的时候像是图省事,后面排查生命周期时就容易变成灾难片。
尤其是 enable_shared_from_this 一上场,说明所有权模型已经开始拧巴了,代码表面工整,底下全是暗流。

The Rust Solution: Arena with Index Linkage
Rust 的解法:arena 加索引关联

#![allow(unused)]
fn main() {
// 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()
    }
}
}

Rust 这里的思路是干净得多的:树里所有节点统一交给一个 Vec<PcieDevice> 持有,节点之间只存索引。
索引就是普通整数,不带所有权,不参与引用计数,更不会自己长出环。父子关系还在,但生命周期纠缠已经被拆开了。

Key Insight
关键理解

  • No shared_ptr, no weak_ptr, no enable_shared_from_this
    没有 shared_ptr,没有 weak_ptr,也不需要 enable_shared_from_this
  • No reference cycles possible — indices are just usize values
    不会出现引用环,因为索引只是 usize 值,本身不拥有任何对象。
  • Better cache performance — all devices in contiguous memory
    缓存性能更好,所有设备对象都连续摆在同一块内存里。
  • Simpler reasoning — one owner (the Vec), many viewers (indices)
    推理更简单:只有一个真正的拥有者,也就是 Vec;其余地方都只是通过索引去看。
graph LR
    subgraph "C++ shared_ptr Tree"
        A1["shared_ptr<Device>"] -->|"shared_ptr"| B1["shared_ptr<Device>"]
        B1 -->|"shared_ptr (parent)"| A1
        A1 -->|"shared_ptr"| C1["shared_ptr<Device>"]
        C1 -->|"shared_ptr (parent)"| A1
        style A1 fill:#ff6b6b,color:#000
        style B1 fill:#ffa07a,color:#000
        style C1 fill:#ffa07a,color:#000
    end

    subgraph "Rust Arena + Index"
        V["Vec<PcieDevice>"]
        V --> D0["[0] Root<br/>upstream: None<br/>down: [1,2]"]
        V --> D1["[1] Child<br/>upstream: Some(0)<br/>down: []"]
        V --> D2["[2] Child<br/>upstream: Some(0)<br/>down: []"]
        style V fill:#51cf66,color:#000
        style D0 fill:#91e5a3,color:#000
        style D1 fill:#91e5a3,color:#000
        style D2 fill:#91e5a3,color:#000
    end

这张图已经把差异画得挺残忍了。左边那套是对象互相抱着不撒手,右边这套是一个统一仓库存对象,关系全部走编号。
当数据结构规模一上来,后者在调试、性能和维护成本上都会舒服很多。