Rust 过程宏(第二弹)

## ⚡ Rust Macros 宏编程
- [The Little Book of Rust Macros](https://veykril.github.io/tlborm/)
- [The Little Book of Rust Macros](https://danielkeep.github.io/tlborm/book/index.html)
- [Introduction to Procedural Macros in Rust](https://tinkering.xyz/introduction-to-proc-macros/)
- [Procedural Macros in Rust 2018](https://blog.rust-lang.org/2018/12/21/Procedural-Macros-in-Rust-2018.html)
- [3.2. Procedural Macros](https://doc.rust-lang.org/reference/procedural-macros.html)
- https://doc.rust-lang.org/rust-by-example/macros.html
- https://doc.rust-lang.org/book/ch19-06-macros.html
- https://doc.rust-lang.org/stable/reference/attributes.html#active-and-inert-attributes
- https://docs.rs/json/0.12.4/json/
- https://github.com/Jeangowhy/Godot-Tour
首先,不像 C/C++ 的宏定义,只是简单的代码预处理程序,用宏代码替换一下部分源代码。Rust 的宏具有相当复杂的功能,更贴近编译器的语法树处理。
简单地说,Rust 宏就是内嵌的 DSL - Domain Specific Languages 可以让你可以发明自己的语法,编写出可以自行展开的代码,并且还可以实现静态反射功能。
宏的应用是符合 DRY (Don't Repeat Yourself) 软件工程原理的,有轮子的车就让它跑,没有必要重新造轮子,Don't write DRY code!
Rust 作为静态编译型语言,rustc 编译器本身由 Rust 语言实现,即实现了自举,后端部分则基于现成的 LLVM。
Rust 编译器简要工作流程如下:
- 首先,读取源代码做 Tokens 扫描,得到 Token stream 数据,这部分程序也叫做 Syntax Analyzer;
- 然后对源码进行词法分析得到 Abstract Syntax Tree (AST) 抽像语法树,这部分程序叫做 Parser;
- 再将 AST 转换为 High-Level IR (HIR) 以便做类型推断、trait 接口处理以及静态类型安全性检查;
- 再转换为 Mid-level IR (MIR) 以便做所有权借用检查和代码优化,MIR 也是 Control-Flow Graph (CFG);
- 经过以上前端工作后,代码会转译为 LLVM IR,文件后缀一般是 .ll,是文本格式,字节码文件后缀是 .bc;
- 得到中间代码表达,下一步就是生成相应的机器码,这就是 LLVM 要做的工作。
通过 `macro_rules` 关键字定义的一系列规则,在调用宏时,编译器会先匹配规则,匹配中的 $expansion 才被展开。
除了使用 `macro_rules` 关键字定义宏规则,Rust 有三种宏程序:
- Function-like macros - `custom!(...)` 使用 `#[proc_macro]` 定义;
- Derive macros - `#[derive(CustomDerive)]` 使用 `#[proc_macro_derive]` 定义;
- Attribute macros - `#[CustomAttribute]` 使用 `#[proc_macro_attribute]` 定义;
宏程序使用 Cargo new --lib 命令创建一个库,并且指定 proc-macro 类型,这样才能够使用过程宏:
[lib]
proc-macro = true
[dependencies]
quote = "1.0"
syn = "1.0"
proc-macro2 = "1.0"
宏程序与函数的区别在于后缀的感叹号:
```rust,ignore
macro_rules! $name {
($pattern) => {$expansion}; // $rule0 ;
($pattern) => {$expansion}; // $rule1 ;
($pattern) => {$expansion}; // ...
($pattern) => {$expansion}; // $ruleN ;
}
```
宏至少定义一条规则,最后一条规则的分号可省略。
列如,有以下这样一个宏定义:
macro_rules! four {
() => {1 + 3};
}
调用宏时,编译器会匹配到输入为空的条件,包括 `four!()`, `four![]` or `four!{}` 这几种调用形式。
很有必要从编译器语法树构建原理的角度来解析宏的概念。
Rust 系统中有许多类型的 Tokens:
- Identifiers: foo, Bambous, self, we_can_dance, LaCaravane, …
- Integers: 42, 72u32, 0_______0, …
- Keywords: _, fn, self, match, yield, macro, …
- Lifetimes: 'a, 'b, 'a_rare_long_lifetime_name, …
- Strings: "", "Leicester", r##"venezuelan beaver"##, …
- Symbols: [, :, ::, ->, @, <-, …
这些会出现在代码中的 Tokens 经过编译器初步处理,就会转换成 AST - Abstract Syntax Tree,Token trees 则是介于 Tokens 与 AST 之间的东西。以树状数据结构方式存放,所以叫做语法树。
将源代码的字符流转换成 Token 是编译原理最开始的部分,做这一步工作的程序叫做 Lexical Analyzer 词法分析器,然后将源代码中字符串中的 Tokens 转换为 AST,这一步对应的程序叫做 Syntax Analyzer,即词法解析器 Parser。
编写过程宏,通常需要借助三个 crate 来解析语法树中的节点数据:
- syn 模块用来解析语法树(AST)的各种语法构成,即是 Syntax Analyzer。
- quote 解析语法树,生成 Rust 代码,从而实现想要的功能。
- proc_macro(std) 和 proc_macro2(3rd-party)
例如,以下是使用 `println!("{input:#?}");` 打印 `2, 2` 这个表达式对应的 TokenStream 对象:

示范:创建一个库 hello_macro_derive,并配置依赖,还有设置库类型为 proc-macro,即一个宏库:
[lib]
proc-macro = true
[dependencies]
syn = "0.14.4"
quote = "0.6.3"
然后在 hello_macro_derive/lib.rs 文件中自定义宏的功能实现。

编写自定义宏使用的注解是 `#[proc_macro_derive(HelloMacro)]`,其中 HelloMacro 是宏的名称,使用可继承宏时,只需要使用注解 `#[derive(HelloMacro)]` 即可。
在使用时我们应该先引入这个依赖
hello_macro_derive = { path = "../hello_macro_derive" }
然后再来使用,这里定义一个 trait 名字 HelloMacro 和方法名对应上面 quote! 宏定义的内容相匹配:

通过 HelloMacro 可继承宏,Pancakes 这个结构体便自动实现了 hello_macro() 这个接口方法。