实例宏

语法
宏规则定义 :
   macro_rules ! 标识符 宏规则组定界

宏规则组定界 :
      ( 宏规则组 ) ;
   | [ 宏规则组 ] ;
   | { 宏规则组 }

宏规则组 :
   宏规则 ( ; 宏规则 )* ;?

宏规则 :
   宏匹配器 => 宏转录器

宏匹配器 :
      ( 宏匹配* )
   | [ 宏匹配* ]
   | { 宏匹配* }

宏匹配 :
      Token不包括 $定界符号 之外的任意 token
   | 宏匹配器
   | $ ( 标识符或关键字 不包括 crate 之外的任意标识符或关键字 | 原始标识符 | _ ) : 宏片段规格
   | $ ( 宏匹配+ ) 宏表示分割? 宏表示操作符

宏片段规格 :
      block | expr | ident | item | lifetime | literal
   | meta | pat | pat_param | path | stmt | tt | ty | vis

宏表示分割 :
   Token不包括 定界符号 和 宏表示操作符 之外的任意 token_

宏表示操作符 :
   * | + | ?

宏转录器 :
   定界Token树

允许用户以 macro_rules 声明的方式定义语法扩展。 这种语法扩展称为 "实例宏" ,或者简单的称为 "宏" 。

每个实例宏的定义都要有一个名称和一个以上 规则 。 每个规则都有两个部分:一个 匹配器 部分,用于描述匹配语法,另一个 转录器 部分,用于描述成功匹配并调用后将替换的语法。 匹配器和转录器都必须被包裹在定界符号中。 宏可以展开为表达式、语句、条目 (包括 trait 、实现和外部条目) 、类型或模式。

转录

当宏被调用时,宏展开器通过名称查找宏调用,并依次尝试匹配包含的宏规则。 在首个成功匹配处转录,如果导致错误,则不会尝试后续的规则。 在进行匹配时,不会对 Token 进行前瞻,如果编译器无法逐个明确如何解析,则会出错。 在下面的示例中,编译器不会前瞻标识符 $j ,不会前瞻是否已至 ) 定界位置,虽然这能够更明确地解析:

#![allow(unused)]
fn main() {
macro_rules! ambiguity {
    ($($i:ident)* $j:ident) => { };
}

ambiguity!(error); // Error: 局部歧义
}

在匹配器和转录器中, $ 符号用于宏引擎实现调用的特定行为 (具体在 元变量重复 中描述)。 非该符号标记的 Token 会按字面进行匹配和转录,特例是匹配器的外部定界符将匹配任意一对定界符, 例如,匹配器 (()) 将匹配 {()} ,但是不匹配 {{}} 。字符 $ 不能按字面匹配或转录。

转发匹配片段

在将匹配后的片段转发给内部的实例宏时,内部宏的匹配器将只能看到类型化的不透明 AST 片段。 内部宏无法简单通过字面 token 来匹配这个片段,只能使用相同类型规格的片段接收。 identlifetimett 片段类型例外,可以按照字面 token 的方式匹配。 以下是一个示例:

#![allow(unused)]
fn main() {
macro_rules! foo {
    // expr 表达式片段与 3 的字面值规则不匹配,虽然此表达式是 3 的字面值表达式。
    ($l:expr) => { bar!($l); }
// ERROR:               ^^ 在宏调用中这个 token 没有预期的规则,
}

macro_rules! bar {
    (3) => {}
}

foo!(3);
}

以下是在匹配 tt Token 树片段后内部规则通过字面匹配 Token 的一个示例:

#![allow(unused)]
fn main() {
// 编译成功
macro_rules! foo {
    // `tt` Token 树片段直接作为字面 Token 与 3 的字面值表达式成功匹配。
    ($l:tt) => { bar!($l); }
}

macro_rules! bar {
    (3) => {}
}

foo!(3);
}

元变量

在匹配器中, $ 名称 : 片段规格 定义元变量,用于匹配指定类型的语法片段,并与语法片段绑定。 有效的片段规格包括:

译注:这里 '片段规格' 的概念实际上与 '片段类型' 等价,使用规格这个词是与 rust 中的类型系统有所区分。

元变量的片段类型在匹配器中已被指定,在转录器中可以用 $名称 来引用。关键字元变量 $crate 用于引用当前的 crate。 转录器中元变量将替换为匹配到的语法片段。 可以将元变量多次转录,或者并不进行转录。 参阅下面的 卫生性

出于向后兼容的原因,单独的 _ 下划线表达式 不会被 expr 片段匹配。当 _ 做为子表达式时可被 expr 片段匹配。

版次差异: 从 2021 版本开始,pat 规格片段可以匹配顶层或模式 (即它们接受 模式) 。

在 2021 版本之前,则完全匹配与 pat_param 相同的片段 (即它们接受 模式非顶层项 ) 。

这与 macro_rules! 定义生效的版次相关联。

重复

在匹配器和转录器中,重复语法是通过将要重复的 token 放在 $() 中,后跟重复运算符,可选地包含分隔符 token 。 分隔符 token 可以是除定界符或重复运算符外的任意 token ,但常用的是 ;, 。 例如, $( $i:ident ),* 表明由逗号分隔的任意数量的标识符。重复允许嵌套。

重复运算符有:

  • * — 表示任意数量的重复。
  • + — 表示任意数字,但至少是 1 。
  • ? — 表示有 0 或 1 个产生的可选片段。

由于 ? 表示最多出现一次,所以它不能与分隔符一起使用。

重复的片段同时匹配和转录成指定数量的该片段,并由分隔符隔开。 元变量将匹配到对应片段的每个重复项。例如,上面示例的 $( $i:ident ),* 中元变量 $i 将匹配到列表中的所有标识符。

在转录过程中,重复操作受到额外的限制,以便能够正确地展开:

  1. 匹配器中的 '重复' ,在转录器中出现时,必须具有完全相同的数量、种类和嵌套顺序。 因此,对于匹配器中 $( $i:ident ),* '重复' ,转录器中 => { $i }=> { $( $( $i)* )* }=> { $( $i )+ } 不一致的形式是不合法的, 对于 => { $( $i );* } 是正确的,这里只是将由逗号分隔的标识符列表替换为了分号分隔。
  2. 在转录器中,每个重复部分必须至少包含一个元变量,以决定展开它的次数。 如果同一个重复部分中出现多个元变量时,则它们绑定的片段数量必须相同。 例如,( $( $i:ident ),* ; $( $j:ident ),* ) => (( $( ($i,$j) ),* ))$i$j 必须绑定相同数量的片段。 所以,以 (a, b, c; d, e, f) 这样的内容调用宏是合法的,将展开为 ((a,d), (b,e), (c,f)) ,但 (a, b, c; d, e) 却不行,因为数量不同。 这个要求对于嵌套的重复同样适用。

作用域、导出和导入

由于历史原因,实例宏的作用域机制与条目并不完全相同。 宏有两种作用域形式:文本作用域和基于路径的作用域。 文本作用域是基于宏在源文件中的顺序,甚至会跨越多个文件,并且这是默认的作用域。在下面会进一步解释。 基于路径的作用域与条目的作用域完全相同。宏的作用域、导出和导入主要由属性控制。

当以未限定的标识符 (指宏不是多部分组成的路径的一部分) 形式调用宏时,则首先在文本作用域中查找宏。 如果未找到结果,那么将按路径作用域查找。 如果宏的名称带有路径限定符,则仅在基于路径的作用域中查找。

use lazy_static::lazy_static; // 基于路径导入。

macro_rules! lazy_static { // 定义文本。
    (lazy) => {};
}

lazy_static!{lazy} // 首先在文本中找到了宏。
self::lazy_static!{} // 基于路径的查找忽略了文本中的宏,找到导入的宏。

文本作用域

文本作用域主要基于宏在源文件中出现的顺序,机制与 let 声明的局部变量的作用域类似,但宏文本作用域将应用于模块级别。 当使用 macro_rules! 定义了宏,则宏在定义的以下部分进入作用域。需注意的是,由于名称是从调用位置查找,因而可以递归。 在其围绕的作用域 (通常是模块) 关闭之前,宏可以进入子模块,甚至跨越多个文件:

//// src/lib.rs
mod has_macro {
    // m!{} // Error: m 不在作用域内。

    macro_rules! m {
        () => {};
    }
    m!{} // OK: 出现在 m 的声明之后。

    mod uses_macro;
}

// m!{} // Error: m 不在作用域内。

//// src/has_macro/uses_macro.rs

m!{} // OK: 在 src/lib.rs 中出现在 m 的声明之后。

多次定义宏不会产生错误,如果未超出作用域,最近一次的声明会隐藏先前的声明。

#![allow(unused)]
fn main() {
macro_rules! m {
    (1) => {};
}

m!(1);

mod inner {
    m!(1);

    macro_rules! m {
        (2) => {};
    }
    // m!(1); // Error: 没有规则可以匹配 '1'
    m!(2);

    macro_rules! m {
        (3) => {};
    }
    m!(3);
}

m!(1);
}

宏也可以在函数内部声明和使用,工作方式与模块中声明类似:

#![allow(unused)]
fn main() {
fn foo() {
    // m!(); // Error: m 不在作用域内。
    macro_rules! m {
        () => {};
    }
    m!();
}


// m!(); // Error: m 不在作用域内。
}

macro_use 属性

macro_use 属性 有两个用法。一是,应用于模块后,使模块中宏的作用域在模块关闭后仍不结束:

#![allow(unused)]
fn main() {
#[macro_use]
mod inner {
    macro_rules! m {
        () => {};
    }
}

// 这里使 m 的作用域延长到了模块之外
m!();
}

二是,应用于 crate 根模块中的 extern crate 声明,从而从另一个 crate 导入宏。 这种方式会将宏导入到 macro_use 预定义 中,而不是文本形式的导入,这表示可在任意位置隐藏此宏名称。 虽然 #[macro_use] 导入的宏可以在导入语句之前使用,但名称冲突时,最后导入的宏优先。 可选语法是,可以使用 [元标识符列表][MetaListIdents] 指定要导入的宏列表。当将 #[macro_use] 应用于模块时,不支持此功能。

#[macro_use(lazy_static)] // 仅 #[macro_use] 表示导入所有宏。
extern crate lazy_static;

lazy_static!{}
// self::lazy_static!{} // Error: lazy_static 在 `self` 未定义。

使用 #[macro_use] 导入的宏导出时必须使用 #[macro_export] ,下面将对此进行描述。

基于路径作用域

默认情况,宏没有基于路径的作用域。然而,如果赋予 #[macro_export] 属性,那么宏就被声明在 crate 根作用域内:

#![allow(unused)]
fn main() {
self::m!();
m!(); // OK: 基于路径方式查找,在当前模块中找到了 m 。

mod inner {
    super::m!();
    crate::m!();
}

mod mac {
    #[macro_export]
    macro_rules! m {
        () => {};
    }
}
}

#[macro_export] 标记的宏总是 pub 的,可以通过路径或以上面描述的 #[macro_use] 方式引入其他 crate 中。

卫生性

默认情况下,在宏中引用的所有标识符都会按原样展开,并从宏调用的位置查找。 如果宏引用的条目或宏,在调用位置不在作用域内,则可能会出现问题。 为了缓解这个问题,可以在路径中使用 $crate 元变量,以强制在定义宏的 crate 内部进行查找。

//// 定义在 `helper_macro` crate 中。
#[macro_export]
macro_rules! helped {
    // () => { helper!() } // 由于 'helper' 不在作用域中,这可能会导致错误。
    () => { $crate::helper!() }
}

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

//// 在另一个 crate 里使用。
// 请注意,未导入 `helper_macro::helper` 。
use helper_macro::helped;

fn unit() {
    helped!();
}

注意,因为 $crate 指的是当前的 crate ,所以当引用非宏条目时,必须使用完全限定的模块路径。

#![allow(unused)]
fn main() {
pub mod inner {
    #[macro_export]
    macro_rules! call_foo {
        () => { $crate::inner::foo() };
    }

    pub fn foo() {}
}
}

此外,尽管 $crate 允许宏在展开时引用其所在 crate 内的条目,但并不会影响被引用项的可见性。 被引用的条目或宏必须从调用位置可见。 在下面的示例中,从 crate 外部尝试调用 call_foo!() 将失败,因为 foo() 不是公开的。

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! call_foo {
    () => { $crate::foo() };
}

fn foo() {}
}

在 Rust 1.30 之前,不支持 $cratelocal_inner_macros (下面会描述) 。 这两个语法与基于路径导入宏的语法一起添加,是为保证一些辅助宏不需要用户手动导入。 使用辅助宏的早期 Rust 版本编写的 crate 需要进行修改后,以使用 $cratelocal_inner_macros

当一个宏被导出时,可以在 #[macro_export] 属性中添加 local_inner_macros 关键字,以自动在所有包含的宏调用中添加 $crate:: 前缀。 这主要是为了迁移在 $crate 添加到语言之前编写的代码,使其能够与 Rust 2018 的基于路径的宏导入一起使用。 不建议在新代码中使用此功能。

#![allow(unused)]
fn main() {
#[macro_export(local_inner_macros)]
macro_rules! helped {
    () => { helper!() } // 自动转换为 $crate::helper!() 。
}

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

后继符冲突限制

宏系统使用的解析器相当强大,但出于防止当前或未来版本的语言产生歧义的考虑,对于宏有一些限制。 特别是,除了有关模糊展开的规则之外,由元变量匹配的非终端符号必须后跟一个 token ,此 token 已决定可以安全地在该类型的匹配之后使用。

举例来说,像 $i:expr [ , ] 这样的宏匹配器理论上是可以接受的,因为 [,] 不能成为合规的表达式的一部分,因此解析不会有歧义。 但是,由于 [ 可以作为尾随的表达式的开始,因此 [ 不是一个可以安全地在表达式之后排除的字符。 如果在以后的 Rust 版本中接受了 [,] ,这个匹配器就会变成有歧义或者解析错误,从而破坏工作中的代码。 对于,像 $i:expr,$i:expr; 这样的匹配器是合规的,因为 ,; 是合规的表达式分隔符。 具体的规则如下:

  • exprstmt 后面只能跟着一个: =>,;
  • pat_param 后面只能跟着一个: =>,=|ifin
  • pat 后面只能跟着一个: =>,=ifin
  • pathty 后面只能跟着一个: =>,=|;:>>>[{aswhere , 或 block 片段规格的元变量 。
  • vis 后面只能跟着一个: ,, 一个非原始的 priv 以外的标识符,任何可以开始一个类型的标记,或者一个带有 identtypath 片段规格的元变量。
  • 对所有其他片段规格没有限制。

版次差异: 在 2021 版之前, pat 后面还可以跟着 |

当涉及到重复时,规则适用于每次可能的展开,同时要考虑分隔符。这意味着:

  • 如果重复包括一个分隔符,则该分隔符必须能够跟在重复内容之后。
  • 如果重复可以重复多次 (*+) ,则其内容必须能够跟随它们自身。
  • 重复的内容必须能够跟随之前的任何内容,而跟随重复内容的任何内容后面都必须能够跟随。
  • 如果重复可以匹配零次 (*?) ,则后面必须能够跟随前面。

更多细节,见 形式说明