10

この記事は最終更新日から5年以上が経過しています。

投稿日

Rust CString::as_ptrの正しい使い方

はじめに

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ライブラリを久しぶりにメンテナンスしようと思ったら
テストが全滅したので気づきました。

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
ログインすると使える機能について

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
新規登録
すでにアカウントを持っている方はログイン
10