文字列の受け渡し
説明
FFI関数に文字列を渡す場合、次の4つの原則に従う必要があります:
- 所有している文字列のライフタイムを可能な限り長くします。
- 変換の間の
unsafe
コードを最小限に抑えます。 - Cのコードが文字列データを変更し得る場合は、
CString
の代わりにVec
を使用します。 - 外部関数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()
}
}
長所
この例は、以下のことを担保するように記述されています:
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
の寿命が延長されることはないからです。
もうひとつよく挙げられる問題は、1kのゼロ埋めベクトルの初期化が「遅い」ということです。しかし、Rustの最近のバージョンでは、この特定のマクロはなんと zmalloc
の呼び出しに最適化されています。これは、オペレーティングシステムのゼロ埋めしたメモリを返す機能 (これはかなり高速です) と同じ速さであることを意味します。
短所
なし?