[バージョンアップに追従] Elixirから簡単にRustを呼び出せるRustler #1 準備編 [rustler 0.21.1]


この記事は何?

rustler のバージョンが上がって Elixirから簡単にRustを呼び出せるRustler #1 準備編 のやり方では試せなくなったので、現在のバージョン (rustler 0.21.1) 向けの rustler の試し方を記載します。

基本的に元記事と同じ方法を辿るため、元記事の文章をコピペしています。ただしバージョンアップにより一部手順が異なります。

なお、rustler 0.21.x の API docs は生成が失敗しているので、代わりに rustler 0.22.0-rc.0 の docs を見ることをお勧めします。

(コピペ + 改変ここから)

導入手順

mix に rustler を追加

まずは rustler のパッケージをインストールして、mix rustler.new コマンドが実行できるようにします。
rustler 0.21.x に依存させるために、deps に {:rustler, "~> 0.21.0"} を加えてください。

# 通常通り elixir のプロジェクトを作成
$ mix new phx_rust_21
$ cd phx_rust_21
mix.exs
...
  defp deps do
    [
      {:rustler, "~> 0.21.0"}
    ]
  end
...
$ mix deps.get

rustler のボイラープレートを生成

mix に rustler の命令が追加されたので、さっそく使います。

$ mix rustler.new

Elixir 側の Module 名と Rust でのユニット名 (ファイル名) をそれぞれ対話形式で聞いてくるので、NifExample, example と入力します。
これで準備は完了です。

ここで指定した「example」をAtom化した :example は Ruslter の公式ドキュメント内では NIF ID と呼ばれています。

$ mix rustler.new
==> toml
Compiling 10 files (.ex)
Generated toml app
==> rustler
Compiling 5 files (.ex)
warning: EEx.eval_string/3 defined in application :eex is used by the current application but the current application does not directly depend on :eex. To fix this, you must do one of:

  1. If :eex is part of Erlang/Elixir, you must include it under :extra_applications inside "def application" in your mix.exs

  2. If :eex is a dependency, make sure it is listed under "def deps" in your mix.exs

  3. In case you don't want to add a requirement to :eex, you may optionally skip this warning by adding [xref: [exclude: EEx]] to your "def project" in mix.exs

  lib/mix/tasks/rustler.new.ex:79: Mix.Tasks.Rustler.New.copy_from/3

warning: Toml.decode!/1 defined in application :toml is used by the current application but the current application does not directly depend on :toml. To fix this, you must do one of:

  1. If :toml is part of Erlang/Elixir, you must include it under :extra_applications inside "def application" in your mix.exs

  2. If :toml is a dependency, make sure it is listed under "def deps" in your mix.exs

  3. In case you don't want to add a requirement to :toml, you may optionally skip this warning by adding [xref: [exclude: Toml]] to your "def project" in mix.exs

  lib/mix/tasks/compile.rustler.ex:177: Mix.Tasks.Compile.Rustler.check_crate_env/1

Generated rustler app
==> phx_rust_21
This is the name of the Elixir module the NIF module will be registered to.
Module name > NifExample
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (nifexample) > example
* creating native/example/.cargo/config
* creating native/example/README.md
* creating native/example/Cargo.toml
* creating native/example/src/lib.rs
Ready to go! See /Users/koba_mac/srcview/phx_rust_21/native/example/README.md for further instructions.
koba_mac@kobas-MacBook-Air:~/srcview/phx_rust_21 $ 

このような流れでボイラープレートが出来上がります。
Rust のテンプレートはこちらになります。

native/example/src/lib.rs
use rustler::{Encoder, Env, Error, Term};

mod atoms {
    rustler_atoms! {
        atom ok;
        //atom error;
        //atom __true__ = "true";
        //atom __false__ = "false";
    }
}

rustler::rustler_export_nifs! {
    "Elixir.NifExample",
    [
        ("add", 2, add)
    ],
    None
}

fn add<'a>(env: Env<'a>, args: &[Term<'a>]) -> Result<Term<'a>, Error> {
    let num1: i64 = args[0].decode()?;
    let num2: i64 = args[1].decode()?;

    Ok((atoms::ok(), num1 + num2).encode(env))
}

プロジェクト概要をmix.exsに定義

mix.exsに3箇所コードを追加します。

  1. compilers: 行の追加
  2. rustler_crates: 行の追加
  3. rustler_crates 関数の追加
mix.exs
...
  def project do
    [
      app: :phx_rust_21,
      version: "0.1.0",
      elixir: "~> 1.11",
      compilers: [:rustler] ++ Mix.compilers, # 1. 追加
      rustler_crates: rustler_crates(),  #2. 追加
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end
...

  # 3. この関数(rustler_crates)を追加
  defp rustler_crates() do
    [example: [ # 呼び出し側Elixirモジュール NifExampleのマクロで使用するAtom
      path: "native/example",
      mode: (if Mix.env == :prod, do: :release, else: :debug),
    ]]
  end
...

Elixir側のコード実装

Elixirの呼び出し側モジュールを実装します。ElixirとRustのモジュールと関数を具体的にマッピングしていく作業になります。

use Rustlerでは、モジュールにrustler拡張を適用しています。
以下の2箇所の設定を、確認して下さい。

  • otp_app:mix.exs内の project[ app: ~ ]で指定してある App 名。通常はプロジェクト名 (ここでは :phx_rust_21 )です。
  • crate:はmix.exsのrustler_crates関数で定義された、NIF ID(前述)。
lib/example.ex
defmodule NifExample do
  use Rustler, otp_app: :phx_rust_21, crate: :example

  def add(_a, _b), do: exit(:nif_not_loaded)
end

ちなみにadd関数は最初からボイラープレートに含まれる関数です。

Rust側のコード実装

テンプレートとして作られるコードを多少改変します。
以下を設定しています。

  • Elixir側のモジュール名
  • ElixirとRustの関数の関連付けリスト (elixir関数名, アリティ, Rust関数名)
native/example/src/lib.rs
...

mod atoms {
    // 生成されるコードでは rustler_atoms! となっているが、
    // cannot find macro `rustler_atoms` in this scope というエラーが出るため、絶対パスで参照するように変更する。
    rustler::rustler_atoms! {
        atom ok;
        //atom error;
        //atom __true__ = "true";
        //atom __false__ = "false";
    }
}

rustler::rustler_export_nifs! {
    "Elixir.NifExample", // Elixirのmodule名
    [
        ("add", 2, add) // Elixirとrustの関数の関連付け 
    ],
    None
}

実行してみる

プロジェクトを起動すると、Rustのコンパイラーが走ります。
警告は出ますが、無事完了。

$ iex -S mix
Erlang/OTP 23 [erts-11.1.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]

Compiling NIF crate :example (native/example)...
   Compiling example v0.1.0 (/Users/koba_mac/srcview/phx_rust_21/native/example)
    Finished dev [unoptimized + debuginfo] target(s) in 4.39s
Interactive Elixir (1.11.2) - press Ctrl+C to exit (type h() ENTER for help)

iex のプロンプトが起動するので、早速 add 関数を実行してみます。

iex(1)> NifExample.add(1,2)
{:ok, 3}

無事動作しました!

注目すべきは、戻りがタプルになっていることです。Rust側のタプルの構造がそのままElixirに返ってきてます。素晴らしいですね。

mix.exsの追加とexample.exを追加しただけで、rustの関数を呼び出せてしまいました。

(コピペ + 改変ここまで)

まとめ

使用したコードは https://github.com/koba-e964/phx_rust_21/tree/intro に置いてあります。
次回のElixirから簡単にRustを呼び出せるRustler #2 クレートを使ってみるはまだ試していないため、この記事の続きが必要かどうかは未定です。