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 rulesprocedure 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
  • 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是<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