連記事目次
- Ruby/Rust 連携 (1) 目的
- Ruby/Rust 連携 (2) 手段
- Ruby/Rust 連携 (3) FFI で数値計算
- Ruby/Rust 連携 (4) Rutie で数値計算①
- Ruby/Rust 連携 (5) Rutie で数値計算② ベジエ
- Ruby/Rust 連携 (6) 形態素の抽出
- Ruby/Rust 連携 (7) インストール時ビルドの Rust 拡張 gem を作る
はじめに
一口に Ruby と Rust を連携する手段といっても,さまざまなレベルややり方がある。
Rust 製コマンドを利用
「なんじゃそりゃ」と言われそうだが,Rust で作られた CLI ツール(要するにコマンド)を Ruby から呼び出して使う,というのも連携方法の一つ。
Ruby 側から Kernel.#system や IO.popen などを使って外部プログラムを呼び出す。
たとえば,列数・行数ともに大きな CSV があり,そのなかのいくつかの列を抜き出して集計などの処理を行いたいとする。
列の抜き出しには,Rust 製の xsv というツールが便利だ。
例えば
xsv select name,phone people.csv
と書くと,people.csv から name 列と phone 列だけを抜き出した CSV を標準出力に流してくれる。
これを前処理としてかましてやるわけだ。
とはいえ,外部コマンドとして呼び出すだけなら Rust かどうかは全く関係ない。
この節では,「連携といっても,全く独立に作ったプログラムをコマンド呼び出しと標準入出力やで組み合わせる方法もある」と示したかったのだが,以後は取り上げない。
FFI: Foreign Function Interface 経由
(注意:他の節もそうだけど,よく分からずに書いてるので,間違いがあるかも)
FFI は日本語で「他言語関数インターフェース」と呼ばれ,異なるプログラミング言語間で関数(など)を呼ぶための仕組みの一つ。
渡せるデータの型は C 言語のそれに基づいているらしく(?),かなり制約がある。
例えば整数をやり取りするのでも,uint8
(符号無し 8 ビット整数)だの int32
(符号付き 32 ビット整数)だのといった型を明確にしなければならず,その範囲に収まらない Integer オブジェクトを渡したりはできない。
Ruby の配列やハッシュなんかも(少なくとも簡単には)やり取りできないようだ。
文字列のやり取りはやや面倒くさい。
使える型の一覧は,たぶんこれだと思う:
Types · ffi/ffi Wiki
FFI 経由で Ruby から Rust の関数を呼ぶのは難しくない。
Rust の側では
#[no_mangle]
pub extern fn hoge(x: f64, y: f64) -> f64 {
x + y
}
みたいにして,関数を定義し,コンパイルする。
Ruby の側では ffi という gem を使う。
そして,
require "ffi"
module Hoge
extend FFI::Library
ffi_lib "path/to/library/file"
attach_function :hoge, [:double, :double], :double
end
p Hoge.hoge(2.0, 3.0) # => 5.0
のように呼ぶ。
拍子抜けするほど簡単ではないか。
あとで FFI で Ruby から Rust の関数をよぶ記事を書く。
Helix
FFI がさまざまな言語間を結ぶ汎用の仕様であるのに対し,本節の Helix と次節の Rutie は Ruby と Rust をつなぐためだけにある仕組みだ。
Helix の公式のサイトは以下の二つを見ればいいと思う。
- tildeio/helix: Native Ruby extensions without fear(GitHub のリポジトリー)
- Helix: Native Ruby Extensions Without Fear
また,以下の記事がたいへん役に立つ。
RubyからRustを呼び出すいくつかの方法のまとめ - Qiita
公式サイトのサンプルではいきなり Ruby on Rails プロジェクトに組み込むようになっていて,Rails やってない人にはよく分からないが,上記サイトでは gem を作る例になっていて理解しやすい。
Helix は Rust で Ruby のクラスが作れてしまうところがスゴイ。マジかよ。
Rust 側では helix というクレートを用いる。すると ruby!
というマクロが使え,
ruby! {
class Hoge {
// インスタンスメソッド
def foo(&self, s: String) {
}
// クラスメソッド
def bar(s: String) {
}
}
}
みたいな感じで書けるらしい。
FFI と違って,文字列のやり取りも上のように簡単。
面白いのは,Ruby の「String オブジェクトまたは nil
」「Float オブジェクトまたは nil
」という値が Rust の Option<String>
,Option<f64>
としてやり取りできる,ということ(知らんけど)。
配列やハッシュはやりとりできない。Roadmap によると長期的目標に
- Support key data structures: Array and Hash
が上がっている。
少し残念なのは,開発が(はた目には)ゆっくりになっているように見えること。2020 年 9 月 4 日時点では,
- 最新リリース:2018 年 6 月 4 日(バージョン 0.7.5)
- リポジトリー最終更新:2019 年 7 月 31 日
となっている。
また,サイトの保守も十分であるとは言いがたい印象だ。
Helix はまだほとんど試していないが,もうちょっと調べて記事を書いてみたい。
2020-10-29 追記
Helix は開発を継続することが困難になり,2020 年 10 月で deprecated になった。
Rutie
Rutie は「ルーティー」のような感じで発音するらしい。Ruby の Ru と Rust の Ru とを tie する(結ぶ)というような含みの命名のようだ。
公式サイトはこちら:
- danielpclark/rutie: “The Tie Between Ruby and Rust.”(GitHub のリポジトリー)
これも Helix のように,Rust で Ruby のクラスが作れてしまう。モジュールも作れる。
Rust 側では rutie というクレートを使う。
例えば
module!(Hoge);
mehtods!(
Hoge,
_rtself,
fn foo(s: RString) -> RString {
// 云々
}
);
みたいな感じで Hoge
モジュールとその特異メソッド foo
が定義できるようだ。
(これを Ruby で使えるようにするには Rust 側にもう少し記述が必要)
Ruby 側では rutie という(クレートと同名の)gem を使う。
Ruby 側のコード例は略す。あとで別記事を書く予定。
Rutie では,Rust 側で Ruby のメソッドを利用することもできる。これは面白い。しかし,まだ試していないので,どんなもんかよく分からない。
Rutie の 2020 年 9 月 4 日時点での最新リリースは 2020 年 7 月(バージョン 0.8.0)。
その他
ほかにもいろんな手段がある。
既に挙げた
RubyからRustを呼び出すいくつかの方法のまとめ - Qiita
には,WASM(WebAssembly)を用いた方法も載っている。
ところで,FFI にしろ,Helix や Rutie にしろ,やり取りできるデータの型の制約が大きかった。
巨大データのやり取りでは,Apache Arrow を介す方法も有望だろうか? 筆者はこれについて全く分からない。ただ,Ruby にも Rust にも Arrow を使うライブラリーが用意されている。
複雑な構造のデータで,エンコード/デコードの時間を気にしない用途なら,JSON や MessagePack 経由もありなのかもしれない。
JSON も MessagePack も,Ruby や Rust で簡単に利用できる。
ベンチマークテスト(2020-09-18 追記)
私自身はそれぞれの手段の速度を比較してみてはいないが,ベンチマークテストのサイトを見つけた。
https://github.com/bbugh/ruby-rust-extension-benchmark
(2019 年 10 月のもの)
このベンチマークテストではどうも Helix は Rutie よりも遅いようだ。Ruby/Rust 間のやりとりのコストがやや大きいのだろう。
ベンチマークテストは,内容や規模をちょっと変えるだけで順位が入れ替わることはよくあり,上記のテストだけで決めつけるのは危険かもしれない。
ただ,機能面や開発のペースを見ても,どちらか一つだけを調査・使用するなら現時点では Rutie のほうなのかな,という気がする。