はじめに
Rubyの処理を高速化するためにはC言語でRuby拡張を書く方法が有名です。
先日、海外のインターネット掲示板Redditで、「Rustで書かれたGemをリリースしたよ!」という記事を見つけました。
どうやらRustで処理が書かれたRubyのGemを作ることができるようです。
このGemの中身を観察して、Rustでフィボナッチ数を計算するシンプルなGemを作ってみました。
Gem を作る
bundler
でGemのテンプレートを作成します。
bundle gem fib -t rspec # RSpecのテストを同時作成
cd fib
fib はフィボナッチ(Fibonacci)の略です。
gemspec を編集する
fib.gemspec
を編集します。今回は遊びなので、下記のように単純にしました。
require_relative 'lib/fib/version'
Gem::Specification.new do |spec|
spec.name = "fib"
spec.version = Fib::VERSION
spec.author = ["kojix2"]
spec.summary = "Get the fibonacci number"
spec.files = Dir['lib/**/*', 'src/**/*.rs', 'Cargo.toml', 'LICENSE', 'README.md']
spec.require_paths = ["lib"]
end
cargo init する
Cargo.tomlを作成します。
cargo init --lib
Cargo.toml を編集する
Cargo.toml
に次の2行を書き加えます。
[lib]
crate-type = ["cdylib"]
全体は次のようになります。
[package]
name = "fib"
version = "0.1.0"
authors = ["author <soda_mail_siyo@mail.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
これでGemのテンプレートができました。
フィボナッチ数列を計算する Rust のコードを追加する
を参照して src/lib.rs
を開いて下記のように関数を追加します。
#[no_mangle]
pub extern fn fib(n: u32) -> u32 {
if n <= 1 {
n
} else {
fib(n - 1) + fib(n - 2)
}
}
Rakefileを編集する
Rakefileに rust_build
タスクを追加します。
(共有ライブラリ libfib.so
の拡張子は、Macでは libfib.dylib
Windowsでは libfib.dll
など手元の環境に合わせて書きかえてください。)
task :rust_build do
`cargo rustc --release`
`mv -f ./target/release/libfib.so ./lib/fib` # OSによって書き換える
end
task :build => :rust_build
task :spec => :rust_build
動作確認のため rake rust_build
してみます。ターミナルに次の表示が現れれば成功です。
Compiling fib v0.1.0 (/home/kojix2/Ruby/fib)
Finished release [optimized] target(s) in 3.50s
tree
または exa -T
コマンドでディレクトリの構成を確かめてみます。
lib/fib
ディレクトリにlibfib.so
が配置されているのがわかりますね。
同じようにclean
を追加してみます(これは必須ではありません)。
task :rust_clean do
`cargo clean`
`rm -f ./lib/fib/libfib.so` # OSによって書き換える
end
task :clean => :rust_clean
rake clean
してみましょう。
exa -T
target
ディレクトリとlibfib.so
が削除されているのがわかると思います。
FFI
Rubyからrustを呼ぶ方法はForeign function interface (FFI) を使います。
Rubyには2つのFFIのライブラリがあります。fiddle と ruby-ffi です。今回のような簡単なケースでは、どちらでも大丈夫です。
FFIモジュールを作成する
Fibモジュールの中に、FFIモジュールを追加します。このモジュールに、rustの関数をRubyのメソッドとして追加します。
module Fib
module FFI
end
end
fiddle を使用する場合
fiddle はRubyの標準ライブラリですのですぐ使えます。
lib/fib/ffi.rb
を新規作成します。libfib.so の拡張子に注意してください。
require 'fiddle/import'
module Fib
module FFI
extend Fiddle::Importer
dlload File.expand_path('libfib.so', __dir__)
# Mac は libfib.dylib, Win は libfib.dll 等に書き換える。
extern 'unsigned int fib(unsigned int n)' # C言語の形式で表記する
end
end
FiddleがWindowsで動作しない場合は、下記のスタックオーバーフローの日本語トピックを参照。
- Windows の Ruby の fiddle で lib○○.dll が読み込めない時、何をチェックすればよいでしょうか?
- RubyInstaller2 for Windows porting guide for gem developers - DLL loading
ruby-ffi を使用する場合
ruby-ffi はRubyの標準ライブラリではないので、fib.gemspec
を編集して追加します。
spec.add_dependency "ffi"
bundle update
してruby-ffi
をインストールします。
lib/fib/ffi.rb
を新規作成します。
require 'ffi'
module Fib
module FFI
extend FFI::Library
lib_name = "libfib.#{::FFI::Platform::LIBSUFFIX}"
ffi_lib File.expand_path(lib_name, __dir__)
attach_function :fib, [:uint], :uint
end
end
Rubyらしくfib関数を呼び出せるようにする
最後に使用感をRubyらしく変更します。今回は
Fib[3] # => 2
という風に角括弧をつかったイディオムでフィボナッチ数を呼び出せるようにします。
lib/fib.rb
を次のように編集して、Fib
モジュールに[]
メソッドを追加します。
require "fib/ffi" # 追加
require "fib/version"
module Fib
def self.[](n)
FFI.fib(n)
end
end
Fibモジュールに直接Rustの関数を追加することができます。しかし、ほとんどの場合、Rustのような他の言語で関数を呼び出す前に、Ruby側で何かを行う必要があります。したがって、FFI用の特別なモジュールを作成し、そのメソッドを間接的に呼び出す方が安全です。
最初から Fib モジュールにrustの関数を追加することもできます。しかし、他言語の関数を直接呼び出す前に、Ruby側で何か処理が必要になるケースがほとんどだと思います。だから今回のケースのように専用のモジュール(今回はFib::FFI)を作成して、Fibモジュールからは専用のモジュールのメソッドを間接的に呼び出すようにした方が安全でしょう。
それでは、動かしてみます。
bundle exec bin/consle
うまく動いているようです。
テストを追加する
(少しタイミングが遅いと感じる人もいるかも知れませんが)ここでテストを追加します。
spec/fib_spec.rb
を編集します。
RSpec.describe Fib do
it "has a version number" do
expect(Fib::VERSION).not_to be nil
end
it "calculates fibonatti" do
expect(Fib[10]).to eq(55)
end
end
インストール
rake install
bundlerなしでもgemが動作するか確認してみましょう。
公開用Gemを作る上での注意点など
- 処理中の割り込み
- 今回のフィボナッチ数を計算するプログラムは Ctrl+C を押しても中断ができません。
- エラー処理
- rust側で発生したエラーを回収する方法を工夫する必要がありそうです。
- クロスプラットフォーム対応
- 実際にGemを作って配布するとなると、色々なOSで動作させたり、共有ライブラリのパスを探す処理も必要になるでしょう。
- Gem配布のときに共有ライブラリを含めるかどうか
- これはRubyとRustというよりは、バインディング全般の問題ですね。
- 全てのOSの共有ライブラリをバージョン決め打ちで最初からGemに含めておく方法。
- gemをリリースするタイミングで共有ライブラリを生成する方法。
- Gemをリリースするタイミングで共有ライブラリをどこかからダウンロードする方法。
- ユーザーがrustを持っているのを前提にしてユーザー側でコンパイルさせる方法。
- ユーザーがパッケージマネージャー等で事前にlib.soを入手することを前提にして、Ruby側ではそれを探すだけにする方法
- などなど他にもあるかもしれませんが、目的や想定利用者に応じて選ぶ必要がありそうですね。
- これはRubyとRustというよりは、バインディング全般の問題ですね。
Githubに公開されている良作のGemなどをよく観察して、良さそうな方法を少しずつ取り入れていくのがよいと思われます。
最後に
思いのほか簡単にRustを組み込んだRubyのGemが作れる気がしてきました。少しrustの勉強意欲がわきました。
気がついたことなどあったら、コメント欄などになんでもやさしくご指摘ください。
この記事は以上です。