Rust起飞系列: c. 编译系统
编译系统
参考文档:
Rust编译过程
rustc 的编译过程包含两个层面的“转换阶段”:
- 一是源代码到可执行文件的编译流程
- 二是编译器自身的构建流程
┌─────────────────────┐
│ 源代码 (.rs) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 词法分析 (Lexer) │ ← 普通注释 (//, /* */) 在此阶段被识别并丢弃
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 语法分析 (Parser) │ → 生成 AST(含宏调用、属性)
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 宏展开 & 配置处理 │ ← 文档注释 (///, //!) 被转换为 #[doc] 属性
│ (Macro Expansion) │ 并在此阶段展开所有宏
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ HIR 生成 │ → 去语法糖,结构化表示
│ (High-Level IR) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 类型检查 & 借用检查 │ ← 最复杂阶段:类型推导、trait解析、生命周期、所有权
│ (Type & Borrowck) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ MIR 生成与优化 │ → 基于控制流图,进行多种优化
│ (Mid-Level IR) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 代码生成 (LLVM IR) │ → 将 MIR 转换为 LLVM IR,并交由 LLVM 优化
│ (Code Generation) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 链接 (Linking) │ → 调用系统链接器,合并目标文件和库
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 可执行文件 / 库 │
└─────────────────────┘
一. 源代码的编译流程:从文本到机器码
rustc 将 Rust 源代码转换为可执行文件,主要经历以下几个关键阶段:
-
词法分析 (Lexing)
- 输入:源代码 (
&str)。 - 过程:
rustc_lexer将源代码字符流转换为一个个的标记(Token),如关键字、标识符、运算符等。 - 输出:Token 流。
- 输入:源代码 (
-
语法分析 (Parsing)
- 输入:Token 流。
- 过程:
rustc_parse将 Token 流按照 Rust 语法规则,构建成一棵抽象语法树(AST)。此时,AST 中仍包含宏调用和属性。 - 输出:AST(抽象语法树)。
-
宏展开与配置处理 (Macro Expansion & Configuration)
- 输入:未展开的 AST。
- 过程:
rustc_expand负责展开所有宏,并处理#[cfg]等条件编译属性,移除不需要的代码。 - 输出:一个经过展开、不再包含宏的完整 AST。
-
高层中间表示 (HIR) 生成
- 输入:展开后的 AST。
- 过程:
rustc_hir将 AST 降级为更便于后续分析的高层中间表示(HIR)。HIR 对 AST 进行了结构化处理,去除了部分语法糖。 - 输出:HIR。
-
类型检查与借用检查 (Type Checking & Borrow Checking)
- 输入:HIR。
- 过程:这是最复杂的阶段。
rustc_hir_analysis和rustc_borrowck等组件会对 HIR 进行完整的类型推导、trait 解析、生命周期检查和所有权验证,确保代码的内存安全。 - 输出:验证通过的 HIR。
-
中间中间表示 (MIR) 生成与优化
- 输入:验证后的 HIR。
- 过程:
rustc_mir_build将 HIR 转换为更底层的中间中间表示(MIR)。MIR 是一种基于控制流图的表示,更适合进行借用检查和各种优化。随后,rustc_mir_transform会对 MIR 执行一系列优化。 - 输出:优化后的 MIR。
-
代码生成 (Code Generation)
- 输入:优化后的 MIR。
- 过程:这是后端的工作。
rustc_codegen_llvm(或其他后端如 GCC、Cranelift)将 MIR 转换为 LLVM IR。LLVM IR 随后被传递给 LLVM 进行进一步的优化,并最终生成目标平台的机器码。 - 输出:目标文件(
.o文件)或汇编代码。
-
链接 (Linking)
- 输入:多个目标文件和系统库。
- 过程:编译器调用系统链接器,将所有目标文件和必要的库组合在一起,解决符号引用。
- 输出:最终的可执行文件或库。
二. 编译器自身的构建流程:自举 (Bootstrapping)
Rust 编译器本身也是用 Rust 写的,它的构建过程被称为“自举”,分为几个阶段:
-
Stage 0:使用预编译好的上一版本官方
rustc编译器(即所谓的stage0编译器)来编译当前rustc的源码。这个阶段产生的是stage1编译器。 -
Stage 1:使用刚刚在 Stage 0 中生成的
stage1编译器,再次编译当前的rustc源码。这个阶段产生的是stage2编译器。 -
Stage 2:
stage2编译器被认为是“真正的”当前版本编译器。为了验证其正确性,通常还会用它再次编译标准库,确保结果与 Stage 1 编译的一致(即 Stage 3 构建)。这种多阶段构建方式确保了新版本的编译器能够自我编译,是一种对编译器本身的验证。
三. 源码注释的处理
普通注释在词法分析阶段就被丢弃,而文档注释则在宏展开阶段被转换为 #[doc] 属性,供 rustdoc 等工具在后续阶段使用。
-
普通注释:在词法分析阶段被忽略
普通的代码注释(如
//和/* ... */)在词法分析(Lexing)阶段就被识别并当作空白符(whitespace)丢弃了。它们不会进入后续的编译流程,也不会对生成的程序产生任何影响。rustc的词法分析器(rustc_lexer)会将它们识别为LineComment或BlockComment类型的 Token,但在构建用于编译的 Token 流时,这些注释 Token 会被直接忽略。 -
文档注释:在宏展开阶段转换为属性
以
///或//!开头的文档注释,处理方式则完全不同。它们在宏展开(Macro Expansion)阶段被编译器捕获,并转换为#[doc = "..."]这样的属性。这个转换过程在编译早期发生,其关键点在于:
- 文档注释的内容会被提取并转换成
doc属性,附加到它们所注解的项(item)上。 - 从编译器的角度看,
///注释最终和#[doc = "..."]属性是等价的。 - 这些
doc属性会像其他属性一样,保存在抽象语法树(AST)中,并随之后的编译流程传递。
- 文档注释的内容会被提取并转换成
-
后续阶段
转换为
#[doc]属性后,文档注释的信息就可供编译器的后续阶段和其他工具使用了:rustdoc:这是最主要的消费者。rustc_resolvecrate 中的rustdoc.rs模块会解析这些#[doc]属性,提取其中的信息来生成 API 文档。- 宏展开:在宏展开过程中,编译器会特别注意文档注释,因为它们会被转换为特定的属性。
- 抽象语法树(AST):注释的位置信息会通过
Span与 AST 节点关联,但注释本身并不作为独立的语法结构存在。在需要保留注释的场景(如代码格式化工具)中,会通过特殊手段(如注释向量)来管理。
浙公网安备 33010602011771号