裸のポインタと FFI

19 分で読了
このページの目次

裸のポインタは、Rust の参照モデルに直接収まらない状態を表現するために使用されます。例えば、null 可能な外部ハンドル、コンテナ内部の未初期化ストレージ、低レベルなアロケーションやハードウェアインターフェースなどが該当します。FFI は重要な用途ですが、唯一の用途ではありません。

*const T と *mut T は、借用チェッカーが完全に追跡できるライフタイムを保持していません。これらは null だったり、アライメントが合っていなかったりします。具体的なアクセスを実行した時点で、その操作に必要な条件をすべて満たす必要があります。

ポインタの取得元の割り当てと provenance。数値アドレスだけでは不十分1ストレージの有効性を確認オブジェクトのライフタイム、アクセス範囲、アライメント、初期化2アクセス方法の選択読み書き・未整列アクセス・参照構築は前提が異なる3エイリアシングと並行性の制約を維持4裸のポインタがアドレスから合法なアクセスに至るには、条件を順次満たす必要がある複数のポインタが存在しても、任意の同時アクセスが許可されるわけではないnull またはダングリングポインタの値は存在し得る;それを保持できることと、それをデリファレンスできることは別問題である。

ポインタの存在とアクセスの有効性は異なる

fn main() {
    let value = 42_u32;
    let pointer = &raw const value;
    // SAFETY: value は存続中で初期化済み;pointer はアライメントされており、読み取りに競合する書き込みはない。
    let copied = unsafe { pointer.read() };
    assert_eq!(copied, 42);
    let null = std::ptr::null::<u32>();
    assert!(null.is_null()); // null ポインタの保存とチェックにはデリファレンスが必要ない
}

複数の *mut T が同じ位置を指すことは可能ですが、これは同時に有効で互いに競合する &mut T を作成したり、非原子性の並行読み書きを行ったりすることを許可するものではありません。ポインタの const/mut は、その型を通じて許可されるアクセス方向を示すだけで、元々読み取り専用または共有参照で保護されているオブジェクトを自動的に書き込み可能にはしません。ポインタモジュール

ptr.read は非 Copy 型に対して、ビット単位で所有権を持つ値を読み出します。呼び出し側は、その後同じリソースを独立した所有権の2つとして解放しないようにする必要があります。ptr.write は元の値をデストラクトしません。上書きする前に既存のリソースを解放する必要があるかどうかは、別途処理する必要があります。

裸のポインタから参照への変換は追加の約束を増やす

&*ptr や &mut *ptr は「より便利な構文に変えるだけ」ではありません。新しい参照は、null ではないこと、アライメントされていること、有効なターゲット値を持つこと、そして借用期間全体を通じての適切なアクセス制約を要求します。その後すぐに読み書きしなくても、これらの要件を無視することはできません。

同様に、slice::from_raw_parts の長さがゼロの場合でも、通常は null ではなくアライメントされたポインタを要求します。C インターフェースで NULL + 0 が許可されていても、Rust の空スライスを直接構築できるわけではありません。外部バッファは、それぞれのインターフェースの要件を最初に確認する必要があります。

参照を返す安全なラッパーは、ライフタイムを実際にストレージを保持しているオブジェクトにバインドすべきです。所有者情報を持たない裸のポインタに基づいて、呼び出し側に無制限の長さの借用を許してはいけません。これは、関数の外側に unsafe マークを追加するよりも重要です。

NULL + 0 の場合を個別に分支させるが、null チェックを完全な検証と見なさない

以下の関数はバイト単位で XOR チェックサムを計算し、空のバッファを NULL + 0 で表現することを許可します。unsafe fn を使用するのは、非空ポインタに対するメモリ有効性を整数チェックだけでは証明できないためです。

/// # Safety
/// len > 0 かつ data が非空の場合、呼び出し側は同一のアロケーション内の len バイトの初期化済みデータを指していることを保証し、
/// 呼び出し全体を通じて読み取り可能で存続し、競合する書き込みがないこと;長さが isize::MAX を超えないことを保証する。
unsafe fn xor_bytes(data: *const u8, len: usize) -> Result<u8, &'static str> {
    if len == 0 { return Ok(0); }
    if data.is_null() { return Err("非空の長さに null ポインタ"); }
    if len > isize::MAX as usize { return Err("長さが大きすぎる"); }
    // SAFETY: 上記で null ポインタと大きすぎる長さを除外;残りの範囲、初期化、アクセスの前提は呼び出し側が保証する。
    let bytes = unsafe { std::slice::from_raw_parts(data, len) };
    Ok(bytes.iter().fold(0, |result, byte| result ^ byte))
}
fn main() {
    let bytes = [1u8, 2, 3, 4];
    // SAFETY: 配列は有効であり、ポインタと長さは同一の完全な配列から取得され、並行書き込みはない。
    assert_eq!(unsafe { xor_bytes(bytes.as_ptr(), bytes.len()) }, Ok(4));
    // SAFETY: これらの組み合わせはインターフェースによって明確に処理され、null ポインタにアクセスすることはない。
    assert_eq!(unsafe { xor_bytes(std::ptr::null(), 0) }, Ok(0));
    assert!(unsafe { xor_bytes(std::ptr::null(), 1) }.is_err());
}

data が非空であり、len がオーバーフローしていないことを確認した後でも、バッファの実際のサイズや、それが解放済みかどうかは分かりません。したがって、関数を任意の安全な呼び出しに対するポインタ検証器に変更することはできません。from_raw_parts の len はバイト数ではなく要素数で指定されるため、u32 に変更する場合は、アライメントと総バイトサイズも考慮する必要があります。from_raw_parts

上位レイヤーがすでに &[u8] を保持している場合は、スライスを直接受け取るだけで十分です。Rust のライフタイム情報が欠如した境界を本当に跨ぐ場合のみ、ここで追加の前提条件を維持する必要があります。unsafe を示すために、信頼できるスライスを一旦ポインタに分解し、別の場所で長さを推測して復元するようなことはしないでください。

所有権(provenance):整数としてのアドレスが一致するからといって交換可能ではない

ポインタはアドレスだけでなく、アクセスの来源などのセマンティクスも持っています。整数への変換、境界外ポインタ演算、アロケーションを跨ぐ結合は、操作の前提条件を破壊する可能性があります。2つのアロケーションがたまたま隣接していても、自動的に1つのRustスライスを合成できるわけではありません。

add/offset などの操作には範囲とオーバーフローの要件があります。wrapping_add は計算段階の条件を緩和できますが、最終的なデリファレンスを合法にするものではありません。アドレスを調整する必要がある場合は、任意に as usize に変換して戻すよりも、ソースの所有権を保持する addr、with_addr などのインターフェースを理解した方が意図を表現しやすくなります。Rust の完全なエイリアシングモデルは現在も発展中であり、特定のインタープリタモデルをすべての詳細の最終仕様として扱うのではなく、明確な標準ライブラリの契約に依存すべきです。

重複コピーと非重複コピーは異なる契約である

fn main() {
    let mut bytes = [1u8, 2, 3, 4, 5];
    let pointer = bytes.as_mut_ptr();
    // SAFETY: ソース 0..4 とターゲット 1..5 はどちらも有効な配列内にあり;u8 はアライメントされており、重複が許可される。
    unsafe { std::ptr::copy(pointer, pointer.add(1), 4); }
    assert_eq!(bytes, [1, 1, 2, 3, 4]);
    let mut safe = [1u8, 2, 3, 4, 5];
    safe.copy_within(0..4, 1);
    assert_eq!(safe, bytes);
}

ptr::copy はセマンティクス上、重複を許可しており、ソース領域を一旦保存してからターゲットに書き込むような効果を持ちます。テストが1回通ったからといって、ここで copy_nonoverlapping に置き換えてはいけません。両方の count は T の要素数を表します。通常のスライスデータには、境界をチェックする copy_within、copy_from_slice などのインターフェースを優先して使用してください。ptr::copy

ビット単位のコピーは、Clone の呼び出しではありません。String などのリソースを所有する型に対して、ポインタ/長さフィールドをコピーした後、両方のオブジェクトがデストラクトされると、所有権が破壊されます。コンテナ内部でこのような操作を使用して要素を移動する場合、どのスロットがまだ有効な値を所有していると見なされるかを同時に維持する必要があります。u8 の例はこのデストラクトの責任を回避していますが、任意の T に汎用化しても同様に簡単であるとは限りません。

コピー方向では非重複契約を代替できない

コピー先を動かし、分離した範囲を重ねます。結果は ptr::copy の意味に従い、常にコピー前の元の値を使います。手書きループの方向が偶然正しくても、copy_nonoverlapping の呼び出し前提は変わりません。

図を準備しています
コピー方向では非重複契約を代替できない

ptr::copy は重複を許し、copy_nonoverlapping は非重複を要求します。u8 のコピーには二重破棄はありません。

packed:非アライメントされたレイアウトは許可されるが、非アライメントされた参照は許可されない

#[repr(C, packed)]
struct Packet {
    kind: u8,
    length: u32,
}

fn main() {
    let packet = Packet { kind: 1, length: 42 };
    let pointer = &raw const packet.length;
    // SAFETY: 初期化済みの u32 を指しており、アクセス期間中にストレージは有効;非アライメント読み取りを使用する。
    let length = unsafe { pointer.read_unaligned() };
    assert_eq!(length, 42);
}

&packet.length を書いてからそれを裸のポインタに変換しないでください。それはすでに非アライメントされた参照の形成を試みていることになります。repr(packed) はバイトオーダーを処理しないため、このメモリレイアウトがクロスプラットフォーム向けのネットワークエンコーディング用に設計されていることを意味するわけではありません。バイトプロトコルに対しては、構造体のメモリレイアウトに依存しないように from_le_bytes/from_be_bytes をよく使用します。

FFI の3つの境界

extern "C" はターゲットプラットフォームの C 呼び出し規約を決定し、repr(C) は対応する型のレイアウトルールを決定し、具体的なポインタ/長さ/解放プロトコルはリソースのセマンティクスを決定します。x86-64 Unix と Windows の C ABI は異なり、System V レジスタ表をすべての x86-64 に一般化することはできません。

Rust 2024 の外部関数宣言は unsafe extern ブロックを使用します。固定された名前でエクスポートするには #[unsafe(no_mangle)] を使用しますが、正しい extern "C" 関数型も必要です。整数の幅は core::ffi などのターゲットエイリアスを使用して一致させ、すべての C の int/long を同じ Rust の幅に永久にバインドしないでください。

repr(C) 構造体の実際のオフセットは、ターゲット ABI およびフィールド自体のレイアウトの影響を受けます。外側の repr(C) は、内側のデフォルトの Rust 型を再帰的に修正しません。境界を跨ぐ列挙型や bool などの有効値も一致させる必要があり、sizeof が同じだからといって安全であると見なすことはできません。型レイアウト

CString がバッファを所有最終的な解放を担当as_ptr()一時的な読み取り専用借用ポインタ外部による読み取り返却後、ポインタを保持しないinto_raw():所有責任を移動元の CString は自動的に解放されなくなるfrom_raw():契約に従って取得1回だけ取得でき、長さは変更できない境界を跨ぐ前に借用ポインタと所有権の移動を区別する2つの異なるプロトコル;as_ptr の結果を from_raw に渡したり、任意の free に渡したりすることはできない。

C 文字列:終端文字、エンコーディング、所有権関係をそれぞれ確認する

use std::ffi::{CStr, CString};
fn main() {
    let name = CString::new("hello").unwrap();
    let pointer = name.as_ptr();
    // SAFETY: pointer は存続中の CString から借用されており、NUL は同一のアロケーション内にあり、期間中に書き込みはない。
    let borrowed = unsafe { CStr::from_ptr(pointer) };
    assert_eq!(borrowed.to_str().unwrap(), "hello");
    assert!(CString::new(b"a\0b".to_vec()).is_err());
    let non_utf8 = CStr::from_bytes_with_nul(b"\xff\0").unwrap();
    assert!(non_utf8.to_str().is_err());
    assert_eq!(non_utf8.to_bytes(), &[0xff]);
}

CString は自身の NUL 終端レイアウトを保証しますが、内容が UTF-8 であることは保証しません。CStr は合法な非 UTF-8 バイト列を借用できるため、&str に変換するには to_str による検証が必要です。CStr::from_ptr は終端文字を検索しますが、呼び出し側はそれが有効なストレージの範囲内で読み取り可能であることを保証する必要があります。外部から長さが提供される場合は、無制限のスキャンではなく、境界付きバッファインターフェースを優先して使用してください。

CString::new(...).unwrap().as_ptr() と書いてから、次のステートメントでポインタを使い続けるようなことはしないでください。一時 CString はすでに解放されている可能性があります。CString を変数に保存した場合でも、外部ライブラリが関数返却後にこのポインタを引き続き保存する場合は、真の所有者の有効期間を延長し、最後の外部アクセスと調整する必要があります。CString

所有権を移動した後、元の契約に従ってのみ取得できる

use std::ffi::CString;
fn main() {
    let raw = CString::new("owned").unwrap().into_raw();
    // ここで契約に従って raw を外部コードに渡すことができる;例では内容や長さを変更しない。
    // SAFETY: raw は上記の into_raw に由来し、まだ回収されておらず、終端文字の位置は変更されていない。
    let restored = unsafe { CString::from_raw(raw) };
    assert_eq!(restored.to_str().unwrap(), "owned");
}

from_raw は「任意の C 文字列を Rust の所有者に変える」ものではありません。それは、一致する into_raw によって移動された CString のみを再接続します。外部が終端文字の位置を変更した場合、再計算された長さが元のアロケーションと一致しなくなり、回収契約に違反します。外部ライブラリによって割り当てられた文字列は、それが提供する解放関数を使用すべきであり、必要に応じて Rust が所有するデータにコピーする必要があります。

ソース一般的な正しい解放責任直接置き換え不可
Box::into_raw元の型とアロケーション契約に従い、Box::from_raw を1回libc free, 重複する from_raw
CString::into_raw長さなどの要件を満たした後、CString::from_raw を1回任意の C 文字列ポインタ
外部 create 返却ハンドル同一ライブラリの destroy/closeデフォルトの Rust アロケータによる解放
借用のみの as_ptr真の所有者が借用終了後に解放借用ポインタから2つ目の所有者を生成

バインディング生成後の作業

bindgen はヘッダーファイルから宣言を生成し、出力は OUT_DIR に配置し、include! パスと一致させる必要があります。C がポインタを保存しているか、コールバックスレッドが並行しているか、ある返却値を誰が解放するかなどを自動的には発見しません。

完全に動作する C/Rust バインディングとリソースプロトコルについては、C ABI と bindgen を参照してください。2つの記事の役割分担は、ここでは1回のポインタアクセスの有効条件を説明し、後者で外部ライブラリの契約を安全なインターフェースとしてどのようにラップするかを説明することです。

コールバックとアンワインドは具体的な ABI に基づいて判断する

Rust のパニックが通常の非アンワインド対応の C ABI 境界を越えると、プロセスが終了します。外部の例外がアンワインドを許可しない Rust の境界に入ると、未定義動作になる可能性があります。C-unwind は、アンワインドを許可するシナリオに対して異なる規約を提供しており、「C スタックには必ずアンワインド情報がない」という理由で帰納することはできません。

コールバック内部で catch_unwind を使用するのは、アンワインド可能な Rust パニックのみを処理するためであり、abort はキャッチできず、中断されたデータ構造の更新を自動的に回復することもできません。コールバック登録解除後にコールバックを継続できるかどうか、または保留中の呼び出しの終了を待機する必要があるかどうかは、FFI の安全条件に属します。FFI アンワインド

コールバックの最後のアクセスを可視化する

登録関数が callback と user_data を受け取る場合、少なくともコールバックが並行して実行可能かどうか、どのスレッドで実行されるか、登録解除が保留中の呼び出しを待機するかどうか、失敗時に user_data が保持されるかどうかを知る必要があります。外部ライブラリがもはやコールバックアクセスを行わないことを保証してから、Rust は指し示すオブジェクトを解放できます。

一般的な安全なラッパーは、所有者が外部ハンドルを保存し、Drop で登録解除を行い、必要な保留中の呼び出しの調整を行います。登録解除が同期的にコールバックをトリガーする可能性がある場合は、コールバックが必要とするロックを保持しないようにする必要があります。ライブラリの破棄プロトコルが非同期的な完了を要求する場合、同期 Drop のみでは不十分なことがあり、明示的なクローズインターフェースを定義し、閉じずに破棄した場合の動作を定義する必要があります。

これらの制約は外部ライブラリの実際の契約に由来するものであり、extern "C" fn(*mut c_void) のシグネチャから導き出すことはできません。型宣言は呼び出し方法を解決し、安全なラッパーはリソースのタイミングを補完して完成させます。