💎

Fiddle で Ruby から C ライブラリを使う

2022/01/21に公開

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 frame 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 みたいな感じ。structextern で C の標準以外の型を書きたい場合は書いておく。

struct で C の構造体に対応した Ruby のクラスを作る。
extern 内で struct timeval *tvstruct 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 ** でポインタのポインタは扱いにくいので structMysqlRow を作ってる。


こんな感じで、Ruby にライブラリが用意されてない大きな C ライブラリの一部をつまみ食いするには Fiddle は便利。

MySQL はちゃんと mysql2ruby-mysql があるのでそれを使いましょう :-)

Discussion