Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

文字列の受け入れ

説明

FFI を介してポインター経由で文字列を受け入れる場合、従うべき原則が 2 つあります。

  1. 外部の文字列を直接コピーするのではなく、「借用」された状態に保つ。
  2. C 形式の文字列からネイティブな Rust 文字列へ変換する際に伴う複雑さと unsafe コードの量を最小限に抑える。

動機

C で使用される文字列は、Rust で使用される文字列とは異なる振る舞いをします。具体的には次のとおりです。

  • C 文字列は null 終端である一方、Rust の文字列は長さを保持する
  • C 文字列には任意の非ゼロバイトを含められる一方、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文字列の変換に失敗しました");
                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文字列の変換に失敗しました");
                    return;
                }
            }
        };

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

このコードは、次の 2 つの点で元のコードより劣っています。

  1. unsafe コードがはるかに多く、さらに重要なことに、それが維持しなければならない不変条件も多い。
  2. 必要となる算術処理が多いため、このバージョンには Rust の undefined behaviour を引き起こすバグがある。

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

その後、Vector のサイズは、ゼロパディングされた文字列 の長さに 設定 されました。末尾にゼロを追加できたはずの リサイズ ではありません。その結果、Vector の最後のバイトは未初期化メモリになります。ブロックの最後で CString が作成されるとき、その Vector の読み取りによって undefined behaviour が発生します!

このような多くの問題と同様に、これは追跡が難しい問題になります。文字列が UTF-8 ではないために panic することもあれば、文字列の末尾に奇妙な文字が付くこともあり、完全にクラッシュすることもあります。

欠点

なし?