FFI
Foreign function Interface の略で、別のプログラミング言語で記述された関数を利用するための機能です。
Rubyでは、ffiというgemや、Rubyに梱包される標準ライブラリのFiddleなどが同等の機能を提供しています。
今回は、gemである提供するffiを使って、RubyからCプログラムを利用してみたいと思います。
値渡し
まずはffiに慣れていきましょう。
呼び出し元のコードは以下です。
int add(int a, int b) {
return a + b;
}
int型の引数を2つ取って、それらの加算結果を返すadd
を定義しました。
このaddをRubyから呼び出すために、共有ライブラリ(*.so)の形式に変換する必要があります。
以下のコマンドを使って変換します。
$ gcc -shared lib.c -o libadd.so
gccコンパイラに-shared
オプションを渡すと、共有ライブラリとしてオブジェクトファイルを吐き出してくれます。
Rubyから呼び出すファイルはlibadd.so
です。
require 'ffi'
module AddFFI
extend FFI::Library
ffi_lib 'libadd.so'
attach_function :add, [:int, :int], :int
end
puts AddFFI.add(1, 2)
ffi_lib
メソッドで、先程生成した共有ライブラリを読み込みます。
そしてattach_function
で、Cプログラムで定義した関数を、Rubyから扱えるようにマッピングします。
第1引数にメソッド名
、第2引数にCで定義した関数の引数の型
を、第3引数に返り値の型
を与えてあげます。
メソッド名は、Cプログラム内で定義した関数と同じである必要があります。
このRubyファイルを実行します。
$ ruby add.rb
3
無事FFIを介してCプログラムを呼び出すことができました。
ポインタを使って渡す
参照渡しとは変数のメモリ番地を渡すことで、変数の値を共有する仕組みです。値はコピーされないので、参照渡し先でその変数についての情報を書き換えると、元の保持していた変数の値も変化します。
[追記]
参照渡しとは変数のメモリ番地を渡すことで、変数の値を共有する仕組みです。値はコピーされないので、参照渡し先でその変数についての情報を書き換えると、元の保持していた変数の値も変化します。
筆者の参照渡しの理解が誤っており、@shiracamus さん および @c-yan さんからご指摘を頂きました。
参照渡しという言葉の定義については本記事のコメント欄に、お二人からのご教示があるので、そちらをご覧ください。(ご指摘ありがとうございました!)
例
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
int *p, *q;
p = (int *)malloc(sizeof(int));
*p = 1;
printf("%d\n", *p);
// ここで参照渡し
// [追記] ここは参照渡しではなく参照の値渡し(詳細はコメントで)
q = p;
*q = 2;
printf("%d\n", *p);
}
これを実行すると以下のようになり、*p
の値が、*q
への代入後に変化していることがわかります。
$ gcc -o sansyo sansyo.c
$ ./sansyo
1
2
やりたいことはCプログラム内で、アドレス値(ポインタ)を返す関数を定義して、Rubyからはそのアドレスを参照することで値を取得する、ということです。
作成したCプログラムは以下です。
#include <stdio.h>
#include <stdlib.h>
int *calc(int a, int b) {
int *p;
p = (int *)malloc(sizeof(int));
*p = a + b;
printf("sansyo function address = %p\n", p);
return p;
}
int main(int argc, char *argv[]) {
int *q = calc(1, 2);
printf("calc(1, 2) = %d\n", *q);
printf("calc(1, 2)'s address = %p\n", q);
}
デバッグも兼ねて、main
の中でcalc
を呼び出しています。
Rubyへアドレス値を返したいので、calcはint型のポインタを返すようにしています。
実行結果は以下です。意図したとおりに動いていそうです。
$ gcc -o add_sansyo add_sansyo.c
$ ./add_sansyo
sansyo function address = 0x7f94d54026b0
calc(1, 2) = 3
calc(1, 2)'s address = 0x7f94d54026b0
このCプログラムをadd_sansyo.so
として共有ライブラリへ変換して、Rubyがロードします。
require 'ffi'
module AddFFI
extend FFI::Library
ffi_lib "add_sansyo.so"
attach_function :sansyo, [:int, :int], :pointer
end
result = AddFFI.sansyo(1, 2)
puts result
puts result.read(:int)
attach_function
で行うマッピングについては、その名もズバリ、:pointer
という形で、返り値を指定してあげます。
AddFFI.sansyo
メソッドの返り値は、FFI::Pointer
クラスが返ってくるので、read
というインスタンスメソッドで、そのアドレスの値を取得しています。
実行すると以下のようになり、Cプログラムで参照しているアドレスをRubyから触ることで、関数の出力結果を意図したように取得できていることが理解できます。
$ ruby sansyo.rb
sansyo function address = 0x7fa1aad31860
#<FFI::Pointer address=0x00007fa1aad31860>
3
まとめ
Rubygemsのffiを使って、Cプログラムで書かれた関数を値やポインタを使ってRubyから呼び出すことができました。
言語の特徴を組み合わせすることでプログラムを最適化する、という考え方は個人的にとても気に入りました。