过程宏

过程宏 是以可执行函数的形式创建语法扩展。 过程宏有三种添加形式:

  • [函数宏] - custom!(...)
  • [衍生宏] - #[derive(CustomDerive)]
  • [属性宏] - #[CustomAttribute]

过程宏是在编译时运行代码,对 Rust 语法进行操作,输入语法,而产生新的语法。 你可以把过程宏想象成,把一段代码片段转换为另一段代码片段的函数。

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

注意: 在使用 Cargo 时,可用 crate 配置清单中的 proc-macro 键来定义。

[lib]
proc-macro = true

这个特殊的函数,要么返回语法,要么恐慌,要么死循环。 返回语法时,至于是替换语法,还是增加语法,取决于过程宏的类型。 编译器能够捕获过程宏的恐慌,并变成编译器错误。 编译器无法捕获死循环,将导致编译器挂起。

过程宏在编译过程中运行,类似于编译器的脚本,因而拥有与编译器相同的资源。 例如,标准输入输出及错误与编译器所能访问的相同。 同样地,文件系统的访问也是一样的。 因而,过程宏与 Cargo 构建脚本 有同样的安全问题。

过程宏有两种报告错误的方式。一是恐慌。 二是触发 compile_error 宏调用。

proc_macro crate

过程宏 crate 往往需要链接编译器提供的 proc_macro crate ,其提供了编写过程宏所需的相关类型和功能,使编写过程宏更加容易。

该 crate 包含的主要类型为 TokenStream 。 从而编写过程宏时可以操作 token 流 ,而不是 AST 节点,这更加便捷和稳定。 token 流 大致相当于 Vec<TokenTree> ,其中 TokenTree 可以大致认为是词法 Token 。 例如 fooIdent Token, .Punct Token,而 1.2Literal Token。 与 Vec<TokenTree> 不同,TokenStream 类型的克隆更轻便。

所有 Token 都有一个相关联的 Span 值,该值是不透明的,不能修改,但可以创建。 该值表示源码中一个作用域,作用主要是用于错误报告。 虽然不能直接修改,但 Token 可以改变与之 关联Span ,比如从另一个 Token 获得一个 Span

过程宏卫生性

过程宏是 unhygienic "非卫生" 的,行为简单,像是直接把输出 Token 流写入到了其围绕的代码中一样。 意味着,会受到外部条目的影响,也会影响到外部导入部分。

因为有这个限制,宏作者需要非常小心,以保证宏能在更多情况下工作。 通常需要: 使用库中条目的绝对路径 (例如 ::std::option::Option 而不是 Option ) , 或者确保生成的函数名称不太可能与其他函数冲突 (例如 __internal_foo 而不是 foo ) 。

函数式过程宏

函数式过程宏 使用宏调用操作符 (!) 来调用。

这些宏是由一个具有 proc_macro 属性(TokenStream) -> TokenStream 函数签名的 pub 函数 定义的。 函数输入 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()
}

然后可在 crate 中使用该宏后,打印 "42" 到标准输出。

extern crate proc_macro_examples;
use proc_macro_examples::make_answer;

make_answer!();

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

函数式过程宏可以处在任意宏调用位置,其中包括 语句表达式模式类型表达式条目 , 包括 extern 中的条目,内部及trait 实现 ,以及 trait 定义

衍生宏

衍生宏derive 属性 定义了新的输入项。 可以为 结构体枚举联合体 创建新的 条目 。 也可以定义 衍生宏辅助属性

衍生宏可在 pub 函数 附加 proc_macro_derive 属性来定义,函数签名为 (TokenStream) -> TokenStream

函数输入 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 属性pub 函数 来定义的,其函数签名为 (TokenStream, TokenStream) -> TokenStream 。 第一个 TokenStream 是属性名称后定界符号中的 token 树,不包括外部定界符号。 如果属性仅为名称,则该 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
}

下面这个例子展示了,通过属性宏得到的字符串化的 TokenStreams 。 运行后,其输出内容展示在以 "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.to_string());
    println!("item: \"{}\"", item.to_string());
    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 规格类似,但也有所不同。

macro_rules 中的 Token 树 (对应于 tt 匹配器) 被定义为

  • 定界符号的组 ((...), {...}, 等) 。
  • 语言支持的所有运算符,包括单字符和多字符运算符 (+, +=) 。
    • 请注意,这个集合不包括单引号 '
  • 字面值 ("string", 1, 等) 。
    • 请注意,负 (如 -1) 并不是这类字面值 Token 的一部分,而是单独的 Token 。
  • 标识符,包括关键字 (ident, r#ident, fn)
  • 生命周期 ( 'ident )
  • macro_rules 中元变量将替换 (如 $my_exprmacro_rules! mac { ($my_expr: expr) => { $my_expr } }mac 的展开之后,无论传递的表达式是什么,都会被认为是单一的 Token 树 )

过程宏中的 Token 树被定义为

  • 定界符号的组 ((...), {...}, 等)
  • 语言支持的运算符中使用的所有标点符号 (+ ,但不是 +=),还有单引号 ' 字符 (通常用在生命周期上,关于生命周期的分割和连接行为见下文)
  • 字面值 ("string", 1, 等)
    • 支持负 (如 -1 ) 作为整数和浮点数字面值的一部分。
  • 标识符,包括关键字 (ident, r#ident, fn)

当 Token 流被传递到过程宏时,会考虑这两个定义之间不匹配的情况。 请注意,下面的转换可能会产生惰性,所以如果没有实际检验 Token ,就可能不会发生。

传递给过程宏时

  • 所有的多字符运算符都分解成单字符。
  • 生命周期分解成一个 ' 字符和一个标识符。
  • 所有元变量的替换都表示为它们实际的 Token 流。
    • 当需要保留语法分析的优先级时,这样的 Token 流可以被包装成带有隐式定界符号 ([Delimiter::None]) 的定界组 ([Group]) 。
    • ttident 的替换从来不会被包装在这样的组中,而总是作为它们的实际的 Token 树来表示。

当从过程宏触发时

  • 在适用的情况下,标点符号被粘接在多字符运算符中。
  • 与标识符连接的单引号 ' 被粘接在生命周期中。
  • 负字面值被转换为两个标记 ( - 和字面值 ),当需要保留语法分析的优先级时,可能会被包裹在一个带定界符号的组 (Group) 中,并带有隐式定界符号 (Delimiter::None) 。

请注意,无论是声明性的还是过程性的宏都不支持 doc 注释标记 (例如 /// Doc ) ,当其传递给宏时,总是转换为表示其等价的 #[doc = r"str"] 属性的 Token 流。