Rust 编译器与运行时揭秘

前言:站到编译器的视角

作者 杨艺韬 · 4,348 字

前言:站到编译器的视角

"Zero-cost abstractions: what you don't use, you don't pay for. And further: what you do use, you couldn't hand code any better." — Bjarne Stroustrup, later embraced by Rust

写作动机:和编译器搏斗的那十年

每一个 Rust 开发者都经历过这样的时刻:编译器报了一个 lifetime 错误,你盯着那几行代码看了十分钟,试了各种写法,终于编译通过了——但你不知道为什么

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    if x.len() > y.len() { x } else { y }
    //                        ^ error: lifetime mismatch
}

你学会了"和编译器搏斗"。你知道在这里加一个 'a,在那里加一个 clone(),在某些地方用 Arc<Mutex<T>>。你的代码能编译、能运行、性能不错。

但有一个问题始终萦绕在脑海:

编译器到底在做什么?

这些问题,Rust 官方文档不会告诉你,《Rust 程序设计语言》(The Book)不会告诉你,绝大多数 Rust 教程也不会告诉你。因为它们教的是"怎么用 Rust",而不是"Rust 怎么工作"。

这本书填补的就是这个空白。

一个典型的"开窍"场景

Rust 工程师常遇到这样一段代码,折磨好几小时:

async fn process(items: Vec<String>) -> Vec<String> {
    let mut futs = Vec::new();
    for item in items.iter() {       // ← 编译器报错从这里开始蔓延
        futs.push(async move { transform(item).await });
    }
    futures::future::join_all(futs).await
}

报错 "borrowed value does not live long enough"。按网上找到的各种建议试一圈:items.clone()items.into_iter()、用 Arc<Vec<_>>... 凑巧有一个能过,但不知道为什么它能过

打开 cargo +nightly rustc -- -Zunpretty=mir 看 MIR——一瞬间豁然开朗:

// MIR 显示 async 块被编译器生成为一个状态机 struct
// 状态机需要捕获 `item`,而 `item: &String` 的 lifetime 绑定到 `items.iter()`
// 但状态机要 'static 才能送给 join_all
// 于是编译器要求 item 被 owned —— 所以 `into_iter()` 才能过

一切都清楚了。不是"碰对了",而是编译器在按一个可理解的规则工作。规则可学,搏斗可以结束。

本书想帮你达到这个"开窍"的时刻——不是通过更多的经验主义技巧,而是通过站到编译器的视角

这本书讲什么

本书不教 Rust 语法。如果你还不会写 Rust,请先阅读 The Rust Programming Language

本书讲的是 Rust 编译器的内部行为——从你写下一行 Rust 代码到最终生成机器码的过程中,编译器对每一个核心语言特性做了什么。

具体来说,本书回答以下问题:

所有权与内存

类型系统与泛型

异步

闭包、unsafe 与 FFI

宏与元编程

编译器后端

本书的方法论:对照法(Comparison Method)

本书的核心方法是**"你写的 vs 编译器做的"对照**。

每一个核心概念,我们都会先给出一段简洁的 Rust 代码,然后展示编译器对这段代码的处理结果——可能是 MIR、可能是 LLVM IR、可能是内存布局图、可能是状态机的展开形式。

示例:async/await 的状态机展开

// 你写的
async fn fetch(url: &str) -> String {
    let resp = http_get(url).await;
    let body = resp.text().await;
    body.to_uppercase()
}

编译器内部生成的(简化,实际更复杂):

// 编译器生成的
enum FetchFuture<'a> {
    Start { url: &'a str },
    WaitingHttpGet { url: &'a str, fut: HttpGetFuture<'a> },
    WaitingText { fut: TextFuture },
    Done,
}

impl<'a> Future for FetchFuture<'a> {
    type Output = String;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<String> {
        // 状态转移逻辑:
        // 如果当前是 Start → 调用 http_get → 转到 WaitingHttpGet
        // 如果当前是 WaitingHttpGet → poll 子 future → 完成则转到 WaitingText
        // 如果当前是 WaitingText → poll 子 future → 完成则转到 Done
        ...
    }
}

这种对照让你同时看到"抽象"和"实现"。你不需要去猜编译器做了什么——你直接看到了。

示例:Move 语义的 MIR 形式

// 你写的
fn main() {
    let s = String::from("hello");
    let t = s;           // move
    // println!("{}", s);   // 注释掉这行才能编译——为什么?
}

MIR 展开(关键部分):

bb0: {
    _1 = String::from(const "hello") -> bb1;
}
bb1: {
    _2 = move _1;                    // 关键:move _1 把所有权转移到 _2
    StorageDead(_1);                 // _1 被标记为已消耗
    return;
}

StorageDead(_1) 这一行解释了一切——所有权转移之后,编译器已经把原变量标记为"死亡"。你再试图用 s,编译器查到它是 StorageDead 状态,直接拒绝。

如何自己生成这些输出

书中所有的 MIR 和 LLVM IR 输出都是通过以下命令实际生成的,不是凭空编造:

# 查看 MIR(需要 nightly)
cargo +nightly rustc -- -Zunpretty=mir

# 查看 LLVM IR
cargo rustc --release -- --emit=llvm-ir

# 查看汇编
cargo rustc --release -- --emit=asm

# 查看内存布局(需要 nightly)
cargo +nightly rustc -- -Zprint-type-sizes

# 查看编译器的借用检查分析
RUSTC_LOG=rustc_borrowck cargo rustc 2>&1

# 查看 trait selection 的过程
RUSTC_LOG=rustc_trait_selection cargo rustc 2>&1

你可以用同样的命令在自己的代码上验证本书的每一个结论。本书鼓励你边读边实验——这是远比被动阅读更高效的学习方式。

本书的组织:从表层到深层的八部分

全书分为八个部分,按照 Rust 语言特性从基础到高级组织:

graph TD
    P1[第一部分<br/>编译全景<br/>ch01]
    P2[第二部分<br/>所有权与内存<br/>ch02-05]
    P3[第三部分<br/>类型系统与泛型<br/>ch06-08]
    P4[第四部分<br/>异步机制<br/>ch09-10]
    P5[第五部分<br/>闭包/unsafe/FFI<br/>ch11-13]
    P6[第六部分<br/>宏与元编程<br/>ch14]
    P7[第七部分<br/>编译器后端<br/>ch15-17]
    P8[第八部分<br/>设计哲学<br/>ch18]

    P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7 --> P8

    style P1 fill:#dbeafe,stroke:#3b82f6
    style P2 fill:#dbeafe,stroke:#3b82f6
    style P3 fill:#fef3c7,stroke:#f59e0b
    style P4 fill:#fef3c7,stroke:#f59e0b
    style P5 fill:#fecaca,stroke:#dc2626
    style P6 fill:#fecaca,stroke:#dc2626
    style P7 fill:#dcfce7,stroke:#22c55e
    style P8 fill:#f3e8ff,stroke:#a855f7

第一部分:编译全景(第 1 章)——Rust 代码从源文件到可执行文件的完整旅程:解析 → HIR → MIR → LLVM IR → 机器码。这一章建立全局地图,后续章节在这张地图上逐个区域深入。

第二部分:所有权与内存(第 2-5 章)——Rust 最核心的创新。我们会深入 MIR 看 move/copy 的实现,拆解 NLL 借用检查算法,理解 lifetime 推导的规则,以及编译器如何决定一个类型在内存中的排列方式。

第三部分:类型系统与泛型(第 6-8 章)——零成本抽象的真正含义。单态化如何消除泛型开销,trait 的两种分发策略在底层有什么区别,vtable 的结构和代价。

第四部分:异步机制(第 9-10 章)——Rust 异步模型的编译器实现。async/await 的状态机展开、Pin 的必要性、Waker 的协作机制。这两章是后续阅读 Tokio 源码的前置知识。

第五部分:闭包、unsafe 与 FFI(第 11-13 章)——编译器的"柔软面"和"硬边界"。闭包的匿名类型生成、unsafe 的精确边界、跨 ABI 的函数调用。

第六部分:宏与元编程(第 14 章)——编译前的编译。声明宏的模式匹配和过程宏的 token 流处理。

第七部分:编译器后端与优化(第 15-17 章)——从 MIR 到机器码的最后一程。MIR 层的优化 pass、LLVM IR 的生成与优化、增量编译的缓存策略。

第八部分:设计哲学(第 18 章)——回到"为什么"。Rust 的核心设计决策背后的哲学:编译期 vs 运行时、安全 vs 灵活、零成本 vs 易用。以及这些设计模式如何迁移到其他系统的设计中。

每一章的写作节奏

本书每一章都按同一个节奏组织:

  1. 问题域:这一章要解决的语言机制是什么?它想达到什么目标?
  2. 朴素方法:最直接的实现思路是什么?为什么不够?
  3. Rust 的选择:Rust 编译器选择了什么方案?设计权衡是什么?
  4. 真实实现:MIR / LLVM IR / 数据结构层面具体是什么?
  5. 工程含义:这对你写 Rust 代码意味着什么?什么时候该用、什么时候避免?

这种结构保证了既有理论深度,又有实用价值——你读完不仅理解了"它怎么工作",还知道"我该怎么做"。

本书读者

你需要

你不需要

你将获得

与后续书籍的关系

本书是"Rust 后端系列"的第一本。后续书籍将深入 Tokio、Axum、Pingora、MeiliSearch、TiKV 等 Rust 基础设施项目的源码。

在那些书中,你会频繁遇到本书讲解过的机制:

后续书籍 重点依赖本书哪几章
Tokio 源码解析 第 9-10 章(async 状态机、Pin/Waker)
Axum 架构剖析 第 6-8 章(单态化、trait 分发、trait object)
TiKV 核心实现 第 12-13 章(unsafe、FFI) + 第 6-8 章
MeiliSearch 存储引擎 第 12-13 章(unsafe)+ 第 2-5 章(所有权)
任何 Rust 项目阅读 第 2-5 章(所有权、借用检查、内存布局)

把这本书当作整个系列的"解码器"——先装好它,后面的书读起来会顺畅得多。

源码版本与验证

编译器源码对照

本书基于 Rust 1.88 stable(2026 年 4 月)分析。编译器源码引用来自 rust-lang/rust 仓库。关键的源码文件位置:

主题 源码位置
借用检查器 compiler/rustc_borrowck/
MIR 定义 compiler/rustc_middle/src/mir/
MIR 优化 compiler/rustc_mir_transform/
Trait 系统 compiler/rustc_trait_selection/
类型推导 compiler/rustc_hir_typeck/
单态化 compiler/rustc_monomorphize/
代码生成 compiler/rustc_codegen_llvm/
async 展开 compiler/rustc_mir_transform/src/coroutine.rs
闭包展开 compiler/rustc_hir_typeck/src/upvar.rs
增量编译 compiler/rustc_query_system/

本书不要求你读懂这些源码——但会在关键处指出"你想深入就去这里"。

实验工具链

书中所有编译器输出(MIR、LLVM IR、类型布局)均可通过标准工具链复现:

# 安装 nightly(用于部分诊断输出)
rustup install nightly

# 查看 MIR
cargo +nightly rustc -- -Zunpretty=mir

# 查看 LLVM IR
cargo rustc --release -- --emit=llvm-ir

# 查看类型大小和布局
cargo +nightly rustc -- -Zprint-type-sizes

# 查看借用检查日志
RUSTC_LOG=rustc_borrowck=debug cargo rustc 2>&1 | less

# 查看汇编(目标平台相关)
cargo rustc --release -- --emit=asm -C target-cpu=native

还有几个社区工具会让这个过程更轻松:

# cargo-show-asm - 查看某个函数的汇编
cargo install cargo-show-asm
cargo asm <crate>::<function>

# cargo-llvm-lines - 统计每个函数生成了多少 LLVM IR 行
cargo install cargo-llvm-lines
cargo llvm-lines | head -50

# rust-demangler - 把 mangled name 变成可读的
cargo install rustc-demangle

一个约定:不要只读,要动手

不要只读——动手验证。 把书中的示例代码粘贴到你的项目里,用上面的命令看编译器的实际输出,和书中的分析对照。这是最快的学习方式。

每一章结尾都会提供 3-5 个"动手实验"——小练习,让你在自己的机器上重现书中的观察。花 10 分钟做实验,比多读 30 分钟书更有价值。

关于"理解编译器"的本质

最后说几句题外话——关于为什么理解编译器记住语法规则更重要。

Rust 的语法规则很多:lifetime 的五条省略规则、trait 的孤儿规则、impl Trait 的位置约束、dyn Trait 的对象安全要求... 你可以把它们全部背下来。但背规则有两个问题:

  1. 规则在演进——今天的 NLL 可能明年变成 Polonius,今天的 impl Trait in argument position 明年会加入返回位置
  2. 规则之间有例外——孤儿规则有例外(fundamental trait),自动 trait 推导有例外(Send/Sync 的反向实现)

而规则背后的原理不会变。

所有权是一个图论问题(资源的获取关系是有向无环图);借用检查是一个活跃变量分析问题(compile-time 的静态分析);单态化是编译期的代码特化(partial evaluation 的应用);async 状态机是编译期的 CPS 变换(continuation-passing style)。

这些原理一旦掌握,未来 Rust 语言加的任何新特性,你都能自己推断出它会怎么工作、代价是什么。

这就是本书试图给你的——不是一套规则清单,而是一副看穿 Rust 的眼镜

戴上之后,编译器不再是你的对手,而是你最有耐心的老师

翻页,让我们开始这场旅程。


杨艺韬 2026 年 4 月 · 于北京


延伸阅读的推荐起点