マクロ
この本全体を通して println! のようなマクロを使ってきましたが、マクロが
何であり、どのように機能するのかは、まだ十分には掘り下げていません。マクロ という用語は、
Rust における機能の一群、すなわち macro_rules! による宣言的マクロと3種類の
手続き型マクロを指します:
- 構造体や列挙型で使う
derive属性によって追加されるコードを指定する、 カスタム#[derive]マクロ - あらゆる要素に使えるカスタム属性を定義する属性風マクロ
- 関数呼び出しのように見えるものの、引数として指定されたトークンに対して 動作する関数風マクロ
これらそれぞれについて順に見ていきますが、その前に、すでに関数があるのに なぜマクロが必要なのかを見てみましょう。
マクロと関数の違い
基本的に、マクロは別のコードを書くコードを書くための手段であり、これは
メタプログラミング として知られています。付録Cでは、さまざまなトレイトの実装を
生成してくれる derive 属性について説明しました。この本を通して、
println! マクロや vec! マクロも使ってきました。これらのマクロはすべて、
手で書いたコードよりも多くのコードを生成するように 展開 されます。
メタプログラミングは、書いて保守しなければならないコード量を減らすのに役立ちます。 これは関数の役割の1つでもあります。しかし、マクロには関数にはない 追加の力がいくつかあります。
関数シグネチャでは、その関数が持つパラメータの数と型を宣言しなければ
なりません。一方、マクロは可変個のパラメータを受け取れます。たとえば、
println!("hello") は1つの引数で呼び出せますし、
println!("hello {}", name) は2つの引数で呼び出せます。また、マクロは
コンパイラがコードの意味を解釈する前に展開されるため、たとえば与えられた
型に対してトレイトを実装できます。関数ではそれはできません。関数は実行時に
呼び出され、トレイトはコンパイル時に実装されている必要があるからです。
関数の代わりにマクロを実装する欠点は、マクロ定義のほうが関数定義よりも 複雑になることです。なぜなら、Rustコードを書くRustコードを書いているからです。 この間接性のため、マクロ定義は一般に、関数定義よりも読み、理解し、保守する ことが難しくなります。
マクロと関数のもう1つの重要な違いは、マクロはファイル内で呼び出す 前 に 定義するかスコープに導入しておく必要があることです。これに対し、関数はどこで 定義しても、どこからでも呼び出せます。
一般的なメタプログラミングのための宣言的マクロ
Rustで最も広く使われているマクロの形式は、宣言的マクロ です。これらは
「例によるマクロ」「macro_rules! マクロ」、
あるいは単に「マクロ」と呼ばれることもあります。その本質として、宣言的マクロは
Rustの match 式に似たものを書けるようにします。第6章で説明したように、
match 式は、式を取り、その式の結果の値をパターンと比較し、その後で
一致したパターンに関連付けられたコードを実行する制御構造です。マクロもまた、
値を特定のコードに関連付けられたパターンと比較します。この場合、値は文字どおりの
Rustソースコードであり、それがマクロに渡されます。パターンはその
ソースコードの構造と比較され、各パターンに関連付けられたコードは、
一致するとマクロに渡されたコードを置き換えます。これはすべて
コンパイル中に起こります。
マクロを定義するには、macro_rules! 構文を使います。vec! マクロがどのように
定義されているかを見ることで、macro_rules! の使い方を見ていきましょう。第8章では、
特定の値を持つ新しいベクタを作るために vec! マクロをどのように使えるかを
説明しました。たとえば、次のマクロは3つの整数を含む新しいベクタを
作成します:
#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}
vec! マクロを使えば、2つの整数からなるベクタや、5つの文字列スライスからなるベクタを
作ることもできます。同じことを関数で行うことはできません。というのも、値の数や型を
事前に知ることができないからです。
リスト20-35は、vec! マクロの定義を少し単純化したものを示しています。
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
注: 標準ライブラリにおける
vec!マクロの実際の定義には、 正しい量のメモリを事前に割り当てるためのコードが含まれています。そのコードは、 例を単純にするため、ここには含めていない最適化です。
#[macro_export] アノテーションは、このマクロが定義されているクレートが
スコープに導入されるたびに、このマクロも利用可能になるべきことを示します。
このアノテーションがないと、マクロをスコープに導入できません。
次に、macro_rules! と、定義しようとしているマクロの名前を 感嘆符を付けずに
書いて、マクロ定義を始めます。この場合、
vec という名前の後には、マクロ定義の本体を表す波括弧が続きます。
vec! の本体にある構造は、match 式の構造に似ています。
ここには、パターン ( $( $x:expr ),* ) を持つ1つのアームがあり、
その後に => と、このパターンに関連付けられたコードブロックが続きます。もし
パターンが一致すれば、関連付けられたコードブロックが生成されます。これが
このマクロにおける唯一のパターンであることを考えると、有効なマッチ方法は1つ
しかありません。それ以外のパターンはエラーになります。より複雑なマクロには
複数のアームがあります。
マクロ定義で有効なパターン構文は、第19章で扱ったパターン構文とは異なります。 なぜなら、マクロのパターンは値ではなくRustコードの構造に対してマッチするからです。 リスト20-29のパターンの各部分が何を意味するのか見ていきましょう。完全なマクロ パターン構文については、Rust リファレンス を参照してください。
まず、パターン全体を囲むために一組の丸括弧を使います。次に、
ドル記号($)を使って、パターンに一致するRustコードを保持する
マクロシステム内の変数を宣言します。ドル記号により、これが
通常のRust変数ではなくマクロ変数であることが明確になります。次に現れるのは、
括弧の内側でパターンに一致する値を捕捉し、それを置き換えコードで使うための一組の
丸括弧です。$() の中には $x:expr があり、これは任意の
Rust式に一致し、その式に $x という名前を付けます。
$() の後にあるコンマは、$() 内のコードに一致するコードの各要素の間に、
リテラルなコンマ区切り文字が現れなければならないことを示しています。
* は、パターンが * の前にあるものに0回以上一致することを
指定します。
このマクロを vec![1, 2, 3]; で呼び出すと、$x パターンは3つの
式 1、2、3 に対して3回一致します。
では、このアームに対応するコード本体内のパターンを見てみましょう。
$()* 内の temp_vec.push() は、パターン内の $() に一致する各部分に対して、
パターンが一致した回数に応じて 0 回以上生成されます。$x は、一致した各式
に置き換えられます。vec![1, 2, 3]; でこのマクロを呼び出すと、このマクロ
呼び出しを置き換えるために生成されるコードは次のようになります。
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
これで、任意の型の任意個の引数を受け取り、指定された要素を含むベクタを作成 するコードを生成できるマクロを定義できました。
マクロの書き方についてさらに学ぶには、オンラインドキュメントや、 Daniel Keep が始めて Lukas Wirth が引き継いだ 『The Little Book of Rust Macros』 のような他の資料を参照してくだ さい。
属性からコードを生成するためのプロシージャルマクロ
マクロの 2 つ目の形式はプロシージャルマクロで、これは関数により近い形で振る
舞います(そして一種のプロシージャです)。プロシージャルマクロ はコードを
入力として受け取り、そのコードに対して処理を行い、コードを出力として生成し
ます。これは、宣言的マクロが行うようにパターンに対してマッチし、そのコード
を別のコードに置き換えるのとは異なります。プロシージャルマクロには、カスタ
ム derive、属性風、関数風の 3 種類があり、いずれも似たような仕組みで動作
します。
プロシージャルマクロを作成する場合、その定義は特別な crate type を持つ専用
のクレート内になければなりません。これは複雑な技術的理由によるもので、将来
的には解消したいと考えています。リスト 20-36 では、プロシージャルマクロを
定義する方法を示します。ここで some_attribute は、特定の種類のマクロを使
うことを表すプレースホルダーです。
use proc_macro::TokenStream;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
プロシージャルマクロを定義する関数は、入力として TokenStream を受け取り、
出力として TokenStream を生成します。TokenStream 型は Rust に含まれる
proc_macro クレートによって定義されており、トークン列を表します。これが
このマクロの中核です。マクロが処理対象とするソースコードが入力
TokenStream を構成し、マクロが生成するコードが出力 TokenStream です。
この関数には、どの種類のプロシージャルマクロを作成しているのかを指定する属
性も付いています。同じクレート内に複数種類のプロシージャルマクロを持つこと
もできます。
では、さまざまな種類のプロシージャルマクロを見ていきましょう。まずはカスタ
ム derive マクロから始め、その後で他の形式を異なるものにしている小さな違
いを説明します。
カスタム derive マクロ
hello_macro という名前のクレートを作成し、その中で HelloMacro という名
前のトレイトと、hello_macro という名前の 1 つの関連関数を定義してみましょ
う。利用者にそれぞれの型ごとに HelloMacro トレイトを実装してもらうのでは
なく、利用者が自分の型に #[derive(HelloMacro)] と注釈を付けることで、
hello_macro 関数のデフォルト実装を得られるように、プロシージャルマクロを
提供します。そのデフォルト実装は、そのトレイトが定義されている型の名前が
TypeName であるとして、Hello, Macro! My name is TypeName! を出力しま
す。言い換えると、私たちのクレートを使うことで、別のプログラマがリスト
20-37 のようなコードを書けるようにするクレートを書くことになります。
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
完成すると、このコードは Hello, Macro! My name is Pancakes! を出力しま
す。最初のステップは、次のように新しいライブラリクレートを作成することで
す。
$ cargo new hello_macro --lib
次に、リスト 20-38 で HelloMacro トレイトとその関連関数を定義します。
pub trait HelloMacro {
fn hello_macro();
}
これで、トレイトとその関数が用意できました。この時点で、このクレートの利用 者は、リスト 20-39 のようにトレイトを実装して目的の機能を実現できます。
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}
fn main() {
Pancakes::hello_macro();
}
しかし、その場合は hello_macro とともに使いたい型ごとに実装ブロックを書
かなければなりません。私たちは、その作業を利用者にさせたくありません。
さらに、トレイトが実装されている型の名前を出力する hello_macro 関数のデ
フォルト実装も、まだ提供できません。Rust にはリフレクション機能がないた
め、実行時に型名を調べることができないのです。コンパイル時にコードを生成す
るためのマクロが必要になります。
次のステップは、プロシージャルマクロを定義することです。これを書いている時
点では、プロシージャルマクロは専用のクレートに置く必要があります。いずれ、
この制約はなくなるかもしれません。クレートとマクロクレートを構成する際の慣
例は次のとおりです。foo という名前のクレートに対して、カスタム derive
プロシージャルマクロのクレートは foo_derive と呼ばれます。hello_macro
プロジェクトの中に、hello_macro_derive という新しいクレートを作成してみ
ましょう。
$ cargo new hello_macro_derive --lib
この 2 つのクレートは密接に関連しているため、hello_macro クレートのディ
レクトリ内にプロシージャルマクロクレートを作成します。hello_macro 内のト
レイト定義を変更した場合は、hello_macro_derive 内のプロシージャルマクロ
実装も変更しなければなりません。この 2 つのクレートは別々に公開する必要が
あり、これらのクレートを使うプログラマは、両方を依存関係として追加し、両方
をスコープに導入する必要があります。別の方法として、hello_macro クレート
が hello_macro_derive を依存関係として使い、プロシージャルマクロのコード
を再エクスポートすることもできます。しかし、ここでのプロジェクト構成にして
おけば、derive 機能を望まないプログラマでも hello_macro を使えるように
なります。
hello_macro_derive クレートをプロシージャルマクロクレートとして宣言する必
要があります。また、すぐにわかるように、syn クレートと quote クレート
の機能も必要になるので、それらを依存関係として追加する必要があります。
hello_macro_derive の Cargo.toml ファイルに次を追加してください。
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
手続きマクロの定義を始めるには、リスト 20-40 のコードを
hello_macro_derive クレートの src/lib.rs ファイルに配置してください。このコードは、
impl_hello_macro 関数の定義を追加するまではコンパイルされないことに注意してください。
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate.
let ast = syn::parse(input).unwrap();
// Build the trait implementation.
impl_hello_macro(&ast)
}
このコードは hello_macro_derive 関数と impl_hello_macro
関数に分けられていることに注目してください。前者は TokenStream のパースを担当し、
後者は構文木の変換を担当します。こうすることで、手続きマクロをより便利に
書けるようになります。外側の関数本体
(この場合は hello_macro_derive)のコードは、目にする、あるいは作成する
ほぼすべての手続きマクロクレートで同じになります。内側の関数本体
(この場合は impl_hello_macro)で指定するコードは、
その手続きマクロの目的に応じて変わります。
ここでは 3 つの新しいクレートを導入しています。proc_macro、syn、
そして quote です。proc_macro クレートは Rust に
付属しているため、Cargo.toml の依存関係に追加する必要はありませんでした。
proc_macro クレートはコンパイラの API であり、これによって自分たちのコードから
Rust コードを読み取り、操作できるようになります。
syn クレートは、文字列としての Rust コードを、操作を行えるデータ構造へと
パースします。quote クレートは、syn のデータ構造を再び Rust コードへと
変換します。これらのクレートのおかげで、扱いたいあらゆる種類の Rust コードを
はるかに簡単にパースできます。Rust コード用の完全なパーサを書くのは、
決して簡単な作業ではありません。
ライブラリの利用者が型に #[derive(HelloMacro)] を指定すると、
hello_macro_derive 関数が呼び出されます。これが可能なのは、ここで
hello_macro_derive 関数に proc_macro_derive をアノテーションし、
トレイト名と一致する HelloMacro という名前を指定しているからです。
これは、ほとんどの手続きマクロが従っている慣例です。
hello_macro_derive 関数はまず、input を TokenStream から、
解釈や操作を行えるデータ構造へ変換します。ここで syn が登場します。
syn の parse 関数は TokenStream を受け取り、パースされた Rust コードを表す
DeriveInput 構造体を返します。リスト 20-41 は、
struct Pancakes; という文字列をパースして得られる DeriveInput
構造体の関連部分を示しています。
DeriveInput {
// --中略--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
この構造体のフィールドから、パースした Rust コードが、ident
(identifier、つまり名前)として Pancakes を持つユニット構造体であることが
分かります。あらゆる種類の Rust コードを記述するためのフィールドがこの構造体には
さらにあります。詳しくは、DeriveInput の syn
ドキュメントを確認してください。
まもなく impl_hello_macro 関数を定義します。ここで、組み込みたい新しい
Rust コードを構築します。しかしその前に、derive
マクロの出力もまた TokenStream であることに注意してください。返された
TokenStream はクレートの利用者が書いたコードに追加されるため、
利用者が自分のクレートをコンパイルすると、変更後の TokenStream
の中で私たちが提供した追加機能を得ることになります。
ここで unwrap を呼び出して、syn::parse 関数の呼び出しに失敗した場合に
hello_macro_derive 関数がパニックするようにしていることに気づいたかもしれません。
手続きマクロ API に従うには、proc_macro_derive 関数は Result ではなく
TokenStream を返さなければならないため、エラー時に手続きマクロが
パニックすることは必要です。この例では unwrap を使って単純化していますが、
実運用のコードでは、panic! や expect
を使って、何がうまくいかなかったのかについて、より具体的なエラーメッセージを
提供すべきです。
これで、アノテーションが付いた Rust コードを TokenStream から
DeriveInput インスタンスへ変換するコードができました。次は、
リスト 20-42 に示すように、アノテーションが付いた型に対して HelloMacro
トレイトを実装するコードを生成しましょう。
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let generated = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
generated.into()
}
ast.ident を使うことで、アノテーションが付いた型の名前
(識別子)を含む Ident 構造体インスタンスを取得します。リスト 20-41 の
構造体は、リスト 20-37 のコードに対して impl_hello_macro
関数を実行すると、取得される ident の ident フィールドの値が
"Pancakes" になることを示しています。したがって、リスト 20-42 の
name 変数には Ident 構造体インスタンスが入り、それを表示すると
"Pancakes"、つまりリスト 20-37 の構造体の名前になります。
quote! マクロを使うと、返したい Rust コードを定義できます。コンパイラが期待するのは
quote! マクロの実行結果そのものとは少し異なるものなので、それを
TokenStream に変換する必要があります。これには into
メソッドを呼び出します。これにより、この中間表現が消費され、必要な
TokenStream 型の値が返されます。
quote! マクロは非常に便利なテンプレート機構も提供しています。#name
と書くと、quote! はそれを変数 name の値で置き換えます。通常のマクロの
動作と似た繰り返しも行えます。詳しい導入については、quote
クレートのドキュメントを参照してください。
私たちは、利用者がアノテーションを付けた型に対して HelloMacro
トレイトの実装を生成したいので、#name
を使ってその型を取得できます。トレイト実装には hello_macro
という 1 つの関数があり、その本体には私たちが提供したい機能、すなわち
Hello, Macro! My name is を出力し、その後にアノテーションが付いた型の
名前を出力する処理が含まれます。
ここで使っている stringify! マクロは Rust に組み込まれています。これは
1 + 2 のような Rust 式を受け取り、コンパイル時にその式を "1 + 2" のような
文字列リテラルへ変換します。これは format! や println!
とは異なります。これらは式を評価してから、その結果を String
に変換するマクロです。#name
入力が、そのまま文字どおり出力したい式である可能性があるため、ここでは
stringify! を使います。また、stringify! を使うことで、#name
をコンパイル時に文字列リテラルへ変換するため、割り当てを 1 回節約できます。
この時点で、hello_macro と
hello_macro_derive の両方で cargo build が正常に完了するはずです。これらのクレートをリスト
20-37 のコードに組み込み、手続きマクロが実際に動く様子を見てみましょう! cargo new pancakes を使って、
projects ディレクトリに新しいバイナリプロジェクトを作成してください。pancakes
クレートの Cargo.toml に、依存関係として hello_macro と
hello_macro_derive を追加する必要があります。自分の hello_macro と
hello_macro_derive のバージョンを crates.io に公開するのであれば、それらは通常の依存関係になります。そうでない場合は、次のように path
依存関係として指定できます:
[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
リスト 20-37 のコードを src/main.rs に入れて、cargo run を実行してください。すると
Hello, Macro! My name is Pancakes! と表示されるはずです。手続きマクロによる
HelloMacro トレイトの実装が含まれるので、pancakes
クレート側でそれを実装する必要はありません。#[derive(HelloMacro)] がトレイト実装を追加したのです。
次に、他の種類の手続きマクロがカスタム derive マクロとどのように異なるかを見ていきましょう。
属性風マクロ
属性風マクロはカスタム derive マクロに似ていますが、derive
属性用のコードを生成する代わりに、新しい属性を作成できます。さらに、こちらの方が柔軟です。derive
は構造体と列挙型にしか使えませんが、属性は関数のような他の項目にも適用できます。以下は属性風マクロの使用例です。Web
アプリケーションフレームワークを使うときに、関数に注釈を付ける route
という属性があるとしましょう:
#[route(GET, "/")]
fn index() {
この #[route]
属性は、そのフレームワークによって手続きマクロとして定義されます。マクロ定義関数のシグネチャは次のようになります:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
ここでは、型 TokenStream の引数が 2 つあります。1 つ目は属性の内容、つまり
GET, "/" の部分です。2 つ目は、その属性が付けられている項目の本体で、この場合は
fn index() {} と関数本体の残りです。
それ以外については、属性風マクロはカスタム derive
マクロと同じように動作します。proc-macro
クレート型を持つクレートを作成し、必要なコードを生成する関数を実装します!
関数風マクロ
関数風マクロは、関数呼び出しのように見えるマクロを定義します。macro_rules!
マクロと同様に、これらは関数よりも柔軟で、たとえば引数を不定個受け取れます。しかし、macro_rules!
マクロは、前の「一般的なメタプログラミングのための宣言的マクロ」節で説明した、マッチに似た構文でしか定義できません。
関数風マクロは TokenStream 引数を取り、その定義では他の 2
種類の手続きマクロと同様に Rust コードを使ってその TokenStream
を操作します。関数風マクロの一例として、次のように呼び出せる sql!
マクロがあります:
let sql = sql!(SELECT * FROM posts WHERE id=1);
このマクロは内部の SQL 文を解析し、それが構文的に正しいことを検査します。これは
macro_rules! マクロでできる処理よりも、はるかに複雑です。sql!
マクロは次のように定義されます:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
この定義はカスタム derive
マクロのシグネチャと似ています。括弧の中にあるトークンを受け取り、生成したいコードを返します。
まとめ
ふう!これで、頻繁には使わないかもしれないものの、ごく特定の状況で利用できる Rust の機能がいくつか道具箱に加わりました。ここでは複雑なトピックをいくつも導入しましたが、それはエラーメッセージの提案や他の人のコードの中でそれらに出会ったときに、これらの概念や構文を見分けられるようにするためです。解決策へ導くための参考として、この章を活用してください。
次は、この本を通して議論してきたことをすべて実践に移し、もう 1 つプロジェクトに取り組みます!