Fiddle 経由で取得した char ** を Ruby の文字列配列に変換する

C の文字列配列(char**)を返す関数を fiddle 経由で呼び出し、StringArray に変換する方法について。

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::PointerNULL判定はアドレスが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"

まとめ

  • 多重ポインタでも理屈がわかれば定形作業として処理できる
  • Integer, String, Fiddle::Pointerを行き来することになるのは面倒
  • fiddle と仲良くなりたい時はString#unpack, Array#packとも仲良くなっておいた方がいい