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手続きマクロ エラーハンドリング手法

Last updated at Posted at 2024-12-25

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

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

今回は今までの回でなあなあにしてきた、手続きマクロのエラーハンドリングに関する話をまとめたいと思います!

本記事で伝えたいこと

  • 手続きマクロの出力はあくまでも proc_macro::TokenStream
  • 通常のエラーハンドリングとの違いは proc_macro::Span を扱うことである
  • マクロ定義部について定石な書き方がある。関数を切り分けることで丁寧にハンドリングしやすくする
    • 入力のパースは parse_macro_input 一択
    • パース後の入力を利用して切り分けた関数の引数とする、切り分けた関数の返り値型を syn::Result にしておく
    • 切り分けた関数の返り値 syn::Result を、 unwrap_or_elsesyn::Error::into_compile_error を活用することでエラーの場合でも TokenStream に変換できるようにする

まさに syn::Error にて紹介されている書き方が定石です!

Rust
#[proc_macro_derive(MyDerive)]
pub fn my_derive(input: TokenStream) -> TokenStream {
    // 入力パース
    let input = parse_macro_input!(input as DeriveInput);

    // 出力
    // fn my_derive(DeriveInput) -> syn::Result<proc_macro2::TokenStream>
    expand::my_derive(input)
        .unwrap_or_else(syn::Error::into_compile_error) // proc_macro2::TokenStream へ変換
        .into() // proc_macro::TokenStream へ変換
}

本記事では、 compile_error! マクロsyn::Error に備わっている機能、 Span 及び syn::Error の生成方法についての説明を通し、上記コードが行っていることを解説していきます!

記事中の表記が煩雑になることを避けるため proc_macro::TokenStream および proc_macro2::TokenStream を共に TokenStream と呼んでいます。両者の違いは、 2 がついていない方は手続きマクロ以外では使用できない(マクロをテストしたい場合テスト環境下では使えない)点と、故に三種の神器(syn, quote, proc_macro2)では 2 の方が使われているという点です。

proc_macro2::TokenStream.into() でシンプルに proc_macro::TokenStream に変換できるため存在としては同一視して問題ありません。一応こまめにどちらであるか明記していますが忘れている箇所があるかもしれません。余力があれば 2 の方なのか無印の方なのか意識して読んでみてください。

手続きマクロエラーの行く末「 compile_error! マクロ

Rustマクロの事前知識①「入出力はトークン木」 で解説した通り、Rustの手続きマクロは proc_macro::TokenStream を入力として受け取り proc_macro::TokenStream を出力します。

「じゃあマクロが失敗したらどうするのさ...?」それを知りたくて本記事にたどり着いたという人もいるでしょう。2つ手段があります。

パニックでも実は特に問題はないのですが、マクロがパニックした場合マクロ全体にエラーの赤線が引かれてしまいます。ユーザー目線だと「マクロ実装の方に問題があったみたいだ。信頼できないマクロだなぁ」となってしまいます。

そこで、後者の手法を取ります。後者の方法はパニックせずマクロが仕事を完遂させるための書き方です。ただマクロをエラーで終わらせるのではなく、「(ユーザーの入力では) コンパイルしようとするとエラーになりますよ」ということを伝えるべく、 compile_error!TokenStream として出力します!

syn_result2.drawio.png

compile_error! マクロがあると、即座にその箇所がコンパイルエラーとなります。ややこしいのですが、この compile_error! マクロは手続きマクロの処理途中で実行させるのではなく、手続きマクロの出力結果となる TokenStream それ自体を構成するのに使います!

筆者にとって compile_error! マクロの使い道は今まで謎だったのですが、手続きマクロの出力に使えたのですね1...上手いことできてるなぁと思いました

syn::Errorinto_compile_error

何かしら想定外の結果になった際、自分で compile_error! を吐きだす処理を書くのは技術的には可能ですが面倒です。

syn::Error が持つ into_compile_error メソッドまたは to_compile_error メソッドを呼べば、 compile_error! への変換を代わりに行ってくれるようになります!

よって、内側で呼び出す関数の返り値型を syn::Result <proc_macro2::TokenStream> = Result<proc_macro2::TokenStream, syn::Error> にしておけば、 unwrap_or_else メソッドを呼び出し、コールバックに into_compile_error を指定することで最終的な返り値型を TokenStream にできます!

src/lib.rs
#[proc_macro]
pub fn my_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    inner::my_macro_inner(input.into())
        .unwrap_or_else( // Ok(TokenStream) ならそのまま出力
            // Err(syn::Error) なら compile_error! を TokenStream として吐きだしてもらう
            syn::Error::into_compile_error
        )
        .into()
}

mod inner {
    pub fn my_macro_inner(
        input: proc_macro2::TokenStream,
    ) -> syn::Result<proc_macro2::TokenStream> {
        // ...
    }
}

syn::Error の作り方と Span

エラー発生時はパニックしなくても syn::Error さえ返せれば compile_error! を結果として返せることがわかりました。

では syn::Error をどうやって生成するか?なのですが、エラーメッセージに加え、構造体 proc_macro2::Span が必要になります。

Rust
syn::Error::new(Span構造体, エラーメッセージ)

Span 構造体は「ソースコード中の位置情報」と「マクロ展開における情報( call_sitemixed_site か)」から成ります。詳細は今回は省きますが、 ソースコード中の位置情報 はマクロを使用するユーザーにコンパイルエラーを伝えるにあたり活用されるので、適切な設定が求められます。

Span 構造体の取得は基本的には難しくなく、大体は入力の構文要素を利用することで得られます。少し実験してみましょう!トークン列を受け取り、 error という識別子が出てきたらその箇所をエラーにしてみます。

src/lib.rs
mod inner {
    pub fn my_macro_inner(
        input: proc_macro2::TokenStream,
    ) -> syn::Result<proc_macro2::TokenStream> {
        use proc_macro2::TokenTree::Ident;

        for tt in input.clone() {
            if let Ident(ident) = tt {
                if ident == "error" {
                    // panic!("`error` ident found (panic)");
                    return Err(syn::Error::new_spanned(ident, "`error` ident found"));
                }
            }
        }

        Ok(input)
    }
}

#[proc_macro]
pub fn my_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    inner::my_macro_inner(input.into())
        .unwrap_or_else(syn::Error::into_compile_error)
        .into()
}

パニックの場合はマクロ全体に赤線が引かれてしまいます。

error_ident_found_panic.png

一方で、 syn::Error::new_spanned(ident, "error ident found") と書くことによりエラー原因となる箇所を Error に渡すようにした場合は、該当箇所のみに赤線が引かれます!

error_ident_found.png

syn::Error は、 new メソッドの他、ソースコード中で示した new_spanned メソッドを使うことで生成できます。どちらを使用しても構わないのですが使い心地が若干異なります。

  • new メソッド: 第一引数は Span 本体
  • new_spanned メソッド: 第一引数は quote::ToTokens トレイトを実装した型

ToTokens トレイトを実装した型というのは、大体は入力の TokenStream をパースする過程で登場します。 ToTokens を実装していると syn::spanned::Spanned トレイトも自動実装される ことより、これらの型はSpan情報を保持していることがわかります。なお、マクロ展開のための情報としては、マクロの入力(ユーザーのソースコード)から取られた範囲の場合 call_site が設定されています。

new メソッドを使用する場合で、こちらに設定する Span については、由来となる構文要素がある場合は由来元に対し .span() メソッドを呼んでみると得られるかもしれません。syn::spanned::Spanned トレイトが実装されていれば Span を得ることができ、エラーに活用できます。マクロ側で作った Span でエラーにしたかったり、複雑な条件より導き出される Span でエラーにしたい時は、 new を。特に問題がなければシンプルな new_spanned を使うと良さそうです。

Rust
pub fn my_macro_inner(
    input: proc_macro2::TokenStream,
) -> syn::Result<proc_macro2::TokenStream> {
    use proc_macro2::TokenTree::Ident;

    let mut guido_mista_counter = 0;

    for tt in input.clone() {
        if let Ident(ident) = tt {
            if ident == "error" {
                // return Err(syn::Error::new_spanned(ident, "`error` ident found"));

                /* ユーザー定義側のSpanを使う例 */
                let span = ident.span();
                return Err(syn::Error::new(span, "`error` ident found"));
            }
        }

        guido_mista_counter += 1;
    }

    if guido_mista_counter == 4 {

        /* マクロ側の都合でErrorを作る場合 */
        return Err(syn::Error::new(
            Span::mixed_site(),
            "Unlucky number 4 is detected",
        ));
    }

    Ok(input)
}

最終的にはどうやってSpanの情報をコンパイラに伝えてるの?

とにもかくにも syn::ErrorSpan 情報を渡せればいい感じの場所に赤線を引いてくれることはわかりました。しかし一体このSpanをどうやってコンパイラに伝えているのでしょうか?

into_compile_error の実装 を見るとなんとなく答えが載っており、方法はシンプルで set_span メソッドを使い compile_error! マクロに対し与えられたSpan情報を設定しているようです。

入力パースのマクロ parse_macro_input

手続きマクロは「入力のパース」 → 「出力のレンダリング」の順で行われます。マクロ処理は当然副作用を持つべきではなく参照透過的であるべきなので、入力が正しければ出力でエラーになることはあまりないでしょう(実際は入力パースが中途半端になる影響でしばしばありますが...)。

ということで、入力パースの成功失敗は関心事です。自前で用意した入力構造体が syn::parse::Parse トレイト を実装していれば、 syn::parse 関数で TokenStream から入力構造体へ変換することができます。

Trait Parse
pub trait Parse: Sized {
    // Required method
    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self>;
}

ここまでの話を踏まえれば、 Parse::parse の返り値型が syn::Result なのは納得できる話でしょう。 #[proc_macro] を付与された手続きマクロエントリにて into_compile_error / to_compile_error を活用することで、適切に入力パースエラーを処理できます!

Rust
use syn::parse::Parse;

struct MyInput;

impl Parse for MyInput {
    // ...省略...
}

#[proc_macro]
pub fn my_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input: MyInput = match syn::parse(input) {
        Ok(parsed) => parsed,
        Err(err) => return err.to_compile_error().into(),
    };

    todo!()
}

いちいち match 式を書くのは...ちょっと面倒ですよね?このボイラープレートに展開してくれるのが parse_macro_input! マクロです!

Rust
#[proc_macro]
pub fn my_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = syn::parse_macro_input!(input as MyInput);

    todo!()
}

// ↓ cargo expand

#[proc_macro]
pub fn my_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = match ::syn::parse::<MyInput>(input) {
        ::syn::__private::Ok(data) => data,
        ::syn::__private::Err(err) => {
            return ::syn::__private::TokenStream::from(err.to_compile_error());
        }
    };
    ::core::panicking::panic("not yet implemented")
}

すなわち、入力パースに関して丁寧なエラーハンドリングを短い記述で行えるのが parse_macro_input! マクロということです。というわけでこのマクロの使用が定石になっています。

定石的なエラーハンドリング まとめ

本記事のまとめとして、「与えられた回数分、ステートメントを繰り返す。ただし、回数が4回の時はエラーにする」という手続きマクロを書いてみます!

以下作成のポイントです。

src/lib.rs
use quote::{quote, ToTokens};
use syn::parse::Parse;

#[proc_macro]
pub fn repeat_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // 入力部
    // 入力パースに失敗したら `compile_error!` を出力してくれる
    let input = syn::parse_macro_input!(input as Repeat);

    // 出力部
    repeat_macro_inner(input)
        .unwrap_or_else(
            // Errorの時、 `compile_error!` に変換してもらう
            syn::Error::into_compile_error
        )
        .into()
}

struct Repeat {
    stmt: syn::Stmt,
    count: syn::LitInt,
}

// repeat_macro!(回数; ステートメント); をパース
impl Parse for Repeat {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let count: syn::LitInt = input.parse()?;
        let _ = input.parse::<syn::Token![;]>()?;
        let stmt = input.parse()?;

        Ok(Repeat { stmt, count })
    }
}

fn repeat_macro_inner(Repeat { count, stmt }: Repeat) -> syn::Result<proc_macro2::TokenStream> {
    let span = count.span(); // Error生成用にSpanを確保しておく
    let count = count.base10_parse::<usize>()?;

    if count == 4 {
        return Err(syn::Error::new(
            span, // 繰り返し回数部分にだけエラーの赤線が引かれる
            "The `4` is unlucky number for Mr. Guido Mista. Please choose another number.",
        ));
    }

    let expanded = (0..count).map(|_| stmt.to_token_stream());

    Ok(quote! {
        #(#expanded)*
    })
}

まとめ・所感

筆者的に色々試したりドキュメントを読んだりした上でたどり着いた手続きマクロのエラーハンドリング手法及び定石な書き方を紹介しました!

「これが定石!こういう風に書くべき!」と言われると反発したくなるものです。もちろん、別に本記事で書いた通りに書く必要はないと思います。本記事が色々な書き方をコネコネする際の参考になれば幸いです。

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

  1. ところで専用のキーワードでも良さそうな感じはしますが、この機能のためだけに設けるのもヘン、ということで、ビルトインマクロに落ち着いているんだと思います。こういうマクロ他にもありそう...トレイトにしろマクロにしろ、Rustはヘンテコな言語機能をRustの基本的な文法要素で提供するのが上手だなぁと改めて感じました。

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?