过程宏
过程宏允许将语法扩展创建为函数的执行。过程宏有三种形式:
过程宏允许您在编译时运行代码,这些代码操作 Rust 语法,既消费又产生 Rust 语法。您可以将过程宏视为从一个 AST 到另一个 AST 的函数。
过程宏必须在 crate 类型为 proc-macro 的 crate 根中定义。这些宏不能在定义它们的 crate 中使用,只能在导入到另一个 crate 时使用。
Note
使用 Cargo 时,过程宏 crate 在清单中用
proc-macro键定义:[lib] proc-macro = true
作为函数,它们必须返回语法、panic 或无限循环。返回的语法根据过程宏的种类替换或添加语法。Panic 被编译器捕获并转换为编译器错误。无限循环不会被编译器捕获,这会挂起编译器。
过程宏在编译期间运行,因此拥有与编译器相同的资源。例如,标准输入、错误和输出与编译器可以访问的相同。类似地,文件访问也相同。因此,过程宏与 Cargo 的构建脚本具有相同的安全问题。
过程宏有两种报告错误的方式。第一种是 panic。第二种是发出 compile_error 宏调用。
proc_macro crate
过程宏 crate 几乎总是链接到编译器提供的 proc_macro crate。proc_macro crate 提供了编写过程宏所需的类型和使其更容易的工具。
此 crate 主要包含 TokenStream 类型。过程宏操作词法单元流而不是 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 属性
Example
此宏定义忽略其输入并在其作用域中发出函数
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()); }
proc_macro 属性使用 MetaWord 语法。
proc_macro 属性只能应用于类型为 fn(TokenStream) -> TokenStream 的 pub 函数,其中 TokenStream 来自 proc_macro crate。它必须具有“Rust” ABI。不允许使用其他函数限定符。它必须位于 crate 的根中。
proc_macro 属性只能在函数上指定一次。
proc_macro 属性在 crate 根的宏命名空间中以与函数相同的名称公开定义宏。
函数式过程宏的函数式宏调用将宏调用分隔符内的内容作为输入 TokenStream 参数传递,并用函数的输出 TokenStream 替换整个宏调用。
函数式过程宏可以在任何宏调用位置调用,包括:
proc_macro_derive 属性
将 proc_macro_derive [属性] 应用于函数会定义一个派生宏,可以通过 [derive 属性]调用。这些宏被给予结构体、枚举或联合体定义的词法单元流,并可以在其后发出新的项。它们还可以声明和使用派生宏辅助属性。
Example
此派生宏忽略其输入并附加定义函数的词法单元。
#![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 属性的语法是:
Syntax
ProcMacroDeriveAttribute →
proc_macro_derive ( DeriveMacroName ( , DeriveMacroAttributes )? ,? )
DeriveMacroAttributes →
attributes ( ( IDENTIFIER ( , IDENTIFIER )* ,? )? )
派生宏的名称由 DeriveMacroName 给出。可选的 attributes 参数在 macro.proc.derive.attributes 中描述。
proc_macro_derive 属性只能应用于在 crate 根中定义的具有 Rust ABI 的 pub 函数,类型为 fn(TokenStream) -> TokenStream,其中 TokenStream 来自 proc_macro crate。函数可以是 const,可以使用 extern 显式指定 Rust ABI,但不能使用任何其他限定符(例如,不能是 async 或 unsafe)。
proc_macro_derive 属性只能在函数上使用一次。
proc_macro_derive 属性在 crate 根的宏命名空间中公开定义派生宏。
输入 TokenStream 是应用 derive 属性的项的词法单元流。输出 TokenStream 必须是(可能为空的)项集合。这些项在输入项之后附加到同一模块或块中。
派生宏辅助属性
派生宏可以声明派生宏辅助属性,以在派生宏应用的项的作用域内使用。这些属性是惰性的。虽然它们的目的是由声明它们的宏使用,但任何宏都可以看到它们。
派生宏的辅助属性通过将其标识符添加到 proc_macro_derive 属性中的 attributes 列表来声明。
Example
此示例声明辅助属性然后忽略它。
#![crate_type="proc-macro"] extern crate proc_macro; use proc_macro::TokenStream; #[proc_macro_derive(WithHelperAttr, attributes(helper))] pub fn derive_with_helper_attr(_item: TokenStream) -> TokenStream { TokenStream::new() }要使用它,我们可以编写:
#[derive(WithHelperAttr)] struct Struct { #[helper] field: (), }
当派生宏调用应用于项时,该派生宏引入的辅助属性在以下情况下进入作用域:1)应用于该项且在派生宏调用之后词法出现的属性,以及 2)应用于项内字段和变体的属性。
Note
rustc 目前允许在引入它们的宏之前使用派生辅助。这种无序使用的派生辅助不能遮蔽其他属性宏。此行为已弃用并计划移除。
#[helper] // Deprecated, hard error in the future. #[derive(WithHelperAttr)] struct Struct { field: (), }有关详细信息,请参阅 Rust issue #79202。
proc_macro_attribute 属性
proc_macro_attribute 属性 定义了一个属性宏,可用作[外部属性][outer attributes]。
Example
此属性宏接受输入流并按原样发出它,实际上是无操作属性。
#![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 }
Example
此示例在编译器输出中显示属性宏看到的字符串化
TokenStream。// 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: Delimiters in the input. #[show_streams { delimiters }] fn invoke4() {} // out: attr: "delimiters" // out: item: "fn invoke4() {}"
proc_macro_attribute 属性使用 MetaWord 语法。
proc_macro_attribute 属性只能应用于类型为 fn(TokenStream, TokenStream) -> TokenStream 的 pub 函数,其中 TokenStream 来自 proc_macro crate。它必须具有“Rust” ABI。不允许使用其他函数限定符。它必须位于 crate 的根中。
proc_macro_attribute 属性只能在函数上指定一次。
proc_macro_attribute 属性在 crate 根的宏命名空间中以与函数相同的名称定义属性。
属性宏只能用于:
第一个 TokenStream 参数是属性名称之后但不包括外部分隔符的分隔词法单元树。如果应用的属性仅包含属性名称或属性名称后跟空分隔符,则 TokenStream 为空。
第二个 TokenStream 是项的其余部分,包括项上的其他属性。
应用属性的项被返回的 TokenStream 中的零个或多个项替换。
声明宏词法单元和过程宏词法单元
声明宏 macro_rules 和过程宏使用相似但不同的词法单元定义(或者更准确地说是 [TokenTree])。
macro_rules 中的词法单元树(对应于 tt 匹配器)定义为
- 分隔组(
(...)、{...}等) - 语言支持的所有运算符,包括单字符和多字符(
+、+=)。- 请注意,此集合不包括单引号
'。
- 请注意,此集合不包括单引号
- 字面量(
"string"、1等)- 请注意,取反(例如
-1)从来不是此类字面量词法单元的一部分,而是单独的运算符词法单元。
- 请注意,取反(例如
- 标识符,包括关键字(
ident、r#ident、fn) - 生命周期(
'ident) macro_rules中的元变量替换(例如,在mac展开后macro_rules! mac { ($my_expr: expr) => { $my_expr } }中的$my_expr,无论传递的表达式如何,都将被视为单个词法单元树)
过程宏中的词法单元树定义为
- 分隔组(
(...)、{...}等) - 语言支持的运算符中使用的所有标点字符(
+,但不是+=),以及单引号'字符(通常用于生命周期,有关生命周期拆分和连接行为,请参见下文) - 字面量(
"string"、1等)- 支持取反(例如
-1)作为整数和浮点字面量的一部分。
- 支持取反(例如
- 标识符,包括关键字(
ident、r#ident、fn)
当词法单元流传递到过程宏和从过程宏传递时,会考虑这两个定义之间的不匹配。请注意,下面的转换可能延迟发生,因此如果词法单元实际上未被检查,它们可能不会发生。
传递到 proc-macro 时
- 所有多字符运算符都被拆分为单个字符。
- 生命周期被拆分为
'字符和标识符。 - 关键字元变量
$crate作为单个标识符传递。 - 所有其他元变量替换都表示为其底层词法单元流。
- 当需要保留解析优先级时,此类词法单元流可能被包装到具有隐式分隔符(
Delimiter::None)的分隔组(Group)中。 tt和ident替换永远不会被包装到此类组中,始终表示为其底层词法单元树。
- 当需要保留解析优先级时,此类词法单元流可能被包装到具有隐式分隔符(
从 proc 宏发出时
- 当适用时,标点字符被粘合成多字符运算符。
- 与标识符连接的单引号
'被粘合成生命周期。 - 负字面量被转换为两个词法单元(
-和字面量),当需要保留解析优先级时,可能被包装到具有隐式分隔符(Delimiter::None)的分隔组(Group)中。
请注意,声明宏和过程宏都不支持文档注释词法单元(例如 /// Doc),因此在传递给宏时,它们总是被转换为表示其等效 #[doc = r"str"] 属性的词法单元流。