文字列を渡す
説明
FFI 関数に文字列を渡すときは、従うべき 4 つの原則があります。
- 所有文字列のライフタイムを可能な限り長くする。
- 変換中の
unsafeコードを最小限にする。 - C コードが文字列データを変更できる場合は、
CStringではなくVecを使う。 - 外部関数 API が要求しない限り、文字列の所有権を呼び出し先へ移転すべきではない。
動機
Rust は、CString 型と CStr 型により C スタイル文字列を組み込みでサポートしています。しかし、Rust 関数から外部関数呼び出しへ送られる文字列については、いくつか異なるアプローチを取ることができます。
ベストプラクティスは単純です。unsafe コードを最小限にするように CString を使うことです。ただし、二次的な注意点として、オブジェクトは十分に長く生存しなければならない、つまりライフタイムを最大化すべきである、という点があります。さらに、ドキュメントでは、変更後に CString を「往復変換」することは UB であると説明されているため、その場合には追加の作業が必要です。
コード例
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: ドキュメント上、入力は呼び出し中だけ生存していればよいことが
// 示唆されている FFI を呼び出している
let written: usize = geterr(buffer.as_mut_ptr(), 1023).into();
buffer.truncate(written + 1);
}
std::ffi::CString::new(buffer).unwrap().into_string()
}
}
利点
この例は、次のことを保証するように書かれています。
unsafeブロックが可能な限り小さい。CStringが十分に長く生存する。- 型キャストに伴うエラーは、可能な場合は常に伝播される。
よくある誤り(あまりに一般的なのでドキュメントにも載っています)は、最初のブロックで変数を使わないことです。
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 のライフタイムは延長されないためです。
もう 1 つ頻繁に取り上げられる問題は、ゼロで埋めた 1k のベクターの初期化は「遅い」というものです。しかし、最近のバージョンの Rust では実際にはその特定のマクロが zmalloc の呼び出しに最適化されるため、オペレーティングシステムがゼロ化済みメモリを返す能力と同じくらい高速です(これはかなり高速です)。
欠点
なし?