逆引きRuby-FFI
Ruby-FFI
RubiniusやRuby-FFIとも互換性のあるJRuby-FFIについてのTipsです。FFIはRubyスクリプトから外部の動的ライブラリ(DLLやso)を呼び出す仕組みです。動的ライブラリを呼び出すためのRuby拡張ライブラリをC言語で書かなくとも、Rubyスクリプトから直接動的ライブラリを利用できるのがメリットです。
- Rubinius … http://rubini.us/
- Ruby FFI … http://kenai.com/projects/ruby-ffi
それぞれの環境でも共通だと思われますが、JRuby trunk上のJRuby-FFIでしか検証していません。JRubyでは内部的にJNA(Java Native Access)を利用してRuby-FFI機能を実現しています。
Ruby-FFIはRubyに対して以下の機能を提供します。
- 外部ライブラリのロード
- 外部ライブラリ内のメソッド呼び出し
- 外部ライブラリ内の変数の読み書き
- Rubyのブロックを外部ライブラリのコールバック関数として利用
- 引数や返り値の型変換
- C構造体をRubyで定義する機能
- ポインターやメモリをRubyで操作する機能
- C言語ヘッダファイルからスタブファイルの生成(実験的)
- 標準入出力を扱う機能
およそ外部ライブラリを利用するための機能はひととおり用意されています。
参考情報
- Headius: FFI for Ruby Now Available … http://blog.headius.com/2008/10/ffi-for-ruby-now-available.html
- Ruby FFI example … http://kenai.com/projects/ruby-ffi/pages/Examples
LibCの関数を呼び出したい
FFIは標準でlibcライブラリのラッパーとして振る舞います。FFI::Libraryモジュールを継承したモジュールを定義することでlibcが提供する関数を呼び出せるようになります。
module POSIX extend FFI::Library end
これは次のようにライブラリ名を明示した書き方と同じです。
module POSIX extend FFI::Library ffi_lib 'c' end
Unix系の環境ではlibc.XX.soにバインドします。Windows環境ではVisual C++のランタイムライブラリであるmsvcrt.dllをバインドします。
任意の拡張ライブラリを呼び出したい
ffi_libクラスメソッドでバインドする拡張ライブラリを指定できます。
module LIBFIB extend FFI::Library ffi_lib 'fib' end
ffi_libにはバインドする拡張ライブラリを複数指定することができます。
ffi_lib 'fib', 'c'
拡張ライブラリはフルパスで指定します。__FILE__を使って指定すると便利です。
ffi_lib File.dirname(__FILE__) + "/../../ext/extlib"
拡張子省略を省略した場合は環境によって適切な拡張子が補われます。Unix系では.so、Windowsでは.dll、MacOSXでは.dylibが使われます。
次のように複数のパスから探索するような使い方も便利です。File.exist?で存在チェックする場合は拡張子省略できませんので注意します。
paths = Array(ENV['EXT_LIB'] || %w{
/opt/local/lib/libexample.dylib
/usr/local/lib/libexample.dylib
/usr/local/lib/libexample.so
})
ffi_lib(paths.find { |path| File.exist?(path) })
当初はset_ffi_libというクラスメソッドが使われていましたが、2009/01時点ではffi_libが標準となっているので注意してください。
関数を呼び出したい
次のようなC言語の動的ライブラリを用意したとします。
fib.c:
DLLEXPORT static int hello(const char *name) {
printf("Hello, %s!", name)
return 0;
}
このfib関数を呼び出すには次のようにattach_functionを使います。
attach_function :hello, [:string], :int
attach_functionは引数を3〜4個とり拡張ライブラリのメソッドシグニチャを指定できます。詳しい使い方は次のパターンになります。
attach_fuction :メソッド名, [引数の型の配列], :返り値の型 attach_fuction :Rubyメソッド名, :拡張ライブラリの関数名, [引数の配列], :返り値の型
呼び出すためのプログラム全体と、利用についての例です。
module Hello
extend FFI::Library
ffi_lib 'libhello'
attach_function :hello, [:string], :int
end
Hello.hello("tom")
変数を参照したい
動的ライブラリがexportしている変数を参照するにはattach_variableを使います。
attach_variable :greeter_name, :int
attach_variableは2〜3個の引数をとり以下のパターンで変数のシグニチャを指定します。
attach_variable :変数名, :変数の型 attach_variable :Ruby変数名, :拡張ライブラリの変数名, :返り値の型
次の例は、変数を参照するプログラムです。
module Hello extend FFI::Library ffi_lib 'libhello' attach_variable :greeter_name, :string end puts Hello.greeter_name
使える型の種類を知りたい
FFIではCで使える型情報をそのまま扱えます。attach_functionやattach_variableで適切な型情報を指定する必要があります。
FFIで使える基本的な型は以下の7つです。これらは動作するプラットフォームによって適切なサイズのデータ長が適用されます。
- :pointer
- :char
- :int
- :long
- :ulong
- :long_long
- :ulong_long
他に動作プラットフォームに依存しないで明示的にデータ長を指定することもできます。次の8個の型になります。
- :int8
- :uint8
- :int16
- :uint16
- :int32
- :uint32
- :int64
- :uint64
効率のために抽象的な型情報からプラットフォームに即した型情報に変換したスクリプトを生成するRakefileも提供されています。詳しくは別項を参照してください(まだ書いてない)。
引数にNULL値を渡したい
動的ライブラリの関数へNULL値を渡したいことがあります。Ruby-FFIではRubyのnilオブジェクトをNULL値に変換してくれるため、nilを渡せばよいです。
module Hello extend FFI::Library ffi_lib 'libhello' attach_function :hello, [:string], :int end Hello.hello(nil)
コールバック関数を登録したい
動的ライブラリの関数を呼び出すだけでなく、Rubyで実装したメソッドを動的ライブラリ側から呼び出して使いたいことがあります。これはコールバック関数として関数のシグネチャ情報を設定することで使えるようになります。
簡単なサンプルとしてjrubyに付属しているlibcのqsort関数を紹介します。次のmoduleは、qsortが使うソート条件関数のシグネチャをコールバック関数として登録し、qsortの引数として渡すという設定になります。
module LibC extend FFI::Library callback :qsort_cmp, [ :pointer, :pointer ], :int attach_function :qsort, [ :pointer, :int, :int, :qsort_cmp ], :int end
また、callbackモジュールメソッドには、次のようにシグニチャを設定します。
callback :任意のコールバック名, [引数の型の配列], :返り値の型
ブロック付きでqsortを呼び出すを呼び出すことでコールバック関数を3番目の引数として渡すことができます。
LibC.qsort(p, 2, 4) do |p1, p2| i1 = p1.get_int32(0) i2 = p2.get_int32(0) i1 < i2 ? -1 : i1 > i2 ? 1 : 0 end
プログラム全体はJRubyのsample/ffi/qsort.rbにあります。
コールバック関数でNULL値を返したい
動的ライブラリから呼ばれるコールバック関数がNULL値を返す必要があったりします。その場合でもnilオブジェクトを返すことでRuby-FFIがNULL値に変換してくれます。
return nil
比較的あたらしい版(2009/02月以降)のJRuby-FFIでなければ動作しないので注意してください。
変数にコールバック関数のポインタを設定したい
コールバック関数を関数の引数に渡すのではなく、動的ライブラリ側にexportされた変数に代入したいことがあります。この場合でも関数に渡すのと同様にコールバック関数として型情報を設定します。
module Hello extend FFI::Library ffi_lib 'libhello'
callback :greeting_func, [:string], :string attach_variable :greeter, :greeting_frunc end
変数の型としてコールバック関数の名前のシンボルを指定します。
構造体を定義したい
動的ライブラリから取得したデータをCの構造体と同じように構造化されたデータとしてアクセスする機能があります。次のようにFFI::Structクラスを継承したクラスを作って構造体を定義します。
class DataEntry < FFI::Struct
layout :line, :string, 0,
:timstamp, :string, 4,
:data, :pointer, 8
end
layoutメソッドには、データ名、データ型、オフセットの3つをデータメンバの数だけ繰り返して渡します。
引数も返り値もない関数にアタッチしたい
引数を指定する配列に空配列を指定し、返り値に:voidを指定することでこのような関数にアタッチできます。
attach_function :hoge, [], :void
Ruby文字列のポインターを取得したい
MemoryPointerのfrom_stringメソッドを使います。
string = "string" MemoryPointer.from_string(string)
ポインターを文字列として参照したい
動的ライブラリから渡された\0終端の文字列を指すポインターをRuby文字列として参照するには次のようにPointer#read_stringメソッドを使います。
p = Hello.greeting puts p.read_string
ポインターのポインターを扱いたい
何を指すポインターであるかによってやり方が変わってきますが、例としてchar型のポインターのポインターを返す関数のコールバックを定義します。C言語でのシグネチャは以下です。
char **nested_pointer(const char *text)
MemoryPointerクラスでポインターの配列を作ります。ブロックの値としてMemoryPointer#addressメソッドで自身のポインターを返すとポインターのポインターを渡すことができます。
proc = Proc.new do
ary = MemoryPointer.new :pointer, 3
ary[0].put_pointer(0, MemoryPointer.from_string("Hello"))
ary[1].put_pointer(0, nil)
ary.address
end
配列の末尾にNULL値を入れたい
C言語のプログラムではポインターの配列の末尾にNULL値をいれて終端を表現するようなデータがよく登場します。Ruby-FFIでこのようなデータを表現するには次のように記述します。
ary = MemoryPointer.new :pointer, data.length + 1 i = 0 data.each do |word| ary[i].put_pointer(0, MemoryPointer.from_string(word)) i += 1 puts word end ary[i].put_pointer(0, nil)
Keyword(s):
References: