はじめに
RustのCStringはCと同様にヌル終端された文字列型で、主にffiによるCの関数呼び出し時に使います。
普通Cで文字列を引数に取る関数はポインタで渡すので以下のように書けますが、これは間違いです。
//文字列hogeのポインタを取得
let ptr = CString::new("hoge").unwrap().as_ptr();
//Cの関数呼び出し
unsafe {
c_func(ptr);
}
このptr
は"hoge"
の先頭アドレスを指すように見えますが、実際には大抵の場合、空文字列の先頭アドレスを指します。
ただしそうなる保証は(たぶん)ありません。
以下では、その理由と正しい書き方について説明します。
間違いやもっといい書き方などあればご指摘ください。
何が起きているのか
分かってしまえば単純な話なのですが、new
したCStringそのもののbind先がない(bindしてるのはあくまでポインタ)のでCStringのlifetimeは文の最後で尽きています。
Rustのコード内でlifetimeが尽きたものを参照しようとすると当然怒られますが、
この場合参照するのはCの関数内なのでコンパイラのチェックは通ってしまうというわけです。
「空文字列になる」というのはlifetimeが尽きてメモリ解放時に呼び出されるCStringのdrop
実装によります。Rust 1.13.0あたりからCStringのdrop
で先頭に0を書き込むよう変更されました。
ヌル終端文字列の先頭が0なので空文字列になります。
もちろんdrop
後のメモリ領域が即座に再利用される可能性はあるので、空文字列になる保証はないはずです。
このような挙動になった経緯を最後にまとめておきましたので興味のあるかたはご覧下さい。
正しい使い方
as_ptr
の場合
CStringをbindするのが一番無難です。
let c_str = CString::new("hoge").unwrap();
unsafe {
c_func(c_str.as_ptr());
}
また、関数呼び出しまで一気にやってしまうという手もあります。
unsafe {
c_func(CString::new("hoge").unwrap().as_ptr());
}
CStringがdrop
されるのは文の最後なので、一気に書けば関数呼び出し完了後にdrop
されるようです。
ただしリファクタリングと思って外に出した途端に動かなくなるのであまり良くないと思います。
この挙動が仕様として保証されているのかどうかもよくわかりません。
また文字列ポインタの配列へのポインタを渡す場合(つまりc_func(char * const *)
のような場合)はこのような感じです。
let mut ptrs = Vec::new();
let mut strs = Vec::new();
for _ in 0..10 {
let c_str = CString::new("hoge").unwrap();
ptrs.push(c_str.as_ptr());
strs.push(c_str);
}
ptrs.push(ptr::null());
unsafe {
c_func(ptrs.as_ptr());
}
普通に書くと実際に渡したいVec<*const c_char>
だけを作ってしまいますが、
それとは別にlifetimeを維持するためのVec<CString>
が必要です。
into_raw
/from_raw
の場合
ここまではas_ptr
の例を示しましたがinto_raw
/from_raw
を使うことも出来ます。
into_raw
/from_raw
の扱うポインタは*const
ではなく*mut
なのでas_ptr
から置き換える場合は適宜変換が必要です。
//文字列hogeのポインタを取得しつつ所有権を放棄
//なのでdropはされない
let ptr: *const c_char = CString::new("hoge").unwrap().into_raw();
unsafe {
c_func(ptr);
//所有権を取り戻す。文末でlifetimeが切れてdrop
CString::from_raw(ptr as *mut c_char);
}
ポインタ配列版はCString本体を保持する必要がなくなります。
let mut ptrs = Vec::new();
for _ in 0..10 {
let c_str = CString::new("hoge").unwrap();
ptrs.push(c_str.into_raw() as *const c_char);
}
ptrs.push(ptr::null());
unsafe {
c_func(ptrs.as_ptr());
for p in ptrs {
if !p.is_null() {
CString::from_raw(p as *mut c_char);
}
}
}
into_raw
で所有権を放棄しているのでもしfrom_raw
を忘れるとそのままメモリリークします。
過去の経緯
だいたいの経緯は以下のRFCからたどれます。
元々この問題はGithubに間違ったコードが結構あるけどどうしよう?というところから来ているようです。
最初の提案は、そもそも間違いやすいas_ptr
が良くないので廃止してwith_ptr
に置き換えよう、というものでした。
//with_strは却下されたので実際には使えません。
CString::new("hoge").unwrap().with_ptr(|ptr| {
c_func(ptr);
});
確かにwith_ptr
はlifetimeがはっきりするのでこの問題は起きないのですが、
複数使う場合ネストが深くなる、ポインタの配列が再帰を駆使しないと書けない、ということで却下されました。
そうはいっても間違いやすいので「ドキュメントに注意書きを書く」という提案があり、こちらは採用されましたが当然既存のコードには効果がありません。
という訳で最終的に提案されたのがdrop
時のゼロ書き込みでした。
これなら保証はないとはいえ、関数呼び出し自体は予定と違う結果になって、間違いに気づくことが出来ます。
また1バイトの書き込みなのでパフォーマンスへの影響も最小限です。
実際私の場合も過去に書いたffiライブラリを久しぶりにメンテナンスしようと思ったら
テストが全滅したので気づきました。