1
0

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マクロ冬期講習Advent Calendar 2024

Day 15

Rust 手続きマクロ(proc-macro)の勘所

Posted at

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

前回までは宣言マクロ ( macro_rules! によるマクロ ) に関する話題でした。今回から手続きマクロの話題に入ります! が、 宣言マクロほど詳細な解説はしないつもりです。というのも、基本事項を除けば三種の神器1クレート proc-macro2, syn, quote の解説に終始するのですが、全ての解説を試みると12月が何日あっても足りないためです。

というわけで、手続きマクロの一連のシリーズでは、基本事項及び筆者が悩んだ・気になった話題をピックアップして記事にする予定です!

投稿予定

  • Rust 手続きマクロ(proc-macro)の勘所 <- 今回
  • 関数風マクロ
    • RTA
    • ちょっとしたマクロをハンズオン
  • 属性風マクロ
    • RTA
    • ちょっとしたマクロをハンズオン
  • deriveマクロ
    • RTA
    • ちょっとしたマクロをハンズオン
  • Rust 手続きマクロ エラーハンドリング手法
  • darlingクレートを使ってみる

なお、一連のシリーズは、主に dtolnay/proc-macro-workshop: Learn to write Rust procedural macros というワークショップに取り組んで得た知識等を利用しています。一次ソースに当たりたい方はぜひ取り組んでみることをオススメします!

手続きマクロの概要

MacroFlow3.drawio.png

上図は Rustマクロの事前知識①「入出力はトークン木」 で登場させたものです。Rust マクロの分類 で解説したように、上図左下部分にあるような「 トークン木(proc_macro::TokenStream) を受け取り、 トークン木(proc_macro::TokenStream) を吐き出す手続きプログラム」を作成し、そのプログラムをマクロを使用しているソースコードのコンパイル時に実行するのが手続きマクロです。

宣言マクロと比べると、「どのようになるか」ではなくて 「どのような流れ(手続き)でRustソースコードを書き換えるか」を記述して作成する ことになるので、手続きマクロ (Procedual Macro) と呼ばれています。

src/lib.rs (マクロ定義側)
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;

#[proc_macro]
pub fn hello_macro(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as syn::Ident); // <- お決まりの解析処理

    /* この部分にトークン木書き換えのプログラムを書いていく!! */

    quote! { // <- お決まりの出力処理
        ::std::println!("Hello, {}!", stringify!(#input));
    }
    .into()
}
src/main.rs (マクロ利用側)
use proc_macro_example2::hello_macro;

fn main() {
    hello_macro!(world);
}

// ↓ cargo expand

fn main() {
    {
        ::std::io::_print(format_args!("Hello, {0}!\n", "world"));
    };
}

proc_macro_picture2.drawio.png

サクッと始めてみる!

「手続きマクロは(宣言マクロと比べて)難しい!」というイメージがあるのは、同一クレート内で定義することができず、単にお手軽さを感じられないからかなと思います。

詳細に実装するまではともかく、ガワを作るまでは簡単なのでちょっとやってみましょう!

まず、いつも通りライブラリクレートを用意します。

cargo new --lib hello_macro

Cargo.toml を書き換えます! [lib] proc-macro = true という記述を入れます。

Cargo.toml
[package]
name = "hello_macro"
version = "0.1.0"
edition = "2021"

+[lib]
+proc-macro = true

[dependencies]

src/lib.rs の中身を全部消して、マクロを定義します!

Rust
use proc_macro::TokenStream;

#[proc_macro]
pub fn hello(input: TokenStream) -> TokenStream {
    input
}

何もしない関数風マクロ hello の完成です! nop とかの名前のほうがいいかも...?

同一クレートからは呼べないのですが、 hello_macro/src/main.rs からは hello_macro/src/lib.rs をクレート hello_macro として呼べるので、ここに書いてマクロを試してみましょう!

src/main.rs
use hello_macro::hello;

fn main() {
    hello!(println!("Hello, world!"));
}

コンパイルが通るはずです。基礎の基礎は以上です!多少手間なだけで手続きマクロの定義自体はWebAssembly等と比べれば格段に楽であることが伝わったんじゃないかなと思います。

手続きマクロ 三種の神器クレート

「...いやいや、『伝わったんじゃないかなと思います』じゃねーよ!!! input: TokenStream に手を加えるんじゃないのか?!どうするんだよ!!!」

ツッコミありがとうございます! proc_macro::TokenStream はそのままだと扱うのが面倒です。というか proc_macro が特殊なクレートすぎて扱えません (テストで利用できなかったりなど)。これらを扱うのに必須な3つのクレートがあるので、それらを Cargo.toml に記述するのが次の作業になります。

cargo add proc-macro2 quote
cargo add syn --features full --features extra-traits

proc-macro2, quote, syn というクレートを入れました!

クレート名 役割 利用度 説明
proc-macro2 TokenStream 等の基本的な型の提供 手続きマクロの入出力で最も基本的で重要な型である TokenStreamSpan を提供するクレート。 2 なのは 「 proc-macro の後発だから」...というわけではなくて、 proc-macro 自体は扱いが特殊すぎるクレートなので、これを普通のクレートと同様に扱えるようにしたものであるため。
quote 出力の整形 宣言マクロと似たような記述で出力されるトークン木を宣言的に楽に書ける quote! を提供するクレート。 #変数 という形式を使うことで ToTokens トレイトを実装する変数をトークンとして組み込むことが可能になる。
syn Rustの 文法に沿った解析 ★★★ Rustの文法に出現する構文要素を提供したり組み立てたりとにかくいろいろするクレート。おそらく "syntax" の略。他2つのクレートはRustの文法にまつわる機能は提供しておらず、よって文法解析でめちゃくちゃお世話になる。

proc-macro2quote は入出力時に必須 & 使い方も多くないのでお決まりで入れるという感じです。一方、 syn クレートは 「トークン木を受け取りトークン木を返す」だけのプログラムである手続きマクロに Rustの文法 という文化を取り入れる目的でかなり多用します。(独自構文ではない)普通のRustコードを扱う場合、ドキュメントをかなり参照することになるでしょう。

マクロの入力に独自構文を扱いたい

入力がRustの文法に沿っていないようなマクロを作りたいことがあるかもしれません。 yew::html などが良い例でしょう。

マクロ入力の制約は TokenStream 、すなわちトークン木(TokenTree) のストリームであることです。

すなわち、マクロの入力は syn クレートから提供されているRustの文法に沿っている必要はありません。しかし、トークン木であることより多少の制約は受けます。

トークン木に関する解説は Rustマクロの事前知識①「入出力はトークン木」 に書いたのでそちらを参考にしてほしいのと、普通に TokenTree のドキュメントが参考になるかと思います。(ついでに syn::parse::Parse トレイトも参考になるかも...?)

手続きマクロで作れるもの

Rustに存在するマクロ(ほぼ2)全てをカバーしているのが手続きマクロです。

呼び出し方によって若干定義方法も変わります。

マクロ名 呼び出し方 特徴
関数風マクロ macro!(引数) 普通に呼び出せるマクロで、引数部分で受け取ったトークン木を元に何かしらのトークン木を出力する
属性風マクロ #[macro(メタ)] アイテム アイテム部分(関数や構造体や impl ブロックなど)を入力として受け取り、何かしらの処理後にトークン木を出力する
deriveマクロ #[derive(Macro)] 構造体/列挙体 構造体/列挙体に実装等を 付与 するマクロです。入力として構造体/列挙体の情報を受け取るものの、置換ではなく追加生成のような処理を行う点で属性風マクロと異なっています。トレイトの簡易実装に用いるのが最も慣例的な利用方法ですが、それ以外の活用も可能です。

さらなる詳細は Rust マクロの分類 #Rust - Qiita にもまとめているのでそちらも参照してください!次回以降は簡単にこれらのマクロを作ってみたいと思います。

手続きマクロについて手っ取り早く学ぶには: proc-macro-workshop

日本語のソースが少ないなと感じていたことをキッカケに始めたのが本アドカレで、手続きマクロパートでその元となっているのがここで紹介する proc-macro-workshop です!

先ほど挙げた三種の神器、そして anyhowthiserror をはじめとし様々なクレートを作ったRust界の重鎮 dtolnay 氏謹製のワークショップなのでほぼ公式ドキュメントです。英語に抵抗がない方はぜひやりましょう!

dtolnay/proc-macro-workshop: Learn to write Rust procedural macros

本当はこのワークショップの内容を全部アドベントカレンダー記事に盛り込みたかったのですが筆者も道半ばです...アドベントカレンダーのまとめ記事を書く頃にはワークショップの内容をもっとガッツリ盛り込みたい...(願望)

まとめ・所感

手続きマクロに関する俯瞰図を与えられたかなと思います。手続きマクロは宣言マクロと比べ広大ではあるのですが、制限が少なくRustプログラムとしてわかりやすく記述できるため、宣言マクロよりも覚えなければならないことは案外少ないんじゃないかなと思います。 "easy" ではありませんが "simple" というやつでしょうか...?

次回以降は駆け足気味にはなるかもしれませんが、3種類のマクロを軽く触っていきたいと思います!

  1. 「三種の神器」(3種の神器) という表現は Rust の procedural macro を操って黒魔術師になろう〜proc-macro-workshop の紹介 という記事様から拝借しました。良い呼び方だと思います!

  2. 一部組み込みで用意されているマクロがあります。これらが手続きマクロでも再現できるかは不明です。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?