rust中除了声明宏以外还有过程宏,过程宏允许在执行函数的时候创建句法扩展,过程宏允许在编译时运行对rust句法进行操作的代码,他可以在消费掉一些rust句法输入的同时产生新的rust句法输出。可以把过程宏想象成从AST到另一个AST的函数映射。过程宏必须在crate类型为proc-macro的crate中定义。需要注意的是我们使用cargo时,定义过程宏的crate的配置文件里面需要使用proc-macro,在cargo.toml中加入以下代码:
[lib]
proc-macro = true
过程宏的三种形式
- 类宏函数(function-like macros):-foo!(...)
- 派生宏(derive macros): #[derive(Debug)]
- 属性宏(arribute macros):#[CustomAttribute]
作为函数,要么有返回句法,要么不返回,或者返回panic,还有就是喋喋不休的循环。返回类型句法根据过程宏的类型替换或者添加句法。panic会被编译器捕获,捕获之后并把它转化为编译器错误;无限循环不会被编译器捕获,但会让编译器被挂起。这就很尴尬了。
过程宏在编译时运行,因此具有与编译器相同的环境资源。比如说:它可以访问标准的输入输出和错误等,这些只有编译器才可以访问的资源。
过程宏有两种报告错误的方法。首先就是我们熟悉的panic。其次就是发布compile_error性质的宏调用。
proc-macro crate
过程宏我们上面提到过必须在crate类型为proc-macro
的crate中定义。那么过程宏类型的crate几乎总是回去链接编译器提供的proc-macro crate
.proc-macro crate
提供了编写过程宏所需要的各种类型和工具以便于让我们编写过程宏更加容易。
这种crate主要包含了一个TokenStream
类型。那么显而易见的就是说过程宏是在token流 上操作的,而不是在某个AST节点上操作的,因为他足够稳定。
我们上面已经说明过程宏有三种形式,接下来我们对这三种形式做一个详细的解释:
类函数过程宏
类函数过程宏是使用宏调用运算符(!)调用的过程宏。
这种宏是由一个带有proc_macro
属性和(TokenStream)->TokenStream
签名的公有可见性函数定义。输入TokenStream
是由宏调用的定界符界定的内容,输出TokenStream
将替换整个宏调用。
我们来用一个简单的例子来说明:
extern crate proc_macro;
use proc_macro::TokenStrream;
#[proc_macro]
pub fn re_num(item:TokenStram)->TokenStream{
"fn re_num()->u32 {42}".parse().unwrap()
}
接着我们调用使用它:
extern crate proc_macro_example;
use proc_macro_examples::re_num;
re_num!();
fn main(){
println!("{}",re_num());
}
类函数过程宏可以在任何宏调用位置调用,这些位置包括:语句
,表达式
,模式
,类型表达式
,程序项
可以出现的位置都可以调用。
派生宏
派生宏本质就是派生属性定义的新输入。这类宏在给定输入结构体(struct)
,枚举(enum)
或联合体(union)
token流的情况下创建的程序项。他们可以定义为派生辅助属性
。
自定义派生宏的语法:proc_macro_derive属性和(TokenStream) ->TokenStream签名的公有可见性函数定义。
输入TokenStream
是带有derive
属性的程序项的token流
。输出 TokenStream
必须是一组程序项
,然后将这组程序项追加到输入 TokenStream
中的那条程序项所在的模块或块中。
我们还是通过一个例子来说明:
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_derive(AnimalRunFn)]
pub fn derive_animal_run_fn(_item:TokenStream) ->TokenStream{
"fn run() ->String {"🐶用四条腿跑".to_string()}"
}
然后我们使用我们自己创建的派生宏:
extern crate proc_macro_examples;
use proc_macro_examples::AnimalRunFn;
#[derive(AnimalRunFn)]
struct Dog;
fn main(){
println!("{}",run());
}
派生宏辅助属性
派生宏可以将额外的属性添加它们所在的程序项的作用域中。这些属性被称为派生宏属性。这些属性是惰性的,它们存在的唯一目的是将这些属性在使用现场获得的属性值反向输入到定义它们的派生宏中。也就是说所有该宏的宏应用都可以看到它们。
定义辅助属性的方法是在 proc_macro_derive 宏中放置一个 attributes项 ,这些项带有一个使用逗号分隔的标识符列表,这些标识符是辅助属性的名称。
例如,下面的派生宏定义了一个辅助属性 helper,但最终没有用它做任何事情。
#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#![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树。如果该属性作为裸属性给出,则第一个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() {}"
最后希望大家多多关注我的公众号:花说编程,一个狂热的rust爱好者和布道者。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。