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 {(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 中 <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!(); // 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