Ruby FFIを使ったエクステンションの作り方
Ruby FFIについて
ここ最近libclangのRuby Bindingのffi-clangに機能を追加していたのですが、その過程でFFI(Foreign function interface)というものを知りました。FFIとはようするに多言語を呼び出すためのインターフェイスのことのようで、RubyにおけるFFIはlibffiを利用しているようです。
Rubyでは(C言語で)エクステンションを書けば共有ライブラリを利用するようなライブラリ(Gem)を作ることができますが、FFIを使えばRubyで書くことができますしポータブルな実装になります。本家曰く:
- コンパイルが必要無い
- マルチプラットフォーム(変更無しでJRubyやRubiniousなどで動作する)
- 記述がRubyなので読み書きしやすい
- Rubyの実装の変更に影響されない
実際FFIを使ってみると、単純なものなら一瞬でバインディングを作ることができるので非常に便利でした。慣れるまでは「このパターンだとどう記述すれば良いのだろう」というので悩んでしまいますが慣れ次第です。日本語で解説しているブログなどはほとんど見つからないですが、使い方は公式のドキュメントが割とよくできているので英語さえ読めればなんとかなります。このエントリでは公式チュートリアルにないようなちょっとだけ複雑なケースを含む、簡単なFFIの書き方を紹介したいと思います。
FFI導入部分
Ruby FFIはffiを使うのでまず最初にインストールしておきます。基本的にはバインディングを割り当てるモジュールの中でFFI::Library
を有効にしてffi_lib
で共有ライブラリを読み込むことです。例としては以下のような感じになります。
require 'ffi' module FooLib extend FFI::Library ffi_lib "libfoo.so" ## ここに実装を書く end
この後はattach_function
を使って共有ライブラリの各関数を割り当てていくことになります。また、必要に応じて構造体やenumの定義を行います。
サンプル
まず簡単な例として以下のfoo.c
とfoo.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_foo
とuse_foo
とfree_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::Struct
はinitialize
でFFI::Pointer
を渡すことでインスタンスを作成できます。構造体の中身のデータを取得したい場合には一度変換する必要があります。use_foo
とfree_foo
の引数にはcreate_foo
の戻り値をそのまま渡すこともできますが、FooLib::Foo
を渡すこともできます。それぞれ渡されたポインタのアドレスが正しい値になっていることがわかります。
オブジェクトの自動的な解放
先ほどの例ではfree_foo
を明示的に呼び出す必要があるため、メモリリークする可能性がありますし、何よりRubyっぽくないです。FFIにはオブジェクトが解放(GC)されるときに自動的にメソッドを呼び出してくれる機能があります。方法はいくつかありますが、自分はManagedStruct
もしくはAutoPointer
を使った実装を使っています。
ManagedStructを使った解放
ManagedStruct
はStruct
にリリース機能を付けただけで基本的に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.release
はGC時に自動的に呼び出されるメソッドで、クラスメソッドなのが特徴です。これを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を使った解放
AutoPointer
はPointer
にリリース機能を付けたもので、Struct
におけるManagedStruct
のようなものです。
AutoPointer
もself.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
FooPointer
はinitialize
の中で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::MemoryPointer
はcaalloc
のようなもので、第一引数に型、第二引数に個数を指定します。割り当てたメモリはインスタンスがGCされる時に自動的に解放されます。
read_int
のようにread_hoge
はhogeでキャストしてからポインタを参照するといったような動作になります。
最初のパターンではread_int
するだけで値がとれます。
二個目のパターンではint型の配列を作ってから渡します。get_int
はread_int
と似ていますが、引数で指定したオフセットからポインタを参照することになります。
三個目のパターンではint配列のポインタとintのポインタを渡しますが、int配列のポインタの方はただのポインタのポインタとして渡します。arr_ptr.read_pointer
で1段ポインタを参照し、arr_get_int
で実際の値を取得します。
三個目のパターンでは共有ライブラリ側でメモリを割り当てているので、解放関数(例えばfoo_deallocate_array
)を使う必要があるケースがあります。自分はこの場合には、foo_allocate_array
の戻り値を管理するクラスをFFI::AutoPointer
で作成してrelease
でfoo_deacllocate_array
を呼び出すようにしています。このクラスでEnumerable
かArray
を継承すれば見た目上は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だとかが存在して、これらが何者なのかイマイチわかっていないですが、使いこなせるともっと簡単にかけるのかもしれません。