Rust macro_rules 入门
本文阐述了Rust语言中有关macro_rules
的基本知识。如果你对宏毫不了解,那么读完本教程你会对Rust的宏有基本的认识,也可以看懂一些宏的编写;但如果你需要自己编写功能丰富的宏,仅仅知道这些内容还不足够。
本教程的所有内容基于Rust 2021;但其实与之前版本差异很小。对于版本之间有差异的内容,本文进行了特别的说明。
参看文献:
基本概念
宏可以看作是一种映射(或函数),只不过它的输入与输出与一般函数不同:输入参数是Rust代码,输出值也是Rust代码(或者称为语法树)。另外,宏调用是在编译时(compile time),而不是在运行时(runtime)执行的;所以调用时发生的任何错误都属于编译错误(compile error),将导致编译失败。
Rust中,macro有两类形式,即macro rules和procedure macro(程序宏)。其中macro rules也被称为macro by example,或者declarative macro(声明式宏);procedure macro也简称为proc macro。
本文涉及macro_rules
和宏调用方式。
Macro By Example
macro_rules
,顾名思义,通过一系列规则(rules)生成不同的代码:
// 定义
macro_rules! macro_name {
规则1 ;
规则2 ;
// ...
规则N ;
}
// 使用
macro_name!(/*...*/);
每个规则是一个“例子”,所以macro_rules
也被称为macro by example。
匹配
编写规则的方式是使用匹配(matching),即从第一条规则开始,对输入macro的token tree(所有tokens)进行匹配,如果匹配成功则结束匹配,否则进行下一条规则的匹配(对于包含元变量的规则,情况有所不同,下文详述)。
基本格式如下:
macro_rules! match_let {
// a rule
( /* matcher */ ) => {
/* expansion */
};
// other rules ...
}
- 每个matcher被
()
,{}
或[]
包含;无论定义时使用其中哪一个,在调用时既可以使用()
,也可以使用[]
或{}
。 - 每条规则都有一个宏展开方式,宏展开的内容使用
{}
包含。 - 规则之间需要使用
;
分隔。(最后一条规则后的;
可以省略)
需要注意,输入的token tree必须和rules完全一致才算匹配成功(token之间可以是任意长度的空白)。
最简单的规则就是逐个字符地匹配(可以在Rust Playground查看代码)):
macro_rules! match_let {
() => {
println!("empty tokens matched")
};
(let v: u32;) => {
println!("matched: `let v: u32;`")
};
}
fn main() {
match_let!();
match_let!(let v: u32;);
match_let!(let v
: u32;); // token之间可以是任意的空白
// compile_error! missing ending token `;`
// match_let!(let v: u32);
// compile_error! no rules match `{`
// match_let!({ let var:u32 };);
}
匹配的内容不必是正确的rust代码,例如:
macro_rules! match_let {
(s0meτext@) => {
println!("matched: `s0meτext@`")
};
}
fn main() {
match_let!(s0meτext@);
}
元变量捕获
要进行复杂的匹配,需要使用捕获(capture)。捕获的内容可以是任何符合Rust语法的代码片段(fragment)。
元变量(metavariables)是捕获内容的基本单元,可以作为变量使用。和Rust变量相同,每个元变量需要给定一个类型。
在The Rust Reference中,元变量的“类型”被称为fragment-specifier
支持的元变量类型如下:
block
:代码块,形如{ //..your code }
。expr
:表达式。ident
:标识符,或rust关键字。其中标识符又包括变量名、类型名等(所以任意单词都可以被视为ident
)item
:一个item
可以是一个函数定义、一个结构体、一个module、一个impl块,......lifetime
:生命周期(例如'a
,'static
,......)literal
:字面量。包括字符串字面量和数值字面量。meta
:包含在“attribute”(#[...]
)中的内容pat
:模式(pattern),至少为任意的[PatternNoTopAlt](根据Rust版本而有所不同)- 在2018和2015Edition中,
pat
完全等价于pat_param
- 2021Edition中
(以及以后的版本),pat
为 任何可以出现在match{ pat => ..., }
中的pat
- 在2018和2015Edition中,
pat_param
: a PatternNoTopAltpath
:路径(例如std::mem::replace
,transmute::<_, int>
,foo
, ...)stmt
:一条语句,但实际上捕获的内容不包含末尾的;
(item语句除外)tt
:单个Token Treety
:某个类型vis
:可见性。例如pub
,pub(in crate)
,......
这些类型并不是互斥的,例如stmt
元变量中可以包含expr
,而expr
元变量中可以包含ident
,ty
,literal
,......等。需要注意的是,由于元变量的捕获基于Rust compiler的语法解析器,所以捕获的内容必须符合rust语法。
其他阅读材料:
stmt
捕获内容不包含末尾的;
:Fragment Specifiers章节,The Little Book of Rust Macrospat
含义变化:Pull Request #1135 - Document the 2021 edition changes to macros-by-examplepat
metavariables- 要想更准确的理解各个元变量的含义,你可以阅读Fragment Specifiers 章节,或Metavariables - The Rust Reference。
在macro_rules
中声明元变量的方式,与一般rust代码声明变量的方式相似,但变量名要以$
开头,即$var_name: Type
。
下面的例子演示了如何进行捕获:
macro_rules! add {
($a:ident, $b:ident) => {
$a + $b
};
($a:ident, $b:ident, $c: ident) => {
$a + $b + $c
};
}
fn main() {
let a = 3u16;
println!("{}", add!(a,a));
println!("{}", add!(a,a,a));
// compile error! (标识符(ident)只能是单词,而不能是字面量(literal))
// println!("{}", add!(1,2,3));
}
元变量可以和Token Tree结合使用 [playground link]:
macro_rules! call {
(@add $a:ident, $b:ident) => {
$a + $b
};
(@mul $a:ident, $b:ident) => {
$a * $b
};
}
fn main() {
let a = 3u16;
println!("{}", call!(@add a,a));
println!("{}", call!(@mul a,a));
// compile error!
// println!("{}", call!(add 1,2));
}
捕获重复单元
如果需要匹配(捕获)一个元变量多次,而不关心匹配到的具体次数,可以使用重复匹配。基本形式是$(...) sep rep
。
其中...
是要重复的内容,它可以是任意符合语法的matcher,包括嵌套的repetition。
sep
是可选的,指代多个重复元素之间的分隔符,例如,
或;
,但不能是?
。(更多可用的分隔符可阅读后缀部分)
最后的rep
是重复次数的约束,有三种情况:
- 至少匹配一次:
$(...)+
- 至多匹配一次:
$(...)?
- 匹配0或多次:
$(...)*
在编写宏展开时,也可以对某一单元进行重复,其重复次数等于其中包含的元变量的重复次数。基本形式也是$(...) sep rep
。其中sep
是可选的。
例如,编写一个将键值对解析为HashMap
的宏 :
use std::collections::HashMap;
macro_rules! kv_map {
() => {
$crate::HashMap::new()
};
[$($k:tt = $v:expr),+] => {
$crate::HashMap::from([
$( ($k,$v) ),+ // repetition
])
};
}
fn main() {
println!("{:?}", kv_map![
"a" = 1,
"b" = 2
]);
}
另外,也可以在一个重复单元中包含多个元变量,但要求这些元变量的重复次数相同。
下面的例子会出现编译错误,注释掉第一条println
语句即可通过编译:
macro_rules! match__ {
($($e:expr),* ; $($e2:expr),* ) => {
($($e, $e2)*)
}
}
fn main() {
// compile error!
println!("{:?}", match__!(1,2,3;1));
// OK
println!("{:?}", match__!(1,2,3;1,2,3));
}
其他学习资源
The Little Book of Rust Macros 的部分中文翻译
英文原版地址:A Beginner’s Guide to Rust Macros ✨ | by Phoomparin Mano
宏调用
宏展开可以作为一个表达式,也可以作为一个item或一条statement,或者作为meta构成属性(attribute)。对于这些不同的用途,在宏调用上有不同的写法。
- 对于作为meta的宏调用,写法是
#[macro_name(arg,arg2,)]
,#[macro_name(arg = val,...)]
,或#[macro_name]
- 如果宏调用作为表达式,写法则是:
macro_name!( /* Token Tree*/ )
或:
macro_name![ /* Token Tree*/ ]
或:
macro_name!{ /* Token Tree*/ }
比如:
if a == macro_name!(...) {
// ...
} else b == macro_name!{...} {
}
- 如果宏调用作为item或statement,写法与上面有所不同:
macro_name!( /* Token Tree*/ );
或:
macro_name![ /* Token Tree*/ ];
或:
macro_name!{ /* Token Tree*/ }
比如:
macro_rules! foo {
() => {
}
}
foo!(); // OK
foo!{} // OK
// foo!() // ERROR
// foo!{}; // ERROR
似乎没什么值得特别一提的,但是看下面的代码(playground link):
macro_rules! a {
() => {
b!()
// Error ^^
}
}
macro_rules! b {
() => {
}
}
a!();
编译该程序,你会得到一个错误:
error: macros that expand to items must be delimited with braces or followed by a semicolon