Rust|Rust macro_rules 入门

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)进行匹配,如果匹配成功则结束匹配,否则进行下一条规则的匹配(对于包含元变量的规则,情况有所不同,下文详述)。
【Rust|Rust macro_rules 入门】基本格式如下:
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
  • pat_param: a PatternNoTopAlt
  • path:路径(例如std::mem::replace, transmute::<_, int>foo, ...)
  • stmt:一条语句,但实际上捕获的内容不包含末尾的; (item语句除外)
  • tt:单个Token Tree
  • ty:某个类型
  • vis:可见性。例如 pub, pub(in crate),......
这些类型并不是互斥的,例如stmt元变量中可以包含expr,而expr元变量中可以包含identtyliteral,......等。需要注意的是,由于元变量的捕获基于Rust compiler的语法解析器,所以捕获的内容必须符合rust语法。
其他阅读材料:
  • stmt捕获内容不包含末尾的; :Fragment Specifiers章节,The Little Book of Rust Macros
  • pat含义变化:Pull Request #1135 - Document the 2021 edition changes to macros-by-example pat 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

    推荐阅读