本稿では、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標準関数のabsとprintfを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は他の言語と連携でき、既存資産の再利用や性能最適化が可能になる。