Rust语言的宏编程极简教程Macro

Rust的宏和c/c++的异同

Rust语言的宏设计得比较复杂,当然也可以说功能非常强大,跟c/c++语言的宏非常不一样。
相同点 当然是宏能够让代码更精简,码农可以少敲很多样板代码(boilerplate code)。当然你说函数和类不也可以抽象代码,使代码精简吗?但宏能够获取很多在编译期的信息,函数和类却不能。比如,代码所在的文件和行数,这个信息只有宏才能获得。
不同点 是,C/C++的宏只是在预编译期简单地做模式替换,预编译后再交由编译器。c/c++的include头文件展开也是在预编译阶段。但Rust宏的展开不是发生在预编译期,而是编译期。于是Rust能够获得更复杂的更详细的编译器的信息。这里有两个概念Token Tree和AST。如果有大学阶段学习过的编译原理课程的背景就很容易理解。

Token Tree

Token Tree简写成TT。编译器拿到源代码后先做词法分析,即把源代码字节流分成一个一个的token。token和token之间的逻辑关系也记录下来。比如下面的一个简单语句:

a + b + (c + d[0]) + e

的Token Tree就长这个样子:

«a» «+» «b» «+» «(   )» «+» «e»
         ╭────────┴──────────╮
          «c» «+» «d» «[   ]»
                       ╭─┴─╮
                        «0»

AST

AST的全称是Abstract Syntax Tree,抽象语法树。编译器把Token Tree翻译成AST,这是一个更有利于编译器理解源代码的结构。上面的TT翻译成对应的AST后长这个样子:

              ┌─────────┐
              │ BinOp   │
              │ op: Add │
            ┌╴│ lhs: ◌  │
┌─────────┐ │ │ rhs: ◌  │╶┐ ┌─────────┐
│ Var     │╶┘ └─────────┘ └╴│ BinOp   │
│ name: a │                 │ op: Add │
└─────────┘               ┌╴│ lhs: ◌  │
              ┌─────────┐ │ │ rhs: ◌  │╶┐ ┌─────────┐
              │ Var     │╶┘ └─────────┘ └╴│ BinOp   │
              │ name: b │                 │ op: Add │
              └─────────┘               ┌╴│ lhs: ◌  │
                            ┌─────────┐ │ │ rhs: ◌  │╶┐ ┌─────────┐
                            │ BinOp   │╶┘ └─────────┘ └╴│ Var     │
                            │ op: Add │                 │ name: e │
                          ┌╴│ lhs: ◌  │                 └─────────┘
              ┌─────────┐ │ │ rhs: ◌  │╶┐ ┌─────────┐
              │ Var     │╶┘ └─────────┘ └╴│ Index   │
              │ name: c │               ┌╴│ arr: ◌  │
              └─────────┘   ┌─────────┐ │ │ ind: ◌  │╶┐ ┌─────────┐
                            │ Var     │╶┘ └─────────┘ └╴│ LitInt  │
                            │ name: d │                 │ val: 0  │
                            └─────────┘                 └─────────┘

AST图有排版有点乱,我懒得改了。这两人个图,TT和AST都是从 https://danielkeep.github.io/tlborm/book/mbe-syn-source-analysis.html 抄来的。您可以移步到那个页面。
重点来了,Rust语言的宏展开就发生在编译器生成了源代码的AST的时候。Rust语言的宏可以从AST获得非常丰富的信息,并操作AST。

Rust的宏有两种

Rust语言设计了两种宏,一种叫Declarative Macros(声明式宏),以前的版本也有Macros by example这个名字,旧的名字怪怪的。另一种是Procedure Macros(过程宏)我不明白为什么取这个名字。Declarative Macro相对Procedure Macros要简单一些,而过程宏则可以玩出另复杂的花样。

Declarative Macro

常见的这样一个定义vector的Rust语句:

let v: Vec<u32> = vec![1, 2, 3];

就使用了声明式宏。这个宏的定义如下:

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

#[macro_export]表示将定义的宏将导出crate,这样不仅在定义了这个macro的crate的内部可以使用该宏,导入该crate的其它crate亦可使用这个宏。
然后看这个声明宏的定义,非常像rust语中的匹配表达式(match expression)。$(x:expr)$()* 语法表,表示展开为0条或者多条表达式,其他的都比较好理解。
示匹配一个表达式并把匹配到的表达式存在变量$x里。 $(x:expr)被$(),* 包裹。这里有点像正则式的描述,它表示匹配0个或者多个被逗号,分隔的表达式。 =>后面的部分就是描述怎么展开宏了,除了也用到类似正则式的$()* 语法,表示展开为0条或者多条表达式,其他的都比较好理解。
另外,这个宏的定义只使用了一条规则,实际上可以像匹配表达式一样定义多个规则。

Procedure Macros

过程宏可以为一个类自动生成特性trait(类似c++中的纯虚函数,java/c#中的interface)的实现;还能实现类似python中的decoration概念,如下代码所示:

#[route(GET, "/")]
fn index() {
// ...
}

这就跟python的http框架flask用来定义路由的decoration的写法几乎一样了。
不过,编写procedure macros需要使用两个辅助的crate用来操作AST,它们是sync和quote。挺复杂的,你们还是去看官方方档吧。

hygiene是什么

在读官方的Rust Macro相关的资料时,时不时看到hygiene这个詞。一开始不懂这是个什么玩艺儿。后来,才知道原来是rust的宏机制避免了c/c++简单的宏经常出现的副作用。在Rust的话术里,把这个东东称为Hygiene。

就写到这吧,写文章好累呀,还是写代码轻松有意思一些。