Rustの静的変数とmutable staticを徹底解説!なぜunsafeが必要なのか?
生徒
「Rustでプログラム全体からアクセスできる『グローバル変数』を作りたいんですけど、どうすればいいですか?」
先生
「Rustではstaticキーワードを使って静的変数を定義します。ただし、値を書き換えられるmutable staticを使うときは、少し注意が必要なんですよ。」
生徒
「書き換えようとするとエラーが出るし、unsafeっていう怪しいブロックで囲めって言われるんです。これって危ないコードなんですか?」
先生
「鋭いですね。Rustが最も大切にしている『メモリ安全』や『データ競合の防止』というルールを、静的変数が壊してしまう可能性があるからなんです。なぜunsafeが必要なのか、その理由を深掘りしてみましょう!」
1. Rustの静的変数(static)の基本構造
Rustにおける静的変数(static variable)とは、プログラムの実行開始から終了までメモリ上の同じ場所に存在し続ける変数のことです。一般的なローカル変数は、関数が終わるとスコープを抜けて消滅してしまいますが、静的変数は「ライフタイム」がプログラム全体にわたるため、どこからでも参照できるのが特徴です。
静的変数を定義する際は、必ず型を明示する必要があります。これは、コンパイラがバイナリ内のデータセグメントにどれだけのスペースを確保すべきかを事前に把握する必要があるためです。また、代入できる値はコンパイル時に決定できる「定数式」に限られます。動的な計算結果をそのまま入れることはできません。
static GREETING: &str = "こんにちは、Rustの世界へ!";
fn main() {
println!("メッセージ: {}", GREETING);
}
メッセージ: こんにちは、Rustの世界へ!
上記のコードでは、GREETINGという静的変数を定義しています。これは読み取り専用のデータとして扱われ、安全にアクセスできます。しかし、値を変更したくなった場合に、Rustの厳格な安全性が牙を剥くことになります。
2. 定数(const)と静的変数(static)の決定的な違い
初心者がよく混乱するのが、const(定数)とstatic(静的変数)の使い分けです。どちらも値が変わらないイメージがありますが、メモリ上の扱われ方が全く異なります。この違いを理解することが、後のmutable staticの理解に繋がります。
定数(const)は、インライン展開されます。つまり、コンパイル時にその定数が使われている箇所すべてに値が直接埋め込まれます。実体としてのメモリ住所(アドレス)を必ずしも持ちません。一方で、静的変数(static)はメモリの特定の場所に唯一の実体として配置されます。そのため、変数のアドレスを取得して共有することが可能です。シングルトンパターンのような、プログラム全体で一つの状態を共有したい場合には、必ずstaticが必要になります。
また、定数は不変であることが保証されていますが、静的変数は次に説明するmutを付けることで、値を書き換える余地が残されています。この「共有されているのに書き換え可能」という性質が、マルチスレッドプログラミングにおいて非常に大きなリスクを生む原因となります。
3. 可変な静的変数であるmutable staticの書き方
Rustでは、静的変数の宣言にmutキーワードを付けることで、値を変更できる変数を作ることができます。これをmutable staticと呼びます。しかし、これを実際に使おうとすると、Rustコンパイラは「これは安全ではありません」と警告し、ビルドを拒否します。アクセスするためには、unsafeブロックで囲む必要があります。
まずは、どのような構文になるのか、実際のコードを見てみましょう。ここではカウンターをグローバルに保持し、それを増やしていく処理を記述します。
static mut GLOBAL_COUNTER: i32 = 0;
fn increment_counter() {
// static mutへのアクセスはunsafeブロックが必須
unsafe {
GLOBAL_COUNTER += 1;
}
}
fn main() {
increment_counter();
increment_counter();
unsafe {
println!("現在のカウンター: {}", GLOBAL_COUNTER);
}
}
現在のカウンター: 2
このように、定義自体は普通にできますが、読み取る際も書き込む際もunsafeが必要です。Rustがここまで慎重になるのは、プログラマが「この操作が安全であること」を自分自身の責任で保証しなければならないからです。
4. なぜmutable staticはunsafeなのか?データ競合のリスク
Rustがmutable staticへのアクセスを危険だと判断する最大の理由は、データ競合(Data Race)です。データ競合とは、複数のスレッドが同時に同じメモリ位置にアクセスし、かつ少なくとも一つのスレッドが書き込みを行っている状態を指します。
もし、スレッドAがGLOBAL_COUNTERを読み取っている最中に、スレッドBがその値を書き換えたらどうなるでしょうか。スレッドAは中途半端に破壊されたデータを読み込んでしまうかもしれません。また、二つのスレッドが同時に「1を足す」という操作を行った場合、計算が重なってしまい、結果として1しか増えないというバグが発生することもあります。
通常のRustの変数であれば、所有権システムと借用チェッカーがスレッド間のデータの貸し出しを厳しく監視し、このような事態を未然に防ぎます。しかし、静的変数はプログラムのどこからでも、誰でも、いつでも触れる場所にあります。コンパイラには「いつ、どのスレッドがアクセスしてくるか」を完全に予測することができません。そのため、標準の安全ルールを適用できず、プログラマに責任を委ねるunsafeが必要になるのです。
5. 所有権システムが静的変数を管理できない理由
Rustの強力な武器である「所有権」は、データの持ち主が一人であることを保証し、持ち主がいなくなったらデータを捨てる、という仕組みです。しかし、静的変数は「最初から最後までそこにあり続ける」ことが前提であるため、誰か一人が所有するという概念が馴染みません。
通常、可変な参照(&mut T)は、ある時点で世界にたった一つしか存在できないというルールがあります。これにより、データの整合性が保たれています。ところが、グローバルな静的変数は、関数の引数として渡さなくても直接触れてしまいます。複数の関数が同時にstatic mutを書き換えようとしたとき、Rustが守ろうとしている「可変参照の排他性」が簡単に破られてしまうのです。
この矛盾を解消するために、Rustは「静的変数を書き換える行為は、コンパイラによる安全保証の対象外」と設定しました。これが、私たちが不便に感じるunsafeの正体です。つまり、不便にすることで、安易にグローバルな状態を持たないように促しているとも言えます。
6. 実践:unsafeを避けて安全にグローバル状態を管理する方法
実際の開発では、static mutを直接使う機会はそれほど多くありません。なぜなら、Rustにはもっと安全で使いやすい代替手段が用意されているからです。その代表例が、Mutex(ミューテックス)やAtomic(アトミック)型です。
以下の例では、AtomicI32を使用して、unsafeを使わずに安全に数値を更新する方法を示します。アトミック型はCPUレベルで排他制御を行うため、スレッドセーフに値を操作できます。さらに、初期化のタイミングを制御できるLazyな仕組み(std::sync::OnceLockなど)を組み合わせるのが一般的です。
use std::sync::atomic::{AtomicI32, Ordering};
// 原子的な操作をサポートする型を使えば安全
static SAFE_COUNTER: AtomicI32 = AtomicI32::new(0);
fn main() {
// 読み込みと更新をスレッドセーフに行う
SAFE_COUNTER.fetch_add(1, Ordering::SeqCst);
SAFE_COUNTER.fetch_add(1, Ordering::SeqCst);
let final_val = SAFE_COUNTER.load(Ordering::SeqCst);
println!("安全なカウンターの値: {}", final_val);
}
安全なカウンターの値: 2
このコードにはunsafeが一つも登場しません。内部的には安全な方法で同期が行われており、Rustのコンパイラも「これならデータ競合は起きない」と太鼓判を押してくれます。初心者のうちは、無理にstatic mutを使おうとせず、こうしたスレッドセーフな道具を頼るのが上達の近道です。
7. FFI(外部関数インターフェース)での静的変数の活用
「そんなに危険なら、なぜstatic mutなんて機能があるの?」と思うかもしれません。その主な理由の一つが、C言語などで書かれたライブラリとの連携(FFI)です。C言語の世界ではグローバル変数が多用されており、それらをRustから操作する必要がある場面が出てきます。
低レイヤーのシステムプログラミングや、OSの自作、組み込み開発などでは、メモリの特定の番地にある値を書き換えることが必須となります。このような極限の環境において、Rustは「危険を承知で自由にメモリを触る手段」としてunsafeとstatic mutをあえて残しているのです。ただし、それはあくまで例外的な処置であり、通常のアプリケーション開発では避けるべき手法であることは間違いありません。Rustの柔軟性と厳格さが同居している面白い部分でもあります。
8. メモリ安全性を守るためのプログラマの心構え
unsafeを使うということは、コンパイラに対して「ここからは私が責任を持つから、口出ししないでくれ」と宣言することと同じです。しかし、人間はミスをする生き物です。unsafeブロックの中身は最小限に抑え、その周囲を安全な公開関数で包み込む(カプセル化する)のがRustの上級者のテクニックです。
例えば、ハードウェアのレジスタを操作するためにstatic mutを使わざるを得ない場合でも、その変数を直接外部に公開してはいけません。必ず、安全なメソッドを介してアクセスするように設計しましょう。そうすることで、プログラムの大部分はRustの保護の下に置かれ、不具合が起きた際の調査対象をunsafeブロック内に限定できるようになります。これが、Rustが提供する「実用的な安全性」の考え方です。
struct GlobalState {
value: i32,
}
// 実際にはもっと複雑な同期処理が必要になることが多い
static mut INTERNAL_STATE: GlobalState = GlobalState { value: 0 };
pub fn update_state(new_val: i32) {
// 危険な操作を関数内に閉じ込める
unsafe {
INTERNAL_STATE.value = new_val;
}
}
pub fn get_state() -> i32 {
unsafe {
INTERNAL_STATE.value
}
}
fn main() {
update_state(100);
println!("状態を取得: {}", get_state());
}
状態を取得: 100
この例では、メイン関数からはunsafeが見えなくなっています。このように「危険を隔離する」設計こそが、Rustで堅牢なシステムを作り上げる鍵となります。静的変数の扱いを通じて、Rustがなぜこれほどまでにメモリ安全に執着するのか、その思想を感じ取っていただけたでしょうか。