mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1666 字
5 分钟
Rust 编译器与借用检查器
2026-03-02

Go 用逃逸分析决定堆分配、用 GC 回收垃圾——运行时兜底,编译器配合。Rust 不走这条路。没有 GC,没有运行时开销,内存安全完全由编译器在编译期保证。借用检查器(Borrow Checker)就是 Rust 的安全网——它拒绝一切可能违反所有权规则的代码,哪怕那代码在运行时永远不会出问题。这种”宁可误杀不可放过”的策略,让 Rust 成了系统编程领域近十年来最重要的语言。

一、rustc 架构#

1.1 查询驱动编译#

graph TB SRC["源码"] --> AST["AST<br/>解析"] AST --> HIR["HIR<br/>高级 IR<br/>宏展开+类型检查"] HIR --> MIR["MIR<br/>中级 IR<br/>借用检查+优化"] MIR --> LLVM["LLVM IR<br/>代码生成"] LLVM --> BIN["机器码"] QUERY["查询系统<br/>增量编译<br/>按需计算"] style QUERY fill:#fff9c4,stroke:#f9a825

rustc 采用查询驱动架构:每个编译步骤是一个查询,结果被缓存,只有输入变化时才重新计算。这使得增量编译高效——修改一个函数只重新编译受影响的部分。

编译阶段输入输出主要工作
解析源码AST词法分析 + 语法分析
宏展开AST → HIRHIRmacro_rules! + 过程宏
类型检查HIR带类型的 HIR类型推导 + trait 求解
LoweringHIRMIR降级为控制流图
借用检查MIRMIR所有权 + 生命周期验证
优化MIRMIR常量折叠、内联等
代码生成MIRLLVM IR调用 LLVM 后端

1.2 与 Go 编译器对比#

维度GoRust
编译架构顺序流水线查询驱动
增量编译简单(包级)精细(查询级)
类型系统简单(结构化)复杂(泛型 + 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 // 析构
// return
graph TB BB0["bb0: _1 = &_2<br/>_3 = move _1<br/>switch _3"] -->|"true"| BB1["bb1: _4 = (*_3).field<br/>drop _3<br/>return"] BB0 -->|"false"| BB2["bb2: drop _1<br/>return"] style BB0 fill:#e3f2fd,stroke:#1565c0

MIR 的关键特性:

  • 基本块:每个块以一个终结指令结束(分支/返回/调用)
  • 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, %3

Rust 通过 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——借用的生命周期不再基于词法作用域,而是基于实际使用

graph LR subgraph "词法生命周期(旧)" L1["let r = &x;"] --> L2["println!(r);"] L2 --> L3["x = 2; "] L3 --> L4["}"] L_NOTE["r 的生命周期到 } 结束<br/>x = 2 时 r 仍活跃"] end subgraph "NLL(新)" N1["let r = &x;"] --> N2["println!(r);"] N2 --> N3["x = 2; "] N3 --> N4["}"] N_NOTE["r 的生命周期到 println! 结束<br/>x = 2 时 r 已不活跃"] end style L3 fill:#ffcdd2,stroke:#c62828 style N3 fill:#c8e6c9,stroke:#2e7d32

四、生命周期推导#

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 内所有引用的生命周期
- 返回约束:返回值的生命周期必须 ≤ 某个输入的生命周期
Note

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; // 推导为 i32
let y = 3.14; // 推导为 f64
let z = vec![]; // 需要类型注解或上下文
let z: Vec<i32> = vec![]; // 显式类型
// 闭包类型推导
let add = |x, y| x + y; // 推导为 i32 -> i32 -> i32

6.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 的控制流图,为每个程序点计算借用状态

flowchart TB MIR_INPUT["MIR 控制流图"] --> BORROW["提取借用信息<br/>每个 Local 的借用"] BORROW -> LIFETIME["计算生命周期<br/>NLL: 基于数据流分析"] LIFETIME -> CHECK["检查冲突<br/>可变+不可变同时活跃?"] CHECK -> RESULT["通过 或 报错"] LIFETIME -> REGION["区域推断<br/>每个引用的生命周期区域"] REGION -> OUTLIVES["检查 outlives 约束<br/>'a: 'b"] OUTLIVES -> RESULT style MIR_INPUT fill:#e3f2fd,stroke:#1565c0 style CHECK fill:#fce4ec,stroke:#c62828 style RESULT fill:#e8f5e9,stroke:#2e7d32

NLL 算法的核心步骤:

步骤输入输出说明
1. 提取借用MIR借用集合找到所有 &mut/& 引用
2. 计算区域MIR + 借用生命周期区域每个引用活跃的程序点集合
3. 生成约束区域outlives 约束’a: ‘b 表示 a 活至少和 b 一样长
4. 求解约束约束集是否有解如果无解,存在生命周期错误
5. 检查冲突借用+区域错误报告可变+不可变同时活跃则报错

6B.3 宏展开深入#

Rust 的宏系统在编译早期阶段工作——宏展开发生在 HIR 构建之前:

flowchart TB SRC["源码(含宏)"] --> LEX["词法分析<br/>Token 流"] LEX --> PARSE["语法分析<br/>AST"] PARSE --> EXPAND["宏展开<br/>递归展开直到无宏"] EXPAND --> NAME_RES["名称解析<br/>模块树+作用域"] NAME_RES --> HIR2["HIR 构建与类型检查"] EXPAND -->|"调用 proc-macro"| PM["过程宏<br/>接收 TokenStream<br/>返回 TokenStream"] EXPAND -->|"匹配 macro_rules!"| DM["声明宏<br/>模式匹配+替换"] style EXPAND fill:#fff3e0,stroke:#e65100 style PM fill:#e1bee7,stroke:#6a1b9a

三种过程宏的区别:

类型签名用途示例
派生宏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
闭包编译器自动推断
Note

生命周期省略规则是 Rust 编程体验的关键——如果每个函数都需要显式标注生命周期,代码会变得非常冗长。省略规则在保证安全的前提下,让最常见的模式无需标注。但当你看到编译器报 lifetime 错误时,理解这三条规则能帮你快速定位问题。

七、总结#

上一章中,Go 编译器用逃逸分析决定堆分配、用并发 GC 回收垃圾——运行时兜底,编译器配合。Rust 不走这条路:没有 GC,没有运行时开销,内存安全完全由编译器在编译期保证。借用检查器就是 Rust 的安全网,它拒绝一切可能违反所有权规则的代码。

组件作用类比
rustc 查询系统增量编译、按需计算数据库查询引擎
HIR宏展开 + 类型检查Go 的带类型 AST
MIR借用检查 + 优化Go 的 SSA
借用检查器编译期内存安全Go 的逃逸分析
NLL非词法生命周期
宏系统编译时代码生成C 预处理器(但更安全)
LLVM 后端优化 + 代码生成Go 的自研后端
Tip

Rust 编译器最独特之处是借用检查器——它在编译期证明了内存安全,无需运行时开销。这是编译器技术与程序验证的交叉点,也是 Rust 区别于所有其他语言的核心创新。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Rust 编译器与借用检查器
https://blog.souloss.com/posts/compiler/rust-compiler/
作者
Souloss
发布于
2026-03-02
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时