过程宏

过程宏 允许通过执行函数来创建语法扩展。过程宏有三种形式:

过程宏允许你在编译时运行代码,这些代码可以操作 Rust 语法,既可以消费 Rust 语法,也可以生成 Rust 语法。你可以将过程宏看作是从一个 AST 到另一个 AST 的函数。

过程宏必须在 包类型proc-macro 的包中定义。

注意:使用 Cargo 时,过程宏包在清单中的 proc-macro 键中定义。

[lib]
proc-macro = true

作为函数,它们必须返回语法、panic 或无限循环。返回的语法将替换或添加语法,具体取决于过程宏的类型。Panic 会被编译器捕获并转换为编译器错误。无限循环不会被编译器捕获,这会导致编译器挂起。

过程宏在编译期间运行,因此拥有与编译器相同的资源。例如,标准输入、错误和输出与编译器可以访问的相同。类似地,文件访问也是相同的。因此,过程宏与 Cargo 的构建脚本 具有相同的安全问题。

过程宏有两种报告错误的方式。第一种是 panic。第二种是发出 compile_error 宏调用。

proc_macro

过程宏包几乎总是会链接到编译器提供的 proc_macroproc_macro 包提供了编写过程宏所需的类型和便利设施。

该包主要包含一个 TokenStream 类型。过程宏操作的是 *词法单元流* 而不是 AST 节点,这对于编译器和过程宏的目标来说,随着时间的推移,是一个更加稳定的接口。*词法单元流* 大致等效于 Vec<TokenTree>,其中 TokenTree 可以粗略地认为是词法单元。例如,foo 是一个 Ident 词法单元,. 是一个 Punct 词法单元,而 1.2 是一个 Literal 词法单元。与 Vec<TokenTree> 不同,TokenStream 类型的克隆成本很低。

所有词法单元都有一个关联的 SpanSpan 是一个不透明的值,不能修改但可以制造。Span 表示程序中源代码的范围,主要用于错误报告。虽然你不能修改 Span 本身,但你始终可以更改与任何词法单元 *关联* 的 Span,例如通过从另一个词法单元获取 Span

过程宏的卫生性

过程宏是 *不卫生的*。这意味着它们的行为就像输出词法单元流只是简单地内联写入到它旁边的代码中一样。这意味着它会受到外部项目的影响,也会影响外部导入。

宏作者需要注意,鉴于此限制,要确保他们的宏在尽可能多的上下文中工作。这通常包括使用库中项目的绝对路径(例如,使用 ::std::option::Option 而不是 Option)或确保生成的函数具有不太可能与其他函数冲突的名称(例如使用 __internal_foo 而不是 foo)。

类似函数的过程宏

类似函数的过程宏 是使用宏调用运算符(!)调用的过程宏。

这些宏由带有 proc_macro 属性公共 函数 定义,其签名为 (TokenStream) -> TokenStream。输入 TokenStream 是宏调用分隔符内的内容,输出 TokenStream 替换整个宏调用。

例如,以下宏定义忽略其输入并输出一个函数 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()
}

然后我们在一个二进制包中使用它来将 "42" 打印到标准输出。

extern crate proc_macro_examples;
use proc_macro_examples::make_answer;

make_answer!();

fn main() {
    println!("{}", answer());
}

类似函数的过程宏可以在任何宏调用位置调用,包括 语句表达式模式类型表达式 位置,包括 extern 中的项、固有和特征 实现 以及 特征定义

派生宏

派生宏derive 属性 定义新的输入。这些宏可以根据 结构体枚举联合体 的标记流创建新的 。它们还可以定义 派生宏辅助属性

自定义派生宏由带有 proc_macro_derive 属性且签名为 (TokenStream) -> TokenStream公共 函数 定义。

输入 TokenStream 是具有 derive 属性的项的标记流。输出 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 中的项、固有和特征 实现 以及 特征定义

属性宏由带有 proc_macro_attribute 属性公共 函数 定义,该函数的签名为 (TokenStream, TokenStream) -> TokenStream。第一个 TokenStream 是属性名称后面的带分隔符的标记树,不包括外部分隔符。如果属性写成一个简单的属性名称,则属性 TokenStream 为空。第二个 TokenStream 的其余部分,包括 上的其他 属性。返回的 TokenStream 替换为任意数量的

例如,此属性宏获取输入流并按原样返回,实际上是属性的无操作。

#![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)永远不是此类字面量标记的一部分,而是单独的运算符标记。
  • 标识符,包括关键字(identr#identfn
  • 生命周期('ident
  • macro_rules 中的元变量替换(例如,macro_rules! mac { ($my_expr: expr) => { $my_expr } } 中的 $my_exprmac 扩展之后,无论传递的表达式是什么,都将被视为单个标记树)

过程宏中的标记树定义为

  • 带分隔符的组((...){...} 等)
  • 语言支持的运算符中使用的所有标点符号(+,但不是 +=),以及单引号 ' 字符(通常用于生命周期,有关生命周期的拆分和连接行为,请参见下文)
  • 字面量("string"1 等)
    • 支持否定(例如 -1)作为整数和浮点字面量的一部分。
  • 标识符,包括关键字(identr#identfn

在将标记流传递到过程宏和从过程宏传递标记流时,会考虑这两个定义之间的不匹配。

请注意,以下转换可能会延迟发生,因此如果实际上没有检查标记,则可能不会发生。

传递给过程宏时

  • 所有多字符运算符都被分解为单个字符。
  • 生命周期被分解为一个 ' 字符和一个标识符。
  • 所有元变量替换都表示为其底层标记流。
    • 当需要保留解析优先级时,此类标记流可能会被包装到具有隐式分隔符(Delimiter::None)的带分隔符的组(Group)中。
    • ttident 替换永远不会被包装到此类组中,而是始终表示为其底层标记树。

从过程宏发出时

  • 标点符号在适用时粘贴到多字符运算符中。
  • 与标识符连接的单引号 ' 被粘贴到生命周期中。
  • 负字面量被转换为两个标记(- 和字面量),并在需要保留解析优先级时可能会被包装到具有隐式分隔符(Delimiter::None)的带分隔符的组(Group)中。

请注意,声明性宏和过程宏都不支持文档注释标记(例如 /// Doc),因此在将它们传递给宏时,它们始终会被转换为表示其等效 #[doc = r"str"] 属性的标记流。