Rustのunsafeコード:unsafe関数、トレイト、外部関数インターフェース(FFI)

本稿では、unsafeコード、特にunsafe関数とunsafeトレイト、そして外部関数インターフェース(FFI)について解説する。まずunsafeブロックの基本を確認し、その後具体例を通じて理解を深める。

unsafeブロックの基本

unsafeブロックは以下のように記述する:

unsafe {
    // 安全でない操作
}

関数内や式の中でunsafeブロックを使用できる。例:

println!("r1: {}, r2: {}", unsafe { *r1 }, unsafe { *r2 });

unsafe関数とunsafeトレイト

unsafe関数

関数の前にunsafeキーワードを付けることでunsafe関数になる。重要な点は二つ:

  • unsafe関数内部では改めてunsafeブロックを書く必要がない。
  • unsafe関数を呼び出す側はunsafe{}ブロックで囲む必要がある。

推奨されるプラクティスは、危険な操作をunsafe関数内にカプセル化し、ブラックボックスとして管理することである。

unsafeトレイト

トレイト自体もunsafeにできる:

unsafe trait Foo {
    // メソッド定義
}

unsafe impl Foo for i32 {
    // 実装
}

fn main() {}

トレイトがunsafeである条件は、「コンパイラが検証できない不変条件(invariant)を少なくとも一つのメソッドが持つ場合」である。

不変条件とは、例えば「ポインタが常に有効なメモリを指す」「データ構造が常に有効な状態にある(二分探索木の順序性など)」といった、プログラムの正しさのために満たされるべき条件である。

簡単に言えば、unsafeトレイトとは、そのメソッドの一部にunsafeコードが含まれるトレイトと捉えてよい。

外部関数インターフェース(FFI)

外部関数インターフェース(Foreign Function Interface, FFI)は、異なる言語で書かれたコードを呼び出す仕組みである。Rustでは主にC言語との連携に使われる。例えば、JavaのJNIやPythonのctypesと似ている。FFIを利用することで既存のライブラリを活用したり、パフォーマンス重視の部分をCで記述したりできる。

以下では、C標準関数のabsprintfをRustから呼び出す例を示す。また、unsafe関数の定義と呼び出しも含む。

サンプルコード

use std::ffi::CString;
use encoding_rs::GBK;
use libc::{c_char, c_int};

// Cのabs関数の宣言
extern "C" {
    fn abs(x: i32) -> i32;
}
// Cのprintf関数の宣言
extern "C" {
    fn printf(format: *const c_char, ...) -> c_int;
}

fn main() {
    // 1. Cのabsを呼ぶ
    call_c_abs();

    // 2. Cのprintfを呼ぶ(コメントアウト解除で動作、Cランタイムが必要)
    // call_c_function();

    // 3. unsafe関数の例
    unsafe { dangerous(); }
}

fn call_c_abs() {
    let val = -42;
    let result = unsafe { abs(val) };
    println!("abs({}) = {}", val, result);
}

fn call_c_function() {
    let format = CString::new("%s %d\n").unwrap();
    let utf8_msg = "Hello from Rust, calling C printf!";
    // UTF-8をGBKに変換(コンソールがGBKを期待する場合)
    let (gbk_bytes, _, _) = GBK.encode(utf8_msg);
    let gbk_no_null = if gbk_bytes.ends_with(&[0]) {
        &gbk_bytes[..gbk_bytes.len()-1]
    } else {
        &gbk_bytes[..]
    };
    let msg = CString::new(gbk_no_null).unwrap();
    let num = 114975;
    unsafe {
        printf(format.as_ptr(), msg.as_ptr(), num);
    }
}

unsafe fn dangerous() {
    println!("I am an unsafe function, no extra unsafe block needed inside.");
    play();
}

unsafe fn play() {
    // play関数は解放後の参照を行う(意図的に危険)
    let game = Box::new(String::from("fuck dog")); // 英文字列だと落ちやすい
    // let game = Box::new(String::from("中国語文字列")); // 中国語だと落ちにくい(環境依存)
    let raw = Box::into_raw(game);
    println!("Game: {:?}", *raw);
    drop(Box::from_raw(raw));
    println!("After drop: {:?}", *raw); // 未定義動作
}

play()関数内では、Boxのメモリを解放した後に再び参照している(未定義動作)。このコードの振る舞いは環境に依存し、例えば英文字列の場合はセグフォが起きやすいが、中国語文字列だと起きにくい場合がある。これはRustのバグではなく、未定義動作の典型的な現象である。

まとめ

  • unsafe関数は危険な操作を局所化し、可読性・デバッグ性を向上させる。
  • unsafeトレイトは不変条件をコンパイラが検証できない場合に使用し、実装者はそれらの条件を自ら保証する必要がある。
  • FFIによりRustは他の言語と連携でき、既存資産の再利用や性能最適化が可能になる。

タグ: rust unsafe FFI C言語 トレイト

8月6日 16:25 投稿