Fiddle 経由で取得した char ** を Ruby の文字列配列に変換する
備忘録。
C の文字列配列(char**)を返す関数を fiddle 経由で呼び出し、String の Array に変換する方法について。
2つのケースについて考える。今回は取得したC文字列やC配列の解放処理はしなくていいとする。
case1: char **を受け取れるが長さはわからない場合
例えばこういう C 関数:
char **get_strs();
返ってくる配列の長さは直接はわからないが、NULLで終端されているとする。
例えばこんな感じ。
{ "aaa", "bbb", "ccc", NULL }
get_strsを呼び出して Ruby の["aaa", "bbb", "ccc"]に変換するコードは以下のようになる:
require 'fiddle' module Foo extend '/path/to/libfoo.so' extern 'char **get_strs()' end # char ** のアドレスをラップした Fiddle::Pointer で受け取る # C: char **strs_ptr = get_strs(); strs_ptr = Foo.get_strs() # NULL まで辿って result に String を詰めていく result = [] loop do # 配列ポインタが現在指しているアドレスから先頭8バイトをバイト列として取り出す str_addr_bytes = strs_ptr.to_s(Fiddle::SIZEOF_VOIDP) # バイト列を uintptr_t 型整数としてデコード str_addr = str_ptr_bytes.unpack('J').first # str_addr が 0 <=> NULL break if str_addr.zero? # str_addr が指しているアドレスは'\0'終端された文字列なので、 # Fiddle::Pointer#to_s で直接文字列に変換できる result << Fiddle::Pointer.new(str_addr).to_s # 配列ポインタを次のインデックスに進める # C で言うところの `strs_ptr++;` strs_ptr += Fiddle::SIZEOF_VOIDP end p result
ポイント
char**が指している領域に書き込まれているchar*を取り出す時は、Fiddle::Pointer.to_s(Fiddle::SIZEOF_VOIDP)で先頭ワードを切り出し、String#unpackでデコードするFidle::Pointer#+にFiddle::SIZEOF_VOIDPを足すことでptr++;と同じ作用を得るchar*が\0で終端されているならFiddle::Pointer#to_sで文字列化できるFiddle::PointerのNULL判定はアドレスが0かどうかの比較でOK
case2: char **を戻り値で、配列の長さをポインタ経由で受け取る場合
例えばこういう C 関数:
char **get_strs_and_size(int *size);
get_strs_and_sizeは文字列配列を返すと同時に、引数で渡したアドレスにその長さを書き込むとする。
戻り値の仕様はget_strsと同じとすると、get_strs_and_sizeを呼び出して Ruby の["aaa", "bbb", "ccc"]に変換するコードは以下のようになる:
require 'fiddle' module Foo extend '/path/to/libfoo.so' extern 'char **get_strs_and_size(int *)' end # サイズを書き込むためのメモリ領域を用意(String をバイト列バッファとして扱う) # C: int *size_buf = malloc(sizeof(int)); size_buf = "\0" * Fiddle::SIZEOF_INT # char ** のアドレスをラップした Fiddle::Pointer で受け取り、size_buf にサイズを書き込ませる # C: char **strs_ptr = get_strs_and_size(size_buf); strs_ptr = Foo.get_strs_and_size(size_buf) # バイト列を int 型整数としてデコード size = size_buf.unpack('i').first # 配列ポインタが指しているアドレスからポインタサイズ × size バイトをバイト列として取り出す strs_bytes = strs_ptr.to_s(Fiddle::SIZEOF_VOIDP * size) # バイト列にアドレス値が size 個連続しているとみなしてアドレスの配列へデコード addrs = strs_bytes.unpack("J#{size}") # アドレスの配列から文字列の配列へ変換 result = addrs.map { |addr| Fiddle::Pointer.new(addr).to_s } p result
ポイント
- 結果を書き込むためのアドレスを渡す必要がある場合、バッファ用の
Stringを用意し、呼び出し後にString#unpackでデコード - 配列のサイズがわかっている場合、
String#unpackのテンプレートに長さを指定することでまとめてデコードできる(例: サイズが3なら"J3"。"J"はuintptr_tからの変換を意味する)
まとめ
- 多重ポインタでも理屈がわかれば定形作業として処理できる
Integer,String,Fiddle::Pointerを行き来することになるのでちょっと面倒- fiddle と仲良くなりたい時は
String#unpack,Array#packとも仲良くなっておいた方がいい