こちらの記事は Rustマクロ冬期講習アドベントカレンダー 25日目の記事です1!
これまでカレンダーに投稿してきた、Rustの宣言マクロ ( macro_rules!
によるマクロ)・手続きマクロを作成する際に役立つTipsをまとめました!
ピックアップショートカット
RustのマクロはC言語など他言語のマクロのように、長いコードを短い記述でまとめることができる便利な機能です。しかしながら、「マクロは最終手段」「マクロを書くな」とか、「Rustはマクロを書くと重くなる」とか言われ、その上更に追加の学習コストがかかるため敬遠されがちです。そうは言っても、「書けないし書かない」のと「書けるけどあえて書かない」では天と地の差があります。マクロについてしっかりした知識があれば、マクロの使用判断も適切に行えるようになります。酸っぱいマクロにならないよう、誰でもマクロを書けたほうが良いでしょう。
しかし、Rustマクロの日本語の情報は(もちろん全く無いわけではないですが)他のトピックほどまとめられている感じがしませんでした。というわけで、今回まとめてみた次第です!マクロを作成する際の参考になれば幸いです
※ チートシートのため読みやすさよりも情報の使用頻度や集約率を優先しています。記事としては読みにくいかもしれませんがご了承ください。
バージョン情報
記事執筆時最新は 1.84.0
でマクロに関してはこちらでも特に変わりないです。
表示しているバージョンはアドベントカレンダー執筆中に使用していたものになります。
$ rustup --version
rustup 1.27.1 (54dd3d00f 2024-04-24)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.83.0 (90b35a623 2024-11-26)`
$ cargo --version
cargo 1.83.0 (5ffbef321 2024-10-29)
本記事の参考文献
-
The Little Book of Rust Macros
- 主に宣言マクロについて詳細にまとめられています
-
proc-macro-workshop
- このワークショップを一通りやれば大体の手続きマクロが書けるようになります
- The Rust Reference
- The Rust Programming Language 日本語版
アドカレに投稿した記事および本記事は、上記4つのドキュメント・リポジトリを参考にしています。よくわからない部分があればこちらの方を参照してほしいです!
フラグメント指定子
Rust構文要素の種類を区別する識別子を、フラグメント指定子といいます。例えば宣言マクロのメタ変数の種類(以下の ???
の部分)を指定する時に目にすると思います。
macro_rules! my_awesome_macro {
($hoge:???) => {
// ...
};
}
フラグメント指定子を制すれば宣言マクロ/手続きマクロの9割を制したと言っても過言ではありません(過言)。
というわけで、宣言マクロでよく使うフラグメント指定子のtier表を作ってみました!
-
tt
は特殊なフラグメント指定子で、任意のトークン木要素にマッチします- 例えば再帰的に解析したい時などに使用できます
- なんでも受けることができますが、常用する類のものではないです
tier Sについて選定理由を軽く解説
-
expr
: Rustは式指向言語なのでカバー範囲が広いです -
ident
: 変数名や関数名がなければ始まらないため
宣言マクロにおいては、迷ったらとりあえずSかAのものを使っておけば大体はカバーできます。
以下は各フラグメント指定子の簡単な詳細の表です。
フラグメント指定子 | 例 | 説明 |
---|---|---|
ident |
hoge , self
|
変数名や関数名などの識別子全般 |
expr |
10 + 20 , funcall() , if a { b } else { c }
|
式。いわゆる評価後に値を返す何か |
literal |
10 , "hoge" , true
|
リテラル。いわゆるソースコード内で直接表現される固有値 |
stmt |
let a = 10; , struct Hoge; , fn foo() {}
|
ステートメント。文。何かしら意味を持つものの評価値とはならないもの |
ty |
i32 , &'a str , fn() -> ()
|
型 |
path |
std::option::Option , Turbo::<Fish> , ::std::println
|
パス。いわゆる複数のidentからなる名前空間 |
block |
{} |
スコープを作り出す中括弧を含めた全体 |
item |
pub fn foo() {} , use std::fs; , impl Hoge {}
|
use文や関数宣言など、モジュール内で一番外側に置けるもの |
pat / pat_param
|
Some(ref hoge) , None , 0..10
|
パターンマッチとして使用できる構造。簡単に言えば match 式のアーム部分 |
lifetime |
'a , 'static , '_
|
ライフタイム |
meta |
derive(Debug) , path = "hoge" , serde
|
属性風マクロ等のアトリビュート(属性) |
vis |
pub , pub(crate)
|
Visibility、すなわち可視性 |
tt |
トークン木 (Token Tree) のことで、 任意の Rust構文要素(トークン木)にマッチするワイルドカード |
フラグメント指定子がキャプチャできる構文例やさらなる詳細は次の記事にまとめました
「それでもどうしてもフラグメント指定子が覚えられないんだ!!!」という方のために、マクロで置き換えたい箇所の構文を入力すれば該当する指定子を識別できるツールを用意しました、よかったら使ってみてください。
マクロの種類とユースケースフローチャート
こちらにもまとめました!
Rustのマクロは、定義方法で2種類、呼び出し方法で3種類に大まかに分けることができます。
以下は定義方法と呼び出し方法の組み合わせです。宣言マクロは関数風マクロしか定義できず、手続きマクロならば全てのマクロを定義できます。
呼び出し方法 → ↓定義方法 |
関数風マクロ hoge!
|
属性風マクロ #[hoge]
|
deriveマクロ #[derive(Hoge)]
|
---|---|---|---|
宣言マクロ macro_rules! |
|||
手続きマクロ [lib] proc-macro = true |
どの種類のマクロとして作るべきか迷うかもしれません、というわけで、決定するフローチャートを作成しました!
ポイントごとに軽く解説します。
★ 宣言マクロで作るべきか?
宣言マクロは例示マクロとも呼ばれるとおり、「入力が単純で、置換結果は簡単に書ける場合」に最も楽にマクロを定義できます。大体の場合は宣言マクロで大丈夫です。
★ 関数風手続きマクロで作るべきか?
一方で、マクロが受け取れる入力を様々なパターンにしたかったり、複雑だったりする場合、あるいは出力の構築が複雑な場合は無理して宣言マクロを使う必要はないでしょう。手続きマクロで書くことで、入力の解析を手続き的に行ったほうが宣言マクロよりも簡潔になる場合があります。
quote::quote!
マクロを使用すれば、宣言マクロで置換を書く場合と遜色ない容易さで置換を記述できますので、宣言マクロで限界を感じたらすぐ手続きマクロに切り替えてしまうのが吉です。
手続きマクロには同一クレート内では使えない2という制約がありますが、無理に一つのクレートだけにせず、 ワークスペース を活用すればよいだけなのでやはり宣言マクロで無理をする必要はないです。
属性風マクロ・deriveマクロともに、「入力となる構文要素が item
である」という共通点があります。言い換えると、例えば関数などの処理や構造体などの宣言といった、何かしらのセマンティクスの塊はそのままRustの通常の文法で記述しておきたくて、しかしながら実際の処理や宣言にはある程度の改変や追記を加えたい場合に使用します。
use tokio::time::{Duration, sleep};
#[tokio::main]
// プログラムとしてはmain関数内に書かれた内容にしか興味がない
async fn main() {
sleep(Duration::from_millis(100)).await;
}
// 展開後。興味のないボイラープレートを多く含む
fn main() {
let body = async {
sleep(Duration::from_millis(100)).await;
};
return tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body);
}
属性風マクロはJavaScriptやPythonのデコレータのように何かしらの機能(属性・アトリビュート)を付与したい時に、deriveマクロは王道であろう構造体・列挙体に何かしらのトレイトを #[derive(MyTrait)]
という記述で簡潔に実装したい時などに設けるのが良い感じです。
属性風マクロとderiveマクロの使い分けについては、まずderiveマクロは item
の中でも構造体・列挙体にしか使えないという制約があります。構造体・列挙体なら属性風マクロもderiveマクロも利用可能です。この場合は、マクロの出力を「置換」したい場合は属性風マクロ、「追記」したい場合(例えば struct Hoge {...}
はそのままにして、 impl Fuga for Hoge {...}
のみを出力としたい場合)はderiveマクロにすると良さそうです。構造体・列挙体なら大体の場合はderiveマクロが適しているでしょう。
宣言マクロ
こちらにもまとめました!
宣言マクロは macro_rules!
3を使用して定義できるマクロです。モジュール内において 宣言後の行 で使用できるようになります。
macro_rules! マクロ名 {
( $メタ変数:フラグメント指定子 ) => {
// $メタ変数を利用した置き換え内容
}; // <- 一つのパターンはセミコロンで終わる。忘れがちなので注意!
}
fn main() {
// マクロ呼び出し
マクロ名!( マクロに与える引数 );
}
置換結果を例示することでマクロを作るという特徴より、 宣言マクロ (あるいは 宣言的マクロ 。Declarative macro ) 4や、 例示マクロ ( Macros By Example , MBE ) と呼ばれています。
フラグメント指定子については先ほど解説しました。その他宣言マクロで覚えておくべき文法は 案外少ない です!
- 引数部分
-
$メタ変数:フラグメント指定子
: キャプチャしたい構文要素を指定 -
$(...) 区切り文字 繰り返し種類
: 繰り返し。例:$($v:literal),*
なら1, 2, 3
などにマッチ- 区切り文字:
,
か;
が一般的 - 繰り返し種類
-
?
: 0回か1回 -
*
: 0回以上の繰り返し -
+
: 1回以上の繰り返し
-
- 区切り文字:
-
- 展開される部分
-
$メタ変数
でキャプチャした変数へ展開 -
$(...)*
で繰り返して展開
-
macro_rules! マクロ名 {
// 呼び出しパターン
// $メタ変数:フラグメント指定子
($メタ変数1:ident, $メタ変数2:expr) => { // マクロ名!( a, b ) という呼び出しにマッチ
// 置き換え内容をここに書く
// 例↓
let $メタ変数1 = $メタ変数2;
println!("{} = {}", stringify!($メタ変数1), $メタ変数1);
}; // <- セミコロンで終わる
// 複数のパターンからなることもある
// $(...) 区切り文字 * で繰り返しにマッチできる
(# $($s:stmt);* ) => { // マクロ名!(# let hoge = "..."; fn fuga() {} ) のような呼び出しにマッチ
$( // $(...)* で繰り返しを展開
println!("{}", stringify!($s));
$s
)*
};
// ヘンテコなパターンも書ける
// ただしマッチ順に注意
(@~ $t:tt ~%) => {
println!("Hey, {}!", stringify!($t));
};
}
fn main() {
マクロ名!(hoge, 10);
// ↑↓ カッコの種類 (), [], {} は呼び出しに無関係
マクロ名![hoge, 10];
// 共に以下に展開される
// let hoge = 10;
// println!("{} = {}", stringify!(hoge), hoge);
// 2つ目にマッチ
マクロ名!{# fn fuga() { println!("beep"); }; fuga(); fuga() };
// 3つ目にマッチ
マクロ名!(@~ + ~%);
}
- パターンは複数書けて
;
で区切る必要がある- セミコロン忘れがちです。(n敗)
- メタ変数及びフラグメント指定子の書き方
- 繰り返しの書き方(
#(...)*
など)
上記以上に特記することはありません。まだ細々とした話はいくつかあります(落穂拾い の節に書きました)が、「これ以上を期待されても...」というのが実際のところです。超頑張ればもう少し複雑なことができるかもしれませんが、その場合は無理に宣言マクロで書かずに、手続きマクロに移行してしまいましょう。
手続きマクロ
こちらにもまとめました!(が、本記事の方がこの記事より詳しいかも)
マクロの入出力の基本は「トークン木」です。トークン木は、ソースコードを「識別子」「句読点」「リテラル」といった最小単位(トークン)によって区切った列のようなものです。
Rustマクロ入出力のイメージ図 (概念図であり実際のコンパイラの挙動を表したわけではありません。)
Rustのマクロは、一旦抽象構文木(AST)を構築したのち、該当箇所のトークン木を書き換え、元のASTに戻すことで置き換えが成されます。
入出力についての詳細はこちらにもまとめました!
この流れの通り、「トークン木(proc_macro::TokenStream
) を受け取り、同じくトークン木(proc_macro::TokenStream
) を吐き出す手続きプログラム」を作成し、そのプログラムをマクロを使用しているソースコードのコンパイル時に実行するのが 手続きマクロ (あるいは手続き的マクロ。Procedual macro )です。
宣言マクロと比べると、「どのようになるか」ではなくて 「どのような流れ(手続き)でRustソースコードを書き換えるか」を記述して作成する ことになるので、"手続き" マクロと呼ばれています。
手続きマクロの Cargo.toml
手続きマクロを作成するための Cargo.toml
への設定は、
[lib]
proc-macro = true
という記述を加えるのみです!
[package]
name = "project"
version = "0.1.0"
edition = "2021"
+[lib]
+proc-macro = true
[dependencies]
proc-macro = true
を加えることで、手続きマクロを作成するための特殊なクレート proc-macro
を使えるようになります。
しかし proc-macro
だけでは何もできません。手続きマクロ 三種の神器 5と呼ばれるクレートを入れるのが定石です。
クレート名 | 役割 | 利用度 |
---|---|---|
proc-macro2 |
TokenStream 等の基本的な型の提供 |
★ |
quote |
出力の整形 | ★ |
syn |
Rustの 文法に沿った解析 | ★★★ |
★ proc-macro2 説明
手続きマクロの入出力で最も基本的で重要な型である TokenStream
や Span
を提供するクレート。 2
なのは 「 proc-macro
の後発だから」...というわけではなくて、 proc-macro
自体は扱いが特殊すぎるクレート(具体的には実行ファイル向けやテスト向けのコンパイルができません)なので、これを普通のクレートと同様に扱えるようにしたものであるため。
★ quote 説明
宣言マクロと似たような記述で出力されるトークン木を宣言的に楽に書ける quote!
を提供するクレート。 #変数
という形式を使うことで ToTokens
トレイトを実装する変数をトークンとして組み込むことが可能になる。
★★★ syn 説明
Rustの文法に出現する構文要素を提供したり組み立てたりとにかくいろいろするクレート。おそらく "syntax" の略。他2つのクレートはRustの文法にまつわる機能は提供しておらず、よって文法解析でめちゃくちゃお世話になる。
三種の神器を加えた Cargo.toml
はこんな感じになります。 syn
には extra-traits
featureと full
featureを予め入れておくとマクロを作成する際にストレスがないです。(もちろん不要であれば入れる必要はありません)
[package]
name = "project"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.93"
quote = "1.0.38"
syn = { version = "2.0.96", features = ["extra-traits", "full"] }
第四の神器 darling
属性風マクロ・deriveマクロの入力部分の構造を宣言的に書き表すのに便利な darling
クレートというクレートがあります。三種の神器で慣れてきてなお辛くなってきたら使ってみてください。詳細は darling
クレート の節で解説しています。
どの呼び出しタイプの手続きマクロでも Cargo.toml
は共通です。ここからは各マクロのスケルトンを載せていきます!
関数風マクロ
my_macro!(hoge)
!
が末尾についている関数のように呼び出せるマクロを 関数風マクロ (Function-like macro) と呼びます。まぁ、宣言マクロですでに登場済みの、おなじみの呼び出し方法ですね。
手続きマクロにおける関数風マクロは、 src/lib.rs
直下にあるパブリックな関数に #[proc_macro]
アトリビュートを加えることで宣言できます!
use proc_macro::TokenStream;
#[proc_macro]
pub fn my_macro(
input: TokenStream // <- my_macro!(★) の ★部分が input
) -> TokenStream {
// ...
}
マクロの実装方法について、 TokenStream
を元にして TokenStream
を出力できるならその間は別にどのように記述しても構わないのですが、次に示すようなテンプレートの構造に沿っていればとりあえず間違いないです。
use proc_macro::TokenStream;
#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
// 入力検証はここで済ませる
let input = syn::parse_macro_input!(input as macro_impl::MacroInput);
// マクロの本体を呼び出し
macro_impl::my_macro_impl(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
なぜこのテンプレートのように書くとよいかは、次の記事にまとめました!
macro_impl
モジュールの中にて、実際に処理を実装したマクロ本体と、マクロが受け取る入力(以降ここでは MacroInput
とする)を定義しています。
use proc_macro::TokenStream;
/// ```rust
/// my_macro!(beep);
///
/// // ↓
///
/// fn beep() {
/// println!("Hello! My name is beep.");
/// }
/// ```
#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
// 入力検証
let input = syn::parse_macro_input!(input as macro_impl::MacroInput);
// マクロの本体を呼び出し
macro_impl::my_macro_impl(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
// proc_macro と proc_macro2 で棲み分けをするためにモジュールを分ける
mod macro_impl {
use proc_macro2::TokenStream;
// マクロ本体
pub fn my_macro_impl(input: MacroInput) -> syn::Result<TokenStream> {
// 本来はここで色々処理を行う
let MacroInput { fn_name } = input;
// quoteマクロで宣言的に記述できる
Ok(quote::quote! {
fn #fn_name() {
println!("Hello! My name is {}.", stringify!(#fn_name));
}
})
}
// マクロの入力
pub struct MacroInput {
fn_name: syn::Ident,
}
// 入力への syn::parse::Parse トレイトの実装
impl syn::parse::Parse for MacroInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let fn_name = input.parse()?;
Ok(Self { fn_name })
}
}
}
もちろんモジュール名や関数名、構造体名、モジュールの分け方等はすべて一例で、名前や配置に法則や決まりはありません。
次のような実装手順が直感的かなと思います。(しつこいようですが、この手順については Rust手続きマクロ エラーハンドリング手法 も参考にしていただけると幸いです。)
関数風手続きマクロの実装手順
1. MacroInput
に syn::parse::Parse
トレイトを実装
必須というわけではないですが、入力を表す構造体(例では MacroInput
)を用意し、 syn::parse::Parse
をその構造体に実装すると読みやすいです。以下2つの恩恵を受けられるためです。
-
syn::Result
を返す形での入力パースを書ける-
syn::Result
はsyn
クレートの関数やメソッドが返すResult
型です。
-
- その上で、
syn::parse_macro_input
を使い簡単にパース処理を記述できる
マクロにおいては入力パース部が最もコンパイルエラーに繋がりやすい箇所であるため、最もエラーハンドリングしたくなる部分です。 syn::parse::Parse
トレイト実装という形でこちらの処理をひとまとめにできて、見通しがよくなります。
2. MacroInput
に基づき出力を用意
my_macro_impl
関数のメインロジック、すなわち出力を生成するための手続きを書きます。ある意味で手続きマクロの処理中核に来る部分と言えなくもないです。
とはいえこの部分は薄っぺらくなる可能性もあります。究極に薄っぺらい、もといこの手続き部分が要らないマクロは、ほぼほぼ宣言マクロと同じと言えます。
3. quote::quote!
マクロ等を使用して出力を組み立て
quote::quote!
マクロを利用することで、 my_macro_impl
関数の出力部で宣言的に proc_macro2::TokenStream
を構築できます。
quote::quote! {
fn #fn_name() {
println!("Hello! My name is {}.", stringify!(#fn_name));
}
}
宣言マクロの出力部では、Rust構文要素が入ったメタ変数には $hoge
のように $
を付けてアクセスできました。 quote!
マクロの中身では、処理を通して作成したRust構文要素入り変数には #hoge
のように #
をつけてアクセスします。この変数の型としては、 quote::ToTokens
トレイトを実装していることが求められます。
関数風マクロを実装する実践的なハンズオンは別記事にまとめました!
属性風マクロ
#[my_macro]
fn some_item() {}
// メタ部分ありの場合
#[my_macro(name = "hoge")]
fn hoge_item() {}
PythonやJavaScript等に存在するデコレータのように、関数や構造体に"属性(アトリビュート6)的な何か"を"付与"するように呼び出すマクロを 属性風マクロ (Attribute macro) と呼びます。
マクロを作成する側から見ると、item
に分類される構文要素(主に関数 fn xxx() {...}
、 ブロック {...}
、実装ブロック impl Xxx {...}
、 use
文、モジュールブロック mod xxx {...}
、 構造体 struct Xxx {...}
、列挙体 enum Xxx {...}
など、モジュール直下に直接書ける構文要素)を受け取り、その内容を置換するマクロになります。
属性風マクロは手続きマクロでのみ定義できます。 src/lib.rs
直下にあるパブリックな関数に #[proc_macro_attribute]
アトリビュートを加えることで宣言できます!(ややこしいw)
use proc_macro::TokenStream;
#[proc_macro_attribute] // 関数風とは異なることに注意
pub fn my_macro(
attr: TokenStream, // #[my_macro(★)] の ★ 部分
item: TokenStream // #[my_macro] を付与した item 全体
) -> TokenStream {
// ...
}
伏線というかderiveマクロとの対比として話すと、関数風マクロなら関数名( hoge
とすると)がそのままマクロの呼び出し( hoge!
)に使われたのと同様に、属性風マクロも関数名( fuga
とする)がそのまま属性付与の呼び出し( #[fuga]
)として使われます。一方でderiveマクロのみ呼び出すためのマクロ名設定に少々クセがあります。
関数風マクロと同様、次に示すようなテンプレートの構造に従っていればとりあえず大丈夫でしょう...と言っても関数風マクロと入力パース部以外同じです
#[proc_macro_attribute]
pub fn my_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
// 入力検証はここで済ませる
// 属性とitem部分は別々に与えられているので、別々にパースする
let name = syn::parse_macro_input!(attr as macro_impl::ItemName); // 今回はたまたま ItemName という名前にしてみた
let item = syn::parse_macro_input!(item as syn::Item);
let input = macro_impl::MacroInput { name, item };
// マクロの本体を呼び出し
macro_impl::my_macro_impl(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
なぜこのテンプレートのように書くべきかの理由は関数風マクロと同様です。(再々掲)
今回も関数風マクロ同様、 macro_impl
モジュールの中にて、実際に処理を実装したマクロ本体と、マクロが受け取る入力( MacroInput
)を定義しています。
use proc_macro::TokenStream;
/// ```rust
/// #[my_macro(name = "hello")]
/// fn hoge() {
/// analyze_hello();
/// }
///
/// // ↓
///
/// fn hoge() {
/// analyze_hello();
/// }
/// pub fn analyze_hello() {
/// println!("{}", "...");
/// }
/// ```
#[proc_macro_attribute]
pub fn my_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
// 入力検証はここで済ませる
// 属性とitem部分は別々に与えられているので、別々にパースする
let name = syn::parse_macro_input!(attr as macro_impl::ItemName); // 今回はたまたま ItemName という名前にしてみた
let item = syn::parse_macro_input!(item as syn::Item);
let input = macro_impl::MacroInput { name, item };
// マクロの本体を呼び出し
macro_impl::my_macro_impl(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
// proc_macro と proc_macro2 で棲み分けをするためにモジュールを分ける
mod macro_impl {
use proc_macro2::{Span, TokenStream};
// マクロ本体
pub fn my_macro_impl(input: MacroInput) -> syn::Result<TokenStream> {
// 本来はここで色々処理を行う
let macro_input_analyze_str = format!("{:?}", input);
let MacroInput { name, item } = input;
let analyze_fn_ident = syn::Ident::new(&format!("analyze_{}", name.0), Span::mixed_site());
// quoteマクロで宣言的に記述できる
Ok(quote::quote! {
#item // !!!重要!!! この記述がないと入力として受け取ったitemが出力されず消えてしまう
pub fn #analyze_fn_ident() {
println!("{}", #macro_input_analyze_str);
}
})
}
// マクロの入力
#[derive(Debug)]
pub struct MacroInput {
pub name: ItemName,
pub item: syn::Item,
}
#[derive(Debug)]
pub struct ItemName(String);
// 入力への syn::parse::Parse トレイトの実装
impl syn::parse::Parse for ItemName {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
/*
*
* 今回のparseメソッド実装はボイラープレート的
*
* darling クレートの使用を推奨
*
*/
let meta = input.parse::<syn::Meta>()?;
let syn::Meta::NameValue(syn::MetaNameValue { path, value, .. }) = meta else {
return Err(syn::Error::new_spanned(meta, "expected `name = \"...\"`"));
};
if !path.is_ident("name") {
return Err(syn::Error::new_spanned(path, "expected `name = \"...\"`"));
}
let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit),
..
}) = value
else {
return Err(syn::Error::new_spanned(value, "expected string literal"));
};
Ok(Self(lit.value()))
}
}
}
もちろんモジュール名や関数名、構造体名、モジュールの分け方等はすべて一例で、名前や配置に法則や決まりはありません。
ソースコード中にも注意書きしましたが、属性風マクロは入力に対し 置換 を行うので、入力をそのまま残したい場合は出力に含める必要があります。 #[tokio::main]
のように入力そのものは残したくない属性風マクロも存在するため、この仕様は納得です。
quote::quote! {
#item // 入力のitemを残したい場合出力に残す!
// 以下は追加の出力になる
pub fn #analyze_fn_ident() {
println!("{}", #macro_input_analyze_str);
}
}
実装手順例は関数風マクロと同様なので省略します。上記マクロはサンプルとして「属性風マクロの入力として受け取った内容をデバッグ出力する関数を設ける」というマクロにしてみたのですが、例としてはいささか煩雑になってしまいました...
属性風マクロ・deriveマクロはメタ部分(#[hoge(...)]
の ...
の部分)やitem部分の入力パースが煩雑になりがちです。deriveマクロの節でも紹介予定ですが、このような場合は darling
クレートを利用するとボイラープレートを避けられるでしょう。本記事の後ろの節で軽く解説している他、Rust 手続きマクロ 第四の神器 darling という記事でも概要を解説しています。 serde
クレートや clap
クレートと同様に便利なのでぜひ使ってみてほしいです。
属性風マクロを実装する実践的なハンズオンは別記事にまとめました!
- Rust 属性風マクロを軽くハンズオン
- 上記の続編、
darling
クレートを使用: Rust 手続きマクロ 第四の神器 darling
deriveマクロ
#[derive(MyMacro)]
#[my_attr]
struct SomeStruct {
#[my_attr(meta)]
some_field: String,
}
構造体・列挙体7に対して #[derive(MyTrait)]
を"付与"するように呼び出すマクロをそのまま deriveマクロ (Derive macro) と呼びます。deriveは「派生」や「導出」という意味を持つ英単語であり、和訳では「導出マクロ」と呼ばれることもあります。
属性風マクロと比べると、関数 fn xxx() {}
や実装ブロック impl Xxx {}
には使用できずあくまでも構造体・列挙体のためのものであることと、属性風マクロでは出力結果は入力部分が出力で"置換"されたのに対し、deriveマクロでは(入力である構造体・列挙体には変化が加えられず)出力が 追記 される点が異なります。「導出」のニュアンスは入力に伴い出力が追記される挙動を指しているのでしょう。
deriveマクロは手続きマクロでのみ定義できます。 src/lib.rs
直下にあるパブリックな関数に #[proc_macro_derive(Xxx)]
アトリビュートを加えることで宣言できます!
use proc_macro::TokenStream;
#[proc_macro_derive(
MyMacro, // #[derive(★)] の★部分をここで指定
attributes(my_macro) // #[my_macro] という不活性属性を有効にする
)]
pub fn my_macro_impl( // deriveマクロでは関数名は何でも良い
item: TokenStream // #[derive(MyMacro)] を付与した item (構造体 or 列挙体) 全体
) -> TokenStream {
// ...
}
関数風マクロ・属性風マクロでは関数名がそのままマクロ名になりましたが、deriveマクロでは #[proc_macro_derive(Xxx)]
の Xxx
部分が実質的なマクロ名になります。
また、 "不活性属性" なる概念が登場しました。「属性風マクロ(活性属性, active attribute)ではなく、単体ではマクロとはならない属性」が 不活性属性 (inert attribute) であり8、deriveマクロにおいて補助的な役割(例えばフィールドごとに設定を加えるなど)を担います。使用する不活性属性は、 attributes(...)
の中に列挙しておく必要があります。どのderiveマクロからも列挙されていない(組み込みでない)不活性属性は、コンパイルエラーとなります。
マクロ処理の流れは関数風マクロ・属性風マクロとほぼ同じですが、入力のみ少し異なり、 専用の型 DeriveInput
を用いるのが定石です。(もちろん DeriveInput
の解析を内包した入力構造体を定義するのもアリでしょう。)
#[proc_macro_derive(MyMacro, attributes(my_macro))]
pub fn my_macro_impl(item: TokenStream) -> TokenStream {
// 入力検証はここで済ませる
// deriveマクロの最初の入力解析先はDeriveInputほぼ一択
let input = syn::parse_macro_input!(item as syn::DeriveInput);
// マクロの本体を呼び出し
macro_impl::my_macro_impl(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
再々々掲になりますが次の記事にてなぜこのテンプレートのように書くべきかをまとめました!
先に挙げた2つのマクロと同様、macro_impl
モジュールの中にて、実際に処理を実装したマクロ本体を定義しています。
use proc_macro::TokenStream;
/// ```rust
/// #[derive(MyMacro)]
/// #[my_macro(all = "全体向け属性")]
/// struct Hoge {
/// #[my_macro(field = "フィールド向け属性")]
/// fuga: i32,
/// }
///
/// // ↓ 以下の内容が追記される
///
/// impl Hoge {
/// pub fn print_derive_input(&self) {
/// println!("{}", "...");
/// }
/// }
/// ```
#[proc_macro_derive(MyMacro, attributes(my_macro))]
pub fn my_macro_impl(item: TokenStream) -> TokenStream {
// 入力検証はここで済ませる
// deriveマクロの最初の入力解析先はDeriveInputほぼ一択
let input = syn::parse_macro_input!(item as syn::DeriveInput);
// マクロの本体を呼び出し
macro_impl::my_macro_impl(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
// proc_macro と proc_macro2 で棲み分けをするためにモジュールを分ける
mod macro_impl {
use proc_macro2::TokenStream;
// マクロ本体
pub fn my_macro_impl(input: syn::DeriveInput) -> syn::Result<TokenStream> {
// 本来はここで色々処理を行う
let ident = input.ident.clone();
let content = format!("{:?}", input);
// ★ 大体ここでフィールドごとやバリアントごとなどのイテレータ処理が入る
// quoteマクロで宣言的に記述できる
Ok(quote::quote! {
// 入力となる構造体・列挙体を改めて出力する必要はない
// 追加で出力したい内容を記述
impl #ident {
pub fn print_derive_input(&self) {
println!("{}", #content);
}
}
})
}
}
もちろんモジュール名や関数名、構造体名、モジュールの分け方等はすべて一例で、名前や配置に法則や決まりはありません。
入力がsyn::DeriveInput
になっている点・ 属性風マクロと異なり追記したい内容だけ出力すれば良い 点を押さえておけば大丈夫です。
syn::DeriveInput
の注目すべき構造は次の部分です。結構骨が折れる構造になっています。
pub struct DeriveInput {
pub attrs: Vec<Attribute>, // 構造体・列挙体全体にかかっている属性
pub ident: Ident, // 構造体・列挙体名
pub data: Data, // フィールド・バリアント情報
.. // 他は一旦無視
}
pub enum Data {
Struct(DataStruct),
Enum(DataEnum),
Union(DataUnion),
}
// 一例としてDataStructを見る
pub struct DataStruct {
pub fields: Fields, // フィールドの情報
.. // 他は一旦無視
}
// ↓ 各フィールド。
// ちょっと深いところ
// (例:
// Fields::Named(FieldsNamed)
// FieldsNamed.named: Punctuated<Field, Token![,]>)
// にあり最後にたどり着く
pub struct Field {
pub attrs: Vec<Attribute>, // このフィールドにかかっている属性
pub ident: Option<Ident>, // フィールド名
.. // 他は一旦無視
}
例示したコード内の★の部分ではしばしばフィールドやバリアントに対する繰り返し処理を行いその結果をイテレータにまとめます。最終的には例えば構造体に対してならば各 Field
ごとに TokenStream
を生成して、そのイテレータ(仮に field_iter
とする)を作れば、 quote::quote!
マクロの出力部では #( #field_iter )*
と書くことで繰り返しが展開され全体の出力が構成されます。
属性風マクロで解説した通り、deriveマクロもまたdarling
クレートを利用することでボイラープレートを避けられます(本記事の後ろの節 も参照のこと)。特に darling::FromDeriveInput トレイトを入力構造体にderiveする(ややこしい!)方針で実装すれば、欲しい情報のみコンパクトにまとめることができます。属性風マクロ以上に darling
クレートが活躍します!
本当はこの繰り返し部分を本記事にも掲載したかったのですが、 darling
クレートを導入しないと記述量が多くなりそうだったためやめておきました。以下に掲載しているハンズオン記事の方を読んでみてほしいです。
というわけで例によって、
deriveマクロを実装する実践的なハンズオンは別記事にまとめました!
- Rust deriveマクロを軽くハンズオン
- 上記の続編、
darling
クレートを使用: Rust 手続きマクロ 第四の神器 darling
マクロ全般の小話
宣言マクロ/手続きマクロに共通したマクロのいくつかのトピックになります。
マクロのデバッグ方法 / 困ったら cargo expand
「マクロでエラーが出た!でもソースコードわからないかも... 」
そんな時に使えるのが cargo expand
です!
cargo install cargo-expand
プロジェクト中でマクロを使用した際にどのように展開されるか教えてくれます。
cargo expand
例:
macro_rules! awesome_macro {
() => {
let value = "展開される部分";
println!("{}\nネストされたマクロについても全て展開される", value);
}
}
fn main() {
println!("マクロが全て展開された結果がわかる");
awesome_macro!();
}
$ cargo expand
Checking project v0.1.0 (/path/to/project)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
fn main() {
{
::std::io::_print(
format_args!("マクロが全て展開された結果がわかる\n"),
);
};
let value = "展開される部分";
{
::std::io::_print(
format_args!(
"{0}\nネストされたマクロについても全て展開される\n",
value,
),
);
};
}
大規模だったり複雑なプロジェクトでは、モジュールを指定すると対象モジュールの展開結果のみ得られて便利です!
cargo expand 対象モジュールへの::パス
例:
macro_rules! my_macro {
() => {
const _: &'static str = "Hello, world!";
};
}
mod a {
mod very {
mod deep {
mod module {
my_macro!();
}
}
}
}
fn main() {}
$ cargo expand a::very::deep::module
Checking project v0.1.0 (/path/to/project)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
mod module {
const _: &'static str = "Hello, world!";
}
cargo expand
があれば大体なんとかなります!なんとかならない時は...うーん頑張れ!
一応、 cargo expand
以外にもマクロをデバッグする手段はなくはないです。
- 宣言マクロ:
- nightlyですが
trace_macros
マクロやlog_syntax
マクロというマクロがあります。詳細は Rust 宣言マクロ小ネタ集【光編① マクロを作る時に便利なマクロ】 にまとめました!
- nightlyですが
- 手続きマクロ:
-
syn
クレートを依存に加える際、extra-traits
feature を有効にしてください。いろいろな構造体にDebug
トレイトが実装されるのでデバッグが捗るようになります
-
マクロの強み・弱み
記事序盤で「マクロは使わない方が良いと言われている」みたいなことを書きました。そもそもどのような場面で使うべき/使わないべきなのでしょうか?本節では「Rustマクロでできること・できないこと」を独断と偏見で改めて軽くまとめたいと思います。
マクロで実現できること・強み・メリット
本節ではマクロを使うメリットやRustマクロの強みを改めて確認します。
① 短い記述で長いコードへと変換できる
マクロの本領です。いわゆるボイラープレートとなってしまうような処理を短い記述でまとめられます。頻出するコードなら、純粋に可読性を向上できます。特に、属性風マクロなら処理に対して何かしらの修飾を、deriveマクロならば構造体・列挙体へ何かしらの機能の付与(特にトレイトの実装)を簡潔に記述できます。
② 可変長引数
Rustはあえて可変長引数機能を関数に対して設けていない言語です9。一方で println!
マクロに代表される通り、マクロを使うことで擬似的に可変長引数を実現しています。
後述予定のデメリットで「マクロは関数よりI/Fがわかりにくく煩雑」であることを挙げていますが、ある意味で可変長引数機能は煩雑なので、可変長引数がマクロによって実現されているというのは筋が良い設計に見えますね。
③ Rustの文法に従わない記法でRustコードを記述できる
特に手続きマクロによって関数風マクロを作成することによって、Rustソースコード中に、Rustの文法に従わない記法や宣言を記述することができます。
- 例1:
serde_json::json!
マクロ でJSONを記述できます - 例2:
yew::html!
マクロ でHTMLを記述できます
fn main() {
let json = serde_json::json! {
{
"hoge": "aaaa",
"fuga": 10
}
};
println!("{:?}", json);
}
Rustに存在する記法だけでは満足できない!というユーザーでも、マクロを使うことで、ある程度記法を拡張できたり、DSLを設けたりできるわけです!
④ 置換以上に変なことはしない
このように強力な機能を持つRustのマクロですが、評価後の出力に関してはC/C++のマクロよりも制約があり、暴走することはありません。Rustのマクロは純粋な文字列置換ではないため、Rust文法の文脈上適切な出力でない場合コンパイルエラーになってくれます。
このあたりについて詳しくは次の記事に書きました!(再掲)
マクロが適さない場面・弱み・デメリット
逆に本節ではマクロのデメリットや、使うべきではない場面について確認します。
① 処理内容が隠蔽される/インターフェースが分かりにくい
よく言われるデメリットその1です。短く書ける分置き換えられる処理部分が隠蔽されてしまうので、コードの処理内容がわかりにくくなる時があります。
ただ短く置き換えるという点では関数とマクロは同じに感じるかもしれません。しかし、関数はシグネチャにより引数/返り値の型が明示されていることより、ドキュメントやIDEの補助による恩恵を受けやすいです。一方で、マクロは展開結果を確認しなければ処理内容が判別できなくなることがあります。なるべく明確な名前で、処理内容も明確であるが、省略を行うのに十分な内容に対してのみマクロ化を検討する、というのが対策になるでしょう。
利用する時にマクロの処理結果を確認したい場合は、先の節で言及した cargo expand
が便利です。
② コンパイルが遅くなる
よく言われるデメリットその2です。経験則的であり要出典ですが、マクロを使用することでコンパイル時間が遅くなることがしばしばあります。
例えば宣言マクロでは、再帰処理の書き方次第でオーダーが大きくなった際、当然ながらコンパイル時間も激増するという話がLittle Bookに掲載されています。
とはいえこれは宣言マクロで下手な書き方をした場合の話で、すべてのマクロについてコンパイル時間が伸びることを意味してはいません。しかしながら次節で述べるような副作用があるほどのマクロだったり、マクロがネストしていたりする場合はやはりコンパイル時間が伸びてしまうことに繋がるでしょう。
③ 副作用のある処理には適さない
先に説明した通りマクロはトークン木( TokenStream
)を入出力とします。言い換えると、マクロはトークン木からトークン木への写像です。
コンパイルごとに出力が変化してはいけないので、マクロは純粋関数であることが理想になります。仮に出力が変化しないのだとしても、コンパイル時間改善の観点でも結果は決定的であるべきで、副作用のある処理は避けるべきでしょう。
ちなみに書こうと思えば副作用を伴うマクロを書くことは可能です。例えば sqlx
の query
マクロにはDBにアクセスして静的に型解析する機能があります。ただ、この手のマクロは乱用注意ということです。
コンパイル時に副作用を伴う処理をしたい場合、丁寧に行いたければ Build Scripts を使うのも手です。
④ 型情報を使うことはできない
手続きマクロは何やら豪勢な仕組みに見え、コンパイル時にできることならどのようなことでもできそうに見えます。
しかし、所詮はトークン木の置換です。型情報の解析機能は提供されていません。例えば、deriveマクロの付与対象である構造体や列挙体、あるいはそのフィールドが、実際にどのような型であるかに忖度した出力をする、といったことは不得手です(ほぼ不可能ですが頑張れば擬似的に可能かもしれないので一応断言はしません)。
コンパイル時に型情報が解析できない、あるいは(解析対象の)名前解決ができないせいでマクロの実装が煩雑になったり、ある程度の機能を諦めなければならない場合があります。
裏を返せば、マクロは単なるトークン木の入出力だからこそ扱いやすい機能なのかもしれません。
衛生性 (健全性, hygiene)
先ほどマクロのメリットとして「置換以上に変なことはしない」ということを挙げました。本当でしょうか?
脈略なく突然変数を定義してしまうようなマクロを考えてみます。
macro_rules! shadowing {
() => {
let hoge = "マクロが書き換えた!";
};
}
fn main() {
let hoge = "書き換えられてない";
shadowing!();
println!("{}", hoge);
}
「マクロが書き換えた!」が出力結果になると思うでしょう。
書き換えられてない
playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=123b6e8e2175e9de3180823f7e2f338b
ユーザーが定義した hoge
とマクロが定義した hoge
は別物として扱われているので、シャドーイングがなされていません。よってこのような実行結果になっています。
Rustのコンパイラは、マクロの呼び出し元に存在する識別子( ident
。変数名などのこと)とマクロが生成した識別子を異なるものだと判断します。
この仕組みの背後に、 衛生性 (または 健全性 , Hygiene ) というコンセプトがあります。考え方としては関数の参照透過性と似たもので、「周囲のコンテキストによってマクロの展開結果(の意図)10が変わらない」ようにするための仕組みあるいはマナーを指しています。
特にマクロを書いている時は識別子( ident
)とパス( path
)において顔を出す概念です。
筆者なりに次の記事にまとめてみたので読んでいただけると幸いです!
...が、衛生性はしっかりした説明をできている気がしないので、保険としてThe Little Bookのリンクも以下に掲載しておきます。
ちなみに先ほどの「呼び出し元とマクロ生成の識別子は区別される」という話ですが、手続きマクロの識別子 syn::Ident::new
にて proc_macro2::Span
を第二引数としている部分にこの区別を感じることができます。
-
Span::call_site
: 呼び出し元 -
Span::mixed_site
: マクロ側(と、部分的に呼び出し側)- The Rust Reference 曰く変数名はマクロ側、関数名等は呼び出し元側判定のようです
- (一応、
def_site
も定義はされていますが今のところnightlyのはずです。表立っては出てきません。)
宣言マクロの落穂拾い
本節では宣言マクロ関連のここまでしなかった細かい話をします。
#[macro_use]
と #[macro_export]
マクロチートシート的に宣言マクロにおいて本当に話しておかなければならない事項は正直本節が最後です。他は割とおまけです。
宣言マクロは関係するアトリビュートの付与によって使用できるスコープが変わります。
#[macro_use]
アトリビュート
モジュールや extern crate xxx;
につけることでモジュール内・クレート内のマクロをこの行以降使用可能にするものです。
mod a {
// X!(); // ここからは使えない
}
// X!(); // ここでも使えない
#[macro_use] // mod bで利用可能なマクロをこの行以降で利用可能にする
mod b {
macro_rules! X { () => {}; }
}
X!(); // defined
mod c {
X!(); // defined
}
#[macro_export]
アトリビュート
このアトリビュートが付与された宣言マクロは クレートルートに 公開されます。この時マクロやモジュールの可視性(pub等)は無視されます。
なお、 宣言マクロをクレート外に公開する手段は #[macro_export]
のみ です。
mod hoge {
mod fuga {
// クレートルートにsecret_macro公開
#[macro_export]
macro_rules! secret_macro {
() => {};
}
// hogeからは見えるがそこより外では見えない
pub fn secret_func() {}
}
fn hoge_func() {
fuga::secret_func();
}
}
pub fn func() {
// fugaはここからは見えない
// hoge::fuga::secret_func();
}
// 可視性を無視して利用可能!
secret_macro!();
結局どう違うの?
#[macro_export]
をつけるか最初からクレートルートの一番上で宣言マクロを定義してしまえば、 #[macro_use]
がなくても以降の行でその宣言マクロは利用可能になります。親モジュールから子モジュールのマクロにアクセスしたい、といった場合でない限り、 #[macro_use]
は正直要らない子です11。
一方でクレート内のマクロを外部に公開する手段は #[macro_export]
しかありません。外部公開したいマクロは最初から素直にクレートルートに書き、その上で #[macro_export]
を付与するとわかりやすそうです。
詳細は次の記事にまとめました!
メタ変数式
宣言マクロの出力で使える主な記法は2つでした。メタ変数 $var
と、その繰り返し $( $var ),*
です。
「え?なんかこう...もっとないの?」って気持ちになりますよね。自分はなりました。
一応「メタ変数式( macro_metavar_expr
)」という機能があります。 nightlyですが...
-
${index(depth)}
: 繰り返し構文中における現在のインデックスを取得 -
${ignore($ident)}
: 繰り返し構文中で他のメタ変数式の情報だけ活用したい場合に、メタ変数を無視する機能 -
${len(depth)}
: 繰り返し全体の長さを取得
詳細は次の記事にまとめています。
正直にいって利用価値がある機能かと聞かれると微妙なところです。こんな機能しか紹介できない時点で察してほしいのですが、 宣言マクロにリッチな機能はありません 。何かもっと高度なマクロを作成したいのでしたら、すぐに見切りをつけて手続きマクロに移ってしまった方がよいんじゃないかなと個人的に思います。
宣言マクロのパターンマッチはあまり期待しない方が良い
宣言マクロのパターンマッチは直感に反し制限や奇妙な挙動が多かったりします。
例えば、フラグメント指定子次第でメタ変数の後ろに取ることができるトークンに制限があったりします。
フラグメント指定子 | 許容されるトークン |
---|---|
stmt または expr
|
=> , , , ;
|
pat |
=> , , , = , if , in , (| ) |
pat_param |
=> , , , = , if , in , |
|
path または ty
|
=> , , , = , | , ; , : , > , >> , [ , { , as , where , ブロック(block )要素 |
vis |
, , priv 以外の識別子, 型(ty )を始められるトークン(< など), 「ident , ty , path 」のうちのどれかのメタ変数 |
その他 | とくに制限なし |
こちらの制限にはまだ納得できる理由があったりしますが、とはいえパターンを書いていると不自由を感じることが多いです。上述の後続トークンの話を含め、詳細を次の記事にまとめました!(流石にチートシートに全部掲載するのは疲れてきました )
...記事を読んでいただければわかると思いますが、やはり、入力が複雑ならば下手に宣言マクロで頑張ったりはせず手続きマクロにしてしまった方が見通しが良いでしょう。
手続きマクロの落穂拾い
本節では手続きマクロに関する関連したトピックを2つ載せます。(どちらもすでに関連記事リンクは登場させており、改めての軽い紹介です。)
手続きマクロのエラーハンドリング / syn::Error
と Span
手続きマクロの節にて、「マクロの入出力の基本はトークン木だ」と述べました。
ではもしユーザーが誤ったマクロの使い方をしており、マクロ側が適切にトークン木を作成できない、いわゆるコンパイルエラーにしたい場合はどうすれば良いでしょうか...?
コンパイルエラー時も、トークン木を出力するのが理想です。ではどうするか...?
compile_error!()
を表すトークン木を出力すれば良い のです!
そしてこのトークン木を構成するための機能は、 syn::Error
が into_compile_error
メソッドとして提供してくれています。
3種類の手続きマクロで毎回、「次のように書くと良い」と言っていたのは、 syn::Result
が Err
だった時にも proc_macro2::TokenStream
を生成できるようにするためだったというわけです。
macro_impl::my_macro_impl(input)
.unwrap_or_else(syn::Error::into_compile_error) // Err でも proc_macro2::TokenStream になる
.into() // proc_macro2::TokenStream -> proc_macro::TokenStream
というわけで、手続きマクロで失敗する可能性がある関数やメソッドは syn::Result<T> = Result<T, syn::Error>
を返すシグネチャに統一すると幸せになれます。
伴い、 syn::Error
を作成するためには、エラーの発生箇所を提示するための Span
が必要です。
Span
を直接渡せる場合は syn::Error::new
を、エラー原因となったRustの構文要素( ToTokens
トレイトを実装した型の値)が判明している場合は syn::Error::new_spanned
を利用することで syn::Error
を作成することができます。
詳細は次の記事にまとめてあるので、よかったら読んでみてください。
darling
クレート
属性風マクロやderiveマクロでは通常の item
入力に加え、メタ部分(#[attr(...)]
の ...
部分)や不活性属性のパースも必要でした。特にderiveマクロに至っては DeriveInput
へのパース後もフィールドごとに不活性属性をパースしなければならず、 syn::parse::Parse
トレイトの愚直実装だけではボイラープレート祭りとなってしまうのでした。三種の神器だけではこの問題は解決できないのです。
そんなあなたに darling
クレート
darling
クレートは手続きマクロ界の serde
や clap
に相当するクレートです!
例えば #[hoge(sep = "...", fmt = "...")]
という属性をパースしたければ、 sep
や fmt
をフィールドに持つ構造体に FromMeta
をderiveすれば良いだけです!
use darling::{ast::NestedMeta, FromMeta};
use proc_macro2::{Span, TokenStream};
use syn::LitStr;
// deriveマクロのオプション部( sep = "...", fmt = "..." )を宣言的に表せる!
#[derive(Debug, FromMeta)]
pub struct DbgStmtsOption {
pub sep: Option<LitStr>,
#[darling(default = default_fmt)]
pub fmt: LitStr,
}
fn default_fmt() -> LitStr {
LitStr::new("[dbg_stmt] {}", Span::mixed_site())
}
impl DbgStmtsOption {
pub fn from_token_stream(attr: TokenStream) -> syn::Result<Self> {
// sep = "...", fmt = "..." というトークン列を darling::ast::NestedMeta でパース
let attr_args = NestedMeta::parse_meta_list(attr)?;
// NestedMeta から DbgStmtsOption を構成
Ok(Self::from_list(&attr_args)?)
}
}
もう自分で syn::parse::Parse
を手動実装する必要はありません。 clap
を使えば構造体で宣言的にCLIアプリのオプションを記述できるように、 darling
を使えば構造体で宣言的にメタ部分構造を書き表せるのです!ほぼ必須級のクレートと言っても過言ではないでしょう。
こちらでは詳細は省略しますが、deriveマクロの方でも darling::FromDeriveInput
トレイトおよびderiveマクロを使えばかなりのボイラープレートを減らすことができ、出力部分の構築に集中することが可能です。
さらなる詳細(もといハンズオン)は次の記事にまとめてみました!
まとめ・所感
アドベントカレンダーおよび本記事を通し、宣言マクロ・手続きマクロに関して筆者が欲しかった内容をまとめてみましたが、お望みの機能や項目は見つかったでしょうか...?もしかして見つからなかった...?
マクロについてまとめ切った感想ですが、手続きマクロですら、「所詮トークン木からトークン木への写像でしかない」ことがわかったのが筆者的には一番大きな収穫でした。(まるでRustそれ自身のように)思ったほど学習コストは高くなく、Rustのマクロも万能に見えてその実大したことはできないのです。しかしながら衛生性やASTレベルでの検証等が行われ、ヘンテコな出力は許さない設計になっていてそこには安心感があります...
...そう、こんな仕組みなので、(パフォーマンスさえ気にしなければ) Rustの手続きマクロは暴れまくるのにちょうどよい機能 でしょう!用法用量を守って使う分には、そこまで臆病になる必要はないんじゃないかなと思います。
皆さんもぜひ手続きマクロで面白いマクロを作ってみましょう!ここまで読んでいただきありがとうございました!
-
新年になってしかももう下旬...アドベントカレンダーとは...その分内容は濃くしたつもりです! ↩
-
ライブラリクレートにも
main.rs
を設けることができて、そのライブラリクレートの名前空間から手続きマクロをmain.rs
にて呼び出すことは可能ですが、それは例外というか、どちらにせよ同一クレートで使えているとは見做されないです。 ↩ -
マクロ呼び出しの構造には従っていないため、
macro_rules
はマクロというよりは一種の構文と捉えたほうが良さそうです。 ↩ -
https://doc.rust-jp.rs/book-ja/ch19-06-macros.html に従えば「宣言的マクロ」「手続き的マクロ」が公式の和訳のようですが、気づけば筆者が「宣言マクロ」「手続きマクロ」と呼んでしまっていました。アドベントカレンダーに投稿してきた記事での呼称すべてを更新するのは大変 & 自分の呼び方も間違いではないと考えたため、「的」は抜いて呼ぶままとしました。 ↩
-
Rust の procedural macro を操って黒魔術師になろう〜proc-macro-workshop の紹介 という記事様の受け売りで、筆者もこの呼称を利用させてもらっています。 ↩
-
HTMLタグ等でも登場する"属性"を意味する単語 Attribute (アトリビュート)ですが、西洋宗教画では人物を暗喩するアイテムみたいな意味合いがあるらしいですね。なんか現代創作でもアトリビュートって使えそうな概念な気がしますが見かけないです。閑話休題。 ↩
-
C/C++等の言語とのFFIのために用意されている超マイナーなRustの構文要素である共用体
union
に対しても適用できますが、まぁ付与することはまずないと思います。 ↩ -
説明のための嘘を付いています。The Rust Referenceに記載があるのでそちらを参照してほしいです。実際は属性を読み込む処理過程の話で、処理後に自身を取り除く属性が活性属性、取り除かない属性が不活性属性で、結果的に属性風マクロは活性属性、deriveマクロ用オマケ属性は不活性属性となっているため、このように説明しています。 ↩
-
あくまでも筆者の見解です。Rustは便利でも乱用されがちな機能は設けないがちな言語と思っています。 ↩
-
例えばマクロ出力の結果、新たに関数が生成されていればある意味で周囲のコンテキストが変わったといえるでしょう。しかしそれはマクロの目的の一つでもあります。すなわちマクロは、完全な意味でコンテキストを変えないというわけではありません。よって、展開結果それ自体がどうであるかというよりは、展開結果が意図通りであるかが変わっていないかが注目するべき点になっています。シャドーイングの存在もあり、識別子に関しては容易に意図が変えられてしまうので、世界が区別されているのでしょう。 ↩
-
Edition 2018以前は外部クレートのマクロを利用するために
#[macro_use] extern crate xxx;
と書く必要がありましたが、不要になったため忘れて大丈夫です。 ↩