第4章 宏
可以使用称被为宏的自定义句法形式来扩展 Rust 的功能和句法。宏需要被命名,并通过一致的句法去调用:some_extension!(...)。
定义新宏有两种方式:
- 声明宏(Macros by Example)以更高级别的声明性的方式定义了一套新句法规则。
- 过程宏(Procedural Macros)可用于实现自定义派生。
Macro Invocation
宏调用
句法
MacroInvocation :
SimplePath!DelimTokenTreeDelimTokenTree :
(TokenTree*)
|[TokenTree*]
|{TokenTree*}TokenTree :
Token排除 定界符(delimiters) | DelimTokenTreeMacroInvocationSemi :
SimplePath!(TokenTree*);
| SimplePath![TokenTree*];
| SimplePath!{TokenTree*}
宏调用是在编译时执行宏,并用执行结果替换该调用。可以在下述情况里调用宏:
当宏调用被用作程序项或语句时,此时它应用的 MacroInvocationSemi 句法规则要求它如果不使用花括号,则在结尾处须添加分号。在宏调用或宏(macro_rules)定义之前不允许使用可见性限定符。
#![allow(unused)] fn main() { // 作为表达式使用. let x = vec![1,2,3]; // 作为语句使用. println!("Hello!"); // 在模式中使用. macro_rules! pat { ($i:ident) => (Some($i)) } if let pat!(x) = Some(1) { assert_eq!(x, 1); } // 在类型中使用. macro_rules! Tuple { { $A:ty, $B:ty } => { ($A, $B) }; } type N2 = Tuple!(i32, i32); // 作为程序项使用. use std::cell::RefCell; thread_local!(static FOO: RefCell<u32> = RefCell::new(1)); // 作为关联程序项使用. macro_rules! const_maker { ($t:ty, $v:tt) => { const CONST: $t = $v; }; } trait T { const_maker!{i32, 7} } // 宏内调用宏 macro_rules! example { () => { println!("Macro call in a macro!") }; } // 外部宏 `example` 展开后, 内部宏 `println` 才会展开. example!(); }
声明宏
句法
MacroRulesDefinition :
macro_rules!IDENTIFIER MacroRulesDefMacroRulesDef :
(MacroRules);
|[MacroRules];
|{MacroRules}MacroRules :
MacroRule (;MacroRule )*;?MacroRule :
MacroMatcher=>MacroTranscriberMacroMatcher :
(MacroMatch*)
|[MacroMatch*]
|{MacroMatch*}MacroMatch :
Token排除 $ 和 定界符
| MacroMatcher
|$IDENTIFIER:MacroFragSpec
|$(MacroMatch+)MacroRepSep? MacroRepOpMacroFragSpec :
block|expr|ident|item|lifetime|literal
|meta|pat|path|stmt|tt|ty|visMacroRepSep :
Token排除 定界符 和 重复操作符MacroRepOp :
*|+|?MacroTranscriber :
DelimTokenTree
macro_rules 允许用户以声明性的(declarative)方式定义句法扩展。我们称这种扩展形式为“声明宏(macros by example)”或简称“宏”。
每个声明宏都有一个名称和一条或多条规则。每条规则都有两部分:一个匹配器(matcher),描述它匹配的句法;一个转码器(transcriber),描述成功匹配后将执行的替代调用句法。匹配器和转码器都必须由定界符(delimiter)包围。宏可以扩展为表达式、语句、程序项(包括 trait、impl 和外来程序项)、类型或模式。
转码
当宏被调用时,宏扩展器(macro expander)按名称查找宏调用,并依次尝试此宏中的每条宏规则。宏会根据第一个成功的匹配进行转码;如果当前转码结果导致错误,不会再尝试进行后续匹配。在匹配时,不会执行预判;如果编译器不能明确地确定如何一个 token 一个 token 地解析宏调用,则会报错。在下面的示例中,编译器不会越过标识符,去提前查看后跟的 token 是 ),尽管这能帮助它明确地解析调用:
#![allow(unused)] fn main() { macro_rules! ambiguity { ($($i:ident)* $j:ident) => { }; } ambiguity!(error); // 错误: 局部歧义(local ambiguity) }
在匹配器和转码器中,token $ 用于从宏引擎中调用特殊行为(下文元变量和重复元中有详述)。不属于此类调用的 token 将按字面意义进行匹配和转码,除了一个例外。这个例外是匹配器的外层定界符将匹配任何一对定界符。因此,比如匹配器 (()) 将匹配 {()},而 {{}} 不行。字符 $ 不能按字面意义匹配或转码。
当将当前匹配的匹配段转发给另一个声明宏时,第二个宏中的匹配器看到的将是此匹配段类型的不透明抽象句法树(opaque AST)。第二个宏不能使用字面量token 来匹配匹配器中的这个匹配段,唯一可看到/使用的就是此匹配段类型一样的匹配段选择器(fragment specifier)。但匹配段类型 ident、lifetime、和 tt 是几个例外,它们可以通过字面量token 进行匹配。下面示例展示了这一限制:(译者注:匹配段选择器和匹配段,以及宏中各部件的定义可以凑合着看看译者未能翻译完成的宏定义规范)
#![allow(unused)] fn main() { macro_rules! foo { ($l:expr) => { bar!($l); } // ERROR: ^^ no rules expected this token in macro call } macro_rules! bar { (3) => {} } foo!(3); }
以下示例展示了 tt 类型的匹配段在成功匹配(转码)一次之后生成的 tokens 如何能够再次直接匹配:
#![allow(unused)] fn main() { // 成功编译 macro_rules! foo { ($l:tt) => { bar!($l); } } macro_rules! bar { (3) => {} } foo!(3); }
元变量
在匹配器中,$名称:匹配段选择器 这种句法格式匹配符合指定句法类型的 Rust 句法段,并将其绑定到元变量 $名称 上。有效的匹配段选择器包括:
item: 程序项block: 块表达式stmt: 语句,注意此选择器不匹配句尾的分号(如果匹配器中提供了分号,会被当做分隔符),但碰到分号是自身的一部分的程序项语句的情况又会匹配。pat: 模式expr: 表达式ty: 类型ident: 标识符或关键字path: 类型表达式 形式的路径tt: token树 (单个 token 或宏匹配定界符()、[]或{}中的标记)meta: 属性,属性中的内容lifetime: 生存期tokenvis: 可能为空的可见性限定符literal: 匹配-?字面量表达式
因为匹配段类型已在匹配器中指定了,则在转码器中,元变量只简单地用 $名称 这种形式来指代就行了。元变量最终将被替换为跟它们匹配上的句法元素。元变量关键字 $crate 可以用来指代当前的 crate(请参阅后面的卫生性(hygiene)章节)。元变量可以被多次转码,也可以完全不转码。
重复元
在匹配器和转码器中,重复元被表示为:将需要重复的 token 放在 $(…) 内,然后后跟一个重复运算符(repetition operator),这两者之间可以放置一个可选的分隔符(separator token)。分隔符可以是除定界符或重复运算符之外的任何 token,其中分号(;)和逗号(,)最常见。例如: $( $i:ident ),* 表示用逗号分隔的任何数量的标识符。嵌套的重复元是合法的。
重复运算符为:
*— 表示任意数量的重复元。+— 表示至少有一个重复元。?— 表示一个可选的匹配段,可以出现零次或一次。
因为 ? 表示最多出现一次,所以它不能与分隔符一起使用。
通过分隔符的分隔,重复的匹配段都会被匹配和转码为指定的数量的匹配段。元变量就和这些每个段中的重复元相匹配。例如,之前示例中的 $( $i:ident ),* 将 $i 去匹配列表中的所有标识符。
在转码过程中,重复元会受到额外的限制,以便于编译器知道该如何正确地扩展它们:
- 在转码器中,元变量必须与它在匹配器中出现的次数、指示符类型以及其在重复元内的嵌套顺序都完全相同。因此,对于匹配器
$( $i:ident ),*,转码器=> { $i },=> { $( $( $i)* )* }和=> { $( $i )+ }都是非法的,但是=> { $( $i );* }是正确的,它用分号分隔的标识符列表替换了逗号分隔的标识符列表。 - 转码器中的每个重复元必须至少包含一个元变量,以便确定扩展多少次。如果在同一个重复元中出现多个元变量,则它们必须绑定到相同数量的匹配段上,不能有的多,有的少。例如,
( $( $i:ident ),* ; $( $j:ident ),* ) => (( $( ($i,$j) ),* ))里,绑定到$j的匹配段的数量必须与绑定到$i上的相同。这意味着用(a, b, c; d, e, f)调用这个宏是合法的,并且可扩展到((a,d), (b,e), (c,f)),但是(a, b, c; d, e)是非法的,因为前后绑定的数量不同。此要求适用于嵌套的重复元的每一层。
作用域、导出以及导入
由于历史原因,声明宏的作用域并不完全像各种程序项那样工作。宏有两种形式的作用域:文本作用域(textual scope)和基于路径的作用域(path-based scope)。文本作用域基于宏在源文件中(定义和使用所)出现的顺序,或是跨多个源文件出现的顺序,文本作用域是默认的作用域。(后本节面将进一步解释这个。)基于路径的作用域与其他程序项作用域的运行方式相同。宏的作用域、导出和导入主要由其属性控制。
当声明宏被非限定标识符(unqualified identifier)(非多段路径段组成的限定性路径)调用时,会首先在文本作用域中查找。如果文本作用域中没有任何结果,则继续在基于路径的作用域中查找。如果宏的名称由路径限定,则只在基于路径的作用域中查找。
use lazy_static::lazy_static; // 基于路径的导入.
macro_rules! lazy_static { // 文本定义.
(lazy) => {};
}
lazy_static!{lazy} // 首先通过文本作用域来查找我们的宏.
self::lazy_static!{} // 忽略文本作用域查找,直接使用基于路径的查找方式找到一个导入的宏.
文本作用域
文本作用域很大程度上取决于宏本身在源文件中的出现顺序,其工作方式与用 let语句声明的局部变量的作用域类似,只不过它可以直接位于模块下。当使用 macro_rules! 定义宏时,宏在定义之后进入其作用域(请注意,这不影响宏在定义中递归调用自己,因为宏调用的入口还是在定义之后的某次调用点上,此点开始的宏名称递归查找一定有效),在封闭它的作用域(通常是模块)结束时离开。文本作用域可以覆盖/进入子模块,甚至跨越多个文件:
//// src/lib.rs
mod has_macro {
// m!{} // 报错: m 未在作用域内.
macro_rules! m {
() => {};
}
m!{} // OK: 在声明 m 后使用.
mod uses_macro;
}
// m!{} // Error: m 未在作用域内.
//// src/has_macro/uses_macro.rs
m!{} // OK: m 在上层模块文件 src/lib.rs 中声明后使用
多次定义宏并不报错;除非超出作用域,否则最近的宏声明将屏蔽前一个。
#![allow(unused)] fn main() { macro_rules! m { (1) => {}; } m!(1); mod inner { m!(1); macro_rules! m { (2) => {}; } // m!(1); // 报错: 没有设定规则来匹配 '1' m!(2); macro_rules! m { (3) => {}; } m!(3); } m!(1); }
宏也可以在函数内部声明和使用,其工作方式类似:
#![allow(unused)] fn main() { fn foo() { // m!(); // 报错: m 未在作用域内. macro_rules! m { () => {}; } m!(); } // m!(); // Error: m 未在作用域内. }
macro_use属性
macro_use属性有两种用途。首先,它可以通过作用于模块的方式让模块内的宏的作用域在模块关闭时不结束:
#![allow(unused)] fn main() { #[macro_use] mod inner { macro_rules! m { () => {}; } } m!(); }
其次,它可以用于从另一个 crate 里来导入宏,方法是将它附加到当前 crate 根模块中的 extern crate 声明前。以这种方式导入的宏会被导入到macro_use预导入包里,而不是直接文本导入,这意味着它们可以被任何其他同名宏屏蔽。虽然可以在导入语句之前使用 #[macro_use] 导入宏,但如果发生冲突,则最后导入的宏将胜出。可以使用可选的 MetaListIdents元项属性句法指定要导入的宏列表;当将 #[macro_use] 应用于模块上时,则不支持此指定操作。
#[macro_use(lazy_static)] // 或者使用 #[macro_use] 来导入所有宏.
extern crate lazy_static;
lazy_static!{}
// self::lazy_static!{} // 报错: lazy_static 没在 `self` 中定义
要用 #[macro_use] 导入宏必须先使用 #[macro_export] 导出,下文会有讲解。
基于路径的作用域
默认情况下,宏没有基于路径的作用域。但是如果该宏带有 #[macro_export] 属性,则相当于它在当前 crate 的根作用域的顶部被声明,它通常可以这样引用:
#![allow(unused)] fn main() { self::m!(); m!(); // OK: 基于路径的查找发现 m 在当前模块中有声明. mod inner { super::m!(); crate::m!(); } mod mac { #[macro_export] macro_rules! m { () => {}; } } }
标有 #[macro_export] 的宏始终是 pub 的,以便可以通过路径或前面所述的 #[macro_use] 方式让其他 crate 来引用。
卫生性
默认情况下,宏中引用的所有标识符都按原样展开,并在宏的调用位置上去查找。如果宏引用的程序项或宏不在调用位置的作用域内,则这可能会导致问题。为了解决这个问题,可以替代在路径的开头使用元变量 $crate,强制在定义宏的 crate 中进行查找。
//// 在 `helper_macro` crate 中.
#[macro_export]
macro_rules! helped {
// () => { helper!() } // 这可能会导致错误,因为 'helper' 在当前作用域之后才定义.
() => { $crate::helper!() }
}
#[macro_export]
macro_rules! helper {
() => { () }
}
//// 在另一个 crate 中使用.
// 注意没有导入 `helper_macro::helper`!
use helper_macro::helped;
fn unit() {
helped!();
}
请注意,由于 $crate 指的是当前的($crate 源码出现的)crate,因此在引用非宏程序项时,它必须与全限定模块路径一起使用:
#![allow(unused)] fn main() { pub mod inner { #[macro_export] macro_rules! call_foo { () => { $crate::inner::foo() }; } pub fn foo() {} } }
此外,尽管 $crate 允许宏在扩展时引用其自身 crate 中的程序项,但它的使用对可见性没有影响(,或者说它的使用仍受可见性条件的约束)。引用的程序项或宏必须仍然在调用位置处可见。在下面的示例中,任何试图从此 crate 外部调用 call_foo!() 的行为都将失败,因为 foo() 不是公有的。
#![allow(unused)] fn main() { #[macro_export] macro_rules! call_foo { () => { $crate::foo() }; } fn foo() {} }
(译者注:原文给出的这个例子是能正常调用的,原文并没有给出在 crate 外部调用的例子)
版本&版次差异:在 Rust 1.30 之前,
$crate和local_inner_macros(后面会讲)不受支持。从该版本开始,它们与基于路径的宏导入(前面讲过)一起被添加进来,用以确保不需要在当前 crate 已经使用导入了某导出宏的情况下再额外手动导入此导出宏下面用到的辅助宏。如果要让 Rust 的早期版本编写的 crate 要使用辅助宏,需要修改为使用$crate或local_inner_macros,以便与基于路径的导入一起工作。
当一个宏被导出时,可以在 #[macro_export] 属性里添加 local_inner_macros 属性值,用以自动为该属性修饰的宏内包含的所有宏调用自动添加 $crate:: 前缀。这主要是作为一个工具来迁移那些在引入 $crate 之前的版本编写的 Rust 代码,以便它们能与 Rust 2018 版中基于路径的宏导入一起工作。在使用新版本编写的代码中不鼓励使用它。
#![allow(unused)] fn main() { #[macro_export(local_inner_macros)] macro_rules! helped { () => { helper!() } // 自动转码为 $crate::helper!(). } #[macro_export] macro_rules! helper { () => { () } } }
随集歧义限制(译者注:该节还需要继续校对打磨,主要难点还是因为附录的宏定义规范译者还没有能全部搞懂)
宏系统使用的解析器相当强大,但是为了防止其在 Rust 的当前或未来版本中出现二义性解析,因此对它做出了限制。特别地,在消除二义性展开的基本规则之外又增加了一条:元变量匹配的非终结符(nonterminal)必须后跟一个已经确定为可以用来安全分隔匹配段的分隔符。
例如,像 $i:expr [ , ] 这样的宏匹配器在现今的 Rust 中理论上是可以接受的,因为现在 [,] 不可能是合法表达式的一部分,因此解析始终是明确的。但是,由于 [ 可以开始一个尾随表达式(trailing expressions),因此 [ 不是一个可以安全排除在表达式后面出现的字符。如果在接下来的 Rust 版本中接受了 [,],那么这个匹配器就会产生歧义或是错误解析,破坏正常代码。但是,像$i:expr, 或 $i:expr; 这样的匹配符始终是合法的,因为 , 和; 是合法的表达式分隔符。目前规范中的规则是:(译者注:下面的规则不是绝对的,因为宏的基础理论还在发展中。)
-
expr和stmt只能后跟一个:=>、,、;。 -
pat只能后跟一个:=>、,、=、|、if、in。 -
path和ty只能后跟一个:=>、,、=、|、;、:、>、>>、[、{、as、where、块(block)型非终结符(block nonterminals)。 -
vis只能后跟一个:,、非原生字符串priv以外的任何标识符和关键字、可以表示类型开始的任何 token、ident或ty或path型非终结符。(译者注:可以表示类型开始的 token 有:{
(,[,!,\*,&,&&,?, 生存期,>,>>,::, 非关键字标识符,super,self,Self,extern,crate,$crate,_,for,impl,fn,unsafe,typeof,dyn}。注意这个列表也不一定全。) -
其它所有的匹配段选择器没有限制。
当涉及到重复元时,随集歧义限制适用于所有可能的展开次数,注意需将重复元中的分隔符考虑在内。这意味着:
- 如果重复元包含分隔符,则分隔符必须能够跟随重复元的内容重复。
- 如果重复元可以重复多次(
*或+),那么重复元的内容必须能自我重复。 - 重复元前后内容必须严格匹配匹配器中指定的前后内容。
- 如果重复元可以匹配零次(
*或?),那么它后面的内容必须能够直接跟在它前面的内容后面。
有关更多详细信息,请参阅[正式规范]。
过程宏
过程宏允许在执行函数时创建句法扩展。过程宏有三种形式:
- 类函数宏(function-like macros) -
custom!(...) - 派生宏(derive macros)-
#[derive(CustomDerive)] - 属性宏(attribute macros) -
#[CustomAttribute]
过程宏允许在编译时运行对 Rust 句法进行操作的代码,它可以在消费掉一些 Rust 句法输入的同时产生新的 Rust 句法输出。可以将过程宏想象成是从一个 AST 到另一个 AST 的函数映射。
过程宏必须在 crate 类型为 proc-macro 的 crate 中定义。
注意: 使用 Cargo 时,定义过程宏的 crate 的配置文件里要使用
proc-macro键做如下设置:[lib] proc-macro = true
作为函数,它们要么有返回句法,要么触发 panic,要么永无休止地循环。返回句法根据过程宏的类型替换或添加句法;panic 会被编译器捕获,并将其转化为编译器错误;无限循环不会被编译器不会捕获,但会导致编译器被挂起。
过程宏在编译时运行,因此具有与编译器相同的环境资源。例如,它可以访问的标准输入、标准输出和标准错误输出等,这些编译器可以访问的资源。类似地,文件访问也是一样的。因此,过程宏与 Cargo构建脚本 具有相同的安全考量。
过程宏有两种报告错误的方法。首先是 panic;第二个是发布 compile_error 性质的宏调用。
The proc_macro crate
过程宏类型的 crate 几乎总是会去链接编译器提供的 proc_macro crate。proc_macro crate 提供了编写过程宏所需的各种类型和工具来让编写更容易。
此 crate 主要包含了一个 TokenStream 类型。过程宏其实是在 *token流(token streams)*上操作,而不是在某个或某些 AST 节点上操作,因为这对于编译器和过程宏的编译目标来说,这是一个随着时间推移要稳定得多的接口。token流大致相当于 Vec<TokenTree>,其中 TokenTree 可以大致视为词法 token。例如,foo 是标识符(Ident)类型的 token,. 是一个标点符号(Punct)类型的 token,1.2 是一个字面量(Literal)类型的 token。不同于 Vec<TokenTree> 的是 TokenStream 的克隆成本很低。
所有类型的 token 都有一个与之关联的 Span。Span 是一个不透明的值,不能被修改,但可以被制造。Span 表示程序内的源代码范围,主要用于错误报告。可以事先(通过函数 set_span)配置任何 token 的 Span。
过程宏的卫生性
过程宏是非卫生的(unhygienic)。这意味着它的行为就好像它输出的 token流是被简单地内联写入它周围的代码中一样。这意味着它会受到外部程序项的影响,也会影响外部导入。
鉴于此限制,宏作者需要小心地确保他们的宏能在尽可能多的上下文中正常工作。这通常包括对库中程序项使用绝对路径(例如,使用 ::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()
}
然后我们用它在一个二进制 crate 里打印 “42” 到标准输出。
extern crate proc_macro_examples;
use proc_macro_examples::make_answer;
make_answer!();
fn main() {
println!("{}", answer());
}
类函数过程宏可以在任何宏调用位置调用,这些位置包括语句、表达式、模式、类型表达式、程序项可以出现的位置(包括extern块里、固有(inherent)实现里和 trait实现里、以及 trait声明里)。
派生宏
派生宏为派生(derive)属性定义新输入。这类宏在给定输入结构体(struct)、枚举(enum)或联合体(union) token流的情况下创建新程序项。它们也可以定义派生宏辅助属性。
自定义派生宏由带有 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)块、固有实现、trate实现,以及 trait声明中的各类程序项。
属性宏由带有 proc_macro_attribute属性和 (TokenStream, TokenStream) -> TokenStream签名的公有可见性函数定义。签名中的第一个 TokenStream 是属性名称后面的定界 token树(delimited token tree)(不包括外层定界符)。如果该属性作为裸属性(bare attribute)给出,则第一个 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.to_string());
println!("item: \"{}\"", item.to_string());
item
}
// src/lib.rs
extern crate my_macro;
use my_macro::show_streams;
// 示例: 基础函数
#[show_streams]
fn invoke1() {}
// out: attr: ""
// out: item: "fn invoke1() { }"
// 示例: 带输入参数的属性
#[show_streams(bar)]
fn invoke2() {}
// out: attr: "bar"
// out: item: "fn invoke2() {}"
// 示例: 输入参数中有多个 token 的
#[show_streams(multiple => tokens)]
fn invoke3() {}
// out: attr: "multiple => tokens"
// out: item: "fn invoke3() {}"
// 示例:
#[show_streams { delimiters }]
fn invoke4() {}
// out: attr: "delimiters"
// out: item: "fn invoke4() {}"