Rust macro_rules 入门
本文论述了Rust语言中无关macro_rules
的基本知识。如果你对宏毫不理解,那么读完本教程你会对Rust的宏有根本的意识,也能够看懂一些宏的编写;但如果你须要本人编写功能丰富的宏,仅仅晓得这些内容还不足够。
本教程的所有内容基于Rust 2021;但其实与之前版本差别很小。对于版本之间有差别的内容,本文进行了特地的阐明。
参看文献:
- The Rust Reference
- The Little Book of Rust Macros
基本概念
宏能够看作是一种映射(或函数),只不过它的输出与输入与个别函数不同:输出参数是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 { (s0meext@) => { println!("matched: `s0meext@`") };}fn main() { match_let!(s0meext@);}
元变量捕捉
要进行简单的匹配,须要应用捕捉(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中<s>(以及当前的版本)</s>,
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
是<u>可选的</u>,指代多个反复元素之间的分隔符,例如,
或;
,但不能是?
。(更多可用的分隔符可浏览后缀局部)
最初的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!(); // OKfoo!{} // 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