一文读懂Rust宏(二) ---- 派生宏

935 阅读4分钟

派生宏是 Rust 中一类特殊的过程宏,它们可以用来自动派生某些 Rust 结构体或枚举类型的实现。这种宏通常用于简化代码编写和提高代码可读性。

在 Rust 中,可以使用 derive 属性来为结构体或枚举类型自动实现一些常见的 trait,例如 DebugCloneCopyPartialEq 等等。当我们在定义结构体或枚举类型时添加 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函数里讲读取到的属性值赋值给被我们标记的结构体;

源码地址

github.com/FaceWaller/…