「proc-macro-workshop」で学ぶ手続き型マクロ

にあえん

October 10, 2023

Rustにハマりつつあるナナオです。

最近Rustばかり触っているのですが、Rustはpythonと比べるととにかくコーディングに時間がかかってしまうのが欠点だなと思うこの頃…

自前の構造体とか作って運用してると、気をつけないと同じよーな処理がプロジェクト内に点在してしまいがちだなと感じています。

(deriveを活用できていないケースとか)

そこで今回はRustのメタプログラミングツール、マクロについて学んでいこうと思います。

-> 僕がマクロを学ぶきっかけになった記事はこちら

前提知識となるマクロの参考資料は以下。

マクロ - The Rust Programming Language 日本語版

Rustのマクロを覚える #Rust - Qiita

マクロのチュートリアルを実践する

マクロを学ぶためのちょうどいいリポジトリがありました。

GitHub - dtolnay/proc-macro-workshop: Learn to write Rust procedural macros [Rust Latam conference, Montevideo Uruguay, March 2019]

マクロを使用するパターンについて、いくつかのケーススタディが用意されています。

とりあえず聞き馴染みのあるbuilderからやってみます。

9つテストケースが用意されており、一つづつパスできるようにしていくことで、段階的にBuilderマクロを実装できるという内容になっています。

わかりやすい。。

ここからは自分の備忘録も兼ねて答えとなるコードを載せていきます。

もし自分で解きたいと言う場合はこれから先の閲覧は注意してください。

01-parse.rs

これはとりあえずTokenStreamを返却できればいいだけですね。

必要なライブラリを追加してあげます。

cargo add syn quote

とりあえずquote!マクロの結果を返却するだけでこのテストは通ります。

use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _ = input;

    let q = quote! {}
    q.into()
}

02-create-builder.rs

さて、ここからが本番です。

マクロを使用してビルダーを作成するようにしましょう。

quoteのドキュメントを参考にして実装しました。

quote in quote - Rust

quote!マクロ内に構造体のフィールド名を入れたりするためには、一度変数として取り出してあげる必要があるんですね。

(メソッドチェーンなどをやってしまうと、それも含めてマクロとして出力してしまうため)

以下のように実装しました。

use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::DeriveInput;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let derive_input = syn::parse_macro_input!(input as DeriveInput);

    let struct_data = match &derive_input.data {
        syn::Data::Struct(v) => v,
        _ => {
            return syn::Error::new_spanned(
                &derive_input.ident,
                "Must be struct type",
            ).to_compile_error().into();
        }
    };

    let struct_name = &derive_input.ident;
    let builder_name = format_ident!("{}Builder", &derive_input.ident);

    let mut builder_field = vec![];
    let mut builder_method_field = vec![];
    for field in &struct_data.fields {
        let field_name = field.ident.as_ref().unwrap();
        let ty = &field.ty;
        builder_field.push(quote! { #field_name: Option<#ty> });
        builder_method_field.push(quote! { #field_name: None });
    }

    let q = quote! {
        pub struct #builder_name {
            #(#builder_field),*
        }

        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#builder_method_field),*
                }
            }
        }
    };

    println!("{}", q.to_string());

    q.into()
}

03-call-setters.rs

あんまり変わらないですけど、セッターを追加しました。

use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::DeriveInput;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let derive_input = syn::parse_macro_input!(input as DeriveInput);

    let struct_data = match &derive_input.data {
        syn::Data::Struct(v) => v,
        _ => {
            return syn::Error::new_spanned(
                &derive_input.ident,
                "Must be struct type",
            ).to_compile_error().into();
        }
    };

    let struct_name = &derive_input.ident;
    let builder_name = format_ident!("{}Builder", &derive_input.ident);

    let mut builder_field = vec![];
    let mut builder_method_field = vec![];
    // 追加
    let mut builder_setter_method = vec![];
    for field in &struct_data.fields {
        let field_name = field.ident.as_ref().unwrap();
        let ty = &field.ty;
        builder_field.push(quote! { #field_name: Option<#ty> });
        builder_method_field.push(quote! { #field_name: None });

        // 追加
        builder_setter_method.push(quote! {
            fn #field_name(&mut self, #field_name: #ty) -> &mut Self {
                self.#field_name = Some(#field_name);
                self
            }
        });
    }

    let q = quote! {
        pub struct #builder_name {
            #(#builder_field),*
        }

        // 追加
        impl #builder_name {
            #(#builder_setter_method)*
        }

        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#builder_method_field),*
                }
            }
        }
    };

    q.into()
}

04-call-build.rs

この辺からだんだんデバッグどうしようかなぁ…と思って、適当に使い回せるCargoパッケージ作って一旦CommandBuilderを実装してみて動くかどうか確認してから実装するようになりました。

use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::DeriveInput;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let derive_input = syn::parse_macro_input!(input as DeriveInput);

    let struct_data = match &derive_input.data {
        syn::Data::Struct(v) => v,
        _ => {
            return syn::Error::new_spanned(
                &derive_input.ident,
                "Must be struct type",
            ).to_compile_error().into();
        }
    };

    let struct_name = &derive_input.ident;
    let builder_name = format_ident!("{}Builder", &derive_input.ident);

    let mut builder_field = vec![];
    let mut builder_setter_method = vec![];
    let mut struct_fields = vec![];
    for field in &struct_data.fields {
        let field_name = field.ident.as_ref().unwrap();
        struct_fields.push(field_name);

        let ty = &field.ty;
        builder_field.push(quote! { #field_name: Option<#ty> });

        builder_setter_method.push(quote! {
            fn #field_name(&mut self, #field_name: #ty) -> &mut Self {
                self.#field_name = Some(#field_name);
                self
            }
        });
    }

    let q = quote! {
        pub struct #builder_name {
            #(#builder_field),*
        }

        impl #builder_name {
            #(#builder_setter_method)*

            pub fn build(&mut self) -> Result<#struct_name, Box<dyn std::error::Error>> {
                let _struct = #struct_name {
                    #(#struct_fields: self.#struct_fields.take().unwrap()),*
                };
                Ok(_struct)
            }
        }

        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#struct_fields: None),*
                }
            }
        }
    };

    q.into()
}

05-method-chaining.rs

前のコードが動けばパスします。

(説明見たら「おまけコード」と書いてありました)

06-optional-field.rs

これは少し難しかったので、他の方の実装を参考にして実装しました。

proc_macro_workshopでRustの手続き的マクロに入門する 後編 - CADDi Tech Blog

この実装をかいつまんで言うと、構造体の中でOptionが設定されているフィールドに関しては別の処理をさせればいいです。

実装は以下のとおりです。(だんだん長くなってきた)

use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::DeriveInput;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let derive_input = syn::parse_macro_input!(input as DeriveInput);

    let struct_data = match &derive_input.data {
        syn::Data::Struct(v) => v,
        _ => {
            return syn::Error::new_spanned(
                &derive_input.ident,
                "Must be struct type",
            ).to_compile_error().into();
        }
    };

    let struct_name = &derive_input.ident;
    let builder_name = format_ident!("{}Builder", &derive_input.ident);

    let mut builder_field = vec![];
    // Vecを追加
    let mut item_construct_fields = vec![];
    let mut builder_setter_method = vec![];
    let mut struct_fields = vec![];
    for field in &struct_data.fields {
        let field_name = field.ident.as_ref().unwrap();
        struct_fields.push(field_name);

        let mut ty = &field.ty;

        // 構造体のフィールドの型がOptionかどうか判定
        // Optionの場合、Some([Optionのジェネリクス型])を返却するようにしています。
        let option_generics: Option<&syn::Type> = match &field.ty {
            syn::Type::Path(p) => {
                if p.path.segments[0].ident.to_string() == "Option" {
                    let seg = p.path.segments.first().unwrap();
                    match &seg.arguments {
                        syn::PathArguments::AngleBracketed(ref abga) => {
                            abga.args.first().and_then(|args| match args {
                                &syn::GenericArgument::Type(ref ty) => Some(ty),
                                _ => None,
                            })
                        },
                        _ => None,
                    }
                } else {
                    None
                }
            },
            _ => {
                None
            }
        };

        if option_generics.is_some() {
            // Option型のジェネリクスを再設定してあげる
            ty = option_generics.unwrap();
            // 元がOptionなのでそのまま返却する
            item_construct_fields.push(quote! {
                #field_name: self.#field_name.take()
            });
        } else {
            item_construct_fields.push(quote! {
                #field_name: self.#field_name.take().unwrap()
            });
        }

        builder_field.push(quote! { #field_name: Option<#ty> });

        builder_setter_method.push(quote! {
            fn #field_name(&mut self, #field_name: #ty) -> &mut Self {
                self.#field_name = Some(#field_name);
                self
            }
        });
    }

    let q = quote! {
        pub struct #builder_name {
            #(#builder_field),*
        }

        impl #builder_name {
            #(#builder_setter_method)*

            pub fn build(&mut self) -> Result<#struct_name, Box<dyn std::error::Error>> {
                let _struct = #struct_name {
                    #(#item_construct_fields),*
                };
                Ok(_struct)
            }
        }

        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#struct_fields: None),*
                }
            }
        }
    };

    q.into()
}

あとデバッグに悩んだんですが、ここにある「cargo-expand」を利用することでかなり楽になりました。

[Rust] Procedural Macroの仕組みと実装方法

まとめ

まだチュートリアルは残っているんですが、この後からかなり難しくなってきて解くのにだいぶ時間がかかっているんで、今回はここまでにします。

でもかなり面白い内容なので、ほかのもやってみたいですね。