Elixirから簡単にRustを呼び出せるRustler #2 クレートを使ってみる

（この記事は、「Elixir or Phoenix Advent Calendar 2017」の25日目です）

前日は @tuchiroさんの [**「ElixirでSI開発入門 #5 Ectoで自由にSQLを書いて実行する」**](https://qiita.com/tuchiro/items/34a3c7f0f7deb5d3241f)でした。
本日は[**「Elixirから簡単にRustを呼び出せるRustler #1 準備編」**](https://qiita.com/twinbee/items/aabc11d0d667800fc0bb)の続きです。


:black_square_button::black_large_square::black_square_button:**　お知らせ　**:black_square_button::black_large_square::black_square_button:
[**「fukuoka.ex#11：DB／データサイエンスにコネクトするElixir」**](https://fukuokaex.connpass.com/event/87241)を6/22（金）19時に開催します
私もETSとFlowを交えた発表で参加します!
ストリームとデータ処理に興味ある方は、是非ともご参加下さい！

[![image.png](https://connpass-tokyo.s3.amazonaws.com/thumbs/cb/13/cb130a03343bc310b07244c308b940db.png)](https://fukuokaex.connpass.com/event/87241)

---

## Rustler

Rustといえば、メモリ安全なネイティブコンパイラ言語です。厳格なメモリ安全チェックで有名なのですが、Rustlerではその厳格な部分がうまいことラップされており、今回ご紹介するような文字列変換ではRustの既存ライブラリを、**Rustのコンパイルエラーの嵐に巻き込まれず**に、そのスピードの恩恵を受けることができます。


RustlerはElixirからNIF経由で安全にRustのモジュールを呼び出すためのライブラリと**mix拡張を含めたボイラープレート**を作成するパッケージです。
今回は、新たに関数を追加してRustのライブラリであるクレートをElixirから呼び出してみます。

- hex [公式ドキュメント](https://hexdocs.pm/rustler/basics.html)

- Github [Rustler](https://github.com/hansihe/rustler)

---

## Rustのクレートとは？

『クレートは他の言語における「ライブラリ」や「パッケージ」と同じ意味です。このことからRustのパッケージマネジメントツールの名前を「Cargo」としています。』[公式サイト](https://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/crates-and-modules.html)より。

というわけで、Elixirの`mix`にあたるツールが**Cargo**。`mix.exs`にあたるのが、**Cargo.toml**になります。今回は、Rustの外部クレートである**[unicode-jp](https://crates.io/crates/unicode-jp)**を使用します。その手順を見ていきましょう。

## Rustlerでの設定

以下の手順で行います。

### Rust側

Cargo.tomlに使用する外部クレートを追加します。

今回使用するクレートの`unicode-jp`を`Cargo.toml`の依存関係の場所に追加します。このへんはmix.exsと同じ感覚です。mix deps.getにあたる動作は不要で、コンパイル時に読み込んでくれます。

```toml:./native/example/Cargo.toml
[package]
name = "example"
version = "0.1.0"
authors = []

[lib]
name = "example"
path = "src/lib.rs"
crate-type = ["dylib"]

[dependencies]
rustler = "0.15.1"
rustler_codegen = "0.15.1"
lazy_static = "0.2"
unicode-jp = "0.3.0"      # <==== 追加
```

rustのソースでは、クレート[unicode-jp](https://crates.io/crates/unicode-jp)は、ソース内ではkanaというクレート名です。externとuseの設定を行います。（「unicode-jp」と「kana」で一切つながりがないのが不思議ですね:upside_down:)

```rust:./native/examle/src/lib.rs
#[macro_use] extern crate rustler;
#[macro_use] extern crate rustler_codegen;
#[macro_use] extern crate lazy_static;
extern crate kana;                // <=== 追加

use rustler::{NifEnv, NifTerm, NifResult, NifEncoder};
use rustler::types::atom::NifAtom;
use kana::*;                      // <=== 追加

```

elixirの関数とrustの関数をマッピングします。
`rust_half2kana`という関数を、elixirの`half2kana`にマッピングします。タプルの２番目はアリティ(引数の数)です。

```rust
rustler_export_nifs! {
    "Elixir.NifExample",
    [("add", 2, add),
     ("half2kana", 1, rust_half2kana) # <== elixirの関数とrustの関数をマッピング
    ],
    None
}

```

次は、rustの関数の実装です。

String型の変数s1に文字列を取り込んで、`kana`(unicode-jp)クレートの関数である`half2kana`に文字列の参照を渡し、戻り値をNIF用にencodeしています。関数の型定義はHaskellとTypescriptを足したような怪獣みたいな雰囲気ですが、**一切触らなくて良い**ので安心です。関数の内部は、Rustの文法を知らなくても、見ただけでなんとなく理解できると思います。


```rust
fn rust_half2kana<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
    let s1: String = try!(args[0].decode());

    Ok((hira2kata(&s1)).encode(env))
}
```
前回のadd関数ではタプルを返していましたが、今回は文字列単体を返すようにしてみます。

Ok()の行を見ると、**戻り値に型を設定していない**のが、おわかりいただけるでしょうか？


### Elixir側

エラー処理のコードを追加するだけです。

```elixir:./lib/example.ex

 def half2kana(_a), do: exit(:nif_not_loaded)

```



一度ボイラープレートを設定が完了してしまえば、面倒な型のマッピングはほとんどありません。**Rustの関数を書いてしまえば、２か所の設定でElixirから利用できるんです !** ・・・・ 

---

## 実行してみる

replを起動して実行してみましょう。

```
$ iex -S mix
```

```elixir
iex(1)> NifExample.half2kana("ｴﾘｸｻｰとラスト")
"エリクサーとラスト"
iex(2)>
```

半角文字が全角文字に変換できました。

戻り値も前回のadd関数では{:ok, 3}のタプルでしたが、今回は文字列型で帰ってきています。Rustで`(atoms::ok(), 3)`を返せばElixirではタプルで戻ってくる。`("文字列")`で返せば文字列(バイナリ)型で戻ってくるという優れものなのです。

---

## パフォーマンス計測

さてパフォーマンスはどうでしょうか？簡単な半角カナ⇒全角カナ変換を100万回繰り返してみます。

実行環境
- Elixir 1.6.5 OTP 20
- Rustc 1.22.1

まずは筆者の書いたElixirの全角半角変換ライブラリ[mojiex](https://github.com/enpedasi/Mojiex)で計測してみます。

```elixir
iex(1)> :timer.tc(fn -> Enum.reduce(0..1_000_000, 0, fn _,_-> Mojiex.convert("ｱｲｳｴｵかきくけこｻｼｽｾｿたちつてと",{:hk,:zk})end) end
) |> case do {elapsed, res} -> {elapsed/1000000, res} end
{25.864065, "アイウエオかきくけこサシスセソたちつてと"}
```
約26秒です。

では、Rustのクレート版を計測してみましょう。

```elixir
iex(1)> :timer.tc(fn -> Enum.reduce(0..1_000_000, 0, fn _,_ -> NifExample.half2kana("ｱｲｳｴｵかきくけこｻｼｽｾｿたちつてと")end) end) |> case do {elapsed, res} -> {elapsed/1000000, res} end
{39.003642, "アイウエオかきくけこサシスセソたちつてと"}
```

約39秒です。

え！？　**ElixirよりRustのほうが大分遅い！**
**Elixir速いじゃない !**

・・・・・

っと。ここまでテンプレですね。
そんなはずはないです ^^。

---

## Rustの最適化

Rustコンパイルして遅いというのは、**「Rustあるある」**のようです。

前回mix.exsの設定をご紹介しました。設定を見てみると、RustのコンパイルモードがMix.envに依存しています。ここはRustのコンパイルモードを、強制的にreleaseモードにして再コンパイルしてみます。

```elixir:mix.exs
  # ～
  defp rustler_crates() do
    [example: [
      path: "native/example",
      # mode: (if Mix.env == :prod, do: :release, else: :debug),
      mode: :release  # 強制的 releaseモードにする
    ]]
  end
```


```elixir
$ iex -S mix
iex(1)> :timer.tc(fn -> Enum.reduce(0..1_000_000, 0, fn _,_ -> NifExample.half2kana("ｱｲｳｴｵかきくけこｻｼｽｾｿたちつてと")end) end) |> case do {elapsed, res} -> {elapsed/1000000, res} end
{4.450794, "アイウエオかきくけこサシスセソたちつてと"}
```
約4.45秒となりました。

elixirの**約5.8倍速い**という結果が出ました。


## 終わりに
これでRustのクレートを自由にElixirから使えるようになりました。次回は[「
Elixirから簡単にRustを呼び出せるRustler #3 いろいろな型を呼び出す」](https://qiita.com/twinbee/items/f94eb7f74ff39c781da0)になります。


明日は @zacky1972 さんの[**「ZEAM開発ログv0.1.4 Python/NumPyとElixir/Flow一本勝負！ElixirはAI/ML業界に革命をもたらすか!?」**](https://qiita.com/zacky1972/items/5e7a31b3ee03bc0d31dd)す！