宏可以帮助我们减少重复代码的编写,在 Rust 中有两种宏定义,
其中,声明宏只是简单的 token
替换,我们无法知道代码结构中的其他信息,过程宏可以获取更加详细的数据,比如我们可以获取结构体中字段的名称,类型等等。
下面介绍如何编写一个简单的 derive 过程宏。
首先对于过程宏来说,不能和引用的 crate
放置在同一个 crate
中,需要单独放置在一个 crate
中,同时我们需要在 Cargo.toml
中配置
对于 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
宏的具体编写步骤主要分成以下三步
- 使用
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 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); }
|
- 通过
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
| impl #ident { #( pub fn #fn_name() { println!("call {}", #fn_name.to_string()) } ) * }
|
因为宏是在编译的过程中进行处理的,所以即使我们宏中代码实现的不高效,不影响运行时性能。编写好的宏,可以使用 cargo expand
命令进行展开,如果提示没找到该命令,使用 cargo install cargo-expand
进行安装。
简单写了一个 derive
宏自动为结构体生成 setter
和 getter
方法,仓库:construct,可以学习参考。