はじめに
Rustには unsafe という禁断の扉があります。
「使うな」とは言われていないけど、「できるだけ使わない方がいい」とは言われている。じゃあ、完全に封印したらどこまでできるの?
というわけで検証してみました。
目次
unsafeとは何か
unsafe ブロックでは、通常のRustでは禁止されている以下の操作ができます:
- 生ポインタの参照外し
- unsafe な関数やメソッドの呼び出し
- 可変な静的変数へのアクセス・変更
- unsafe なトレイトの実装
- 共用体のフィールドへのアクセス
fn main() {
let mut num = 5;
// 生ポインタを作る(ここまではsafe)
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// 参照外し(ここからunsafe)
unsafe {
println!("r1 is: {}", *r1);
*r2 = 10;
println!("r2 is: {}", *r2);
}
}
unsafeでしかできないこと
1. 生ポインタの参照外し
fn main() {
let address = 0x012345usize;
let r = address as *const i32;
// 任意のアドレスを読む(危険!)
unsafe {
// println!("{}", *r); // 実行するとセグフォで死ぬ
}
}
2. FFI(外部関数呼び出し)
Cライブラリを呼ぶには unsafe が必要:
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
let x = -3;
let result = unsafe { abs(x) };
println!("abs({}) = {}", x, result);
}
3. 可変静的変数
static mut COUNTER: u32 = 0;
fn add_to_counter(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_counter(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
4. Send/Syncの手動実装
struct MyType {
// ...
}
// 「このポインタはスレッド間で安全に送れる」と宣言
unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}
safe Rustの限界に挑む
チャレンジ1:リンクリストを作る
Rustでリンクリストを作るのは有名な難題です。
❌ 素朴な実装(コンパイルエラー)
struct Node {
value: i32,
next: Option<Node>, // エラー!サイズが無限大
}
⭕ Boxを使う(safe)
#[derive(Debug)]
struct Node {
value: i32,
next: Option<Box<Node>>,
}
impl Node {
fn new(value: i32) -> Self {
Node { value, next: None }
}
fn append(&mut self, value: i32) {
match self.next {
Some(ref mut next) => next.append(value),
None => self.next = Some(Box::new(Node::new(value))),
}
}
}
fn main() {
let mut list = Node::new(1);
list.append(2);
list.append(3);
println!("{:?}", list);
}
Node { value: 1, next: Some(Node { value: 2, next: Some(Node { value: 3, next: None }) }) }
結論: 単方向リストならsafe Rustで作れる!
双方向リストは?
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
next: Option<Rc<RefCell<Node>>>,
prev: Option<Weak<RefCell<Node>>>, // 循環参照を避けるためWeak
}
できるけど...Rc<RefCell<Node>> がつらい。これは後で詳しく書きます。
チャレンジ2:自己参照構造体
構造体の中で自分自身への参照を持つパターン:
// これはできない!
struct SelfRef {
value: String,
ptr: &str, // valueの一部を参照したい
}
解決策1:Pin + 外部クレート(ouroboros)
use ouroboros::self_referencing;
#[self_referencing]
struct SelfRef {
value: String,
#[borrows(value)]
#[covariant]
ptr: &'this str,
}
fn main() {
let sr = SelfRefBuilder {
value: "Hello, World!".to_string(),
ptr_builder: |s: &String| &s[0..5],
}.build();
sr.with_ptr(|ptr| {
println!("{}", ptr); // Hello
});
}
解決策2:インデックスで持つ
struct SelfRefAlt {
value: String,
start: usize,
end: usize,
}
impl SelfRefAlt {
fn get_slice(&self) -> &str {
&self.value[self.start..self.end]
}
}
結論: 自己参照は外部クレート or 設計変更で対応できる
チャレンジ3:グローバル変数
❌ 素朴な可変グローバル(unsafe必要)
static mut CONFIG: Option<Config> = None; // unsafeが必要
⭕ once_cell を使う(safe)
use once_cell::sync::Lazy;
static CONFIG: Lazy<Config> = Lazy::new(|| {
Config::load("config.toml")
});
fn main() {
println!("{:?}", CONFIG.setting); // 初回アクセスで初期化
}
⭕ std::sync::OnceLock (Rust 1.70+)
use std::sync::OnceLock;
static CONFIG: OnceLock<String> = OnceLock::new();
fn main() {
CONFIG.set("initialized".to_string()).unwrap();
println!("{}", CONFIG.get().unwrap());
}
結論: グローバル変数もsafeでできる!
チャレンジ4:高速な配列操作
境界チェックを飛ばしたい場面:
fn sum_fast(arr: &[i32]) -> i32 {
let mut sum = 0;
for i in 0..arr.len() {
// 普通のインデックスアクセスは境界チェックが入る
sum += arr[i];
}
sum
}
⭕ イテレータを使う(最適化される)
fn sum_fast(arr: &[i32]) -> i32 {
arr.iter().sum()
}
コンパイラが境界チェックを除去してくれることが多い。
get_unchecked は unsafe
fn sum_unsafe(arr: &[i32]) -> i32 {
let mut sum = 0;
for i in 0..arr.len() {
unsafe {
sum += *arr.get_unchecked(i);
}
}
sum
}
結論: イテレータを使えば大抵は最適化される。本当に必要なときだけ unsafe。
unsafeが必要になる場面
1. FFI(Cライブラリ呼び出し)
SQLite、OpenSSL、SDL2など、Cで書かれたライブラリを使うには unsafe が必須。
// bindgenで生成されたコード
extern "C" {
pub fn sqlite3_open(
filename: *const c_char,
ppDb: *mut *mut sqlite3,
) -> c_int;
}
2. ハードウェア直接操作
組み込みやOS開発では避けられない:
// メモリマップドI/O
const GPIO_BASE: usize = 0x3F20_0000;
fn set_pin_high(pin: u32) {
let set_register = (GPIO_BASE + 0x1C) as *mut u32;
unsafe {
*set_register = 1 << pin;
}
}
3. 極限のパフォーマンス
本当に必要なケースは限られるけど:
// SIMD命令を直接使う
use std::arch::x86_64::*;
unsafe fn sum_simd(arr: &[f32]) -> f32 {
// AVX命令で4つ同時に計算
// ...
}
4. 内部可変性の高度なパターン
UnsafeCell を直接使う場合:
use std::cell::UnsafeCell;
struct MyCell<T> {
value: UnsafeCell<T>,
}
impl<T> MyCell<T> {
fn get(&self) -> &T {
unsafe { &*self.value.get() }
}
fn set(&self, val: T) {
unsafe { *self.value.get() = val; }
}
}
unsafe封印の結論
⭕ safe Rustでできること
| やりたいこと | safe での方法 |
|---|---|
| リンクリスト |
Box, Rc<RefCell<T>>
|
| グローバル変数 |
once_cell, OnceLock
|
| 内部可変性 |
Cell, RefCell, Mutex
|
| 高速な配列処理 | イテレータ |
| スレッド間通信 |
Arc<Mutex<T>>, チャネル |
❌ unsafeが必要なこと
| やりたいこと | 理由 |
|---|---|
| FFI | Cのコードは安全性を保証できない |
| 生ポインタ操作 | 境界や有効性をチェックできない |
| ハードウェア操作 | メモリマップドI/Oなど |
| 可変静的変数 | データ競合の可能性 |
| 極限の最適化 | 境界チェック省略など |
実感
普通のアプリケーション開発なら、unsafe なしで90%以上できる。
でも低レイヤーをやるなら、unsafeは避けられない。大事なのは:
- unsafeの範囲を最小限にする
- 安全な抽象化でラップする
- なぜunsafeが必要か、コメントで説明する
/// # Safety
///
/// `ptr` は有効なアドレスを指していること
/// アラインメントが正しいこと
unsafe fn dangerous_operation(ptr: *mut i32) {
// ...
}
まとめ
チェックリスト
- safe Rustで実現できないか、まず考える
- 外部クレート(once_cell等)で解決できないか調べる
- unsafeが必要なら、範囲を最小限にする
- unsafeにはドキュメントコメントで理由を書く
今すぐできるアクション
-
unsafeを見たら「なぜ必要か」を考える - safe な代替手段がないか調べる
- unsafeを使うときは、安全性の契約を明記する
unsafe封印縛り、意外と快適でした。でも低レイヤーをやるなら、unsafeと友達になるしかない。
こわいですねぇ。でも正しく使えば、unsafeは強力な武器になります。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!