Rust2018
中的过程宏
在Rust2018
版本中,我最喜欢的功能是过程宏
.在Rust
中,过程宏有着悠久而传奇
的历史(并继续
拥有传奇的未来!)
因为2018
年版极大
改善了定义和使用
它们的体验.
什么是过程宏
过程宏是,在编译时
用一段
语法,生成新语法
的函数.Rust2018
中的过程宏
有三个风格:
1,自Rust1.15
以来,#[derive]
模式宏一直很稳定,并把#[derive(Debug)]
的所有优点和易用性
也带到了用户定义
的特征中,如Serde
的#[derive(Deserialize)]
.
2,函数式宏
,在2018
版中是新的稳定版本
,并允许在基于crates.io
的库中定义:
rust
env!("FOO")
format_args!("...")
宏.类似macro_rules!
宏.
3,我最喜欢的属性宏
,也是2018
版中的新功能,它允许在Rust
函数上提供轻量注解
,来编译时
语法转换代码
.
可在清单
中用proc-macro=true
指定宏.使用时,Rust
编译器会加载过程宏
,并在展开调用
时执行它.
即Cargo
可控制过程宏
的版本
,且可像其他Cargo
依赖项一样轻松使用它们!
定义过程宏
这三类的定义方式
略微不同,在此以属性宏
为例.首先,标记Cargo.toml
:
rust
[lib]
proc-macro = true
然后在src/lib.rs
中,可编写宏:
rust
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn hello(attr: TokenStream, item: TokenStream) -> TokenStream {
//...
}
然后可在tests/smoke.rs
中编写单元测试
:
rust
#[my_crate::hello]
fn wrapped_function() {}
#[test]
fn works() {
wrapped_function();
}
...就这样!执行cargo test
的测试时,Cargo
编译过程宏
.之后,它编译编译时
加载宏的单元测试
,执行hello
函数并编译生成
的语法.
可见过程宏的几个重要属性:
1,输入/输出
是TokenStream
类型
2,编译时
可执行任意代码,即几乎不受限!
3,过程宏
与模块系统
整合,即可像其他名字
一样导入.
先深入了解其中的一些要点.
宏和模块系统
宏现在与Rust
中的模块系统
整合.表明导入宏
时不再需要笨拙的#[macro_use]
属性!不是:
rust
#[macro_use]
extern crate log;
fn main() {
debug!("hello, ");
info!("world!");
}
你可以如下:
rust
use log::info;
fn main() {
log::debug!("hello, ");
info!("world!");
}
好处不仅限于!
风格的macro_rules
宏,因为现在可转换
如下代码:
rust
#[macro_use]
extern crate serde_derive;
#[derive(Deserialize)]
struct Foo {
//...
}
//为
use serde::Deserialize;
#[derive(Deserialize)]
struct Foo {
//...
}
甚至不需要显式
依赖Cargo.toml
中的serde_derive!
,只需要:
rust
[dependencies]
serde = { version = '1.0.82', features = ['derive'] }
TokenStream
内部
神秘的TokenStream
类型,来自编译器提供的proc_macro
仓库.
首次添加TokenStream
时,只能调用to_string()
或parse()
来回来转换
其为串
或从串
转换.
从Rust2018
开始,可直接操作TokenStream
中的令牌.
TokenStream
"只是"TokenTree
上的一个迭代器
.Rust
中的所有语法
都分四类
,即TokenTree
的四种变体:
1,Ident
是如foo
或bar
的标识.它还包含如self
和super
的关键字.
2,字面(Literal)
包括像1,"foo"
和"b"
等内容.所有字面
都是表示程序中常量值
的一个令牌
.
3,Punct
表示标点符号
,而不是分隔符
.
如.
是foo.bar
字段访问中的Punct
令牌.像=>
此多符
标点符号表示为两个Punct
标记,一个表示=
,一个表示>
,Spacing
枚举表示=与>
相邻.
4,Group
是"树"
项最相关的地方,因为Group
代表一个分隔
的子令牌流
.如,(a,b)
是以括号
作为分隔符
的Group
,内部令牌流
是a,b
.
最小化TokenTree
对稳定性至关重要.
稳定Rust
的AST
是不可行的,因为那表示不能改变它.(想像假如如果不能添加?
符号).
用TokenStream
与过程宏
通信,在同时可编译和处理
较旧过程宏
时,编译器
可添加
新的语言语法
.不过,先看看如何从TokenStream
中取有用的信息.
解析TokenStream
但,只需要看看syn
仓库.
使用syn
仓库,可用单行代码
解析RustAST
:
rust
#[proc_macro_attribute]
pub fn hello(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::ItemFn);
let name = &input.ident;
let abi = &input.abi;
//...
}
syn
仓库不仅可解析内置语法
,且还可轻松地为自己
的语法编写递归下降
解析器.更多.
生成TokenStream
不仅要以TokenStream
作为过程宏
的输入,还要生成TokenStream
作为输出.一般要求输出
是有效的Rust
语法,但与输入
一样,它只是要构建
的令牌列表.
创建TokenStream
的唯一方法是通过其FromIterator
实现,即必须逐个创建每个令牌
并聚集它到TokenStream
中.
不过,这很乏味,所以看看syn
的quote
兄弟仓库.
quote
仓库是Rust
的准引用实现,主要提供了一个方便的宏
:
rust
use quote::quote;
#[proc_macro_attribute]
pub fn hello(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::ItemFn);
let name = &input.ident;
//输入函数总是等价于返回`42`,对不?
let result = quote! {
fn #name() -> u32 { 42 }
};
result.into()
}
quote!
宏这里允许你编写大部分Rust
语法,并用#foo
从环境
中快速插值变量
.
令牌和跨度(Span)
也许Rust2018
中过程宏的最大特性
是可自定义和使用
每个令牌上的Span
信息,这样可从过程宏
中取得惊人
的语法错误消息
:
rust
error: expected `fn`
--> src/main.rs:3:14
|
3 | my_annotate!(not_fn foo() {});
| ^^^^^^
及完全自定义
的错误消息:
rust
错误:`导入`方法必须至少`有一个`参数
--> invalid-imports.rs:12:5
|
12 | fn f1();
| ^^^^^^^^
Span
可看作是原始源文件
的指针,一般表示,foo
,"Ident
令牌来自文件bar.rs
,第4行第5列
,长度为3个
字节".
此信息主要由包含警告和错误消息
的编译器诊断使用.
在Rust2018
中,每个TokenTree
都有个与之关联的Span
.即,如果把所有输入令牌
的Span
保留到输出
中,则即使生成全新语法
,编译器的错误消息
仍是准确的!
如,如下一个小宏
:
rust
#[proc_macro]
pub fn make_pub(item: TokenStream) -> TokenStream {
let result = quote! {
pub #item
};
result.into()
}
按如下调用
:
rust
my_macro::make_pub! {
static X: u32 = "foo";
}
是无效的,因为从应该返回u32
的函数返回一个串
,编译器帮助诊断
问题为:
rust
`error[E0308]`:`类型`不匹配
--> src/main.rs:1:37
|
1 | my_macro::make_pub!(static X: u32 = "foo");
| ^^^^^ expected u32, found reference
|
=注意:期望类型为`"U32"`
找到类型`'&'staticstr'`
错误:因为上一个错误而中止
在此可见,尽管正在生成全新语法
,但编译器可保留span
信息,以继续为编写
代码提供针对性
的诊断.
生态
中的过程宏
syn,quote
和proc-macro2
是编写过程宏
的首选库
.方便自定义解析器,解析现有语法,创建新语法,使用旧版本
的Rust
等等!
Serde
这里及Serialize
和Deserialize
的继承宏
可能是生态
中最常用的宏.它们有令人印象深刻的配置量,是小注解
但强大的很好示例
.
wasm-bindgen
项目在Rust
中,使用属性宏
轻松定义接口
,并从JS
导入接口.
#[wasm_bindgen]
轻量注解方便理解传入和传出
内容,并删除了大量
转换样板.
gobject_gen!
宏是GNOME
项目的实验性IDL
,来在Rust
中安全地定义GObject
对象,避免
手写来与C语言
通信,并用Rust
写其他GObject
实例交互期望的所有胶水.
Rocket
框架最近切换到了过程宏,并展示了过程宏
的一些最新功能,如自定义诊断,自定义跨度创建
等.