Rust过程宏入门(一)——过程宏简介
写于2021年1月6日。最早发表于知乎。
什么是宏?
熟悉C/C++的朋友应该很熟悉 宏(Macro) 的概念,而Rust初学者也必定会接触到Rust中的宏,其Hello world程序中就会用到println!
宏。
可以简单地理解为: 宏即编译时将执行的一系列指令。其重点在于「编译时」,尽管宏与函数(或方法)形似,函数是在运行时发生调用的,而宏是在编译时执行的。
不同于C/C++中的宏,Rust的宏并非简单的文本替换,而是在词法层面甚至语法树层面作替换,其功能更加强大,也更加安全。
如下所示的一个C++的宏SQR的定义
#include <iostream>
#define SQR(x) (x * x)
int main() {
std::cout << SQR(1 + 1) << std::endl;
return 0;
}
我们希望它输出4
,但很遗憾它将输出3
,因为SQR(1 + 1)
在预编译阶段通过文本替换展开将得到(1 + 1 * 1 + 1)
,并非我们所期望的语义。
而在Rust中,按如下方式定义的宏:
macro_rules! sqr { ($x:expr) => {$x * $x} } fn main() { println!("{}", sqr!(1 + 1)); }
将得到正确的答案4
。这是因为Rust的宏展开发生在语法分析阶段,此时编译器知道sqr!
宏中的$x
变量是一个表达式(用$x:expr
标记),所以在展开后它知道如何正确处理,会将其展开为((1 + 1) * (1 + 1))
。
由于本文介绍的重点是过程宏,因此涉及普通宏的内容便不多赘述,有兴趣者可参考官方文档上的介绍。
什么是过程宏?
过程宏(Procedure Macro) 是Rust中的一种特殊形式的宏,它将提供比普通宏更强大的功能。方便起见,本文将Rust中由macro_rules!
定义的宏称为 声明宏 以示区分。
过程宏分为三种:
- 派生宏(Derive macro):用于结构体(struct)、枚举(enum)、联合(union)类型,可为其实现函数或特征(Trait)。
- 属性宏(Attribute macro):用在结构体、字段、函数等地方,为其指定属性等功能。如标准库中的
#[inline]
、#[derive(...)]
等都是属性宏。 - 函数式宏(Function-like macro):用法与普通的声明宏类似,但功能更加强大,可实现任意语法树层面的转换功能。
过程宏的定义与使用方法
派生宏
派生宏的定义方法如下:
#[proc_macro_derive(Builder)]
fn derive_builder(input: TokenStream) -> TokenStream {
let _ = input;
unimplemented!()
}
其使用方法如下:
#[derive(Builder)]
struct Command {
// ...
}
属性宏
属性宏的定义方法如下:
#[proc_macro_attribute]
fn sorted(args: TokenStream, input: TokenStream) -> TokenStream {
let _ = args;
let _ = input;
unimplemented!()
}
使用方法如下:
#[sorted]
enum Letter {
A,
B,
C,
// ...
}
函数式宏
函数式宏的定义方法如下:
#[proc_macro]
pub fn seq(input: TokenStream) -> TokenStream {
let _ = input;
unimplemented!()
}
使用方法如下:
seq! { n in 0..10 {
/* ... */
}}
过程宏的原理
以上三种过程宏的定义方法已全部介绍。可以发现,它的定义方式与普通函数无异,只不过其函数调用发生在编译阶段而已。下面以较为常见的派生宏为例,介绍过程宏的原理。
回顾刚才的定义:
#[proc_macro_derive(Builder)]
fn derive_builder(input: TokenStream) -> TokenStream {
let _ = input;
unimplemented!()
}
首先,#[proc_macro_derive(Builder)]
表明derive_builder
是一个派生宏,Builder
表示它将作用的地方。比如定义如下结构体
#[derive(Builder)]
struct Command {
// ...
}
就会触发以上派生宏执行。至于其中的Builder
具体代表什么含义,本期暂不展开,后面再详细介绍。
fn derive_builder(input: TokenStream) -> TokenStream
函数头部表明该函数将接受一个TokenStream
对象作为输入,并返回一个TokenStream
对象。
要理解TokenStream
,需要一些简单的编译原理知识。编译器在编译一段程序时,会首先将输入的文本转换成一系列的Token(标识符、关键字、符号、字面量等),同时忽略注释(文档注释除外)与空白字符等。
例如println!("Hello world");
这句代码将被转换成标识符println
、叹号!
、圆括号(
、字面量"Hello world"
、圆括号)
、分号;
几个Token。
TokenStream
顾名思义,是Rust中对一系列连续的Token的抽象。在宏展开的过程中,遇到派生宏时,会将整个结构体(或enum
、union
)展开成TokenStream
作为派生宏函数的输入,然后将其输出的TokenStream
附加到结构体后面,再继续作语法分析。
本期的介绍到此结束,主要介绍了过程宏的基本概念、定义及使用方法、实现原理。接下来将通过几个具体实例详细介绍Rust过程宏的编程方法。
实例来源自GitHub上的仓库: dtolnay/proc-macro-workshop。