マクロ
Rust のマクロが特別な理由
多くの言語にもマクロの仕組みは存在する.C/C++ のプリプロセッサマクロ (#define) は最もよく知られた例だ.しかし,C のプリプロセッサマクロは単なるテキスト置換であり,型安全性がなく,デバッグが困難で,予期しない副作用を引き起こしやすい.
Rust のマクロはこれとは根本的に異なる.Rust のマクロは 構文木 (Syntax Tree) レベルで動作し,型チェックの前に展開される.テキスト置換ではなく,構造化されたコード生成だ.
Rust には 2 種類のマクロがある:
- 宣言的マクロ (Declarative Macros):
macro_rules!で定義.パターンマッチによるコード生成 - 手続き的マクロ (Procedural Macros): Rust コードで構文木を操作するプログラム
宣言的マクロ (macro_rules!)
基本
macro_rules! は,パターンマッチに基づくマクロ定義だ:
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
fn main() {
say_hello!(); // println!("Hello!"); に展開される
}
マクロ呼び出しは ! で示される.println!, vec!, format! — Bitterless Rust で「おまじない」として使っていたこれらもすべてマクロだ.
パターンマッチ
宣言的マクロの真価はパターンマッチにある:
macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("Called: {}", stringify!($func_name));
}
};
}
create_function!(foo); // fn foo() { ... } が生成される
create_function!(bar); // fn bar() { ... } が生成される
fn main() {
foo(); // "Called: foo"
bar(); // "Called: bar"
}
$func_name:ident の :ident は フラグメント指定子 と呼ばれ,マクロが受け付ける構文要素の種類を指定する:
| フラグメント指定子 | マッチする対象 |
|---|---|
ident |
識別子 (foo, my_var) |
expr |
式 (1 + 2, func()) |
ty |
型 (i32, Vec<String>) |
pat |
パターン (Some(x), _) |
stmt |
文 (let x = 1;) |
block |
ブロック ({ ... }) |
item |
アイテム (fn, struct, impl) |
tt |
任意の単一トークンツリー |
literal |
リテラル (42, "hello") |
繰り返し
マクロは繰り返しパターンをサポートする:
macro_rules! vec_of_strings {
($($x:expr),* $(,)?) => {
vec![$($x.to_string()),*]
};
}
fn main() {
let v = vec_of_strings!["hello", "world", "rust"];
// vec!["hello".to_string(), "world".to_string(), "rust".to_string()] に展開
println!("{:?}", v);
}
$(...),*は「カンマ区切りで 0 回以上の繰り返し」$(...),+は「カンマ区切りで 1 回以上の繰り返し」$(,)?は「末尾カンマのオプション」
複数のパターン
macro_rules! calculate {
(add $a:expr, $b:expr) => { $a + $b };
(mul $a:expr, $b:expr) => { $a * $b };
}
fn main() {
let sum = calculate!(add 1, 2); // 3
let product = calculate!(mul 3, 4); // 12
}
match 式のように,複数のパターンを定義して入力に応じた展開ができる.
標準ライブラリのマクロの仕組み
vec! マクロの簡略化した実装:
macro_rules! vec {
() => { Vec::new() };
($($x:expr),+ $(,)?) => {
{
let mut temp_vec = Vec::new();
$(temp_vec.push($x);)*
temp_vec
}
};
($x:expr; $n:expr) => {
vec::from_elem($x, $n)
};
}
vec![1, 2, 3] と書くと,Vec::new() + 各要素の push に展開される.vec![0; 10] と書くと,from_elem を使った初期化に展開される.
手続き的マクロ (Procedural Macros)
手続き的マクロは,Rust のコード自体を使って構文木を操作するプログラムだ.コンパイル時に実行される Rust プログラムと考えてよい.
3 種類の手続き的マクロがある:
1. derive マクロ
#[derive(...)] で使えるマクロ.構造体や enum に自動的にトレイト実装を追加する:
// 使う側
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
name: String,
age: u32,
}
Debug や Clone は標準ライブラリの derive マクロ,Serialize / Deserialize は serde クレートの derive マクロだ.
derive マクロの実装(概要):
// これは別のクレート (proc-macro クレート) に書く
use proc_macro::TokenStream;
#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(input: TokenStream) -> TokenStream {
// input: 構造体/enum の定義の構文木
// → これを解析し,トレイト実装のコードを生成して返す
// ...
}
手続き的マクロは TokenStream(トークン列)を受け取り,TokenStream を返す関数だ.入力の構文木を解析し,新しいコードを生成する.
2. 属性マクロ
任意の属性として使えるマクロ:
#[route(GET, "/")]
fn index() -> String {
String::from("Hello!")
}
// route マクロが index 関数を変換し,
// ルーティングの登録コードなどを自動生成する
Web フレームワーク (Actix Web, Axum, Rocket など) で広く使われている.
3. 関数風マクロ
関数呼び出しのような構文で使えるマクロ:
let sql = sql!(SELECT * FROM users WHERE id = 1);
// sql! マクロがコンパイル時に SQL の構文チェックを行い,
// 正しい SQL でなければコンパイルエラーを出す
マクロの衛生性 (Hygiene)
Rust の宣言的マクロは 衛生的 (hygienic) だ.マクロ内で定義した変数が,マクロの呼び出し元のスコープの変数と衝突しない:
macro_rules! make_var {
() => {
let x = 42;
};
}
fn main() {
let x = 10;
make_var!();
println!("{}", x); // 10(マクロ内の x とは別)
}
C のプリプロセッサマクロでは,こうした変数名の衝突が深刻な問題になるが,Rust のマクロシステムでは構造的に防がれる.
実用的なマクロの例
cfg マクロ: 条件付きコンパイル
#[cfg(target_os = "linux")]
fn platform_specific() {
println!("Running on Linux");
}
#[cfg(target_os = "macos")]
fn platform_specific() {
println!("Running on macOS");
}
// コンパイル時にターゲット OS に応じた関数だけがコンパイルされる
todo! / unimplemented! / unreachable!
fn complex_algorithm(input: &str) -> Result<String, Error> {
todo!("implement this later") // コンパイルは通るが,実行するとパニック
}
todo!() は「まだ実装していない」ことを示すマクロで,型チェックを通すためのプレースホルダーとして機能する.返り値の型は !(Never 型)であるため,任意の型に推論される.
まとめ
Rust のマクロシステムは 2 層構造で設計されている:
| 種類 | 仕組み | 用途 |
|---|---|---|
宣言的マクロ (macro_rules!) |
パターンマッチによる展開 | ボイラープレートの削減,DSL |
| 手続き的マクロ | Rust コードによる構文木操作 | derive,属性,コード生成 |
両者に共通する重要な特性:
- コンパイル時に展開される: 実行時のオーバーヘッドがない
- 構文木レベルで動作する: テキスト置換ではない
- 型チェックの前に展開される: 展開後のコードが通常通り型チェックされる
- 衛生性: 変数名の意図しない衝突が防がれる(宣言的マクロ)
マクロは Rust の「ゼロコスト抽象化」の原則に完全に合致する — コンパイル時にすべて解決され,実行時のコストはない.