过程宏
过程宏允许通过执行函数来创建语法扩展。 过程宏分为三种类型:
过程宏允许你在编译时运行操作Rust语法的代码,既可以消费Rust语法,也可以生成Rust语法。你可以将过程宏视为从一个AST到另一个AST的函数。
过程宏必须定义在crate类型为proc-macro的crate根目录中。
宏不能在其定义的crate中使用,只能在另一个crate中导入后才能使用。
注意
当使用Cargo时,过程宏crate通过清单中的
proc-macro键定义:[lib] proc-macro = true
作为函数,它们必须返回语法、恐慌,或无限循环。返回的语法会根据过程宏的种类替换或添加语法。编译器会捕获恐慌,并将其转换为编译器错误。无限循环不会被编译器捕获,这会导致编译器挂起。
过程宏在编译期间运行,因此拥有与编译器相同的资源。例如,标准输入、错误和输出与编译器可访问的相同。同样,文件访问也相同。因此,过程宏具有与Cargo的构建脚本相同的安全问题。
过程宏有两种报告错误的方式。第一种是恐慌。第二种是发出一个compile_error宏调用。
proc_macro crate
过程宏crate几乎总是会链接到编译器提供的proc_macro crate。proc_macro crate提供了编写过程宏所需的类型和使其更方便的工具。
这个crate主要包含词法单元流类型。过程宏操作词法单元流而不是AST节点,这对于编译器和过程宏来说都是一个更稳定的长期接口。词法单元流大致等同于Vec<TokenTree>,其中TokenTree大致可以被认为是词法单元。例如,foo是一个Ident词法单元,.是一个Punct词法单元,1.2是一个Literal词法单元。词法单元流类型与Vec<TokenTree>不同,克隆开销很小。
所有词法单元都带有一个关联的Span。Span是一个不透明的值,不能被修改但可以被创建。Span代表程序中源代码的范围,主要用于错误报告。虽然你不能修改Span本身,但你总是可以更改与任何词法单元关联的Span,例如通过从另一个词法单元获取Span。
过程宏卫生
过程宏是不卫生的。这意味着它们的行为就如同输出词法单元流被直接内联写入其旁边的代码一样。这意味着它受外部项影响,也影响外部导入。
宏作者需要小心,以确保他们的宏在给定此限制的情况下,尽可能在更多上下文中工作。这通常包括使用库中项的绝对路径(例如,::std::option::Option而不是Option),或者通过确保生成的函数具有不太可能与其他函数冲突的名称(如__internal_foo而不是foo)。
proc_macro属性
例子
此宏定义忽略其输入,并在其作用域中发出一个
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函数,其中词法单元流来自proc_macro crate。它必须具有 “Rust” ABI。不允许使用其他函数限定符。它必须位于crate的根目录中。
proc_macro属性在一个函数上只能指定一次。
proc_macro属性在crate根目录中的宏命名空间中公开定义宏,其名称与函数同名。
一个函数式过程宏的函数式宏调用会将宏调用分隔符内的内容作为输入词法单元流参数传递,并用函数的输出词法单元流替换整个宏调用。
函数式过程宏可以在任何宏调用位置被调用,包括:
proc_macro_derive属性
将 proc_macro_derive属性 应用于函数定义一个 派生宏 ,它可以通过 derive属性 调用。这些宏会获得一个结构体、枚举或联合体定义的词法单元流,并可以在其后发出新的项。它们还可以声明和使用派生宏辅助属性。
例子
这个派生宏忽略其输入,并添加定义一个函数的词法单元。
#![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,其中词法单元流来自proc_macro crate。该函数可以是const,并且可以使用extern显式指定 Rust ABI,但不能使用任何其他限定符(例如,它不能是async或unsafe)。
proc_macro_derive属性在一个函数上只能使用一次。
proc_macro_derive属性在crate的根目录中的宏命名空间中公开定义派生宏。
输入词法单元流是应用derive属性的项的词法单元流。输出词法单元流必须是(可能为空的)一组项。这些项会在输入项之后,在同一模块或块内被追加。
派生宏辅助属性
派生宏可以声明 派生宏辅助属性 ,用于派生宏所应用的项的作用域内。这些属性是惰性的。虽然它们的目的被声明它们的宏使用,但它们可以被任何宏看到。
派生宏的辅助属性通过将其标识符添加到proc_macro_derive属性的attributes列表中来声明。
例子
这声明了一个辅助属性然后忽略了它。
#![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() }要使用它,我们可以这样写:
extern crate proc_macro_examples; use proc_macro_examples::WithHelperAttr; #[derive(WithHelperAttr)] struct Struct { #[helper] field: (), }
当一个派生宏调用应用于一个项时,该派生宏引入的辅助属性会在以下情况下生效:1) 用于应用于该项并在派生宏调用后词法应用的属性,以及 2) 用于应用于项内部的字段和变体的属性。
注意
rustc目前允许在引入它们的宏之前使用派生辅助。这种乱序使用的派生辅助可能不会遮蔽其他属性宏。此行为已被弃用,并计划移除。
#[helper] // 已弃用,未来将成为硬错误。 #[derive(WithHelperAttr)] struct Struct { field: (), }更多详情,请参见Rust问题单#79202。
proc_macro_attribute属性
proc_macro_attribute属性 定义一个 属性宏 ,它可以作为外部属性使用。
例子
此属性宏接受输入流并原样发出,实际上是一个空操作属性。
#![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 }
例子
这显示了在编译器输出中,属性宏所看到的字符串化
词法单元流。// 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; // 示例:基本函数。 #[show_streams] fn invoke1() {} // 输出:attr: "" // 输出:item: "fn invoke1() {}" // 示例:带输入的属性。 #[show_streams(bar)] fn invoke2() {} // 输出:attr: "bar" // 输出:item: "fn invoke2() {}" // 示例:输入中有多个词法单元。 #[show_streams(multiple => tokens)] fn invoke3() {} // 输出:attr: "multiple => tokens" // 输出:item: "fn invoke3() {}" // 示例:输入中有分隔符。 #[show_streams { delimiters }] fn invoke4() {} // 输出:attr: "delimiters" // 输出:item: "fn invoke4() {}"
proc_macro_attribute属性使用MetaWord语法。
proc_macro_attribute属性只能应用于fn(TokenStream, TokenStream) -> TokenStream类型的pub函数,其中词法单元流来自 proc_macro crate。它必须具有 “Rust” ABI。不允许使用其他函数限定符。它必须位于crate的根目录中。
proc_macro_attribute属性在一个函数上只能指定一次。
proc_macro_attribute属性在crate的根目录中的宏命名空间中定义属性,其名称与函数同名。
属性宏只能用于:
第一个词法单元流参数是属性名称后的带分隔符词法单元树,但不包括外部分隔符。如果应用的属性只包含属性名称,或属性名称后跟空分隔符,则词法单元流为空。
声明宏词法单元和过程宏词法单元
声明式macro_rules宏和过程宏对词法单元(或更确切地说,TokenTrees)使用相似但不同的定义。
macro_rules中的词法单元树(对应tt匹配器)定义如下:
- 带分隔符的组(
(...)、{...}等) - 语言支持的所有运算符,包括单字符和多字符的(
+、+=)。- 请注意,此集合不包括单引号
'。
- 请注意,此集合不包括单引号
- 字面量(
"string"、1等)- 请注意,负号(例如
-1)永远不会是此类字面量词法单元的一部分,而是一个单独的运算符词法单元。
- 请注意,负号(例如
- 标识符,包括关键字(
ident、r#ident、fn) - 生命周期(
'ident) macro_rules中的元变量替换(例如macro_rules! mac { ($my_expr: expr) => { $my_expr } }在mac展开后的$my_expr,无论传递的表达式如何,都将被视为单个词法单元树)
过程宏中的词法单元树定义如下:
- 带分隔符的组(
(...)、{...}等) - 语言支持的运算符中使用的所有标点符号(
+,但不包括+=),以及单引号'字符(通常用于生命周期,有关生命周期拆分和合并行为,请参见下文) - 字面量(
"string"、1等)- 负号(例如
-1)作为整数和浮点字面量的一部分受支持。
- 负号(例如
- 标识符,包括关键字(
ident、r#ident、fn)
当词法单元流在过程宏之间传递时,会考虑这两种定义之间的不匹配。 请注意,下面的转换可能以惰性方式发生,因此如果词法单元未被实际检查,则可能不会发生转换。
当传递给过程宏时
- 所有多字符运算符都会被拆分为单字符。
- 生命周期会被拆分为一个
'字符和一个标识符。 - 关键字元变量
$crate作为单个标识符传递。 - 所有其他元变量替换都表示为其底层词法单元流。
- 此类词法单元流可能被包装到带分隔符的组(
Group)中,使用隐式分隔符(Delimiter::None),当需要保留解析优先级时。 tt和ident替换永远不会被包装到此类组中,并且始终表示为其底层词法单元树。
- 此类词法单元流可能被包装到带分隔符的组(
当从过程宏发出时
- 标点符号在适用时会被组合成多字符运算符。
- 与标识符连接的单引号
'会被组合成生命周期。 - 负字面量会被转换为两个词法单元(
-和字面量),可能被包装到带分隔符的组(Group)中,使用隐式分隔符(Delimiter::None),当需要保留解析优先级时。
请注意,声明宏和过程宏都不支持文档注释词法单元(例如/// Doc),因此当它们被传递给宏时,总是会被转换为表示其等效#[doc = r"str"]属性的词法单元流。