proc-macro-workshopの進め方(derive_builder編)
はじめに
Rustには、煩雑な記述を簡潔にできるマクロという言語機能があります。
マクロには大きく分けて
-
macro_rules!
を用いたdeclarative(宣言的)マクロ - それ以外のprocedual(手続き的)マクロ
の2種類があり、手続き的マクロはさらに
-
#[derive]
マクロ - attribute-likeマクロ
- function-likeマクロ
の3種類に分かれます。
declarativeマクロの代表例としてはvec!
マクロがあります。
#[derive]
マクロの代表例にはDebug
やSerialize
などがあります。構造体やenumの前に書くやつです。
attribute-likeマクロの代表例には#[repr(C)]
などがあります。構造体やenumの前に書くことができるほか、そのフィールドの前に書くこともできます。derive
それ自体もattribute-likeマクロの一つです。
function-likeマクロは名前の通り関数のように使えるマクロで、SQL文を解析するマクロなどが紹介されています。
proc-macro-workshopはprocedualマクロの実装の練習ができるリポジトリです。いくつかのコースが用意されており、何段階か用意されたテストをパスするように実装を進めることでマクロの自作ができるようになっています。
この記事は、「proc-macro-workshopをやってみたいけど進め方がわからない」「やってみたが途中でわからなくなってしまった」といった方向けに、進め方や実装例を示すものです。
準備
以下の環境でやっていきます。
$ cargo --version
cargo 1.82.0 (8f40fc59f 2024-08-21)
$ rustc --version
rustc 1.82.0 (f6e511eec 2024-10-15)
また、proc-macro-workshopは記事執筆時点で最新のもの(390e9d0)を使用します。
まずは先ほどのリポジトリをforkし、それをgit clone
します。
エディタで開けたら、以下のコマンドを実行していくつかクレートを追加します。synクレートはextra-traitsのfeatureを追加します。
$ cargo add -p derive_builder syn -F syn/extra-traits quote proc-macro2
synは入力されたTokenStreamをRustのコードとして解釈するクレートです。本来マクロにはRustのコードでないものも渡せる(冒頭で紹介したsql!()
など)のですが、多くの場合はRustのコードを扱うことになるためsynを用います。
同様に、RustのコードからTokenStreamへの変換を容易にするためにquoteクレートとproc-macro2クレートを使用します。
01-parse
builder/tests/progress.rs
を開くとコメントアウトされたテストケースがたくさん並んでいます。
// t.pass("tests/01-parse.rs");
と書かれた行のコメントアウトを外してテストを実行すると以下のようなエラーが出ます。これを解消するようにマクロを実装します。
test tests/01-parse.rs ... error
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
error: proc-macro derive panicked
--> tests/01-parse.rs:26:10
|
26 | #[derive(Builder)]
| ^^^^^^^
|
= help: message: not implemented
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
builder/tests/01-parse.rs
を見ると以下のようなガイドがあります。
要約:
まずは空っぽのTokenStreamを返せるようにしましょう。
それができたら、次に進む前にマクロのinputをsyn::DeriveInputの構文木としてパースするようにしておきましょう。
builder/src/lib.rs
に雛形があるのでここに実装を追加していきます。
use proc_macro::TokenStream;
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let _ = input;
unimplemented!()
}
前半についてはTokenStream::new()
を返せばOKです。
後半についてはsynのdocsを見ると方法が書いてあります。よって以下のようにすればOKです。
解答例
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let _ = parse_macro_input!(input as DeriveInput);
TokenStream::new()
}
02-create-builder
builder/tests/progress.rs
で2番目のテストのコメントアウトを外します。
2番目のテストケースの中身を見ると次のように書かれています。
要約:
Command構造体にbuilder
メソッドを実装しましょう。
次に進む前に、CommandBuilder構造体を定義してbuilder
メソッドでこれを返すようにしましょう。
ここではマクロが実装された構造体の名前(Command
)を取得する必要があります。syn::DeriveInput
のパース結果を確認して、これの取得方法を探ります。以下のようにしてパース結果を出力させることができます。この結果は実行時ではなくビルド時に出力されます。
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
dbg!(input);
TokenStream::new()
}
結果(長いので畳んでいます)
DeriveInput {
attrs: [],
vis: Visibility::Public(
Pub,
),
ident: Ident {
ident: "Command",
span: #0 bytes(1001..1008),
},
generics: Generics {
lt_token: None,
params: [],
gt_token: None,
where_clause: None,
},
data: Data::Struct {
struct_token: Struct,
fields: Fields::Named {
brace_token: Brace,
named: [
Field {
attrs: [],
vis: Visibility::Inherited,
mutability: FieldMutability::None,
ident: Some(
Ident {
ident: "executable",
span: #0 bytes(1015..1025),
},
),
colon_token: Some(
Colon,
),
ty: Type::Path {
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "String",
span: #0 bytes(1027..1033),
},
arguments: PathArguments::None,
},
],
},
},
},
Comma,
Field {
attrs: [],
vis: Visibility::Inherited,
mutability: FieldMutability::None,
ident: Some(
Ident {
ident: "args",
span: #0 bytes(1039..1043),
},
),
colon_token: Some(
Colon,
),
ty: Type::Path {
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "Vec",
span: #0 bytes(1045..1048),
},
arguments: PathArguments::AngleBracketed {
colon2_token: None,
lt_token: Lt,
args: [
GenericArgument::Type(
Type::Path {
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "String",
span: #0 bytes(1049..1055),
},
arguments: PathArguments::None,
},
],
},
},
),
],
gt_token: Gt,
},
},
],
},
},
},
Comma,
Field {
attrs: [],
vis: Visibility::Inherited,
mutability: FieldMutability::None,
ident: Some(
Ident {
ident: "env",
span: #0 bytes(1062..1065),
},
),
colon_token: Some(
Colon,
),
ty: Type::Path {
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "Vec",
span: #0 bytes(1067..1070),
},
arguments: PathArguments::AngleBracketed {
colon2_token: None,
lt_token: Lt,
args: [
GenericArgument::Type(
Type::Path {
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "String",
span: #0 bytes(1071..1077),
},
arguments: PathArguments::None,
},
],
},
},
),
],
gt_token: Gt,
},
},
],
},
},
},
Comma,
Field {
attrs: [],
vis: Visibility::Inherited,
mutability: FieldMutability::None,
ident: Some(
Ident {
ident: "current_dir",
span: #0 bytes(1084..1095),
},
),
colon_token: Some(
Colon,
),
ty: Type::Path {
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "String",
span: #0 bytes(1097..1103),
},
arguments: PathArguments::None,
},
],
},
},
},
Comma,
],
},
semi_token: None,
},
}
Command
構造体の定義を変えて何度か試すと、それぞれのフィールドがどのような情報を表しているのか見えてくると思います。
これを見ると、構造体の名前はinput.ident
で取り出せそうです。また、input.vis
でpub
かどうかを、input.generics
でジェネリクスを取得できそうです。よってbuilder
メソッドの実装は以下のようにできます。quote
マクロの中では変数名の先頭に#
をつけることで展開することができます。
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let DeriveInput {
ident,
vis,
generics,
..
} = input;
let expanded = quote! {
impl #generics #ident #generics {
#vis fn builder() {}
}
};
TokenStream::from(expanded)
}
これでテストは通るのですが、次に進む前にCommandBuilder
構造体を定義できるようにしておきます。Command
という構造体名からCommandBuilder
という構造体名を作る必要がありますが、これは
let builder_ident = format_ident!("{}Builder", ident);
とすると実現できます。format_ident
マクロはquoteクレートに含まれています。
現時点ではbuilderのフィールド定義はハードコードで良いので、全体は以下のようになります。
解答例
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let DeriveInput {
ident,
vis,
generics,
..
} = input;
let builder_ident = format_ident!("{}Builder", ident);
let expanded = quote! {
impl #generics #ident #generics {
#vis fn builder() -> #builder_ident #generics {
#builder_ident {
executable: None,
args: None,
env: None,
current_dir: None,
}
}
}
#vis struct #builder_ident #generics {
executable: Option<String>,
args: Option<Vec<String>>,
env: Option<Vec<String>>,
current_dir: Option<String>,
}
};
TokenStream::from(expanded)
}
03-call-setters
builderに、それぞれのフィールドに対応するsetterのメソッドを実装していきます。
Command
構造体のフィールド定義を取得する必要がありますが、これはinput.data
で取り出せそうです。もしこのマクロがenumに対して実装された場合や、無名のフィールドを持つ構造体(struct Foo(i32)
など)に対して実装された場合はコンパイルエラーになるようにしておきます。
let DeriveInput {
ident,
vis,
generics,
data,
..
} = input;
let fields = match data {
Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }),
..
}) => named,
_ => {
return quote! {
compile_error!("Builder derive only works on structs with named fields");
}
.into();
}
};
let builder_field_definitions = fields.iter().map(|field| {
..
});
また、builder_field_definitions
がTokenStream
をアイテムとするイテレータであるとき、quote!
マクロ内では
#vis struct #builder_ident #generics {
#(#builder_field_definitions)*
}
のようにしてイテレータを展開できます。
したがって、以下のようにするとテストが通ります。
解答例
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::*;
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let DeriveInput {
ident,
vis,
generics,
data,
..
} = input;
let builder_ident = format_ident!("{}Builder", ident);
let fields = match data {
Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }),
..
}) => named,
_ => {
return quote! {
compile_error!("Builder derive only works on structs with named fields");
}
.into();
}
};
let builder_defaults = fields.iter().map(|field| {
let field_name = &field.ident;
quote! {
#field_name: None,
}
});
let builder_field_definitions = fields.iter().map(|field| {
let field_name = &field.ident;
let ty = &field.ty;
quote! {
#field_name: Option<#ty>,
}
});
let builder_setters = fields.iter().map(|field| {
let field_name = &field.ident;
let ty = &field.ty;
quote! {
#vis fn #field_name(&mut self, #field_name: #ty) -> &mut Self {
self.#field_name = Some(#field_name);
self
}
}
});
let expanded = quote! {
impl #generics #ident #generics {
#vis fn builder() -> #builder_ident #generics {
#builder_ident {
#(#builder_defaults)*
}
}
}
#vis struct #builder_ident #generics {
#(#builder_field_definitions)*
}
impl #generics #builder_ident #generics {
#(#builder_setters)*
}
};
TokenStream::from(expanded)
}
04-call-build, 05-method-chaining
impl CommandBuilder {
pub fn build(&mut self) -> Result<Command, Box<dyn Error>> {
...
}
}
のようなメソッドを定義してbuilderを元の構造体に変換できるようにします。
Result<_, Box<dyn Error>>
について軽くおさらいしておくと、
#[derive(Debug)]
struct MyError {
message: String
}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "MyError: {}", self.message)
}
}
impl std::error::Error for MyError {}
fn foo() -> Result<i32, Box<dyn Error>> {
let a = Some(42i32);
a.ok_or_else(|| MyError { message: "error".into() }.into())
}
のようにしてError
トレイトを実装したものならなんでも入るようなエラーを返すことができます(synクレートにもResult
やError
といった型があり紛らわしいためここではあえてフルパスを記述しています)。
今回はMyError
に相当する構造体としてString
を用います。builder構造体から値を取り出す際は
#field_ident: self.#field_ident
.clone()
.ok_or_else(|| -> Box<dyn std::error::Error> { #message.into() })?,
のようにしてOption<T>
をResult<T, Box<dyn Error>>
に変換します。最後の行は
ok_or(Box<dyn std::error::Error> { #message.into() })?
でもほぼ同じ挙動が得られますが、ok_or_else
が遅延評価なのに対しok_or
は即時評価なので、ok_or_else
を使うことが推奨されています。
解答例
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::*;
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let DeriveInput {
ident,
vis,
generics,
data,
..
} = input;
let builder_ident = format_ident!("{}Builder", ident);
let fields = match data {
Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }),
..
}) => named,
_ => {
return quote! {
compile_error!("Builder derive only works on structs with named fields");
}
.into();
}
};
let builder_defaults = fields.iter().map(|field| {
let field_name = &field.ident;
quote! {
#field_name: None,
}
});
let builder_field_definitions = fields.iter().map(|field| {
let field_name = &field.ident;
let ty = &field.ty;
quote! {
#field_name: Option<#ty>,
}
});
let builder_setters = fields.iter().map(|field| {
let field_name = &field.ident;
let ty = &field.ty;
quote! {
#vis fn #field_name(&mut self, #field_name: #ty) -> &mut Self {
self.#field_name = Some(#field_name);
self
}
}
});
let build_attrs = fields.iter().map(|field| {
let field_ident = field.ident.clone().unwrap();
let message = format!("field {} isn't set", field_ident);
quote! {
#field_ident: self.#field_ident
.clone()
.ok_or_else(|| -> Box<dyn std::error::Error> { #message.into() })?,
}
});
let expanded = quote! {
impl #generics #ident #generics {
#vis fn builder() -> #builder_ident #generics {
#builder_ident {
#(#builder_defaults)*
}
}
}
#vis struct #builder_ident #generics {
#(#builder_field_definitions)*
}
impl #generics #builder_ident #generics {
#(#builder_setters)*
#vis fn build(&mut self) -> Result<#ident #generics, Box<dyn std::error::Error>> {
Ok(#ident {
#(#build_attrs)*
})
}
}
};
TokenStream::from(expanded)
}
06-optional-field
そろそろderive
関数が長くなってきたので分割をします。fields.iter().map(..)
という記述が何度も繰り返されているのでこれを一つにまとめます。quote! {}
で作成したTokenStream
を返す関数の型を記述する必要がありますが、このTokenStream
はproc_macro::TokenStream
ではなくproc_macro2::TokenStream
であるため注意してください。
リファクタ例
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::*;
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let DeriveInput {
ident,
vis,
generics,
data,
..
} = input;
let builder_ident = format_ident!("{}Builder", ident);
let fields = match data {
Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }),
..
}) => named,
_ => {
return quote! {
compile_error!("Builder derive only works on structs with named fields");
}
.into();
}
};
let mut builder_defaults = Vec::with_capacity(fields.len());
let mut builder_field_definitions = Vec::with_capacity(fields.len());
let mut builder_setters = Vec::with_capacity(fields.len());
let mut build_attrs = Vec::with_capacity(fields.len());
for field in &fields {
builder_defaults.push(builder_default(field));
builder_field_definitions.push(builder_field_definition(field));
builder_setters.push(builder_setter(field, &vis));
build_attrs.push(build_attr(field));
}
let expanded = quote! {
impl #generics #ident #generics {
#vis fn builder() -> #builder_ident #generics {
#builder_ident {
#(#builder_defaults)*
}
}
}
#vis struct #builder_ident #generics {
#(#builder_field_definitions)*
}
impl #generics #builder_ident #generics {
#(#builder_setters)*
#vis fn build(&mut self) -> Result<#ident #generics, Box<dyn std::error::Error>> {
Ok(#ident {
#(#build_attrs)*
})
}
}
};
TokenStream::from(expanded)
}
fn builder_default(Field { ident, .. }: &Field) -> TokenStream2 {
quote! {
#ident: None,
}
}
fn builder_field_definition(Field { ident, ty, .. }: &Field) -> TokenStream2 {
quote! {
#ident: Option<#ty>,
}
}
fn builder_setter(Field { ident, ty, .. }: &Field, vis: &Visibility) -> TokenStream2 {
quote! {
#vis fn #ident(&mut self, #ident: #ty) -> &mut Self {
self.#ident = Some(#ident);
self
}
}
}
fn build_attr(Field { ident, .. }: &Field) -> TokenStream2 {
let ident = ident.clone().unwrap();
let message = format!("field {} isn't set", ident);
quote! {
#ident: self.#ident
.clone()
.ok_or_else(|| -> Box<dyn std::error::Error> { #message.into() })?,
}
}
この章では、もとの構造体のフィールドにOption<T>
がある場合はそのフィールドの指定をoptionalにします。builder構造体のフィールド定義はこのままだとOption<Option<T>>
になりますが、値がSome(None)
になることはないのでOption<Option<T>>
ではなくOption<T>
になるようにします。したがってやるべきことは
- builder構造体の定義を変更する
- それにあわせてsetterの定義も変更する
- それにあわせて
build
メソッドの定義も変更する
の3点です。まずはフィールドがOption<T>
かどうか判定する方法から考えます。
テストケースにあるcurrent_dir: Option<String>
のフィールドは以下のようにパースされます。
ty: Type::Path {
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "Option",
span: #0 bytes(2884..2890),
},
arguments: PathArguments::AngleBracketed {
colon2_token: None,
lt_token: Lt,
args: [
GenericArgument::Type(
Type::Path {
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "String",
span: #0 bytes(2891..2897),
},
arguments: PathArguments::None,
},
],
},
},
),
],
gt_token: Gt,
},
},
],
},
},
したがって次のようにして判定できます。
#[derive(Debug)]
enum FieldType<'a> {
Optional(&'a Type),
Required(&'a Type),
}
fn field_type(Field { ty, .. }: &Field) -> FieldType {
if let Type::Path(TypePath {
path: Path { segments, .. },
..
}) = ty
{
if let Some(PathSegment {
ident,
arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
}) = segments.first()
{
if ident == "Option" {
if let Some(GenericArgument::Type(ty)) = args.first() {
return FieldType::Optional(ty);
}
}
}
}
return FieldType::Required(ty);
}
builder構造体の定義変更は以下のようにできます。
fn builder_field_definition(Field { ident, .. }: &Field, field_type: &FieldType) -> TokenStream2 {
let ty = match field_type {
&FieldType::Optional(ty) => ty,
&FieldType::Required(ty) => ty,
};
quote! {
#ident: Option<#ty>,
}
}
setterおよびbuildメソッドも同様に変更できます。
解答例
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::*;
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let DeriveInput {
ident,
vis,
generics,
data,
..
} = input;
let builder_ident = format_ident!("{}Builder", ident);
let fields = match data {
Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }),
..
}) => named,
_ => {
return quote! {
compile_error!("Builder derive only works on structs with named fields");
}
.into();
}
};
let mut builder_defaults = Vec::with_capacity(fields.len());
let mut builder_field_definitions = Vec::with_capacity(fields.len());
let mut builder_setters = Vec::with_capacity(fields.len());
let mut build_attrs = Vec::with_capacity(fields.len());
for field in &fields {
let ft = field_type(field);
builder_defaults.push(builder_default(field));
builder_field_definitions.push(builder_field_definition(field, &ft));
builder_setters.push(builder_setter(field, &vis, &ft));
build_attrs.push(build_attr(field, &ft));
}
let expanded = quote! {
impl #generics #ident #generics {
#vis fn builder() -> #builder_ident #generics {
#builder_ident {
#(#builder_defaults)*
}
}
}
#vis struct #builder_ident #generics {
#(#builder_field_definitions)*
}
impl #generics #builder_ident #generics {
#(#builder_setters)*
#vis fn build(&mut self) -> Result<#ident #generics, Box<dyn std::error::Error>> {
Ok(#ident {
#(#build_attrs)*
})
}
}
};
TokenStream::from(expanded)
}
fn builder_default(Field { ident, .. }: &Field) -> TokenStream2 {
quote! {
#ident: None,
}
}
fn builder_field_definition(Field { ident, .. }: &Field, field_type: &FieldType) -> TokenStream2 {
let ty = match field_type {
&FieldType::Optional(ty) => ty,
&FieldType::Required(ty) => ty,
};
quote! {
#ident: Option<#ty>,
}
}
fn builder_setter(
Field { ident, .. }: &Field,
vis: &Visibility,
field_type: &FieldType,
) -> TokenStream2 {
let ty = match field_type {
&FieldType::Optional(ty) => ty,
&FieldType::Required(ty) => ty,
};
quote! {
#vis fn #ident(&mut self, #ident: #ty) -> &mut Self {
self.#ident = Some(#ident);
self
}
}
}
fn build_attr(Field { ident, .. }: &Field, field_type: &FieldType) -> TokenStream2 {
match field_type {
FieldType::Optional(_) => quote! {
#ident: self.#ident.clone(),
},
FieldType::Required(_) => {
let ident = ident.clone().unwrap();
let message = format!("field {} isn't set", ident);
quote! {
#ident: self.#ident
.clone()
.ok_or_else(|| -> Box<dyn std::error::Error> { #message.into() })?,
}
}
}
}
#[derive(Debug)]
enum FieldType<'a> {
Optional(&'a Type),
Required(&'a Type),
}
fn field_type(Field { ty, .. }: &Field) -> FieldType {
if let Type::Path(TypePath {
path: Path { segments, .. },
..
}) = ty
{
if let Some(PathSegment {
ident,
arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
}) = segments.first()
{
if ident == "Option" {
if let Some(GenericArgument::Type(ty)) = args.first() {
return FieldType::Optional(ty);
}
}
}
}
return FieldType::Required(ty);
}
07-repeated-field, 08-unrecognized-attribute
フィールド定義の上に#[builder(each = "...")]
というattributeがある場合、フィールドの型がVec<T>
であると仮定してattributeで指定した名前のメソッドで要素を一つずつ追加できるようにします。
まず、コンパイラがbuilder
attributeを認識できるよう、以下のようにします。
#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: TokenStream) -> TokenStream {
..
}
#[builder(each = "...")]
というattributeがあるかどうかはfield.attrs
を見ればわかりそうです。そもそもbuilder
attributeがない場合None
を、attributeがあるが構文が間違っている場合はSome(Err(err))
を、正しく記述されている場合はSome(Ok(ident))
を返すような関数を定義します。なお、次のコードのResult<T>
はsyn::Result<T>
です。
fn each_attr(attrs: &Vec<Attribute>) -> Option<Result<Ident>> {
todo!()
}
args
フィールドは以下のような構造になっています。ここから"arg"
を拾い上げられるように実装します。
Attribute {
pound_token: Pound,
style: AttrStyle::Outer,
bracket_token: Bracket,
meta: Meta::List {
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "builder",
span: #0 bytes(1417..1424),
},
arguments: PathArguments::None,
},
],
},
delimiter: MacroDelimiter::Paren(
Paren,
),
tokens: TokenStream [
Ident {
ident: "each",
span: #0 bytes(1425..1429),
},
Punct {
ch: '=',
spacing: Alone,
span: #0 bytes(1430..1431),
},
Literal {
kind: Str,
symbol: "arg",
suffix: None,
span: #0 bytes(1432..1437),
},
],
},
}
ひたすらパターンマッチで拾うよりも、Attribute
型のparse_nested_meta
メソッドを使うとより簡潔に実装できます。docs.rsに今回実装したいattributeと全く同じ形式の例があります。
エラーメッセージの内容も指定されているので、それに従います。エラーのspan
を正しく設定する方法がわからない場合、docs.rsを見るとよいでしょう。
以下に実装例を示します。
fn each_attr(attrs: &Vec<Attribute>) -> Option<Result<Ident>> {
let mut each: Option<Result<Ident>> = None;
for attr in attrs {
if !attr.path().is_ident("builder") {
continue;
}
if let Meta::List(meta_list) = &attr.meta {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("each") {
let value = meta.value()?;
let s: LitStr = value.parse()?;
each = Some(Ok(format_ident!("{}", s.value())));
} else {
each = Some(Err(Error::new_spanned(meta_list, "expected `builder(each = \"...\")`")));
}
Ok(())
});
}
}
each
}
あわせて、先ほど作成したenum FieldType
も変更します。
#[derive(Debug)]
enum FieldType<'a> {
Optional(&'a Type),
Required(&'a Type),
+ Repeated(&'a Type, Ident),
}
field_type
関数の戻り値もResult<T>
で囲い、each_attr
からのエラーをバケツリレーします。
fn field_type<'a>(Field { ty, attrs, .. }: &'a Field) -> Result<FieldType<'a>> {
if let Type::Path(TypePath {
path: Path { segments, .. },
..
}) = ty
{
if let Some(PathSegment {
ident,
arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
}) = segments.first()
{
if ident == "Option" {
if let Some(GenericArgument::Type(ty)) = args.first() {
return Ok(FieldType::Optional(ty));
}
} else if ident == "Vec" {
if let Some(each) = each_attr(&attrs) {
if let Some(GenericArgument::Type(ty)) = args.first() {
return Ok(FieldType::Repeated(ty, each?));
}
}
}
}
}
return Ok(FieldType::Required(ty));
}
derive
関数内では、field_type()
関数がErr
を返したら即座にコンパイルエラーになるようにします。
for field in &fields {
match field_type(field) {
Ok(field_type) => {
builder_defaults.push(builder_default(field));
builder_field_definitions.push(builder_field_definition(field, &field_type));
builder_setters.push(builder_setter(field, &vis, &field_type));
build_attrs.push(build_attr(field, &field_type));
}
Err(err) => return err.to_compile_error().into(),
}
}
最後に、builder_setter
など他のメソッドでFieldType::Repeated
の分岐を実装すればOKです。
解答例
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::*;
#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let DeriveInput {
ident,
vis,
generics,
data,
..
} = input;
let builder_ident = format_ident!("{}Builder", ident);
let fields = match data {
Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }),
..
}) => named,
_ => {
return quote! {
compile_error!("Builder derive only works on structs with named fields");
}
.into();
}
};
let mut builder_defaults = Vec::with_capacity(fields.len());
let mut builder_field_definitions = Vec::with_capacity(fields.len());
let mut builder_setters = Vec::with_capacity(fields.len());
let mut build_attrs = Vec::with_capacity(fields.len());
for field in &fields {
match field_type(field) {
Ok(field_type) => {
builder_defaults.push(builder_default(field));
builder_field_definitions.push(builder_field_definition(field, &field_type));
builder_setters.push(builder_setter(field, &vis, &field_type));
build_attrs.push(build_attr(field, &field_type));
}
Err(err) => return err.to_compile_error().into(),
}
}
let expanded = quote! {
impl #generics #ident #generics {
#vis fn builder() -> #builder_ident #generics {
#builder_ident {
#(#builder_defaults)*
}
}
}
#vis struct #builder_ident #generics {
#(#builder_field_definitions)*
}
impl #generics #builder_ident #generics {
#(#builder_setters)*
#vis fn build(&mut self) -> Result<#ident #generics, Box<dyn std::error::Error>> {
Ok(#ident {
#(#build_attrs)*
})
}
}
};
TokenStream::from(expanded)
}
fn builder_default(Field { ident, .. }: &Field) -> TokenStream2 {
quote! {
#ident: None,
}
}
fn builder_field_definition(
Field { ident, ty, .. }: &Field,
field_type: &FieldType,
) -> TokenStream2 {
let ty = match field_type {
&FieldType::Optional(ty) => ty,
&FieldType::Required(ty) => ty,
&FieldType::Repeated(_, _) => ty,
};
quote! {
#ident: Option<#ty>,
}
}
fn builder_setter(
Field { ident, .. }: &Field,
vis: &Visibility,
field_type: &FieldType,
) -> TokenStream2 {
match field_type {
FieldType::Optional(ty) | FieldType::Required(ty) => quote! {
#vis fn #ident(&mut self, #ident: #ty) -> &mut Self {
self.#ident = Some(#ident);
self
}
},
FieldType::Repeated(ty, each) => quote! {
#vis fn #each(&mut self, #each: #ty) -> &mut Self {
self.#ident.get_or_insert_with(Vec::new).push(#each);
self
}
},
}
}
fn build_attr(Field { ident, .. }: &Field, field_type: &FieldType) -> TokenStream2 {
match field_type {
FieldType::Optional(_) => quote! {
#ident: self.#ident.clone(),
},
FieldType::Required(_) => {
let ident = ident.clone().unwrap();
let message = format!("field {} isn't set", ident);
quote! {
#ident: self.#ident
.clone()
.ok_or_else(|| -> Box<dyn std::error::Error> { #message.into() })?,
}
}
FieldType::Repeated(_, _) => quote! {
#ident: self.#ident.clone().unwrap_or_default(),
},
}
}
#[derive(Debug)]
enum FieldType<'a> {
Optional(&'a Type),
Required(&'a Type),
Repeated(&'a Type, Ident),
}
fn field_type<'a>(Field { ty, attrs, .. }: &'a Field) -> Result<FieldType<'a>> {
if let Type::Path(TypePath {
path: Path { segments, .. },
..
}) = ty
{
if let Some(PathSegment {
ident,
arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
}) = segments.first()
{
if ident == "Option" {
if let Some(GenericArgument::Type(ty)) = args.first() {
return Ok(FieldType::Optional(ty));
}
} else if ident == "Vec" {
if let Some(each) = each_attr(&attrs) {
if let Some(GenericArgument::Type(ty)) = args.first() {
return Ok(FieldType::Repeated(ty, each?));
}
}
}
}
}
return Ok(FieldType::Required(ty));
}
fn each_attr(attrs: &Vec<Attribute>) -> Option<Result<Ident>> {
let mut each: Option<Result<Ident>> = None;
for attr in attrs {
if !attr.path().is_ident("builder") {
continue;
}
if let Meta::List(meta_list) = &attr.meta {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("each") {
let value = meta.value()?;
let s: LitStr = value.parse()?;
each = Some(Ok(format_ident!("{}", s.value())));
} else {
each = Some(Err(Error::new_spanned(
meta_list,
"expected `builder(each = \"...\")`",
)));
}
Ok(())
});
}
}
each
}
09-redefined-prelude-types
いよいよ最終問題です。Option
などの型が上書きされていてもマクロが壊れないようにします。
とはいってもやるべきことは簡単で、問題文にもほとんど答えのようなものが書かれています。
意訳:
一般に、proceduralかdeclarativeかを問わず他人に使われることを想定した全てのマクロは、展開されるコードに含まれる一切を絶対パスで記述すべきである。例えば、std::result::Result
のように。
解答例
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::*;
#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let DeriveInput {
ident,
vis,
generics,
data,
..
} = input;
let builder_ident = format_ident!("{}Builder", ident);
let fields = match data {
Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }),
..
}) => named,
_ => {
return quote! {
compile_error!("Builder derive only works on structs with named fields");
}
.into();
}
};
let mut builder_defaults = Vec::with_capacity(fields.len());
let mut builder_field_definitions = Vec::with_capacity(fields.len());
let mut builder_setters = Vec::with_capacity(fields.len());
let mut build_attrs = Vec::with_capacity(fields.len());
for field in &fields {
match field_type(field) {
Ok(field_type) => {
builder_defaults.push(builder_default(field));
builder_field_definitions.push(builder_field_definition(field, &field_type));
builder_setters.push(builder_setter(field, &vis, &field_type));
build_attrs.push(build_attr(field, &field_type));
}
Err(err) => return err.to_compile_error().into(),
}
}
let expanded = quote! {
impl #generics #ident #generics {
#vis fn builder() -> #builder_ident #generics {
#builder_ident {
#(#builder_defaults)*
}
}
}
#vis struct #builder_ident #generics {
#(#builder_field_definitions)*
}
impl #generics #builder_ident #generics {
#(#builder_setters)*
#vis fn build(&mut self) -> std::result::Result<#ident #generics, std::boxed::Box<dyn std::error::Error>> {
std::result::Result::Ok(#ident {
#(#build_attrs)*
})
}
}
};
TokenStream::from(expanded)
}
fn builder_default(Field { ident, .. }: &Field) -> TokenStream2 {
quote! {
#ident: std::option::Option::None,
}
}
fn builder_field_definition(
Field { ident, ty, .. }: &Field,
field_type: &FieldType,
) -> TokenStream2 {
let ty = match field_type {
&FieldType::Optional(ty) => ty,
&FieldType::Required(ty) => ty,
&FieldType::Repeated(_, _) => ty,
};
quote! {
#ident: std::option::Option<#ty>,
}
}
fn builder_setter(
Field { ident, .. }: &Field,
vis: &Visibility,
field_type: &FieldType,
) -> TokenStream2 {
match field_type {
FieldType::Optional(ty) | FieldType::Required(ty) => quote! {
#vis fn #ident(&mut self, #ident: #ty) -> &mut Self {
self.#ident = std::option::Option::Some(#ident);
self
}
},
FieldType::Repeated(ty, each) => quote! {
#vis fn #each(&mut self, #each: #ty) -> &mut Self {
self.#ident.get_or_insert_with(std::vec::Vec::new).push(#each);
self
}
},
}
}
fn build_attr(Field { ident, .. }: &Field, field_type: &FieldType) -> TokenStream2 {
match field_type {
FieldType::Optional(_) => quote! {
#ident: self.#ident.clone(),
},
FieldType::Required(_) => {
let ident = ident.clone().unwrap();
let message = format!("field {} isn't set", ident);
quote! {
#ident: self.#ident
.clone()
.ok_or_else(|| -> std::boxed::Box<dyn std::error::Error> { #message.into() })?,
}
}
FieldType::Repeated(_, _) => quote! {
#ident: self.#ident.clone().unwrap_or_default(),
},
}
}
#[derive(Debug)]
enum FieldType<'a> {
Optional(&'a Type),
Required(&'a Type),
Repeated(&'a Type, Ident),
}
fn field_type<'a>(Field { ty, attrs, .. }: &'a Field) -> Result<FieldType<'a>> {
if let Type::Path(TypePath {
path: Path { segments, .. },
..
}) = ty
{
if let Some(PathSegment {
ident,
arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
}) = segments.first()
{
if ident == "Option" {
if let Some(GenericArgument::Type(ty)) = args.first() {
return Ok(FieldType::Optional(ty));
}
} else if ident == "Vec" {
if let Some(each) = each_attr(&attrs) {
if let Some(GenericArgument::Type(ty)) = args.first() {
return Ok(FieldType::Repeated(ty, each?));
}
}
}
}
}
return Ok(FieldType::Required(ty));
}
fn each_attr(attrs: &Vec<Attribute>) -> Option<Result<Ident>> {
let mut each: Option<Result<Ident>> = None;
for attr in attrs {
if !attr.path().is_ident("builder") {
continue;
}
if let Meta::List(meta_list) = &attr.meta {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("each") {
let value = meta.value()?;
let s: LitStr = value.parse()?;
each = Some(Ok(format_ident!("{}", s.value())));
} else {
each = Some(Err(Error::new_spanned(
meta_list,
"expected `builder(each = \"...\")`",
)));
}
Ok(())
});
}
}
each
}
最後に
お疲れ様でした。これであなたもマクロが怖くなくなったはずです。poc-macro-workshopにはこれ以外にもたくさんの練習問題が用意されているので、興味がある方はぜひ挑戦してみてください。
Discussion