はじめに
Rustのマクロ、便利そうだから使ってみたんですよ。
vec![] とか println!() とか、標準ライブラリでよく見るし、自分でも作れたらかっこいいじゃないですか。
3時間後、自分で書いたコードが読めなくなりました。
この記事では、私がマクロ沼にハマった過程と、そこから得た知見を共有します。
目次
マクロとは何か
マクロは コードを生成するコード です。
関数との違い:
| 項目 | 関数 | マクロ |
|---|---|---|
| 実行タイミング | 実行時 | コンパイル時 |
| 引数の数 | 固定 | 可変 |
| 生成するもの | 値 | コード |
// 関数:値を返す
fn add(a: i32, b: i32) -> i32 {
a + b
}
// マクロ:コードを生成する
macro_rules! add_macro {
($a:expr, $b:expr) => {
$a + $b
};
}
fn main() {
let x = add(1, 2); // 関数呼び出し
let y = add_macro!(1, 2); // マクロ展開 → 1 + 2 に置き換わる
}
宣言的マクロ (macro_rules!)
Rustのマクロには2種類ありますが、まずは macro_rules! から。
基本構文
macro_rules! マクロ名 {
(パターン) => {
展開されるコード
};
}
最小のマクロ
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
fn main() {
say_hello!(); // Hello!
}
パターンマッチの基本
メタ変数
$名前:種類 でパターンを定義します。
| 種類 | 説明 | 例 |
|---|---|---|
expr |
式 |
1 + 2, foo()
|
ident |
識別子 |
foo, bar
|
ty |
型 |
i32, String
|
pat |
パターン |
Some(x), _
|
stmt |
文 | let x = 1; |
block |
ブロック | { ... } |
item |
アイテム | fn foo() {} |
literal |
リテラル |
42, "hello"
|
tt |
トークンツリー | なんでも |
使用例
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
}
複数のパターン
macro_rules! test {
// パターン1:引数なし
() => {
println!("No arguments")
};
// パターン2:1つの式
($e:expr) => {
println!("One expression: {}", $e)
};
// パターン3:2つの式
($a:expr, $b:expr) => {
println!("Two expressions: {} and {}", $a, $b)
};
}
fn main() {
test!(); // No arguments
test!(42); // One expression: 42
test!(1, 2); // Two expressions: 1 and 2
}
繰り返しパターン
ここからが本番。vec![] みたいな可変長引数を受け取るマクロを作ります。
基本構文
$( パターン ),* // 0回以上、カンマ区切り
$( パターン ),+ // 1回以上、カンマ区切り
$( パターン )? // 0回か1回
vec! を自作してみる
macro_rules! my_vec {
// 空のベクタ
() => {
Vec::new()
};
// 要素を持つベクタ
($($elem:expr),+ $(,)?) => {
{
let mut v = Vec::new();
$(
v.push($elem);
)+
v
}
};
}
fn main() {
let v1 = my_vec![]; // []
let v2 = my_vec![1, 2, 3]; // [1, 2, 3]
let v3 = my_vec![1, 2, 3,]; // 末尾カンマOK
}
解説
-
$($elem:expr),+: 1つ以上の式をカンマ区切りでマッチ -
$(,)?: 末尾のカンマはオプション -
$( v.push($elem); )+: マッチした各要素に対してコード生成
実用的なマクロ例
HashMap初期化マクロ
macro_rules! hashmap {
($($key:expr => $value:expr),* $(,)?) => {
{
let mut map = std::collections::HashMap::new();
$(
map.insert($key, $value);
)*
map
}
};
}
fn main() {
let scores = hashmap! {
"Alice" => 100,
"Bob" => 85,
"Charlie" => 92,
};
}
デバッグプリントマクロ
macro_rules! debug_print {
($($arg:tt)*) => {
#[cfg(debug_assertions)]
{
println!("[DEBUG] {}", format!($($arg)*));
}
};
}
fn main() {
debug_print!("x = {}", 42); // デバッグビルドでのみ出力
}
変数名と値を同時に表示
macro_rules! dbg_vars {
($($var:ident),+ $(,)?) => {
$(
println!("{} = {:?}", stringify!($var), $var);
)+
};
}
fn main() {
let x = 10;
let y = "hello";
let z = vec![1, 2, 3];
dbg_vars!(x, y, z);
// x = 10
// y = "hello"
// z = [1, 2, 3]
}
newtype パターン
macro_rules! newtype {
($name:ident, $inner:ty) => {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct $name($inner);
impl $name {
pub fn new(value: $inner) -> Self {
Self(value)
}
pub fn value(&self) -> &$inner {
&self.0
}
}
};
}
newtype!(UserId, u64);
newtype!(Email, String);
fn main() {
let user_id = UserId::new(12345);
let email = Email::new("test@example.com".to_string());
}
手続き的マクロ
macro_rules! より強力だけど、複雑。別クレートが必要。
3種類
-
derive マクロ:
#[derive(MyTrait)] -
属性マクロ:
#[my_attribute] -
関数マクロ:
my_macro!()
derive マクロの例(概要)
// proc-macro クレートで定義
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(MyDebug)]
pub fn my_debug_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let expanded = quote! {
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, stringify!(#name))
}
}
};
TokenStream::from(expanded)
}
使用側:
#[derive(MyDebug)]
struct Point {
x: i32,
y: i32,
}
手続き的マクロは深いので、また別の記事で...
マクロのデバッグ
cargo expand
マクロが何に展開されるか見るのに必須:
cargo install cargo-expand
cargo expand
// 展開前
fn main() {
let v = vec![1, 2, 3];
}
// 展開後
fn main() {
let v = <[_]>::into_vec(
#[rustc_box]
::alloc::boxed::Box::new([1, 2, 3])
);
}
trace_macros!(nightly)
#![feature(trace_macros)]
trace_macros!(true);
fn main() {
let v = vec![1, 2, 3];
}
trace_macros!(false);
println! デバッグ
マクロの中で compile_error! を使う:
macro_rules! debug_macro {
($($arg:tt)*) => {
compile_error!(stringify!($($arg)*));
};
}
よくあるハマりポイント
ハマり1:セミコロンの位置
// ❌ 間違い
macro_rules! bad {
($e:expr) => {
$e // セミコロンがない
}
}
let x = bad!(1 + 2); // エラーになる場合がある
// ⭕ 正しい
macro_rules! good {
($e:expr) => {
$e
}; // ここにセミコロン
}
ハマり2:式とブロックの違い
macro_rules! returns_value {
() => {
{
let x = 42;
x // 最後にセミコロンなし → 式として評価
}
};
}
let v = returns_value!(); // 42
ハマり3:衛生性(Hygiene)
macro_rules! declare_x {
() => {
let x = 42;
};
}
fn main() {
declare_x!();
// println!("{}", x); // エラー!マクロ内のxは外から見えない
}
マクロ内で定義した変数は外からアクセスできない。これを「衛生的マクロ」と呼びます。
ハマり4:再帰マクロ
macro_rules! count {
() => { 0 };
($head:tt $($tail:tt)*) => {
1 + count!($($tail)*)
};
}
fn main() {
let n = count!(a b c d e); // 5
}
再帰は便利だけど、深すぎると recursion limit に引っかかる。
まとめ
マクロを使うべき場面
- 可変長引数が必要
- ボイラープレートコードの削減
- DSL(ドメイン特化言語)の作成
- コンパイル時のコード生成
マクロを避けるべき場面
- 関数で十分な場合
- 可読性が大きく損なわれる場合
- デバッグが困難になる場合
チェックリスト
-
cargo expandでマクロの展開結果を確認 - パターンマッチの順序に注意(上から順にマッチ)
- 繰り返しパターンの区切り文字を正しく指定
- 衛生性を理解して変数スコープに注意
今すぐできるアクション
-
cargo install cargo-expandでツールを入れる - 標準ライブラリのマクロを
cargo expandで見てみる - 簡単なマクロから書いてみる
マクロ、最初は本当に意味がわからなかったけど、cargo expand でしか展開結果を見るようにしたら少しわかるようになりました。
でも複雑なマクロは今でも読めません。こわいですねぇ。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!