proc-macro-workshop:builder-2

godme發表於2022-06-28

為了方便針對性的進行解答和歸納,後續結構目錄會定義成如下的方式

proc-macro-workshop:builder-2

  • common:定義通用的方法
  • solutionX:每一道題的題解

因此,原來的基礎方法變成了這樣

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    solution1(input)
}

後續根據在針對的地方進行修改,以便於表明每道題的用意

// Have the macro produce a struct for the builder state, and a `builder`
// function that creates an empty instance of the builder.
//
// As a quick start, try generating the following code (but make sure the type
// name matches what is in the caller's input).
//
//     impl Command {
//         pub fn builder() {}
//     }
//
// At this point the test should pass because it isn't doing anything with the
// builder yet, so `()` as the builder type is as good as any other.
//
// Before moving on, have the macro also generate:
//
//     pub struct CommandBuilder {
//         executable: Option<String>,
//         args: Option<Vec<String>>,
//         env: Option<Vec<String>>,
//         current_dir: Option<String>,
//     }
//
// and in the `builder` function:
//
//     impl Command {
//         pub fn builder() -> CommandBuilder {
//             CommandBuilder {
//                 executable: None,
//                 args: None,
//                 env: None,
//                 current_dir: None,
//             }
//         }
//     }
//
//
// Resources:
//
//   - The Quote crate for putting together output from a macro:
//     https://github.com/dtolnay/quote
//
//   - Joining together the type name + "Builder" to make the builder's name:
//     https://docs.rs/syn/1.0/syn/struct.Ident.html

use derive_builder::Builder;

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

fn main() {
    let builder = Command::builder();

    let _ = builder;
}

從用例上面能夠簡單的看到,主要是提供了Command::Builder的方法實現。
但是根據提示,讓我們最好實現

pub struct CommandBuilder {
    executable: Option<String>,
    args: Option<Vec<String>>,
    env: Option<Vec<String>>,
    current_dir: Option<String>,
}
impl Command {
    pub fn builder() -> CommandBuilder {
        CommandBuilder {
            executable: None,
            args: None,
            env: None,
            current_dir: None,
        }
    }
}

最開始的時候,我按照題目示意的方式進行書寫,但是沒有使用模板,但模板才是精髓所在。
其中參考連結如下

因此,首要的任務就是識別全部的欄位,然後按照模板生成程式碼。

// common.rs

// 欄位型別簡化定義
pub(crate) type FieldsType = syn::punctuated::Punctuated<syn::Field, syn::Token!(,)>;
// 欄位提取方法
pub(super) fn parse_fields(ast: &syn::DeriveInput) -> syn::Result<&FieldsType> {
    // 必須是struct
    if let syn::Data::Struct(
        // 結構解析
        syn::DataStruct {
            // 命名欄位列舉匹配
            fields: syn::Fields::Named(
                // 命名欄位結構
                syn::FieldsNamed { 
                    ref named, 
                    .. 
                }
            ),
            ..
        }
    ) = ast.data {
        return syn::Result::Ok(named);
    }
    // 結果不匹配,返回錯誤
    let err = syn::Error::new_spanned(ast, "parse fields error");
    syn::Result::Err(err)
}

在解析語法節點的時候,span是一個關鍵的資訊,雖然感覺毫無意義,但是報錯的時候能夠針對性的在關鍵位置進行錯誤提示。在後續的題目中,核對錯誤十分依靠span進行定位。

pub(super) fn solution(
    fields: &crate::common::FieldsType,
    origin_ident: &syn::Ident,
    builder_ident: &syn::Ident,
) -> proc_macro2::TokenStream {
    // 遍歷fields,獲取ident識別符號
    let idents: Vec<_> = fields.iter().map(|f| &f.ident).collect();
    // 遍歷fields,獲取指定型別
    let tys: Vec<_> = fields.iter().map(|f| &f.ty).collect();
    quote::quote! {
        // 定義XXBuilder
        pub struct #builder_ident {
            // 重複 // option包裝型別
            #(
                pub #idents: std::option::Option<#tys>
            ),*
        }

        // 實現builder方法
        impl #origin_ident {
            pub fn builder() -> #builder_ident {
                #builder_ident {
                    #(
                        #idents: std::option::Option::None
                    ),*
                }
            }
        }
    }
}

因為相關的fieldsorigin_identbuilder_ident很常用,因此透過外部傳入。

// lib.rs
mod common;
mod solution2;

#[proc_macro_derive(Builder)]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    solution1(input)
}

fn solution1(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let ast = syn::parse_macro_input!(input as syn::DeriveInput);
    let fields = {
        match common::parse_fields(&ast) {
            Ok(f) => f,
            Err(_e) => std::panic!(std::stringify!(_e)),
        }
    };

    let origin_ident = &ast.ident;
    let builder_ident = &quote::format_ident!("{}Builder", origin_ident);
    let mut token_stream = proc_macro2::TokenStream::new();

    // solution2
    let solution2_stream = solution2::solution(fields, origin_ident, builder_ident);
    token_stream.extend(solution2_stream);

    proc_macro::TokenStream::from(token_stream)
  • 解析: 結構解析大多是先列舉匹配,然後再解析具體結構獲取其中的定義
  • 拼接:模板中使用#進行變數的讀取,主要依賴外部變數進行鋪開

具體的解析欄位最好詳讀文件,或者死記硬背(我是這樣做的),逐步熟悉之後慢慢理解。

本作品採用《CC 協議》,轉載必須註明作者和本文連結