為了方便針對性的進行解答和歸納,後續結構目錄會定義成如下的方式
- 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,
}
}
}
最開始的時候,我按照題目示意的方式進行書寫,但是沒有使用模板,但模板才是精髓所在。
其中參考連結如下
- github.com/dtolnay/quote
- docs.rs/syn/1.0/syn/struct.Ident.h...
quote::quote!
:進行程式碼生成#(),*
:迴圈遍歷TokenStream2::extend
:節點拼接ident
:自動欄位識別
因此,首要的任務就是識別全部的欄位,然後按照模板生成程式碼。
// 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
),*
}
}
}
}
}
因為相關的
fields
,origin_ident
,builder_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 = "e::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 協議》,轉載必須註明作者和本文連結