📘

なんとなく Rust のマクロを書いてみる

2022/11/14に公開

はじめに

引き続き Rust + WebAssembly + SolidJS で遊んでいます。お付き合いいただきありがとうございます。前回までで、任意のメモリバッファを確保して、その上で TypedArray や String や JSON をやりとり出来る様になったのですが、2 点ほど気になっていることがあります。

https://zenn.dev/a24k/articles/20221012-wasmple-simple-console
https://zenn.dev/a24k/articles/20221107-wasmple-passing-buffer

気になっていること①

JavaScript 側の console に Rust 側から出力する際のコードですが、こんな感じになっています。

https://github.com/a24k/wasmple/blob/d36e9eec887eeafecdeee65b506554d044d76e20/wasmple/src/buffer/manager.rs#L34

まぁ・・・やりたい事は出来ているのですが、console::debugString を受け取る仕様のため、いちいち format! して渡すカタチになっています。ログを出したいなんて時は、かなりの確率で {} とか {:?} とかしたいハズなので、地味にメンドウ・・・というかイケてない感があります。

気になっていること②

typed な JSON 変換を実現する際に、JsonConvertee というトレイトでマークする様にしたのですが、それを実装するコードがこちらです。

https://github.com/a24k/wasmple/blob/d36e9eec887eeafecdeee65b506554d044d76e20/wasmple/src/lib.rs#L11-L24

うん・・・こちらもこれで出来ているとは思うのですが、やっぱり私も derive したいです。

Rust のマクロなるもの

Rust の世界では、print!vec! などのビックリさん達も、#[derive] などのハッシュカッコさん達も、マクロという括りで扱われているようです。どちらも、コードを生成するためのコード、ということでしょうか。前者は関数マクロ、後者は属性マクロ、と呼ばれている模様です。

マクロをかじるのに、こちら ↓ のサイトが分かりやすくて参考になりました。実装に入る前に、少し勉強した事を整理しておきます。

https://veykril.github.io/tlborm/

使われ方による分類

Rust で使われるマクロには大きく 3 つのパターンがあります。

関数マクロ

print!vec! の様に関数風な感じで用いるマクロです。Rust の関数は、引数の数と型が決まっていないとイケないのですが、それだけだと不便なケースをフォローしてくれています。

console::debug の例は、このタイプのマクロを作れば良さそうです。

属性マクロ

もしくは、単に「属性」と呼ばれている気がします。こちらは、Rust のアイテムに何らかの加工を加えてくれるマクロです。アイテムというのは、fn struct trait など、クレートの構成要素のことです。属性注釈が付けられたアイテムを、丸ごと書き換える能力を持っています。

https://doc.rust-lang.org/reference/items.html

いま思えば、#[wasm_bindgen] って属性マクロですよね。自在に操れば、魔法使いになれるかもです。

導出マクロ

コード上は属性マクロにそっくりの見た目をしていて、英語でも derive attributes と書かれているのですが、derive は他の属性マクロとは区別した方が良さそうです。属性マクロはアイテムを丸ごと書き換えることができるのですが、導出マクロはコードを追加することしか出来ません。むしろ安心。また、対象となるアイテムは enum struct union のみとなります。

JsonConvertee の例は、導出マクロでイケそうです。

作り方による分類

さて、気になるマクロの作り方ですが、こちらも 3 パターンあります。

宣言マクロ

macro_rules! というビルトインマクロを用いて、関数マクロを作ることが出来ます。引数に渡されたトークンの種類や数でマッチさせて、多少処理を分けるくらいのことが可能です。

macro_rules! $name {
    $rule0 ;
    $rule1 ;
    // …
    $ruleN ;
}

宣言マクロ 2.0

まだ unstable の様ですが、新しい宣言マクロのシステムも検討されている[1] みたいです。

手続マクロ

属性マクロや導出マクロを作る際には、手続マクロという方法を用いる必要があります。宣言マクロでは作れない関数マクロもこの方法で作ることになります。

手続マクロの実体は、トークン列 TokenStream を入出力する関数になります。普通の Rust の関数として書けるので、非常に自由度が高そうです。また、syn[2] というクレートを用いることで、TokenStream を AST[3] として扱う事ができる様になります。なんか・・・頑張ればなんでも出来そうですね。

ただ、手続マクロは普通のクレート内には書けない様です。手続マクロ用のクレートを作成して、その中に書くことになります。後ほど、一緒にやっていきましょう。

https://veykril.github.io/tlborm/proc-macros/methodical.html

ビルトイン

#[no_mangle]#[cfg] などは、Rust コンパイラに組み込みのマクロ[4] になります。print! は最終的に format_args! という関数マクロを呼び出しているのですが、コイツもビルトインマクロの様です。この領域のマクロを思いついてしまった場合には、Rust 自体にコントリビュートするしかなさそうです。

宣言マクロで log! する

それでは、宣言マクロからやっていきましょう。元々こんな感じで format! マクロを用いて書いていた部分を、

https://github.com/a24k/wasmple/blob/d36e9eec887eeafecdeee65b506554d044d76e20/wasmple/src/buffer/manager.rs#L34

こういう感じに自作のマクロで置き換えるのがゴールです。

https://github.com/a24k/wasmple/blob/2680d775fc20b41a124cf2dbbb0101bcc20ad596/wasmple/src/buffer/manager.rs#L34

クレートの分割

宣言マクロは、手続マクロとは違いクレートを分けなくても実装は可能です。可能なのですが、モジュール外に公開するために #[macro_export] 属性を付けると、クレートルートレベルで公開されてしまう様です。気にしなければ気にしないで良いとは思うのですが、すごく気になります。

https://veykril.github.io/tlborm/decl-macros/minutiae/scoping.html

良い機会なので、console モジュールを別クレートに分割しようと思います。wasmple_console という名前にしましょう。元々ルートディレクトリは npm 側のプロジェクトになっていたので、並列にこんな感じになります。

wasmple
├── Cargo.toml
├── package.json
├── wasmple
│   ├── Cargo.toml
│   └── src
└── wasmple_console
    ├── Cargo.toml
    └── src

新たにルートディレクトリにも Cargo.toml を追加します。メンバーとして各クレートを参照するのと、プロファイルまわりはワークスペースレベルで設定する必要がある様なのでコチラに移しておきます。

Cargo.toml
[workspace]
members = [
    "wasmple",
    "wasmple_console",
]

[profile.release]
opt-level = "z"
lto = true

https://doc.rust-jp.rs/book-ja/ch14-03-cargo-workspaces.html

Rust 内で use している部分については適宜メンテをしていきましょう。また、ワークスペースにすると target ディレクトリの位置もワークスペースルート直下に変わるので、JavaScript 側や GitHub Actions の設定もメンテしておきます。

https://github.com/a24k/wasmple/commit/c69d8186e6319e56fe0a6ff51853d5f0d25a6349

宣言マクロの実装

いろいろ試行錯誤しつつですが、いったんこんな感じの実装で落ち着いています。

https://github.com/a24k/wasmple/blob/d0426d8cb08f07c6829421b72e6637cce4163884/wasmple_console/src/define.rs#L1-L5

Rust コンパイラの処理フロー上、マクロの展開(実行)は変数とか型とかの意味付けが行われる前に実施されるため、引数(に相当するもの)の特定はもう少し大きな単位(フラグメントというのかな)で実施されます。各フラグメント型については、↓ の記事が詳しいです。

https://veykril.github.io/tlborm/decl-macros/minutiae/fragment-specifiers.html

ちなみに macro というキーワードが、宣言マクロ 2.0 をニラんでか予約語になっているので、衝突回避のために define.rs というファイル名にしてみました。郷愁を感じますね。

print! スタイルのルール

いわゆる print! スタイルのルールがこの部分です。

https://github.com/a24k/wasmple/blob/d0426d8cb08f07c6829421b72e6637cce4163884/wasmple_console/src/define.rs#L4

こんな ↓ 感じでマクロが呼ばれた場合はコチラのルールにマッチして、全ての引数を format! マクロに渡して得られた String を、$crate::log(msg: String) に渡すコードが生成されます。

log!("some message with values {}, {} and {}.", a, b, c);

$crate は、マクロを定義したクレートのルート参照として利用できます。マクロの展開先は、どこか知らないクレートの内であることがほとんどかと思いますので、log(msg: String) 関数を見つけるのに必要になります。

to_string() スタイルのルール

今回は、もうひとつ別のルールも実装しています。

https://github.com/a24k/wasmple/blob/d0426d8cb08f07c6829421b72e6637cce4163884/wasmple_console/src/define.rs#L3

これにより、こういう ↓ 感じでもログ出力ができる様になっています。to_string() が実装されていれば動きます。良いよね。

log!(value);
log!(1 + 2 * 3 + 4);
log!("some message");

1 + 2 * 3 + 4 は、全体が式 expr として扱われるので、(1 + 2 * 3 + 4).to_string() となった結果、ちゃんと 11 と出ます。最後の例 "some message" は、先に説明した print! スタイルのルールでも正しく動くのですが、コチラのルールの方が先に定義されているので、コチラのルールで展開されます。

宣言マクロの利用

作成したマクロには、#[macro_export] 属性が付いているため、クレートルートレベルで公開されています。使う側からは、こんな感じで use しておいて、関数の様に呼び出します(もちろん use せずに wasmple_console::info! でも大丈夫です)。

https://github.com/a24k/wasmple/blob/d0426d8cb08f07c6829421b72e6637cce4163884/wasmple/src/lib.rs#L5

https://github.com/a24k/wasmple/blob/d0426d8cb08f07c6829421b72e6637cce4163884/wasmple/src/lib.rs#L24

実装のところで書いた様に、この例の場合、実際には $crate::info(msg: String) 関数の呼び出しに展開されますので、こちらの関数も見えている必要があります。今回の場合は、こんな ↓ 感じでマクロと同名の関数をクレートルートで pub use しているため、$crate::info(msg: String) で参照できる様になっています。

https://github.com/a24k/wasmple/blob/d0426d8cb08f07c6829421b72e6637cce4163884/wasmple_console/src/lib.rs#L6

手続マクロで #[derive(JsonConvertee)] する

この調子で、手続マクロにも手を出してみましょう。こちらも非常に奥が深そうなのですが、今回はこんな感じで JsonConvertee を実装している部分を、

https://github.com/a24k/wasmple/blob/d36e9eec887eeafecdeee65b506554d044d76e20/wasmple/src/lib.rs#L23

derive マクロを用いて書ける様になるのがゴールです。

https://github.com/a24k/wasmple/blob/d0426d8cb08f07c6829421b72e6637cce4163884/wasmple/src/lib.rs#L10-L14

クレートの分割

console と同様に buffer モジュールも wasmple_buffer クレートに分割をしておきます。手続マクロは、それ用の別クレートにする必要があるので、wasmple_buffer_derive とでもしておきましょう。全体の構成はこんな感じになります。

wasmple
├── Cargo.toml
├── package.json
├── wasmple
│   ├── Cargo.toml
│   └── src
├── wasmple_buffer
│   ├── Cargo.toml
│   └── src
├── wasmple_buffer_derive
│   ├── Cargo.toml
│   └── src
└── wasmple_console
    ├── Cargo.toml
    └── src

手続マクロの実装

専用クレートの準備

wasmple_buffer_derive クレートを準備します。lib.proc-macro = true とすることで、手続マクロ用のクレートになります。

https://github.com/a24k/wasmple/blob/d0426d8cb08f07c6829421b72e6637cce4163884/wasmple_buffer_derive/Cargo.toml

手続マクロ用クレートには、proc_macro[5] というクレートが自動的にリンクされます。また、quote と syn というクレートを追加しています。

https://docs.rs/syn/latest/syn/

syn は、Rust のパーサーライブラリです。Rust のソースコードや手続マクロに渡ってくる TokenStream を食わせると AST を起こしてくれるので、意味的な(?)アクセスをすることが出来る様になります。struct キーワードの次に出てくる識別子を・・・とかやらなくて良いわけですね。すごい。

https://docs.rs/quote/latest/quote/

quote は、Rust のコードっぽい感じの記述から TokenStream を生成するマクロを提供してくれます。正直 TokenStream を直接作ろうとすると、かなりまどろっこしいコードを書く羽目になりそうなのですが、それを普段の Rust と同じ様な感じで書ける様になります。べんり。

この他に、手続マクロの実装でよく使われるクレートとして、proc_macro2[6] というものがあります。こちらは、手続マクロクレート内でしか使えない proc_macro クレート依存の TokenStream 等を他のクレートに持ち出すために使われます。そもそも手続マクロクレート内にはテストを書くことができない様で、複雑なマクロを作成する際には実装部分を普通のクレートに外出しするのがベストプラクティスの様なのですが、その際に proc_macro::TokenStreamproc_macro2::TokenStream に変換して持ち出したりする様です。今回は、非常に簡単な処理しかしませんので、wasmple_buffer_derive 内で完結するため使いません。

導出マクロの実装

実装はいったんこんな感じです。

https://github.com/a24k/wasmple/blob/d0426d8cb08f07c6829421b72e6637cce4163884/wasmple_buffer_derive/src/lib.rs

input: TokenStream には、#[derive] 属性が付けられたアイテム全体が渡って来ます。これに対して、コードに追加したい内容を TokenStream で返してあげれば良さそうです。

syn でパースをすると、DeriveInput という Struct を介してアイテムの情報にアクセスすることが出来ます。今回は、Struct 名 ident を取得して impl を生成しています。quote! マクロを利用すると、こんな感じで TokenStream を生成出来ます。実際には、quote!proc_macro2::TokenStream を生成するので、into() して proc_macro::TokenStream に変換すれば OK です。

手続マクロの利用

使う際に、いちいち wasmple_buffer_deriveuse するのは面倒なので、JsonConvertee 自体と同じところで pub use しておきます。

https://github.com/a24k/wasmple/blob/d0426d8cb08f07c6829421b72e6637cce4163884/wasmple_buffer/src/convert.rs#L4-L5

こうしておけば、こんな感じでお馴染みの #[derive] マクロが使えます。なんとかなりましたね。

https://github.com/a24k/wasmple/blob/d0426d8cb08f07c6829421b72e6637cce4163884/wasmple/src/lib.rs#L7

https://github.com/a24k/wasmple/blob/d0426d8cb08f07c6829421b72e6637cce4163884/wasmple/src/lib.rs#L10-L14

おわりに

Rust のマクロについて、ほんの入口だけですが覗いてみることが出来ました。おまじないだと思っていた #[wasm_bindgen] が、属性マクロだということも、少しだけ分かった様な気がします。相当奥深い世界だと認識しましたので、また機会があれば触れ合ってみたいとおもいます。

動作は変わっていませんが、実際に動くものはこちら。コードは、こちら ↓ を参照ください。

https://github.com/a24k/wasmple/tree/d0426d8cb08f07c6829421b72e6637cce4163884

脚注
  1. Declarative macros 2.0 - A replacement for macro_rules!. ↩︎

  2. syn - Syn is a parsing library for parsing a stream of Rust tokens into a syntax tree of Rust source code. ↩︎

  3. Abstract Syntax Tree - 抽象構文木ですね。AST Explorer を用いると syn がパースした構文木を手軽に確認する事ができます。 ↩︎

  4. Built-in attributes index - The Rust Reference - Attributes ↩︎

  5. proc_macro - A support library for macro authors when defining new macros. ↩︎

  6. proc_macro2 - A wrapper around the procedural macro API of the compiler’s proc_macro crate. ↩︎

Discussion