派生宏是 Rust 中一类特殊的过程宏,它们可以用来自动派生某些 Rust 结构体或枚举类型的实现。这种宏通常用于简化代码编写和提高代码可读性。
在 Rust 中,可以使用 derive 属性来为结构体或枚举类型自动实现一些常见的 trait,例如 Debug、Clone、Copy、PartialEq 等等。当我们在定义结构体或枚举类型时添加 derive 属性时,编译器会自动为我们生成相应的 trait 实现代码。
#[derive(Debug, Clone, PartialEq)]
struct MyStruct {
// ...
}
在上述代码中,我们为 MyStruct 结构体添加了 derive 属性,指定了需要自动实现的三个 trait,然后编译器会根据这个属性为我们自动生成相应的实现代码。
需要注意的是,派生宏是基于结构体或枚举类型的定义来生成代码的,因此它们只能用于特定类型的派生;
类似地,我们也可以通过自己实现派生宏来实现某些trait或者其他代码;下面我们实现一个较为复杂的派生宏;
假设有一个结构体Command,我们为它增加初始化能力,可以使用链式函数初始化;
目标效果如下:
pub struct Command{
pub executable: Option<String>,
pub args: Vec<i32>,
pub current_dir: String,
}
let _ = Command::builder()
.executable(Some("123".to_string()))
.args(vec![1,2,3])
.current_dir("id".to_string())
.build();
简单分析一下,初步判断为了避免对于原结构体Command的影响,我们需要生成一个新的builder结构体作为辅助,并为它生成 executabl、args、current_dir函数,再通过build函数生成Command结构体并赋值;那么这个辅助结构体大概是这样的:
pub struct Command{
pub executable: Option<String>,
pub args: Vec<i32>,
pub current_dir: String,
}
impl Command {
pub fn builder() -> CommandBuilder {
CommandBuilder::default()
}
}
#[derive(Default)]
pub struct CommandBuilder {
pub executable: Option<Option<String>>,
pub args: Option<Vec<i32>>,
pub current_dir: Option<String>,
}
impl CommandBuilder {
pub fn executable(mut self, value: Option<String>) -> CommandBuilder {
self.executable = Some(value);
self
}
pub fn args(mut self, value: Vec<i32>) -> CommandBuilder {
self.args = Some(value);
self
}
pub fn current_dir(mut self, value: String) -> CommandBuilder {
self.current_dir = Some(value);
self
}
pub fn build(self) -> Result<Command, String> {
let executable = self.executable.ok_or(format!("缺少 executable"))?;
let args = self.args.ok_or(format!("缺少 args"))?;
let current_dir = self.current_dir.ok_or(format!("缺少current_dir"))?;
Ok(Command { executable, args, current_dir, })
}
}
那么我们的宏主要做的事有两点,1是生成这个辅助builder 2是能够遍历属性并生成新的相关读写代码
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
unimplemented!()
}
像上面这样的代码就是定义了一个宏Builder,其中input就是被他标记的结构体或枚举等,但类型TokenStream 并不能直接解析成我们可以理解的数据;这里我们需要引入两个库,syn和quote,syn用来解析input为我们能够操作的类型,quote可以方便的生成TokenStream; 我们还是先把看完整的代码,再一步步解读
use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::{DeriveInput, parse_macro_input, Data, Fields, Type};
use proc_macro2::TokenStream as TokenStream2;
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let input_ident = input.ident;
let ident_builder = format_ident!("{}Builder", input_ident.to_string());
if let Data::Struct(r) = input.data {
let fields = r.fields;
let builder_fields = map_fields(&fields, &mut |(ident, ty)| {
quote!(
#ident: Option<#ty>,
)
});
let builder_set_fields = map_fields(&fields, &mut |(ident, ty)| {
quote!(
pub fn #ident(mut self, value: #ty) -> Self {
self.#ident = Some(value);
self
}
)
});
let builder_lets = map_fields(&fields, &mut |(ident, _)| {
quote!(
let #ident = self.#ident.ok_or(format!(
"field {:?} not set yet", stringify!(#ident),
))?;
)
});
let builder_fields_values = map_fields(&fields, &mut |(ident, _)| {
quote!(
#ident,
)
});
quote!(
impl #input_ident {
pub fn builder() -> #ident_builder {
#ident_builder::default()
}
}
#[derive(Default)]
pub struct #ident_builder {
#builder_fields
}
impl #ident_builder {
#builder_set_fields
pub fn build(self) -> Result<#input_ident, String> {
#builder_lets
Ok(#input_ident{ #builder_fields_values })
}
}
).into()
} else {
// 不支持非struct类型
quote!().into()
}
}
fn map_fields<F>(fields: &Fields, mapper:&mut F) -> TokenStream2
where
F: FnMut((&Option<proc_macro2::Ident> , &Type)) -> TokenStream2,
{
let fs = fields.iter().map(|field| mapper((&field.ident ,&field.ty)) );
let stream2 = TokenStream2::from_iter(fs);
stream2
}
这段代码行数较多,也比较难理解,需要一点点分析:
前几行引用可以看到我们需要使用4个库,proc_macro是实现派生宏必须的、quote用来生成TokenStream、syn解析TokenStream、proc_macro2主要是使用TokenStream2的from_iter能力
#[proc_macro_derive(Builder)] 表示该宏的名字是Builder
pub fn derive_builder(input: TokenStream) -> TokenStream 是宏的实现,input就是被宏标记的结构体信息,返回值是我们新生成的代码
let input = parse_macro_input!(input as DeriveInput); 将input解析为DeriveInput 我们可以理解的类型,可以看下他的定义
pub struct DeriveInput {
/// Attributes tagged on the whole struct or enum.
pub attrs: Vec<Attribute>,
/// Visibility of the struct or enum.
pub vis: Visibility,
/// Name of the struct or enum.
pub ident: Ident,
/// Generics required to complete the definition.
pub generics: Generics,
/// Data within the struct or enum.
pub data: Data,
}
let input_ident = input.ident; 获取原始类名
let ident_builder = format_ident!("{}Builder", input_ident.to_string()); 这里通过format_ident宏拼接builder类名,也就是我们需要生成的辅助builder
if let Data::Struct(r) = input.data 这里的data是一个枚举,我们判断只处理结构体类型,其他类型不做处理
let fields = r.fields; 这里的fields类型就是结构体的所有属性,可以拿到名字和类型
接下来就是通过fileds细致的拼装builder的声明代码并增加属性、为builder增加set函数、读取builder的属性并在build函数里讲读取到的属性值赋值给被我们标记的结构体;