Rust-高级-小宏书(声明宏)

一、语法

将介绍 Rust 的声明宏系统: macro_rules!

1.1 语法拓展

1.1.1 源代码解析

1.1.1.1 标识化:

Rust程序编译过程的第一阶段是标记解析。 在这一过程中,源代码将被转换成一系列的标记 (token)

token:无法被分割的词法单元;在编程语言世界中等价于单词

Rust包含多种标记,比如:

  • 标识符 (identifiers): foo, Bambous, self, we_can_dance, LaCaravane
  • 字面值 (literals): 42, 72u32, 0_______0, 1.0e-40, “ferris was here”
  • 关键字 (keywords): _, fn, self, match, yield, macro
  • 符号 (symbols): [, :, ::, ?, ~, @

有些地方值得注意:

  1. self 既是一个标识符又是一个关键词。 几乎在所有情况下它都被视作是一个关键词,但它有可能被视为标识符。 稍后会提到这种情况。
  2. 关键词里列有一些可疑的家伙,比如 yieldmacro。 它们在当前的Rust语言中并没有任何含义,但编译器的确会把它们视作关键词进行解析。 这些词语被保留作语言未来扩充时使用。
  3. 符号里也列有一些未被当前语言使用的条目。比如 <-,这是历史残留: 目前它被移除了Rust语法,但词法分析器仍然没丢掉它。
  4. 注意 :: 被视作一个独立的标记,而非两个连续的 : 。 这一规则适用于截至 Rust 1.2 版本的所有的多字符符号标记
  1. @ 被用在模式中,用来绑定模式非终止的部分到一个名称——但这似乎被大多数人完全地遗忘了。
  2. 严格来说, Rust 1.52 版本中存在两个词法分析器 (lexer): rustc_lexer 只将单个字符作为 标记 (tokens)rustc_parse 里的 lexer 把多个字符作为不同的 标记 (tokens)

作为对比,某些语言的宏系统正扎根于这一阶段。Rust并非如此。 举例来说,从效果来看,C/C++的宏就是在这里得到处理的。这也正是下列代码能够运行的原因:

#define SUB void
#define BEGIN {
#define END }

SUB main() BEGIN
printf("Oh, the horror!\n");
END

  1. 实际上,C 预处理程序使用与 C 自身所不同的词法结构,但这些区别很大程度上无关紧要。
  2. 是否应该这样运行完全是一个另外的话题了。

1.1.1.2 语法解析 (Parsing):

编译过程的下一个阶段是语法解析 (parsing)
这一过程中,一系列的 token 将被转换成一棵抽象语法树 (AST: Abstract Syntax Tree)。 此过程将在内存中建立起程序的语法结构。
标记序列 1+2 将被转换成某种类似于:

__________      __________
| BinOp | __| LitInt |
| op: Add |__| | val: 1 |
| lhs: ◌ | |________|
| rhs: ◌ |__ _________
|_________| |__| LitInt |
| val: 2 |
|________|

AST 将包含整个程序的结构,但这一结构仅包含词法信息。
在这个阶段编译器虽然可能知道某个表达式提及了某个名为 a 的变量, 但它并没有办法知道 a 究竟是什么,或者它从哪来。
在 AST 生成之后,宏处理过程才开始。 但在讨论宏处理过程之前,我们需要先谈谈标记树 (token tree)

1.1.1.2 标记树 (Token Trees)

标记树 是介于 标记 (token)AST 之间的东西。
首先明确一点,几乎所有标记都构成标记树。 具体来说,它们可被看作标记树叶节点。 还有另一类事物也可被看作标记树叶节点,我们将在稍后提到它。
仅有的一种基础标记不是标记树叶节点——分组标记:(...), [...] 和 {...}。 这三者属于标记树内的节点,正是它们给标记树带来了树状的结构。
给个具体的例子,这列标记:

a + b + (c + d[0]) + e

将被解析为这样的标记树:
«a» «+» «b» «+» «(   )» «+» «e»
╭────────┴──────────╮
«c» «+» «d» «[ ]»
╭─┴─╮
«0»

注意它跟最后生成的 AST 并没有关联。 AST 将仅有一个根节点,而这棵标记树有七个。 作为参考,最后生成的 AST 应该是这样:
               __________
| BinOp |
| op: Add |
__| lhs: ◌ |__
___________ | | rhs: ◌ || ___________
| Var |__| |_________| |__| BinOp |
| name: a | | op: Add |
|_________| __| lhs: ◌ |__
___________ | | rhs: ◌ | | ___________
| Var | | |_________| |__| BinOp |
| name: b |__| | op: Add |
|_________| __| lhs: ◌ |__
___________ | | rhs: ◌ | | ___________
| BinOp | | |_________| |__| Var |
| op: Add |_| | name: e |
___| lhs: ◌ | |_________|
___________ | | rhs: ◌ |_ ___________
| Var | | |_________| |__| Index |
| name: c |__| __| arr: ◌ |__
|_________| ___________ | | ind: ◌ | | ___________
| Var |_| |_________| |__| LitInt |
| name: d | | val: 0 |
|_________| |_________|

理解 AST标记树 (token tree) 之间的区别至关重要。 写宏时,你将同时与这两者打交道。
还有一条需要注意:不可能出现不匹配的小/中/大括号,也不可能存在包含错误嵌套结构的标记树。

1.1.2 AST 中的宏

在 Rust 中,宏处理发生在 AST 生成之后。 因此,调用宏的语法 必须 是Rust语言语法中规整相符的一部分。
Rust 语法包含数种 语法扩展 的形式。具体来说有以下四种(顺便给出例子):

  • #[$arg] 形式的:#[derive(Clone)], #[no_mangle], …
  • #![$arg] 形式的:#![allow(dead_code)], #![crate_name="blang"], …
    头两种形式被称作 属性 (attributes) 。属性用来给条目 (items) 、表达式、语句加上注解。
    属性有三类:内置的属性 (built-in attributes)宏属性 (macro attributes)派生属性(derive attributes)
    内置的属性由编译器实现。宏属性和派生属性在 Rust 第二类宏系统——过程宏 ([procedural macros]https://doc.rust-lang.org/reference/procedural-macros.html)——中实现。
  • $name!$arg 形式的:println!("Hi!"), concat!("a", "b"), …
    我们感兴趣的是第三种:$name!$arg——像函数那样使用的宏。 这种形式的宏可以用 macro_rules! 来声明。
    注意第三种形式的宏是一种一般的语法拓展形式,并非仅用 macro_rules! 写出。 比如 format! 是一个宏,而用来实现 format!format_args! 不是这里谈论的宏(后者由编译器实现)。
  • $name!$arg0 $arg1 形式的:macro_rules! dummy { () => {}; }.
    第四种形式本质上是宏的变种。其实,这种形式的唯一用例只有 macro_rules!,我们将在稍后谈到它。

将注意力集中到第三种形式 $name ! $arg 上,我们的问题变成,对于每种可能的语法扩展, Rust的语法解析器 (parser) 如何知道 $arg 究竟长什么样?
答案是它 不需要 知道。其实,提供给每次语法扩展调用的参数,是一棵 标记树 (token tree)。 具体来说,是一棵非叶节点的标记树: (...)、[...] 或 {...}。 知道这一点后,语法分析器如何理解如下调用形式,就变得显而易见了:

bitflags! {
struct Color: u8 {
const RED = 0b0001,
const GREEN = 0b0010,
const BLUE = 0b0100,
const BRIGHT = 0b1000,
}
}

lazy_static! {
static ref FIB_100: u32 = {
fn fib(a: u32) -> u32 {
match a {
0 => 0,
1 => 1,
a => fib(a-1) + fib(a-2)
}
}

fib(100)
};
}

fn main() {
use Color::*;
let colors = vec![RED, GREEN, BLUE];
println!("Hello, World!");
}

虽然上述调用看起来包含了各式各样的 Rust 代码,但对语法分析器来说,它们仅仅是堆毫无意义的标记树。 为了让事情变得更清晰,我们把所有这些句法“黑盒”用 ⬚ 代替,仅剩下:
bitflags!

lazy_static! ⬚

fn main() {
let colors = vec! ⬚;
println! ⬚;
}

再次重申,语法分析器对 ⬚ 不作任何假设;它记录黑盒所包含的标记,但并不尝试理解它们。
以下几点很重要:

  • Rust包含多种语法扩展。我们将仅仅讨论定义在 macro_rules! 结构中的宏。
  • 当遇见形如 $name!$arg 的结构时,该结构并不一定是宏,可能是其它语法扩展,比如过程宏
  • 所有宏的 输入都是非叶节点的单个标记树
  • 宏(其实所有一般意义上的语法扩展)都将作为抽象语法树的一部分被解析。

最后一点最为重要,它带来了一些深远的影响。 由于宏将被解析进 AST 中,宏只能出现在那些明确支持它们出现的位置。 具体来说,宏能在如下位置出现:

  • 模式 (pattern)
  • 语句 (statement)
  • 表达式 (expression)
  • 条目 (item) (包括 impl 块)
  • 类型

一些并不支持的位置包括:

  • 标识符 (identifier)
  • match 分支
  • 结构体的字段中

在上述第一个列表——支持的位置之外,绝对没有任何地方有使用宏的可能。

1.1.3 宏展开

展开相对简单。在生成 AST 之后 ,对程序进行语义理解之前的某个时间点,编译器将会对所有宏进行展开。包括,遍历 AST,定位所有宏调用,并将它们用其展开进行替换。

在非宏的语法扩展情境中,此过程具体如何发生根据具体情境各有不同。 但所有语法扩展在展开完成之后所经历的历程都与宏所经历的相同。
每当编译器遇见一个语法扩展,都会根据上下文解析成语法元素集。 该语法扩展的展开结果应能被顺利解析为集合中的某个元素。

举例来说,如果在模组作用域内调用了宏, 那么编译器就会尝试将该宏的展开结果解析为一个表示某项条目 (item) 的 AST 节点。 如果在需要表达式的位置调用了宏,那么编译器就会尝试将该宏的展开结果解析为一个表示表达式的 AST 节点。
事实上,语义扩展能够被转换成以下任意一种:

  • 一个表达式
  • 一个模式
  • 一个类型
  • 零或多个条目(包括的 impl 块)
  • 零或多个语句

换句话讲,宏调用所在的位置,决定了该宏展开之后的结果被解读的方式。编译器将把 AST 中表示宏调用的节点用其宏展开的输出节点完全替换。 这一替换是结构性 (structural)的,而非织构性 (textural) 的。
比如思考以下代码:

let eight = 2 * four!();

我们可将这部分 AST 表示为:
_______________
| Let |
| name: eight | ___________
| init: ◌ |___| BinOp |
|_____________| | op: Mul |
__| lhs: ◌ |
__________ | | rhs: ◌ |__ ______________
| LitInt |_| |_________| |_| Macro |
| val: 2 | | name: four |
|________| | body: () |
|____________|

根据上下文,four!() 必须展开成一个表达式 (初始化语句只可能是表达式)。 因此,无论实际展开结果如何,它都将被解读成一个完整的表达式。
_______________          
| Let |
| name: eight | ___________
| init: ◌ |___| BinOp |
|_____________| | op: Mul |
__| lhs: ◌ |
__________ | | rhs: ◌ |__ ___________
| LitInt |_| |_________| |_| BinOp |
| val: 2 | | op: Add |
|________| __| lhs: ◌ |
__________ | | rhs: ◌ |__ __________
| LitInt |_| |_________| |_| LitInt |
| val: 1 | | val: 3 |
|________| |________|


这又能被重写成:
let eight = 2 * (1 + 3);

注意到虽然表达式本身不包含括号,我们仍加上了它们。 这是因为,编译器总是将宏展开结果作为完整的AST节点对待,而不是仅仅作为一列标记。 换句话说,即便不显式地把复杂的表达式用括号包起来, 编译器也不可能错意宏替换的结果,或者改变求值顺序。
理解这一点——宏展开被当作AST节点看待——非常重要,它造成两大影响:

  • 宏不仅调用位置有限制,其展开结果也只能跟语法分析器在该位置所预期的 AST 节点种类一致。
  • 因此,宏必定无法展开成不完整或不合语法的结构。

有关展开还有一条值得注意: 如果某次语法扩展的展开结果包含了另一次语法扩展调用,那会怎么样? 例如,上述 four! 如果被展开成了 1 + three!(),会发生什么?

let x = four!();

展开成:
let x = 1 + three!();

编译器将会检查扩展结果中是否包含更多的宏调用; 如果有,它们将被进一步展开。 因此,上述 AST 节点将被再次展开成:
let x = 1 + 3;

这个例子告诉我们,宏展开发生在传递过程中;要完全展开所有调用,就需要同样多的传递。事实上,编译器为此设置了一个上限它被称作宏递归上限,默认值为 128。 如果第 128 次展开结果仍然包含宏调用,编译器将会终止并返回一个递归上限溢出的错误信息。

此上限可通过 #![recursion_limit="…"] 被改写, 但这种改写必须是 crate 级别的。 一般来讲,可能的话最好还是尽量让宏展开递归次数保持在默认值以下,因为会影响编译时间。

1.2 macro_rules!

有了这些知识,我们终于可以引入 macro_rules! 了。 如前所述,macro_rules! 本身就是一个语法扩展, 也就是说它并不是 Rust 语法的一部分。它的形式如下:

macro_rules! $name {
$rule0 ;
$rule1 ;
//
$ruleN ;
}

至少得有一条规则 (rule) ,最后一条规则后面的分号可被省略。 规则里你可以使用大/中/小括号:{}、[]、()。 每条规则都形如:
($matcher) => {$expansion}

如前所述,分组符号可以是任意一种括号,在模式匹配外侧使用小括号、表达式外侧使用大括号只是出于传统。
注意:选择哪种括号并不会影响宏被调用。 事实上,调用宏时可以使用这三种中任意一种括号,只不过使用 { .. } 或者 ( ... ); 的话会有所不同(关注点在于末尾跟随的分号 ; )。 有末尾分号的宏调用总是会被解析成一个条目 (item)。
macro_rules! 的调用将被展开为 空 (nothing) 。 至少,在 AST 中它被展开为空。 它所影响的是编译器内部的结构,以将该宏注册 (register) 进去。 因此,技术上讲你可以在任何一个空展开合法的位置使用 macro_rules!

1.2.1 模式匹配:

当一个宏被调用时,对应的 macro_rules! 解释器将按照声明顺序一一检查规则。 对每条规则,它都将尝试将输入标记树的内容与该规则的进行匹配。 某个模式必须与输入完全匹配才被认为是一次匹配。(这里所译“模式”的原词叫 matcher)
如果输入与某个模式相匹配,则该调用项将被相应的展开内容所取代; 否则,将尝试匹配下条规则。 如果所有规则均匹配失败,则宏展开会失败并报错。

最简单的例子是空模式:

macro_rules! four {
() => { 1 + 3 };
}

它将且仅将匹配到空的输入,即 four!()、four![] 或 four!{}
注意调用所用的分组标记并 不需要 匹配定义时采用的分组标记。 也就是说,你可以通过 four![] 调用上述宏,此调用仍将被视作匹配。 只有调用时的输入内容才会被纳入匹配考量范围。
模式中也可以包含 字面值 (literal) 标记树,这些标记树必须被完全匹配。 将整个对应标记树在相应位置写下即可。 比如,为匹配标记序列 4 fn ['spang "whammo"] @_@ ,我们可以这样写:
macro_rules! gibberish {
(4 fn ['spang "whammo"] @_@) => {...};
}

使用 gibberish!(4 fn ['spang "whammo"] @_@) 即可成功匹配和调用。

1.2.2 元变量 (Metavariables):

模式 (macther) 还可以包含捕获。 即基于某种通用语法来匹配输入类别,并将结果捕获到元变量中,然后将其替换到输出中。
捕获以美元($)形式写入,其后跟标识符冒号(:),最后是捕获类型, 也称为片段分类符 ([fragment-specifier]https://doc.rust-lang.org/nightly/reference/macros-by-example.html#metavariables), 片段分类符必须是以下类型之一:

  • block 块:比如用大括号包围起来的语句和/或表达式
  • expr 表达式 (expression)
  • ident 标识符 (identifier):包括关键字 (keywords)
  • item 条目:比如函数、结构体、模块、impl 块
  • lifetime 生命周期注解:比如 ‘foo、’static
  • literal 字面值:比如 “Hello World!”、3.14、’🦀’
  • meta 元信息:指 #[…] 和 #![…] 属性内部的元信息条目
  • pat 模式 (pattern)
  • path 路径:比如 foo、::std::mem::replace、transmute::<_, int>
  • stmt 语句 (statement)
  • tt:单棵标记树 (single token tree)
  • ty:类型
  • vis:可视标识符:可能为空的可视标识符,比如 pub、pub(in crate)

比如以下 macro_rules! 宏捕获一个表达式输入,并绑定给元变量 $e

macro_rules! one_expression {
($e:expr) => {...};
}

元变量对 Rust 编译器的解析器产生影响,使得元变量总是正确无误expr 表达式元变量总是捕获完整且符合 Rust 编译版本的表达式。
你可以在有限的条件内同时结合字面值标记树和元变量。当元变量被明确匹配到的时候,只需要写 $name 就能引用元变量的值。比如:
macro_rules! times_five {
($e:expr) => { 5 * $e };
}

元变量被替换成完整的 AST 节点,这很像宏展开。这也意味着被 $e 捕获的任何标记序列都会被解析成单个完整的表达式。你也可以一个模式匹配中使用多个元变量:
macro_rules! multiply_add {
($a:expr, $b:expr, $c:expr) => { $a * ($b + $c) };
}

然后在展开的地方(指 => 到 ; 之间)使用任意多的元变量:
macro_rules! discard {
($e:expr) => {};
}
macro_rules! repeat {
($e:expr) => { $e; $e; $e; };
}

有一个特殊的宏变量叫做 $crate ,它用来指代当前 crate

1.2.2重复捕获 (Repetitions):

模式匹配可以重复。这使得匹配一连串标记 (token) 成为可能。反复捕获的一般形式为 $ ( ... ) sep rep

  • $ 是字面上的美元符号标记
  • ( … ) 是被反复匹配的模式,由小括号包围。
  • sep 是可选的分隔标记。它不能是括号或者重复操作符。常用例子包括 , 和 ; 。
  • rep 是必须的重复操作符。当前可以是:
    • ?:表示 最多一次重复,所以不能用于分割标记。
    • *:表示 零次或多次重复。
    • +:表示 一次或多次重复。

重复中可以包含任意有效模式,包括字面标记树、元变量以及任意嵌套的重复。
在展开的地方(指 =>; 之间),重复也采用相同的语法。

举例来说,下面这个宏将每一个元素都转换成字符串。 它将匹配零或多个由逗号分隔的表达式,并分别将它们拓展成构造 Vec 的表达式。

macro_rules! vec_strs {
(
// Start a repetition:
$(
// Each repeat must contain an expression...
$element:expr
)
// ...separated by commas...
,
// ...zero or more times.
*
) => {
// Enclose the expansion in a block so that we can use
// multiple statements.
{
let mut v = Vec::new();

// Start a repetition:
$(
// Each repeat will contain the following statement, with
// $element replaced with the corresponding expression.
v.push(format!("{}", $element));
)*

v
}
};
}

fn main() {
let s = vec_strs![1, "a", true, 3.14159f32];
assert_eq!(s, &["1", "a", "true", "3.14159"]);
}

你可以在一个重复语句里面使用多次和多个元变量,只要这些元变量以相同的次数重复。 下面的宏和调用代码正常运行:
macro_rules! repeat_two {
($($i:ident)*, $($i2:ident)*) => {
$( let $i: (); let $i2: (); )*
}
}

repeat_two!( a b c d e f, u v w x y z );

但是这下面的不能运行:
macro_rules! repeat_two {
($($i:ident)*, $($i2:ident)*) => {
$( let $i: (); let $i2: (); )*
}
}

repeat_two!( a b c d e f, x y z );

运行报以下错误:
error: meta-variable `i` repeats 6 times, but `i2` repeats 3 times
--> src/main.rs:6:10
|
6 | $( let $i: (); let $i2: (); )*
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

1.3 细节

介绍 macro_rules! 宏系统的一些细枝末节。

1.3.1 片段分类符

Rust 已有 13 个片段分类符 (Fragment Specifiers,以下简称分类符) 。 这一节会更深入地探讨他们之中的细节,每次都会展示几个匹配的例子。

注意:除了 ident、lifetime 和 tt 分类符之外, 其余的分类符在匹配后生成的 AST 是不清楚的 (opaque), 这使得在之后的宏调用时不可能检查 (inspect) 捕获的结果。

  • item
  • block
  • stmt
  • pat
  • expr
  • ty
  • ident
  • path
  • tt
  • meta
  • lifetime
  • vis
  • literal

1.3.1.1 item:

item 分类符只匹配 Rust 的 item 的定义 (definitions) , 不会匹配指向 item 的标识符 (identifiers)。例子:

macro_rules! items {
($($item:item)*) => ();
}

items! {
struct Foo;
enum Bar {
Baz
}
impl Foo {}
/*...*/
}

[item]https://doc.rust-lang.org/reference/items.html 是在编译时完全确定的,通常在程序执行期间保持固定,并且可以驻留在只读存储器中。具体指:

  • mod 模块
  • extern crate 声明
  • use 声明
  • 函数定义
  • 类型定义
  • 结构体定义
  • 枚举定义
  • 联合体定义
  • 常量项
  • 静态项
  • trait 定义
  • impl 实现
  • extern blocks 外部块

1.3.1.2 block:

block 分类符只匹配 block 表达式 。
块 (block) 由 { 开始,接着是一些语句,最后是可选的表达式,然后以 } 结束。 块的类型要么是最后的值表达式类型,要么是 () 类型。

macro_rules! blocks {
($($block:block)*) => ();
}

blocks! {
{}
{
let zig;
}
{ 2 }
}

1.3.1.2 stmt:

stmt 分类符只匹配 语句 (statement)。 除非 item 语句要求结尾有分号,否则不会匹配语句最后的分号

什么叫 item 语句要求结尾有分号呢?单元结构体 (Unit-Struct) 就是一个简单的例子, 因为它定义中必须带上结尾的分号。

下面的宏只给出它所捕获的内容,因为有几行不能通过编译。

macro_rules! statements {
($($stmt:stmt)*) => ($($stmt)*);
}

fn main() {
statements! {
struct Foo;
fn foo() {}
let zig = 3
let zig = 3;
3
3;
if true {} else {}
{}
}
}

可以根据报错内容试着删除不能编译的代码,结合 stmt 小节开头的文字再琢磨琢磨。
虽然源代码编译失败,但是我们可以展开宏,使用 [playground]https://play.rust-lang.org/Expand macros 工具 (tool);或者把代码复制到本地,在 nightly Rust 版本中使用 rustup install nightly & rustup default nightly(还原rustup default stable) & cargo install cargo-expand & cargo expand 命令得到宏展开结果:
fn main() {
struct Foo;
fn foo() { }
let zig = 3;
let zig = 3;
;
3;
3;
;
if true { } else { }
{ }
}

综上:

  1. 虽然 stmt 分类符没有捕获语句末尾的分号,但它依然在所需的时候返回了 (emit) 语句。 原因很简单,分号本身就是有效的语句。 所以我们实际输入 10 个语句调用了宏,而不是 8 个!
  2. 在这里你应该注意到:struct Foo; 被匹配到了。 否则我们会看到像其他情况一样有一个额外 ; 语句。 由前所述,这能想通:item 语句需要分号,所以这个分号能被匹配到(例外)。
  3. 仅由块表达式或控制流表达式组成的表达式结尾没有分号, 其余的表达式捕获后产生的表达式会尾随一个分号(在这个例子中,正是这里出错)。

1.3.1.3 pat:

pat 分类符用于匹配任何形式的模式 (pattern)

macro_rules! patterns {
($($pat:pat)*) => ();
}

patterns! {
"literal"
_
0..5
ref mut PatternsAreNice
}

1.3.1.4 expr:

expr 分类符用于匹配任何形式的表达式 (expression)

macro_rules! expressions {
($($expr:expr)*) => ();
}

expressions! {
"literal"
funcall()
future.await
break 'foo bar
}

1.3.1.5 ty:

ty 分类符用于匹配任何形式的类型表达式 (type expression)
类型表达式是在 Rust 中指代类型的语法。

macro_rules! types {
($($type:ty)*) => ();
}

types! {
foo::bar
bool
[u8]
}

1.3.1.6 ident:

ident 分类符用于匹配任何形式的标识符或者关键字。 (identifier)

macro_rules! idents {
($($ident:ident)*) => ();
}

idents! {
// _ /* `_` 不是标识符,而是一种模式 */
foo
async
O_________O
_____O_____
}

1.3.1.7 path:

path 分类符用于匹配类型中的路径 (TypePath)

macro_rules! paths {
($($path:path)*) => ();
}

paths! {
ASimplePath
::A::B::C::D
G::<eneri>::C
}

1.3.1.8 tt:

tt 分类符用于匹配标记树 (TokenTree)。 如果你是新手,对标记树不了解,那么需要回顾 标记树 一节。tt 分类符是最有作用的分类符之一,因为它能匹配几乎所有东西, 而且能够让你在使用宏之后检查 (inspect) 匹配的内容。

1.3.1.9 meta:

meta 分类符用于匹配属性 (attribute), 准确地说是属性里面的内容。通常你会在 #[$meta:meta]#![$meta:meta] 模式匹配中看到这个分类符。

macro_rules! metas {
($($meta:meta)*) => ();
}

metas! {
ASimplePath
super::man
path = "home"
foo(bar)
}

针对文档注释简单说一句: 文档注释其实是具有 #[doc=”…”] 形式的属性,… 实际上就是注释字符串, 这意味着你可以在在宏里面操作文档注释!

1.3.1.10 lifetime:

lifetime 分类符用于匹配生命周期注解或者标签 (lifetime or label)。 它与 ident 很像,但是 lifetime 会匹配到前缀 ‘

macro_rules! lifetimes {
($($lifetime:lifetime)*) => ();
}

lifetimes! {
'static
'shiv
'_
}

1.3.1.11 vis:

vis 分类符会匹配 可能为空的内容(Visibility qualifier)。 重点在于可能为空

macro_rules! visibilities {
// 注意这个逗号,`vis` 分类符自身不会匹配到逗号
($($vis:vis,)*) => ();
}

visibilities! {
,
pub,
pub(crate),
pub(in super),
pub(in some_path),
}

vis 实际上只支持例子里的几种方式,因为这里的 visibility 指的是可见性,与私有性相对。 而涉及这方面的内容只有与 pub 的关键字。所以,vis 在关心匹配输入的内容是公有还是私有时有用。

1.3.1.12 literal:

literal 分类符用于匹配字面表达式 (literal expression)

macro_rules! literals {
($($literal:literal)*) => ();
}

literals! {
-1
"hello world"
2.3
b'b'
true
}

1.3.2 元变量与宏展开

1.3.2.1 书写宏规则的顺序

一旦语法分析器开始消耗标记以匹配某捕获,整个过程便 无法停止或回溯 。 这意味着,无论输入是什么样的,下面这个宏的第二项规则将永远无法被匹配到:

macro_rules! dead_rule {
($e:expr) => { ... };
($i:ident +) => { ... };
}

fn main() {
dead_rule!(x+);
}

考虑当以 dead_rule!(x+) 形式调用此宏时,将会发生什么。 解析器将从第一条规则开始试图进行匹配:它试图将输入解析为一个表达式。 第一个标记 x 作为表达式是有效的,第二个标记——作为二元加符号 + 的节点——在表达式中也是有效的。

由此你可能会以为,由于输入中并不包含二元加号 + 的右侧元素, 分析器将会放弃尝试这一规则,转而尝试下一条规则。 实则不然:分析器将会 panic 并终止整个编译过程,最终返回一个语法错误。

由于分析器的这一特点,下面这点尤为重要: 一般而言,在书写宏规则时,应从最具体的开始写起,依次写直到最不具体的 。

1.3.2.2 片段分类符的跟随限制

为防止将来的语法变动影响宏输入的解析方式macro_rules! 对紧接元变量后的内容施加了限制。 在 Rust 1.52 中,能够紧跟片段分类符后面的内容具有如下限制:

  • stmt 和 expr:=>、,、; 之一
  • pat:=>、,、=、|、if、in 之一
  • path 和 ty:=>、,、=、|、;、:、>、>>、[、{、as、where 之一; 或者 block*型的元变量
  • vis:,、除了 priv 之外的标识符、任何以类型开头的标记、 ident 或 ty 或 ath 型的元变量
  • 其他片段分类符所跟的内容无限制

反复匹配的情况也遵循这些限制,也就是说:

  1. 如果一个重复操作符(* 或 +)能让一类元变量重复数次, 那么反复出现的内容就是这类元变量,反复结束之后所接的内容遵照上面的限制。
  2. 如果一个重复操作符(* 或 ?)让一类元变量重复零次, 那么元变量之后的内容遵照上面的限制。

1.3.2.3 编译器拒绝模糊的规则

解析器不会预先运行代码,这意味着如果编译器不能一次唯一地确定如何解析宏调用, 那么编译器就带着模糊的报错信息而终止运行。 一个触发终止运行的例子是:

macro_rules! ambiguity {
($($i:ident)* $i2:ident) => { };
}

// error:
// local ambiguity: multiple parsing options: built-in NTs ident ('i') or ident ('i2').
fn main() { ambiguity!(an_identifier); }

编译器不会提前看到传入的标识符之后是不是一个,如果提前看到的话就会解析正确。

1.3.2.4 不基于标记的代换

关于代换元变量 (substitution,这里指把已经进行宏解析的 token 再次传给宏) , 常常让人惊讶的一面是,尽管很像是根据标记 (token) 进行代换的,但事实并非如此——代换基于已经解析的 AST 节点。

macro_rules! capture_then_match_tokens {
($e:expr) => {match_tokens!($e)};
}

macro_rules! match_tokens {
($a:tt + $b:tt) => {"got an addition"};
(($i:ident)) => {"got an identifier"};
($($other:tt)*) => {"got something else"};
}

fn main() {
println!("{}\n{}\n{}\n",
match_tokens!((caravan)),
match_tokens!(3 + 6),
match_tokens!(5));
println!("{}\n{}\n{}",
capture_then_match_tokens!((caravan)),
capture_then_match_tokens!(3 + 6),
capture_then_match_tokens!(5));
}

其结果:
got an identifier
got an addition
got something else

got something else
got something else
got something else

通过解析已经传入 AST 节点的输入,代换的结果变得很稳定:你再也无法检查其内容了, 也不再匹配内容。

另一个例子可能也会很令人困惑:

macro_rules! capture_then_what_is {
(#[$m:meta]) => {what_is!(#[$m])};
}

macro_rules! what_is {
(#[no_mangle]) => {"no_mangle attribute"};
(#[inline]) => {"inline attribute"};
($($tts:tt)*) => {concat!("something else (", stringify!($($tts)*), ")")};
}

fn main() {
println!(
"{}\n{}\n{}\n{}",
what_is!(#[no_mangle]),
what_is!(#[inline]),
capture_then_what_is!(#[no_mangle]),
capture_then_what_is!(#[inline]),
);
}

结果是:
no_mangle attribute
inline attribute
something else (#[no_mangle])
something else (#[inline])

避免这个意外情况的唯一方式就是使用 tt、ident 或者 lifetime 分类符。 每当你捕获到除此之外的分类符,结果将只能被用于直接输出。

1.3.3 宏是部分卫生的

卫生性 (hygiene) 描述的是 标识符 (ident) 在宏处理和展开过程中是“唯一的”、“没有歧义的”、 “不被同名标识符污染的”。

1.3.3.1 宏是部分卫生的

Rust 里的 macro_rules!部分卫生的。 具体来说,对于绝大多数标识符,它是卫生的; 但对泛型参数和生命周期注解来说,它不是卫生的。

之所以能做到“卫生”,在于每个标识符都被赋予了一个看不见的“句法上下文”。 在比较两个标识符时,只有在标识符的原文名称和句法上下文都完全一样的情况下, 两个标识符才能被视作等同。

为阐释这一点,考虑下述代码:
github
我们将采用背景色来表示句法上下文。现在,将上述宏调用展开如下:
github

首先,回想一下,在展开的期间调用 macro_rules! 宏,实际不会真正出现展开的结果。

其次,如果我们现在就尝试编译上述代码,编译器将报如下错误:

error[E0425]: cannot find value `a` in this scope
--> src/main.rs:13:21
|
13 | let four = using_a!(a / 10);
| ^ not found in this scope

注意到宏在展开后背景色(即其句法上下文)发生了改变。 每处宏展开均赋予其内容一个新的、独一无二的上下文。 故而,在展开后的代码中实际上存在两个不同的 a,它们分别有不同的句法上下文。 即,第一个 a 与第二个 a 并不相同,即使它们便看起来很像。

尽管如此,被替换进宏展开中的标记仍然保持着它们原有的句法上下文。 因为它们是被传给这宏的,并非这宏本身的一部分。 因此,我们作出如下修改:
github
展开如下:
github

因为只用了一种 a,编译器将欣然接受此段代码。

$crate 元变量
宏需要其定义所在的 (defining) crate 的其他 items 时,由于“卫生性”,我们需要使用 $crate 元变量。

这个特殊的元变量所做的事情是,它展开成宏所定义的 (defining) crate 的绝对路径。

//// 在 `helper_macro` crate 里定义 `helped!` 和 `helper!` 宏
#[macro_export]
macro_rules! helped {
// () => { helper!() } // This might lead to an error due to 'helper' not being in scope.
() => { $crate::helper!() }
}

#[macro_export]
macro_rules! helper {
() => { () }
}

//// 在另外的 crate 中使用这两个宏
// 注意:`helper_macro::helper` 并没有导入进来
use helper_macro::helped;

fn unit() {
// 这个宏能运行通过,因为 `$crate` 正确地展开成 `helper_macro` crate 的路径(而不是使用者的路径)
helped!();
}

请注意,$crate 用在指明非宏的 items 时,它必须和完整且有效的模块路径一起使用。如下:
pub mod inner {
#[macro_export]
macro_rules! call_foo {
() => { $crate::inner::foo() };
}

pub fn foo() {}
}

1.3.4 非标识符的“标识符”

有两个标记,当你撞见时,很有可能最终认为它们是标识符 (ident),但实际上它们不是。 然而正是这些标记,在某些情况下又的确是标识符。
第一个是 self。 毫无疑问,它是一个 关键词 (keyword)。 在一般的 Rust 代码中,不可能出现把它解读成标识符的情况; 但在宏中这种情况则有可能发生:

macro_rules! what_is {
(self) => {"the keyword `self`"};
($i:ident) => {concat!("the identifier `", stringify!($i), "`")};
}

macro_rules! call_with_ident {
($c:ident($i:ident)) => {$c!($i)};
}

fn main() {
println!("{}", what_is!(self));
println!("{}", call_with_ident!(what_is(self)));
}

上述代码的输出将是:
the keyword `self`
the keyword `self`

但这没有任何道理! call_with_ident! 要求一个标识符,而且它的确匹配到了,还成功替换了! 所以,self 同时是一个关键词,但又不是。 你可能会想,好吧,但这鬼东西哪里重要呢?看看这个:
macro_rules! make_mutable {
($i:ident) => {let mut $i = $i;};
}

struct Dummy(i32);

impl Dummy {
fn double(self) -> Dummy {
make_mutable!(self);
self.0 *= 2;
self
}
}

fn main() {
println!("{:?}", Dummy(4).double().0);
}

编译它会失败,并报错:
error: `mut` must be followed by a named binding
--> src/main.rs:2:24
|
2 | ($i:ident) => {let mut $i = $i;};
| ^^^^^^ help: remove the `mut` prefix: `self`
...
9 | make_mutable!(self);
| -------------------- in this macro invocation
|
= note: `mut` may be followed by `variable` and `variable @ pattern`

所以说,宏在匹配的时候,会欣然把self当作标识符接受, 进而允许你把 self 带到那些实际上没办法使用的情况中去。 但是,也成吧,既然得同时记住 self 既是关键词又是标识符, 那下面这个讲道理应该可行,对吧?
macro_rules! make_self_mutable {
($i:ident) => {let mut $i = self;};
}

struct Dummy(i32);

impl Dummy {
fn double(self) -> Dummy {
make_self_mutable!(mut_self);
mut_self.0 *= 2;
mut_self
}
}

fn main() {
println!("{:?}", Dummy(4).double().0);
}

实际上也不行,编译错误变成:
error[E0424]: expected value, found module `self`
--> src/main.rs:2:33
|
2 | ($i:ident) => {let mut $i = self;};
| ^^^^ `self` value is a keyword only available in methods with a `self` parameter
...
8 | / fn double(self) -> Dummy {
9 | | make_self_mutable!(mut_self);
| | ----------------------------- in this macro invocation
10 | | mut_self.0 *= 2;
11 | | mut_self
12 | | }
| |_____- this function has a `self` parameter, but a macro invocation can only access identifiers it receives from parameters
|

这同样也没有任何道理。 这简直就像是在抱怨说,它看见的两个 self 不是同一个 self … 就搞得像关键词 self 就像标识符一样,也有卫生性。
macro_rules! double_method {
($body:expr) => {
fn double(mut self) -> Dummy {
$body
}
};
}

struct Dummy(i32);

impl Dummy {
double_method! {{
self.0 *= 2;
self
}}
}

fn main() {
println!("{:?}", Dummy(4).double().0);
}

还是报同样的错。那这个如何:
macro_rules! double_method {
($self_:ident, $body:expr) => {
fn double(mut $self_) -> Dummy {
$body
}
};
}

struct Dummy(i32);

impl Dummy {
double_method! {self, {
self.0 *= 2;
self
}}
}

fn main() {
println!("{:?}", Dummy(4).double().0);
}

终于管用了。 所以说,self 是关键词,但如果想它变成标识符,那么同时也能是一个标识符。 那么,相同的道理对类似的其它东西有用吗?
macro_rules! double_method {
($self_:ident, $body:expr) => {
fn double($self_) -> Dummy {
$body
}
};
}

struct Dummy(i32);

impl Dummy {
double_method! {_, 0}
}

fn main() {
println!("{:?}", Dummy(4).double().0);
}

当然不行。 _ 在模式以及表达式中是一个有效关键词,而不是一个标识符; 即便它 如同 self 一样从定义上讲符合标识符的特性。
error: no rules expected the token `_`
--> src/main.rs:12:21
|
1 | macro_rules! double_method {
| -------------------------- when calling this macro
...
12 | double_method! {_, 0}
| ^ no rules expected this token in macro call

可能觉得,既然 _ 在模式中有效,那换成 $self_:pat 是不是就能一石二鸟了呢? 可惜了,也不行,因为 self 不是一个有效的模式。

如果你真想同时匹配这两个标记,仅有的办法是换用 tt 来匹配。

1.3.5 调试

调试需要用到 Rust 的 nightly 版本。

trace_macros!
rustc 提供了一些调试 macro_rules! 宏的工具。其中最有用的就是 trace_macros!

trace_macros! 传入 true,从而给编译器发出指令,让这开始的 macro_rules! 宏在展开前被调用。

trace_macros! 传入 false,关闭追踪功能。

请看这个例子:

// 注意:必须使用 nightly 版本的 Rust 编译这段代码
#![feature(trace_macros)]

macro_rules! each_tt {
() => {};
($_tt:tt $($rest:tt)*) => {each_tt!($($rest)*);};
}

each_tt!(foo bar baz quux);
trace_macros!(true);
each_tt!(spim wak plee whum);
trace_macros!(false);
each_tt!(trom qlip winp xod);

fn main() {}

在第二条宏的前后开启和关闭调试,从而得到以下打印结果:
note: trace_macro
--> src/main.rs:11:1
|
11 | each_tt!(spim wak plee whum);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: expanding `each_tt! { spim wak plee whum }`
= note: to `each_tt ! (wak plee whum) ;`
= note: expanding `each_tt! { wak plee whum }`
= note: to `each_tt ! (plee whum) ;`
= note: expanding `each_tt! { plee whum }`
= note: to `each_tt ! (whum) ;`
= note: expanding `each_tt! { whum }`
= note: to `each_tt ! () ;`
= note: expanding `each_tt! { }`
= note: to ``

它在调试递归很深的宏时尤其有用。

此外,你可以在命令行里,给编译指令附加 -Z trace-macros 来打印追踪的宏。

trace_macros!(false); 之后的宏不会被这个附加指令追踪到,所以这里会追踪前两个宏。

参考命令:cargo rustc --bin binary_name -- -Z trace-macros

log_syntax!
另一有用的宏是log_syntax!。 它将使得编译器输出所有经过编译器处理的标记 (tokens)。

举个例子,下述代码可以让编译器唱一首歌:

// 注意:必须使用 nightly 版本的 Rust 编译这段代码
#![feature(log_syntax)]

macro_rules! sing {
() => {};
($tt:tt $($rest:tt)*) => {log_syntax!($tt); sing!($($rest)*);};
}

sing! {
^ < @ < . @ *
'\x08' '{' '"' _ # ' '
- @ '$' && / _ %
! ( '\t' @ | = >
; '\x08' '\'' + '$' ? '\x7f'
, # '"' ~ | ) '\x07'
}

fn main() {}

比起 trace_macros! 来说,它能够做一些更有针对性的调试。 打印结果:
^
<
@
<
.
@
*
'\x08'
'{'
'"'
_
#
' '
-
@
'$'
&&
/
_
%
!
('\t' @ | = > ; '\x08' '\'' + '$' ? '\x7f', # '"' ~ |)
'\x07'

—pretty=expanded
有时问题出在宏展开后的结果里。 对于这种情况,可给 rustc 编译命令添加 —pretty 参数来勘察。 下面给出代码:

// 给 `String` 初始化函数起一个简写
macro_rules! S {
($e:expr) => {String::from($e)};
}

fn main() {
let world = S!("World");
println!("Hello, {}!", world);
}

使用下面一种方式编译这段名为 hello.rs 的脚本文件:
# 对单个 rs 文件
rustc -Z unstable-options --pretty expanded hello.rs
# 对项目里的二进制 rs 文件
cargo rustc --bin hello -- -Z unstable-options --pretty=expanded

将输出如下内容(略经排版):
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2018::*;
#[macro_use]
extern crate std;
macro_rules! S {
($e:expr) => {
String::from($e)
};
}
fn main() {
let world = String::from("World");
{
::std::io::_print(
::core::fmt::Arguments::new_v1(
&["Hello, ", "!\n"],
&match (&world,) {
(arg0,) => {
[::core::fmt::ArgumentV1::new(arg0, ::core::fmt::Display::fmt)]
}
})
);
};
}

—pretty 还有其它一些可用选项,可通过 rustc -Z unstable-options —help -v 来列出。 此处并不提供该选项表;因为,正如指令本身所暗示的,列表中的一切内容在任何时间点都有可能发生改变。

其他调试工具

  1. cargo-expand:帮助我们将项目下的宏全部展开。
  2. 你还可以使用 playground :点击页面右上方的 TOOLS 按钮,再点击宏展开按钮 (expand macros)。
1.3.6 作用域

函数式宏的作用域规则可能有一点反直觉。(函数式宏包括声明宏与函数式过程宏。) 由于历史原因,宏的作用域并不完全像各种程序项那样工作。

有两种形式的作用域:文本作用域 (textual scope) 和 基于路径的作用域 (path-based scope)。

文本作用域:基于宏在源文件中(定义和使用所)出现的顺序,或是跨多个源文件出现的顺序, 文本作用域是默认的作用域。

基于路径的作用域:与其他程序项作用域的运行方式相同。

当声明宏被 非限定标识符(unqualified identifier,非多段路径段组成的限定性路径)调用时, 会首先在文本作用域中查找。 如果文本作用域中没有任何结果,则继续在基于路径的作用域中查找。

如果宏的名称由路径限定 (qualified with a path) ,则只在基于路径的作用域中查找。

文本作用域

1. 宏在子模块中可见
与 Rust 语言其余所有部分都不同的是,函数式宏在子模块中仍然可见。

macro_rules! X { () => {}; }
mod a {
X!(); // defined
}
mod b {
X!(); // defined
}
mod c {
X!(); // defined
}
fn main() {}

2. 宏在定义之后可见
同样与 Rust 语言其余所有部分不同,宏只有在其定义 之后 可见。 下例展示了这一点。同时注意到,它也展示了宏不会“漏出” (leak) 其定义所在的作用域:

mod a {
// X!(); // undefined
}
mod b {
// X!(); // undefined
macro_rules! X { () => {}; }
X!(); // defined
}
mod c {
// X!(); // undefined
}
fn main() {}

要清楚,即使你把宏移动到外层作用域,词法依赖顺序的规则依然适用。
mod a {
// X!(); // undefined
}

macro_rules! X { () => {}; }

mod b {
X!(); // defined
}
mod c {
X!(); // defined
}
fn main() {}

3. 宏与宏之间顺序无关
然而对于宏自身来说,这种具有顺序的依赖行为不存在。 即被调用的宏可以先于调用宏之前声明:

mod a {
// X!(); // undefined
}

macro_rules! X { () => { Y!(); }; } // 注意这里的代码运行通过

mod b {
// 注意这里 X 虽然被定义,但是 Y 不被定义,所以不能使用 X
// X!(); // defined, but Y! is undefined
}

macro_rules! Y { () => {}; }

mod c {
X!(); // defined, and so is Y!
}
fn main() {}

4. 宏可以被暂时覆盖
允许多次定义 macro_rules! 宏,最后声明的宏会简单地覆盖 (shadow) 上一个声明的同名宏; 如果最后声明的宏离开作用域,上一个宏在有效的作用域内还能被使用。

macro_rules! X { (1) => {}; }
X!(1);
macro_rules! X { (2) => {}; }
// X!(1); // Error: no rule matches `1`
X!(2);

mod a {
macro_rules! X { (3) => {}; }
// X!(2); // Error: no rule matches `2`
X!(3);
}
// X!(3); // Error: no rule matches `3`
X!(2);

fn main() { }

5. #[macro_use] 属性
这个属性放置在宏定义所在的模块前 或者 extern crate 语句前。

  1. 在模块前加上 #[macro_use] 属性:导出该模块内的所有宏, 从而让导出的宏在所定义的模块结束之后依然可用。

    mod a {
    // X!(); // undefined
    }

    #[macro_use]
    mod b {
    macro_rules! X { () => {}; }
    X!(); // defined
    }

    mod c {
    X!(); // defined
    }
    fn main() {}

    注意,这可能会产生一些奇怪的后果,因为宏(包括过程宏)中的标识符只有在宏展开的过程中才会被解析。

    mod a {
    // X!(); // undefined
    }

    #[macro_use]
    mod b {
    macro_rules! X { () => { Y!(); }; }
    // X!(); // defined, but Y! is undefined
    }

    macro_rules! Y { () => {}; }

    mod c {
    X!(); // defined, and so is Y!
    }
    fn main() {}
  2. 给 extern crate 语句加上 #[macro_use] 属性: 把外部 crate 定义且导出的宏引入当前 crate 的根/顶层模块。(当前 crate 使用外部 crate)

假设在外部名称为 mac 的 crate 中定义了 X! 宏,在当前模块:

//// 这里的 `X!` 与 `Y!` 无关,前者定义于外部 crate,后者定义于当前 crate

mod a {
// X!(); // defined, but Y! is undefined
}

macro_rules! Y { () => {}; }

mod b {
X!(); // defined, and so is Y!
}

#[macro_use] extern crate macs;
mod c {
X!(); // defined, and so is Y!
}

fn main() {}

6. 当宏放在函数内
前四条作用域规则同样适用于函数。 至于第五条规则, #[macro_use] 属性并不直接作用于函数。

macro_rules! X {
() => { Y!() };
}

fn a() {
macro_rules! Y { () => {"Hi!"} }
assert_eq!(X!(), "Hi!");
{
assert_eq!(X!(), "Hi!");
macro_rules! Y { () => {"Bye!"} }
assert_eq!(X!(), "Bye!");
}
assert_eq!(X!(), "Hi!");
}

fn b() {
macro_rules! Y { () => {"One more"} }
assert_eq!(X!(), "One more");
}

fn main() {
a();
b();
}

7. 关于宏声明的位置
由于前述种种规则,一般来说, 建议将所有应对整个 crate 均可见的宏的定义置于根模块的最顶部, 借以确保它们 一直 可用。 这个建议和适用于在文件 mod 定义的宏:

#[macro_use]
mod some_mod_that_defines_macros;
mod some_mod_that_uses_those_macros;

这里的顺序很重要,因为第二个模块依赖于第一个模块的宏, 所以改变这两个模块的顺序会无法编译。

基于路径的作用域
Rust 的 macro_rules! 宏默认并没有基于路径的作用域。

然而,如果这个宏被加上 #[macro_export] 属性,那么它就在 crate 的根作用域里被定义, 而且能直接使用它。

1.3.7 导入/导出宏

在 Rust 的 2015 和 2018 版本中,导入 macro_rules! 宏是不一样的。 仍然建议阅读这两部分,因为 2018 版使用的结构在 2015 版中做出了解释。

2015版本
1. #[macro_use]
作用域中介绍的 #[macro_use] 属性 适用于模块或者 external crates 。例如:

#[macro_use]
mod macros {
macro_rules! X { () => { Y!(); } }
macro_rules! Y { () => {} }
}

X!();

fn main() {}

2. #[macro_export]
可通过 #[macro_export] 将宏从当前crate导出。注意,这种方式 无视 所有可见性设定。

定义 lib 包 macs 如下:

mod macros {
#[macro_export] macro_rules! X { () => { Y!(); } }
#[macro_export] macro_rules! Y { () => {} }
}

// X! 和 Y! 并非在此处定义的,但它们 **的确** 被导出了(在此处可用)
// 即便 `macros` 模块是私有的

下面(在使用 macs lib 的 crate 中)的代码会正常工作:
X!(); // X 在当前 crate 中被定义
#[macro_use] extern crate macs; // 从 `macs` 中导入 X
X!(); // 这里的 X 是最新声明的 X,即 `macs` crate 中导入的 X

fn main() {}

#[macro_use] 作用于 extern crate 时, 会强制把导出的宏提到 crate 的顶层模块(根模块),所以这里无须使用 macs::macros 路径。

注意:只有在根模组中,才可将 #[macro_use] 用于 extern crate。

在从 extern crate 导入宏时,可显式控制导入 哪些 宏。 从而利用这一特性来限制命名空间污染,或是覆盖某些特定的宏。就像这样:

// 只导入 `X!` 这一个宏
#[macro_use(X)] extern crate macs;

// X!(); // X! 已被定义,但 Y! 未被定义。X 与 Y 无关系。

macro_rules! Y { () => {} }

X!(); // X 和 Y 都被定义

fn main() {}

当导出宏时,常常出现的情况是,宏定义需要其引用所在 crate 内的非宏符号。 由于 crate 可能被重命名等,我们可以使用一个特殊的替换变量 $crate 。 它总将被扩展为宏定义所在的 crate 的绝对路径(比如 :: macs )。

如果你的编译器版本小于 1.30(即 2018 版之前),那么这招并不适用于宏。 也就是说,你没办法采用类似 $crate::Y! 的代码来引用自己 crate 里的定义的宏。 这表示结合 #[macro_use] 来选择性导入会无法保证某个名称的宏在另一个 crate 导入同名宏时依然可用。

推荐的做法是,在引用非宏名称时,总是采用绝对路径。 这样可以最大程度上避免冲突,包括跟标准库中名称的冲突。

2018版本
2018 版本让使用 macro_rules! 宏更简单。 因为新版本设法让 Rust 中某些特殊的东西更像正常的 items 。 这意味着我们能以命名空间的方式正确导入和使用宏!

因此,不必使用 #[macro_use] 来导入 来自 extern crate 导出的宏到全局命名空间, 现在我们这样做就好了:

use some_crate::some_macro;

fn main() {
some_macro!("hello");
// as well as
some_crate::some_other_macro!("macro world");

}

可惜,这只适用于导入外部 crate 的宏; 如果你使用在自己 crate 定义的 macro_rules! 宏, 那么依然需要把 #[macro_use] 添加到宏所定义的模块上来引入模块里面的宏。 因而 作用域规则 就像之前谈论的那样生效。

$crate 前缀(元变量)在 2018 版中可适用于任何东西, 在 1.31 版之后,宏 和类似 item 的东西都能用 $crate 导入了

二、实战

[地址]https://www.bookstack.cn/read/DaseinPhaos-tlborm-chinese/pim-README.md

三、模式

3.1 回调

macro_rules! call_with_larch {
($callback:ident) => { $callback!(larch) };
}

macro_rules! expand_to_larch {
() => { larch };
}

macro_rules! recognize_tree {
(larch) => { println!("#1, the Larch.") };
(redwood) => { println!("#2, the Mighty Redwood.") };
(fir) => { println!("#3, the Fir.") };
(chestnut) => { println!("#4, the Horse Chestnut.") };
(pine) => { println!("#5, the Scots Pine.") };
($($other:tt)*) => { println!("I don't know; some kind of birch maybe?") };
}

fn main() {
recognize_tree!(expand_to_larch!()); // 无法直接使用 `expand_to_larch!` 的展开结果
call_with_larch!(recognize_tree); // 回调就是给另一个宏传入宏的名称 (`ident`),而不是宏的结果
}

// 打印结果:
// I don't know; some kind of birch maybe?
// #1, the Larch.

由于宏展开的机制限制,(至少在最新的 Rust 中) 不可能做到把一例宏的展开结果作为有效信息提供给另一例宏。 这为宏的模块化工作施加了难度。

使用递归并传递回调 (callbacks) 是条出路。 作为演示,上例两处宏调用的展开过程如下:

recognize_tree! { expand_to_larch ! (  )  }
println! { "I don't know; some kind of birch maybe?" }
// ...

call_with_larch! { recognize_tree }
recognize_tree! { larch }
println! { "#1, the Larch." }
// ...

可以反复匹配 tt 来将任意参数转发给回调:
macro_rules! callback {
($callback:ident( $($args:tt)* )) => {
$callback!( $($args)* )
};
}

fn main() {
callback!(callback(println("Yes, this *was* unnecessary.")));
}

如果需要的话,当然还可以在参数中增加额外的标记 (tokens) 。

3.2 tt “撕咬机”

macro_rules! mixed_rules {
() => {};
(trace $name:ident; $($tail:tt)*) => {
{
println!(concat!(stringify!($name), " = {:?}"), $name);
mixed_rules!($($tail)*);
}
};
(trace $name:ident = $init:expr; $($tail:tt)*) => {
{
let $name = $init;
println!(concat!(stringify!($name), " = {:?}"), $name);
mixed_rules!($($tail)*);
}
};
}

fn main() {
let a = 42;
let b = "Ho-dee-oh-di-oh-di-oh!";
let c = (false, 2, 'c');
mixed_rules!(
trace a;
trace b;
trace c;
trace b = "They took her where they put the crazies.";
trace b;
);
}

此模式可能是 最强大 的宏解析技巧。 通过使用它,一些极其复杂的语法都能得到解析。

“标记树撕咬机” (TT muncher) 是一种递归宏, 其工作机制有赖于对输入的顺次、逐步处理 (incrementally processing) 。 处理过程的每一步中,它都将匹配并移除(“撕咬”掉)输入头部 (start) 的一列标记 (tokens), 得到一些中间结果,然后再递归地处理输入剩下的尾部。

名称中含有“标记树”,是因为输入中尚未被处理的部分总是被捕获在 $($tail:tt)* 的形式中。 之所以如此,是因为只有通过使用反复匹配 tt 才能做到 无损地 (losslessly) 捕获住提供给宏的输入部分。

标记树撕咬机仅有的限制,也是整个宏系统的局限:

  • 你只能匹配 macro_rules! 捕获到的字面值和语法结构。
  • 你无法匹配不成对的标记组 (unbalanced group) 。

然而,需要把宏递归的局限性纳入考量。 macro_rules! 没有做任何形式的尾递归消除或优化。 在写标记树撕咬机时,建议多花些功夫,尽可能地限制递归调用的次数。

以下两种做法帮助你做到限制宏递归:

  1. 对于输入的变化,增加额外的匹配规则(而不是采用中间层并使用递归);
  2. 对输入句法施加限制,以便于记录追踪标准式的反复匹配。

3.3 内用规则

#[macro_export]
macro_rules! foo {
(@as_expr $e:expr) => {$e};

($($tts:tt)*) => {
foo!(@as_expr $($tts)*)
};
}

fn main() {
assert_eq!(foo!(42), 42);
}

内用规则可用在以下两种情况:

  1. 将多个宏统一为一个;
  2. 通过显式命名宏中调用的规则,来简化 TT “撕咬机” 的读写。

那么为什么将多个宏统一为一个有用呢? 主要原因是:在 2015 版本中,未对宏进行空间命名。这导致一个问题——必须重新导出内部定义的所有宏, 从而污染整个全局宏命名空间;更糟糕的是,宏与其他 crate 的同名宏发生冲突。 简而言之,这很造成很多麻烦。 幸运的是,在 rustc版本 >= 1.30 的情况下(即 2018 版本之后), 这不再是问题了(但是内用规则可以减少不必要声明的宏)。

好了,让我们讨论如何利用“内用规则” (internal rules) 来把多个宏统一为一个, 以及“内用规则”这项技术到底是什么吧。

这个例子有两个宏,一个常见的 as_expr! 宏 和 foo! 宏,后者使用了前者。如果分开写就是下面的形式:

#[macro_export]
macro_rules! as_expr { ($e:expr) => {$e} }

#[macro_export]
macro_rules! foo {
($($tts:tt)*) => {
as_expr!($($tts)*)
};
}

fn main() {
assert_eq!(foo!(42), 42);
}

这当然不是最好的解决办法,正如前面提到的,因为 as_expr 污染了全局宏命名空间。 在这个特定的例子里,as_expr 只是一个简单的宏,它只会被使用一次, 所以,利用内用规则,把它“嵌入”到 foo 这个宏里面吧!

在 foo 仅有的一条规则前面添加一条新匹配模式(新规则), 这个匹配模式由 as_expr 组成(和命名),然后附加上宏的输入参数 $e:expr ; 在展开里填写这个宏被匹配到时具体的内容。从而得到本章开头的代码:

#[macro_export]
macro_rules! foo {
(@as_expr $e:expr) => {$e};

($($tts:tt)*) => {
foo!(@as_expr $($tts)*)
};
}

fn main() {
assert_eq!(foo!(42), 42);
}

可以看到,没有调用 as_expr 宏,而是递归调用在参数前放置了特殊标记树的 foo!(@as_expr $($tts)*)。 要是你看得仔细些,你甚至会发现这个模式能好地结合 TT 撕咬机

之所以用 @ ,是因为在 Rust 1.2 下,该标记尚无任何在前缀位置的用法; 因此,这个语法定义在当时不会与任何东西撞车。 如果你想用别的符号或特有前缀都可以(比如试试 #! ), 但 @ 的用例已被传播开来,因此,使用它可能更容易帮助读者理解你的代码。

注意:@ 符号很早之前曾作为前缀被用于表示被垃圾回收了的指针, 那时 Rust 还在采用各种记号代表指针类型。
而现在的 @ 只有一种用法: 将名称绑定至模式中(譬如 match 的模式匹配中)。 在这种用法中它是中缀运算符,与我们的上述用例并不冲突。

还有一点要注意,内用规则通常应排在“真正的”规则之前。 这样做可避免 macro_rules! 错把内用规则调用解析成别的东西,比如表达式。

3.4 下推积累

macro_rules! init_array {
(@accum (0, $_e:expr) -> ($($body:tt)*))
=> {init_array!(@as_expr [$($body)*])};
(@accum (1, $e:expr) -> ($($body:tt)*))
=> {init_array!(@accum (0, $e) -> ($($body)* $e,))};
(@accum (2, $e:expr) -> ($($body:tt)*))
=> {init_array!(@accum (1, $e) -> ($($body)* $e,))};
(@accum (3, $e:expr) -> ($($body:tt)*))
=> {init_array!(@accum (2, $e) -> ($($body)* $e,))};
(@as_expr $e:expr) => {$e};
[$e:expr; $n:tt] => {
{
let e = $e;
init_array!(@accum ($n, e.clone()) -> ())
}
};
}

let strings: [String; 3] = init_array![String::from("hi!"); 3];

在 Rust 中,所有宏最终 必须 展开为一个完整、有效的句法元素(比如表达式、条目等等)。 这意味着,不可能定义一个最终展开为残缺构造的宏。

有些人可能希望,上例中的宏能被更加直截了当地表述成:

macro_rules! init_array {
(@accum 0, $_e:expr) => {/* empty */};
(@accum 1, $e:expr) => {$e};
(@accum 2, $e:expr) => {$e, init_array!(@accum 1, $e)};
(@accum 3, $e:expr) => {$e, init_array!(@accum 2, $e)};
[$e:expr; $n:tt] => {
{
let e = $e;
[init_array!(@accum $n, e)]
}
};
}

他们预期的展开过程如下:
[init_array!(@accum 3, e)]
[e, init_array!(@accum 2, e)]
[e, e, init_array!(@accum 1, e)]
[e, e, e]

然而,这一思路中,每个中间步骤的展开结果都是一个不完整的表达式。 即便这些中间结果对外部来说绝不可见,Rust 仍然禁止这种用法。

下推累积 (push-down accumulation) 则使我们得以在完全完成之前毋需考虑构造的完整性, 进而累积构建出我们所需的标记序列。 本章开头给出的示例中,宏调用的展开过程如下:

init_array! { String:: from ( "hi!" ) ; 3 }
init_array! { @ accum ( 3 , e . clone ( ) ) -> ( ) }
init_array! { @ accum ( 2 , e.clone() ) -> ( e.clone() , ) }
init_array! { @ accum ( 1 , e.clone() ) -> ( e.clone() , e.clone() , ) }
init_array! { @ accum ( 0 , e.clone() ) -> ( e.clone() , e.clone() , e.clone() , ) }
init_array! { @ as_expr [ e.clone() , e.clone() , e.clone() , ] }

可以修改一下代码,看到每次调用时 $($body)* 存储的内容变化:
macro_rules! init_array {
(@accum (0, $_e:expr) -> ($($body:tt)*))
=> {init_array!(@as_expr [$($body)*])};
(@accum (1, $e:expr) -> ($($body:tt)*))
=> {init_array!(@accum (0, $e) -> ($($body)* $e+3,))};
(@accum (2, $e:expr) -> ($($body:tt)*))
=> {init_array!(@accum (1, $e) -> ($($body)* $e+2,))};
(@accum (3, $e:expr) -> ($($body:tt)*))
=> {init_array!(@accum (2, $e) -> ($($body)* $e+1,))};
(@as_expr $e:expr) => {$e};
[$e:expr; $n:tt $(; first $init:expr)?] => {
{
let e = $e;
init_array!(@accum ($n, e.clone()) -> ($($init)?,))
}
};
}

fn main() {
let array: [usize; 4] = init_array![0; 3; first 0];
println!("{:?}", array);
}

在 nightly Rust 中使用编译命令: cargo rustc --bin my-project -- -Z trace-macros ,即得到以下输出:
note: trace_macro
--> src/main.rs:20:31
|
20 | let array: [usize; 4] = init_array![0; 3; first 0];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: expanding `init_array! { 0 ; 3 ; first 0 }`
= note: to `{ let e = 0 ; init_array! (@ accum(3, e.clone()) -> (0,)) }`
= note: expanding `init_array! { @ accum(3, e.clone()) -> (0,) }`
= note: to `init_array! (@ accum(2, e.clone()) -> (0, e.clone() + 1,))`
= note: expanding `init_array! { @ accum(2, e.clone()) -> (0, e.clone() + 1,) }`
= note: to `init_array! (@ accum(1, e.clone()) -> (0, e.clone() + 1, e.clone() + 2,))`
= note: expanding `init_array! { @ accum(1, e.clone()) -> (0, e.clone() + 1, e.clone() + 2,) }`
= note: to `init_array!
(@ accum(0, e.clone()) -> (0, e.clone() + 1, e.clone() + 2, e.clone() + 3,))`
= note: expanding `init_array! { @ accum(0, e.clone()) -> (0, e.clone() + 1, e.clone() + 2, e.clone() + 3,) }`
= note: to `init_array! (@ as_expr [0, e.clone() + 1, e.clone() + 2, e.clone() + 3,])`
= note: expanding `init_array! { @ as_expr [0, e.clone() + 1, e.clone() + 2, e.clone() + 3,] }`
= note: to `[0, e.clone() + 1, e.clone() + 2, e.clone() + 3]`

可以看到,每一步都在累积输出,直到规则完成,给出完整的表达式。

上述过程的关键点在于,使用 $($body:tt)* 来保存输出中间值, 而不触发其它解析机制。采用 ($input) -> ($output) 的形式仅是出于传统,用以明示此类宏的作用。

由于可以存储任意复杂的中间结果, 下推累积在构建 TT 撕咬机 的过程中经常被用到。 当构造类似于这个例子的宏时,也会结合 内用规则

3.5 反复替换

macro_rules! replace_expr {
($_t:tt $sub:expr) => {$sub};
}

在上面代码的模式中,匹配到的重复序列将被直接丢弃, 仅留用它所带来的长度信息(以及元素的类型信息); 且原本标记所在的位置将被替换成某种重复元素。

举个例子,考虑如何为一个元素多于12个 (Rust 1.2 下的元组元素个数的最大值) 的 tuple 提供默认值。

macro_rules! tuple_default {
($($tup_tys:ty),*) => {
(
$(
replace_expr!(
($tup_tys)
Default::default()
),
)*
)
};
}

macro_rules! replace_expr {
($_t:tt $sub:expr) => {
$sub
};
}

fn main() {
assert_eq!(tuple_default!(i32, bool, String),
(i32::default(), bool::default(), String::default()));
}

仅对此例: 我们其实可以直接用 $tup_tys::default()

上例中,我们 并未真正使用 匹配到的类型。 实际上,我们把它丢弃了,并用一个表达式重复替代 (repetition replacement) 。 换句话说,我们实际关心的不是有哪些类型,而是有多少个类型。

3.6 tt 捆绑

macro_rules! call_a_or_b_on_tail {
((a: $a:ident, b: $b:ident), call a: $($tail:tt)*) => {
$a(stringify!($($tail)*))
};

((a: $a:ident, b: $b:ident), call b: $($tail:tt)*) => {
$b(stringify!($($tail)*))
};

($ab:tt, $_skip:tt $($tail:tt)*) => {
call_a_or_b_on_tail!($ab, $($tail)*)
};
}

fn compute_len(s: &str) -> Option<usize> {
Some(s.len())
}

fn show_tail(s: &str) -> Option<usize> {
println!("tail: {:?}", s);
None
}

fn main() {
assert_eq!(
call_a_or_b_on_tail!(
(a: compute_len, b: show_tail),
the recursive part that skips over all these
tokens doesn't much care whether we will call a
or call b: only the terminal rules care.
),
None
);
assert_eq!(
call_a_or_b_on_tail!(
(a: compute_len, b: show_tail),
and now, to justify the existence of two paths
we will also call a: its input should somehow
be self-referential, so let's make it return
some eighty-six!
),
Some(92)
);
}

在十分复杂的递归宏中,可能需要非常多的参数, 才足以在每层调用之间传递必要的标识符与表达式。 然而,根据实现上的差异,可能存在许多这样的中间层, 它们转发了 (forward) 这些参数,但并没有用到。

因此,将所有这些参数捆绑 (bundle) 在一起,通过分组将其放进单独一棵标记树 tt 里, 可以省事许多。这样一来,那些用不到这些参数的递归层可以直接捕获并替换这棵标记树, 而不需要把整组参数完完全全准准确确地捕获替换掉。

上面的例子把表达式 $a$b 捆绑起来, 然后作为一棵 tt 交由递归规则处理。 随后,终结规则 (terminal rules) 将这组标记解构 (destructure) , 并访问其中的表达式。

四、构件

4.1 AST 强制转换

在替换 tt 时,Rust 的解析器并不十分可靠。 当它期望得到某类特定的语法结构时, 如果摆在它面前的是一坨替换后的 tt 标记,就有可能出现问题。 解析器常常直接选择放弃解析,而非尝试去解析它们。 在这类情况中,就要用到 AST 强制转换(简称“强转”)

#![allow(dead_code)]

macro_rules! as_expr { ($e:expr) => {$e} }
macro_rules! as_item { ($i:item) => {$i} }
macro_rules! as_pat { ($p:pat) => {$p} }
macro_rules! as_stmt { ($s:stmt) => {$s} }
macro_rules! as_ty { ($t:ty) => {$t} }

fn main() {
as_item!{struct Dummy;}

as_stmt!(let as_pat!(_): as_ty!(_) = as_expr!(42));
}

这些强制变换经常与 下推累积 宏一同使用, 以使解析器能够将最终输出的 tt 序列当作某类特定的语法结构来对待。

注意:之所以只有这几种强转宏, 是由宏 可以展开成什么 所决定的, 而不是由宏能够捕捉哪些东西所决定的。

4.2 计数

4.2.1 反复替换

在宏中计数是一项让人吃惊的难搞的活儿。 最简单的方式是采用反复替换 (repetition with replacement) 。

macro_rules! replace_expr {
($_t:tt $sub:expr) => {$sub};
}

macro_rules! count_tts {
($($tts:tt)*) => {0usize $(+ replace_expr!($tts 1usize))*};
}

fn main() {
assert_eq!(count_tts!(0 1 2), 3);
}

对于小数目来说,这方法不错,但当输入量到达 500 左右的标记时, 很可能让编译器崩溃。想想吧,输出的结果将类似:
0usize + 1usize + /* ~500 `+ 1usize`s */ + 1usize

编译器必须把这一大串解析成一棵 AST , 那可会是一棵完美失衡的 500 多级深的二叉树。

4.2.2 递归

递归 (recursion) 是个老套路。

macro_rules! count_tts {
() => {0usize};
($_head:tt $($tail:tt)*) => {1usize + count_tts!($($tail)*)};
}

fn main() {
assert_eq!(count_tts!(0 1 2), 3);
}

注意:对于 rustc 1.2 来说,很不幸, 编译器在处理大数量的类型未知的整型字面值时将会出现性能问题。 我们此处显式采用 usize 类型就是为了避免这种不幸。
如果这样做并不合适(比如说,当类型必须可替换时), 可通过 as 来减轻问题。(比如, 0 as $ty、1 as $ty 等)。

这方法管用,但很快就会超出宏递归的次数限制( 目前 是 128 )。

与重复替换不同的是,可通过增加匹配分支来增加可处理的输入面值。

以下为增加匹配分支的改进代码,如果把前三个分支注释掉,看看编译器会提示啥 :)

macro_rules! count_tts {
($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt
$_f:tt $_g:tt $_h:tt $_i:tt $_j:tt
$_k:tt $_l:tt $_m:tt $_n:tt $_o:tt
$_p:tt $_q:tt $_r:tt $_s:tt $_t:tt
$($tail:tt)*)
=> {20usize + count_tts!($($tail)*)};
($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt
$_f:tt $_g:tt $_h:tt $_i:tt $_j:tt
$($tail:tt)*)
=> {10usize + count_tts!($($tail)*)};
($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt
$($tail:tt)*)
=> {5usize + count_tts!($($tail)*)};
($_a:tt
$($tail:tt)*)
=> {1usize + count_tts!($($tail)*)};
() => {0usize};
}

fn main() {
assert_eq!(700, count_tts!(
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
));
}

可以复制下面的例子运行看看,里面包含递归和反复匹配(代码已隐藏)两种方法。
macro_rules! count_tts {
($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt
$_f:tt $_g:tt $_h:tt $_i:tt $_j:tt
$_k:tt $_l:tt $_m:tt $_n:tt $_o:tt
$_p:tt $_q:tt $_r:tt $_s:tt $_t:tt
$($tail:tt)*)
=> {20usize + count_tts!($($tail)*)};
($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt
$_f:tt $_g:tt $_h:tt $_i:tt $_j:tt
$($tail:tt)*)
=> {10usize + count_tts!($($tail)*)};
($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt
$($tail:tt)*)
=> {5usize + count_tts!($($tail)*)};
($_a:tt
$($tail:tt)*)
=> {1usize + count_tts!($($tail)*)};
() => {0usize};
}

// 可试试“反复替代”的方式计数
// --snippet--
// macro_rules! replace_expr {
// ($_t:tt $sub:expr) => {
// $sub
// };
// }
//
// macro_rules! count_tts {
// ($($tts:tt)*) => {0usize $(+ replace_expr!($tts 1usize))*};
// }

fn main() {
assert_eq!(2500,
count_tts!(
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

// --snippet--
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,

// 默认的递归限制让改进的递归代码也无法继续下去了
// 反复替换的代码还能够运行,但明显效率不会很高
// ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
// ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
));
}

4.2.3 切片长度

第三种方法,是帮助编译器构建一个深度较小的 AST ,以避免栈溢出。 可以通过构造数组,并调用其 len 方法来做到。(slice length)

macro_rules! replace_expr {
($_t:tt $sub:expr) => {$sub};
}

macro_rules! count_tts {
($($tts:tt)*) => {<[()]>::len(&[$(replace_expr!($tts ())),*])};
}

fn main() {
assert_eq!(count_tts!(0 1 2), 3);

const N: usize = count_tts!(0 1 2);
let array = [0; N];
println!("{:?}", array);
}

经过测试,这种方法可处理高达 10000 个标记数,可能还能多上不少。

而且可以用于常量表达式,比如当作在 const 值或定长数组的长度值。

所以基本上此方法是 首选 。

4.2.4 枚举计数

当你需要统计 互不相同的标识符 的数量时, 可以利用枚举体的 numeric cast 功能来达到统计成员(即标识符)个数。

macro_rules! count_idents {
($($idents:ident),* $(,)*) => {
{
#[allow(dead_code, non_camel_case_types)]
enum Idents { $($idents,)* __CountIdentsLast }
const COUNT: u32 = Idents::__CountIdentsLast as u32;
COUNT
}
};
}

fn main() {
const COUNT: u32 = count_idents!(A, B, C);
assert_eq!(COUNT, 3);
}

此方法有两大缺陷:

  1. 它仅能被用于数有效的标识符(同时还不能是关键词),而且不允许那些标识符有重复
  2. 不具备卫生性:如果你的末位标识符(在 __CountIdentsLast 位置上的标识符)的字面值也是输入之一, 那么宏调用就会失败,因为 enum 中包含重复变量。
4.2.5 bit twiddling

另一个递归方法,但是使用了 位操作 (bit operations):

macro_rules! count_tts {
() => { 0 };
($odd:tt $($a:tt $b:tt)*) => { (count_tts!($($a)*) << 1) | 1 };
($($a:tt $even:tt)*) => { count_tts!($($a)*) << 1 };
}

fn main() {
assert_eq!(count_tts!(0 1 2), 3);
}

这种方法非常聪明。 只要它是偶数个,就能有效地将其输入减半, 然后将计数器乘以 2(或者在这种情况下,向左移1位)。 因为由于前一次左移位,此时最低位必须为 0 ,重复直到我们达到基本规则 () => 0 。 如果输入是奇数个,则从第二个输入开始减半,最终将结果进行 或运算(这等效于加 1)。

这样做的好处是,生成计数器的 AST 表达式将以 O(log(n)) 而不是 O(n) 复杂度增长。 请注意,这仍然可能达到递归限制。

让我们手动分析中间的过程:

count_tts!(0 0 0 0 0 0 0 0 0 0);

由于我们的标记树数量为偶数(10),因此该调用将与第三条规则匹配。 该匹配分支把奇数项的标记树命名给 $a ,偶数项的标记树命名成 $b , 但是只会对奇数项 $a 展开,这意味着有效地抛弃所有偶数项,切断了一半的输入。 因此,调用现在变为:
count_tts!(0 0 0 0 0) << 1;

现在,该调用将匹配第二条规则,因为其输入的令牌树数量为奇数。 在这种情况下,第一个标记树将被丢弃以再次让输入变成偶数个, 然后可以在调用中再次进行减半步骤。 此时,我们可以将奇数时丢弃的一项计数为1,然后再乘以2,因为我们也减半了。
((count_tts!(0 0) << 1) | 1) << 1;

((count_tts!(0) << 1 << 1) | 1) << 1;

(((count_tts!() | 1) << 1 << 1) | 1) << 1;

((((0 << 1) | 1) << 1 << 1) | 1) << 1;

现在,要检查是否正确分析了扩展过程, 我们可以使用 debugging 调试工具。 展开宏后,我们应该得到:
((((0 << 1) | 1) << 1 << 1) | 1) << 1;

没有任何差错,太棒了!

4.3 解析

在有些情况下解析某些 Rust items 会很有用。 这一章会展示一些能够解析 Rust 中更复杂的 items 的宏。

这些宏目的不是解析整个 items 语法,而是解析通用、有用的部分, 解析的方式也不会太复杂。 也就是说,我们不会涉及解析 泛型 之类的东西。

重点在于宏的匹配方式 (matchers) ;展开的部分 ( Reference 里使用的术语叫做 transcribers ), 仅仅用作例子,不需要特别关心它。

4.3.1 函数
macro_rules! function_item_matcher {
(

$( #[$meta:meta] )*
// ^~~~attributes~~~~^
$vis:vis fn $name:ident ( $( $arg_name:ident : $arg_ty:ty ),* $(,)? )
// ^~~~~~~~~~~~~~~~argument list!~~~~~~~~~~~~~~^
$( -> $ret_ty:ty )?
// ^~~~return type~~~^
{ $($tt:tt)* }
// ^~~~~body~~~~^
) => {
$( #[$meta] )*
$vis fn $name ( $( $arg_name : $arg_ty ),* ) $( -> $ret_ty )? { $($tt)* }
}
}

function_item_matcher!(
#[inline]
#[cold]
pub fn foo(bar: i32, baz: i32, ) -> String {
format!("{} {}", bar, baz)
}
);

fn main() {
assert_eq!(foo(13, 37), "13 37");
}

这是一个简单的匹配函数的例子, 传入宏的函数不能包含 unsafe、async、泛型和 where 语句。 如果需要解析这些内容,则最好使用 proc-macro (过程宏) 代替。

这个例子可以检查函数签名,从中生成一些额外的东西, 然后再重新返回 (re-emit) 整个函数。 有点像 Derive 过程宏,虽然功能没那么强大,但是是为函数服务的 ( Derive 不作用于函数)。

理想情况下,我们对参数捕获宁愿使用 pat 分类符,而不是 ident 分类符, 但这里目前不被允许(因为前者的跟随限制,不允许其后使用 : )。 幸好在函数签名里面不常使用模式 ( pat ) ,所以这个例子还不错。

4.3.2 方法
4.3.3 结构体
macro_rules! struct_item_matcher {
// Unit-Struct
(
$( #[$meta:meta] )*
// ^~~~attributes~~~~^
$vis:vis struct $name:ident;
) => {
$( #[$meta] )*
$vis struct $name;
};

// Tuple-Struct
(
$( #[$meta:meta] )*
// ^~~~attributes~~~~^
$vis:vis struct $name:ident (
$(
$( #[$field_meta:meta] )*
// ^~~~field attributes~~~~^
$field_vis:vis $field_ty:ty
// ^~~~~~a single field~~~~~~^
),*
$(,)? );
) => {
$( #[$meta] )*
$vis struct $name (
$(
$( #[$field_meta] )*
$field_vis $field_ty
),*
);
};

// Named-Struct
(
$( #[$meta:meta] )*
// ^~~~attributes~~~~^
$vis:vis struct $name:ident {
$(
$( #[$field_meta:meta] )*
// ^~~~field attributes~~~!^
$field_vis:vis $field_name:ident : $field_ty:ty
// ^~~~~~~~~~~~~~~~~a single field~~~~~~~~~~~~~~~^
),*
$(,)? }
) => {
$( #[$meta] )*
$vis struct $name {
$(
$( #[$field_meta] )*
$field_vis $field_name : $field_ty
),*
}
}
}

struct_item_matcher!(
#[allow(dead_code)]
#[derive(Copy, Clone)]
pub(crate) struct Foo {
pub bar: i32,
baz: &'static str,
qux: f32
}
);
struct_item_matcher!(
#[derive(Copy, Clone)]
pub(crate) struct Bar;
);
struct_item_matcher!(
#[derive(Clone)]
pub(crate) struct Baz (i32, pub f32, String);
);
fn main() {
let _: Foo = Foo { bar: 42, baz: "macros can be nice", qux: 3.14, };
let _: Bar = Bar;
let _: Baz = Baz(2, 0.1234, String::new());
}
4.3.4 枚举体

解析枚举体比解析结构体更复杂一点,所以会用上 模式 这章讨论的技巧: TT 撕咬机 和 内用规则 。

不是重新构造被解析的枚举体,而是只访问枚举体所有的标记 (tokens), 因为重构枚举体将需要我们再通过 下推累积 临时组合所有已解析的标记 (tokens) 。

macro_rules! enum_item_matcher {
// tuple variant
(@variant $variant:ident (
$(
$( #[$field_meta:meta] )*
// ^~~~field attributes~~~~^
$field_vis:vis $field_ty:ty
// ^~~~~~a single field~~~~~~^
),* $(,)?
//∨~~rest of input~~∨
) $(, $($tt:tt)* )? ) => {

// process rest of the enum
$( enum_item_matcher!(@variant $( $tt )*); )?
};

// named variant
(@variant $variant:ident {
$(
$( #[$field_meta:meta] )*
// ^~~~field attributes~~~!^
$field_vis:vis $field_name:ident : $field_ty:ty
// ^~~~~~~~~~~~~~~~~a single field~~~~~~~~~~~~~~~^
),* $(,)?
//∨~~rest of input~~∨
} $(, $($tt:tt)* )? ) => {
// process rest of the enum
$( enum_item_matcher!(@variant $( $tt )*); )?
};

// unit variant
(@variant $variant:ident $(, $($tt:tt)* )? ) => {
// process rest of the enum
$( enum_item_matcher!(@variant $( $tt )*); )?
};

// trailing comma
(@variant ,) => {};
// base case
(@variant) => {};

// entry point
(
$( #[$meta:meta] )*
$vis:vis enum $name:ident {
$($tt:tt)*
}
) => {
enum_item_matcher!(@variant $($tt)*);
};
}

enum_item_matcher!(
#[derive(Copy, Clone)]
pub(crate) enum Foo {
Bar,
Baz,
}
);
enum_item_matcher!(
#[derive(Copy, Clone)]
pub(crate) enum Bar {
Foo(i32, f32),
Bar,
Baz(),
}
);
enum_item_matcher!(
#[derive(Clone)]
pub(crate) enum Baz {}
);

fn main() {}