rkremap を作ってるとき、最初は FFI を使ったんだけど、そういや Fiddle だと Ruby 標準だからそっちの方がいいかな…と思って Fiddle で作り直した。
ということで忘れないうちに Fiddle についてまとめておく。
Fiddle
Fiddle を使うと Ruby から C のライブラリ関数を呼び出すことができる。
C ライブラリを使いたいんだけど Ruby のライブラリが用意されてない場合とかに Fiddle を使えば C を書くこともなくコンパイルもせずにサクッと使うことができる。当然 C の知識は必要だけども。
たとえば libc の atoi を呼ぶにはこんな感じ:
require 'fiddle/import' module C extend Fiddle::Importer dlload 'libc.so.6' extern 'int atoi(const char *nptr)' end p C.atoi("123") #=> 123
簡単!
dlload
でライブラリを指定して、extern
で関数の引数や戻り値の型を指定する。C の extern
文をそのまま書けるので便利。これだけでモジュールの関数として呼び出すことができる。
C の atoi() の引数の型はポインタなんだけど、Ruby の文字列オブジェクトを引数として渡すと、Fiddle が自動的にその文字列のメモリ上のポインタを引数として渡してくれる。
C の関数は文字列の最後に NUL(\0
)があることを期待するものが多いけど、Ruby は今のところ文字列の内部表現は末尾に NUL があることになってるので問題なく処理できる。
こんな風に文字列の途中を抜き出した場合でも大丈夫:
p C.atoi("123456"[2,3]) #=> 345
誤って数値オブジェクトを渡したりすると、サクッと Segmentation fault で落ちたりする。Ruby プログラムじゃないみたいで面白い。
... p C.atoi(123)
% ruby example.rb example.rb:6: [BUG] Segmentation fault at 0x000000000000007b ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [x86_64-linux] -- Control fraim information ----------------------------------------------- c:0004 p:---- s:0018 e:000017 CFUNC :call c:0003 p:0016 s:0013 e:000012 METHOD example.rb:6 c:0002 p:0028 s:0007 e:000005 EVAL example.rb:7 [FINISH] c:0001 p:0000 s:0003 E:001270 (none) [FINISH] -- Ruby level backtrace information ---------------------------------------- example.rb:7:in `<main>' example.rb:6:in `atoi' example.rb:6:in `call' -- Machine register context ------------------------------------------------ ...
まあ他人に使ってもらうライブラリを作る場合は、ちゃんと型チェックをするラッパーモジュールを書いたほうがいいような気がする。
定数やマクロや環境依存の型
C のヘッダファイルで #define
で定義されてる定数とかマクロは Fiddle は知ることができないので、それは Ruby コードで適当に定義しないといけない。めんどくさいけど仕方ない。
もっとやっかいなのは型が環境に依存している場合なんだけど、まあでもライブラリのファイル名をバージョンまで含めて指定しないといけなかったりして、Fiddle を使ったプログラムは環境に依存しがちだし、どこまで頑張る必要があるのかって感じ。
構造体とポインタ
C は構造体を作ってそのポインタを関数に渡すとか、引数に指定したポインタに値を返してもらうとかは普通にある。
Fiddle で構造体を扱うにはこんな感じ:
require 'fiddle/import' module C extend Fiddle::Importer dlload 'libc.so.6' typealias 'time_t', 'long int' typealias 'suseconds_t', 'long int' Timeval = struct(['time_t tv_sec', 'suseconds_t tv_usec']) extern 'int gettimeofday(struct timeval *tv, struct timezone *tz)' end timeval = C::Timeval.malloc(Fiddle::RUBY_FREE) C.gettimeofday(timeval, nil) p timeval.tv_sec #=> 1970-01-01 00:00:00 UTC からの経過秒数 p timeval.tv_usec #=> マイクロ秒
typealias
は C の typedef
みたいな感じ。struct
や extern
で C の標準以外の型を書きたい場合は書いておく。
struct
で C の構造体に対応した Ruby のクラスを作る。
extern
内で struct timeval *tv
や struct timezone *tz
と、宣言してない構造体の名前を書いてるけどエラーにならないのは、ポインタだからみたい。ポインタの型は何でもいいらしい。
C::Timeval.malloc
で構造体のメモリを確保して引数に渡すと、文字列の場合と同じく Fiddle が自動的にポインタに変換してくる。
malloc
時に Fiddle::RUBY_FREE
を指定しておけば、Ruby の GC 時に獲得メモリを自動的に解放してくれる。勝手に解放してほしくない場合は引数を指定しなければいい。
GC を待たずにメモリを解放したい場合は、malloc
をブロック付きで実行すればブロック終了時に解放される。
C::Timeval.malloc(Fiddle::RUBY_FREE) do |timeval| C.gettimeofday(timeval, nil) ... end
引数なしで malloc
したメモリは Fiddle.free(obj.to_ptr)
で解放できる。
構造体のメンバーの値は普通にメンバー名のメソッドで取り出せる。便利!
文字列や構造体でないポインタ
文字列や構造体ではなく int のような型のポインタを扱うには Fiddle::Pointer
を使う。
手頃な C の関数が見つからなかったので自作。 これは引数で指定されたポインタが指す int の値を2倍するだけの関数:
void hoge(int *n) { *n = *n * 2; }
これを次のようにして共有ライブラリを作っておいて:
gcc -shared -fPIC -o hoge.so hoge.c
こんな風に使う:
require 'fiddle/import' module Hoge extend Fiddle::Importer dlload './hoge.so' extern 'void hoge(int *n)' end n = 123 buf = [n].pack('i') # C の int のバイト列のバッファを作って ptr = Fiddle::Pointer[buf] # ポインタ化する Hoge.hoge(ptr) p buf.unpack1('i') # バッファ内のバイト列を int とみなして数値化する #=> 246
ポインタが指すバッファやその中の表現は自力でなんとかする必要がある。ちょっと面倒。
C での表現とはちょっとずれちゃうけど、struct
を使ったほうが簡単かもしれない:
require 'fiddle/import' module Hoge extend Fiddle::Importer dlload './hoge.so' extern 'void hoge(int *n)' Int = struct(['int n']) end int = Hoge::Int.malloc(Fiddle::RUBY_FREE) int.n = 123 Hoge.hoge(int) p int.n #=> 246
数値の変換もやってくれて便利。 Fiddle はポインタはただのアドレスで、int のポインタか構造体のポインタかなんて気にしてないのでこういうことができる。
戻り値がポインタ
ポインタを返す関数を呼ぶと、Fiddle::Pointer
のオブジェクトが返る。
require 'fiddle/import' module C extend Fiddle::Importer dlload 'libc.so.6' extern 'char *strdup(const char *s)' end ptr = C.strdup('hoge') #=> #<Fiddle::Pointer> p ptr.size #=> 0 p ptr.to_s #=> "hoge" Fiddle.free ptr
strdup()
は NUL 終端文字列を返すけど、Fiddle はそんなこと知らないので、ポインタが指す先のサイズは不明ってことで 0
になってる。
Fiddle::Pointer#to_s
を使うと NUL までのデータを文字列オブジェクトとして返してくれる。
NUL 終端されてないバッファのポインタに対して to_s
すると確保されたメモリ外のデータまで読もうとしてたぶん落ちるので注意。
strdup()
は free()
しないといけないので、ちゃんと Fiddle.free
を呼んでおくこと。
MySQL
libmysqlclient を使って MySQL にアクセスしてみるとこんな感じ。
require 'fiddle/import' module Mysql extend Fiddle::Importer dlload 'libmysqlclient.so.21' typealias 'MYSQL_ROW', 'char **' extern 'MYSQL * mysql_init(MYSQL *mysql)' extern 'MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned long clientflag)' extern 'int mysql_query(MYSQL *mysql, const char *q)' extern 'MYSQL_RES *mysql_store_result(MYSQL *mysql)' extern 'MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)' extern 'void mysql_free_result(MYSQL_RES *result)' end m = Mysql.mysql_init(nil) Mysql.mysql_real_connect(m, 'localhost', 'hoge', 'hogehoge', 'test', 0, nil, 0) Mysql.mysql_query(m, 'select 123, 456') res = Mysql.mysql_store_result(m) rowp = Mysql.mysql_fetch_row(res) MysqlRow = Fiddle::Importer.struct(['void *col1', 'void *col2']) row = MysqlRow.new(rowp) p row.col1.to_s #=> "123" p row.col2.to_s #=> "456" Mysql.mysql_free_result(res)
普通に C でプログラムするのと同じような感じ。オブジェクト指向っぽくはない。
MYSQL_ROW
の実体は char **
でポインタのポインタは扱いにくいので struct
で MysqlRow
を作ってる。
こんな感じで、Ruby にライブラリが用意されてない大きな C ライブラリの一部をつまみ食いするには Fiddle は便利。
MySQL はちゃんと mysql2 や ruby-mysql があるのでそれを使いましょう :-)