Ruby/Rust 連携 (4)　Rutie で数値計算


* [Ruby/Rust 連携 (1)　目的](https://qiita.com/scivola/items/1f51a294c86a70aa78aa)
* [Ruby/Rust 連携 (2)　手段](https://qiita.com/scivola/items/d6de19cd985a5cd27f7e)
* [Ruby/Rust 連携 (3)　FFI で数値計算](https://qiita.com/scivola/items/5ab8c6ba0b81a63d976e)
* Ruby/Rust 連携 (4)　Rutie で数値計算①
* [Ruby/Rust 連携 (5)　Rutie で数値計算②　ベジエ](https://qiita.com/scivola/items/2ab41a03beb00f6ce3d2)
* [Ruby/Rust 連携 (6)　形態素の抽出](https://qiita.com/scivola/items/b131aab4e637c4d782ee)
* [Ruby/Rust 連携 (7)　インストール時ビルドの Rust 拡張 gem を作る](https://qiita.com/scivola/items/8f101dc301c8df385c5c)


# はじめに

前回は FFI 経由で

```math
\sqrt{ x^2 + y^2 + z^2 }
```

を計算する Rust の関数を Ruby から呼んでみた。
意外と簡単に実装できることが分かったのは収穫だったが，肝心の速度はというと，Ruby で

```rb
Math.sqrt(x * x + y * y + z * z)
```

を計算するよりはるかに遅い，という残念なものだった。
原因はよく分からないが，FFI を経由するコストが大きかったのではないかと推測した。

では FFI 以外の手段で Rust と Ruby を繋いでみたらどうだろう。
この記事では Rutie というものを使う。

Qiita には Rutie の記事は無いようなので，これが最初の記事になると思う。

なお，

* macOS 10.13.6（High Sierra）
* Rust 1.46.0
* Ruby 2.7.1

を使用した。

# Rutie とは

公式サイト：[danielpclark/rutie: “The Tie Between Ruby and Rust.”](https://github.com/danielpclark/rutie)

Rutie は「ルーティー」のように読むらしい。

FFI が複数の言語間を結ぶ汎用の仕組みであるのに対し，Rutie は Ruby と Rust の間だけを取り持つ。
大きな特長は，Rust で Ruby のクラス，モジュール，メソッドを書くことができる，ということだ。
Ruby の String，Array，Hash に対応する型も Rust 側で持っている。
また，Rust 側から Ruby のメソッドを呼ぶこともできるらしい。


# 題材

FFI のときと同じく

```math
\sqrt{ x^2 + y^2 + z^2 }
```

を計算する関数を Rust で記述し，Ruby で呼んでみる。

FFI のときは，Rust の関数を Ruby のメソッドに割り当てる，という感じだったのに対し，Rutie の場合は Rust で直接 Ruby のメソッドを記述する，という感じになる。

# 実装：Rust 側

FFI のときと同様，Rust をあまり知らない人でも再現できるように書いていく。
ただし，Rust のインストールは済んでいるとする。
 
## プロジェクト作成

まずターミナルで

```shell
cargo new my_rutie_math --lib
```

とやって，Rust のプロジェクトを一つ作る。

`my_rutie_math` はプロジェクト名。これと同名のディレクトリーが出来，そこに初期ファイル一式が収まる。

`--lib` は「ライブラリークレートを作るぞ」という指定。

## Cargo.toml の編集

プロジェクトルートの Cargo.toml の末尾が `[dependencies]` で終わっているが，これを以下のようにする。

```toml:Cargo.toml
[dependencies]
rutie = "0.7.0"

[lib]
crate-type = ["cdylib"]
```

（追記 2020-10-01）rutie の 2020-10-01 時点の最新版は 0.8.1 なので，いまから試す人は

```toml
rutie = "0.8.1"
```

としてください。
### [dependencies]

`[dependencies]` のところは，依存クレートの指定である。Ruby で言えば Gemfile で依存 gem を指定するようなもの。
ここでは，[rutie](https://crates.io/crates/rutie) というクレートを使いますよ，と言っている。
`"0.7.0"` は rutie クレートのバージョン指定なのだが，「バージョン 0.7.0 にしろ」という意味ではなく「バージョン 0.7.0 以上，0.8.0 未満」という指定なのだ。
つまり，Ruby の Gemfile で言えば `"~> 0.7.0"` という指定と同じ。

2020 年 9 月 4 日時点での rutie クレートの最新リリースはバージョン 0.8.0 なのだが，どういうわけか 0.8.0 ではうまくいかなかったので[^dame]，原因究明は後回しにして，一つ古い 0.7.0 で話を進める。

[^dame]: macOS と，Windows の msvc および gnu で試したが，いずれもコンパイル時のリンクの段階でエラーが出る。詳しくは調べていないが，機会があれば別記事にまとめたい。

公式サイトの説明は 0.8.0 を前提に書かれているので注意されたい。

（追記 2020-10-01）その後，macOS で 0.8.0 を再び試したところ，とくに問題は無かった。0.8.1 も大丈夫だった。何か環境が変わったのかも。ビルドでエラーが出る方は教えてください。

### [lib]


次の `[lib]` のところの意味は私はよく分かっていない。

`crate-type` は文字通りクレートのタイプを指定するもの。バイナリークレートとライブラリークレートのうち，ライブラリークレートを作るわけだが，実はライブラリークレートにも種類があるようだ。
以下の記事が役に立つ。

[Rust の crate_type をまとめてみた - Qiita](https://qiita.com/etoilevi/items/4bd4c5b726e41f5a6689#fnref1)

`cdylib` というのは，他言語（つまり Rust 以外の言語）用の動的ライブラリーを意味するらしい。
えっと，`dy` は dynamic で，`c` は C 言語を意味するのかな？


## モジュールとメソッドの記述

では本体の記述を。

Rutie では Ruby のモジュールもクラスも作ることができる。
今回は関数（的メソッド）を一つ作りたいだけなので，クラスでなくモジュールにしよう。
モジュール名は `MyMath` にする。
メソッド名は `hypot3` にし，`MyMath` の特異メソッドとして定義する。
方針は固まった。

src/lib.rs というファイルを以下のようにする。もともとテストコードの雛形が書かれているが，これは削除してしまって構わない。

```rust:src/lib.rs
#[macro_use]
extern crate rutie;

use rutie::{Object, Module, Float};

module!(MyMath);

methods!(
    MyMath,
    _rtself,

    fn pub_hypot3(x: Float, y: Float, z: Float) -> Float {
        let x = x.unwrap().to_f64();
        let y = y.unwrap().to_f64();
        let z = z.unwrap().to_f64();
        Float::new((x * x + y * y + z * z).sqrt())
    }
);

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_MyMath() {
    Module::new("MyMath").define(|module| {
        module.def_self("hypot3", pub_hypot3)
    });
}
```

FFI 版と比べると記述量がやや多い。

### マクロ

まず，`module!` と `methods!` に注目。
これらは rutie クレートで定義されているマクロで，Ruby のモジュールとメソッドを定義するものらしい。

これらのマクロを使用するため，冒頭に

```rust
#[macro_use]
extern crate rutie;
```

という記述が必要になる（知らんけど）。

### Object, Module, Float

Rutie では，Ruby の Object，Module，Class，Array，Float，Hash，Symbol といったクラスに対応する Rust の型が**同じ名前**で定義されているようだ。
ただし，Ruby の String については同名ではなく RString という名前になっている。Rust の String とかぶらないように `R` を付けたのだろう。

今回はこれらのうち，Object, Module, Float の三つが必要になるので，

```rust
use rutie::{Object, Module, Float};
```

と書いておく。

### モジュールの定義

`MyMath` という Ruby のモジュールを作るには

```rust
module!(MyMath);
```

と書く。
たぶん実際にモジュールが出来るのはこの箇所ではなく，あとのほうに出てくる `Module::new("MyMath")` を実行したときなんだろう。

### メソッドの定義

メソッドの定義は `methods!` マクロに三つの引数を与えている。
第一引数はモジュール名 `MyMath`。
第二引数の `_rtself` はさっぱり分からない。

第三引数に関数の定義を与えている。
抜き出してみよう：

```rust
fn pub_hypot3(x: Float, y: Float, z: Float) -> Float {
    let x = x.unwrap().to_f64();
    let y = y.unwrap().to_f64();
    let z = z.unwrap().to_f64();
    Float::new((x * x + y * y + z * z).sqrt())
}
```

まず関数名なのだが，`pub_` を頭に付けているのは，Rutie のサイトに載っているコード例で，「関数名が他とかぶらないよう `pub_` を付けた」としているのに倣ったもので，かぶらないことがはっきりしていれば付けなくてよい。
なお，`pub_hypot3` が Ruby 側では `hypot3` というメソッド名になるようにするので，安心されたい。

さて，引数，返り値はともに `f64` でなく `Float` 型になっている。
`Float` はここで定義されているようだ：
https://github.com/danielpclark/rutie/blob/v0.7.0/src/class/float.rs

ここに書かれたコメントがドキュメントとして下記で見られる：
https://docs.rs/rutie/0.7.0/rutie/struct.Float.html

ここで大きな疑問が涌いた。引数から `f64` に変換するところで

```rust
x.unwrap().to_f64()
```

としているところだ。
さきのドキュメントによれば，`Float` は `to_f64()` で `f64` に変換できるはず。
なぜに `unwrap()` をかましているのか？
どうも，この `x` の型は

```
std::result::Result<rutie::Float, rutie::AnyException>
```

であるらしい。おそらく，Ruby から値をもらうときに，変なものを渡される可能性があるので `Result` になっているんだろうな。だから `unwrap()` で `Float` 型の値を取り出すのだろう。
んが，しかし！　関数の型は

```rust
fn pub_hypot3(x: Float, y: Float, z: Float) -> Float
```

となっていたではないか。`Float` だよ，`x` は。
ぁん〜？

調べても私の能力では分からなかった。
ただ，この箇所は `methods!` マクロの引数である，というところがポイントかもしれないと思った。
そう，この関数定義**らしきもの**はマクロに渡される何かなのだ。Rust の関数そのものではない。

この問題は脇に置いて先に進もう。
関数本体では

```rust
let x = x.unwrap().to_f64();
```

としている。引数に `x` があるのに同名の `x` を定義しているのは，いわゆるシャドーイングというやつ。元の `x` はもうここ以降では要らないので，同じ名前の変数を使うね，ってこと。

最後の

```rust
Float::new((x * x + y * y + z * z).sqrt())
```

は，計算された `f64` の値を元に，Ruby 側に返す `Float` 型の値を生成しているところ。

### 初期化関数の定義

「初期化関数」という呼び方は私が勝手に考えたもので，適切でないかもしれない。
ともかく，Ruby 側から呼び出す関数を一つ定義する。これを実行することによって，Rust で定義した Ruby のモジュールやメソッドが Ruby 側で実際に使えるようになるのだろうと思う。

以下に抜粋する。

```rust
#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_mymath() {
    Module::new("MyMath").define(|module| {
        module.def_self("hypot3", pub_hypot3)
    });
}
```

関数名の命名規則が分からないが，コード例に従って，`Init_XXXX` の形式にした。
冒頭に `#[allow(non_snake_case)]` とあるのは，「スネークケースにしなかったのは意図的なんだから文句言うなよ」とコンパイラーに釘を刺しているのだろう。

`#[no_mangle]` は，ライブラリーが外部に見せる関数を定義するときに付けるおなじみの呪文で，これをつけないと関数名がその名で参照できなくなるということらしい。

関数の中身は，まず `Module::new("MyMath")` でモジュール `MyMath` を作成し，それに対して `def_self` でメソッドを生やしているようだ。
`define` の引数は

```rust
|module| {
    module.def_self("hypot3", pub_hypot3)
}
```

という形になっている。これはクロージャーと呼ばれるもの。
Ruby のブロックとよく似た構文になっているのが面白い。Ruby のブロックパラメーターに当たる部分が `{ }` の外に出ている点が違っているけれども。
Ruby のブロックは値ではない（オブジェクトではない）が，Rust のクロージャーは値であり，関数の引数に渡すことができる。

`module.def_self("hypot3", pub_hypot3)` は，先に定義した `pub_hypot3` を，`hypot3` という名前のメソッドとしてモジュール `MyMath` に生やすことを意味するようだ。

これで Rust 側の実装はオシマイ。
いくつか理解できない点があったし，ちょっとややこしい感じもする。
しかし，この程度の複雑さで，Ruby のクラスやモジュールやメソッドが定義できるなら，いいんではないか？

## コンパイル

プロジェクトのルートディレクトリーで

```shell
cargo build --release
```

とやる。
すると，成果物が `target/release/libmy_rutie_math.dylib` というパスに出来る。
ただし，拡張子はターゲットによって異なるはず。Windows だと `.dll` になるのかな。

（追記）
コンパイルするとき，

```
warning: `extern` fn uses type `MyMath`, which is not FFI-safe
 --> src/lib.rs:9:5
  |
9 |     MyMath,
  |     ^^^^^^ not FFI-safe
```

という警告が出る。
（Ruby や Rust のバージョンは記事の冒頭に書いておいた）

`MyMath` という名前が「FFI-safe でない」と言っているようだ。
どういうことか全く分からないが，エラーでなく警告なので，さしあたり無視することにする。

（追記 2020-10-01）`not FFI-safe` という警告は，Rust 1.46 で出るようになったもの。Rutie 0.8.1 で解消された。
https://github.com/danielpclark/rutie/issues/128

# 実装：Ruby 側

Ruby 側では，`rutie` という gem を使う。Rust で使うクレートと同じ名前。分かりやすい。

以下のサンプルスクリプトは，Rust のプロジェクトのルートディレクトリーに存在すると仮定して書いている。

```rb
gem "rutie", "~> 0.0.4"
require "rutie"

Rutie.new(:my_rutie_math, lib_path: "target/release").init "Init_mymath", __dir__

p MyMath.hypot3(1.0, 2.0, 3.0)
# => 3.7416573867739413

# 参考
p Math.sqrt(1 * 1 + 2 * 2 + 3 * 3)
# => 3.7416573867739413
```

1 ファイルで示そうとして，上のサンプルでは いきなり `gem "rutie", "~> 0.0.4"` とやっているが，ふつうは Gemfile に書くだろう。

さて，rutie gem の使い方だが，まず最初に `Rutie.new` で Ruby オブジェクトを作るようだ。
第一引数に `:my_rutie_math` と書いたが，これは Rust で作ったライブラリーの名前。

この記事では，最初に `cargo new` したときに与えたプロジェクト名がそのままライブラリー名になっている。
しかし，Cargo.toml の `[lib]` のところで

```toml:Cargo.toml
[lib]
name = "hoge"
```

のように `name` を与えてやれば，それがライブラリー名になるはず。
そして，それはコンパイルして出来た成果物のファイル名に反映されるはずだ。

オプションの引数の `lib_path` についてはあとで触れる。

ともかく，そうやって出来た Rutie オブジェクトの `init` を呼び出す。
第一引数の `"Init_mymath"` は，私が「初期化関数」と仮に呼んだアレの名前。
第二引数はすぐあとで触れる。

ともかく，こうやって `init` すると，Rutie さんが使うべきライブラリーファイル `libmy_rutie_math.dylib` を見つけて `Init_mymath` 関数を呼び出してくれるわけだ。
繰り返しになるが，このファイルの拡張子はターゲットによって異なる。
Rutie さんはそのへんもうまく考えて見つけてくれるのだ。

で，その見つける場所なんだけども，ちょっとややこしい。
まず，`init` の第二引数を基準にして，そこから，`lib_path` に与えた相対パスだけ移動したところにある，と見るのである。

この記事の場合，Ruby スクリプトは Rust のプロジェクトのルートに置いたから，`__dir__` はそこ。
で，そこから見た `target/release` にファイルを見出すわけ。

`lib_path` や他のオプションを与えなかった場合，`"../target/release"` になる。
今の場合，これだと都合が悪いので `lib_path` を指定した。

## 使う

使い方は簡単。コード例のとおり。
`MyMath` モジュールに特異メソッド `hypot3` が生えているので，それをふつうに呼ぶだけ。
確認のため `Math.sqrt(1 * 1 + 2 * 2 + 3 * 3)` も表示しているが，同じ数値が得られた。

ただし，一つ注意しなければならないことがある。
`hypot3` の引数は三つとも `Float` である，と（Rust 側で）決めた。
もし，`MyMath.hypot3` に Integer オブジェクトを与えたらどうなるのか？

やってみた。死んだ。いわゆる panic というやつだ。
Float 以外の物を食わせると `x.unwrap()` の箇所で死ぬのだ。
もちろん，Rust 側で，いきなり `unwrap()` じゃなくて，`Ok` と `Err` で場合分けしてやれば死なない関数にできる。
あるいは Ruby 側で，Float に型キャストして呼ぶようにすれば問題ない。

# ベンチマークテスト

前回（[Ruby/Rust 連携 (3)　FFI で数値計算](https://qiita.com/scivola/items/5ab8c6ba0b81a63d976e)）は，FFI を直に使うやり方で `hypot3` をやってみて，「Rust を呼ぶより Ruby で書いたほうがずっと速かった」という残念な結果を得た。

Rutie 版はどうだろうか。あまり期待しないでやってみよう。

## テストコード

今回も [benchmark_driver](https://github.com/benchmark-driver/benchmark-driver) という gem を使って計測する。

あらかじめ

```shell
gem i benchmark_driver
```

とやってインストールしておく。
（よく混乱するけど，gem 名はハイフンじゃなくてアンダースコア）

今回は前回と違って，テストコードを Ruby で書いてやってみる。

ちょっと気をつけなければならないのが，さきほどのサンプルコードでは**ココ**を表すのに `__dir__` を使ったのだが，benchmark_driver でそう書くと，ベンチマークプログラムの居場所ではなく，benchmark_driver  が生成する一時ファイルの居場所になってしまって，Rust のライブラリーが見つけられなくなる，ということ。

以下のコードではそこを一工夫した。

```rb
require "benchmark_driver"

Benchmark.driver do |r|
  r.prelude <<~EOT
    gem "rutie", "~> 0.0.4"
    require "rutie"

    Rutie.new(:my_rutie_math, lib_path: "target/release").init "Init_mymath", "#{__dir__}"
  EOT

  r.report "MyMath.hypot3(1.0, 2.0, 3.0)"
  r.report "Math.sqrt(1.0 * 1.0 + 2.0 * 2.0 + 3.0 * 3.0)"
end
```

`prelude` は，計測に先立ってやっておくことを書く。
`report` は計測したい処理を書く。

## テスト実行

さきほどのスクリプトを実行すると：

```
Math.sqrt(1.0 * 1.0 + 2.0 * 2.0 + 3.0 * 3.0):  11796989.3 i/s
                MyMath.hypot3(1.0, 2.0, 3.0):   5684591.1 i/s - 2.08x  slower
```

ざ，惨敗ですやん。
Rutie 版の実行速度は前回の FFI 版とほぼ同じ。Ruby で書いたのの倍の時間がかかっている。

あの，今日はもう寝ていいすか？
次はもっと重い処理を Rust にやらせて Ruby スクリプトの鼻を明かしてやりますんで。