RubyからRustを呼び出すいくつかの方法のまとめ

この記事は[Ateam Hikkoshi Samurai Inc. & Ateam Connect Inc.（エイチーム引越し侍、エイチームコネクト） Advent Calendar 2019](https://qiita.com/advent-calendar/2019/hikkoshi) 1日目の記事です。


プログラムのパフォーマンス向上のためにRubyからCで書かれた処理を呼び出したいという場合があるとき、拡張ライブラリを書いたりFFIを使って処理を呼び出したりします。
今回はRustで書かれた処理をRubyで呼び出す方法を調べてみました。

## 拡張ライブラリを書く方法
Rubyには[CのためのAPI](https://docs.ruby-lang.org/en/2.6.0/extension_ja_rdoc.html)があり、これを使うことで拡張ライブラリを作ることができます。RustにはCのプログラムとリンクさせるライブラリを作ることができるので、その機能を利用してRustでRubyの拡張ライブラリを作ることができます。

詳細は以下の記事をご参照ください。
[RustだけでRuby native extensionを書く](http://blog.daich.org/2015/12/10/ruby-native-extension-by-rust/)

## FFIで呼び出す方法
Cで拡張ライブラリを記述するよりも、より簡単な方法としてFFI(Foreign function interface)を利用する方法もあります。
FFIを使った場合にはC拡張を書くことなく、利用することができるので、より簡単に使うことができます（内部的にはC拡張の仕組みと同じものが動きます）。

詳細は以下の記事をご参照ください。
[RubyからRustの関数をつかう → はやい](https://qiita.com/rejasupotaro/items/2aa57a67f4a97101950c)

## Helixを使う方法
Rustで簡単に拡張ライブラリを書くための方法として[helix](https://github.com/tildeio/helix)というgemが
あります。Qiita界隈ですとHelixといえばネームスペースはキーボードに占有されている気もしますが、RustとRubyのブリッジということで実在する[橋](https://en.wikipedia.org/wiki/Helix_Bridge)が名前の由来のようです。
[Getting Started](https://usehelix.com/getting_started)のページを見るといきなりRuby on Railsのプロジェクトから作り始めるなかなかに攻めた内容になっていますが、今回はシンプルにgemを作って、それを呼び出す方法を見てみます。
前章の参照記事で書かれているものとほぼ同じフィボナッチ数を求める関数を作成していきます。

### gemテンプレートの作成
まずはgemのプロジェクトの雛型を作ります。

```bash
$ bundle gem helix_fib
Creating gem 'helix_fib'...
MIT License enabled in config
Code of conduct enabled in config
      create  helix_fib/Gemfile
...
$ cd helix_fib
```
プロジェクトの作成が完了したら、helix_fib.gemspecに
以下の内容を追記します。

```
spec.add_runtime_dependency "helix_runtime", "= 0.7.5"
```
またspec.authorsなどTODOが記載されている項目は適当に記入します。
記入が終わったらbundle installを行って依存関係にあるhelix_runtimeをインストールします。

```
bundle install --path=vendor/bundle
```
次にRakeファイルを以下のように書き換えてbuild時にhelix_runtimeが使われるように修正します。

```
require 'bundler/setup'
require 'helix_runtime/build_task'

HelixRuntime::BuildTask.new do |t|
end

task :default => :build
```
### rustテンプレートの作成
まず以下のコマンドを実行してrustのプロジェクトを作成します。

```
$ cargo init --lib
```
このコマンドによってCargo.tomlとsrc/lib.rsが作られます。
他言語用のダイナミックなシステムライブラリを作成したいためにCargo.tomlに以下の記載を加えます。

```
[lib]
crate-type = ["cdylib"]
```
またdependenciesにはhelixを加えます

```
[dependencies]
helix = "0.7.5"
```
次にRustのコードを書いていきます。src/lib.rsに以下の内容を記述します。

```rust

#[macro_use]
extern crate helix;

ruby! {
    class HelixFIB {
        struct {
        }
        
        def initialize(helix) {
            HelixFIB{helix}
        }

        def fib(&self, n: u32) -> u32 {
            if n <= 1 {
                n
            } else {
                self.fib(n - 1) + self.fib(n - 2)
            }
        }
    }    
}
```
lib/helix_fib.rbの先頭に以下の２行を加えてビルドが通るように修正します。

```
require 'helix_runtime'
require 'helix_fib/native'
```

### ビルドと動作確認
rakeコマンドを行ってrustのコードをビルドします。

```bash
$ bundle exec rake
{}
cargo rustc --release -- -C link-args=-Wl,-undefined,dynamic_lookup
...
    Finished release [optimized] target(s) in 0.08s
```
ビルドが完了したらbin/consoleで動作を確認します。

```irb
$ bin/console
irb(main):001:0> helix_fib = HelixFIB.new
=> #<HelixFIB:0x00007fffc4a4afd0>
irb(main):002:0> helix_fib.fib(40)
=> 102334155
```
rustで書いたコードがrubyのクラスとして呼び出されて、関数が機能していることが確認できます。

### gem化とrubyプロジェクトからの呼び出し

gemのパッケージ化をした際にinstall時にビルドされる必要があるためhelix_fib.gemspecに以下の内容を追記します。

```
spec.extensions = %w[extconf.rb] 
```

またextconf.rbには以下の内容を記述します。

```ruby:execonf.rb
abort "Rust compiler required (https://www.rust-lang.org/)" if `which rustc`.empty?

File.open("Makefile", "wb") do |f|

  f.puts(<<EOD)
all:
\tbundle --deployment --path vendor/bundle
\tbundle exec rake
clean:
install:
\trm -r vendor/bundle target
EOD

end
```

gemのbuildの際にディレクトリの内容をgit ls-filesで検索して依存性を解決するので、これまでの作業内容をgitに記録します。

```
git add .
```

gem buildを行うとgemファイルが完成します。

```bash
$ gem build helix_fib
  Successfully built RubyGem
  Name: helix_fib
  Version: 0.1.0
  File: helix_fib-0.1.0.gem
```
新しいディレクトリを作成して、bundle initして作成されたGemfileにhelix_runtimeとhelix_fibを追記します。

```ruby
gem "helix_runtime"
gem "helix_fib"
```
vender/cacheのディレクトリを作成したあとに先ほど作成したhelix_fib-0.1.0.gemをコピーして、bundle install --path=vendor/bundleを行います。
helix_fibを呼び出す処理は以下のように記述します。

```ruby:sample.rb
require 'helix_fib'

helix_fib = HelixFIB.new
puts helix_fib.fib(40)
```
作成したファイルを実行すると以下のような結果が返ってきてRustで作成したファイルが実行されていることがわかります。

```
$ bundle exec ruby sample.rb
102334155
```

## Wasmを使う方法
Rustで書かれたコードをWasmにコンパイルする手法はよく知られているものだと思いますが、最近では[Cloudfrare Workers](https://workers.cloudflare.com/)や、[Compute@Edge](https://www.fastly.com/products/edge-compute/beta)など、フロントエンドだけでなく、サーバーサイドよりの領域でも使われるようになっていました。そして[WASI](https://hacks.mozilla.org/2019/03/standardizing-wasi-a-webassembly-system-interface/)というWASMをウェブブラウザー以外から呼び出すための仕様も策定されています。
今回はRustで書かれたコードをWasmにコンパイルして、それをRubyから呼び出す方法について記載しようと思います。
[wasmer](https://wasmer.io/)というスタンドアローンなWasmのランタイムとそれを呼び出す[gem](https://github.com/wasmerio/ruby-ext-wasm)が存在するので、今回はこれを利用します。

### 環境の準備
まずはrustupを使ってターゲットとするコンパイラにwasmを追加します。

```bash
rustup target add wasm32-unknown-unknown
```


### プロジェクトの作成とCargo.tomlの整備

cargoコマンドを使ってWasm用のプロジェクトを作成します。

```bash
$ cargo new --lib wasm_fib
```

Cargo.tomlに以下の内容を追記します。

```toml:Cargo.toml
...
[lib]
crate-type =["cdylib"]
```

### プログラムの作成

src/lib.rsに以下の内容を記載します。多言語から呼び出すので、[no_mangle]を付けて関数のマングリングをなくします。

```rust:lib.rs
#[no_mangle]
pub extern fn fib(n: u32) -> u32 {
    if n <= 1 {
        n
    } else {
        fib(n - 1) + fib(n - 2)
    }
}
```

### Wasmへのコンパイル

cargo buildでwasmへのコンパイルを行います。

```bash
$ cargo build --target wasm32-unknown-unknown
```
コンパイルが完了するとtarget/wasm32-unknown-unknown/debugにwasm_fib.wasmが作成されます。

### Rubyのプロジェクトの作成
Wasmを呼び出すためのRubyのプロジェクトを作成します。
新しいディレクトリを作成してbundle initを行って作成されたGemfileに以下の記述を追加して、bundle installを行います。

```
gem "wasmer"
```

### Wasmを呼び出すプログラムの作成
前章で作成したwasm_fib.wasmをプロジェクトのディレクトリにコピーした後にsample.rbというファイルを作成し、以下の記述を行います。

```ruby:sample.rb
require "wasmer"

bytes = IO.read "wasm_fib.wasm", mode: "rb"
instance = Wasmer::Instance.new bytes
puts instance.exports.fib 40
```

wasmのバイト列をWasmer::Instance.newの引数として渡すことで、wasmがインスタンス化され、関数を呼べるようになります。
プログラムを実行して結果を確認します。

```bash
$ bundle exec ruby sample.rb
102334155
```

##ベンチマーク
rubyのベンチマーク関数を使ってフィボナッチ数の関数に40を渡したときの結果を出力してみます。

ruby

```bash
$ ruby fib.rb
                 user     system      total        real
fibonatti   11.750000   0.000000  11.750000 ( 11.748531)
102334155
```

ffi

```bash
$ ruby rust-fib.rb
                 user     system      total        real
fibonatti    0.593750   0.000000   0.593750 (  0.588983)
102334155
```

拡張ライブラリ

```bash
$ ruby rust-fib.rb
                 user     system      total        real
fibonatti    1.218750   0.000000   1.218750 (  1.232219)
102334155
```

Helix

```bash
 bundle exec ruby sample.rb
                 user     system      total        real
fibonatti    0.625000   0.000000   0.625000 (  0.627916)
102334155
```

Wasm

```bash
$ bundle exec ruby sample.rb
                 user     system      total        real
fibonatti    5.125000   0.000000   5.125000 (  5.144103)
102334155
```

結果としては、Ruby < Wasm < 拡張ライブラリ < Helix < FFI
の順番になりました。拡張ライブラリがHelixより遅いのはもっと検証が必要かもしれませんが、Wasmはランライムの呼び出しにコストがかかっているように感じました。
しかし、いずれの方法でもRubyに比べてパフォーマンスは向上しているようです。

## まとめ
Helixをつかうことによって、RubyのC APIの内容をあまり知らずとも、Rustで拡張ライブラリを書くことができるようになりました。
ruby!やclassというおよそRustとは思えないコードであったり、
多言語から呼び出す処理に#[no_mangle]がないなど、不思議な感じが
する部分もありますが、簡単に拡張ライブラリが書けるということには少なからずメリットがあると思います。
Wasmに関してはFFIと同じような感覚があります。Rustで記述されたものをRubyで呼び出すためにWasmにする必要はなさそうに思えますが、wasmerの他にも[wasmtime](https://github.com/bytecodealliance/wasmtime)や[lucet](https://github.com/bytecodealliance/lucet)など、いくつかランタイムが登場していているので、興味深い分野だと思いました。

# お知らせ
エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひとも[エイチームグループ採用ページ](http://www.a-tm.co.jp/recruit/)（[Webエンジニア詳細ページ](http://www.a-tm.co.jp/recruit/requirements/career/lifestylesupport-webengineer/)よりお問い合わせ下さい。
あるいは、[Qiita Jobs](https://jobs.qiita.com/)の[引越し侍Webエンジニアチーム](https://jobs.qiita.com/employers/a-hikkoshi/development_teams/173)、[自社サービスWebエンジニア／インフラからサービス改善まで！]()、[求む！引越し侍大規模リプレイスにおけるフロントエンドのリードエンジニア募集！](https://jobs.qiita.com/postings/317)の記事をみて、興味が出てきた方は是非Qiita Jobsのチャットでメッセージをください。

# 明日
明日は[fujimk](https://qiita.com/fujimk)の記事です。
きっとこれがQiita初投稿かな。皆さんお楽しみに。