FABRKNT
スタックを読む — 中級への橋渡し
ソース読みのための Rust
レッスン 8 / 10·CONTENT10 分20 XP
コース
スタックを読む — 中級への橋渡し
レッスンの役割
CONTENT
順序
8 / 10

レッスン7 — unsafe Rust

問い

Rust の安全保証を一時的に外す unsafe ブロック。本来は使わない、でも Reth / Revm のホットパスでは性能のために使われる。いつ使う / なぜ安全 / どう読むか

原理(最小モデル)

  • unsafe で可能になる 5 操作. ① raw pointer dereference、② mutable static 変更、③ unsafe 関数呼び出し、④ unsafe trait 実装、⑤ union field アクセス。
  • Rust の安全保証. メモリ安全 + データ競合なし + 未定義動作なし、所有権 / 借用 / ライフタイムでコンパイル時保証、unsafe でこれを一時的に外す。
  • 契約パターン. unsafe ブロック内で「自分でチェックした不変条件」を符号化、コンパイラに「信用して」と伝える、契約破ると即 UB。
  • unwrap_unchecked(). Option::unwrap の unsafe 版、None で UB だが panic check スキップ、ホットパスで「事前に Some を保証」した後の高速化。
  • Revm の popn_top! マクロ. if stack.len() < N { return Err } でガード → unwrap_unchecked で実行時 Some チェックスキップ、ガードが不変条件を保証。
  • unsafe fn の宣伝. 関数自体が unsafe = 呼び出し側が契約を満たす責任、pub unsafe fn でドキュメントに契約を書く。
  • Soundness. unsafe コードは soundness(任意の入力で UB を起こさない性質)を満たすべき、契約を満たさない入力で UB なら soundness 違反。
  • 読むときの注意. unsafe を見たら「なぜここで必要か」+ 「どの不変条件を前提にしているか」を理解、コメント / docs を必ず読む。

具体例 + ステップで組み立てる

unsafe Rust

標準 Rust 教科書が薄く、Revm インタープリタが深く踏み込む 2 領域の 1 つである。本レッスンでは unsafe { ... } を含む Revm ホットパスを読む語彙を身につける。(もう 1 つの領域 macro_rules! は次レッスンで扱う。)

unsafe が実際に許すもの

Rust の安全性保証 (データ競合なし、use-after-free なし、境界外アクセスなし) はコンパイラが強制する — でも safe Rust に対してだけ。unsafe コードだけが許される 5 つのこと:

  1. 生ポインタの dereference (*const T または *mut T)
  2. unsafe fn の呼び出し (コンパイラが検証できない関数)
  3. 可変 static 変数のアクセス・変更
  4. unsafe trait の実装 (SendSync を手動で)
  5. union のフィールドアクセス

以上がリストのすべて。重要なのは、unsafe はローカル変数の借用チェッカーを 無効化せず、null ポインタ deref を 自動的に 許可せず、型チェックをスキップさせないということ。

契約 — unsafeする こと

unsafe はあなたからコンパイラへの 約束: 「このコードが Rust の安全性不変条件を維持していることを手動で検証した。信用していい」。

その約束が間違っていると、未定義動作 (UB) が起きる — UB は 破滅的。一度 UB が起きると、プログラム全体が不整合状態。コンパイラは UB が起きないという前提で最適化している可能性があり、その結果、実行時の振る舞いは何でもありうる: クラッシュ・誤った結果・セキュリティホール・もっともらしいけれど誤った出力。

「小さな UB」は存在しない。UB のあるプログラムは間違っている、それだけ。

なぜ Revm はホットパスで unsafe を使うか

中級レッスン 1 の Revm の popn_top! マクロには:

let ([$( $x ),*], $top) = unsafe {
    $crate::interpreter_types::StackTr::popn_top(&mut $interpreter.stack)
        .unwrap_unchecked()
};

呼ばれている関数は Option<...> を返す。unwrap_unchecked()unwrap() の unsafe 版 — 実行時の「これは Some か?」チェックをスキップする。

なぜここで安全か? 直前のコードが:

if $interpreter.stack.len() < (1 + $crate::_count!($($x)*)) {
    return Err(...);
}

だから unwrap_unchecked() の瞬間、スタック長は 証明可能に 十分。著者は目視で popn_top が Some を返すと検証済み。.unwrap() を呼ぶと冗長な実行時チェックが入る; .unwrap_unchecked() がそれをスキップ。

節約されるコスト: opcode 実行ごとに 1 分岐。何十億回走るインタープリタのホットパスで、それは計測できる。

Revm/Reth での unsafe 規律

慣用的な unsafe 使用はこんな感じ:

// 安全性不変条件を説明するコメントブロック
// SAFETY: We just checked stack.len() >= N+1 above, so popn_top
// is guaranteed to return Some.
let result = unsafe { popn_top.unwrap_unchecked() };

よく書かれた Rust の各 unsafe ブロックには // SAFETY: コメント があり、なぜ unsafe が健全かを記述する。レビュアーはこれを探す; 不在は code smell。

unsafe fn vs unsafe { ... }

関連するが異なる 2 つの概念:

意味
unsafe { ... } ブロック「5 つある unsafe な操作のうち 1 つを実行している、不変条件は検証済み」
unsafe fn foo(...)「この関数を呼ぶには unsafe が必要 — 呼び出し元 が不変条件を検証する必要がある」

unwrap_unchecked()unsafe fn。呼ぶには呼び出し場所を unsafe { ... } で囲む。それが契約: 関数が「事前条件がある」と宣言、呼び出し元が「チェック済み」と宣言。

まだ知らなくて良いもの

  • Send / Sync の手動実装 (Reth はあまりやらない)
  • インラインアセンブリ (ほぼなし)
  • C への FFI (jemalloc 関連だけ、それも 1 つのグローバル設定)

Revm/Reth ソースを読むには、上のパターン (手動安全性検証 + チェック後の unwrap_unchecked) が見るものの 95%

このレッスンで持ち帰るもの

  • unsafe は 5 つの特定のことを許す; ライセンスではなくコンパイラとの 契約
  • unwrap_unchecked() + 直前の長さ/状態チェックは、Revm のホットパスで冗長な実行時チェックをスキップする正準パターン
  • // SAFETY: コメント 規律 — よく書かれた Rust の各 unsafe ブロックは、それを正当化する不変条件を文書化する

次のレッスンは macro_rules! を扱う — Revm のインタープリタソースを読むのに必要な、もう一方の半分。

まとめ(3行)

  • unsafe = 5 操作可能 + Rust の安全保証を一時的に外す、契約パターンで「自分でチェックした不変条件」をコンパイラに伝える。
  • Revm の popn_top! 等のホットパスで unwrap_unchecked 使用、ガードが Some を保証 → 実行時チェックスキップで高速化。
  • 読むときは「なぜ必要 / どの不変条件 / soundness」を理解、unsafe は最後の手段、次は macro_rules! 基礎。