🦀

【Rust】怖くないunsafe入門

に公開

今回の記事はRustのunsafeについて。最近BunのRust移行やら何やらでunsafe Rustが話題(?)ですが、unsafeという単語から色々と誤解を生んでいるような気がしています。unsafeが含まれているから危険!というわけではなく、低レイヤーやFFI周りでは適切にunsafeを使うことで上手く付き合っていく必要があるでしょう。

というわけで今回は、unsafe Rustの基本からそのベストプラクティスについてなどをまとめていきます。

unsafeとは

unsafeはRustに限ったものではなく、色々な言語に存在する概念です。現代では多くの言語がデフォルトでメモリ安全ですが、限界までパフォーマンスを追求したり、FFIでC ABIを通して他言語とやりとりをおこなう際には、どうしてもポインタを直接扱う必要が生じます。これらは安全性を保証するsafe Rustで扱いきれないため、どうしてもunsafeが必要になってきます。

また、Rustは所有権や未定義動作に対して厳しい制約を敷いている言語ですが、unsafeではその制約を緩和することができます。これはポインタの利用だけではなく、unsafeな関数の呼び出しやmutableなstatic変数へのアクセス、実装によって予期しない動作を起こす可能性のあるunsafe Traitの実装なども含まれます。

なお、よくある誤解として「unsafeが使われているコードは危険である」という認識がありますが、これは必ずしもそうとはいえません。Rustではunsafe内部でも引き続き厳密なチェックは行われ、明示的に危険な操作を行わない限り、直ちに未定義動作を引き起こすようなことはありません。むしろunsafeで適切にスコープを区切って安全性を保証することが出来るため、利用者にsafe Rustでラップした安全なAPIを提供することが可能となっています。慎重な操作を要する部分をunsafeで明示できる、というのがポイントですね。

unsafeを使ってみる

というわけでまずは実際にやってみましょう。Rustではunsafeブロックでコードを囲うことでunsafeを有効化できます。

fn main() {
    // ここまでsafe
    unsafe {
        // ここはunsafe
    }
    // ここからsafe
}

unsafeブロック内部ではポインタを参照外し(deref)することができます。

let mut num = 5;

// 参照からポインタを取得する
// 取得するだけなら安全なので、ここまではsafe Rustでも可能
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

// usizeでアドレスを直接指定してポインタを作ることも可能
// これも作るだけならsafe
// ただし普通はセグフォするのでやらない
let address = 0x012345usize;
let r = address as *const i32;

unsafe {
    // derefにはunsafeが必要
    println!("r1 is: {}", *r1);
    println!("r2 is: {}", *r2);
}

また、unsafeな関数を呼び出すにもunsafeブロックが必要です。試しにunsafe fnを定義してみましょう。

// unsafe名関数
unsafe fn foo() {}

fn main() {
    unsafe {
        // 呼び出しにはunsafeが必要
        foo();
    }

    // これはコンパイルエラー
    foo();
}

また、FFIでextern "C"な関数を呼び出す際にもunsafeが必要です。

extern "C" {
    fn add(a: i32, b: i32);
}

fn main() {
    unsafe {
        let result = add(1, 2);
        println!("{}", result);
    }
}

ほかにも、mutableなstatic変数にアクセスする場合にもunsafeが必要になります。これはstatic変数が常に同じアドレスを指すため、複数のスレッドから書き込まれるとデータ競合を引き起こす可能性があるためです。

static FOO: &str = "foo";
static mut COUNTER: u32 = 0;

fn main() {
    // immutableなstatic変数はsafeでもアクセス可能
    println!("{}", FOO);

    unsafe {
        // mutableなstatic変数にはunsafeが必要
        COUNTER += 1;
        println!("COUNTER: {}", COUNTER);
    }
}

Traitをunsafeでマークすることもできます。unsafe Traitの実装にはunsafe implが必要になります。

// unsafe traitを定義
unsafe trait Foo {
    fn foo();
}

// 実装にはunsafe implが必要
unsafe impl Foo for i32 {
    fn foo() {}
}

unsafeで壊してみる

せっかくなので、実際に「危険な」unsafeの使い方もやってみましょう。

領域外アクセス

Vec<T>からas_mut_ptr()でmutableなポインタを取得できます。これの位置をずらし、範囲外のメモリに書き込んでみます。

fn main() {
    let mut vec = vec![1, 2, 3];
    unsafe {
        let ptr = vec.as_mut_ptr();
        *ptr.add(99999) = 10; // 範囲外に書き込む!
    }
}

これを実行するとExited with signal 11 (SIGSEGV): segmentation violationが発生しました。ポインタ経由の読み書きに関してチェックは一切行われないので、このような動作が起こる可能性があります。

ダングリングポインタ

続いては既に開放済みのポインタにアクセスしてみます。このようなポインタは ダングリングポインタ(Dangling Pointer) と呼ばれます。

fn main() {
    let mut ptr: *const i32 = std::ptr::null();

    {
        let vec = vec![1, 2, 3];
        ptr = vec.as_ptr();
    } // ここでvecはスコープを抜けたため、メモリは解放される

    unsafe {
        // 解放されたメモリを参照している!
        println!("{}", *ptr); 
    }
}

これを実行してみたところ、-1437818960のような値が表示されました。意図しないメモリのアドレスを参照しているため、場合によっては動作せずにクラッシュする場合もあります。

可変参照を複数作成する

通常Rustでは同じデータに対する可変参照&mutは同時に1つまでしか存在することができません。しかし、unsafeを用いることでこの制約を壊すことができます。

fn main() {
    let mut data = 10;

    // 同じデータを指す2つのポインタを作成
    let p1: *mut i32 = &mut data;
    let p2: *mut i32 = &mut data;

    unsafe {
        // 本来なら許されない「同じデータへの2つの可変参照」を作成出来る
        let ref_mut1: &mut i32 = &mut *p1;
        let ref_mut2: &mut i32 = &mut *p2;

        *ref_mut1 = 100;
        *ref_mut2 = 200;

        println!("data: {}, ref1: {}, ref2: {}", data, ref_mut1, ref_mut2);
    }
}

Rustコンパイラは可変参照が1つまでであることを仮定して最適化を行うため、これによって予期しない動作を引き起こす可能性があります。

transmuteで無関係の型に変換する

std::mem::transmuteはメモリ配置が同じ別々の型をコピーなしでそのままで変換する関数です。これはメモリ配置が同等な場合は有効ですが、無関係な型同士で行った場合の動作は未定義です。

fn main() {
    unsafe {
        // u8を無理やりboolとして解釈させる
        let invalid_bool: bool = std::mem::transmute(42u8);
        
        // 以下のどちらが出力されるかは未定義
        match invalid_bool {
            true => println!("true!"),
            false => println!("false!"),
        }
    }
}

このように間違った使い方をすると予期しない動作を引き起こすため、unsafeの利用は慎重に行いましょう。

safeなAPIでラップする

FFIでC APIをラップする場合などでは、unsafeなAPIをそのまま公開すると利用側にまでunsafeが波及してしまいます。このような場合には、あらかじめ内部で安全性を保証したsafeなAPIでラップしておくのがベストプラクティスです。

例えば、以下のようなC APIを呼び出すFFIがあったとします。

use std::os::raw::{c_char, c_int};

#[repr(C)] pub struct CLogBuffer { _private: [u8; 0] }

extern "C" {
    // ログ出力用のバッファを作成する(失敗したらnullを返す)
    fn log_open() -> *mut CLogBuffer;
    
    // 文字列をバッファに書き込む
    fn log_write(buffer: *mut CLogBuffer, message: *const c_char) -> c_int;
    
    // バッファを閉じ、ディスク等にフラッシュしてメモリを解放する
    fn log_close(buffer: *mut CLogBuffer);
}

これをラップするなら以下のようになるでしょう。

use std::ffi::CString;
use std::ptr::NonNull;

// C側のエラー状態を表現するenum
#[derive(Debug)]
pub enum LogError {
    AllocationFailed,
    InvalidString,
    BufferFull,
}

// Rust側でポインタを構造体でラップする
pub struct LogBuffer {
    // NonNullはnullでないことを保証するポインタ
    raw: NonNull<CLogBuffer>,
}

impl LogBuffer {
    pub fn open() -> Result<Self, LogError> {
        unsafe {
            let raw_ptr = log_open();
            
            // nullチェック
            let non_null = NonNull::new(raw_ptr)
                .ok_or(LogError::AllocationFailed)?;
                
            Ok(LogBuffer { raw: non_null })
        }
    }

    pub fn write(&self, message: &str) -> Result<(), LogError> {
        // 文字列をnull終端に変換
        let c_msg = CString::new(message)
            .map_err(|_| LogError::InvalidString)?;

        unsafe {
            let result = log_write(self.raw.as_ptr(), c_msg.as_ptr());
            
            if result == -1 {
                return Err(LogError::BufferFull);
            }
            
            Ok(())
        }
    }
}

// Drop時に自動的にcloseが呼ばれるようにする
impl Drop for LogBuffer {
    fn drop(&mut self) {
        unsafe {
            log_close(self.raw.as_ptr());
        }
    }
}

このようにsafeなAPIでラップすることで、利用者側はunsafeな部分を意識することなく利用できるようになります。

もちろん、呼び出し側のAPIがsafeだからといってそれが安全であるとは限りません。 内部のunsafeコードの実装にミスがある場合、引き起こされた未定義動作はsafe側にまで波及します。そのため、安全性はunsafe側の実装者が責任を持って保証する必要があるでしょう。

まとめ

というわけでunsafe Rustの話でした。unsafeはそれ自体が危険なものではなく、使いどころを理解して局所的に用いていくものです。FFIでは避けて通ることの出来ない概念ですし、標準ライブラリの内部も多くのunsafeが使われています。unsafeをむやみに恐れず、必要に応じて上手く使いこなせるようになっていきましょう。

Discussion