:::info 本章内容改编自《Programming Rust, 2nd Edition》的第21章和 Macros in Rust: A tutorial with examples(中文翻译) :::
Rust 宏是什么
Rust 对宏(macro)有着非常好的支持。宏能够使得你能够通过写代码的方式来生成代码,这通常被称为元编程(metaprogramming)。
宏提供了类似函数的功能,但是没有运行时开销。但是,因为宏会在编译期进行展开,所以它会有一些编译期的开销。
宏允许我们在句法水平上进行抽象。宏是一个“展开后的”句法形式的速记。这个展开发生在编译的早期,在任何静态检查之前。因此,宏可以实现很多 Rust 核心抽象不能做到的代码重用模式。
rust编译器提供了一些宏,这些宏在定义自己的宏时非常有用。它们在rustc中被硬编码。如:file!(),line!(),column!(),stringify!(...tokens...),concat!(str0,str1,...),cfg!(),env!("VAR_NAME"),include!("file.rs"),todo!()等等
注意:Rust 宏非常不同于 C 里面的宏。Rust 宏会被应用于词法树(token tree),而 C 语言里的宏则是文本替换。
Rust 宏的类型
- 声明式宏(Declarative macros)。语法类似 match 表达式,但是匹配后进行代码替换而不是直接返回值。编译时,在调用处对输入参数进行匹配,匹配相应分支进行代码替换。
- 过程式宏(Procedural macros)允许你操作给定 Rust 代码的抽象语法树(abstract syntax tree, AST)。过程宏是从一个(或者两个)
TokenStream到另一个TokenStream的函数,用输出的结果来替换宏调用。
Rust中声明式宏
在Rust中,macro_rules!是定义宏的主要方式。
注意:只有在调用宏的时候有!,而在定义时没有!。
注意:与函数不同,宏必须先定义再调用。
创建声明式宏
宏通过使用macro_rules!来声明。声明式宏虽然功能上相对较弱,但提供了易于使用的接口来创建宏来移除重复性代码。声明式宏提供了一个类似match的接口,在匹配时,宏会被匹配分支的代码替换。
macro_rules! add{($a:expr) => {{ $a }};($a:expr,$b:expr) => {{ $a + $b }};}fn main(){let x = 0;add!(x); // => x;add!(1,2); // => 1 + 2;}
格式:(pattern) => (template);
我们可以自由使用[]或{}代替pattern或template周围的括号;
调用宏的时候也可以自由使用(),[],{}。唯一的区别是大括号后面的分号是可选的
一般来说 我们通常如下使用括号: assert_eq!(...);``vec![...];``macro_rule!{...}
可捕获类型
一个宏可以有多个分支,宏根据不同参数展开到不同代码。每一个分支可以接受多个参数,每个参数以$开头,再跟一个token类型。
macro_rules! add_as{($a:expr,$b:expr,$typ:ty) => { $a as $typ + $b as $typ }}fn main(){println!("{}",add_as!(0,2,u8)); // 0 as u8 + 2 as u8 => 2}
重复模式匹配
Rust宏支持接受可变数量的参数。类似正则表达式。*用于零个或多个参数,+用于零或一个参数。重复的 token 类型被$()包裹,后面跟着一个分隔符,再加一个*或一个+,表示这个 token 将会重复的次数。分隔符用于多个 token 之间互相区分。$()后面跟着*和+用于表示重复的代码块。在上面的例子中,+$a是一段重复的代码。
macro_rules! add{($($a:expr), *) => {{ 0 $(+$a)* }}}fn main(){println!("{}",add!(1,2,3,4)); // => println!("{}",{0+1+2+3+4})}
macro_rules! vec{($elem:expr ; $n:expr) => { // vec![3;5];::std::vec::from_elem($elem, $n)};( $( $x:expr ),* ) => { // vec![1,2,3,4,5];<[_]>::into_vec(Box::new([ $( $x ),* ]))// 代码片段中 $x 不仅仅是一个表达式,而是一个表达式列表。// 此规则的template也使用重复语法:};( $( $x:expr ),+ ,) => { // 处理最后一位可能的多余逗号vec![ $($x),* ]};}

一个实例 —— make_public!
改变结构体及其字段的可见性 make_public!
// 只改变struct的可见性// $vis将会拥有可见性,$struct_name将会拥有一个结构体名。// 为了让一个结构体是公开的,我们只需要添加pub关键字并忽略$vis变量。macro_rules! make_struct_public{($vis:vis struct $struct_name:ident {}) => {{ pub struct $struct_name{ } }}}// 对struct的字段同样处理macro_rules! make_struct_allpublic{($vis:vis struct $struct_name:ident {$($field_vis:vis $field_name:ident : $field_type:ty),*})// 匹配struct和他所有的字段=> {{pub struct $struct_name{$(pub $field_name : $field_type,)*}}}}
通常,struct有一些附加的元数据或者过程宏,比如#[derive(Debug)]。这个元数据需要保持完整。解析这类元数据是通过使用meta类型来完成的。
macro_rules! make_public{($(#[$meta:meta])* // 考虑元数据$vis:vis struct $struct_name:ident {$($(#[$field_meta:meta])* // 考虑元数据$field_vis:vis $field_name:ident : $field_type:ty),*$(,)+ // 考虑最后一位多余的逗号}) => {{$(#[$meta])*pub struct $struct_name{$($(#[$field_meta:meta])*pub $field_name : $field_type,)*}}}}
宏的递归形式
TT muncher 一种强大的宏解析模式,一种递归宏,每次消耗一个值。也许这就是为什么叫做咀嚼器,把值一个一个咀嚼掉。
请注意区分 重复模式(Repetition patterns) 和 宏的递归(Recursion in macro)
macro_rules! add{($a:expr) => {$a};($a:expr, $b:expr) => {{$a+$b}};($a:expr, $($b:tt)*) => {{$a+add!($($b)*)}// tt: token tree}}fn main(){println!("{}",add!(1,2,3,4));}
宏的内部规则
通常来讲,很少有宏会被组合到一个宏中。在这些少数情况中,内部的宏规则会被使用。它有助于操作这些宏输入并且写出整洁的 TT munchers。
要创建一个内部规则,需要添加以@开头的规则名作为参数。这个宏将不会匹配到一个内部的规则除非显式地被指定作为一个参数。
macro_rules! ok_or_return{// ident: 一个标识符($a:ident($($b:tt)*)) => { // 匹配 something(q,r,t,6,7,8) 之类的{match $a($($b)*) {Ok(value) => value,Err(err) => {return Err(err);}}}};}fn some_work(i:i64,j:i64) -> Result<(i64,i64),String>{if i+j>2 {Ok((i,j))} else {Err("error".to_owned())}}fn main() -> Result<(),String>{ok_or_return!(some_work(1,4));ok_or_return!(some_work(1,0));Ok(())}
macro_rules! ok_or_return{// internal rule. 无法被外界调用(@error $a:ident,$($b:tt)* )=>{{match $a($($b)*) {Ok(value)=>value,Err(err)=>{return Err(err);}}}};// public rule. 可以被外界调用($a:ident($($b:tt)*))=>{ok_or_return!(@error $a,$($b)*)};}fn some_work(i:i64,j:i64)->Result<(i64,i64),String>{if i+j>2 {Ok((i,j))} else {Err("error".to_owned())}}fn main()->Result<(),String>{ok_or_return!{some_work(1,4)};ok_or_return!(some_work(1,0));// 如前所说,括号可以随意使用Ok(())}
宏的作用域与卫生宏
编写宏有一个令人惊讶的棘手方面,它们涉及将不同范围的代码粘贴在一起。这就涉及到作用域的问题。
macro_rules! dict{({ $($key:tt : $value:tt),* }) => {{let mut fields = Box::new(HashMap::new()); // fields$( fields.insert($key.to_string(), json!($value)); )*fields}};}fn main(){let fields = "Fields, W.C.";let role = dict!({"name": "Larson E. Whipsnade","actor": fields // fields});}// 这里特意使用一个同名变量名fields,但是编译器会自动处理,正常工作
宏中使用临时变量时,内外同名会被编译器自动重命名处理,因此rust的宏被称为是卫生的(hygienic),或者称rust具有卫生宏。Rust 有一个卫生宏系统,每个宏扩展发生在一个独特的“语法语境”,每个变量被产生它的语法语境所标记。好像宏的调用者与宏本身的变量被涂上不同的颜色,因此他们互相不冲突,但是这并不是rust卫生宏的原理。rust的卫生宏可以识别出是否是同一个标识符。
Rust宏的卫生仅限于局部变量和参数。当涉及到常量、类型、方法、模块、静态和宏名称时,Rust是“色盲”。宏内部所用到的所有东西必须在作用域内,否则将出错。
有时候我们期望宏内使用外界的参数,而rust卫生宏会对此造成一定阻碍,对此我们应当将宏需要的变量作为参数传入。
// 错误的做法 理解成文本替换macro_rules! setup_req {() => {// 由于rust卫生宏,这里根本不知道ServerRequest的定义let req = ServerRequest::new(server_socket.session());}}fn handle_http_request(server_socket: &ServerSocket) {setup_req!(); // declares `req`, uses `server_socket`... // code that uses `req`}// 正确的做法 把需要的东西作为参数传入macro_rules! setup_req {($req:ident, $server_socket:ident) => {let $req = ServerRequest::new($server_socket.session());}}fn handle_http_request(server_socket: &ServerSocket) {setup_req!(req, server_socket);... // code that uses `req`}
宏的导入导出
编译器提供了导出和导入宏的特殊功能。在一个模块中可见的宏在其子模块中自动可见。要将宏从模块“向上”导出到其父模块,请使用#[macro_use]属性。例如下所示:
#[macro_use] mod macros;mod client;mod server;// 在macros模块中定义的所有宏都被导入lib.rs。// 这些宏对crate的其余部分都具有可见性,包括client、server模块
标记为#[macro_export]的宏将自动可见,可以像其他一样通过路径引用
但是这样做意味着宏可以被任意模块调用,所以导出的宏要注意不能依赖作用域中的任何内容,即使是标准prelude的内容也可能被覆盖。
声明式宏的限制
- 缺少对宏的自动完成和展开的支持
- 声明式宏调试困难
- 修改能力有限
- 更大的二进制文件
- 更长的编译时间(这一条对于声明式宏和过程宏都存在)
Rust 中的过程宏
过程宏(Procedural macros)是一种更为高级的宏。过程宏能够扩展 Rust 的现有语法。它接收任意输入并产生有效的 Rust 代码。过程宏之所以“过程化”,是因为它是作为一个Rust函数实现的,而不是一个声明性规则集。此函数通过一层薄薄的抽象层与编译器交互,可以是任意复杂的。
过程宏接收一个TokenStream作为参数并返回另一个TokenStream。过程宏对输入的TokenStream进行操作并产生一个输出。有三种类型的过程宏:
- 属性式宏(Attribute-like macros)
- 派生宏(Derive macros)
- 函数式宏(Function-like macros)
属性式宏
属性式宏能够让我们创建一个自定义的属性,该属性将其自身关联一个项(item),并允许对该项进行操作。它也可以接收参数。 ```rust[some_attribute_macro(some_argument)]
fn perform_task() { // some code }
在上面的代码中,`some_attribute_macros`是一个属性宏,它对函数`perform_task`进行操作。<br />为了编写一个属性式宏,我们先用`cargo new macro-demo --lib`来创建一个项目。创建完成后,修改`Cargo.toml`来通知 cargo,该项目将会创建过程宏。```rust[lib]proc-macro = true[dependencies]syn = {version="1.0.57",features=["full","fold"]}quote = "1.0.8"
过程宏是公开的函数,接收TokenStream作为参数并返回另一个TokenStream。要想写一个过程宏,我们需要先实现能够解析TokenStream的解析器。
Rust 社区已经有了很好的 crate——syn,用于解析TokenStream。syn提供了一个现成的 Rust 语法解析器能够用于解析TokenStream。你可以通过组合syn提供的底层解析器来解析你自己的语法。如上,把[syn](https://crates.io/crates/syn)和[quote](https://crates.io/crates/quote)加到Cargo.toml。
现在我们可以使用proc_macro在lib.rs中写一个属性式宏,proc_macro是编译器提供的用于写过程宏的一个 crate。对于一个过程宏 crate,除了过程``宏外,不能导出其他任何东西,crate 中定义的过程宏不能在 crate 自身中使用。
extern crate proc_macro;use proc_macro::{TokenStream};use quote::{quote};// using proc_macro_attribute to declare an attribute like procedural macro#[proc_macro_attribute]// _metadata is argument provided to macro call and _input is code to which attribute like macro attachespub fn my_custom_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {// returing a simple TokenStream for StructTokenStream::from(quote!{struct H{}})}
一个实例 —— trace_vars!
这个例子实现了对代码中一个或多个变量的追踪。虽然例子有点长,但它十分有趣,当然也可以直接略过。
// 追踪一个变量#[trace_vars(a)]fn do_something(){let a = 9;a = 6;a = 0;}// 追踪多个变量#[trace_vars(a,c,b)]// code// we need to parse a "," seperated list of tokens
trace_vars宏获取它所要追踪的变量名,然后每当输入变量(也就是a)的值发生变化时注入一条打印语句。这样它就可以追踪输入变量的值了。syn提供了一个适用于 Rust 函数语法的内置解析器。ItemFn将会解析函数,并且如果语法无效,它会抛出一个错误。
#[proc_macro_attribute] // 未完成 1pub fn trace_vars(_metadata: TokenStream, input: TokenStream) -> TokenStream {// parsing rust function to easy to use structlet input_fn = parse_macro_input!(input as ItemFn);TokenStream::from(quote!{fn dummy(){}})}
现在我们已经解析了input,让我们开始转移到metadata。对于metadata,没有适用的内置解析器,所以我们必须自己使用syn的parse模块写一个解析器。
要想syn能够工作,我们需要实现syn提供的Parsetrait。Punctuated用于创建一个由','分割Indent的vector。
struct Args{vars:HashSet<Ident>}impl Parse for Args{fn parse(input: ParseStream) -> Result<Self> {// parses a,b,c, or a,b,c where a,b and c are Indentlet vars = Punctuated::<Ident, Token![,]>::parse_terminated(input)?;Ok(Args {vars: vars.into_iter().collect(),})}}
一旦我们实现Parse trait,我们就可以使用parse_macro_input宏来解析metadata。
#[proc_macro_attribute] // 未完成 2pub fn trace_vars(metadata: TokenStream, input: TokenStream) -> TokenStream {let input_fn = parse_macro_input!(input as ItemFn);// 使用新定义的 struct Argslet args= parse_macro_input!(metadata as Args);TokenStream::from(quote!{fn dummy(){}})}
现在,我们准备修改input_fn以便于在当变量值变化时添加println!。为了完成这项修改,我们需要过滤出有复制语句的代码,并在那行代码之后插入一个 print 语句。
impl Args {// 检查表达式是否需要处理fn should_print_expr(&self, e: &Expr) -> bool {match *e {Expr::Path(ref e) => {// 变量不应当以 :: 开头if e.path.leading_colon.is_some() {false// 应当是一个单独的变量类似'x=8'而不是'n::x=0', 所以路径长必须为1} else if e.path.segments.len() != 1 {false} else {// 获取第一部分let first = e.path.segments.first().unwrap();// 检查变量名是否是在Args.vars 的hashset中self.vars.contains(&first.ident) && first.arguments.is_empty()}}// 其他情况一律舍弃_ => false,}}// used for checking if to print let i=0 etc or notfn should_print_pat(&self, p: &Pat) -> bool {match p {// 检查变量名是否在集合中Pat::Ident(ref p) => self.vars.contains(&p.ident),_ => false,}}//处理树,插入打印语句fn assign_and_print(&mut self, left: Expr, op: &dyn ToTokens, right: Expr) -> Expr {// 在右边表达式递归调用let right = fold::fold_expr(self, right);// 返回变化后的子树parse_quote!({#left #op #right;println!(concat!(stringify!(#left), " = {:?}"), #left);})}// 处理 let 语句fn let_and_print(&mut self, local: Local) -> Stmt {let Local { pat, init, .. } = local;let init = self.fold_expr(*init.unwrap().1);// 获得变量名let ident = match pat {Pat::Ident(ref p) => &p.ident,_ => unreachable!(),};// 返回新句法子树parse_quote! {let #pat = {#[allow(unused_mut)]let #pat = #init;// 插入的打印语句println!(concat!(stringify!(#ident), " = {:?}"), #ident);#ident};}}}
在上面的示例中,quote宏用于模板化和生成 Rust 代码。#用于注入变量的值。
现在,我们将会在input_fn上进行 DFS,并插入 println 语句。syn提供了一个Foldtrait 可以用来对任意Item实现 DFS。我们只需要修改与我们想要操作的 token 类型所对应的 trait 方法。
impl Fold for Args {// 对应处理表达式,如:a = 5, a+=1fn fold_expr(&mut self, e: Expr) -> Expr {match e {// 1. for changing assignment like a = 5Expr::Assign(e) => {// check should printif self.should_print_expr(&e.left) {self.assign_and_print(*e.left, &e.eq_token, *e.right)} else {// continue with default travesal using default methodsExpr::Assign(fold::fold_expr_assign(self, e))}}// 2. for changing assigment and operation like a+=1Expr::AssignOp(e) => {// check should printif self.should_print_expr(&e.left) {self.assign_and_print(*e.left, &e.op, *e.right)} else {// continue with default behaviourExpr::AssignOp(fold::fold_expr_assign_op(self, e))}}// 3. continue with default behaviour for rest of expressions_ => fold::fold_expr(self, e),}}// 对应处理语句(statement),如: let d=9fn fold_stmt(&mut self, s: Stmt) -> Stmt {match s {Stmt::Local(s) => {// check should printif s.init.is_some() && self.should_print_pat(&s.pat) {self.let_and_print(s)} else {Stmt::Local(fold::fold_local(self, s))}}_ => fold::fold_stmt(self, s),}}}
Foldtrait 用于对一个Item进行 DFS。它使得你能够针对不同的 token 类型采取不同的行为。
现在我们可以使用fold_item_fn在我们解析的代码中注入 print 语句。
#[proc_macro_attribute] // 全部完成pub fn trace_var(args: TokenStream, input: TokenStream) -> TokenStream {// 解析句法分析 inputlet input = parse_macro_input!(input as ItemFn);// 解析参数 argumentslet mut args = parse_macro_input!(args as Args);// 构造新的句法树 ouputlet output = args.fold_item_fn(input);// 返回新的句法树 outputTokenStream::from(quote!(#output))}
自定义派生宏
Rust 中的自定义派生宏能够对 trait 进行自动实现。这些宏通过使用#[derive(Trait)]自动实现 trait。syn对derive宏有很好的支持
#[derive(Trait)]struct MyStruct{}
要想在 Rust 中写一个自定义派生宏,我们可以使用DeriveInput来解析派生宏的输入。我们还将使用proc_macro_derive宏来定义一个自定义派生宏。
#[proc_macro_derive(Trait)]pub fn derive_trait(input: proc_macro::TokenStream) -> proc_macro::TokenStream {let input = parse_macro_input!(input as DeriveInput);let name = input.ident;let expanded = quote! {impl Trait for #name {fn print(&self) -> usize {println!("{}","hello from #name")}}};proc_macro::TokenStream::from(expanded)}
函数式宏
函数式宏类似于声明式宏,都是通过宏调用操作符!来执行,并且看起来都像是函数调用。他们都作用与圆括号里面的代码。
#[proc_macro]pub fn a_proc_macro(_input: TokenStream) -> TokenStream {TokenStream::from(quote!(fn anwser()->i32{5}))}
函数式宏在编译期而非在运行时执行。它们可以在 Rust 代码的任何地方被使用。函数式宏同样也接收一个TokenStream并返回一个TokenStream。
过程宏的优势
- 使用
span获得更好的错误处理 - 更好的控制输出
- 社区已有
syn和quote两个 crate - 比声明式宏更为强大
关于此文件的一些说明
本章内容
Rust宏的初步学习应当先抓住几个大体思想,比如模式匹配、卫生宏、词法树等。然后我略去了一些琐碎的知识点,比如宏的调试技巧,编译器内置宏的详细介绍等。
宏的内容既有繁琐细节又可以有一定深度,很难一次性介绍清楚,课本的做法是以一个较大的例子带过所有知识点。
声明式宏部分,我没有使用书上的(json宏)的实例,因为这一个例子流程太太太太长了。为了避免变成直接翻译抄书,我将其中的重要知识点抽出、删除部分奇怪的分段(宏中使用trait(这本来就是一件很正常的事,在宏中当然可以正常使用trait))、同时对应加入一些简单的小例子。学习声明式宏时,千万注意与C宏的本质区别。
书上对于过程宏介绍较少,我参考 Macros in Rust: A tutorial with examples (中文翻译),加入适当介绍,并使用其中(trace_var)例子。我觉得这更体现Rust宏的强大之处,如果光介绍声明式宏,不容易深入了解rust宏的词法树工作本质。基于宏的代码更难懂,因为它很少利用 Rust 的内建规则。就像常规函数,一个良好的宏可以在不知道其实现的情况下使用。然而,设计一个良好的宏困难的!另外,在宏中的编译错误更难解释,因为它在展开后的代码上描述问题,不是在开发者使用的代码级别。 这些缺点让宏成了所谓“最后求助于的功能”。这并不是说宏的坏话;只是因为它是 Rust 中需要真正简明,良好抽象的代码的部分。切记权衡取舍。
