文字列の受け渡し

説明

FFI関数に文字列を渡す場合、次の4つの原則に従う必要があります:

  1. 所有している文字列のライフタイムを可能な限り長くします。
  2. 変換の間の unsafe コードを最小限に抑えます。
  3. Cのコードが文字列データを変更し得る場合は、CString の代わりに Vec を使用します。
  4. 外部関数APIがそれを要求しない限り、文字列の所有権は呼び出し側に移してはいけません。

動機形成

Rust は CString 型と CStr 型により C スタイルの文字列をビルトインでサポートしています。しかしながら、Rust 関数から外部関数を呼び出す際に受け渡す文字列に関しては、さまざまなアプローチがありえます。

ベストプラクティスは単純です: unsafe コードを最小限にするように CString を使うことです。ただし、次いでの注意点があり、それは オブジェクトは十分に長く生存する必要がある ことです。これはライフタイムを最大化すべきという意味です。加えてドキュメントでは、変更後の CString を 「ラウンドトリップ」 することは未定義動作であると説明されており、このようなケースでは追加の対応が必要です。

コードの例

pub mod unsafe_module {

    // モジュールの他のコンテンツ

    extern "C" {
        fn seterr(message: *const libc::c_char);
        fn geterr(buffer: *mut libc::c_char, size: libc::c_int) -> libc::c_int;
    }

    fn report_error_to_ffi<S: Into<String>>(err: S) -> Result<(), std::ffi::NulError> {
        let c_err = std::ffi::CString::new(err.into())?;

        unsafe {
            // SAFETY: ポインタがconstであるとドキュメントに記述されているFFIの呼び出し、
            // すなわち変更されることはない
            seterr(c_err.as_ptr());
        }

        Ok(())
        // c_err のライフタイムはここまで続く
    }

    fn get_error_from_ffi() -> Result<String, std::ffi::IntoStringError> {
        let mut buffer = vec![0u8; 1024];
        unsafe {
            // SAFETY: calling an FFI whose documentation implies
            // that the input need only live as long as the call
            let written: usize = geterr(buffer.as_mut_ptr(), 1023).into();

            buffer.truncate(written + 1);
        }

        std::ffi::CString::new(buffer).unwrap().into_string()
    }
}

長所

この例は、以下のことを担保するように記述されています:

  1. unsafe ブロックは可能な限り小さくします。
  2. CStringは十分に長生きします。
  3. 型キャストによるエラーは、可能な限り常に伝播されます。

よくある間違い(ドキュメントにも載っているほど)は、最初のブロックで変数を使わないことです:

pub mod unsafe_module {

    // モジュールの他のコンテンツ

    fn report_error<S: Into<String>>(err: S) -> Result<(), std::ffi::NulError> {
        unsafe {
            // SAFETY: おっと、これはダングリングポインタを含んでいる!
            seterr(std::ffi::CString::new(err.into())?.as_ptr());
        }
        Ok(())
    }
}

このコードではダングリングポインタが発生します。参照の作成とは異なり、ポインタの作成によって CString の寿命が延長されることはないからです。

もうひとつよく挙げられる問題は、1kのゼロ埋めベクトルの初期化が「遅い」ということです。しかし、Rustの最近のバージョンでは、この特定のマクロはなんと zmalloc の呼び出しに最適化されています。これは、オペレーティングシステムのゼロ埋めしたメモリを返す機能 (これはかなり高速です) と同じ速さであることを意味します。

短所

なし?

Last change: 2024-07-09, commit: 317c88e