文字列の受け入れ

説明

FFIから文字列をポインタを通して受け取る場合、次の2つの原則に従う必要があります:

  1. 外部からの文字列を直接コピーするのではなく、「借用」しておいてください。
  2. Cスタイルの文字列から Rust ネイティブの文字列への変換に伴う複雑さとunsafe コードの量を最小限にしてください。

動機形成

C言語で使用される文字列は、Rustで使用される文字列とは振る舞いが異なります。即ち:

  • C言語の文字列はヌル終端であるのに対し、Rustの文字列は長さを保存します
  • C言語の文字列は0以外の任意のバイトを含むことができますが、Rust の文字列は UTF-8 でなければなりません
  • C の文字列は unsafe ポインタ操作を使ってアクセスおよび操作されます。一方、Rust の文字列とのやり取りは安全なメソッドを使用します

Rustの標準ライブラリには、Rustの String&str に相当するC言語の文字列版が用意されており、 CString&CStr と呼びます。これらにより C言語の文字列と Rust の文字列の変換にかかわる複雑さの大部分と unsafe なコードを避けることができます。

また、 &CStr 型は、借用データを扱うことを可能とします。これは Rust とC言語の間での文字列の受け渡しがゼロコストで行えるということです。

コードの例

pub mod unsafe_module {

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

    /// 指定されたレベルでメッセージをログに記録する。
    ///
    /// # Safety
    ///
    /// 呼び出し側は `msg` について以下を保証する:
    ///
    /// - null ポインタではない
    /// - 有効で初期化されたデータを指す
    /// - null バイトで終わるメモリを指す
    /// - この関数呼び出しの間、変更されない
    #[no_mangle]
    pub unsafe extern "C" fn mylib_log(msg: *const libc::c_char, level: libc::c_int) {
        let level: crate::LogLevel = match level { /* ... */ };

        // SAFETY: 呼び出し側は、これが問題ないことを既に保証している
        // (この doc-comment の `# Safety` セクションを参照)。
        let msg_str: &str = match std::ffi::CStr::from_ptr(msg).to_str() {
            Ok(s) => s,
            Err(e) => {
                crate::log_error("FFI string conversion failed");
                return;
            }
        };

        crate::log(msg_str, level);
    }
}

長所

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

  1. unsafe ブロックは可能な限り小さくします。
  2. 「追跡されない」ライフタイムのポインタを「追跡される」共有参照にします

別の方法として、文字列を実際にコピーする方法を考えてみましょう:

pub mod unsafe_module {

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

    pub extern "C" fn mylib_log(msg: *const libc::c_char, level: libc::c_int) {
        // このコードは使わないでください。
        // このコードは醜く、冗長で、微妙なバグを含んでいます。

        let level: crate::LogLevel = match level { /* ... */ };

        let msg_len = unsafe { /* SAFETY: strlenはその通りのものだ、と思った? */
            libc::strlen(msg)
        };

        let mut msg_data = Vec::with_capacity(msg_len + 1);

        let msg_cstr: std::ffi::CString = unsafe {
            // SAFETY: スタックフレーム全体が生きていると
           // 期待される外部ポインタから、所有メモリへのコピー

            std::ptr::copy_nonoverlapping(msg, msg_data.as_mut(), msg_len);

            msg_data.set_len(msg_len + 1);

            std::ffi::CString::from_vec_with_nul(msg_data).unwrap()
        }

        let msg_str: String = unsafe {
            match msg_cstr.into_string() {
                Ok(s) => s,
                Err(e) => {
                    crate::log_error("FFI string conversion failed");
                    return;
                }
            }
        };

        crate::log(&msg_str, level);
    }
}

このコードは2つの点で元のものより劣っています:

  1. unsafe コードがより多くあり、さらに重要なこととして、維持する必要のある不変性がより多くあります。
  2. 追加の計算が必要となったことにより、このバージョンには Rust の undefined behavior を引き起こすバグがあります。

このバグはポインタ演算の単純なミスです: 文字列は msg_len バイトすべてをコピーされましたが、末尾の NUL 終端がコピーされませんでした。

ベクタのサイズは、 ゼロパディングされた文字列 の長さに 設定 されています – 終端に 0 を追加できるよう、 リサイズ されるのではなく。その結果、ベクタの最後のバイトは未初期化のメモリになります。ブロックの最後にて CString を作成する際、このベクタの読み出しが undefined behaviour を引き起こします!

このような問題の多くがそうであるように、これは突き止めることが難しい問題となるでしょう。ときには文字列が UTF-8 でなかったためにパニックが発生したり、またときには文字列の末尾に変な文字が置かれたり、または完全にクラッシュしたりすることでしょう。

短所

なし?

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