过程宏
过程宏允许通过执行函数来创建语法扩展。过程宏有以下三种类型:
过程宏允许你在编译时运行代码,这些代码操作 Rust 语法,既消费也生成 Rust 语法。你可以大致将过程宏视为从一个 AST 到另一个 AST 的函数。
过程宏必须在 crate 的根目录下定义,其crate 类型为 proc-macro
。这些宏不能在其定义的 crate 内部使用,只能在导入到其他 crate 后才能使用。
注意
使用 Cargo 时,过程宏 crate 在你的 manifest 文件中通过
proc-macro
键来定义。[lib] proc-macro = true
作为函数,它们必须要么返回语法,要么 panic(崩溃),要么无限循环。返回的语法会根据过程宏的类型替换或添加原有的语法。Panic 会被编译器捕获并转化为编译错误。无限循环不会被编译器捕获,这会导致编译器挂起。
过程宏在编译期间运行,因此拥有与编译器相同的资源。例如,标准输入、错误和输出与编译器可访问的相同。类似地,文件访问也相同。正因为如此,过程宏与 Cargo 的构建脚本具有相同的安全顾虑。
过程宏有两种报告错误的方式。第一种是 panic(崩溃)。第二种是发出 compile_error
宏调用。
proc_macro
crate
过程宏 crate 几乎总是会链接到编译器提供的 proc_macro
crate。proc_macro
crate 提供了编写过程宏所需的类型以及使其更便捷的工具。
这个 crate 主要包含一个 TokenStream
类型。过程宏操作的是标记流(token stream)而不是 AST 节点,这对于编译器和过程宏来说都是一个随时间推移更为稳定的接口。一个标记流大致等同于 Vec<TokenTree>
,其中 TokenTree
可以大致被认为是词法标记。例如,foo
是一个 Ident
标记,.
是一个 Punct
标记,1.2
是一个 Literal
标记。TokenStream
类型,与 Vec<TokenTree>
不同,克隆起来开销很小。
所有标记都关联着一个 Span
。Span
是一个不透明的值,不能被修改但可以被创建。Span
代表了程序中一段源代码的范围,主要用于错误报告。虽然你不能修改 Span
本身,但你总可以改变与任何标记关联的 Span
,例如通过从另一个标记获取一个 Span
。
过程宏的卫生性
过程宏是不卫生的。这意味着它们的行为就像输出标记流直接内联到其旁边的代码一样。这表示它受外部项的影响,也会影响外部导入。
鉴于此限制,宏作者需要小心确保他们的宏能在尽可能多的上下文中使用。这通常包括使用库中项的绝对路径(例如,使用 ::std::option::Option
而不是 Option
),或者确保生成的函数名不太可能与其他函数冲突(例如使用 __internal_foo
而不是 foo
)。
函数式过程宏
函数式过程宏是使用宏调用运算符 (!
) 来调用的过程宏。
这些宏通过带有 proc_macro
属性且签名类型为 (TokenStream) -> TokenStream
的公共函数来定义。输入 TokenStream
是宏调用分隔符内部的内容,输出 TokenStream
会替换整个宏调用。
proc_macro
属性在 crate 的根目录下将宏定义在宏命名空间中。
例如,下面的宏定义忽略其输入,并在其作用域内输出一个函数 answer
。
#![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()
}
然后我们在一个二进制 crate 中使用它来将“42”打印到标准输出。
extern crate proc_macro_examples;
use proc_macro_examples::make_answer;
make_answer!();
fn main() {
println!("{}", answer());
}
函数式过程宏可以在任何宏调用位置被调用,包括语句、表达式、模式、类型表达式、项位置,包括 extern
块中的项、固有和 trait 实现,以及 trait 定义。
派生宏
派生宏为 derive
属性定义新的输入。这些宏可以根据结构体(struct)、枚举(enum)或联合体(union)的标记流创建新的项。它们还可以定义派生宏辅助属性。
自定义派生宏通过带有 proc_macro_derive
属性且签名类型为 (TokenStream) -> TokenStream
的公共函数来定义。
proc_macro_derive
属性在 crate 的根目录下将自定义派生定义在宏命名空间中。
输入 TokenStream
是带有 derive
属性的项的标记流。输出 TokenStream
必须是一组项,这些项随后被附加到输入 TokenStream
中的项所在的模块(module)或块(block)中。
以下是一个派生宏的示例。它没有对输入做任何有用的事情,只是附加了一个函数 answer
。
#![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());
}
派生宏辅助属性
派生宏可以在其所在的项的作用域内添加额外的属性。这些属性被称为派生宏辅助属性。这些属性是惰性的(inert),它们唯一的目的是提供给定义它们的派生宏。不过,它们可以被所有宏看到。
定义辅助属性的方法是在 proc_macro_derive
宏中放置一个 attributes
键,其值为一个逗号分隔的标识符列表,这些标识符就是辅助属性的名称。
例如,下面的派生宏定义了一个辅助属性 helper
,但最终没有使用它。
#![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: ()
}
属性宏
属性宏定义了新的外部属性(outer attributes),这些属性可以附加到项上,包括 extern
块中的项、固有和 trait 实现,以及 trait 定义。
属性宏通过带有 proc_macro_attribute
属性且签名类型为 (TokenStream, TokenStream) -> TokenStream
的公共函数来定义。第一个 TokenStream
是属性名后面带分隔符的标记树,不包括外部分隔符。如果属性只写了属性名而没有括号,则属性 TokenStream
为空。第二个 TokenStream
是剩下的项,包括项上的其他属性。返回的 TokenStream
会用任意数量的项替换原有的项。
proc_macro_attribute
属性在 crate 的根目录下将属性定义在宏命名空间中。
例如,这个属性宏接受输入的标记流并原样返回,实际上是一个无操作(no-op)属性。
#![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}\"");
println!("item: \"{item}\"");
item
}
// src/lib.rs
extern crate my_macro;
use my_macro::show_streams;
// Example: Basic function
#[show_streams]
fn invoke1() {}
// out: attr: ""
// out: item: "fn invoke1() {}"
// Example: Attribute with input
#[show_streams(bar)]
fn invoke2() {}
// out: attr: "bar"
// out: item: "fn invoke2() {}"
// Example: Multiple tokens in the input
#[show_streams(multiple => tokens)]
fn invoke3() {}
// out: attr: "multiple => tokens"
// out: item: "fn invoke3() {}"
// Example:
#[show_streams { delimiters }]
fn invoke4() {}
// out: attr: "delimiters"
// out: item: "fn invoke4() {}"
声明宏标记和过程宏标记
声明式 macro_rules
宏和过程宏对标记(或者更确切地说,TokenTree
)使用相似但不同的定义。
macro_rules
中的标记树(对应于 tt
匹配器)定义如下:
- 带分隔符的组 (
(...)
,{...}
等) - 语言支持的所有运算符,包括单字符和多字符的 (
+
,+=
)。- 注意,这个集合不包含单引号
'
。
- 注意,这个集合不包含单引号
- 字面量 (
"string"
,1
等)- 注意,负号(例如
-1
)绝不是此类字面量标记的一部分,而是一个单独的运算符标记。
- 注意,负号(例如
- 标识符,包括关键字 (
ident
,r#ident
,fn
) - 生命周期 (
'ident
) macro_rules
中的元变量替换(例如,在macro_rules! mac { ($my_expr: expr) => { $my_expr } }
中,mac
扩展后的$my_expr
,无论传入的表达式是什么,都将被视为一个单独的标记树)
过程宏中的标记树定义如下:
- 带分隔符的组 (
(...)
,{...}
等) - 语言支持的运算符中使用的所有标点符号(例如
+
,但不包括+=
),以及单引号'
字符(通常用于生命周期,有关生命周期拆分和合并行为请参见下文) - 字面量 (
"string"
,1
等)- 负号(例如
-1
)作为整数和浮点数字面量的一部分被支持。
- 负号(例如
- 标识符,包括关键字 (
ident
,r#ident
,fn
)
在标记流传递进出过程宏时,会考虑这两种定义之间的不匹配之处。
注意,以下转换可能是惰性发生的,因此如果标记实际上没有被检查,转换可能不会发生。
当传递给过程宏时
- 所有多字符运算符都被分解成单字符。
- 生命周期被分解成一个
'
字符和一个标识符。 - 所有元变量替换都表示为其底层的标记流。
- 当需要保留解析优先级时,此类标记流可能会被包裹在带有隐式分隔符(
Delimiter::None
)的带分隔符的组(Group
)中。 tt
和ident
替换绝不会被包裹在此类组中,而是始终表示为其底层的标记树。
- 当需要保留解析优先级时,此类标记流可能会被包裹在带有隐式分隔符(
当从过程宏发出时
- 适用时,标点符号会被拼接成多字符运算符。
- 单引号
'
与标识符拼接时,会被合并成生命周期。 - 当需要保留解析优先级时,负数字面量会被转换为两个标记(
-
和字面量),并可能被包裹在带有隐式分隔符(Delimiter::None
)的带分隔符的组(Group
)中。
注意,声明宏和过程宏都不支持文档注释标记(例如 /// Doc
),因此当它们传递给宏时,总是会被转换为表示其等效的 #[doc = r"str"]
属性的标记流。