过程宏
过程宏 允许创建作为函数执行的语法扩展。过程宏有三种形式:
过程宏允许您在编译时运行代码,这些代码操作 Rust 语法,既可以消费也可以生成 Rust 语法。您可以将过程宏视为从一个 AST 到另一个 AST 的函数。
过程宏必须在 proc-macro
crate 类型的 crate 的根部定义。这些宏不能在其定义的 crate 中使用,只能在导入到另一个 crate 时使用。
注意:使用 Cargo 时,过程宏 crate 在清单中使用
proc-macro
键定义。[lib] proc-macro = true
作为函数,它们必须返回语法、panic 或无限循环。返回的语法要么替换,要么根据过程宏的种类添加语法。Panics 会被编译器捕获并转换为编译器错误。无限循环不会被编译器捕获,这会导致编译器挂起。
过程宏在编译期间运行,因此具有与编译器相同的资源。例如,标准输入、错误和输出与编译器可以访问的相同。类似地,文件访问也相同。因此,过程宏具有与 Cargo 的构建脚本相同的安全问题。
过程宏有两种报告错误的方法。第一种是 panic。第二种是发出 compile_error
宏调用。
proc_macro
crate
过程宏 crate 几乎总是链接到编译器提供的 proc_macro
crate。proc_macro
crate 提供了编写过程宏所需的类型和使其更容易的工具。
此 crate 主要包含一个 TokenStream
类型。过程宏操作的是 token 流 而不是 AST 节点,对于编译器和过程宏来说,这是一个随着时间推移更为稳定的接口。一个 token 流 大致相当于 Vec<TokenTree>
,其中 TokenTree
可以大致认为是词法 token。例如,foo
是一个 Ident
token,.
是一个 Punct
token,1.2
是一个 Literal
token。与 Vec<TokenTree>
不同,TokenStream
类型克隆的开销很小。
所有 token 都有一个关联的 Span
。Span
是一个不能修改但可以制造的不透明值。Span
表示程序中源代码的范围,主要用于错误报告。虽然您不能修改 Span
本身,但您始终可以更改与任何 token 关联 的 Span
,例如通过从另一个 token 获取 Span
。
过程宏卫生
过程宏是 不卫生的。这意味着它们的行为就像输出 token 流只是直接写入它旁边的代码一样。这意味着它会受到外部项的影响,也会影响外部导入。
宏作者需要注意确保他们的宏在给定此限制的情况下尽可能多地在上下文中工作。这通常包括使用库中项的绝对路径(例如,::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
属性定义新的输入。这些宏可以根据 结构体、枚举或 联合的 token 流创建新的 项。它们还可以定义 派生宏辅助属性。
自定义派生宏由具有 proc_macro_derive
属性和 (TokenStream) -> TokenStream
签名的 公共 函数 定义。
proc_macro_derive
属性在 crate 的根部的 宏命名空间中定义自定义派生。
输入 TokenStream
是具有 derive
属性的项的 token 流。输出 TokenStream
必须是一组项,然后将其附加到输入 TokenStream
中的项所在的 模块或 代码块中。
以下是派生宏的示例。它没有对其输入做任何有用的事情,只是附加了一个函数 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());
}
派生宏辅助属性
派生宏可以将额外的 属性 添加到它们所在的 项 的作用域中。所述属性称为 派生宏辅助属性。这些属性是 惰性的,它们的唯一目的是馈送到定义它们的派生宏中。也就是说,它们可以被所有宏看到。
定义辅助属性的方法是在 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: ()
}
属性宏
属性宏 定义新的 外部属性,这些属性可以附加到 项,包括 extern
代码块中的项、固有和 trait 实现以及 trait 定义。
属性宏由具有 proc_macro_attribute
属性 的 公共 函数 定义,该函数具有 (TokenStream, TokenStream) -> TokenStream
签名。第一个 TokenStream
是属性名称后面的分隔 token 树,不包括外部分隔符。如果该属性被写为裸属性名称,则属性 TokenStream
为空。第二个 TokenStream
是 项 的其余部分,包括 项 上的其他 属性。返回的 TokenStream
用任意数量的 项 替换 项。
proc_macro_attribute
属性在 crate 的根部的 宏命名空间中定义属性。
例如,此属性宏获取输入流并将其按原样返回,实际上是属性的空操作。
#![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() {}"
声明性宏 token 和过程宏 token
声明性 macro_rules
宏和过程宏使用类似但不同的 token 定义(更确切地说,是 TokenTree
。)
macro_rules
中的 token 树(对应于 tt
匹配器)定义为
- 分隔组(
(...)
、{...}
等) - 该语言支持的所有运算符,包括单字符和多字符运算符(
+
、+=
)。- 请注意,此集合不包括单引号
'
。
- 请注意,此集合不包括单引号
- 字面量(
"string"
、1
等)- 请注意,否定(例如
-1
)永远不是此类字面量 token 的一部分,而是一个单独的运算符 token。
- 请注意,否定(例如
- 标识符,包括关键字(
ident
、r#ident
、fn
) - 生命周期(
'ident
) macro_rules
中的元变量替换(例如,在mac
扩展之后,macro_rules! mac { ($my_expr: expr) => { $my_expr } }
中的$my_expr
,无论传递的表达式如何,都将被视为单个 token 树)
过程宏中的 token 树定义为
- 分隔组(
(...)
、{...}
等) - 该语言支持的运算符中使用的所有标点字符(
+
,但不包括+=
),以及单引号'
字符(通常用于生命周期,请参阅下面的生命周期拆分和加入行为) - 字面量(
"string"
、1
等)- 否定(例如
-1
)作为整数和浮点字面量的一部分受支持。
- 否定(例如
- 标识符,包括关键字(
ident
、r#ident
、fn
)
当 token 流在过程宏之间传递时,会考虑这两个定义之间的不匹配。
请注意,下面的转换可能会延迟发生,因此如果实际上没有检查 token,则它们可能不会发生。
传递给 proc-macro 时
- 所有多字符运算符都分解为单字符。
- 生命周期被分解为一个
'
字符和一个标识符。 - 所有元变量替换都表示为其底层的 token 流。
- 当需要保留解析优先级时,此类 token 流可以使用隐式分隔符(
Delimiter::None
)包装到分隔组(Group
)中。 tt
和ident
替换永远不会被包裹在这种组中,并且总是以其底层的 token 树表示。
- 当需要保留解析优先级时,此类 token 流可以使用隐式分隔符(
当从过程宏发出时
- 标点符号在适用时会粘合成多字符运算符。
- 单引号
'
与标识符连接时会粘合成生命周期。 - 负字面量会被转换为两个 token(
-
和字面量),如果需要保持解析优先级,可能会被包裹在带有隐式分隔符(Delimiter::None
)的定界组(Group
)中。
请注意,声明宏和过程宏都不支持文档注释 token (例如 /// Doc
),因此当传递给宏时,它们总是被转换为表示其等效的 #[doc = r"str"]
属性的 token 流。