CrystalでWebAssemblyに出力した関数をRubyから呼び出す

## はじめに

この記事は昨日の記事の続編です。前回はCrystalで書いたコードをWebAssemblyにして、wasmer や wasmtimeで実行しました。

https://qiita.com/kojix2/items/4c9376b7e46bfe72d823

今回は、Crystalで書いた関数をWebAssemblyに出力し、Rubyから呼び出して使ってみます。

## 前回のおさらい

CrystalはLLVMの機能を利用して、WebAssemblyを出力できる。
その際、コンパイル済みのwasmのライブラリを入手してリンクする必要がある。

コンパイル済みライブラリを以下のリポジトリのリリースから入手する。（本記事作成時点では 0.0.2 が最新）

https://github.com/lbguilherme/wasm-libs

CUI操作でダウンロードする場合以下のコマンド

```sh
wget https://github.com/lbguilherme/wasm-libs/releases/download/0.0.2/wasm32-wasi-libs.tar.gz
mkdir wasm32-wasi-libs
tar -xvf wasm32-wasi-libs.tar.gz -C wasm32-wasi-libs
rm wasm32-wasi-libs.tar.gz
```

環境変数 `CRYSTAL_LIBRARY_PATH` を指定してビルド

```
export CRYSTAL_LIBRARY_PATH=$PWD/wasm32-wasi-libs
crystal build hello.cr -o hello.wasm --target wasm32-wasi
```

実行 [wasmtime](https://github.com/bytecodealliance/wasmtime) もしくは [wasmer](https://github.com/wasmerio/wasmer) を用いる。

```
wasmtime hello.wasm
wasmer hello.wasm
```

## Crystalでフィボナッチ数を求める関数を用意する

[フィボナッチ数](https://ja.wikipedia.org/wiki/%E3%83%95%E3%82%A3%E3%83%9C%E3%83%8A%E3%83%83%E3%83%81%E6%95%B0)とは、こういう感じで数がだんだん増えていく数列です。

$F_1 = F_2 = 1,\ F_{n + 2} = F_n + F_{n+1}\ (n \ge 1)$

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, ...

実装は[Crystal公式ブログの記事](https://crystal-lang.org/2016/07/15/fibonacci-benchmark.html)から拝借しました。（Crystal公式ブログの記事は本記事の内容とは無関係ですが一読の価値があります。）wasmに関数として書き出すためには、型を完全に指定した上で `def` ではなく`fun` で関数を宣言します。

```crystal:fib.cr
# エクスポートする関数は型を完全に指定して `fun` で宣言する 
fun fib(n : Int32) : Int32
  if n <= 1
    1
  else
    fib(n - 1) + fib(n - 2)
  end
end

```

wasmにビルドします。(ビルドには `wasm32-wasi-libs` が必要です。入手していない場合は上の「前回のおさらい」参照）

```sh
CRYSTAL_LIBRARY_PATH=$PWD/wasm32-wasi-libs \
  crystal build fib.cr \
    -o fib.wasm \
    --target wasm32-wasi \
    --link-flags="--export fib"
```

ビルドしたら、実際に関数が呼び出せるか確認してみましょう。`wasmtime` コマンドでは、 `--invoke` コマンドをつけることで、簡単に関数を呼び出すことができます。またバージョン3以上の `wasmer` でも同様のことができます。

```sh
wasmtime run --invoke fib fib.wasm 5 # 8
wasmer run --invoke fib fib.wasm 6 # 13
```

うまく呼び出せませたか？

## Rubyからwasmの関数を呼び出してみよう

Rubyからwasmを呼び出す方法は今後いろいろ出てくるのかもしれませんが、現状では [wasmer-ruby](https://github.com/wasmerio/wasmer-ruby) が最も有力だと思います。wasmerのバインディングです。

```
gem install wasmer
```

それでは、Rubyから `fib.wasm` を呼び出すコードを作成してみましょう。ここでの注意は、`fib.wasm` は WASI を利用しているため、WASIを手動で公開しなければならいことです。なので、READMEのチュートリアルにあるコードではなく、example ディレクトリ内の [wasi.rb](https://github.com/wasmerio/wasmer-ruby/blob/master/examples/wasi.rb) を参考にします。

```ruby:fib.rb
require 'wasmer'

MAX = ARGV[0].to_i

wasm_bytes = IO.read('fib.wasm', mode: 'rb')

store = Wasmer::Store.new
module_ = Wasmer::Module.new store, wasm_bytes

wasi_version = Wasmer::Wasi.get_version module_, true

wasi_env = Wasmer::Wasi::StateBuilder
           .new('wasi_test_program')
           .argument('--test')
           .environment('COLOR', 'true')
           .environment('APP_SHOULD_LOG', 'false')
           .map_directory('the_host_current_dir', '.')
           .finalize

import_object = wasi_env.generate_import_object store, wasi_version

instance = Wasmer::Instance.new module_, import_object
wasm_func = instance.exports.fib
```

10回関数を呼び出すことにします。

```ruby
10.times do |i|
  puts wasm_func.call(i)
end
```

これを実行すると、

```
1
1
2
3
5
8
13
21
34
55
```

フィボナッチ数列が表示されれば成功です！（最初は n=0 が実行されるていることに注意）

## ベンチマーク

せっかくなので、簡単なベンチマークを取ってみましょう。

* Rubyで素朴にフィボナッチ数を計算する場合
* Rubyから mwasmer-ruby で fib.wasm の関数を呼ぶ場合

の比較をします。（筆者はベンチマークの測定の方法に詳しくなく苦手としているので、もっときちんとしたベンチマークができる場合は、コメント欄に実行結果を書き込んで教えてください。）ここでは、v0droさんの作成した、[benchmark-plot](https://github.com/v0dro/benchmark-plot) を使って直感的にベンチマークを取っていきます。

先ほどのコードのうち、1〜10番目のフィボナッチ数列を表示するコードを取り除き、ベンチマーク用のコードを追加しました。（途中トップレベルのインスタンス変数とかが入っていてアレですが見逃してください）

```ruby:bench.rb
require 'wasmer'
require 'benchmark/plot'

MAX = ARGV[0].to_i

wasm_bytes = IO.read('fib.wasm', mode: 'rb')

store = Wasmer::Store.new
module_ = Wasmer::Module.new store, wasm_bytes

wasi_version = Wasmer::Wasi.get_version module_, true

wasi_env = Wasmer::Wasi::StateBuilder
           .new('wasi_test_program')
           .argument('--test')
           .environment('COLOR', 'true')
           .environment('APP_SHOULD_LOG', 'false')
           .map_directory('the_host_current_dir', '.')
           .finalize

import_object = wasi_env.generate_import_object store, wasi_version

instance = Wasmer::Instance.new module_, import_object
@wasm_func = instance.exports.fib

# wasm でフィボナッチ数を計算するメソッド
def fib_wasm(n)
  @wasm_func.call(n)
end

# ruby でフィボナッチ数を計算するメソッド
def fib(n)
  if n <= 1
    1
  else
    fib(n - 1) + fib(n - 2)
  end
end

# ベンチマーク用のコード
n = (1..MAX).to_a

fib_wasm(1) # ウォームアップのため１回実行しておく

Benchmark.plot(n) do |x|
  x.report('fib_wasm') do |i|
    fib_wasm(i)
  end

  x.report('fib_ruby') do |i|
    fib(i)
  end
end

```

実行してみましょう。事前に wasm のフィボナッチ関数で、事前に45まで正しい数値が得られることを確認しています。

```
ruby bench.rb 45
```

![benchmark_plot_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/684f84bf-3699-92f0-aea7-65c28f6495e1.png)

例えば、n が 40 のときで、こんな感じです。11.7倍つまり、10倍強ぐらいの速度が出るようです。

```
              user     system      total        real
fib_wasm  0.389907   0.000521   0.390428 (  0.390452)
fib_ruby  4.562256   0.000447   4.562703 (  4.562934)
```

wasmの方が速いのですが、正直な感想を言うと「こんなものか」という感じもあります。C拡張ならおそらくもっとスピードが出るでしょう。
速度よりも、プラットフォームを超えて利用できるという点の方が魅力的かもしれません。

もう少し拡大して見てみましょう n が 1〜20 のとき

![benchmark_plot_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/5916d803-9e6b-d321-f924-c3a0098736ec.png)

さらに拡大します。 n が １〜10　のとき

![benchmark_plot_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/3412733f-1f87-0667-4dd8-ad728e840846.png)

こんな感じで、n=1 のときかなり遅くなるので、実行順序を逆にしてみると、

![benchmark_plot_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/dc0f8c0c-3ba9-03c7-f584-216408800752.png)

こんな風になるため、wasmを呼ぶ時は最初のうちは時間がかかり、あとから早くなることがあるようです。本当にきちんと計測するならウォームアップ的な処理をしっかりやる必要があるでしょう（今回のベンチマークでは、計測前に1回呼び出しているのみ）また、n = 1〜4 ではRubyのフィボナッチ関数を呼ぶ方がwasmを呼び出すときよりも早いことがわかります。wasmを呼び出すときに一定のコストがかかるのでしょう。

## まとめと感想

この記事は、Crystal言語で記述したフィボナッチ数を求める関数をWebAssemblyに出力して、Rubyから呼び出しました。
手元のベンチマークでは、実行速度はRubyで素朴にフィボナッチ数を求めるメソッドを書くよりも10倍ほど速いという結果が得られました。

Crystal言語は非常に高速ですが、現状では[Crystal言語で記述されたメソッドをほかの言語から呼び出せない](https://forum.crystal-lang.org/t/embeddable-interoperable-with-ruby)という課題をかかえています。そのため、Ruby拡張をCrystal言語で記述するなど、確実に有用性が見込めるようなケースでも、なかなか使用することができませんでした。しかしながら、このようにCrystal言語でWasmを出力し、Rubyで利用できるならば、ネイティブ拡張ほどでなくても、Crystalの速度をRubyで享受することが可能になると思われます。

この記事は以上です。

## 参考資料

https://forum.crystal-lang.org/t/trying-out-wasm-support/