Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Rust でつくるかんたん Ruby Gem

はじめに

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 を編集します。今回は遊びなので、下記のように単純にしました。

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

全体は次のようになります。

Cargo.toml
[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 を開いて下記のように関数を追加します。

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 など手元の環境に合わせて書きかえてください。)

Rakefile
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が配置されているのがわかりますね。

image.png

同じようにcleanを追加してみます(これは必須ではありません)。

Rakefile
task :rust_clean do
  `cargo clean`
  `rm -f ./lib/fib/libfib.so` # OSによって書き換える
end

task :clean => :rust_clean

rake clean してみましょう。

exa -T

image.png

targetディレクトリとlibfib.soが削除されているのがわかると思います。

FFI

Rubyからrustを呼ぶ方法はForeign function interface (FFI) を使います。
Rubyには2つのFFIのライブラリがあります。fiddleruby-ffi です。今回のような簡単なケースでは、どちらでも大丈夫です。

FFIモジュールを作成する

Fibモジュールの中に、FFIモジュールを追加します。このモジュールに、rustの関数をRubyのメソッドとして追加します。

lib/fib/ffi.rb
module Fib
  module FFI
  end
end

完成イメージはこんな感じです。
image.png

fiddle を使用する場合

fiddle はRubyの標準ライブラリですのですぐ使えます。

lib/fib/ffi.rb を新規作成します。libfib.so の拡張子に注意してください。

lib/fib/ffi.rb
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で動作しない場合は、下記のスタックオーバーフローの日本語トピックを参照。

ruby-ffi を使用する場合

ruby-ffi はRubyの標準ライブラリではないので、fib.gemspecを編集して追加します。

fib.gemspec
spec.add_dependency "ffi"

bundle updateしてruby-ffiをインストールします。

lib/fib/ffi.rb を新規作成します。

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モジュールに[]メソッドを追加します。

lib/fib.rb
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モジュールからは専用のモジュールのメソッドを間接的に呼び出すようにした方が安全でしょう。

image.png

それでは、動かしてみます。

bundle exec bin/consle

image.png

うまく動いているようです。

テストを追加する

(少しタイミングが遅いと感じる人もいるかも知れませんが)ここでテストを追加します。

spec/fib_spec.rb を編集します。

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側ではそれを探すだけにする方法
      • などなど他にもあるかもしれませんが、目的や想定利用者に応じて選ぶ必要がありそうですね。

Githubに公開されている良作のGemなどをよく観察して、良さそうな方法を少しずつ取り入れていくのがよいと思われます。

最後に

思いのほか簡単にRustを組み込んだRubyのGemが作れる気がしてきました。少しrustの勉強意欲がわきました。
気がついたことなどあったら、コメント欄などになんでもやさしくご指摘ください。

この記事は以上です。

参考文献

kojix2
Rubyがすき。バイオインフォマ見習い。
qiitadon
Qiitadon(β)から生まれた Qiita ユーザー・コミュニティです。
https://qiitadon.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away