TL;DR
- Rust の参照を生ポインタにして FFI 境界に渡すと、生存期間の制約が失われ、メモリ安全性が保証されなくなる。
- メモリの確保と解放が対になるよう、RAII パターンのラッパを用意する。
- unsafe な FFI ライブラリへのアクセスは、safe な Rust のコードにカプセル化する。
内容
項目34 FFI境界を通過するものを制御しようのうち、メモリ安全性の話である。
ポインタと参照
ほとんどのデータ構造はレジスタに収まらないので、メモリ上に置き、メモリアドレスを通じてアクセスする。C はメモリアドレスをポインタ、つまりアドレスをエンコードした数値で表し、Rust は参照で表す。参照の数値表現は生ポインタとして取り出せ、FFI 境界に渡せる。
extern "C" {
pub fn use_struct(v: *const FfiStruct) -> u32;
}
let v = FfiStruct { byte: 1, integer: 42 };
let x = unsafe { use_struct(&v as *const FfiStruct) };生ポインタの危険
Rust の参照には生存期間の制約があるが、生ポインタに変換するとその制約が失われる。そのため、次の危険がある。
- ダングリングが起きうる。
- C 側でポインタの
constをキャストで外すと、Rust が不変としているデータが書き換えられうる。 - C 側が誤ってヒープのメモリを
free()でアロケータに返し、ダングリングを引き起こしうる。
これは、FFI で他の言語のコードを呼ぶことの費用対効果である。費用はメモリ安全性を失うこと、効果は対応する宣言を書く(生成する)だけで既存のコードを再利用できることである。
RAII パターンのラッパ
危険を和らげるには、メモリの確保と解放が対になるよう、RAII パターンのラッパを用意する(項目11)。
extern "C" {
pub fn new_struct(v: u32) -> *mut FfiStruct;
pub fn free_struct(s: *mut FfiStruct);
}
struct FfiWrapper {
// 不変条件: `inner` は NULL ではない
inner: *mut FfiStruct,
}
impl Drop for FfiWrapper {
fn drop(&mut self) {
// 安全性: `inner` は NULL ではない
unsafe { free_struct(self.inner) }
}
}ラッパ構造体に包むと、Result を使った安全なメソッドを提供できる。
impl FfiWrapper {
pub fn new(val: u32) -> Result<Self, String> {
let p: *mut FfiStruct = unsafe { new_struct(val) };
if p.is_null() {
Err("failed to get inner struct!".into())
} else {
Ok(Self { inner: p })
}
}
pub fn set_byte(&mut self, b: u8) {
// 安全性: `inner` が NULL でないという不変条件に依存する
let r: &mut FfiStruct = unsafe { &mut *self.inner };
r.byte = b;
}
}内部のデータ構造を直接操作しても安全なら、AsRef や AsMut を実装して直接使えるようにしてもよい。
一般化すると、「unsafe な FFI ライブラリへのアクセスを safe な Rust のコードにカプセル化しよう」となる。そうすれば、他の部分で unsafe なコードを書く必要がなくなる。危険なコードが 1 か所に集まるので、厳しく検査してテストする場所も、問題の原因を調べる場所も 1 か所で済む。