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
とも仲良くなっておいた方がいい