Rust语言编译整体流程

前言

rust-complie-process

上图中间部分为 Rust 代码的整体编译过程,左右两边分别为过程宏和声明宏的解释过程。

Rust 语言是基于 LLVM 后端实现的编程语言。在编译器层面来说,Rust编译器仅仅是一个编译器前端,它负责从文本代码一步步编译到LLVM中间码(LLVM IR),然后再交给LLVM来最终编译生成机器码,所以LLVM就是编译后端。

编译整体流程

1、词法分析

Rust 文本代码首先要经过「词法分析」阶段。将文本语法中的元素,识别为对 Rust 编译器有意义的「词条」,即token

2、语法分析

经过词法分析之后,再通过语法分析将词条流转成「抽象语法树(AST)」。

3、语义分析

在得到 AST(Abstract Syntax Tree) 之后,Rust 编译器会对其进行「语义分析」。一般来说,语义分析是为了检查源程序是否符合语言的定义。在 Rust 中,语义分析阶段将会持续在两个中间码层级中进行。

IR:Intermediate Representation,中间代码,是处于源代码和目标代码之间的一种表示形式。

3.1、语义分析 HIR(High-Level IR)阶段

HIR 是抽象语法树(AST)对编译器更友好的表示形式,很多 Rust 语法糖在这一阶段,已经被脱糖(desugared)处理。比如 for 循环在这个阶段会被转为loopif let 被转为match,等等。HIR 相对于 AST 更有利于编译器的分析工作,它主要被用于 「类型检查(type check)、推断(type inference)」。

3.2、语义分析 MIR(Middle IR)阶段

MIR 是 Rust 代码的中间表示,基于 HIR 进一步简化构建。MIR 是在RFC 1211中引入的。

MIR 主要用于借用检查。早期在没有 MIR 的时候,借用检查是在 HIR 阶段来做的,所以主要问题就是生命周期检查的粒度太粗,只能根据词法作用域来进行判断,导致很多正常代码因为粗粒度的借用检查而无法通过编译。Rust 2018 edition 中引入的 非词法作用域生命周期(NLL)就是为来解决这个问题,让借用检查更加精细。NLL 就是因为 MIR 的引入,将借用检查下放到 MIR 而出现的一个术语,这个术语随着 Rust 的发展终将消失。

MIR 这一层其实担负的工作很多,除了借用检查,还有代码优化、增量编译、Unsafe 代码中 UB 检查、生成LLVM IR等等。关于 MIR 还需要了解它的三个关键特性:

  • 它是基于控制流图(编译原理:Control Flow Graph)的。
  • 它没有嵌套表达式。
  • MIR 中的所有类型都是完全明确的,不存在隐性表达。人类也可读,所以在 Rust 学习过程中,可以通过查看 MIR 来了解 Rust 代码的一些行为。

图中没有画出来的,还有一个从 HIR 到 MIR 的一个过渡中间代码表示 THIR(Typed HIR) 。THIR 是对 HIR 的进一步降级简化,用于更方便地构建 MIR 。在源码层级中,它属于 MIR 的一部分。

4、中间代码生成

生成 LLVM IR 阶段。LLVM IRLLVM中间语言。LLVM会对LLVM IR进行优化,再生成为机器码。

Rust中的宏

Rust 本质上存在两类宏:声明宏(Declarative Macros) 与 过程宏(Procedural Macros) 。

声明宏

回头再看看上面的图右侧部分。我们知道,Rust 在最初解析文本代码都时候会将代码进行词法分析生成词条流(TokenStream)。在这个过程中,如果遇到了宏代码(不管是声明宏还是过程宏),则会使用专门的「宏解释器(Macro Parser)」 来解析宏代码,将宏代码展开为 TokenStream,然后再合并到普通文本代码生成的 TokenSteam 中。

你可能会有疑问,其他语言的宏都是直接操作 AST ,为什么 Rust 的宏在 Token 层面来处理呢?

这是因为 Rust 语言还在高速迭代期,内部 AST 变动非常频繁,所以无法直接暴露 AST API 供开发者使用。而词法分析相对而言很稳定,所以目前 Rust 宏机制都是基于词条流来完成的。

那么声明宏,就是完全基于词条流(TokenStream)。声明宏的展开过程,其实就是根据指定的匹配规则(类似于正则表达式),将匹配的 Token 替换为指定的 Token 从而达到代码生成的目的。因为仅仅是 Token 的替换(这种替换依然比 C 语言里的那种宏强大),所以你无法在这个过程中进行各种类型计算。

过程宏

声明宏非常方便,但因为它只能做到替换,所以还是非常有局限的。所以后来 Rust 引入了过程宏。过程宏允许你在宏展开过程中进行任意计算。过程宏也是基于 TokenSteam API的,只不过由第三方库作者 dtolnay 设计了一套语言外的 AST ,经过这一层 AST 的操作,就实现了想要的结果。

过程宏的工作机制就如上面图中左侧展示的那样。主要是利用三个库,我称之为 「过程宏三件套」:

  • proc_macro2。该库是对 proc_macro 的封装,是由 Rust 官方提供的。
  • syn。该库是 dtolnay 实现的,基于 proc_macro2 中暴露的 TokenStream API 来生成 AST 。该库提供来方便的 AST 操作接口。
  • quote。该库配合 syn,将 AST 转回 TokenSteam,回归到普通文本代码生成的 TokenSteam 中。

过程宏的整个过程,就像是水的生态循环。 蒸汽从大海(TokenSteam)中来,然后通过大雨(Syn),降到地上(Quote),形成涓涓细流(proc_macro2::TokenStream)最终汇入大海(TokenSteam)。


总结

本篇文章主要介绍了 Rust 代码的编译过程,以及 Rust 宏代码的展开机制,学习这些内容,将有助于你深入理解 Rust 的概念。

参考

https://rustmagazine.github.io/rust_magazine_2021/chapter_1/rustc_part1.html

posted @ 2022-12-10 17:15  gaozejie  阅读(2018)  评论(0编辑  收藏  举报