Ruby FFIを使ったエクステンションの作り方

Ruby FFIについて

ここ最近libclangRuby Bindingのffi-clangに機能を追加していたのですが、その過程でFFI(Foreign function interface)というものを知りました。FFIとはようするに多言語を呼び出すためのインターフェイスのことのようで、RubyにおけるFFIlibffiを利用しているようです。

Rubyでは(C言語で)エクステンションを書けば共有ライブラリを利用するようなライブラリ(Gem)を作ることができますが、FFIを使えばRubyで書くことができますしポータブルな実装になります。本家曰く:

実際FFIを使ってみると、単純なものなら一瞬でバインディングを作ることができるので非常に便利でした。慣れるまでは「このパターンだとどう記述すれば良いのだろう」というので悩んでしまいますが慣れ次第です。日本語で解説しているブログなどはほとんど見つからないですが、使い方は公式のドキュメントが割とよくできているので英語さえ読めればなんとかなります。このエントリでは公式チュートリアルにないようなちょっとだけ複雑なケースを含む、簡単なFFIの書き方を紹介したいと思います。

FFI導入部分

Ruby FFIffiを使うのでまず最初にインストールしておきます。基本的にはバインディングを割り当てるモジュールの中でFFI::Libraryを有効にしてffi_libで共有ライブラリを読み込むことです。例としては以下のような感じになります。

require 'ffi'

module FooLib
  extend FFI::Library
  ffi_lib "libfoo.so"

  ## ここに実装を書く 
end

この後はattach_functionを使って共有ライブラリの各関数を割り当てていくことになります。また、必要に応じて構造体やenumの定義を行います。

サンプル

まず簡単な例として以下のfoo.cfoo.hからlibfooを作成して、そのバインディングを作成する場合を紹介します。

// foo.c
static const char *foobar = "FOOBAR";

double func_a(int a, float b, unsigned long c, char *name) {
  printf("libfoo: func_a: %d %f %lx %s\n", a, b, c, name);
  return 0.1;
}
struct Foo* create_foo() {
  struct Foo *foo = (struct Foo *)malloc(sizeof(struct Foo));
  foo->name = (char *)foobar;
  foo->ptr = (int *)malloc(sizeof(int));
  *foo->ptr = 100;
  printf("libfoo: create_foo: %p\n", foo);
  return foo;
}
void use_foo(struct Foo *foo, int a) {
  printf("libfoo: use_foo: %p %d\n", foo, a);
  return;
}
void free_foo(struct Foo *foo) {
  printf("libfoo: free_foo: %p\n", foo);
  if (foo != NULL) {
    free(foo->ptr);
    free(foo);
  }
  return;
}
// foo.h
struct Foo {
  char *name;
  int *ptr;
};

double func_a(int a, float b, unsigned long c, char *name);
struct Foo* create_foo();
void use_foo(struct Foo *foo, int a);
void free_foo(struct Foo *foo);

func_aはいろんな型の引数を受け取ってdoubleを返すシンプルな関数です。create_foouse_foofree_fooはFoo構造体を操作するAPI群となります。

これをRuby FFIを使って割り当てたプログラムは以下のようなものになります。

module FooLib
  extend FFI::Library
  ffi_lib "libfoo.so"

  class Foo < FFI::Struct
    layout(
      :name, :string,
      :ptr, :pointer,
    )
  end

  attach_function :func_a, [:int, :float, :ulong, :string], :double
  attach_function :create_foo, [], :pointer
  attach_function :use_foo, [:pointer, :int], :void
  attach_function :free_foo, [:pointer], :void
end

attach_functionは共有ライブラリの関数をRubyのメソッドとして割り当てるメソッドです。第一引数に関数名、第二引数に関数の引数の型を配列で指定、第三引数で戻り値の型を指定します。

構造体を定義する場合はFFI::Structを継承してFFI::Struct#layoutでメンバーを定義します。メンバーの定義方法は他にもいくつかあるようですが、自分はいつもlayoutを使って定義しています。layoutは,名前、型の順でメンバーの個数分並べていきます。

以上で関数の割り当てができたので、このモジュールを使ってみます。プログラムは以下の通りです。

p FooLib.func_a(10, 1.0, 1 << 60, "foobar")
puts 

foo_ptr = FooLib.create_foo
p foo_ptr.class
p foo_ptr.address.to_s(16)
puts

foo = FooLib::Foo.new foo_ptr
p foo.class
p foo[:name].class
p foo[:name]
p foo[:ptr].class
p foo[:ptr].read_int
puts

FooLib.use_foo(foo, 10);
FooLib.free_foo(foo);

このプログラムを実行した結果は以下の通りです。

libfoo: func_a: 10 1.000000 1000000000000000 foobar
0.1

libfoo: create_foo: 0x7f7926dcf220
FFI::Pointer
"7f7926dcf220"

FooLib::Foo
String
"FOOBAR"
FFI::Pointer
100

libfoo: use_foo: 0x7f7926dcf220 10
libfoo: free_foo: 0x7f7926dcf220

func_aでは引数と戻り値が正しい値になっていることがわかります。create_fooでは戻り値の型を:pointerとしたため、FFI::Pointerクラスが返ってきています。FFI::StructinitializeFFI::Pointerを渡すことでインスタンスを作成できます。構造体の中身のデータを取得したい場合には一度変換する必要があります。use_foofree_fooの引数にはcreate_fooの戻り値をそのまま渡すこともできますが、FooLib::Fooを渡すこともできます。それぞれ渡されたポインタのアドレスが正しい値になっていることがわかります。

オブジェクトの自動的な解放

先ほどの例ではfree_fooを明示的に呼び出す必要があるため、メモリリークする可能性がありますし、何よりRubyっぽくないです。FFIにはオブジェクトが解放(GC)されるときに自動的にメソッドを呼び出してくれる機能があります。方法はいくつかありますが、自分はManagedStructもしくはAutoPointerを使った実装を使っています。

ManagedStructを使った解放

ManagedStructStructにリリース機能を付けただけで基本的にStructと変わりません。上記のLibFooモジュールの中でStructの代わりにManagedStructを使って以下のように書き換えます。

  class ManagedFoo < FFI::ManagedStruct
    layout(
      :name, :string,
      :ptr, :pointer,
    )
    def self.release(ptr)
      puts "release 0x#{ptr.address.to_s(16)}"
      FooLib::free_foo(ptr)
    end
  end

self.releaseGC時に自動的に呼び出されるメソッドで、クラスメソッドなのが特徴です。これをcreate_fooの戻り値からnewしてインスタンスを作成すると、そのインスタンスGCされるときに自動的にfree_fooが呼ばれるようになります。

スコープから外れてすぐGCされるわけではないので以下のようにループしておけばいずれGCされて確認できると思います。

loop do
  foo = FooLib::ManagedFoo.new FooLib.create_foo
  p foo.class
  FooLib.use_foo foo.pointer, 1000
end

AutoPointerを使った解放

AutoPointerPointerにリリース機能を付けたもので、StructにおけるManagedStructのようなものです。

AutoPointerself.releaseでポインタが渡されるのでそれを使って解放するだけですが、いろいろとメソッドを生やして、オブジェクトを操作する専用クラスっぽくすることもできます。実際には同じことをManagedStructを使って行うこともできます。

class FooPointer < FFI::AutoPointer
  def self.release(ptr)
    puts "release 0x#{ptr.address.to_s(16)}"
    FooLib::free_foo(ptr)
  end

  def initialize
    ptr = FooLib.create_foo
    super ptr
    @foo = FooLib::Foo.new ptr
  end

  def use
    FooLib::use_foo @foo.pointer
  end

  def name
    @foo[:name]
  end

  def ptr
    @foo[:ptr]
  end
end

FooPointerinitializeの中でcreate_fooを呼び出すようにしています。このインスタンスGCされる時にはself.releaseで自動的にfree_fooが呼び出されます。またその他に構造体の中身へのアクセッサを生やしたりしてカプセル化したりもできます。

型の可読性をあげる

attach_functionの引数や戻り値の型で:pointerとした場合に何のポインタかがわからないので、ここだけ見ても仕様がわからないという問題があります。

attach_function :create_foo, [], :pointer
attach_function :use_foo, [:pointer, :int], :void
attach_function :free_foo, [:pointer], :void

今回このポインタの型はstruct Fooなのでそれに対応するFooLib::Fooにひもづけることができます。

attach_function :create_foo, [], Foo.ptr
attach_function :use_foo, [Foo.ptr, :int], :void
attach_function :free_foo, [Foo.ptr], :void

:pointerの部分をFoo.ptrと置き換えることでFooのポインタとして受け渡しを行うという意味になります。

この状態でcreate_fooを実行すると戻り値の型はFFI::Pointerではなく、最初からFooLib::Fooで返ってことになります。

foo = FooLib.create_foo
p foo.class  # => FooLib::Foo

また、ポインタではなく値で受け渡しをしたい場合は#by_valueを使うことで値渡しを指定できます。

例えば以下のようにstruct Fooを値で受け取るuse_foo_value関数の場合は

void use_foo_value(struct Foo foo);

Foo.by_valueで値渡しを指定することができます。

attach_function :use_foo_value, [Foo.by_value, :int], :void

使用するときはFooLib::Fooインスタンスをそのまま引数に渡すだけで良いです。

foo = FooLib.create_foo
FooLib.use_foo_value foo

ポインタ渡しと配列

関数の使用として引数でポインタを渡して関数内で値を詰めて返すというものや、配列とそのサイズを渡して関数無いで指定サイズ分詰めて返すもの、そして配列のポインタを返すものがあると思います。

Cのコードとしては以下のような3つのパターンです。

void foo_int(int *a);
void foo_int_array(int *a, int num);
void foo_allocate_array(int **a, int *num);

void foo_int(int *a) {
  *a = 10;
}

void foo_int_array(int *a, int num) {
  for (int i = 0; i < num; i++)
    a[i] = 10;
}

void foo_allocate_array(int **a, int *num) {
  int n = 3;
  *a = (int *)malloc(sizeof(int) * n);
  for (int i = 0; i < n; i++)
    (*a)[i] = 10 + i;
  *num = n;
  printf("libfoo: foo_allocate_array: %p %d\n", *a, n);
}

これらの割り当ては以下のようになります。ポインタのポインタも:pointerとして割り当てます。

module FooLib
  extend FFI::Library
  ffi_lib "libfoo.so"

  attach_function :foo_int, [:pointer], :void
  attach_function :foo_int_array, [:pointer, :int], :void
  attach_function :foo_allocate_array, [:pointer, :pointer], :void
end

このモジュールを利用したプログラムは以下のようになります。

a1 = FFI::MemoryPointer.new :int             # int *a = malloc(sizeof(int))
FooLib.foo_int(a1)                           #
r1 = a1.read_int                             # ((int *)a)[0]
p r1                                         # 10

a2 = FFI::MemoryPointer.new(:int, 5)         # int *a = malloc(sizeof(int) * 5)
FooLib.foo_int_array(a2, 5)                  #
r2 = 5.times.map { |i|                       #
  a2.get_int(i * 4)                          # ((int *)a)[i]
}                                            #
p r2                                         # => [10, 11, 12, 13, 14]

arr_ptr = FFI::MemoryPointer.new :pointer    # int **a = malloc(sizeof(int *))
num_ptr = FFI::MemoryPointer.new :int        # int *num = malloc(sizeof(int))
FooLib.foo_allocate_array(arr_ptr, num_ptr)  # => libfoo: foo_allocate_array: 0x7f0d2052bd60 3
arr = arr_ptr.read_pointer                   # int *a = *(int **)a
num = num_ptr.read_int                       # int num = *(int *)num
p arr.address.to_s(16)                       # => "7f0d2052bd60"
p num                                        # => 3
r3 = num.times.map { |i|                     #
  arr.get_int(i * 4)                         # ((int *)a)[i]
}                                            #
p r3                                         # => [10, 11, 12]

FFI::MemoryPointercaallocのようなもので、第一引数に型、第二引数に個数を指定します。割り当てたメモリはインスタンスGCされる時に自動的に解放されます。 read_intのようにread_hogehogeでキャストしてからポインタを参照するといったような動作になります。 最初のパターンではread_intするだけで値がとれます。 二個目のパターンではint型の配列を作ってから渡します。get_intread_intと似ていますが、引数で指定したオフセットからポインタを参照することになります。 三個目のパターンではint配列のポインタとintのポインタを渡しますが、int配列のポインタの方はただのポインタのポインタとして渡します。arr_ptr.read_pointerで1段ポインタを参照し、arr_get_intで実際の値を取得します。

三個目のパターンでは共有ライブラリ側でメモリを割り当てているので、解放関数(例えばfoo_deallocate_array)を使う必要があるケースがあります。自分はこの場合には、foo_allocate_arrayの戻り値を管理するクラスをFFI::AutoPointerで作成してreleasefoo_deacllocate_arrayを呼び出すようにしています。このクラスでEnumerableArrayを継承すれば見た目上はArrayと同様の動作をして自動的に解放もしてくれるクラスになります。

class FooArray < FFI::AutoPointer
  class FooPointer < FFI::Pointer
    attr_reader :ptr
    attr_reader :num
    def initialize(ptr, num)
      super ptr
      @ptr = ptr
      @num = num
    end
  end

  include Enumerable

  def initialize(ptr, num)
    super FooPointer.new(ptr, num)
    @arr = ptr
    @num = num
  end

  def self.release(ptr)
    FooLib::foo_deallocate_array(ptr.ptr, ptr.num)
  end

  def each(&block)
    @num.times { |i|
      block.call(@arr.get_int(i * 4))
    }    
  end
end

arr_ptr = FFI::MemoryPointer.new :pointer    # int **a
num_ptr = FFI::MemoryPointer.new :int        # int *num
FooLib.foo_allocate_array(arr_ptr, num_ptr)
arr = arr_ptr.read_pointer                   # int *a
num = num_ptr.read_int                       # int num

foo = FooArray.new(arr, num)                 # foo_allocate_arrayの戻り値を管理するクラス
p foo.class                                  # => FooArray
p foo.map{|e| e + 100}                       # => [110, 111, 112]

exit                                         # => libfoo: foo_deallocate_array

最後に

Ruby FFIの紹介と実装する上での簡単なテクニック集的なものを紹介しました。実際にはまだEnum周りでいろいろとあるのですが長くなってきたのでここで止めます。FFIには更にGeneratorだとかConverterだとかが存在して、これらが何者なのかイマイチわかっていないですが、使いこなせるともっと簡単にかけるのかもしれません。

参考リンク