はじめに Rust のマクロ 宣言的マクロ 手続き的マクロ proc_macro_workshop proc_macro_workshop について proc_macro_workshop の進め方 derive(Builder) マクロを作る マクロの処理の流れ マクロを開発する上での Tips dbg!マクロの使用 cargo-expand 01-parse 目標 実装方針 実装 02-create-builder 目標 実装方針 実装 空のbuilder関数をCommand構造体に実装する CommandBuilderを返すようにbuilder関数の実装を変更する 構造体名、フィールド名に応じた Builder 構造体を生成する 03-call-setters 目標 実装方針 実装 04-call-build 目標 実装方針 実装 05-method-chaining 目標 実装方針 実装 まとめ 参考文献 はじめに この記事では proc_macro_workshop というリポジトリを使って Rust の手続き的マクロの作り方を学んでいきます。想定している読者は以下のような方です。 Rust の基本的な文法や概念(トレイトや所有権、ライフタイムなど)を知っている 手続き的マクロの作り方について知りたい この記事では以下のことを説明します。 Rust のマクロの概要 手続き的マクロ( derive マクロ)の作り方 proc_macro_workshop の進め方 また、この記事では以下のことは説明 しません 。 Rust の基本的な文法や概念 宣言的マクロの作り方 Rust のマクロ Rust のマクロには宣言的マクロと手続き的マクロの 2 種類があります。 宣言的マクロ 宣言的マクロは macro_rules! 構文を使用して定義されるマクロで、match 式に似た形で処理内容を定義します。 vec! や println! など普段良く使うマクロも宣言的マクロです。 宣言的マクロについては The Rust Programming Language 日本語版 に vec! マクロを例にした解説があるので、そちらをご覧ください。 手続き的マクロ 手続き的マクロはトレイトのデフォルト実装の導出に用いる derive マクロなどに代表されるマクロです。宣言的マクロよりも複雑な処理を記述できることが特徴で、以下の 3 種類があります。 derive マクロ 構造体やenumに付与することでその構造体やenumに追加の処理を実装できます 関数風マクロ 関数風マクロは宣言的マクロのように関数呼び出しと似た形で使用できるマクロです。宣言的マクロと比較するとより複雑な処理を記述できます 属性風マクロ deriveマクロと同様に付与した対象に対して追加の処理を実装できますが、こちらは構造体やenumだけでなく関数などにも適用できます。テストを書くときに用いる #[test] アトリビュートなどがこれにあたります 手続き的マクロについては The Rust Programming Language 日本語版 により詳しい説明があるのですが、これだけで実際に使えるような手続き的マクロを作り始めるのは難しいかと思います。この記事では proc_macro_workshop というリポジトリを使って実際に手を動かしながら手続的マクロの作り方を学んでいきます。 proc_macro_workshop proc_macro_workshop について proc_macro_workshop には作成するマクロの種類によって 5 つのプロジェクトがあります。 derive マクロ derive(Builder) derive マクロ derive(CustomDebug) 関数風マクロ seq! 属性風マクロ #[sorted] 属性風マクロ #[bitfield] これらのマクロの内容については proc_macro_workshop リポジトリ をご覧ください。 この記事では、Builder パターンに必要な実装を導出する derive(Builder) プロジェクトを進めつつ手続き的マクロの作成方法を学んでいきます。 derive(Builder) プロジェクトを進めると最終的には以下のような使い方のできるマクロが完成します。 use derive_builder :: Builder; #[derive(Builder)] pub struct Command { executable: String , #[builder(each = "arg" )] args: Vec < String > , #[builder(each = "env" )] env: Vec < String > , current_dir: Option < String > , } fn main () { let command = Command :: builder () . executable ( "cargo" . to_owned ()) . arg ( "build" . to_owned ()) . arg ( "--release" . to_owned ()) . build () . unwrap (); } proc_macro_workshop の進め方 proc_macro_workshop の各プロジェクトは以下の流れで進めていきます。 テストケースを追加する tests/progress.rs に記載されているテストケースのコメントアウトを解除する 追加したテストケースをパスするようなマクロを実装する src/lib.rs に実装します テストをパスしたら 1.に戻る proc_macro_workshop の各プロジェクトにはそれぞれのマクロがパスすべきテストが記載されており、これらのテストに通過するようなマクロを実装することが目標となります。 derive(Builder) プロジェクトの場合は builder/tests ディレクトリにテストケースが記載されたファイルが格納されています。各ステップでテストケースを 1 つずつ増やしていき、これに対応した機能を実装してきます。 プロジェクトの進捗は tests/progress.rs を用いて管理します。 tests/progress.rs は以下のようになっており、各行がそれぞれのテストケースに対応しています。各ステップでテストケースのコメントアウトを解除し、それぞれのテストを通過するように実装を進めていきます。 #[test] fn tests () { let t = trybuild :: TestCases :: new (); t. pass ( "tests/01-parse.rs" ); //t.pass("tests/02-create-builder.rs"); //t.pass("tests/03-call-setters.rs"); //t.pass("tests/04-call-build.rs"); //t.pass("tests/05-method-chaining.rs"); //t.pass("tests/06-optional-field.rs"); //t.pass("tests/07-repeated-field.rs"); //t.compile_fail("tests/08-unrecognized-attribute.rs"); //t.pass("tests/09-redefined-prelude-types.rs"); } 各テストケースにおける目標はテストケースが記載されているファイルのコメントに書いてあります。実際に作業する際はこれらのコメントを参考にしつつ実装していくことになります。 テストは builder ディレクトリで cargo test コマンドを実行することで実行できます。 derive(Builder) マクロを作る derive(Builder) プロジェクトでは大まかに以下のような流れでマクロを作成していきます。 マクロのひな型を用意する 01-parse で作業します builder パターンを実現するために必要な機能(setter メソッドなど)を実装する 02-create-builder ~ 05-method-chaining で作業します エラーハンドリングなどを実装する 06-optional-field ~ 09-redefined-prelude-types で作業します それでは実際にマクロを書いていきましょう。以降の各節の名前はテストファイルの名称と対応しています。以降の各節では以下の流れで進めていきます。 ステップの目標を確認する 実際には具体的なテストケースがありますが、すべて記載すると煩雑なので各ステップのゴールのみ記載します 実装方針の説明 実装 各ステップの最後にテストケースをパスする実装例を提示します マクロの処理の流れ Rust の手続き型マクロの処理は基本的に以下のようになっています。本記事ではこの内容を具体的に実装していきます。 トークン 列を入力として受け取る 受け取ったトークン列を構文木に変換する 変換した構文木をもとに処理を行い所望の構文木を得る 得られた構文木をトークン列に変換して返す トークン列と構文木の相互変換には syn クレート と quote クレート がよく使われます。この記事でもこれらのクレートを用いてマクロを実装します。 syn クレートはトークン列から構文木への変換に使用します。また、 syn クレートには構文木に関する構造体が定義されており、パースして得られた構文木はこれらの構造体のインスタンスとして保持されます。 quote クレートは構文木からトークン列への変換に使用します。 マクロを開発する上での Tips dbg! マクロの使用 マクロのデバッグをする際などには構文木の中身が実際にどうなっているかを知りたくなることがあるかもしれません。 syn クレートの extra-traits フィーチャー を有効にするとこれらの構造体に Debug トレイトが実装されて dbg! マクロを使ったデバッグができます。開発する上で便利なので開発中は有効にしておくと良いでしょう。 syn = { version = "1.0.86" , features = [ "extra-traits" ] } cargo-expand cargo expand を使うとマクロによって生成されたコードを出力できます。これもデバッグに便利なので詰まった時は使うと良いでしょう。 proc-macro-workshop の GitHub リポジトリ と cargo-expand の GitHub リポジトリ に使い方の説明があるので、使う場合はこちらを参照してください。 01-parse 目標 空の derive マクロを作る 実装方針 テストが実行されるのは tests/01-parse.rs の main 関数ですが、このステップでは何も記載がありません。ですので、特に何もしなくてもテストをパスする…かというとそうではなく、 #[derive(Builder)] が使われた際に呼ばれる derive マクロが存在している必要があります。 なぜかというと、テストコード中で以下のように構造体に対して derive マクロが呼ばれているためです。 #[derive(Builder)] pub struct Command { executable: String , args: Vec < String > , env: Vec < String > , current_dir: String , } マクロの基本的な処理内容についてはすでに説明しましたが、より具体的には以下のような流れになっています。 トークン列 proc_macro::TokenStream (以降では基本的に TokenStream と記載します)を引数として受け取る syn::parse_macro_input! マクロでパースして構文木にする パースした構文木を元に所望の構文木を生成する 生成した構文木を TokenStream に変換して返す このステップではマクロが存在していればよいので、1.と 4.の処理を実装すればテストをパスします。 実装 それでは実際に実装をしていきます。 まずは derive マクロの定義から始めます。関数に proc_macro_derive アトリビュートをつけることでその関数内に記載された処理が derive マクロの実装になります。 derive マクロのシグネチャは (TokenStream) -> TokenStream になっており、実態は Rust のトークンを TokenStream という構造体で受け取り、内部で処理した後に TokenStream という構造体として返す関数です。 #[proc_macro_derive(Builder)] pub fn derive (input: TokenStream) -> TokenStream {} では関数の中身を実装してきましょう。最低限テストが通るためにはこの関数から TokenStream を返せば良いので、以下のように TokenStream を新しく作って返してやれば OK です。 #[proc_macro_derive(Builder)] pub fn derive (input: TokenStream) -> TokenStream { proc_macro :: TokenStream :: new () } このままでも良いのですが、次のステップへの準備として入力をパースできるようにもしておきましょう。入力のパースには syn::parse_macro_input! マクロを使います。derive マクロの入力は syn::DeriveInput で定義される構造をしているので 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); proc_macro :: TokenStream :: new () } 02-create-builder 目標 CommandBuilder 構造体のインスタンスを返す builder 関数を Command 構造体に実装する 実装方針 最終的には以下のような CommandBuilder 構造体のインスタンスを返す builder 関数を Command 構造体に実装するのが目的です。 pub struct CommandBuilder { executable: Option < String > , args: Option < Vec < String >> , env: Option < Vec < String >> , current_dir: Option < Vec < String >> , } 必要な作業は大きく分けて以下の 2 つです。 builder 関数を Command 構造体に実装する CommandBuilder 構造体の定義を実装する 汎用的に使えるようにするには Builder 構造体の構造体名やフィールドの名前、型は derive マクロが適用される構造体に応じたものにする必要がある これを同時に全部進めるのはたいへんですので、以下の順に進めていきます。 空の builder 関数を Command 構造体に実装する CommandBuilder を返すように builder 関数の実装を変更する 構造体名、フィールド名に応じた Builder 構造体を生成する 実装 空の builder 関数を Command 構造体に実装する 前のステップで説明したように、手続き的マクロの処理の概要は以下の通りです。1.と 2.は前のステップで実装しました。このステップでは 3.と 4.の処理を実装していきます。 まずは空の builder 関数を実装する処理を書いていきましょう。この段階では入力について気にする必要はありません。 proc_macro::TokenStream (以降では基本的に TokenStream と記載します)を引数として受け取る syn::parse_macro_input! マクロでパースして構文木にする パースした構文木を元に所望の構文木を生成する 生成した構文木を TokenStream に変換して返す トークン列を生成するためには quote クレートの quote! マクロを使います。以下のように Rust のコードと(ほぼ)同じ構文で記述でき、この内容がトークン列に変換されます。 #[proc_macro_derive(Builder)] pub fn derive (input: TokenStream) -> TokenStream { let _input = parse_macro_input! (input as DeriveInput); let expand = quote! { impl Command { pub fn builder () {} } }; proc_macro :: TokenStream :: new () } このままだと proc_macro::TokenStream::new() によって生成された空のトークン列が戻り値として返るので、生成したトークン列を返すようにしましょう。 quote! マクロを使うと proc_macro::TokenStream ではなく proc_macro2::TokenStream が生成されます。 proc_macro2::TokenStream は proc_macro2 クレートによって提供されるトークン列です。 proc_macro::TokenStream が Rust コンパイラの使用するトークン列です。 proc_macro2::TokenStream は proc_macro::TokenStream::from で proc_macro::TokenStream に変換できるので、以下のようにして生成したトークン列を返します。 #[proc_macro_derive(Builder)] pub fn derive (input: TokenStream) -> TokenStream { let _input = parse_macro_input! (input as DeriveInput); let expand = quote! { impl Command { pub fn builder () {} } }; proc_macro :: TokenStream :: from (expand) } この段階でのマクロの実装は以下のようになります。 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 expand = quote! { impl Command { pub fn builder () {} } }; proc_macro :: TokenStream :: from (expand) } CommandBuilder を返すように builder 関数の実装を変更する 前のステップで実装した builder 関数は空ですので、このままでは何もしてくれません。次の段階として、ハードコードされた CommandBuilder 構造体を返すように builder 関数の実装を変更してみましょう。 まずは CommandBuilder 構造体の定義を quote! マクロ内に追加します。 let expand = quote! { pub struct CommandBuilder { executable: Option < String > , args: Option < Vec < String >> , env: Option < Vec < String >> , current_dir: Option < Vec < String >> , } impl Command { pub fn builder () {} } }; 次に builder 関数内からデフォルト値でフィールドを埋めた CommandBuilder を返すように変更します。 CommandBuilder のフィールドはすべて Optional なので None で埋めておきます。 let expand = quote! { ...略... impl Command { pub fn builder () -> CommandBuilder { CommandBuilder { executable: None , args: None , env: None , current_dir: None , } } } }; 最終的な実装は以下のようになります。 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 expand = quote! { pub struct CommandBuilder { executable: Option < String > , args: Option < Vec < String >> , env: Option < Vec < String >> , current_dir: Option < Vec < String >> , } impl Command { pub fn builder () -> CommandBuilder { CommandBuilder { executable: None , args: None , env: None , current_dir: None , } } } }; proc_macro :: TokenStream :: from (expand) } 構造体名、フィールド名に応じた Builder 構造体を生成する さて、ここまでで Command 構造体用の Builder 構造体を返すマクロができました。ただ、このままだとほかの構造体に対して使用できません。derive マクロが適用された構造体の名前やフィールドに応じて適した実装を与えるようにしたいです。Builder 構造体の要件をまとめると以下のようになります。 構造体の名前は derive マクロの適用された構造体名の末尾に Builder を付けたものとする フィールド名は derive マクロの適用された構造体のフィールド名と同じものを使う フィールドの型は derive マクロの適用された構造体のフィールドの型を Optional にしたものとする このような要件を満たすためには derive マクロが適用された構造体の情報を取得する必要があります。では、どこから取得すれば良いのでしょうか。derive マクロの入力をパースすると以下のような syn::DeriveInput 構造体が得られるのですが、実はこの構造体の中に必要な情報が入っています。 pub struct DeriveInput { pub attrs: Vec < Attribute > , pub vis: Visibility, pub ident: Ident, pub generics: Generics, pub data: Data, } このステップに関係する部分は以下の 3 つです。 vis は構造体の可視性の情報を保持しています ident は identifier の略で、変数名などの Rust コード中の識別子の情報を保持しています。ここでは derive マクロが付与された構造体/enum の名前を保持しています data は構造体の保持するフィールドの情報を持っています 構造体名は ident を使えば取得できそうです。derive マクロの使われた構造体名の末尾に Builder を付けたものを Builder 構造体の名前にしたいので、 format_ident! マクロを使って末尾に Builder を結合します。 format_ident! マクロはこのように新たな識別子を作るときに使用します。詳しくは quote クレートの GitHub リポジトリ をご覧ください。 let ident = input.ident; let builder_name = format_ident! ( "{}Builder" , ident); 構造体名についてはこれで良さそうです。次はフィールド名と型を取得しましょう。フィールドの情報は data フィールドに入っているのでした。 syn::Data は以下のような enum です。今回扱うのは構造体なので DataStruct 構造体の中身を見てみましょう。 pub enum Data { Struct (DataStruct), Enum (DataEnum), Union (DataUnion), } syn::DataStruct はこうなっています。 fields がフィールドの情報を持っています。 pub struct DataStruct { pub struct_token: Struct, pub fields: Fields, pub semi_token: Option < Semi > , } Fields は以下のような enum です。 Named が名前のついたフィールドです。今回は Unit 構造体やタプル構造体はサポートしないので Named だけ見てみましょう。 pub enum Fields { Named (FieldsNamed), Unnamed (FieldsUnnamed), Unit, } FieldsNamed はフィールド情報を持っている Field 構造体を複数保持しており、 named.iter() で Field 構造体のインスタンスをイテレートできます。 pub struct FieldsNamed { pub brace_token: Brace, pub named: Punctuated < Field, Comma > , } syn::Field 構造体はこうなっています。 ident がフィールド名で ty が型の情報を持っています。ようやく必要な情報までたどり着きました。 pub struct Field { pub attrs: Vec < Attribute > , pub vis: Visibility, pub ident: Option < Ident > , pub colon_token: Option < Colon > , pub ty: Type, } パターンマッチを使って必要な情報だけを取り出します。サポートしないケースについては今は panic! させておきましょう(後のステップでちゃんとコンパイルエラーが出るようにします)。この後で使いやすいように ident と ty は Vec に格納しておきます。 let (idents, types): ( Vec < Ident > , Vec < Type > ) = match input.data { Data :: Struct (data) => match data.fields { Fields :: Named (fields) => fields .named . into_iter () . map ( | field | { let ident = field.ident; let ty = field.ty; (ident. unwrap (), ty) }) . unzip (), _ => panic! ( "no unnamed fields are allowed" ), }, _ => panic! ( "expects struct" ), }; さて、必要なものはすべて準備できました。これらを使ってマクロの出力として返すトークン列を生成しましょう。 この前のステップで quote! マクロについて Rust のコードと(ほぼ)同じ構文で記述することができ と説明しましたが、 quote! マクロの内部では特殊な記法を使うことができます。たとえば、 quote::ToTokens トレイトを実装する変数に quote! マクロの中で # をつけるとその変数の内容が挿入されます。これを使って適切な名前や型のフィールドを持った構造体を動的に生成します。今までに出てきた syn::Ident などの構造体は quote::ToTokens トレイトを実装しているので特に気にせず使うことができます。 これを使って CommandBuilder 構造体の構造体名を書き直すと以下のようになります。 quote! { #vis struct #builder_name { executable: Option < String > , args: Option < Vec < String >> , env: Option < Vec < String >> , current_dir: Option < Vec < String >> , } } また、 quote! マクロの中では #(...)* のような形で IntoIterator を実装した型の変数を繰り返して挿入できます。これを使って CommandBuilder 構造体のフィールド名も書き換えると以下のようになります。 idents と types はそれぞれフィールド名と型を格納したベクタですので、以下のようにしてベクタの各値について繰り返し展開できます。 ) と * の間に文字を記述するとその文字で区切って展開してくれるので、以下では #(...),* のようにして構造体のフィールドをベクタから生成しています。 quote! { #vis struct #builder_name { #( #idents : Option < #types > ), * } } さて、これでハードコードされた構造体名やフィールド名などをすべて取り除くことができました。 同じように builder 関数も書き換えると以下のようになります。 quote! { #vis struct #builder_name { #( #idents : Option < #types > ), * } impl #ident { pub fn builder () -> #builder_name { #builder_name { #( #idents : None ), * } } } } 以上をまとめると最終的なマクロの実装は以下のようになります。 use proc_macro :: TokenStream; use quote :: {format_ident, quote}; use syn :: {parse_macro_input, Data, DeriveInput, Fields, Ident, Type}; #[proc_macro_derive(Builder)] pub fn derive (input: TokenStream) -> TokenStream { let input = parse_macro_input! (input as DeriveInput); let ident = input.ident; let vis = input.vis; let builder_name = format_ident! ( "{}Builder" , ident); let (idents, types): ( Vec < Ident > , Vec < Type > ) = match input.data { Data :: Struct (data) => match data.fields { Fields :: Named (fields) => fields .named . into_iter () . map ( | field | { let ident = field.ident; let ty = field.ty; (ident. unwrap (), ty) }) . unzip (), _ => panic! ( "no unnamed fields are allowed" ), }, _ => panic! ( "expects struct" ), }; let expand = quote! { #vis struct #builder_name { #( #idents : Option < #types > ), * } impl #ident { pub fn builder () -> #builder_name { #builder_name { #( #idents : None ), * } } } }; proc_macro :: TokenStream :: from (expand) } 03-call-setters 目標 CommandBuilder 構造体の各フィールドに対する setter メソッドを作る 実装方針 具体的な実装から考えるとやりやすいです。 Command 関数の executable フィールドを例にとると setter は以下のようになるので、これを前のステップと同様、変数の展開や繰り返しを使って書き直しましょう。 pub fn executable ( &mut self , executable: String ) -> &mut Self { self .executable = executable self } 実装 実装方針で書いた具体的なフィールドの実装を書き直しましょう。変数の展開や繰り返しの書き方はすでに説明した通りです。 #(...)* の括弧内には変数以外を入れることもできます。今回は関数の実装自体を #(...)* で繰り返してやれば良いでしょう。 impl #builder_name { #( pub fn #idents ( &mut self , #idents : #types ) -> &mut Self { self . #idents = Some ( #idents ); self }) * } 最終的な実装は以下の通りです。 use proc_macro :: TokenStream; use quote :: {format_ident, quote}; use syn :: {parse_macro_input, Data, DeriveInput, Fields, Ident, Type}; #[proc_macro_derive(Builder)] pub fn derive (input: TokenStream) -> TokenStream { let input = parse_macro_input! (input as DeriveInput); let ident = input.ident; let vis = input.vis; let builder_name = format_ident! ( "{}Builder" , ident); let (idents, types): ( Vec < Ident > , Vec < Type > ) = match input.data { Data :: Struct (data) => match data.fields { Fields :: Named (fields) => fields .named . into_iter () . map ( | field | { let ident = field.ident; let ty = field.ty; (ident. unwrap (), ty) }) . unzip (), _ => panic! ( "no unnamed fields are allowed" ), }, _ => panic! ( "expects struct" ), }; let expand = quote! { #vis struct #builder_name { #( #idents : Option < #types > ), * } impl #builder_name { #( pub fn #idents ( &mut self , #idents : #types ) -> &mut Self { self . #idents = Some ( #idents ); self }) * } impl #ident { pub fn builder () -> #builder_name { #builder_name { #( #idents : None ), * } } } }; proc_macro :: TokenStream :: from (expand) } 04-call-build 目標 Command 構造体のインスタンスを返す build メソッドを CommandBuilder 構造体に実装する CommandBuilder の各フィールドが None の場合はエラーを返すようにする 実装方針 これも具体的な実装から考えるとわかりやすいです。 Command 構造体の場合は以下のようになるので、これを変数の展開や繰り返しを使って書き直しましょう。 impl CommandBuilder { ...略... pub fn build () -> Result < Command, Box < dyn Error >> { if self .executable. is_none () { return Err (...略...) } ...略... if self .current_dir. is_none () { return Err (...略...) } Command { executable: self .executable. clone (). unwrap (), args: self .args. clone (). unwrap (), env: self .env. clone (). unwrap (), current_dir: self .current_dir. clone (). unwrap (), } } } 実装 ガード節をまず作ります。このように quote! を使って部分的にトークン列を作って後で組み合わせることもできます。 let checks = idents. iter (). map ( | ident | { let err = format! ( "Required field '{}' is missing" , ident. to_string ()); quote! { if self . #ident . is_none () { return Err ( #err . into ()) } } }); 前のステップと同じようにして build 関数を作ります。上で作ったガード節を #(#checks)* で展開しています。 let expand = quote! { ...略... pub fn build ( &mut self ) -> Result < #ident , Box < dyn std :: error :: Error >> { #( #checks ) * Ok ( #ident { #( #idents : self . #idents . clone (). unwrap ()), * }) } ...略... } 最終的な実装は以下の通りです。 use proc_macro :: TokenStream; use quote :: {format_ident, quote}; use syn :: {parse_macro_input, Data, DeriveInput, Fields, Ident, Type}; #[proc_macro_derive(Builder)] pub fn derive (input: TokenStream) -> TokenStream { let input = parse_macro_input! (input as DeriveInput); let ident = input.ident; let vis = input.vis; let builder_name = format_ident! ( "{}Builder" , ident); let (idents, types): ( Vec < Ident > , Vec < Type > ) = match input.data { Data :: Struct (data) => match data.fields { Fields :: Named (fields) => fields .named . into_iter () . map ( | field | { let ident = field.ident; let ty = field.ty; (ident. unwrap (), ty) }) . unzip (), _ => panic! ( "no unnamed fields are allowed" ), }, _ => panic! ( "expects struct" ), }; let checks = idents. iter (). map ( | ident | { let err = format! ( "Required field '{}' is missing" , ident. to_string ()); quote! { if self . #ident . is_none () { return Err ( #err . into ()) } } }); let expand = quote! { #vis struct #builder_name { #( #idents : Option < #types > ), * } impl #builder_name { #( pub fn #idents ( &mut self , #idents : #types ) -> &mut Self { self . #idents = Some ( #idents ); self }) * pub fn build ( &mut self ) -> Result < #ident , Box < dyn std :: error :: Error >> { #( #checks ) * Ok ( #ident { #( #idents : self . #idents . clone (). unwrap ()), * }) } } impl #ident { pub fn builder () -> #builder_name { #builder_name { #( #idents : None ), * } } } }; proc_macro :: TokenStream :: from (expand) } 05-method-chaining 目標 CommandBuilder 構造体でメソッドチェーンを使えるようにする 実装方針 03-call-setters ステップで実装した setter は &mut Self を返すようになっているので、実はすでにメソッドチェーンを使えるようになっています。そのため、このステップでは追加の実装をする必要はありません。 実装 このステップでは追加の実装は必要ありません。このステップ用のテストを実行して変更なしでテストをパスすることをチェックしてみましょう。 まとめ 前編では Builder パターンを実現するのに最低限必要な機能を持ったマクロを作りました。 しかし、まだ Optional なフィールドの取り扱いに不十分な点があったり、ベクタ型のフィールドの取り扱いに改善の余地があったりします。後編ではそのあたりの機能を実装していきます。 後編へ続く。 参考文献 proc-macro-workshop/builder に取り組む Rust の procedural macro を操って黒魔術師になろう〜proc-macro-workshop の紹介 実践 Rust プログラミング入門 syn - Docs.rs quote - Docs.rs