4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Rustには unsafe という禁断の扉があります。

「使うな」とは言われていないけど、「できるだけ使わない方がいい」とは言われている。じゃあ、完全に封印したらどこまでできるの?

というわけで検証してみました。

目次

  1. unsafeとは何か
  2. unsafeでしかできないこと
  3. safe Rustの限界に挑む
  4. unsafeが必要になる場面
  5. unsafe封印の結論

unsafeとは何か

unsafe ブロックでは、通常のRustでは禁止されている以下の操作ができます:

  1. 生ポインタの参照外し
  2. unsafe な関数やメソッドの呼び出し
  3. 可変な静的変数へのアクセス・変更
  4. unsafe なトレイトの実装
  5. 共用体のフィールドへのアクセス
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は避けられない。大事なのは:

  1. unsafeの範囲を最小限にする
  2. 安全な抽象化でラップする
  3. なぜunsafeが必要か、コメントで説明する
/// # Safety
/// 
/// `ptr` は有効なアドレスを指していること
/// アラインメントが正しいこと
unsafe fn dangerous_operation(ptr: *mut i32) {
    // ...
}

まとめ

チェックリスト

  • safe Rustで実現できないか、まず考える
  • 外部クレート(once_cell等)で解決できないか調べる
  • unsafeが必要なら、範囲を最小限にする
  • unsafeにはドキュメントコメントで理由を書く

今すぐできるアクション

  1. unsafe を見たら「なぜ必要か」を考える
  2. safe な代替手段がないか調べる
  3. unsafeを使うときは、安全性の契約を明記する

unsafe封印縛り、意外と快適でした。でも低レイヤーをやるなら、unsafeと友達になるしかない。

こわいですねぇ。でも正しく使えば、unsafeは強力な武器になります。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?