Rust过程宏入门(四)——遍历结构体字段

写于2021年1月16日。最早发表于知乎

简单回顾

在上一章中的简单案例中,

#[derive(Builder)]
pub struct Command {
    executable: String,
    args: Vec<String>,
    current_dir: String,
}

我们已经为Command结构体生成了如下代码

impl Command {
    pub fn builder() -> CommandBuilder {
        CommandBuilder
    }
}

pub struct CommandBuilder; 

接下来,自然是补齐CommandBuilder结构体的字段和成员函数,使其成为一个真正可用的类。

首先是CommandBuilder类的字段,相对比较简单,只需要引用原结构体的字段名,并在其字段类型外面套上Option<>即可。

获取结构体中的字段信息

如何获取原结构体的字段信息呢?这就需要用到上一章介绍的DeriveInput了:

pub struct DeriveInput {
    pub attrs: Vec<Attribute>,
    pub vis: Visibility,
    pub ident: Ident,
    pub generics: Generics,
    pub data: Data,
} 

其中的data: Data就是具体的结构体(或enumunion)信息。我们看Data的定义,它也是一个enum

pub enum Data {
    Struct(DataStruct),
    Enum(DataEnum),
    Union(DataUnion),
}

可见,这里的Data确实是支持structenumunion三种结构的,但我们的Builder宏只支持struct,所以先忽略另外两个。 我们看DataStruct的定义

pub struct DataStruct {
    pub struct_token: Struct,
    pub fields: Fields,
    pub semi_token: Option<Semi>,
}

它包含了一个struct的Token,一些字段Fields,以及一个可选的分号;Token。继续看Fields的定义

pub enum Fields {
    Named(FieldsNamed),
    Unnamed(FieldsUnnamed),
    Unit,
}

也是一个enum。这是由于结构体中的字段有具名和匿名(类似元组Tuple)两种写法,还有一种不含任何字段的空结构体Fields::UnitFields类有一个iter函数

impl Fields {
    pub fn iter(&self) -> Iter<'_, Field> { /*...*/ }
}

可以生成其字段的迭代器,我们再看Field的定义:

pub struct Field {
    pub attrs: Vec<Attribute>,
    pub vis: Visibility,
    pub ident: Option<Ident>,
    pub colon_token: Option<Colon>,
    pub ty: Type,
} 

终于找到了我们所需要的信息!其中的ident: Option<Ident>即是可选的字段名(但由于我们的Builder类只支持具名结构体,所以字段名是必须的),ty: Type即为类型信息。

遍历结构体字段

要遍历结构体的字段,首先需要将字段提取出来。前面已经介绍了DeriveInput相关的结构,那么我们需要做的就是用matchif let的方式过滤出需要的信息。

设计只考虑具名的struct类型,因此用if let匹配单个分支即可:

if let Data::Struct(r#struct) = input.data {
    let fields = r#struct.fields;
    if matches!(&fields, Fields::Named(_)) {
        todo!()
    }
} 

todo!()处则已经拿到了fields: Fields,且它是具名的。至于不用if let取出Fields::Named(_)实体的原因是:即使用Named(FieldsNamed)提取出的Field结构体中的ident: Option<Ident>也需要.unwrap()之后才能取出Ident,所以此处用原来的fields: Fields即可。

现在用我们将Fields映射为TokenStream2,以便嵌入最终生成的代码中。

let builder_fields = TokenStream2::from_iter(
    fields
        .iter() // 1
        .map(|field: &Field| (field.ident.as_ref().unwrap(), &field.ty)) // 2
        .map(|(ident: &Ident, ty: &Type)| quote!(#ident: Option<#ty>, )), // 3
);
  • // 1处的fields.iter()产生了&Field的迭代器,由于fields可能被多次使用,所以用只读迭代器,而非按值传递将其消耗;
  • // 2处的.map(...)提取出Field中的标识符&Ident与类型&Type(由于前面保证了结构体字段是具名的,此处直接调用Option::unwrap即可);
  • // 3处的.map(...)根据提取出的identty生成#ident: Option<#ty>,TokenStream,注意不要漏掉末尾的Token。

最后,TokenStream2::from_iter函数将以上的多个TokenStream2串联在一起。对Command类而言,将生成

executable: Option<String>,
args: Option<Vec<String>,
current_dir: Option<String>, 

只需要将上述生成的字段嵌入CommandBuilder结构体中,一个带字段的CommandBuilder结构体便生成好了:

quote! {
    pub struct CommandBuilder {
        builder_fields
    }
}

在为CommandBuilder类添加字段后,Command::builder函数中生成CommandBuilder的默认实例还未添加字段初始值,这里简单起见,我们用#[derive(Default)]派生宏自动为CommandBuilder类实现Default Trait,从而可以调用该Trait的default()函数生成其默认实例。

quote! {
    impl Command {
        pub fn builder() -> CommandBuilder {
            CommandBuilder::default()
        }
    }

    #[derive(Default)]
    pub struct CommandBuilder {
        builder_fields
    }
}

成员函数的生成

接下来,将为CommandBuilder添加字段的设置函数。以executable参数为例,期望通过以下函数设置其值:

impl CommandBuilder {
    pub fn executable(mut self, value: String) -> Self {
        self.executable = Some(executable);
        self
    }
}  

这里的self选择传值是因为希望CommandBuilder类用完即毁,将其中所存的值直接转移到最终创建的Command实例中,避免复制开销。返回一个Self对象是为了实现链式调用。 显然,生成成员函数时也涉及原结构体字段的遍历,因此我们重用builder_fields的生成函数,将其写成一个通用函数:

fn map_fields<F>(fields: &Fields, mapper: F) -> TokenStream2
where
    F: FnMut((&Ident, &Type)) -> TokenStream2,
{
    TokenStream2::from_iter(
        fields
            .iter()
            .map(|field| (field.ident.as_ref().unwrap(), &field.ty))
            .map(mapper),
    )
}

因此即可用不同的映射函数或闭包以生成不同的TokenStream了。

首先是设置字段的成员函数,相对较简单:

let builder_set_fields = map_fields(&fields, |(ident: &Field, ty: &Type)| {
    quote!(pub fn #ident(mut self, value: #ty) -> Self {
        self.#ident = Some(value);
        self
    })
});

基本上就是将前面手写的executable函数中的字段名和类型替换成统一的#ident#ty即可,非常方便。

最后是CommandBuilder::build函数,它将消耗自身,并生成一个Result<Command, String>实例。当所有字段都已设置时,返回正常的Command对象,否则返回错误信息(这里简单起见,暂时用String)。

build函数可以分为两部分:第一部分是依次取出由Option<>包装过的CommandBuilder类中的各个字段的值,第二部分是生成目标Command实例。

先看第一部分,直接遍历原结构体中的字段即可:

let build_lets = map_fields(&fields, |(ident, _)| { // 忽略用不到的ty
    quote!(
        let #ident = self.#ident.ok_or(format!(
            "field \"{}\" required, but not set yet.",
            stringify!(#ident),
        ))?;
    )
});

这里用到了Option::ok_or方法,当其为None时,返回一个错误值,然后用?运算符取出其中正确的值(若出现错误值,则直接将错误值返回上层)。

取出了构造结构体所需要的各字段值后,第二部分就变得相对简单了,直接返回一个Ok(Command{ executable, args, current_dir, })即可。外层的字段名再用一次结构体字段遍历可得:

let build_values = map_fields(&fields, |(ident, _)| quote!(#ident,));      

注意不要漏掉逗号,

最后将代码全部合到一起:

fn map_fields<F>(fields: &Fields, mapper: F) -> TokenStream2
where
    F: FnMut((&Ident, &Type)) -> TokenStream2,
{
    TokenStream2::from_iter(
        fields
            .iter()
            .map(|field| (field.ident.as_ref().unwrap(), &field.ty))
            .map(mapper),
    )
}

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let ident = input.ident;
    let ident_builder = Ident::new(&format!("{}Builder", ident), ident.span());
    if let Data::Struct(r#struct) = input.data {
        let fields = r#struct.fields;
        if matches!(&fields, Fields::Named(_)) {
            let builder_fields = map_fields(&fields, |(ident, ty)| quote!(#ident: Option<#ty>, ));
            let builder_set_fields = map_fields(&fields, |(ident, ty)| {
                quote!(pub fn #ident(mut self, value: #ty) -> Self {
                    self.#ident = Some(value);
                    self
                })
            });
            let build_lets = map_fields(&fields, |(ident, _)| {
                quote!(
                    let #ident = self.#ident.ok_or(format!(
                        "field \"{}\" required, but not set yet.",
                        stringify!(#ident),
                    ))?;
                )
            });
            let build_values = map_fields(&fields, |(ident, _)| quote!(#ident,));
            quote!(
                impl #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<#ident, String> {
                        build_lets
                        Ok(#ident { #build_values })
                    }
                }
            )
            .into()
        }
    }
    quote!().into()
}

输出TokenStream2以检验生成结果

上一章中讲到可以用cargo expand命令来检验代码生成结果。但由于本文中用到了#[defive(Default)派生宏以及format!规则宏等,若用cargo expand命令展开,会有一些不相干的信息,代码会很繁琐。但实际上除了cargo expand之后,还可以直接输出TokenStream2来检验生成结果。

方法很简单,直接将quote!宏生成出的TokenStream2eprintln!宏输出即可(eprintln!的用法与println!相似,只不过是输出到标准错误stderr中):

let tokens = quote!( /* ... */ );
eprintln!("{}", tokens); 

输出后的代码未经格式化,我们可以手动复制到一个空白文件然后使用rustfmt程序格式化,得

impl Command {
    pub fn builder() -> CommandBuilder {
        CommandBuilder::default()
    }
}
#[derive(Default)]
pub struct CommandBuilder {
    executable: Option<String>,
    args: Option<Vec<String>>,
    current_dir: Option<String>,
}
impl CommandBuilder {
    pub fn executable(mut self, value: String) -> Self {
        self.executable = Some(value);
        self
    }
    pub fn args(mut self, value: Vec<String>) -> Self {
        self.args = Some(value);
        self
    }
    pub fn current_dir(mut self, value: String) -> Self {
        self.current_dir = Some(value);
        self
    }
    pub fn build(self) -> Result<Command, String> {
        let executable = self.executable.ok_or(format!(
            "field \"{}\" required, but not set yet.",
            stringify!(executable),
        ))?;
        let args = self.args.ok_or(format!(
            "field \"{}\" required, but not set yet.",
            stringify!(args),
        ))?;
        let current_dir = self.current_dir.ok_or(format!(
            "field \"{}\" required, but not set yet.",
            stringify!(current_dir),
        ))?;
        Ok(Command {
            executable,
            args,
            current_dir,
        })
    }
}

这种方法在调用派生宏中出现编译错误时尤其有用,不仅可以用来检验整体代码片段,也可以随时查看某一小片段,便于调试。

功能验证

最后,为了验证派生宏的功能正确,将编译运行以下程序:

use derive_builder::Builder;

#[derive(Builder)]
pub struct Command {
    executable: String,
    args: Vec<String>,
    current_dir: String,
}

fn main() {
    let command = Command::builder()
        .executable("cargo".to_owned())
        .args(vec!["build".to_owned(), "--release".to_owned()])
        .current_dir("..".to_owned())
        .build()
        .unwrap();

    assert_eq!(command.executable, "cargo");
    assert_eq!(command.args, &["build", "--release"]);
    assert_eq!(command.current_dir, "..");
}

程序将正常执行结束,无任何输出。


本期的介绍到此为止,主要介绍了如何遍历Command结构体中的字段信息,并生成CommandBuilder类的字段与成员函数,实现了一个最简单可用的Builder类。接下来将继续如何处理原结构体中的可选、多选字段等功能。

本文中的代码实现详见 https://github.com/frank-king/proc-macro-workshop/tree/example/builder/05-method-chaining.rs/builder​

其中,测试用的main.rs代码见example/builder/05-method-chaining.rs中的main.rs文件