This page looks best with JavaScript enabled

RustでErlangVM上で動作するWebアプリケーションを開発する

 ·  ☕ 10 min read  ·  ✍️ さんぽし

こんばんは、さんぽしです

僕はElixir好き好き君なのですが、先日以下のような記事を見かけました。

どちらも「DiscordはElixirを使ってるよ〜」という内容の記事なのですが、詳しく読んでいくと
Rustlerを用いてErlangVMのNIFsを応用することで一部の処理をRustで書いている」
とのことでした。

そもそもNIFsって何という詳しい話は以下の公式のページに説明を譲ります
8. NIFs - Erlang

RustlerはErlang NIFs(Native Implemented Functions)を利用してElixir(Erlang)の中でRustの関数をフックできるようにしたライブラリです。

rusterlium/rustler - GitHub

Erlang/Elixirの中でRustを呼べると何が嬉しいか

Rust開発者側から見るとErlangVMの恩恵を受けることができる点が一番大きなメリットです。(それはそうという感じですね)

Elixirは、低レイテンシで分散型のフォールトトレラントシステムや、Webや組み込みシステムの領域で成功を収めている、Erlang VMを利用します。
https://elixir-lang.jp/

ものすごく堅牢と言われるErlangVMのこれらの強みをRustでまるっといただくことができます。

現状、ある程度の環境が整っていてErlangVMを扱うことができる言語はErlang, Elixirがありますが、Webアプリケーションの開発となるとElixir一択と言ってもいいでしょう。

そんな中でErlangVM上のアプリケーションの開発の選択肢としてRustも入ってくるのはコミュニティにとってもかなり良いことだと感じます(Rustをbeamにコンパイルできるという話ではないのでElixir, Erlangとは完全に別の種別になります。今後Rustler等が発展し開発者から見ればRustのみで開発を行いErlangVMを扱えるという環境になる未来はあるのかもですが、Rust単体でErlangVMを扱えるという話ではないです)

そしてElixir側から見たNIFsを利用してRustを使用するメリットとしては(これはNIFs自体のメリットとも言えますが)、実行速度の向上が言われています。

しかし、Erlangの"The Seven Myths of Erlang Performance"として、以下が挙げられています
2.7 Myth: A NIF Always Speeds Up Your Program

  1. NIFで早くなるかは保証できない
  2. 少なくともdangerousにはなる
  3. NIFの関数を呼ぶこと自体や、戻り値や引数をチェックすることの小さなオーバーヘッドがあるから細々とした関数をチマチマ呼ぶとむしろ遅くなったりするかも

なるほど、という感じですね。

1.に関しては(結局は3と合わせてNIFsの扱い方なんだろうとは思いますが)前述のDiscordの記事も然り、実行速度は早くなるという見方が大勢な気がします。

2に関しては以下の記事が分かりやすかったです

Writing Rust NIFs for your Elixir code with the Rustler package


(引用と和訳)

the fastest way being with a Native Implemented Function (NIF) whose API expects them to be written in C. But speaking frankly, the last time I worked with C involved a lengthy debugging session that boiled down to the lack of type safety, so I’d rather not have to repeat that experience. It’s for this reason that Rust is such a compelling language. It has a robust type system with type inference, pattern matching, and many more features. That and it has a C compatible ABI.

最も速い方法はネイティブ実装関数(NIF)で、そのAPIはC言語で書かれていることを期待しています。しかし率直に言って、前回C言語を使って作業したときは、型の安全性がないことが原因で長時間のデバッグセッションが必要でした。だからこそ、Rust は魅力的な言語なのです。型推論、パターンマッチング、その他多くの機能を備えた堅牢な型システムを持っています。また、C互換のABIを持っています。

This is where the Rustler project comes in. In its own words it provides a safe bridge for writing Erlang NIFs. One of its safety guarantees is catching panics before they reach the C code. One of the nice things about Rust is that if the code compiles, you can be reasonably sure you won’t run into a wide range of memory safety related bugs, among others.

ここでRustlerプロジェクトの出番です。Rustlerの言葉を借りれば、ErlangのNIFを書くための安全なブリッジを提供します。その安全性の保証の一つは、パニックがC言語のコードに到達する前にキャッチすることです。Rustの良いところの一つは、コードがコンパイルされた場合、特にメモリ安全性に関連した広範囲のバグに遭遇しないことを合理的に保証できることです。

(引用と和訳ここまで)


NIFsの「クラッシュした際にErlang VMに対する影響がやばい」という諸刃の剣の諸刃の部分(?)をRustのメモリ安全性を利用して、安全面を担保することができます。

実際にRustlerを使ってみる

長い前置きはここまでにして、Rustlerを実際に用いてみて開発の流れを確認してみましょう。今回は簡単なAPIサーバーを作成してみます。

ElixirのデファクトなWebフレームワークであるPhoenixを利用します。

ElixirやPhoenix、Rust自体のインストール方法は公式に説明を譲り、省略します。

elixir - Install
Phoenix - Installation
Rust - Install Rust

この記事の対象読者は全人類です。Elixir/Phoenixに精通していない人、Rust分からんって人にもわかるように割と一歩一歩解説していきます。
また、筆者はRust歴0日なのでそもそもRust分からんの人間です。

今回のソースコードは全て以下のリポジトリに置いてあります。

sanposhiho/rust-to-elixir-phoenix-sample - GitHub

$ mix phx.new rust_phx_sample --no-ecto

このコマンドでPhoenixのプロジェクトが作成されます(rust_phx_sampleはプロジェクト名です)
途中で何か聞かれたらとりあえずYesにしておきましょう

作成されたプロジェクトのディレクトリに入って以下のコマンドでサーバーを立ち上げます

$ mix phx.server

localhost:4000にアクセスして以下のような画面が表示されればうまいことプロジェクトを作成できています。

PhoenixのTOP

PhoenixはRails likeのMVCなフレームワークです。

とりあえずsampleとして簡単なjsonを返すpathを作成します

以下のようにsample_controller.exsample_view.exを作成します

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
defmodule RustPhxSampleWeb.SampleController do
  use RustPhxSampleWeb, :controller

  def sample(conn, _params) do
    num = add(1, 2)

    render(conn, "sample.json", number: num)
  end

  def add(num1, num2) do
    num1 + num2
  end
end

1
2
3
4
5
6
7
defmodule RustPhxSampleWeb.SampleView do
  use RustPhxSampleWeb, :view

  def render("sample.json", %{number: num}) do
    %{number: num}
  end
end

router.exを編集してroutingを追加します

1
2
3
4
5
6
  scope "/", RustPhxSampleWeb do
    pipe_through :browser

    get "/", PageController, :index
    get "/sample", SampleController, :sample
  end

これによって/sampleにアクセスすると以下のように雑なAPiが作成できていることがわかります

json返している

ここまでで一旦雑なAPIサーバーを立てることができました

rustlerの導入

mix.exsrustlerの依存を追加します

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  defp deps do
    [
      {:phoenix, "~> 1.5.6"},
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:phoenix_live_dashboard, "~> 0.3 or ~> 0.2.9"},
      {:telemetry_metrics, "~> 0.4"},
      {:telemetry_poller, "~> 0.4"},
      {:gettext, "~> 0.11"},
      {:jason, "~> 1.0"},
      {:plug_cowboy, "~> 2.0"},
      {:rustler, "~> 0.21.0"} #add
    ]
  end
$ mix deps.get

これでrustlerを導入できました

mix rustler.newで新しいNIFs用のプロジェクトを作成します

$ mix rustler.new
This is the name of the Elixir module the NIF module will be registered to.
Module name > RustPhxSampleWeb.SampleController 
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (rustphxsampleweb_samplecontroller) > 
* creating native/rustphxsampleweb_samplecontroller/.cargo/config
* creating native/rustphxsampleweb_samplecontroller/README.md
* creating native/rustphxsampleweb_samplecontroller/Cargo.toml
* creating native/rustphxsampleweb_samplecontroller/src/lib.rs
Ready to go! See /Users/kenseinakada/workspace/rust_phx_sample/native/rustphxsampleweb_samplecontroller/README.md for further instructions.

作成されたRustのテンプレートファイルを覗いてみると以下のようになっています

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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.RustPhxSampleWeb.SampleController",
    [
        ("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))
}

Elixir側のaddと実装が合うように以下のように修正します

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use rustler::{Encoder, Env, Error, Term};

rustler::rustler_export_nifs! {
    "Elixir.RustPhxSampleWeb.SampleController",
    [
        ("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((num1 + num2).encode(env))
}

Elixir側のaddでRustのaddを呼び出すように以下のように変更を加えます

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
defmodule RustPhxSampleWeb.SampleController do
  use RustPhxSampleWeb, :controller
  use Rustler, otp_app: :rust_phx_sample, crate: :rustphxsampleweb_samplecontroller

  def sample(conn, _params) do
    num = add(1, 2)

    render(conn, "sample.json", number: num)
  end

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

また、mix.exsを再び編集し、Elixirのコンパイル時にRustのコードも一緒にコンパイルされるように設定します

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  def project do
    [
      app: :rust_phx_sample,
      version: "0.1.0",
      elixir: "~> 1.7",
      elixirc_paths: elixirc_paths(Mix.env()),
      compilers: [:phoenix, :gettext, :rustler] ++ Mix.compilers(), #rustlerの追加
      rustler_crates: rustler_crates(), #追加
      start_permanent: Mix.env() == :prod,
      aliases: aliases(),
      deps: deps()
    ]
  end

  defp rustler_crates() do
    [rustphxsampleweb_samplecontroller: [
      path: "native/rustphxsampleweb_samplecontroller",
      mode: rustc_mode(Mix.env)
    ]]
  end

  defp rustc_mode(:prod), do: :release
  defp rustc_mode(_), do: :debug

これによってPhoenixのサーバーを立ち上げ直すことでRustのコンパイルも実行されます

$ mix phx.server

同様に/sampleにアクセスすることで以下のように先ほどと同じ結果が帰ってきていることがわかります
json返している

これによってElixirのaddの関数をNIFに置き換えてRustのaddを代わりに実行することができました。

もう少し本格的なAPIサーバーを実装してみる

ここまででRustlerの雰囲気は掴んでいただけたのではないでしょうか。

「いや足し算の関数置き換えただけでRustでWebアプリケーションって言えるんか」

そう言われると思ったので、そこでここからAPIサーバーが行うことの多いであろう、DBへのアクセスを絡めた処理を実際にRustで実装してみます

先ほどと同様の手順でuser_controller.exuser_view.exを作成し、router.exにrouteを追加します

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
defmodule RustPhxSampleWeb.UserController do
  use RustPhxSampleWeb, :controller
  use Rustler, otp_app: :rust_phx_sample, crate: :rustphxsampleweb_usercontroller

  def create(conn, %{"user" => %{"name" => name, "age" => age}}) do
    {:ok, {id, name, age}} = create_user(name, age)

    user = %{
      id: id,
      name: name,
      age: age,
    }

    render(conn, "user.json", user: user)
  end

  def create_user(_name, _age), do: exit(:nif_not_loaded)
end
1
2
3
4
5
6
7
defmodule RustPhxSampleWeb.UserView do
  use RustPhxSampleWeb, :view

  def render("user.json", %{user: user}) do
    %{user: user}
  end
end
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    # plug :protect_from_forgery  # セキュリティ上ものすごく良くないがCSRFToken Errorを回避するのがめんどくさいので今回は無効化
    plug :put_secure_browser_headers
  end

#..(中略)
    post "/user", UserController, :create

同様の手順で新しいNIFs用のプロジェクトを作成します

$ mix rustler.new
This is the name of the Elixir module the NIF module will be registered to.
Module name > RustPhxSampleWeb.UserController
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (rustphxsampleweb_usercontroller) >
* creating native/rustphxsampleweb_usercontroller/.cargo/config
* creating native/rustphxsampleweb_usercontroller/README.md
* creating native/rustphxsampleweb_usercontroller/Cargo.toml
* creating native/rustphxsampleweb_usercontroller/src/lib.rs
Ready to go! See /Users/kenseinakada/workspace/rust_phx_sample/native/rustphxsampleweb_usercontroller/README.md for further instructions.

mix.exsにも作成しNIFs用のプロジェクトを登録します

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  defp rustler_crates() do
    [rustphxsampleweb_samplecontroller: [
      path: "native/rustphxsampleweb_samplecontroller",
      mode: rustc_mode(Mix.env)
    ],
    rustphxsampleweb_usercontroller: [                   # add
      path: "native/rustphxsampleweb_usercontroller",
      mode: rustc_mode(Mix.env)
    ]
    ]
  end

そして作成されたnative/rustphxsampleweb_usercontroller/src/lib.rsにDBにUserを格納する処理を書いていきます。

DBクライアントライブラリにはDieselを使用しました。

Rustの実際のコードは長いので載せませんが、以下に置いてあります。

sanposhiho/rust-to-elixir-phoenix-sample:native/rustphxsampleweb_usercontroller/src

実際にmix phx.serverをしてサーバーを立てて、curlでAPIを叩いてみます。

$ curl -X POST  -H "Content-Type: application/json" -d '{"user":{"name":"taro", "age":14}}' localhost:4000/user
{"user":{"age":1,"id":1,"name":"taro"}}

$ curl -X POST  -H "Content-Type: application/json" -d '{"user":{"name":"miho", "age":12}}' localhost:4000/user
{"user":{"age":1,"id":2,"name":"miho"}}

しっかりレスポンスが返ってきました。

今回はReadのAPIを立てていないので直接DBを覗きに行くと

diesel_demo=> select * from users;
 id | name | age
----+------+-----
  1 | taro |   14
  2 | miho |   12
(2 rows)

ちゃんとWriteされていることがわかります

Rustlerを使ってみて

RustlerはRust側の関数とElixir側の関数を紐付けるだけでかなりRust側に処理を任せることができることがわかりました。
Phoenixにルーティングとレスポンスの構築のみを任せて内部の処理を全てRustに置き換えるようなことも実現が可能そうな感じがします。まあ、冒頭でもお話ししたようにNIFsは適材適所な面も大きいため、全てをRustに置き換えれればハッピーということもないのかもしれません。

やはりElixir/Phoenix側のコードを少なからずいじる必要もあるので、Rustだけを知っている人がElixir/Phoenixを利用してErlangVM上でRustを動作させるということは現状少しハードルがある感じがします。
この辺をうまく吸収し、RustだけでErlangVM上で動作するアプリケーションを作成できるようになったら面白いかもしれませんね。

Rustlerは現在v0.21.0が最新リリースです。メジャーバージョンが待ち遠しいです。
読んでいただきありがとうございました。

Share on

さんぽし
WRITTEN BY
さんぽし
Web Developer /w Elixir, Go