本文へスキップ
BecomeCoder

Rustコース · 第12章 プロジェクトとツール · レッスン49

マクロと unsafe ― macro_rules! と安全の境界

ローカル実施

導入

この本の最初から使ってきた println!vec! ——末尾に ! が付くこれらは、実は関数ではなく マクロ です。そして、Rust の安全性の看板の裏には、意図的にその保証の一部を外せる unsafe という仕組みもあります。最後のレッスンでは、この2つの「特別な力」を覗いてみます。

説明

マクロは、コンパイル時にコードそのものを展開する仕組みです。関数のように「呼ばれたら実行される」のではなく、「その場に別のコードが書き換わる」とイメージしてください。だからこそ println!("{} {}", a, b) のように、引数の個数が可変でも成り立ちます(普通の関数では引数の数は固定です)。

自分でも macro_rules! で簡単なマクロを作れます。

macro_rules! square {
    ($x:expr) => {
        $x * $x
    };
}

fn main() {
    let result = square!(5);
    println!("{result}");   // 25
}
  • macro_rules! square { ($x:expr) => { $x * $x }; } … 「square!(何か) と書かれたら、それを 何か * 何か に置き換える」というルールです。
  • $x:expr$x という名前で「式(expression)」を受け取る、という意味です。
  • square!(5) は、コンパイル時に 5 * 5 へ置き換わってから型チェック・コンパイルされます。vec![1, 2, 3]assert_eq!(a, b) も、同じ仕組みで動いている標準ライブラリのマクロです。

続いて unsafe です。Rust は通常、コンパイラが所有権・借用・型のルールを機械的にチェックして安全性を保証します。ですが、生のメモリ番地を直接扱う、他言語のライブラリを呼ぶ(FFI)といった場面では、コンパイラには「安全かどうか判断できない」操作が必要になります。そこで使うのが unsafe ブロックです。

fn main() {
    let num = 5;
    let r1 = &num as *const i32;   // 生ポインタ(作るだけなら安全)

    unsafe {
        println!("r1 の指す値: {}", *r1);   // 参照外しには unsafe が必要
    }
}
  • &num as *const i32 … 参照を生ポインタ*const i32)に変換します。これ自体は安全な操作です。
  • unsafe { *r1 } … 生ポインタの参照外しは、コンパイラが「指す先が有効かどうか」を保証できないため、unsafe ブロックの中でしか書けません。

unsafe は「チェックを全部やめる」わけではありません。所有権のルールや型チェックは変わらず働き続け、unsafe ブロックの中でだけ、生ポインタの参照外しなどごく一部の追加できる操作が解禁されます。その代わり、そこで安全性を壊さないことの責任は、コンパイラではなく書き手が負います。

だからこそ実務では、「99%は安全なRustで書き、unsafe はどうしても必要な境界だけに絞って局所化する」のが基本姿勢です。unsafe ブロックを小さく保ち、その周りに安全なAPIの皮をかぶせておけば、unsafe を使うコード自体はごく一部でも、プログラム全体としては安全性を保てます。実際、標準ライブラリの VecString の内部にも unsafe は使われていますが、外から使う私たちはそれを一切意識せずに安全に使えています。それが、Rust が「速度」と「安全」を両立させている理由の1つです。

まとめ

  • マクロ(macro_rules!)はコンパイル時にコードを展開する仕組み。println!/vec!/assert_eq! も標準ライブラリのマクロ。
  • 関数と違い、引数の個数や構文の形を自由に扱えるのがマクロの強み。
  • unsafe は生ポインタの参照外しやFFIなど、コンパイラの一部の安全保証を外して書き手が責任を持つための仕組み。
  • 実務では「基本は安全なRust、unsafe は最小限の境界だけ」という姿勢が徹底される。