Rust 编写 derive 宏

宏可以帮助我们减少重复代码的编写,在 Rust 中有两种宏定义,

  • 声明宏
  • 过程宏

其中,声明宏只是简单的 token 替换,我们无法知道代码结构中的其他信息,过程宏可以获取更加详细的数据,比如我们可以获取结构体中字段的名称,类型等等。

下面介绍如何编写一个简单的 derive 过程宏。

首先对于过程宏来说,不能和引用的 crate 放置在同一个 crate 中,需要单独放置在一个 crate 中,同时我们需要在 Cargo.toml 中配置

1
2
[lib]
proc-macro = true

对于 derive 宏而言,即为输入为 TokenStream,输出也是 TokenStream 的函数,且输出的内容会 append 到代码中,而不会覆盖输入的内容

1
fn proc_marco(input: TokenStream) -> TokenStream;

我们使用 syn 对输入进行解析,获取其中的 token 信息,使用 quote 构造输出

1
2
3
[dependencies]
quote = "1"
syn = "1.0"

使用 proc_macro_derive 来标注这个函数是一个 derive 宏,同时也可以指定 attributes 用于指定可以在宏范围中使用的 attr,可以指定多个使用逗号进行分割。

1
2
#[proc_macro_derive(PrintField, attributes(field, typ))]
pub fn print_field(input: TokenStream) -> TokenStream;

如果要使用该宏,使用方式如下

1
2
3
4
5
6
#[derive(PrintField)]
struct Server {
#[field="hi"]
host: String,
port: u16
}

derive 宏的具体编写步骤主要分成以下三步

  1. 使用 syn 提供的方法解析输入
1
2
3
4
5
#[proc_macro_derive(PrintField, attributes(field, typ))]
pub fn print_field(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);
//....
}
  1. 从输入中获取到需要的信息,进行保存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#[proc_macro_derive(PrintField, attributes(field, typ))]
pub fn print_field(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);

let mut field_names = vec![];

if let syn::Data::Struct(s) = data {
if let syn::Fields::Named(f) = s.fields {
// 获取所有的字段名字,最后进行打印
for field in f.named.iter() {
field_names.push(
field
.ident
.as_ref()
.map(|ident| ident.to_string())
.unwrap_or_default(),
);
}
}
}
println!("{:?}", field_names);
//....
}

  1. 通过 quote 构造输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#[proc_macro_derive(PrintField, attributes(field, typ))]
pub fn print_field(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);

let mut field_names = vec![];

if let syn::Data::Struct(s) = data {
if let syn::Fields::Named(f) = s.fields {
println!("{:?}", f.to_token_stream().to_string());
for field in f.named.iter() {
field_names.push(
field
.ident
.as_ref()
.map(|ident| ident.to_string())
.unwrap_or_default(),
);
}
}
}
println!("{:?}", field_names);
quote!(
impl #ident {
pub fn hello_world(&self) {
println!("Hello World")
}
}
).into()
}

函数中的 Ident 类型变量,使用 #variable_name 的方式在 quote 中进行引用,不能直接使用字符串,可以通过下面的方式进行创建

1
2
3
use proc_macro2::Span;
use syn::Ident;
Ident::new("fn_name", Span::call_site());

如果需要进行循环迭代,可以使用 #()* 的方式表示

1
2
3
4
5
6
7
8
// fn_name 可以理解为 Vec<Ident> 类型
impl #ident {
#(
pub fn #fn_name() {
println!("call {}", #fn_name.to_string())
}
) *
}

因为宏是在编译的过程中进行处理的,所以即使我们宏中代码实现的不高效,不影响运行时性能。编写好的宏,可以使用 cargo expand 命令进行展开,如果提示没找到该命令,使用 cargo install cargo-expand 进行安装。

简单写了一个 derive 宏自动为结构体生成 settergetter 方法,仓库:construct,可以学习参考。


生活杂笔,学习杂记,偶尔随便写写东西。

作者

Edgar

发布于

2022-09-03

更新于

2022-09-04

许可协议

评论