过程宏

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

过程宏允许您在编译时运行代码,这些代码操作 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 crateproc_macro crate 提供了编写过程宏所需的类型和使其更容易的工具。

此 crate 主要包含一个 TokenStream 类型。过程宏操作的是 token 流 而不是 AST 节点,对于编译器和过程宏来说,这是一个随着时间推移更为稳定的接口。一个 token 流 大致相当于 Vec<TokenTree>,其中 TokenTree 可以大致认为是词法 token。例如,foo 是一个 Ident token,. 是一个 Punct token,1.2 是一个 Literal token。与 Vec<TokenTree> 不同,TokenStream 类型克隆的开销很小。

所有 token 都有一个关联的 SpanSpan 是一个不能修改但可以制造的不透明值。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。
  • 标识符,包括关键字(identr#identfn
  • 生命周期('ident
  • macro_rules 中的元变量替换(例如,在 mac 扩展之后,macro_rules! mac { ($my_expr: expr) => { $my_expr } } 中的 $my_expr,无论传递的表达式如何,都将被视为单个 token 树)

过程宏中的 token 树定义为

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

当 token 流在过程宏之间传递时,会考虑这两个定义之间的不匹配。
请注意,下面的转换可能会延迟发生,因此如果实际上没有检查 token,则它们可能不会发生。

传递给 proc-macro 时

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

当从过程宏发出时

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

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