Go 用逃逸分析决定堆分配、用 GC 回收垃圾——运行时兜底,编译器配合。Rust 不走这条路。没有 GC,没有运行时开销,内存安全完全由编译器在编译期保证。借用检查器(Borrow Checker)就是 Rust 的安全网——它拒绝一切可能违反所有权规则的代码,哪怕那代码在运行时永远不会出问题。这种”宁可误杀不可放过”的策略,让 Rust 成了系统编程领域近十年来最重要的语言。
一、rustc 架构
1.1 查询驱动编译
rustc 采用查询驱动架构:每个编译步骤是一个查询,结果被缓存,只有输入变化时才重新计算。这使得增量编译高效——修改一个函数只重新编译受影响的部分。
| 编译阶段 | 输入 | 输出 | 主要工作 |
|---|---|---|---|
| 解析 | 源码 | AST | 词法分析 + 语法分析 |
| 宏展开 | AST → HIR | HIR | macro_rules! + 过程宏 |
| 类型检查 | HIR | 带类型的 HIR | 类型推导 + trait 求解 |
| Lowering | HIR | MIR | 降级为控制流图 |
| 借用检查 | MIR | MIR | 所有权 + 生命周期验证 |
| 优化 | MIR | MIR | 常量折叠、内联等 |
| 代码生成 | MIR | LLVM IR | 调用 LLVM 后端 |
1.2 与 Go 编译器对比
| 维度 | Go | Rust |
|---|---|---|
| 编译架构 | 顺序流水线 | 查询驱动 |
| 增量编译 | 简单(包级) | 精细(查询级) |
| 类型系统 | 简单(结构化) | 复杂(泛型 + trait + 生命周期) |
| 内存安全 | GC | 借用检查器 |
| 编译速度 | 快(~1s/万行) | 慢(~10s/万行) |
| 后端 | 自研 | LLVM |
二、HIR → MIR → LLVM IR
2.1 HIR(High-level IR)
HIR 是宏展开和类型检查后的高级表示,保留了 Rust 的高级语法结构:
// 源码fn greet(name: &str) -> String { format!("Hello, {}!", name)}
// HIR 概念结构(简化)// FnDef {// name: "greet",// inputs: [Ref(Str, Shared)],// output: String,// body: Call(format!, ["Hello, {}!", name])// }2.2 MIR(Mid-level IR)
MIR 是 Rust 编译器的核心 IR,基于控制流图(CFG)+ 基本块:
// MIR 概念结构(简化)// BasicBlock 0:// _1 = &_2 // 借用// _3 = move _1 // 移动// switch _3 -> [true: bb1, false: bb2]//// BasicBlock 1:// _4 = (*_3).field // 解引用// drop _3 // 析构// return//// BasicBlock 2:// drop _1 // 析构// returnMIR 的关键特性:
- 基本块:每个块以一个终结指令结束(分支/返回/调用)
- Local 变量:每个变量有唯一编号(_0, _1, _2…)
- Place:变量 + 投影(
_1.field,*_2,_3[4]) - Rvalue:右值表达式(借用、二元运算、聚合等)
2.3 MIR → LLVM IR
// MIR// _1 = _2 + _3
// LLVM IR// %1 = add i64 %2, %3Rust 通过 rustc_codegen_llvm crate 将 MIR 翻译为 LLVM IR,然后利用 LLVM 的优化和代码生成。
三、借用检查器
3.1 所有权规则
// 规则 1:每个值有唯一的所有者let s1 = String::from("hello");let s2 = s1; // 所有权从 s1 移动到 s2// println!("{}", s1); // 编译错误:s1 的值已被移动
// 规则 2:多个不可变借用 或 一个可变借用let mut data = vec![1, 2, 3];let r1 = &data; // 不可变借用let r2 = &data; // 多个不可变借用// let r3 = &mut data; // 不可变借用存在时不能可变借用
// 规则 3:借用不能超过所有者的生命周期fn dangling() -> &String { let s = String::from("hello"); &s // s 在函数结束时析构,返回悬垂引用}3.2 借用检查器在 MIR 上的实现
借用检查器分析 MIR 的控制流图,为每个程序点计算借用状态:
借用检查器的工作流程:1. 为每个局部变量计算生命周期区域2. 在每个程序点检查借用是否活跃3. 检查是否存在冲突借用(可变+不可变同时活跃)4. 检查借用是否超过所有者的生命周期5. 报告错误或通过// 借用检查器会拒绝的代码fn bad_borrow() { let mut x = 1; let r = &x; // 不可变借用开始 x = 2; // 可变借用冲突 println!("{}", r); // 不可变借用在此使用}
// 借用检查器会接受的代码(NLL)fn good_borrow() { let mut x = 1; let r = &x; // 不可变借用开始 println!("{}", r); // 不可变借用在此结束 x = 2; // 不可变借用已结束,可以可变}3.3 NLL(Non-Lexical Lifetimes)
Rust 2018 引入 NLL——借用的生命周期不再基于词法作用域,而是基于实际使用:
四、生命周期推导
4.1 生命周期参数
// 显式生命周期标注fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}
// 生命周期省略规则(编译器自动推导)fn first_word(s: &str) -> &str { // 等价于:fn first_word<'a>(s: &'a str) -> &'a str let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..]}
// 结构体中的生命周期struct Parser<'a> { input: &'a str, pos: usize,}
impl<'a> Parser<'a> { fn new(input: &'a str) -> Self { Parser { input, pos: 0 } }
fn peek(&self) -> Option<char> { self.input.chars().nth(self.pos) }}4.2 生命周期推导算法
生命周期推导步骤:1. 为每个引用参数和返回值分配生命周期变量2. 根据函数体中的约束生成方程3. 求解方程,确定每个生命周期的范围4. 检查返回值的生命周期 ≤ 某个输入的生命周期
约束类型:- 子类型约束:'a: 'b('a 活至少和 'b 一样长)- 引用约束:&'a T 中的 'a 必须 ≥ T 内所有引用的生命周期- 返回约束:返回值的生命周期必须 ≤ 某个输入的生命周期90% 的 Rust 函数不需要显式生命周期标注——编译器的省略规则可以自动推导。只有当返回值的生命周期不明确时,才需要手动标注。
五、宏系统
5.1 macro_rules!(声明宏)
// 简单的声明宏macro_rules! vec_create { ($($x:expr),*) => { { let mut v = Vec::new(); $(v.push($x);)* v } };}
let v = vec_create!(1, 2, 3);// 展开为:// {// let mut v = Vec::new();// v.push(1);// v.push(2);// v.push(3);// v// }5.2 过程宏
// 派生宏(proc-macro-derive)use proc_macro::TokenStream;
#[proc_macro_derive(Debug)]pub fn derive_debug(input: TokenStream) -> TokenStream { // 解析输入 TokenStream // 生成 Debug impl 的 TokenStream // 返回生成的代码}
// 属性宏(proc-macro-attribute)#[proc_macro_attribute]pub fn inline_test(attr: TokenStream, item: TokenStream) -> TokenStream { // 将函数转换为测试用例}
// 函数式宏(proc-macro)#[proc_macro]pub fn sql(input: TokenStream) -> TokenStream { // 编译时 SQL 解析和验证}六、类型推导与 trait 求解
6.1 Hindley-Milner 类型推导
// Rust 使用 Hindley-Milner 变体进行类型推导let x = 42; // 推导为 i32let y = 3.14; // 推导为 f64let z = vec![]; // 需要类型注解或上下文let z: Vec<i32> = vec![]; // 显式类型
// 闭包类型推导let add = |x, y| x + y; // 推导为 i32 -> i32 -> i326.2 Trait 求解
// Trait 约束求解fn print<T: std::fmt::Display>(value: T) { println!("{}", value);}
// 编译器需要证明:T 实现了 Display// 求解过程:// 1. 检查本地 impl// 2. 检查 trait 约束传递// 3. 检查孤儿规则(不能为外部类型实现外部 trait)六-B、rustc 编译流程深入
6B.1 HIR → MIR → LLVM IR 的完整示例
追踪一个简单函数从源码到 LLVM IR 的完整变换过程:
// 源码fn max(a: i32, b: i32) -> i32 { if a > b { a } else { b }}// HIR(高级 IR)- 保留高级语法结构// FnDef {// name: "max",// inputs: [i32, i32],// output: i32,// body: If {// cond: BinOp(Gt, a, b),// then: a,// else: b// }// }// MIR(中级 IR)- 基本块 + SSA 风格fn max(i32, i32) -> i32 { let _0: i32; // 返回值 let _1: i32; // 参数 a let _2: i32; // 参数 b
bb0: { _3 = Gt(_1, _2); // a > b switchInt(_3) -> [0: bb2, otherwise: bb1] }
bb1: { _0 = _1; // return a return }
bb2: { _0 = _2; // return b return }}; LLVM IR - 最终输出define i32 @max(i32 %a, i32 %b) unnamed_addr {start: %0 = icmp sgt i32 %a, %b br i1 %0, label %then, label %else
then: ret i32 %a
else: ret i32 %b}6B.2 借用检查器在 MIR 上的实现
借用检查器分析 MIR 的控制流图,为每个程序点计算借用状态:
NLL 算法的核心步骤:
| 步骤 | 输入 | 输出 | 说明 |
|---|---|---|---|
| 1. 提取借用 | MIR | 借用集合 | 找到所有 &mut/& 引用 |
| 2. 计算区域 | MIR + 借用 | 生命周期区域 | 每个引用活跃的程序点集合 |
| 3. 生成约束 | 区域 | outlives 约束 | ’a: ‘b 表示 a 活至少和 b 一样长 |
| 4. 求解约束 | 约束集 | 是否有解 | 如果无解,存在生命周期错误 |
| 5. 检查冲突 | 借用+区域 | 错误报告 | 可变+不可变同时活跃则报错 |
6B.3 宏展开深入
Rust 的宏系统在编译早期阶段工作——宏展开发生在 HIR 构建之前:
三种过程宏的区别:
| 类型 | 签名 | 用途 | 示例 |
|---|---|---|---|
| 派生宏 | fn(TokenStream) -> TokenStream | 为类型自动实现 trait | #[derive(Debug)] |
| 属性宏 | fn(TokenStream, TokenStream) -> TokenStream | 修改/增强项 | #[serde(rename)] |
| 函数式宏 | fn(TokenStream) -> TokenStream | 任意代码生成 | sql!("SELECT ...") |
6B.4 生命周期省略规则
Rust 编译器有三条省略规则,让 90% 的函数不需要显式标注生命周期:
规则 1:每个引用参数获得一个独立的生命周期 fn first_word(s: &str) -> &str → fn first_word<'a>(s: &'a str) -> &str
规则 2:如果只有一个输入生命周期,它赋给所有输出 fn first_word<'a>(s: &'a str) -> &'a str // 自动应用
规则 3:如果有多个输入生命周期但包含 &self/&mut self, self 的生命周期赋给所有输出 fn get(&self, key: &str) -> &Value → fn get<'a, 'b>(&'a self, key: &'b str) -> &'a Value| 场景 | 需要显式标注? | 原因 |
|---|---|---|
| 单引用参数 | 否 | 规则 1+2 自动推导 |
| 方法返回 &self 字段 | 否 | 规则 3 自动推导 |
| 多引用参数,返回其一 | 是 | 编译器无法确定返回哪个 |
| 返回值与输入无关 | 是 | 需要静态生命周期 ‘static |
| 闭包 | 否 | 编译器自动推断 |
生命周期省略规则是 Rust 编程体验的关键——如果每个函数都需要显式标注生命周期,代码会变得非常冗长。省略规则在保证安全的前提下,让最常见的模式无需标注。但当你看到编译器报 lifetime 错误时,理解这三条规则能帮你快速定位问题。
七、总结
在上一章中,Go 编译器用逃逸分析决定堆分配、用并发 GC 回收垃圾——运行时兜底,编译器配合。Rust 不走这条路:没有 GC,没有运行时开销,内存安全完全由编译器在编译期保证。借用检查器就是 Rust 的安全网,它拒绝一切可能违反所有权规则的代码。
| 组件 | 作用 | 类比 |
|---|---|---|
| rustc 查询系统 | 增量编译、按需计算 | 数据库查询引擎 |
| HIR | 宏展开 + 类型检查 | Go 的带类型 AST |
| MIR | 借用检查 + 优化 | Go 的 SSA |
| 借用检查器 | 编译期内存安全 | Go 的逃逸分析 |
| NLL | 非词法生命周期 | — |
| 宏系统 | 编译时代码生成 | C 预处理器(但更安全) |
| LLVM 后端 | 优化 + 代码生成 | Go 的自研后端 |
Rust 编译器最独特之处是借用检查器——它在编译期证明了内存安全,无需运行时开销。这是编译器技术与程序验证的交叉点,也是 Rust 区别于所有其他语言的核心创新。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






