Edited at

Elixirから簡単にRustを呼び出せるRustler #1 準備編

More than 1 year has passed since last update.

(この記事は、「Elixir or Phoenix Advent Calendar 2017」の20日目です)

前日は @tuchiroさんの 「ElixirでSI開発入門 #4 本番パスワードを環境変数に持たせる」のでした。

本日は、Elixir並列処理「Flow」の2段ステージ構造を理解するの続きです。「これまでGenStageやFlowで、並列データストリームを扱っていたのに、急にRustネタ!?」と思われるかも知れませんが、追々GenStageやFlowとの絡みが出てきます。

:black_square_button::black_large_square::black_square_button: お知らせ :black_square_button::black_large_square::black_square_button:

「fukuoka.ex#11:DB/データサイエンスにコネクトするElixir」を6/22(金)19時に開催します

私もETSとFlowを交えた発表で参加します!

ストリームとデータ処理に興味ある方は、是非ともご参加下さい!

image.png


Rustler

RustlerはElixirからNIF経由で安全にrustのモジュールを呼び出すためのライブラリとmix拡張を含めたボイラープレートを作成するパッケージです。

数値・バイナリ・文字列に限らず、タプル・マップ・リスト等Elixir/Erlangのベーシックな型にはほとんど対応しています。

OSにRustのインストールが完了していることを前提して、本文を書いてます。



動機

Elixirで住所の正規化を行う案件が出てきたのですが、半角カナ変換を行うライブラリが見当たりませんでした。Erlang VMには苦手な処理に思え、当時検討したのがNIFによるネイティブコンパイラー系ライブラリの利用でした。

その当時ネイティブコンパイラー系の記事を探した時に、 @tatsuya6502氏の「ElixirからRustの関数をつかう → はやい」という記事を見つけて、「何とモダンな!Elixir+Rustの組み合わせは素晴らしい!」とは思ったのですが、「NIFって結構大変そうだな」という思いが脳裏に焼き付いていたので、結局Elixirでmojiexを自前実装しました。

その直後にRustlerを発見し、大変好感触でしたのでここでシェア致します。(GoやNimでも同等のものがないかを探したのですが発見できませんでした。)

半角カナ変換ShiftJIS -> UTF-8変換等を今後扱っていきますのでお楽しみに!



導入手順


mixにrustlerを追加

まずはrustlerのパッケージをインストールして、mix ruslter.newコマンドが実行できるようにします。

# 通常通りelixirのプロジェクトを作成

$ mix new phx_rust
$ cd phx_rust


mix.exs

defp deps do

[
{:rustler, "~> 0.16.0"}
]

$ mix deps.get



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

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

$ mix rustler.new

Elixir側のModule名とRustでのユニット名(ファイル名)をそれぞれ対話形式で聞いてくるので


NifExample

example


と入力します。

これで準備は完了です。

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

/app/phx_rust # mix rustler.new

==> rustler
Compiling 2 files (.erl)
Compiling 6 files (.ex)
Generated rustler app
==> phx_rust
This is the name of the Elixir module the NIF module will be registered to.
Module name > Example
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (example) > example
* creating native/example/README.md
* creating native/example/Cargo.toml
* creating native/example/src/lib.rs
Ready to go! See /app/phx_rust/native/example/README.md for further instructions.

このような流れでボイラープレートが出来上がります。

Rustのテンプレートはこちらになります。


  • native/example/src/lib.rs

#[macro_use] extern crate rustler;

#[macro_use] extern crate rustler_codegen;
#[macro_use] extern crate lazy_static;

use rustler::{NifEnv, NifTerm, NifResult, NifEncoder};

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

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

fn add<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
let num1: i64 = try!(args[0].decode());
let num2: i64 = try!(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,
version: "0.0.1",
elixir: "~> 1.4",
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)です。


  • crate:はmix.exsのrustler_crates関数で定義された、NIF ID(前述)。


lib/example.ex

defmodule NifExample do

use Rustler, otp_app: :app, crate: :example

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


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


Rust側のコード実装

テンプレートとして作られるコードそのままです。

以下を設定しています。


  • Elixir側のモジュール名

  • ElixirとRustの関数の関連付けリスト (elixir関数名, アリティ, Rust関数名)


native/example/src/lib.rs

rustler_export_nifs! {

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



実行してみる

プロジェクトを起動すると、Rustのコンパイラーが走ります。

警告は出ますが、無事完了。(unoptimized警告は重要ですが、次回に解説します)

$ iex -S mix

Compiling syn v0.11.11
Compiling rustler v0.16.0
Compiling rustler_codegen v0.16.0
Compiling example v0.1.0 (file:///app/{dir}/native/example)
warning: unused `#[macro_use]` import
--> src/lib.rs:2:1
|
2 | #[macro_use] extern crate rustler_codegen;
| ^^^^^^^^^^^^
|
= note: #[warn(unused_imports)] on by default

Finished dev [unoptimized + debuginfo] target(s) in 28.70 secs
Interactive Elixir (1.6.4) - 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の関数を呼び出せてしまいました。



終わりに

ElixirからRustの呼び出し。思った以上に簡単だったのではないでしょうか?次回は「Elixirから簡単にRustを呼び出せるRustler #2 クレートを使ってみる」です。

明日は @zacky1972 さんの「ZEAM開発ログv0.1.3 AI/MLを爆速にしたい! Flow のコードを OpenCL で書いてみる〜GPU編」です!