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,
}
}
}
最开始的时候,我按照题目示意的方式进行书写,但是没有使用模板,但模板才是精髓所在。
其中参考链接如下
- 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 协议》,转载必须注明作者和本文链接
推荐文章: