なんとなく Rust のマクロを書いてみる
はじめに
引き続き Rust + WebAssembly + SolidJS で遊んでいます。お付き合いいただきありがとうございます。前回までで、任意のメモリバッファを確保して、その上で TypedArray や String や JSON をやりとり出来る様になったのですが、2 点ほど気になっていることがあります。
気になっていること①
JavaScript 側の console に Rust 側から出力する際のコードですが、こんな感じになっています。
まぁ・・・やりたい事は出来ているのですが、console::debug
が String
を受け取る仕様のため、いちいち format!
して渡すカタチになっています。ログを出したいなんて時は、かなりの確率で {}
とか {:?}
とかしたいハズなので、地味にメンドウ・・・というかイケてない感があります。
気になっていること②
typed な JSON 変換を実現する際に、JsonConvertee
というトレイトでマークする様にしたのですが、それを実装するコードがこちらです。
うん・・・こちらもこれで出来ているとは思うのですが、やっぱり私も derive
したいです。
Rust のマクロなるもの
Rust の世界では、print!
や vec!
などのビックリさん達も、#[derive]
などのハッシュカッコさん達も、マクロという括りで扱われているようです。どちらも、コードを生成するためのコード、ということでしょうか。前者は関数マクロ、後者は属性マクロ、と呼ばれている模様です。
マクロをかじるのに、こちら ↓ のサイトが分かりやすくて参考になりました。実装に入る前に、少し勉強した事を整理しておきます。
使われ方による分類
Rust で使われるマクロには大きく 3 つのパターンがあります。
関数マクロ
print!
や vec!
の様に関数風な感じで用いるマクロです。Rust の関数は、引数の数と型が決まっていないとイケないのですが、それだけだと不便なケースをフォローしてくれています。
console::debug
の例は、このタイプのマクロを作れば良さそうです。
属性マクロ
もしくは、単に「属性」と呼ばれている気がします。こちらは、Rust のアイテムに何らかの加工を加えてくれるマクロです。アイテムというのは、fn
struct
trait
など、クレートの構成要素のことです。属性注釈が付けられたアイテムを、丸ごと書き換える能力を持っています。
いま思えば、#[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] として扱う事ができる様になります。なんか・・・頑張ればなんでも出来そうですね。
ただ、手続マクロは普通のクレート内には書けない様です。手続マクロ用のクレートを作成して、その中に書くことになります。後ほど、一緒にやっていきましょう。
ビルトイン
#[no_mangle]
や #[cfg]
などは、Rust コンパイラに組み込みのマクロ[4] になります。print!
は最終的に format_args!
という関数マクロを呼び出しているのですが、コイツもビルトインマクロの様です。この領域のマクロを思いついてしまった場合には、Rust 自体にコントリビュートするしかなさそうです。
log!
する
宣言マクロで それでは、宣言マクロからやっていきましょう。元々こんな感じで format!
マクロを用いて書いていた部分を、
こういう感じに自作のマクロで置き換えるのがゴールです。
クレートの分割
宣言マクロは、手続マクロとは違いクレートを分けなくても実装は可能です。可能なのですが、モジュール外に公開するために #[macro_export]
属性を付けると、クレートルートレベルで公開されてしまう様です。気にしなければ気にしないで良いとは思うのですが、すごく気になります。
良い機会なので、console
モジュールを別クレートに分割しようと思います。wasmple_console
という名前にしましょう。元々ルートディレクトリは npm 側のプロジェクトになっていたので、並列にこんな感じになります。
wasmple
├── Cargo.toml
├── package.json
├── wasmple
│ ├── Cargo.toml
│ └── src
└── wasmple_console
├── Cargo.toml
└── src
新たにルートディレクトリにも Cargo.toml
を追加します。メンバーとして各クレートを参照するのと、プロファイルまわりはワークスペースレベルで設定する必要がある様なのでコチラに移しておきます。
[workspace]
members = [
"wasmple",
"wasmple_console",
]
[profile.release]
opt-level = "z"
lto = true
Rust 内で use
している部分については適宜メンテをしていきましょう。また、ワークスペースにすると target
ディレクトリの位置もワークスペースルート直下に変わるので、JavaScript 側や GitHub Actions の設定もメンテしておきます。
宣言マクロの実装
いろいろ試行錯誤しつつですが、いったんこんな感じの実装で落ち着いています。
Rust コンパイラの処理フロー上、マクロの展開(実行)は変数とか型とかの意味付けが行われる前に実施されるため、引数(に相当するもの)の特定はもう少し大きな単位(フラグメントというのかな)で実施されます。各フラグメント型については、↓ の記事が詳しいです。
ちなみに macro
というキーワードが、宣言マクロ 2.0 をニラんでか予約語になっているので、衝突回避のために define.rs
というファイル名にしてみました。郷愁を感じますね。
print!
スタイルのルール
いわゆる print!
スタイルのルールがこの部分です。
こんな ↓ 感じでマクロが呼ばれた場合はコチラのルールにマッチして、全ての引数を format!
マクロに渡して得られた String を、$crate::log(msg: String)
に渡すコードが生成されます。
log!("some message with values {}, {} and {}.", a, b, c);
$crate
は、マクロを定義したクレートのルート参照として利用できます。マクロの展開先は、どこか知らないクレートの内であることがほとんどかと思いますので、log(msg: String)
関数を見つけるのに必要になります。
to_string()
スタイルのルール
今回は、もうひとつ別のルールも実装しています。
これにより、こういう ↓ 感じでもログ出力ができる様になっています。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!
でも大丈夫です)。
実装のところで書いた様に、この例の場合、実際には $crate::info(msg: String)
関数の呼び出しに展開されますので、こちらの関数も見えている必要があります。今回の場合は、こんな ↓ 感じでマクロと同名の関数をクレートルートで pub use
しているため、$crate::info(msg: String)
で参照できる様になっています。
#[derive(JsonConvertee)]
する
手続マクロで この調子で、手続マクロにも手を出してみましょう。こちらも非常に奥が深そうなのですが、今回はこんな感じで JsonConvertee
を実装している部分を、
derive
マクロを用いて書ける様になるのがゴールです。
クレートの分割
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
とすることで、手続マクロ用のクレートになります。
手続マクロ用クレートには、proc_macro[5] というクレートが自動的にリンクされます。また、quote と syn というクレートを追加しています。
syn は、Rust のパーサーライブラリです。Rust のソースコードや手続マクロに渡ってくる TokenStream
を食わせると AST を起こしてくれるので、意味的な(?)アクセスをすることが出来る様になります。struct
キーワードの次に出てくる識別子を・・・とかやらなくて良いわけですね。すごい。
quote は、Rust のコードっぽい感じの記述から TokenStream
を生成するマクロを提供してくれます。正直 TokenStream
を直接作ろうとすると、かなりまどろっこしいコードを書く羽目になりそうなのですが、それを普段の Rust と同じ様な感じで書ける様になります。べんり。
この他に、手続マクロの実装でよく使われるクレートとして、proc_macro2[6] というものがあります。こちらは、手続マクロクレート内でしか使えない proc_macro クレート依存の TokenStream
等を他のクレートに持ち出すために使われます。そもそも手続マクロクレート内にはテストを書くことができない様で、複雑なマクロを作成する際には実装部分を普通のクレートに外出しするのがベストプラクティスの様なのですが、その際に proc_macro::TokenStream
を proc_macro2::TokenStream
に変換して持ち出したりする様です。今回は、非常に簡単な処理しかしませんので、wasmple_buffer_derive
内で完結するため使いません。
導出マクロの実装
実装はいったんこんな感じです。
input: TokenStream
には、#[derive]
属性が付けられたアイテム全体が渡って来ます。これに対して、コードに追加したい内容を TokenStream
で返してあげれば良さそうです。
syn でパースをすると、DeriveInput
という Struct を介してアイテムの情報にアクセスすることが出来ます。今回は、Struct 名 ident
を取得して impl
を生成しています。quote!
マクロを利用すると、こんな感じで TokenStream
を生成出来ます。実際には、quote!
は proc_macro2::TokenStream
を生成するので、into()
して proc_macro::TokenStream
に変換すれば OK です。
手続マクロの利用
使う際に、いちいち wasmple_buffer_derive
を use
するのは面倒なので、JsonConvertee
自体と同じところで pub use
しておきます。
こうしておけば、こんな感じでお馴染みの #[derive]
マクロが使えます。なんとかなりましたね。
おわりに
Rust のマクロについて、ほんの入口だけですが覗いてみることが出来ました。おまじないだと思っていた #[wasm_bindgen]
が、属性マクロだということも、少しだけ分かった様な気がします。相当奥深い世界だと認識しましたので、また機会があれば触れ合ってみたいとおもいます。
動作は変わっていませんが、実際に動くものはこちら。コードは、こちら ↓ を参照ください。
-
Declarative macros 2.0 - A replacement for
macro_rules!
. ↩︎ -
syn - Syn is a parsing library for parsing a stream of Rust tokens into a syntax tree of Rust source code. ↩︎
-
Abstract Syntax Tree - 抽象構文木ですね。AST Explorer を用いると syn がパースした構文木を手軽に確認する事ができます。 ↩︎
-
Built-in attributes index - The Rust Reference - Attributes ↩︎
-
proc_macro - A support library for macro authors when defining new macros. ↩︎
-
proc_macro2 - A wrapper around the procedural macro API of the compiler’s proc_macro crate. ↩︎
Discussion