rust代码编译过程与宏展开过程

rust代码编译过程

编译过程泳道图

将源代码传递给预处理器将 \$crate 转换成实际 crate 名称展开所有宏定义,包括通过 include 指令添加的文件将预处理过的代码发送给下一个步骤将预处理过的代码分解成 token 串将代码中的注释删除将 token 组装成 AST(抽象语法树)检查代码是否符合 Rust 的语法规范检查 AST 是否符合 Rust 的语义规范进行 borrow checker 检查,确保没有内存安全问题将 AST 传递给代码优化器构建中间代码 IR(intermediate representation)利用 LLVM 进行高效优化将 IR 翻译成汇编代码在宏展开中对于涉及项的类型推断将汇编代码转成具体的机器代码生成可执行二进制文件或动态链接库预处理词法分析语法分析语义分析代码优化和翻译中间代码生成目标代码生成

声明宏展开

声明宏展开过程

输入 Rust 代码将代码解析成 token将 token 存储为输入语法树读取输入语法树将语法树转换成 Rust 代码结构用语法树分析器确定宏的实现方法生成宏的实现代码用实现代码扩展语法树将宏形式参数输入宏展开将语法树转换成 Rust 代码结构重新处理代码生成最终的代码TokenizationParsingExpansion

Rust 的声明宏在编译时对代码进行转换,因此其展开过程是在编译期间完成的。展开过程可以分为三个阶段:

  1. Tokenization:解析输入的 Rust 代码,将其转换成一个个 token(如标识符、关键字、符号等)。
  2. Parsing:将 token 转化成语法树,用于对代码进行进一步处理。
  3. Expansion:对语法树进行处理,生成最终的代码。

在第三个阶段,宏会根据语法树中的特定模式对代码进行重构。这些模式可以是任何 Rust 代码片段,包括类型、结构体、函数、表达式等。宏甚至可以递归地展开自己。

展开过程中,宏的实现代码可以使用标准的 Rust 代码编写,包括控制结构和函数调用等。

展开后的代码与宏的实现代码完全一样,因此对于用户而言,使用宏和使用函数没有什么区别。

过程宏展开

属性、类型、函数三种过程宏展开过程对比

遇到属性宏yes未遇到属性宏执行属性宏遇到类型宏yes未遇到类型宏执行类型宏遇到函数宏yes未遇到函数宏执行函数宏获取宏参数解析宏参数属性是单个值yes属性是多个值获取属性值修改属性获取属性值列表修改属性获取宏参数解析宏参数参数是单个类型yes参数是多个类型获取类型参数生成新类型修改类型获取类型参数列表生成新类型修改类型获取宏参数解析宏参数执行预处理逻辑获取函数体解析函数体需要额外处理yes无需额外处理执行额外处理生成新代码生成新代码返回属性继续解析代码返回类型继续解析代码返回代码继续解析代码解析代码准备解析宏参数返回属性返回类型返回代码

属性宏和类型宏展开过程的流程较为相似,都需要解析宏参数、生成新的属性或类型、修改原有属性或类型,并返回属性或类型等步骤。但是由于其作用的范围不同,展开过程中需要解析的内容也不尽相同。例如,类型宏需要解析的是类型参数,用于生成新的类型;而属性宏需要解析的是属性参数,用于生成新的属性。

  1. 函数宏和其他两种宏不同的是,函数宏需要对整个函数体进行处理,并生成新的代码。在执行完预处理逻辑和解析函数体后,需要判断是否需要进行额外处理,然后才能最终生成新代码并返回。

对比

声明宏
属性宏/类型宏
函数宏
异同点
不能修改源代码
- 作用于某个规模较小的表达式或语句
- 可以在声明中使用
可以修改源代码
- 作用于属性/类型
- 可以在声明中使用
生成新的代码
- 不能修改原有代码
- 作用于整个函数体
- 不能在声明中使用
使用场景
封装底层功能
- 生成器语法扩展
- DRY原则
重载运算符
- 自定义标记
- 序列化/反序列化
实现特殊流程控制
- 实现嵌套参数模板
关键字
macro
- derive
- repr
- allow
- compiler_builtins
- link
macro_rules