什么是宏?
熟悉C/C++的朋友应该很熟悉宏(Macro)的概念,而Rust初学者也必定会接触到Rust中的宏,其Hello world程序中就会用到println!宏。
可以简单地理解为:宏即编译时将执行的一系列指令。其重点在于「编译时」,尽管宏与函数(或方法)形似,函数是在运行时发生调用的,而宏是在编译时执行的。
不同于C/C++中的宏,Rust的宏并非简单的文本替换,而是在词法层面甚至语法树层面作替换,其功能更加强大,也更加安全。
如下所示的一个C++的宏SQR的定义
#include #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]pubfn seq(input: TokenStream)-> TokenStream{let_=input;unimplemented!()}
使用方法如下:
seq!{nin0..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上的仓库:https://github.com/dtolnay/proc-macro-workshop/github.com
由于作者初学Rust水平有限,如有错误,欢迎批评指正!