mzumi's blog

FFI を使って Ruby から Rust の関数を呼び出す

October 18, 2016

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"]

name で指定した文字列がビルドした際の共有ライブラリの名称となる。

で最後に、作成したプログラムをコンパイルする。

cargo build --release

上記のコマンドを実行すると、target/releaseffi_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 で作成した関数を実行できるよう、以下の内容を記述する。

で、このプログラムを実行すると

$ 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 を削除、コンパイルした後に 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 に渡したり、またその逆もあると思うので、その辺のことは次回しようと思う。