この記事は旧いので、@kobae964さんが書かれた [バージョンアップに追従] Elixirから簡単にRustを呼び出せるRustler #1 準備編 [rustler 0.21.1]をご覧ください
(この記事は、「Elixir or Phoenix Advent Calendar 2017」の20日目です)
前日は @tuchiroさんの 「ElixirでSI開発入門 #4 本番パスワードを環境変数に持たせる」のでした。
本日は、Elixir並列処理「Flow」の2段ステージ構造を理解するの続きです。「これまでGenStageやFlowで、並列データストリームを扱っていたのに、急にRustネタ!?」と思われるかも知れませんが、追々GenStageやFlowとの絡みが出てきます。
お知らせ
「fukuoka.ex#11:DB/データサイエンスにコネクトするElixir」を6/22(金)19時に開催します
私もETSとFlowを交えた発表で参加します!
ストリームとデータ処理に興味ある方は、是非ともご参加下さい!
Rustler
RustlerはElixirからNIF経由で安全にrustのモジュールを呼び出すためのライブラリとmix拡張を含めたボイラープレートを作成するパッケージです。
数値・バイナリ・文字列に限らず、タプル・マップ・リスト等Elixir/Erlangのベーシックな型にはほとんど対応しています。
-
- hex 公式ドキュメント
- Github Rustler
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
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箇所コードを追加します。
-
- compilers:行の追加
-
- rustler_crates:行の追加
- rustler_crates関数の追加
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(前述)。
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関数名)
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編」です!