乐趣区

关于rust:Rust-macrorules-入门

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 {(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
  • 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!();    // 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
退出移动版