Rust 宏
Rust 的功能和语法可以通过称为宏的自定义定义进行扩展。它们被命名,并通过一致的语法调用:some_extension!(...)。
调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Used as an expression.
let x = vec![1,2,3];
// Used as a statement.
println!("Hello!");
// Used in a pattern.
macro_rules! pat {
($i:ident) => (Some($i))
}
if let pat!(x) = Some(1) {
assert_eq!(x, 1);
}
// Used in a type.
macro_rules! Tuple {
{ $A:ty, $B:ty } => { ($A, $B) };
}
type N2 = Tuple!(i32, i32);
// Used as an item.
use std::cell::RefCell;
thread_local!(static FOO: RefCell<u32> = RefCell::new(1));
// Used as an associated item.
macro_rules! const_maker {
($t:ty, $v:tt) => { const CONST: $t = $v; };
}
trait T {
const_maker!{i32, 7}
}
// Macro calls within macros.
macro_rules! example {
() => { println!("Macro call in a macro!") };
}
// Outer macro `example` is expanded, then inner macro `println` is expanded.
example!();
转录
macro_rules 允许用户以声明的方式定义语法扩展。我们将此类扩展称为“示例宏”或简称为“宏”。
每个宏都有一个名称和一个或多个规则。每个规则有两个部分:一个匹配器(matcher),描述它匹配的语法,以及一个转录器(transcriber),描述将替换成功匹配的调用的语法。匹配器和转录器都必须用分隔符包围。宏可以扩展到表达式、语句、项目(包括特征、实现和外来项目)、类型或模式。
在匹配器和转录器中,$ 标记用于调用宏引擎的特殊行为(在下面的元变量和重复中描述)。不属于此类调用的标记将按字面意思进行匹配和转录,但有一个例外。例外是匹配器的外部分隔符将匹配任何一对分隔符。因此,例如,匹配器 (()) 将匹配 {()} 而不是 {\{\}}。字符 $ 不能按字面匹配或转录。
当将匹配的片段转发到另一个宏时,第二个宏中的匹配器将看到片段类型的不透明 AST。第二个宏不能使用文字标记来匹配匹配器中的片段,只能使用相同类型的片段说明符。 ident、lifetime 和 tt 片段类型是一个例外,可以通过文字标记进行匹配。下面说明了这种限制:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
macro_rules! foo {
($l:expr) => { bar!($l); }
// ERROR: ^^ no rules expected this token in macro call
}
// compiles OK
macro_rules! foo {
($l:tt) => { bar!($l); }
}
macro_rules! bar {
(3) => {}
}
foo!(3);
元变量
在匹配器中,$name:fragment-specifier 匹配指定类型的 Rust 语法片段并将其绑定到元变量 $name。
有效片段说明符列表:
itemitem 是 crate 的组成部分。Items 由一组嵌套的模块组织在一个 crate 中。block块级表达式stmt没有尾随分号的语句(需要分号的项目语句除外)pat_param一个 PattterNoTopAlt (模式用于将值与结构匹配,并可选择将变量绑定到这些结构内的值。它们还用于函数和闭包的变量声明和参数)。pat至少是任何 PatternNoTopAlt,可能更多取决于版本expr表达式ty类型表达式ident标识符pathTypePath 样式路径tt一个 TokenTreemeta一个 Attr,一个属性的内容lifetime生命周期标识vis一个可能为空的可见性限定符literal匹配-LiteralExpression
在转录器中,元变量通过 $name 简单地引用,因为片段类型在匹配器中指定。元变量被替换为与其匹配的语法元素。关键字元变量 $crate 可用于引用当前的 crate,元变量可以被转录不止一次或根本不被转录。
出于向后兼容的原因,虽然 _ 也是一个表达式,但独立的下划线与 expr 片段说明符不匹配。但是,当 _ 作为子表达式出现时,它与 expr 片段说明符匹配。
重复
在匹配器和转录器中,重复是通过将要重复的标记放在 $(...) 中来指示的,然后是一个重复运算符,中间可以选择一个分隔符标记。分隔符标记可以是除定界符或重复运算符之一以外的任何标记,但是 ; 和 , 是最常见的。例如,$( $i:ident ),* 表示任意数量的用逗号分隔的标识符。允许嵌套重复。
重复操作符:
*— 表示任意次数的重复。+— 表示任何数字,但至少为一个。?— 表示出现零次或一次的可选片段。
自从 ? 最多表示一次,不能与分隔符一起使用。
重复的片段匹配并转录为指定数量的片段,由分隔符标记分隔。元变量与其相应片段的每次重复相匹配。例如,上面的 $( $i:ident ),* 示例将 $i 匹配到列表中的所有标识符。
在转录期间,额外的限制适用于重复,以便编译器知道如何正确扩展它们:
- 元变量在转录器中的重复次数、种类和嵌套顺序必须与在匹配器中完全相同。所以对于匹配器
$( $i:ident ),*,转录器=> { $i },=> { $( $( $i)* )* }和=> { $( $i )+ }是都是非法的,但是=> { $( $i );* }是正确的,并且用分号分隔的列表替换了逗号分隔的标识符列表。 - 转录器中的每次重复都必须包含至少一个元变量,以决定将其扩展多少次。如果多个元变量出现在同一个重复中,它们必须绑定到相同数量的片段。例如
( $( $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)是非法的,因为它没有相同的数字。此要求适用于嵌套重复的每一层。
作用域、导出和导入
由于历史原因,示例宏的作用域并不完全像项目一样工作。宏有两种形式的范围:文本范围和基于路径的范围。文本范围基于事物在源文件中出现的顺序,甚至是跨多个文件的顺序,并且是默认范围。下面将进一步解释。基于路径的作用域与项目作用域的工作方式完全相同。宏的作用域、导出和导入主要由属性控制。
当一个宏被一个非限定标识符(不是多部分路径的一部分)调用时,首先在文本范围内查找它。如果这没有产生任何结果,则在基于路径的范围内查找它。如果宏的名称由路径限定,则仅在基于路径的作用域中查找它。
1
2
3
4
5
6
7
8
use lazy_static::lazy_static; // 基于路径导入
macro_rules! lazy_static { // 文本范围内定义
(lazy) => {};
}
lazy_static!{lazy} // 首先在文本范围选择
self::lazy_static!{} // 基于路径的查找忽略我们的宏,找到导入的宏。
文本作用域
文本范围主要基于事物在源文件中出现的顺序,并且与使用 let 声明的局部变量的范围类似,除了它也适用于模块级别。当 macro_rules! 用于定义宏,宏在定义之后进入范围(注意它仍然可以递归使用,因为名称是从调用站点查找的),直到其周围的范围,通常是模块,被关闭。这可以进入子模块,甚至跨越多个文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
//// 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
多次定义宏不是错误;除非超出范围,否则最近的声明将影响前一个声明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
宏也可以在函数内部本地声明和使用,并且工作方式类似:
1
2
3
4
5
6
7
8
fn foo() {
// m!(); // Error: m is not in scope.
macro_rules! m {
() => {};
}
m!();
}
// m!(); // Error: m is not in scope.
macro_user 属性
macro_use 属性有两个用途。首先,它可以用于使模块的宏范围在模块关闭时不会结束,方法是将其应用于模块:
1
2
3
4
5
6
7
#[macro_use]
mod inner {
macro_rules! m {
() => {};
}
}
m!();
其次,它可用于从另一个 crate 导入宏,方法是将其附加到 crate 根模块中出现的 extern crate 声明。以这种方式导入的宏被导入到 macro_use 前奏中,而不是以文本形式导入,这意味着它们可以被任何其他名称遮蔽。虽然通过 #[macro_use] 导入的宏可以在 import 语句之前使用,但如果发生冲突,最后导入的宏胜出。可选地,可以使用 MetaListIdents 语法指定要导入的宏列表;当 #[macro_use] 应用于模块时,不支持此功能。
1
2
3
4
#[macro_use(lazy_static)] // 或者使用 #[macro_use] 导入所有的宏
extern crate lazy_static;
lazy_static!{}
// self::lazy_static!{} // Error: lazy_static is not defined in `self`
使用 #[macro_use] 导入的宏必须使用 #[macro_export] 导出,如下所述。
基于路径的作用域
默认情况下,宏没有基于路径的范围。但是,如果它具有 #[macro_export] 属性,那么它会在 crate 根范围内声明,并且可以正常引用如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 并且可以由其他 crate 引用,通过路径或通过 #[macro_use] 如上所述。