Appearance
前言
写作动机
每一个 Rust 开发者都经历过这样的时刻:编译器报了一个 lifetime 错误,你盯着那几行代码看了十分钟,试了各种写法,终于编译通过了——但你不知道为什么。
你学会了"和编译器搏斗"。你知道在这里加一个 'a,在那里加一个 clone(),在某些地方用 Arc<Mutex<T>>。你的代码能编译、能运行、性能不错。但有一个问题始终萦绕在脑海:
编译器到底在做什么?
- 所有权的 move 语义,在编译器内部对应的是什么操作?
- 借用检查器是怎么判断一个引用会不会悬垂的?它用的是什么算法?
- 泛型函数说是"零成本抽象",编译器具体做了什么来消除运行时开销?
async fn明明返回的是一个impl Future,为什么编译器能在编译期确定它的大小?dyn Trait的 vtable 里到底存了什么?一个 trait object 在内存里长什么样?
这些问题,Rust 官方文档不会告诉你,《Rust 程序设计语言》不会告诉你,绝大多数 Rust 教程也不会告诉你。因为它们教的是"怎么用 Rust",而不是"Rust 怎么工作"。
这本书填补的就是这个空白。
这本书讲什么
本书不教 Rust 语法。如果你还不会写 Rust,请先阅读 The Rust Programming Language。
本书讲的是 Rust 编译器的内部行为——从你写下一行 Rust 代码到最终生成机器码的过程中,编译器对每一个核心语言特性做了什么。
具体来说,本书回答以下问题:
所有权与内存
- move 语义在 MIR(中间表示)中是如何表达的?Copy 和 Move 的区别在编译产物中长什么样?
- 借用检查器使用的 NLL(Non-Lexical Lifetimes)算法是怎么工作的?它如何构建控制流图、如何计算借用的活跃区间?
- 编译器是怎么推导 lifetime 的?什么时候需要你手动标注、什么时候不需要?
struct、enum、union在内存中是怎么排列的?Niche 优化是怎么让Option<&T>和&T一样大的?
类型系统与泛型
- 单态化(monomorphization)的展开过程是什么?一个
fn foo<T>(x: T)被三种类型调用后,编译器生成了几份代码? - 静态分发和动态分发的区别在 LLVM IR 层面具体是什么?
dyn Trait的 fat pointer 里存了什么?vtable 的结构是怎样的?
异步
async fn被编译器展开成了什么?生成的状态机有几个状态、每个状态存了哪些字段?- Pin 解决的"自引用结构"问题到底是什么?为什么
&mut self不够用? - Waker 的注册和唤醒机制是怎么和 Tokio 的 reactor 配合的?
闭包、unsafe 与 FFI
- 闭包捕获变量时,编译器生成的匿名 struct 长什么样?
Fn、FnMut、FnOnce的区别在编译产物中如何体现? unsafe块的边界在编译器实现中是如何划定的?编译器在 unsafe 块内关闭了哪些检查?- 跨 FFI 边界调用 C 函数时,参数是怎么传递的?ABI 约定如何影响内存布局?
宏与元编程
- 声明宏(
macro_rules!)和过程宏(proc_macro)的展开发生在编译的哪个阶段? - 过程宏拿到的
TokenStream和最终的 AST 之间经历了什么?
编译器后端
- MIR 层做了哪些优化?常量传播、死代码消除、内联是怎么工作的?
- MIR 是怎么被翻译成 LLVM IR 的?Rust 的类型信息在 LLVM IR 中还存在吗?
- 增量编译是怎么决定"哪些代码需要重新编译"的?
本书的方法论:对照法
本书的核心方法是**"你写的 vs 编译器做的"对照**。
每一个核心概念,我们都会先给出一段简洁的 Rust 代码,然后展示编译器对这段代码的处理结果——可能是 MIR、可能是 LLVM IR、可能是内存布局图、可能是状态机的展开形式。
例如,讲 async/await 时:
rust
// 你写的
async fn fetch(url: &str) -> String {
let resp = http_get(url).await;
let body = resp.text().await;
body.to_uppercase()
}rust
// 编译器生成的(简化)
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> {
// 状态转移逻辑...
}
}这种对照让你同时看到"抽象"和"实现"。你不需要去猜编译器做了什么——你直接看到了。
书中所有的 MIR 和 LLVM IR 输出都是通过以下命令实际生成的,不是凭空编造:
bash
# 查看 MIR
cargo rustc -- --emit=mir
# 查看 LLVM IR
cargo rustc -- --emit=llvm-ir
# 查看内存布局
cargo rustc -- -Zprint-type-sizes # nightly
# 查看编译器的借用检查分析
RUSTC_LOG=rustc_borrowck cargo rustc 2>&1你可以用同样的命令在自己的代码上验证本书的每一个结论。
本书的组织
全书分为八个部分,按照 Rust 语言特性从基础到高级组织:
第一部分:编译全景(第 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 易用。以及这些设计模式如何迁移到其他系统的设计中。
本书读者
你需要:至少写过几千行 Rust 代码,了解所有权、借用、trait、async/await 的基本用法。
你不需要:编译器开发经验、LLVM 知识、计算机科学学位。本书会从零开始解释每一个编译器概念。
你将获得:
- 遇到 lifetime 错误时,知道编译器的判断逻辑,能直接写出正确的代码而不是靠试
- 在
dyn Trait和泛型之间做选择时,知道两者在底层的精确代价 - 读 Tokio、Axum、TiKV 等项目源码时,不会在
Pin<Box<dyn Future>>上卡住 - 理解"零成本抽象"不是营销口号,而是编译器用单态化、内联、LLVM 优化实现的具体工程
与后续书籍的关系
本书是"Rust 后端系列"的第一本。后续书籍将深入 Tokio、Axum、Pingora、MeiliSearch、TiKV 等 Rust 基础设施项目的源码。
在那些书中,你会频繁遇到本书讲解过的机制:
- 读 Tokio 时,第 9-10 章(async 状态机、Pin/Waker)是基础
- 读 Axum 时,第 6-8 章(单态化、trait 分发、trait object)是基础
- 读 TiKV 时,第 12-13 章(unsafe、FFI)是基础
- 读任何 Rust 项目时,第 2-5 章(所有权、借用检查、内存布局)是基础
把这本书当作整个系列的"解码器"——先装好它,后面的书读起来会顺畅得多。
源码版本与验证
本书基于 Rust 1.88 stable(2026 年 4 月)分析。编译器源码引用来自 rust-lang/rust 仓库。
书中所有编译器输出(MIR、LLVM IR、类型布局)均可通过标准工具链复现:
bash
# 安装 nightly(用于部分诊断输出)
rustup install nightly
# 查看 MIR
cargo +nightly rustc -- -Zunpretty=mir
# 查看 LLVM IR
cargo rustc --release -- --emit=llvm-ir
# 查看类型大小和布局
cargo +nightly rustc -- -Zprint-type-sizes不要只读——动手验证。把书中的示例代码粘贴到你的项目里,用上面的命令看编译器的实际输出,和书中的分析对照。这是最快的学习方式。