レッスン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 つのこと:
- 生ポインタの dereference (
*const Tまたは*mut T) unsafe fnの呼び出し (コンパイラが検証できない関数)- 可変 static 変数のアクセス・変更
unsafe traitの実装 (SendやSyncを手動で)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!基礎。