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。
就写到这吧,写文章好累呀,还是写代码轻松有意思一些。