mruby の String から C 言語の文字列を取り出す正しい方法

  • 21
    Like
  • 2
    Comment
More than 1 year has passed since last update.

mruby には、Ruby 文字列 (String クラスのインスタンス) から C 言語の文字列(char *) を取り出す手段が三つあります。

  1. RSTRING_PTR
  2. mrb_string_value_cstr
  3. mrb_str_to_cstr

残念ながらどれも癖があって使い方を間違えるとあっさりバグの原因となります。それぞれの特徴をかんたんに記すと、

  • RSTRING_PTR は効率は良いが危険。
  • mrb_string_value_cstr はそこそこ便利だが注意が必要。
  • mrb_str_to_cstr は効率は劣るが使いやすい。

といったところです。以下、順に解説します。

RSTRING_PTR

RSTRING_PTR は、文字列オブジェクトが内部で保持している、文字列を表すバイト列を直接取り出すマクロです。このマクロを関数と見たてたときの呼び出し規約を書くと以下のようになります。

char *RSTRING_PTR(mrb_value str);

mruby では String オブジェクトは struct RString という構造体として実装されています。RSTRING_PTR マクロは、struct RString が直接または間接的に保持しているバイト列(char *)へのポインタを取り出すものです。そのため、利用者には struct RString の実装に熟知していることが求められます。ありがちな誤りとして、以下の二つがあります:

  1. RSTRING_PTR が適切に NUL 終端されていないケースを無視している。
  2. struct RString が示すバイト列がメモリ空間上で移動したのに、古いポインタ値を使い続ける。

RSTRING_PTR が示すバイト列は、多くの場合には適切に NUL 終端されています。しかし、Ruby の文字列の実装ではバイト列を他の文字列オブジェクトと共有する場合があります。たとえば、

s1 = "a" * 32
s2 = s1[0, 5]

という Ruby のコードは s1 と s2 という二つの Ruby 文字列を生成しますが、これらのオブジェクトに RSTRING_PTR を適用するとどちらも同じポインタが返ってきます。

RSTRING_PTR(s1) == RSTRING_PTR(s2) == "aaaaaaaaaaaaaaaaaaaaaa"

ここで RSTRING_PTR(s2) で得た C 言語文字列は、s2 の長さ(= 5)で NUL 終端されていません。これが一つ目の罠です。

そしてこのポインタ値は文字列に対して何らかの操作を行うと変化する可能性があります。たとえば、

s1 = "a" * 32
s2 = s1[0, 5]
s2[0] = "b"

このように文字列の一部を書き換えると、

RSTRING_PTR(s1) == "aaaaaaaaaaaaaaaaaaaaaa"
RSTRING_PTR(s2) == "baaaaaaaaaaaaaaaaaaaaa"

と共有状態が解除されます (注: s2 の長さが変わっていないのは書き間違いではありません!)。Ruby の世界での変更だけではなく、C 言語 API を使って文字列を変更しても、たとえば文字列オブジェクトに C 言語文字列を追加する mrb_str_cat を使っても、共有状態が解除されます。共有状態が解除された後でもう一度 RSTRING_PTR を使えば正しいポインタ値を得ることができますが、何をした時に共有状態が解除されポインタ値が動くかが自明ではないので、しばしば忘れられます。これが二つ目の罠です。

最後に RSTRING_PTR の良い点を挙げると、NUL 文字が含まれた Ruby 文字列を適切に取り扱えるのはこの API だけです。mrb_string_value_cstr と mrb_str_to_cstr はどちらも NUL 終端された C 言語文字列を返します。すなわち、API の仕様上 NUL 文字を含むようなバイト列は取り扱えません。したがって、NUL 文字を含むようなバイナリ文字列を扱う場合は RSTRING_PTR 一択となります。

mrb_string_value_cstr

mrb_string_value_cstr は癖の強い API です。呼び出し規約はこうなっています:

const char *mrb_string_value_cstr(mrb_state *mrb, mrb_value *ptr)

動作としては、*ptr が示す Ruby 文字列の、C 言語文字列(バイト列)を示すポインタを返します。返ってきたポインタは Ruby 文字列の長さ(バイト数)で NUL 終端されていることが保証されます。NUL 終端が保証されている分 RSTRING_PTR より使いやすくなっていますが、それでもいくつか罠があります。

まず引数の ptr がポインタになっているところに違和感を覚えるでしょう。これは *ptr が String オブジェクトではない時に #to_str#to_s メソッドを呼び出して String オブジェクトを生成し、*ptr に新しく生成した String オブジェクトを入れて返すという動作をするためです。C 言語文字列を取り出したいだけなのにオブジェクトがすげ変わる可能性があるというのが、まず第一の注意点です。

次に、struct RString が保持しているバイト列へのポインタを返すという点は RSTRING_PTR と同じですので、ふとした拍子に共有状態が外れてポインタ値が変化するという動作も同じです。これが二つ目の注意点です。


最後に、#freeze された文字列に対して mrb_string_value_cstr を呼ぶと、例外が発生する可能性があります。mrb_string_value_cstr は NUL 終端された C 言語文字列を作るために適宜共有状態の解除を行いますが、この処理は文字列への変更処理に該当するため、#freeze されたオブジェクトに対しては適用できず、例外が上がってしまいます。 (追記) mruby 1.2.0 リリース時点ではこのような挙動をしていましたが、master では(コメントいただいた通り :smiley: )修正されています。

mrb_str_to_cstr

mrb_str_to_cstr は文字列のバイト列のコピーを作って、それのポインタを返します。Ruby 文字列を引数に取れる strdup(3) といったところでしょうか。呼び出し規約は以下の通りです:

char *mrb_str_to_cstr(mrb_state *mrb, mrb_value str);

先述の二つの API とは違いコピーを作るため、効率は落ちますが安全に使いやすいバイト列が得られます。得た C 言語文字列は、適切に NUL 終端されていることが保証されます。また、コピーなので書き換えても Ruby 文字列には影響しません。const char * ではなく char * を要求するようなライブラリ関数にもそのまま渡すことができます。

そして、返ってきたポインタは free(3) する必要はなく GC で自動的に回収されます。そのカラクリは、arena 以外のどこからも参照されていない文字列オブジェクトです。mrb_str_to_cstr は、内部で新しい文字列オブジェクトを生成します。内容は引数の str と同じものです。しかし内部のバイト列は(str と共有するのではなく)別途新たに確保されています。したがって、str に変更があっても影響を受けませんし、逆に新しく作った文字列オブジェクトを変更しても str には影響しません。そして、新しい文字列オブジェクトは arena に置かれているため arena のインデックスが戻るまでは GC に回収されません。通常は Ruby VM の実行ループに戻るまでは arena のインデックスが戻ることはありませんから、C 言語のスコープ内では安全に使えます。

結局どれを使えばいいのか?

おすすめは、

  1. 基本的に mrb_str_to_cstr を使う。
  2. メモリアロケーションのコストを避けたいビジーループの中や、NUL 文字を含む文字列を扱う場合は、RSTRING_PTRRSTRING_LEN を使ってバイナリバイト列として処理する。

です。mrb_string_value_cstr は癖が強く使いどころが難しいように思います。