Livebook で Rust を実行する

## はじめに

> 効率的で信頼できるソフトウェアを誰もがつくれる言語

引用元

https://www.rust-lang.org/ja

Rustler を使うことで、 Elixir から Rust のコードを呼べるようになります

https://github.com/rusterlium/rustler

:::note info
本来であれば `mix new` で作った Elixir のプロジェクト内で使用するものですが、無理矢理 Livebook で動かしました

Livebook では Rustler 経由での Rust のビルドができなかったため、 Rustler 内部で行なっていることを Livebook で実行しています

ちゃんと Rustler を使っている例は @pojiro さんの記事をご覧ください

https://qiita.com/pojiro/items/a105021cce44ca4ff282
:::

本記事では Livebook で（一部） Rustler を使い、 Rust のコードを実行します

実装したコードはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/rustler/rustler.livemd

## 前提条件

実行環境には Rust をインストールしておきます

## セットアップ

セットアップセルで Rustler をインストールします

```elixir
Mix.install([
  {:rustler, "~> 0.35.1"}
])
```

## Rust プロジェクトの生成

Rustler を使って Rust のプロジェクトを生成します（`cargo new` と同じようなこと）

そのまま実行すると Livebook 実行環境のホームディレクトリーに作られてしまうため、 `/tmp` に移動します

```elixir
File.cd("/tmp")
```

Elixir 側のモジュール名 `module` と Rust 側のプロジェクト名 `name` を定義しておきます

```elixir
module = "LearnRustler.Nif"
name = "learn_rustler_nif"
```

Rust のプロジェクトを作成します

```elixir
Mix.Tasks.Rustler.New.run(["--module", module, "--name", name])
```

上記のコードは `mix rustler.new --module LearnRustler.Nif --name learn_rustler_nif` を実行したのと同じことになります

標準出力には以下のように表示されます

```
* creating native/learn_rustler_nif/.cargo/config.toml
* creating native/learn_rustler_nif/README.md
* creating native/learn_rustler_nif/Cargo.toml
* creating native/learn_rustler_nif/src/lib.rs
* creating native/learn_rustler_nif/.gitignore
Ready to go! See /tmp/native/learn_rustler_nif/README.md for further instructions.
```

生成されたプロジェクトのパス `native_path` と、その中の `src/lib.rs` のパス `src_path` を定義しておきます

```elixir
native_path = Path.join([File.cwd!(), "native", name])
src_path = Path.join([native_path, "src", "lib.rs"])
```

`src/lib.rs` の中身を実装したい内容に書き換えます

今回は加算、乗算、ソート、挨拶の関数を Rust で実装しました

```elixir
File.write!(src_path,"""
#[rustler::nif]
fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[rustler::nif]
fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

#[rustler::nif]
fn sort(v: Vec<i32>) -> Vec<i32> {
    let mut sorted = v;
    sorted.sort();
    sorted
}

#[rustler::nif]
fn hello(name: &str) -> String {
    format!("Hello, {}", name)
}

rustler::init!("Elixir.LearnRustler.Nif");
""")
```

## Rust プロジェクトのビルド

`cargo rustc --release` を実行し、 Rust プロジェクトをビルドします

```elixir
System.cmd(
  "cargo",
  ["rustc", "--release"],
  cd: native_path,
  stderr_to_stdout: true,
  into: IO.stream(:stdio, :line)
)
```

標準出力

```
   Compiling proc-macro2 v1.0.92
   Compiling unicode-ident v1.0.14
   Compiling regex-lite v0.1.6
   Compiling heck v0.5.0
   Compiling cfg-if v1.0.0
   Compiling inventory v0.3.16
   Compiling libloading v0.8.6
   Compiling rustler v0.35.1
   Compiling quote v1.0.38
   Compiling syn v2.0.92
   Compiling rustler_codegen v0.35.1
   Compiling learn_rustler_nif v0.1.0 (/tmp/native/learn_rustler_nif)
    Finished `release` profile [optimized] target(s) in 8.39s
```

ビルドして作成された `liblearn_rustler_nif.so` のパスを定義しておきます

```elixir
lib_path = Path.join([native_path, "target", "release", "liblearn_rustler_nif"])
```

## NIF の読込

Elixir 側のモジュールを定義し、 `load` 関数で NIF を読み込みます

```elixir
defmodule LearnRustler.Nif do
  def load(path), do: :erlang.load_nif(path, 0)

  def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
  def multiply(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
  def sort(_v), do: :erlang.nif_error(:nif_not_loaded)
  def hello(_name), do: :erlang.nif_error(:nif_not_loaded)
end
```

```elixir
LearnRustler.Nif.load(lib_path)
```

## 関数の実行

定義した関数を実行します

```elixir
LearnRustler.Nif.add(3, 4)
```

実行結果

```elixir
7
```

```elixir
LearnRustler.Nif.multiply(3, 4)
```

実行結果

```elixir
12
```

```elixir
LearnRustler.Nif.sort([8, 5, 6, 2])
```

実行結果

```elixir
[2, 5, 6, 8]
```

```elixir
LearnRustler.Nif.hello("Rust")
```

実行結果

```elixir
"Hello, Rust"
```

全て想定通りに動きました

## まとめ

Livebook 上で Rust のコードを実行できました

本来、 Rustler を使う場合は以下のうようなモジュールを定義しておくだけで良いのですが、 Livebook の場合は `:otp_app` に指定するアプリケーション名が存在しないため、この方法は諦めました

```elixir
defmodule LearnRustler.Nif do
  use Rustler, otp_app: :learn_rustler, crate: "learn_rustler_nif"

  def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
end
```

そのおかげで Rustler の中身を見て内容を理解できたので、むしろ良かったと思います