通过示例定义的宏(Macros by Example)
Syntax
MacroRulesDefinition →
macro_rules ! IDENTIFIER MacroRulesDef
MacroRulesDef →
( MacroRules ) ;
| [ MacroRules ] ;
| { MacroRules }
MacroRules →
MacroRule ( ; MacroRule )* ;?
MacroRule →
MacroMatcher => MacroTranscriber
MacroMatcher →
( MacroMatch* )
| [ MacroMatch* ]
| { MacroMatch* }
MacroMatch →
Tokenexcept $ and delimiters
| MacroMatcher
| $ ( IDENTIFIER_OR_KEYWORDexcept 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 → Tokenexcept delimiters and MacroRepOp
MacroRepOp → * | + | ?
macro_rules 允许用户以声明方式定义语法扩展。我们将此类扩展称为“通过示例定义的宏“或简称为“宏“。
每个通过示例定义的宏都有一个名称和一个或多个_规则_。每个规则有两部分:匹配器(描述它匹配的语法)和_转录器_(描述将替换成功匹配的调用的语法)。匹配器和转录器都必须用分隔符括起来。宏可以展开为表达式、语句、项(包括 trait、impl 和外部项)、类型或模式。
转录
当宏被调用时,宏展开器按名称查找宏调用,并依次尝试每个宏规则。它转录第一个成功的匹配;如果这导致错误,则不再尝试未来的匹配。
匹配时不执行前瞻;如果编译器无法明确地一次确定如何解析宏调用一个词法单元,则是错误。在以下示例中,编译器不会在标识符之后向前查看以查看后面的词法单元是否是 ),即使这将允许它明确地解析调用:
#![allow(unused)]
fn main() {
macro_rules! ambiguity {
($($i:ident)* $j:ident) => { };
}
ambiguity!(error); // Error: local ambiguity
}
在匹配器和转录器中,$ 词法单元用于调用宏引擎的特殊行为(下面在元变量和重复中描述)。不属于此类调用的词法单元被字面匹配和转录,有一个例外。例外是匹配器的外部分隔符将匹配任何一对分隔符。因此,例如,匹配器 (()) 将匹配 {()} 但不匹配 {{}}。字符 $ 不能被字面匹配或转录。
转发匹配的片段
当将匹配的片段转发到另一个通过示例定义的宏时,第二个宏中的匹配器将看到片段类型的不透明 AST。第二个宏不能使用字面词法单元来匹配匹配器中的片段,只能使用相同类型的片段说明符。ident、lifetime 和 tt 片段类型是例外,可以被字面词法单元匹配。以下说明了此限制:
#![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);
}
元变量
在匹配器中,$ name : fragment-specifier 匹配指定类型的 Rust 语法片段,并将其绑定到元变量 $name。
有效的片段说明符是:
block:BlockExpressionNoInnerAttributesexpr:Expressionexpr_2021:Expression(不包括 UnderscoreExpression 和 ConstBlockExpression)(参见 macro.decl.meta.edition2024)ident:IDENTIFIER_OR_KEYWORD(不包括_、RAW_IDENTIFIER 或$crate)item:Itemlifetime:LIFETIME_TOKENliteral:匹配-?LiteralExpressionmeta:Attr,属性的内容pat:Pattern(参见 macro.decl.meta.edition2021)pat_param:PatternNoTopAltpath:TypePathstmt:Statement(不带尾部分号)(需要分号的项语句除外)tt:TokenTree (单个 token 或匹配分隔符()、[]或{}中的词法单元)ty:Typevis:可能为空的 Visibility 限定符
在转录器中,元变量通过 $name 简单引用,因为片段类型在匹配器中指定。元变量被匹配它们的语法元素替换。元变量可以被转录多次或根本不转录。
关键字元变量 $crate 可用于引用当前 crate。
2021 Edition differences
从 2021 版本开始,
pat片段说明符匹配顶级或模式(即它们接受 Pattern)。在 2021 版本之前,它们匹配与
pat_param完全相同的片段(即它们接受 PatternNoTopAlt)。相关版本是
macro_rules!定义生效的版本。
2024 Edition differences
在 2024 版本之前,
expr片段说明符在顶级不匹配 UnderscoreExpression 或 ConstBlockExpression。它们在子表达式中是允许的。
expr_2021片段说明符的存在是为了保持与 2024 版本之前的向后兼容性。
重复
在匹配器和转录器中,通过将要重复的词法单元放在 $(…) 内,后跟重复运算符来指示重复,可选地在中间使用分隔词法单元。
分隔词法单元可以是除分隔符或重复运算符之外的任何词法单元,但 ; 和 , 是最常见的。例如,$( $i:ident ),* 表示任意数量的用逗号分隔的标识符。允许嵌套重复。
重复运算符是:
*— 表示任意数量的重复。+— 表示任意数量但至少一个。?— 表示可选片段,出现零次或一次。
由于 ? 表示最多一次出现,因此不能与分隔符一起使用。
重复的片段匹配并转录为指定数量的片段,由分隔词法单元分隔。元变量匹配到其对应片段的每次重复。例如,上面的 $( $i:ident ),* 示例将 $i 匹配到列表中的所有标识符。
在转录期间,对重复应用额外的限制,以便编译器知道如何正确展开它们:
- 元变量在转录器中必须出现与匹配器中完全相同数量、种类和嵌套顺序的重复。因此,对于匹配器
$( $i:ident ),*,转录器=> { $i }、=> { $( $( $i )* )* }和=> { $( $i )+ }都是非法的,但=> { $( $i );* }是正确的,并将逗号分隔的标识符列表替换为分号分隔的列表。 - 转录器中的每个重复必须包含至少一个元变量,以决定展开多少次。如果多个元变量出现在同一重复中,它们必须绑定到相同数量的片段。例如,
( $( $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.
}
宏的文本作用域名称绑定遮蔽基于路径的作用域绑定。
#![allow(unused)]
fn main() {
macro_rules! m2 {
() => {
println!("m2");
};
}
// Resolves to path-based candidate from use declaration below.
m!(); // prints "m2\n"
// Introduce second candidate for `m` with textual scope.
//
// This shadows path-based candidate from below for the rest of this
// example.
macro_rules! m {
() => {
println!("m");
};
}
// Introduce `m2` macro as path-based candidate.
//
// This item is in scope for this entire example, not just below the
// use declaration.
use m2 as m;
// Resolves to the textual macro candidate from above the use
// declaration.
m!(); // prints "m\n"
}
Note
有关不允许遮蔽的区域,请参阅名称解析歧义。
基于路径的作用域
默认情况下,宏没有基于路径的作用域。宏可以通过两种方式获得基于路径的作用域:
宏可以被重导出,以便从 crate 根以外的模块获得基于路径的作用域。
#![allow(unused)]
fn main() {
mac::m!(); // OK: Path-based lookup finds `m` in the mac module.
mod mac {
// Introduce macro `m` with textual scope.
macro_rules! m {
() => {};
}
// Reexport with path-based scope from within `m`'s textual scope.
pub(crate) use m;
}
}
宏具有隐式可见性 pub(crate)。#[macro_export] 将隐式可见性更改为 pub。
#![allow(unused)]
fn main() {
// Implicit visibility is `pub(crate)`.
macro_rules! private_m {
() => {};
}
// Implicit visibility is `pub`.
#[macro_export]
macro_rules! pub_m {
() => {};
}
pub(crate) use private_m as private_macro; // OK.
pub use pub_m as pub_macro; // OK.
}
#![allow(unused)]
fn main() {
// Implicit visibility is `pub(crate)`.
macro_rules! private_m {
() => {};
}
// Implicit visibility is `pub`.
#[macro_export]
macro_rules! pub_m {
() => {};
}
pub(crate) use private_m as private_macro; // OK.
pub use pub_m as pub_macro; // OK.
pub use private_m; // ERROR: `private_m` is only public within
// the crate and cannot be re-exported outside.
}
macro_use 属性
macro_use 属性 有两个用途:它可用于模块以扩展其中定义的宏的作用域,也可用于 extern crate 以将宏从另一个 crate 导入到 macro_use prelude 中。
Example
#![allow(unused)] fn main() { #[macro_use] mod inner { macro_rules! m { () => {}; } } m!(); }#[macro_use] extern crate log;
在模块上使用时,macro_use 属性使用 MetaWord 语法。
在 extern crate 上使用时,它使用 MetaWord 和 MetaListIdents 语法。有关如何使用这些语法的更多信息,请参阅 macro.decl.scope.macro_use.prelude。
macro_use 属性可应用于模块或 extern crate。
Note
rustc会忽略在其他位置的使用,但会发出 lint 警告。这可能在未来成为错误。
macro_use 属性不能在 extern crate self 上使用。
macro_use 属性可以在一个形式上使用任意次数。
可以指定多个 MetaListIdents 语法的 macro_use 实例。所有指定宏的并集将被导入。
Note
在模块上,
rustc会对第一个之后的任何 MetaWordmacro_use属性发出 lint 警告。在
extern crate上,rustc会对由于没有导入任何未被另一个macro_use属性导入的宏而无效的任何macro_use属性发出 lint 警告。如果两个或多个 MetaListIdentsmacro_use属性导入相同的宏,则第一个会被 lint。如果存在任何 MetaWordmacro_use属性,则所有 MetaListIdentsmacro_use属性都会被 lint。如果存在两个或多个 MetaWordmacro_use属性,则第一个之后的会被 lint。
在模块上使用 macro_use 时,模块的宏作用域扩展到模块的词法作用域之外。
Example
#![allow(unused)] fn main() { #[macro_use] mod inner { macro_rules! m { () => {}; } } m!(); // OK }
在 crate 根中的 extern crate 声明上指定 macro_use 会从该 crate 导入导出的宏。
以这种方式导入的宏被导入到 macro_use prelude 中,而不是文本导入,这意味着它们可以被任何其他名称遮蔽。由 macro_use 导入的宏可以在导入语句之前使用。
Note
rustc目前在冲突时优先选择最后导入的宏。不要依赖这一点。此行为是不寻常的,因为 Rust 中的导入通常是顺序无关的。macro_use的此行为将来可能会更改。有关详细信息,请参阅 Rust issue #148025。
使用 MetaWord 语法时,导入所有导出的宏。使用 MetaListIdents 语法时,仅导入指定的宏。
Example
#[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 属性
macro_export 属性 从 crate 导出宏,并使其在 crate 根中可用以进行基于路径的解析。
Example
#![allow(unused)] fn main() { self::m!(); // ^^^^ OK: Path-based lookup finds `m` in the current module. m!(); // As above. mod inner { super::m!(); crate::m!(); } mod mac { #[macro_export] macro_rules! m { () => {}; } } }
macro_export 属性使用 MetaWord 和 MetaListIdents 语法。使用 MetaListIdents 语法时,它接受单个 local_inner_macros 值。
macro_export 属性可应用于 macro_rules 定义。
Note
rustc会忽略在其他位置的使用,但会发出 lint 警告。这可能在未来成为错误。
只有宏上的第一次使用 macro_export 才有效果。
Note
rustc会对第一次之后的任何使用发出 lint 警告。
默认情况下,宏只有[文本作用域]textual scope,无法通过路径解析。当使用 macro_export 属性时,宏在 crate 根中可用,并可以通过其路径引用。
Example
没有
macro_export,宏只有文本作用域,因此宏的基于路径的解析失败。macro_rules! m { () => {}; } self::m!(); // ERROR crate::m!(); // ERROR fn main() {}使用
macro_export,基于路径的解析有效。#[macro_export] macro_rules! m { () => {}; } self::m!(); // OK crate::m!(); // OK fn main() {}
macro_export 属性使宏从 crate 根导出,以便可以在其他 crate 中通过路径引用。
Example
在
logcrate 中给定以下内容:#![allow(unused)] fn main() { #[macro_export] macro_rules! warn { ($message:expr) => { eprintln!("WARN: {}", $message) }; } }从另一个 crate,您可以通过路径引用宏:
fn main() { log::warn!("example warning"); }
macro_export 允许在 extern crate 上使用 macro_use 将宏导入到 macro_use prelude 中。
Example
在
logcrate 中给定以下内容:#![allow(unused)] fn main() { #[macro_export] macro_rules! warn { ($message:expr) => { eprintln!("WARN: {}", $message) }; } }在依赖 crate 中使用
macro_use允许您从 prelude 使用宏:#[macro_use] extern crate log; pub mod util { pub fn do_thing() { // Resolved via macro prelude. warn!("example warning"); } }
在 macro_export 属性中添加 local_inner_macros 会导致宏定义中的所有单段宏调用具有隐式的 $crate:: 前缀。
Note
这主要用作将
$crate添加到语言之前编写的代码迁移到 Rust 2018 的基于路径的宏导入的工具。不鼓励在新代码中使用。
Example
#![allow(unused)] fn main() { #[macro_export(local_inner_macros)] macro_rules! helped { () => { helper!() } // Automatically converted to $crate::helper!(). } #[macro_export] macro_rules! helper { () => { () } } }
卫生性
通过示例定义的宏具有_混合站点卫生性_。这意味着循环标签、块标签和局部变量在宏定义站点查找,而其他符号在宏调用站点查找。例如:
#![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 内的项,其使用对可见性没有影响。引用的项或宏在调用站点必须仍然可见。在以下示例中,任何从其外部调用 call_foo!() 的尝试都将失败,因为 foo() 不是公开的。
#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! call_foo {
() => { $crate::foo() };
}
fn foo() {}
}
Note
在 Rust 1.30 之前,
$crate和local_inner_macros不受支持。它们与基于路径的宏导入一起添加,以确保辅助宏不需要由宏导出 crate 的用户手动导入。为早期版本 Rust 编写的使用辅助宏的 crate 需要修改为使用$crate或local_inner_macros,以便与基于路径的导入良好配合。
跟随集歧义限制
宏系统使用的解析器相当强大,但它是有限的,以防止当前或未来版本的语言中出现歧义。
特别是,除了关于歧义展开的规则外,由元变量匹配的非终结符后面必须跟着一个已被决定可以安全地在该种匹配之后使用的词法单元。
例如,像 $i:expr [ , ] 这样的宏匹配器理论上今天可以在 Rust 中接受,因为 [[,] 不能是合法表达式的一部分,因此解析将始终是明确的。然而,因为 [ 可以开始尾随表达式,[ 不是可以在表达式之后安全排除的字符。如果 [[,] 在更高版本的 Rust 中被接受,此匹配器将变得有歧义或错误解析,破坏可工作的代码。像 $i:expr, 或 $i:expr; 这样的匹配器将是合法的,因为 , 和 ; 是合法的表达式分隔符。具体规则是:
expr和stmt后面只能跟以下之一:=>、,或;。
pat_param后面只能跟以下之一:=>、,、=、|、if或in。
pat后面只能跟以下之一:=>、,、=、if或in。
path和ty后面只能跟以下之一:=>、,、=、|、;、:、>、>>、[、{、as、where或block片段说明符的宏变量。
vis后面只能跟以下之一:,、除priv外的标识符、可以开始类型的任何词法单元,或具有ident、ty或path片段说明符的元变量。
- 所有其他片段说明符没有限制。
2021 Edition differences
在 2021 版本之前,
pat后面也可以跟|。
当涉及重复时,规则适用于每个可能的展开数量,考虑分隔符。这意味着:
- 如果重复包含分隔符,则该分隔符必须能够跟在重复内容之后。
- 如果重复可以重复多次(
*或+),则内容必须能够跟在自己之后。 - 重复的内容必须能够跟在前面的内容之后,后面的内容必须能够跟在重复的内容之后。
- 如果重复可以匹配零次(
*或?),则后面的内容必须能够跟在前面的内容之后。
有关更多详细信息,请参阅正式规范。