宏示例

语法
MacroRulesDefinition :
   macro_rules ! 标识符 MacroRulesDef

MacroRulesDef :
      ( MacroRules ) ;
   | [ MacroRules ] ;
   | { MacroRules }

MacroRules :
   MacroRule ( ; MacroRule )* ;?

MacroRule :
   MacroMatcher => MacroTranscriber

MacroMatcher :
      ( MacroMatch* )
   | [ MacroMatch* ]
   | { MacroMatch* }

MacroMatch :
      Token除了 $分隔符 之外
   | MacroMatcher
   | $ ( IDENTIFIER_OR_KEYWORD 除了 crate 之外 | RAW_IDENTIFIER | _ ) : MacroFragSpec
   | $ ( MacroMatch+ ) MacroRepSep? MacroRepOp

MacroFragSpec :
      block | expr | expr_2021 | ident | item | lifetime | literal
   | meta | pat | pat_param | path | stmt | tt | ty | vis

MacroRepSep :
   Token除了 分隔符 和 MacroRepOp 之外

MacroRepOp :
   * | + | ?

* | + | ? :
   MacroTranscriber

macro_rules 允许用户以声明方式定义语法扩展。我们称这种扩展为“示例宏”,或简称为“宏”。

每个示例宏都有一个名称和一个或多个规则。每个规则都有两个部分:一个匹配器,描述它匹配的语法;以及一个转录器,描述将替换成功匹配的调用的语法。匹配器和转录器都必须用分隔符括起来。宏可以扩展为表达式、语句、项(包括特性、实现和外部项)、类型或模式。

转录

当宏被调用时,宏扩展器会按名称查找宏调用,并依次尝试每个宏规则。它会转录第一个成功匹配的规则;如果这导致错误,则不会尝试未来的匹配。

匹配时,不会执行前瞻;如果编译器无法一次一个标记地明确确定如何解析宏调用,则会发生错误。在以下示例中,编译器不会向前查看标识符后面的标记是否是 ),即使这样做可以明确解析调用

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

ambiguity!(error); // Error: local ambiguity
}

在匹配器和转录器中,$ 标记用于调用宏引擎的特殊行为(如下面的 元变量重复 中所述)。不属于此类调用的标记将按字面匹配和转录,但有一个例外。例外情况是,匹配器的外部分隔符将匹配任何一对分隔符。因此,例如,匹配器 (()) 将匹配 {()},但不匹配 {{}}。字符 $ 不能按字面匹配或转录。

转发匹配的片段

当将匹配的片段转发到另一个示例宏时,第二个宏中的匹配器将看到片段类型的不透明 AST。第二个宏不能使用字面标记来匹配匹配器中的片段,只能使用相同类型的片段说明符。identlifetimett 片段类型是一个例外,它们可以通过字面标记匹配。以下说明了此限制

#![allow(unused)]
fn main() {
macro_rules! foo {
    ($l:expr) => { bar!($l); }
// ERROR:               ^^ no rules expected this token in macro call
}

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

foo!(3);
}

以下说明了在匹配 tt 片段后如何直接匹配标记

#![allow(unused)]
fn main() {
// compiles OK
macro_rules! foo {
    ($l:tt) => { bar!($l); }
}

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

foo!(3);
}

元变量

在匹配器中,$ 名称 : 片段说明符 匹配指定类型的 Rust 语法片段,并将其绑定到元变量 $名称

有效的片段说明符有

在转录器中,元变量仅通过 $名称 引用,因为片段类型在匹配器中指定。元变量将替换为匹配它们的语法元素。

关键字元变量 $crate 可用于引用当前 crate;请参阅下面的 卫生。元变量可以转录多次,也可以根本不转录。

出于向后兼容的原因,尽管 _ 也是一个表达式,但独立的下划线不会被 expr 片段说明符匹配。但是,当 _ 作为子表达式出现时,它会被 expr 片段说明符匹配。出于同样的原因,独立的 常量块 不匹配,但当它作为子表达式出现时会被匹配。

版本差异:从 2021 版本开始,pat 片段说明符匹配顶层或模式(也就是说,它们接受 模式)。

在 2021 版本之前,它们匹配的片段与 pat_param 完全相同(也就是说,它们接受 PatternNoTopAlt)。

相关版本是 macro_rules! 定义生效的版本。

expr_2021 片段说明符的存在是为了保持与 2024 年之前版本的向后兼容性。

重复

在匹配器和转录器中,重复通过将要重复的标记放在 $() 内来指示,后跟重复运算符,可以选择在它们之间使用分隔符标记。

分隔符标记可以是除分隔符或重复运算符之一之外的任何标记,但 ;, 是最常见的。例如,$( $i:ident ),* 表示任意数量的以逗号分隔的标识符。允许嵌套重复。

重复运算符为

  • * — 表示任意次数的重复。
  • + — 表示任意次数的重复,但至少一次。
  • ? — 表示零次或一次出现的可选片段。

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

重复的片段既匹配又转录为指定数量的片段,并由分隔符标记分隔。元变量将匹配到其对应片段的每次重复。例如,上面的 $( $i:ident ),* 示例将 $i 匹配到列表中的所有标识符。

在转录期间,重复项适用其他限制,以便编译器知道如何正确展开它们

  1. 元变量在转录器中出现的重复次数、种类和嵌套顺序必须与在匹配器中出现的完全相同。因此,对于匹配器 $( $i:ident ),*,转录器 => { $i }=> { $( $( $i)* )* }=> { $( $i )+ } 都是非法的,但 => { $( $i );* } 是正确的,它将逗号分隔的标识符列表替换为分号分隔的列表。
  2. 转录器中的每次重复都必须包含至少一个元变量,以确定要展开它的次数。如果多个元变量出现在相同的重复中,则它们必须绑定到相同数量的片段。例如,( $( $i:ident ),* ; $( $j:ident ),* ) => (( $( ($i,$j) ),* )) 必须绑定与 $j 片段相同数量的 $i 片段。这意味着使用 (a, b, c; d, e, f) 调用宏是合法的,它会展开为 ((a,d), (b,e), (c,f)),但 (a, b, c; d, e) 是非法的,因为它没有相同的数量。此要求适用于嵌套重复的每一层。

作用域、导出和导入

由于历史原因,示例宏的作用域并不完全像项那样工作。宏有两种形式的作用域:文本作用域和基于路径的作用域。文本作用域基于事物在源文件中出现的顺序,甚至跨多个文件,并且是默认作用域。下面将进一步解释。基于路径的作用域与项作用域的工作方式完全相同。宏的作用域、导出和导入主要由属性控制。

当宏由非限定标识符(不是多部分路径的一部分)调用时,首先在文本作用域中查找。如果这没有产生任何结果,则在基于路径的作用域中查找。如果宏的名称由路径限定,则仅在基于路径的作用域中查找。

use lazy_static::lazy_static; // Path-based import.

macro_rules! lazy_static { // Textual definition.
    (lazy) => {};
}

lazy_static!{lazy} // Textual lookup finds our macro first.
self::lazy_static!{} // Path-based lookup ignores our macro, finds imported one.

文本作用域

文本作用域主要基于事物在源文件中出现的顺序,其工作方式类似于使用 let 声明的局部变量的作用域,只是它也适用于模块级别。当使用 macro_rules! 定义宏时,宏会在定义后进入作用域(请注意,它仍然可以递归使用,因为名称是从调用站点查找的),直到其周围的作用域(通常是模块)关闭。这可以进入子模块,甚至可以跨越多个文件

//// src/lib.rs
mod has_macro {
    // m!{} // Error: m is not in scope.

    macro_rules! m {
        () => {};
    }
    m!{} // OK: appears after declaration of m.

    mod uses_macro;
}

// m!{} // Error: m is not in scope.

//// src/has_macro/uses_macro.rs

m!{} // OK: appears after declaration of m in src/lib.rs

多次定义宏不是错误;除非之前的宏超出作用域,否则最近的声明将覆盖之前的声明。

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

m!(1);

mod inner {
    m!(1);

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

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

m!(1);
}

宏也可以在函数内部局部声明和使用,并且工作方式类似

#![allow(unused)]
fn main() {
fn foo() {
    // m!(); // Error: m is not in scope.
    macro_rules! m {
        () => {};
    }
    m!();
}

// m!(); // Error: m is not in scope.
}

macro_use 属性

macro_use 属性 有两个目的。首先,可以通过将其应用于模块,使模块的宏作用域在模块关闭时不结束

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

m!();
}

其次,可以通过将其附加到 crate 根模块中出现的 extern crate 声明来从另一个 crate 导入宏。以这种方式导入的宏会被导入到 macro_use prelude 中,而不是文本上,这意味着它们可以被任何其他名称遮蔽。虽然通过 #[macro_use] 导入的宏可以在导入语句之前使用,但在发生冲突的情况下,最后导入的宏会获胜。可以选择使用 MetaListIdents 语法指定要导入的宏列表;当 #[macro_use] 应用于模块时,不支持此功能。

#[macro_use(lazy_static)] // Or #[macro_use] to import all macros.
extern crate lazy_static;

lazy_static!{}
// self::lazy_static!{} // Error: lazy_static is not defined in `self`

要通过 #[macro_use] 导入的宏必须使用 #[macro_export] 导出,这将在下面介绍。

基于路径的作用域

默认情况下,宏没有基于路径的作用域。但是,如果它具有 #[macro_export] 属性,则会在 crate 根作用域中声明,并且可以正常地引用它。

#![allow(unused)]
fn main() {
self::m!();
m!(); // OK: Path-based lookup finds m in the current module.

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

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

标记有 #[macro_export] 的宏始终是 pub,并且可以通过路径或通过上面描述的 #[macro_use] 被其他 crate 引用。

卫生性

示例宏具有混合站点的卫生性。这意味着 循环标签块标签和局部变量在宏定义站点查找,而其他符号在宏调用站点查找。例如

#![allow(unused)]
fn main() {
let x = 1;
fn func() {
    unreachable!("this is never called")
}

macro_rules! check {
    () => {
        assert_eq!(x, 1); // Uses `x` from the definition site.
        func();           // Uses `func` from the invocation site.
    };
}

{
    let x = 2;
    fn func() { /* does not panic */ }
    check!();
}
}

宏展开中定义的标签和局部变量不会在调用之间共享,因此此代码无法编译

#![allow(unused)]
fn main() {
macro_rules! m {
    (define) => {
        let x = 1;
    };
    (refer) => {
        dbg!(x);
    };
}

m!(define);
m!(refer);
}

一个特殊情况是 $crate 元变量。它引用定义宏的 crate,并且可以在路径的开头使用,以查找调用站点中不在作用域中的项或宏。

//// Definitions in the `helper_macro` crate.
#[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 {
    () => { () }
}

//// Usage in another crate.
// Note that `helper_macro::helper` is not imported!
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(如下所述)。它们与宏的基于路径的导入(如上所述)一起添加,以确保宏导出的 crate 的用户不需要手动导入辅助宏。为早期版本的 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!() } // Automatically converted to $crate::helper!().
}

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

后续集歧义限制

宏系统使用的解析器功能相当强大,但为了防止当前或未来版本的语言出现歧义,它受到限制。

特别是,除了关于歧义扩展的规则之外,元变量匹配的非终结符后面必须跟随一个已确定可以安全地在该匹配类型之后使用的标记。

例如,像 $i:expr [ , ] 这样的宏匹配器在理论上可以在今天的 Rust 中接受,因为 [,] 不能是合法表达式的一部分,因此解析将始终是明确的。但是,由于 [ 可以开始尾随表达式,因此 [ 不是一个可以安全地排除在表达式之后出现的字符。如果在以后的 Rust 版本中接受 [,],则此匹配器将变得含糊不清或会误解析,从而破坏工作代码。然而,像 $i:expr,$i:expr; 这样的匹配器将是合法的,因为 ,; 是合法的表达式分隔符。具体规则是

  • exprstmt 只能后跟以下之一:=>,;
  • pat_param 只能后跟以下之一:=>,=|ifin
  • pat 只能后跟以下之一:=>,=ifin
  • pathty 只能后跟以下之一:=>,=|;:>>>[{aswhereblock 片段说明符的宏变量。
  • vis 只能后跟以下之一:,、非原始 priv 之外的标识符、任何可以开始类型的标记,或带有 identtypath 片段说明符的元变量。
  • 所有其他片段说明符都没有限制。

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

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

  • 如果重复包含分隔符,则该分隔符必须能够跟随重复的内容。
  • 如果重复可以重复多次 (*+),则内容必须能够跟随自身。
  • 重复的内容必须能够跟随之前的任何内容,而之后的任何内容必须能够跟随重复的内容。
  • 如果重复可以匹配零次 (*?),则之后出现的任何内容都必须能够跟随之前出现的任何内容。

有关更多详细信息,请参阅正式规范