2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust 手続きマクロ 第四の神器 darling

Last updated at Posted at 2024-12-25

こちらの記事は Rustマクロ冬期講習アドベントカレンダー 23日目の記事です!

アドカレまとめ記事はこちら!: Rustマクロ作成チートシート!

属性風マクロ及びderiveマクロの属性情報を入力でパースするのはなかなか面倒でした。今回はRust手続きマクロ界の serde や clap クレートこと darling を使うと楽に入力が受け取れるよ、ということを紹介したいと思います!

本記事で伝えたいこと

darling クレートを使用すると、syn::parse::Parse を手動実装せず自動実装で 宣言的に ( serde や clap クレートのように) 属性や入力を扱えるようになる!

★ 属性風マクロで効果的

  • FromMeta: #[my_macro(meta)]meta 部分を構造体に簡単にパースできるようになります!

★ deriveマクロで効果的

darling は属性風マクロ・deriveマクロの入力を構造体で宣言的に書けるようにするマクロを提供してくれているクレートです。Rust手続きマクロ三種の神器 proc-macro2, quote, syn に次ぐ 第四の神器 といっても過言ではないぐらい便利なクレートになります。

紹介するとは言ったのですがdarlingはドキュメントがちょっと物足りない感じで若干手探り感があります。今回は筆者が使いこなせた範囲の話をハンズオンの続き的な感じでしていきたいと思います!

darlingでリファクタ: 属性風マクロ「 dbg_stmts 」編

属性風マクロハンズオン において、

Rust
#[dbg_stmts(sep = "=========", fmt = "### {} ###")]

上記の属性風マクロの属性情報を

Rust
#[derive(Debug)]
pub struct DbgStmtsOption {
    pub sep: Option<LitStr>,
    pub fmt: LitStr,
}

このような構造体に変換するのに、手動で syn::parse::Parse トレイトを実装していました。

そのためには実に30行近い行のパースを自分で書かなければいけませんでした。

Rust
impl Parse for DbgStmtsOption {
    fn parse(input: ParseStream) -> Result<Self> {
        let meta_name_value_list = input.parse_terminated(MetaNameValue::parse, Token![,])?;

        let mut sep = None;
        let mut fmt = None;
        for MetaNameValue { path, value, .. } in meta_name_value_list {
            let Expr::Lit(ExprLit {
                lit: Lit::Str(lit), ..
            }) = value
            else {
                return Err(Error::new_spanned(value, "expected string literal"));
            };

            match path {
                p if p.is_ident("sep") => sep = Some(lit),
                p if p.is_ident("fmt") => fmt = Some(lit),
                _ => (),
            }
        }

        Ok(Self {
            sep,
            fmt: fmt.unwrap_or(LitStr::new("[dbg_stmt] {}", Span::mixed_site())),
        })
    }
}

しかしこの処理はかなり典型的な感じがします。すごいボイラープレート臭がします。ここでdarlingクレートの出番です!

FromMeta deriveマクロを DbgStmtsOption にderiveしてあげると、このような長い手続き的な実装を書くことなく、 TokenStream から DbgStmtsOption へ変換できるようになります!

Rust
use darling::{ast::NestedMeta, FromMeta};
use proc_macro2::{Span, TokenStream};
use syn::LitStr;

// deriveマクロのオプション部( sep = "...", fmt = "..." )を宣言的に表せる!
#[derive(Debug, FromMeta)]
pub struct DbgStmtsOption {
    pub sep: Option<LitStr>,
    #[darling(default = default_fmt)]
    pub fmt: LitStr,
}

fn default_fmt() -> LitStr {
    LitStr::new("[dbg_stmt] {}", Span::mixed_site())
}

impl DbgStmtsOption {
    pub fn from_token_stream(attr: TokenStream) -> syn::Result<Self> {
        // sep = "...", fmt = "..." というトークン列を darling::ast::NestedMeta でパース
        let attr_args = NestedMeta::parse_meta_list(attr)?;

        // NestedMeta から DbgStmtsOption を構成
        Ok(Self::from_list(&attr_args)?)
    }
}

実質10行ほどです!両者を比較するとデフォルト値を設定するための関数のみ残り「 MetaNameValue か?」や「目的の属性か?」を確認する部分を全て削ることができるようになっており、非常に読みやすくなりました!

src/lib.rs も含めた全体を見ても、マクロのロジックのみに集中できてイイカンジです、非常にserdeやclap味を感じます!

src/lib.rs
mod impls;

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, ItemFn};

use impls::{insert_println_between_stmts, DbgStmtsOption};

#[proc_macro_attribute]
pub fn dbg_stmts(attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut func = parse_macro_input!(item as ItemFn);
    let option = match DbgStmtsOption::from_token_stream(attr.into()) { // TokenStream化するためにmatchで分岐
        Ok(op) => op,
        Err(e) => return e.into_compile_error().into(),
    };

    insert_println_between_stmts(&mut func.block, option);

    func.into_token_stream().into()
}
src/impls.rs
use darling::{ast::NestedMeta, FromMeta};
use proc_macro2::{Span, TokenStream};
use syn::{parse_quote, Block, LitStr};

pub fn insert_println_between_stmts(
    block: &mut Block,
    DbgStmtsOption { sep, fmt }: DbgStmtsOption,
) {
    let mut stmts = block.stmts.iter().peekable();

    let mut res = Vec::new();

    while let Some(stmt) = stmts.next() {
        let mut v = Vec::with_capacity(4);

        v.push(parse_quote! { println!(#fmt, stringify!(#stmt)); });

        if let Some(sep) = &sep {
            v.push(parse_quote! { println!(#sep); });
        }

        v.push(stmt.clone());

        if let (true, Some(sep)) = (stmts.peek().is_some(), &sep) {
            v.push(parse_quote! { println!(#sep); });
        }

        res.extend(v);
    }

    block.stmts = res;
}

fn default_fmt() -> LitStr {
    LitStr::new("[dbg_stmt] {}", Span::mixed_site())
}

#[derive(Debug, FromMeta)]
pub struct DbgStmtsOption {
    pub sep: Option<LitStr>,
    #[darling(default = default_fmt)]
    pub fmt: LitStr,
}

impl DbgStmtsOption {
    pub fn from_token_stream(attr: TokenStream) -> syn::Result<Self> {
        let attr_args = NestedMeta::parse_meta_list(attr)?;

        Ok(Self::from_list(&attr_args)?)
    }
}

一旦 darling::ast::NestedMeta を経由しなければならないのがちょっと煩雑なぐらいでしょうか...? FromMeta だと守備範囲が広そうなので、メタリスト fmt = "...", sep = "..." からじゃないとパースできないという制約をつけるためには必要な実装なんだろうとは思います。

darlingでリファクタ: deriveマクロ「 enum_display 」編

deriveマクロハンズオン において、

Rust
#[derive(EnumDisplay)]
#[enum_display(rename_all = "snake_case")]
enum Hoge {
    Fuga,
    #[enum_display(rename = "barbar")]
    Bar,
}

上記のderiveマクロの入力( Hoge 構造体全体と不活性属性の付与情報)は syn::DeriveInput で受け取られていました。

syn::DeriveInput は入力のパースに関しては最低限の機能しか提供してくれていないため、 #[enum_display(rename_all = "...")] から Option<convert_case::Case> へのパースはその後手動で行う必要があります。

Rust
use convert_case::Case;
use syn::{Attribute, Expr, ExprLit, Lit, Meta, MetaList, MetaNameValue};

fn get_case(attrs: Vec<Attribute>) -> Option<Case> {
    let mut res = None;

    /* ここからボイラープレートな処理ばかり!!!! */

    for Attribute { meta, .. } in attrs {
        // hoge(...) という構造か?
        let Meta::List(MetaList { path, tokens, .. }) = meta else {
            continue;
        };

        // path: enum_display
        // tokens: (...) の中身

        // #[enum_display(...)] か?
        if !path.is_ident("enum_display") {
            continue;
        }

        // fuga = xxx という構造か?
        let Ok(MetaNameValue { path, value, .. }) = syn::parse2(tokens) else {
            continue;
        };

        // rename_all = xxx であるか?
        if !path.is_ident("rename_all") {
            continue;
        }

        // rename_all = "..." であるか?
        let Expr::Lit(ExprLit {
            lit: Lit::Str(s), ..
        }) = value
        else {
            continue;
        };

        /* ここまではどのマクロでも同じことをする!!!!!! */

        // rename_all = "..." の "..." に基づいて変換ケースを決定
        res = str2case(&s.value()).ok();
    }

    res
}

オプション名が rename_all であって文字列リテラルを受け取ってそこからオプションの値を決定する...なんていうことを確認するパース処理はお決まりです。さらにハンズオンでは各バリアントに付与される #[enum_display(rename = "...")] についても手動でパース処理を実装しています。

darling::FromDeriveInput を実装すれば、これらのパース処理の記述は全て不要になります!

Rust
use convert_case::Case;
use darling::ast::Data;
use darling::util::Ignored;
use darling::{FromDeriveInput, FromVariant};
use syn::Ident;

// 各バリアントで補足する情報
#[derive(FromVariant)]
#[darling(attributes(enum_display))] // #[enum_display] 不活性属性を捕捉する
struct DisplayVariant {
    ident: Ident, // バリアント名
    #[darling(default)]
    rename: Option<String>, // rename = "..."
}

// enum_displayを付与された構造体・列挙体全体の情報
#[derive(FromDeriveInput)]
#[darling(attributes(enum_display))] // #[enum_display] 不活性属性を捕捉する
struct EnumDisplayInput {
    data: Data<DisplayVariant, Ignored>, // 構造体側は使わないのでIgnore
    ident: Ident, // 列挙体名
    #[darling(map = "str2case", default = "default_case")]
    rename_all: Case, // rename_all = "..."
}

fn default_case() -> Case {
    Case::Camel
}

fn str2case(s: String) -> Case {
    match s.to_ascii_lowercase().as_str() {
        "upper" | "uppercase" | "upper_case" => Case::Upper,
        "lower" | "lowercase" | "lower_case" => Case::Lower,
        "camel" | "camelcase" | "camel_case" => Case::Camel,
        "pascal" | "pascalcase" | "pascal_case" => Case::Pascal,
        "snake" | "snakecase" | "snake_case" => Case::Snake,
        "kebab" | "kebabcase" | "kebab_case" => Case::Kebab,
        _ => Case::Pascal,
    }
}

属性風マクロの場合と同様大幅に記述を削減できています!

FromDeriveInput を実装した EnumDisplayInput 構造体は対象列挙体のバリアントの情報もパースした上で保持しています。 DeriveInput からのパース処理を気にせずに、出力のみにフォーカスした記述が可能になります!

以下全体

src/lib.rs
mod impls;

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(EnumDisplay, attributes(enum_display))]
pub fn enum_display_impl(item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as DeriveInput);

    impls::enum_display_impl(input)
        .unwrap_or_else(syn::Error::into_compile_error)
        .into()
}
src/impls.rs
use convert_case::{Case, Casing};
use darling::ast::Data;
use darling::util::Ignored;
use darling::{FromDeriveInput, FromVariant};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::Ident;
use syn::{spanned::Spanned, DeriveInput};

#[derive(FromVariant)]
#[darling(attributes(enum_display))]
struct DisplayVariant {
    ident: Ident,
    #[darling(default)]
    rename: Option<String>,
}

fn default_case() -> Case {
    Case::Camel
}

#[derive(FromDeriveInput)]
#[darling(attributes(enum_display))]
struct EnumDisplayInput {
    data: Data<DisplayVariant, Ignored>,
    ident: Ident,
    #[darling(map = "str2case", default = "default_case")]
    rename_all: Case,
}

pub fn enum_display_impl(input: DeriveInput) -> syn::Result<TokenStream> {
    let span = input.span();

    let EnumDisplayInput {
        data,
        ident: enum_ident,
        rename_all,
    } = EnumDisplayInput::from_derive_input(&input)?;

    let Data::Enum(enm) = data else {
        return Err(syn::Error::new(span, "expected enum"));
    };

    // 出力に必要な情報の収集はこの時点で全て完了

    // 以降出力の作成
    let tokens = enm
        .into_iter()
        .map(|DisplayVariant { ident, rename }| {
            let name = match rename {
                Some(name) => name.to_token_stream(),
                None => {
                    let ident = ident.to_string().to_case(rename_all);
                    ident.to_token_stream()
                }
            };

            Ok(quote! {
                Self::#ident => write!(f, #name),
            })
        })
        .collect::<syn::Result<Vec<_>>>()?;

    Ok(quote! {
        impl ::std::fmt::Display for #enum_ident {
            fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
                match self {
                    #(
                        #tokens
                    )*
                }
            }
        }
    })
}

fn str2case(s: String) -> Case {
    match s.to_ascii_lowercase().as_str() {
        "upper" | "uppercase" | "upper_case" => Case::Upper,
        "lower" | "lowercase" | "lower_case" => Case::Lower,
        "camel" | "camelcase" | "camel_case" => Case::Camel,
        "pascal" | "pascalcase" | "pascal_case" => Case::Pascal,
        "snake" | "snakecase" | "snake_case" => Case::Snake,
        "kebab" | "kebabcase" | "kebab_case" => Case::Kebab,
        _ => Case::Pascal,
    }
}

まとめ・所感

属性風マクロ・deriveマクロの作成をする上で面倒な入力のパースをしてくれる darling クレートの紹介でした!

上手く紹介できたか微妙というか、darlingクレートの良さは clap クレートでCLIアプリケーションを作ったことがあると感じられるんじゃないかなと思っています。

clapもdarlingも、「deriveマクロのおかげで入力を宣言的に書ける」というメリットがあります!両方とも触ってみるのが吉なのでぜひ使ってみてほしいです。

ここまで読んでいただきありがとうございました!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?