Rust macro_rules 入门

Rust macro_rules 入门

本文阐述了Rust语言中有关macro_rules的基本知识。如果你对宏毫不了解,那么读完本教程你会对Rust的宏有基本的认识,也可以看懂一些宏的编写;但如果你需要自己编写功能丰富的宏,仅仅知道这些内容还不足够。

本教程的所有内容基于Rust 2021;但其实与之前版本差异很小。对于版本之间有差异的内容,本文进行了特别的说明。

参看文献

基本概念

宏可以看作是一种映射(或函数),只不过它的输入与输出与一般函数不同:输入参数是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 {
    (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语法。

其他阅读材料:

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 macro_rules 入门)