Wonderfull Rust

マクロ

Rust のマクロが特別な理由

多くの言語にもマクロの仕組みは存在する.C/C++ のプリプロセッサマクロ (#define) は最もよく知られた例だ.しかし,C のプリプロセッサマクロは単なるテキスト置換であり,型安全性がなく,デバッグが困難で,予期しない副作用を引き起こしやすい.

Rust のマクロはこれとは根本的に異なる.Rust のマクロは 構文木 (Syntax Tree) レベルで動作し,型チェックの前に展開される.テキスト置換ではなく,構造化されたコード生成だ.

Rust には 2 種類のマクロがある:

  1. 宣言的マクロ (Declarative Macros): macro_rules! で定義.パターンマッチによるコード生成
  2. 手続き的マクロ (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,
}

DebugClone は標準ライブラリの derive マクロ,Serialize / Deserializeserde クレートの 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 の「ゼロコスト抽象化」の原則に完全に合致する — コンパイル時にすべて解決され,実行時のコストはない.

本に戻る