第三章. 宏
宏分为声明宏和过程宏
声明宏:以更高级别的声明性的方式定义了一套新句法规则。
过程宏:可用于实现自定义派生
句法
MacroInvocation :
SimplePath!
DelimTokenTreeDelimTokenTree :
(
TokenTree*)
|[
TokenTree*]
|{
TokenTree*}
TokenTree :
Token排除 定界符(delimiters) | DelimTokenTreeMacroInvocationSemi :
SimplePath!
(
TokenTree*)
;
| SimplePath!
[
TokenTree*]
;
| SimplePath!
{
TokenTree*}
宏调用是在编译时执行宏,并用执行结果替换该调用。可以在下述情况里调用宏:
#![allow(unused)]
fn main() {
// 作为表达式使用.
let x = vec![1,2,3];
// 作为语句使用.
println!("Hello!");
// 在模式中使用.
macro_rules! pat {
($i:ident) => (Some($i))
}
if let pat!(x) = Some(1) {
assert_eq!(x, 1);
}
// 在类型中使用.
macro_rules! Tuple {
{ $A:ty, $B:ty } => { ($A, $B) };
}
type N2 = Tuple!(i32, i32);
// 作为程序项使用.
use std::cell::RefCell;
thread_local!(static FOO: RefCell<u32> = RefCell::new(1));
// 作为关联程序项使用.
macro_rules! const_maker {
($t:ty, $v:tt) => { const CONST: $t = $v; };
}
trait T {
const_maker!{i32, 7}
}
// 宏内调用宏
macro_rules! example {
() => { println!("Macro call in a macro!") };
}
// 外部宏 `example` 展开后, 内部宏 `println` 才会展开.
example!();
}
3.1 声明宏
句法
MacroRulesDefinition :
macro_rules
!
IDENTIFIER MacroRulesDefMacroRulesDef :
(
MacroRules)
;
|[
MacroRules]
;
|{
MacroRules}
MacroRules :
MacroRule (;
MacroRule )*;
?MacroRule :
MacroMatcher=>
MacroTranscriberMacroMatcher :
(
MacroMatch*)
|[
MacroMatch*]
|{
MacroMatch*}
MacroMatch :
Token排除 $ 和 定界符
| MacroMatcher
|$
IDENTIFIER:
MacroFragSpec
|$
(
MacroMatch+)
MacroRepSep? MacroRepOpMacroFragSpec :
block
|expr
|ident
|item
|lifetime
|literal
|meta
|pat
|path
|stmt
|tt
|ty
|vis
MacroRepSep :
Token排除 定界符 和 重复操作符MacroRepOp :
*
|+
|?
MacroTranscriber :
DelimTokenTree
3.1.1 转码
当宏被调用时,宏扩展器(macro expander)按名称查找宏调用,并依次尝试此宏中的每条宏规则。宏会根据第一个成功的匹配进行转码;如果当前转码结果导致错误,不会再尝试进行后续匹配。在匹配时,不会执行预判;如果编译器不能明确地确定如何一个 token 一个 token 地解析宏调用,则会报错。在下面的示例中,编译器不会越过标识符,去提前查看后跟的 token 是 )
,尽管这能帮助它明确地解析调用:
#![allow(unused)]
fn main() {
macro_rules! ambiguity {
($($i:ident)* $j:ident) => { };
}
ambiguity!(error); // 错误: 局部歧义(local ambiguity)
}
3.1.2 元变量
在匹配器中,$
名称:
匹配段选择器 这种句法格式匹配符合指定句法类型的 Rust 句法段,并将其绑定到元变量 $
名称 上。有效的匹配段选择器包括:
item
: 程序项block
: 块表达式stmt
: 语句,注意此选择器不匹配句尾的分号(如果匹配器中提供了分号,会被当做分隔符),但碰到分号是自身的一部分的程序项语句的情况又会匹配。pat
: 模式expr
: 表达式ty
: 类型ident
: 标识符或关键字path
: 类型表达式 形式的路径tt
: token树 (单个 token 或宏匹配定界符()
、[]
或{}
中的标记)meta
: 属性,属性中的内容lifetime
: 生存期tokenvis
: 可能为空的可见性限定符literal
: 匹配-
?字面量表达式
3.1.3 重复元
*
— 表示任意数量的重复元。+
— 表示至少有一个重复元。?
— 表示一个可选的匹配段,可以出现零次或一次。
3.1.4 文本作用域
文本作用域很大程度上取决于宏本身在源文件中的出现顺序,其工作方式与用 let
语句声明的局部变量的作用域类似,只不过它可以直接位于模块下。当使用 macro_rules!
定义宏时,宏在定义之后进入其作用域(请注意,这不影响宏在定义中递归调用自己,因为宏调用的入口还是在定义之后的某次调用点上,此点开始的宏名称递归查找一定有效),在封闭它的作用域(通常是模块)结束时离开。文本作用域可以覆盖/进入子模块,甚至跨越多个文件:
src/lib.rs
mod has_macro {
// m!{} // 报错: m 未在作用域内.
macro_rules! m {
() => {};
}
m!{} // OK: 在声明 m 后使用.
mod uses_macro;
}
// m!{} // Error: m 未在作用域内.
src/has_macro/uses_macro.rs
m!{} // OK: m 在上层模块文件 src/lib.rs 中声明后使用
多次定义宏并不报错;除非超出作用域,否则最近的宏声明将屏蔽前一个。
#![allow(unused)]
fn main() {
macro_rules! m {
(1) => {};
}
m!(1);
mod inner {
m!(1);
macro_rules! m {
(2) => {};
}
// m!(1); // 报错: 没有设定规则来匹配 '1'
m!(2);
macro_rules! m {
(3) => {};
}
m!(3);
}
m!(1);
}
3.1.5 macro_use 属性
macro_use
属性有两种用途。首先,它可以通过作用于模块的方式让模块内的宏的作用域在模块关闭时不结束:
#![allow(unused)]
fn main() {
#[macro_use]
mod inner {
macro_rules! m {
() => {};
}
}
m!();
}
其次,它可以用于从另一个 crate 里来导入宏,方法是将它附加到当前 crate 根模块中的 extern crate
声明前。以这种方式导入的宏会被导入到macro_use预导入包里,而不是直接文本导入,这意味着它们可以被任何其他同名宏屏蔽。虽然可以在导入语句之前使用 #[macro_use]
导入宏,但如果发生冲突,则最后导入的宏将胜出。可以使用可选的 MetaListIdents元项属性句法指定要导入的宏列表;当将 #[macro_use]
应用于模块上时,则不支持此指定操作。
要用 #[macro_use]
导入宏必须先使用 #[macro_export]
导出,下文会有讲解。
#[macro_use(lazy_static)] // 或者使用 #[macro_use] 来导入所有宏.
extern crate lazy_static;
lazy_static!{}
// self::lazy_static!{} // 报错: lazy_static 没在 `self` 中定义
3.16 macro_export 属性
#![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 来引用。
3.1.7 卫生性
默认情况下,宏中引用的所有标识符都按原样展开,并在宏的调用位置上去查找。如果宏引用的程序项或宏不在调用位置的作用域内,则这可能会导致问题。为了解决这个问题,可以替代在路径的开头使用元变量 $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
源码出现的)crate,因此在引用非宏程序项时,它必须与全限定模块路径一起使用:
#![allow(unused)]
fn main() {
pub mod inner {
#[macro_export]
macro_rules! call_foo {
() => { $crate::inner::foo() };
}
pub fn foo() {}
}
}
当一个宏被导出时,可以在 #[macro_export]
属性里添加 local_inner_macros
属性值,用以自动为该属性修饰的宏内包含的所有宏调用自动添加 $crate::
前缀。这主要是作为一个工具来迁移那些在引入 $crate
之前的版本编写的 Rust 代码,以便它们能与 Rust 2018 版中基于路径的宏导入一起工作。在使用新版本编写的代码中不鼓励使用它。
#![allow(unused)]
fn main() {
#[macro_export(local_inner_macros)]
macro_rules! helped {
() => { helper!() } // 自动转码为 $crate::helper!().
}
#[macro_export]
macro_rules! helper {
() => { () }
}
}
3.2 过程宏
过程宏允许在执行函数时创建句法扩展。过程宏有三种形式:
- 类函数宏(function-like macros) -
custom!(...)
- 派生宏(derive macros)-
#[derive(CustomDerive)]
- 属性宏(attribute macros) -
#[CustomAttribute]
可以将过程宏想象成是从一个 AST 到另一个 AST 的函数映射
过程宏必须在 crate 类型为 proc-macro
的 crate 中定义。
注意: 使用 Cargo 时,定义过程宏的 crate 的配置文件里要使用
proc-macro
键做如下设置:[lib] proc-macro = true
过程宏在编译时运行,因此具有与编译器相同的环境资源
过程宏有两种报告错误的方法。首先是 panic;第二个是发布 compile_error 性质的宏调用。
过程宏类型的 crate 几乎总是会去链接编译器提供的 proc_macro crate。proc_macro
crate 提供了编写过程宏所需的各种类型和工具来让编写更容易。
此 crate 主要包含了一个 TokenStream 类型。过程宏其实是在 *token流(token streams)*上操作,而不是在某个或某些 AST 节点上操作,因为这对于编译器和过程宏的编译目标来说,这是一个随着时间推移要稳定得多的接口。token流大致相当于 Vec<TokenTree>
,其中 TokenTree
可以大致视为词法 token。例如,foo
是标识符(Ident
)类型的 token,.
是一个标点符号(Punct
)类型的 token,1.2
是一个字面量(Literal
)类型的 token。不同于 Vec<TokenTree>
的是 TokenStream
的克隆成本很低。
所有类型的 token 都有一个与之关联的 Span
。Span
是一个不透明的值,不能被修改,但可以被制造。Span
表示程序内的源代码范围,主要用于错误报告。可以事先(通过函数 set_span
)配置任何 token 的 Span
。
3.2.1卫生性
过程宏是非卫生的(unhygienic)。这意味着它的行为就好像它输出的 token流是被简单地内联写入它周围的代码中一样。这意味着它会受到外部程序项的影响,也会影响外部导入。
鉴于此限制,宏作者需要小心地确保他们的宏能在尽可能多的上下文中正常工作。这通常包括对库中程序项使用绝对路径(例如,使用 ::std::option::Option
而不是 Option
),或者确保生成的函数具有不太可能与其他函数冲突的名称(如 __internal_foo
,而不是 foo
)。
3.2.2 类函数过程宏
#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
extern crate proc_macro_examples;
use proc_macro_examples::make_answer;
make_answer!();
fn main() {
println!("{}", answer());
}
类函数过程宏可以在任何宏调用位置调用,这些位置包括语句、表达式、模式、类型表达式、程序项可以出现的位置(包括extern块里、固有(inherent)实现里和 trait实现里、以及 trait声明里)。
3.2.3 派生宏
派生宏为派生(derive)属性定义新输入。这类宏在给定输入结构体(struct)、枚举(enum)或联合体(union) token流的情况下创建新程序项。它们也可以定义派生宏辅助属性。
自定义派生宏由带有 proc_macro_derive
属性和 (TokenStream) -> TokenStream
签名的公有可见性函数定义。
定义:
#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_derive(AnswerFn)]
pub fn derive_answer_fn(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
使用:
extern crate proc_macro_examples;
use proc_macro_examples::AnswerFn;
#[derive(AnswerFn)]
struct Struct;
fn main() {
assert_eq!(42, answer());
}
派生宏可以将额外的属性添加到它们所在的程序项的作用域中。这些属性被称为派生宏辅助属性。这些属性是惰性的,它们存在的唯一目的是将这些属性在使用现场获得的属性值反向输入到定义它们的派生宏中。也就是说所有该宏的宏应用都可以看到它们。
#![crate_type="proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_derive(HelperAttr, attributes(helper))]
pub fn derive_helper_attr(_item: TokenStream) -> TokenStream {
TokenStream::new()
}
#[derive(HelperAttr)]
struct Struct {
#[helper] field: ()
}
3.2.4 属性宏
属性宏定义可以附加到程序项上的新的外部属性,这些程序项包括外部(extern)块、固有实现、trate实现,以及 trait声明中的各类程序项。
例如,下面这个属性宏接受输入流并按原样返回,实际上对属性并无操作。
#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn return_as_is(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
下面示例显示了属性宏看到的字符串化的 TokenStream。输出将显示在编译时的编译器输出窗口中。(具体格式是以 "out:"为前缀的)输出内容也都在后面每个示例函数后面的注释中给出了。
// my-macro/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
println!("attr: \"{}\"", attr.to_string());
println!("item: \"{}\"", item.to_string());
item
}
// src/lib.rs
extern crate my_macro;
use my_macro::show_streams;
// 示例: 基础函数
#[show_streams]
fn invoke1() {}
// out: attr: ""
// out: item: "fn invoke1() { }"
// 示例: 带输入参数的属性
#[show_streams(bar)]
fn invoke2() {}
// out: attr: "bar"
// out: item: "fn invoke2() {}"
// 示例: 输入参数中有多个 token 的
#[show_streams(multiple => tokens)]
fn invoke3() {}
// out: attr: "multiple => tokens"
// out: item: "fn invoke3() {}"
// 示例:
#[show_streams { delimiters }]
fn invoke4() {}
// out: attr: "delimiters"
// out: item: "fn invoke4() {}"