Ruby でバッチ処理などを書いていると、処理に時間がかかってしまい、もっと速い言語で書きたい時がある。 そんな時、FFI を使うと、遅い処理部分を Rust で書くことができるということで、試してみた。
Rust
まず、呼び出される側の Rust のプロジェクトを作成する。
$ mkdir ffi-sample
$ cd ffi-sample
$ rustup override set nightly
$ cago init .
rust の version は nightly(2016/10/18 現在)である 1.14.0 を使用する。
lib.rs に Ruby から呼び出す関数を定義する。
今回は perform という関数を定義し、これを Ruby側から呼び出すようにする。 (内容は、「Hello, world」 を表示するだけ)
次に、Cargo.toml に下記を追加
[lib]
name = "ffi_sample"
crate-type = ["dylib"]
で最後に、作成したプログラムをコンパイルする。
cargo build –release
上記のコマンドを実行すると、``` target/release ``` に ``` ffi_sample.dylib ``` が作成される。
以上で Rust 側の設定は完了。
## Ruby
次に、Ruby 側のプログラムを作成する。
今回は、Rust のプロジェクトと同じディクレトリに Ruby プログラムを作成するが、
別々のプロジェクトにしても問題はない。
$ rbenv local 2.3.1 $ bundle init
Gemfile に以下のように、FFI を追加する。
gem ‘ffi’
で、ffi をインストール
bundle install –path vendor/bundle
プロジェクトの直下に、ffi_sample.rb というファイルを作成し、Rust で作成した関数を実行できるよう、以下の内容を記述する。
<script src="https://gist.github.com/mzumi/1734848ce5ebed1ee2f9128ff72d657b.js"></script>
で、このプログラムを実行すると
$ bundle exec ruby ffi_sample.rb Hello, world
と Rust 側のプログラムを呼び出すことができた。
## Rust 側の補足
### no_mangle 属性について
``` #[no_mangle] ``` をつけるとマングリングされなくなる。
これはどういうことかというと、作成されたシンボルテーブルの
シンボル名が関数の名前のままになるということ。
これは、 ``` nm ``` コマンドを使用すれば確認できる。
nm target/release/libffi_sample.dylib | grep perform
のコマンドを実行すると、
* no_mangle がある場合
0000000000000bf0 T _perform
* no_mangle がない場合
0000000000000bf0 T __ZN10ffi_sample7perform17he3cddfa65fed67b7E
となる。
マングリングしてしまうとシンボル名が関数名だけではなく、引数情報や名前空間などの情報の文字列が足されてしまった文字列になってしまう。
実際に no_mangle を削除、コンパイルした後に Ruby のプログラムを呼び出すと
attach_function': Function 'perform' not found in [target/release/libffi_sample.dylib] (FFI::NotFoundError)
``
というメッセージが表示され、Rust 側の perform 関数が見つからず、プログラムが呼び出されないことがわかる。
extern について
extern は C ABI に従うようにするために指定する。 extern "C"
と明示的に指定することもできる。
C 以外にもいくつか ABI があるようだ。
まとめ
Ruby と Rust の連携は FFI を使うと簡単にできることが分かった。 しかし、実際のプログラムだと、Ruby で計算したものを Rust に渡したり、またその逆もあると思うので、その辺のことは次回しようと思う。