Rust|Rust macro_rules 入门
Rust macro_rules 入门
本文阐述了Rust语言中有关macro_rules
的基本知识。如果你对宏毫不了解,那么读完本教程你会对Rust的宏有基本的认识,也可以看懂一些宏的编写;但如果你需要自己编写功能丰富的宏,仅仅知道这些内容还不足够。
本教程的所有内容基于Rust 2021;但其实与之前版本差异很小。对于版本之间有差异的内容,本文进行了特别的说明。
参看文献:
- The Rust Reference
- The Little Book of Rust Macros
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)进行匹配,如果匹配成功则结束匹配,否则进行下一条规则的匹配(对于包含元变量的规则,情况有所不同,下文详述)。
【Rust|Rust macro_rules 入门】基本格式如下:
macro_rules! match_let {
// a rule
( /* matcher */ ) => {
/* expansion*/
};
// other rules ...
}
- 每个matcher被
()
,{}
或[]
包含;无论定义时使用其中哪一个,在调用时既可以使用()
,也可以使用[]
或{}
。 - 每条规则都有一个宏展开方式,宏展开的内容使用
{}
包含。 - 规则之间需要使用
;
分隔。(最后一条规则后的;
可以省略)
最简单的规则就是逐个字符地匹配(可以在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));
}
其他学习资源 Rust Macro 手册 - 知乎
The Little Book of Rust Macros 的部分中文翻译Rust宏编程新手指南【Macro】
英文原版地址: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
推荐阅读
- Rust-Sqlx极简教程
- python|python 推理引擎_【Rust日报】 2019-05-12(Snip开源神经网络推理引擎Tract)
- docker|基于Rust-vmm实现Kubernetes运行时
- java|【Rust日报】2022-03-21 Firefox 现在约 10% 的代码为 Rust
- python|【Rust日报】2022-03-22 fluent-uri(一个快速、简单和严格的URI解析器)
- c++|【Rust日报】2022-03-23 RustSBI软件发布v0.2.2版本
- java|【Rust日报】2022-01-28 Rust 编程,2022 年的展望
- java|【Rust日报】2021-12-19 Foundry(以太坊应用程序开发工具包)
- 嵌入式|【Rust 日报】2021-11-21 The RustFest Global - Rust in Arts
- 嵌入式|【Rust日报】2021-11-14 一个开源的基于Rust和Flutter的Notion替代产品