Rust でつくるかんたん Ruby Gem

## はじめに

Rubyの処理を高速化するためにはC言語でRuby拡張を書く方法が有名です。
先日、海外のインターネット掲示板Redditで、「[Rustで書かれたGemをリリースしたよ！](https://www.reddit.com/r/ruby/comments/fhu07l/released_a_string_metricsdistance_gem_written_in/)」という記事を見つけました。

* [Github - StrMetrics](https://github.com/anirbanmu/str_metrics)

どうやらRustで処理が書かれたRubyのGemを作ることができるようです。
このGemの中身を観察して、[Rustでフィボナッチ数を計算](https://qiita.com/rejasupotaro/items/2aa57a67f4a97101950c)するシンプルなGemを作ってみました。

## Gem を作る

`bundler`でGemのテンプレートを作成します。

```sh
bundle gem fib -t rspec # RSpecのテストを同時作成
cd fib
```

fib はフィボナッチ(Fibonacci)の略です。

## gemspec を編集する

`fib.gemspec` を編集します。今回は遊びなので、下記のように単純にしました。

```ruby: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]
```

* 参考資料： [Rust の crate_type をまとめてみた](https://qiita.com/etoilevi/items/4bd4c5b726e41f5a6689)

これでGemのテンプレートができました。

## フィボナッチ数列を計算する Rust のコードを追加する

* [RubyからRustの関数をつかう → はやい](https://qiita.com/rejasupotaro/items/2aa57a67f4a97101950c#rust%E3%82%92%E5%85%B1%E6%9C%89%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AB%E3%81%99%E3%82%8B)

を参照して `src/lib.rs` を開いて下記のように関数を追加します。

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

```ruby: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`](https://qiita.com/navitime_tech/items/c249269a3b47666c784b) コマンドでディレクトリの構成を確かめてみます。
`lib/fib`ディレクトリに`libfib.so`が配置されているのがわかりますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/4ef211dc-9a32-30b3-c4ec-7a923583e2ac.png)

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

```ruby: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/d76aa0b0-e327-d450-aa28-59df1e0831ce.png)

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

## FFI

Rubyからrustを呼ぶ方法は[Foreign function interface (FFI)](https://ja.wikipedia.org/wiki/Foreign_function_interface) を使います。
Rubyには2つのFFIのライブラリがあります。[fiddle](https://github.com/ruby/fiddle) と [ruby-ffi](https://github.com/ffi/ffi) です。今回のような簡単なケースでは、どちらでも大丈夫です。

### FFIモジュールを作成する

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

```ruby:lib/fib/ffi.rb
module Fib
  module FFI
  end
end
```

完成イメージはこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/a4b42595-feb9-67cf-eb51-2492d202a866.png)


### fiddle を使用する場合

[fiddle](https://github.com/ruby/fiddle) はRubyの標準ライブラリですのですぐ使えます。

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

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

* [Windows の Ruby の fiddle で lib○○.dll が読み込めない時、何をチェックすればよいでしょうか？](https://ja.stackoverflow.com/questions/60784/)
* [RubyInstaller2 for Windows porting guide for gem developers - DLL loading](https://github.com/oneclick/rubyinstaller2/wiki/For-gem-developers#-dll-loading)

### ruby-ffi を使用する場合

[ruby-ffi](https://github.com/ffi/ffi) はRubyの標準ライブラリではないので、`fib.gemspec`を編集して追加します。

```ruby:fib.gemspec
spec.add_dependency "ffi"
```

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

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

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

```ruby: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/a4b42595-feb9-67cf-eb51-2492d202a866.png)

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

`bundle exec bin/consle`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/dc367c66-6cad-6c43-4df7-573ff740182e.png)

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

## テストを追加する

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

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

```ruby: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の勉強意欲がわきました。
気がついたことなどあったら、コメント欄などになんでもやさしくご指摘ください。

この記事は以上です。

## 参考文献

* [RubyからRustを呼び出すいくつかの方法のまとめ](https://qiita.com/dd511805/items/fe603fe40b51307a8a9f)