文字列の受け入れ
説明
FFI を介してポインター経由で文字列を受け入れる場合、従うべき原則が 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);
}
}
利点
この例は、次の点を保証するように書かれています。
unsafeブロックが可能な限り小さい。- 「追跡されていない」ライフタイムを持つポインターが、「追跡された」共有参照になる
文字列を実際にコピーする別の方法を考えてみましょう。
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 つの点で元のコードより劣っています。
unsafeコードがはるかに多く、さらに重要なことに、それが維持しなければならない不変条件も多い。- 必要となる算術処理が多いため、このバージョンには Rust の
undefined behaviourを引き起こすバグがある。
ここでのバグは、ポインター演算における単純なミスです。文字列はコピーされ、その msg_len バイトすべてがコピーされました。しかし、末尾の NUL 終端文字はコピーされていません。
その後、Vector のサイズは、ゼロパディングされた文字列 の長さに 設定 されました。末尾にゼロを追加できたはずの リサイズ ではありません。その結果、Vector の最後のバイトは未初期化メモリになります。ブロックの最後で CString が作成されるとき、その Vector の読み取りによって undefined behaviour が発生します!
このような多くの問題と同様に、これは追跡が難しい問題になります。文字列が UTF-8 ではないために panic することもあれば、文字列の末尾に奇妙な文字が付くこともあり、完全にクラッシュすることもあります。
欠点
なし?