こちらの記事は Rustマクロ冬期講習アドベントカレンダー 22日目の記事です!
アドカレまとめ記事はこちら!: Rustマクロ作成チートシート!
今回は今までの回でなあなあにしてきた、手続きマクロのエラーハンドリングに関する話をまとめたいと思います!
本記事で伝えたいこと
- 手続きマクロの出力はあくまでも
proc_macro::TokenStream
- 通常のエラーハンドリングとの違いは
proc_macro::Span
を扱うことである - マクロ定義部について定石な書き方がある。関数を切り分けることで丁寧にハンドリングしやすくする
- 入力のパースは
parse_macro_input
一択 - パース後の入力を利用して切り分けた関数の引数とする、切り分けた関数の返り値型を
syn::Result
にしておく - 切り分けた関数の返り値
syn::Result
を、unwrap_or_else
とsyn::Error::into_compile_error
を活用することでエラーの場合でもTokenStream
に変換できるようにする
- 入力のパースは
まさに syn::Error
にて紹介されている書き方が定石です!
#[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
として出力する
パニックでも実は特に問題はないのですが、マクロがパニックした場合マクロ全体にエラーの赤線が引かれてしまいます。ユーザー目線だと「マクロ実装の方に問題があったみたいだ。信頼できないマクロだなぁ」となってしまいます。
そこで、後者の手法を取ります。後者の方法はパニックせずマクロが仕事を完遂させるための書き方です。ただマクロをエラーで終わらせるのではなく、「(ユーザーの入力では) コンパイルしようとするとエラーになりますよ」ということを伝えるべく、 compile_error!
を TokenStream
として出力します!
compile_error!
マクロがあると、即座にその箇所がコンパイルエラーとなります。ややこしいのですが、この compile_error!
マクロは手続きマクロの処理途中で実行させるのではなく、手続きマクロの出力結果となる TokenStream
それ自体を構成するのに使います!
筆者にとって compile_error!
マクロの使い道は今まで謎だったのですが、手続きマクロの出力に使えたのですね1...上手いことできてるなぁと思いました
syn::Error
の into_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
にできます!
#[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
が必要になります。
syn::Error::new(Span構造体, エラーメッセージ)
Span
構造体は「ソースコード中の位置情報」と「マクロ展開における情報( call_site
か mixed_site
か)」から成ります。詳細は今回は省きますが、 ソースコード中の位置情報 はマクロを使用するユーザーにコンパイルエラーを伝えるにあたり活用されるので、適切な設定が求められます。
Span
構造体の取得は基本的には難しくなく、大体は入力の構文要素を利用することで得られます。少し実験してみましょう!トークン列を受け取り、 error
という識別子が出てきたらその箇所をエラーにしてみます。
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()
}
パニックの場合はマクロ全体に赤線が引かれてしまいます。
一方で、 syn::Error::new_spanned(ident, "error ident found")
と書くことによりエラー原因となる箇所を Error
に渡すようにした場合は、該当箇所のみに赤線が引かれます!
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
を使うと良さそうです。
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::Error
に Span
情報を渡せればいい感じの場所に赤線を引いてくれることはわかりました。しかし一体このSpanをどうやってコンパイラに伝えているのでしょうか?
into_compile_error
の実装 を見るとなんとなく答えが載っており、方法はシンプルで set_span
メソッドを使い compile_error!
マクロに対し与えられたSpan情報を設定しているようです。
入力パースのマクロ parse_macro_input
手続きマクロは「入力のパース」 → 「出力のレンダリング」の順で行われます。マクロ処理は当然副作用を持つべきではなく参照透過的であるべきなので、入力が正しければ出力でエラーになることはあまりないでしょう(実際は入力パースが中途半端になる影響でしばしばありますが...)。
ということで、入力パースの成功失敗は関心事です。自前で用意した入力構造体が syn::parse::Parse
トレイト を実装していれば、 syn::parse
関数で TokenStream
から入力構造体へ変換することができます。
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
を活用することで、適切に入力パースエラーを処理できます!
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!
マクロです!
#[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回の時はエラーにする」という手続きマクロを書いてみます!
以下作成のポイントです。
- 入力のパースは
parse_macro_input!
で行う- そのために、入力用構造体に
syn::parse::Parse
トレイト を実装する
- そのために、入力用構造体に
- 実際に出力を生成する関数は切り分けて、
syn::Result
<
proc_macro2::TokenStream
>
を返り値型とする-
syn::Error
を作成するために、proc_macro2::Span
を何かしらの方法で得る。Span
があるならnew
、元となる構文要素があるならnew_spanned
を使用する
-
-
syn::Result<proc_macro2::TokenStream>
は、unwrap_or_else
とinto_compile_error
を利用してTokenStream
に変換する
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)*
})
}
まとめ・所感
筆者的に色々試したりドキュメントを読んだりした上でたどり着いた手続きマクロのエラーハンドリング手法及び定石な書き方を紹介しました!
「これが定石!こういう風に書くべき!」と言われると反発したくなるものです。もちろん、別に本記事で書いた通りに書く必要はないと思います。本記事が色々な書き方をコネコネする際の参考になれば幸いです。
ここまで読んでいただきありがとうございました!
-
ところで専用のキーワードでも良さそうな感じはしますが、この機能のためだけに設けるのもヘン、ということで、ビルトインマクロに落ち着いているんだと思います。こういうマクロ他にもありそう...トレイトにしろマクロにしろ、Rustはヘンテコな言語機能をRustの基本的な文法要素で提供するのが上手だなぁと改めて感じました。 ↩