こんばんは、今年は受験生なのでクリスマスでも勉強していたtechnoです。というか試験が年明けにあるので大学入ってからクリスマスとか正月なんてものはなくなっています。来年からは多分この状況から抜け出せると思うのですが。
前置きはともかく、elixirでアプリを作っていたのですが、httpoisonやhttpotionが特定のケースで変な挙動を起こして使うことができなかったため、rustlerを使ってrustのreqwestを使った時のノウハウです。
プロジェクトのコードはこちらにあります。
rustlerのセットアップ
mixでelixirのプロジェクトを作ったら(今回は"rustler_docker"という名前にしました)早速mix.exsを編集していきます。
defmodule RustlerDocker.MixProject do
use Mix.Project
def project do
[
app: :rustler_docker, # ここのアプリ名は後で使う
version: "0.1.0",
elixir: "~> 1.7",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:rustler, "~> 0.18.0"} # 追加
]
end
end
deps.getしてからrustライブラリを生成します。
$ mix deps.get
$ mix rustler.new
This is the name of the Elixir module the NIF module will be registered to.
Module name > Client
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (client) > client
この時のモジュールの名前は主にelixir側のモジュールの名前になります。
ライブラリの名前は主にrust側の名前になります。
この時点でrust側のサンプル実装は終わっているのでElixir側のコードを書いていきます。まずはmix.exsの変更
defmodule RustlerDocker.MixProject do
use Mix.Project
def project do
[
app: :rustler_docker,
version: "0.1.0",
elixir: "~> 1.7",
compilers: [:rustler] ++ Mix.compilers, # 変更
rustler_crates: rustler_crates(), # 追加
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:rustler, "~> 0.18.0"}
]
end
# 追加
defp rustler_crates() do
[
# rustライブラリの名前
client: [
path: "native/client", # native/#{rustライブラリの名前}
mode: (if Mix.env == :prod, do: :release, else: :debug)
]
]
end
end
# mix rustler.newで入力したモジュールの名前
defmodule Client do
# opt_appはmix.exsのproject.appの名前
# crateはrustler.newで入力したライブラリの名前
use Rustler, otp_app: :rustler_docker, crate: :client
def add(_a, _b), do: exit(:nif_not_loaded)
end
add
という関数は生成されたrustライブラリに定義してある関数を示しています。
この時点でiexからrustのadd
を呼び出せるはずです。
iex(1)> Client.add(1, 3)
{:ok, 4}
これでrustlerが使えるようになりました。
rustでやりたいことを書く
冒頭で書いた通り、rustでhttpリクエストを行うコードを書きます。
Cargo.tomlにreqwestの依存を追加して、ソースを変更します。
#[macro_use] extern crate rustler;
#[macro_use] extern crate rustler_codegen;
#[macro_use] extern crate lazy_static;
extern crate reqwest; // 追加
use rustler::{Env, Term, NifResult, Encoder};
mod atoms {
rustler_atoms! {
atom ok;
atom error; // コメントイン
//atom __true__ = "true";
//atom __false__ = "false";
}
}
rustler_export_nifs! {
"Elixir.Client",
[
("get_body", 1, get_body) // 変更
],
None
}
// 追加
fn get_body<'a>(env: Env<'a>, args: &[Term<'a>]) -> NifResult<Term<'a>> {
let url: String = try!(args[0].decode());
match reqwest::get(&url) {
Ok(mut res) => {
let body = res.text().unwrap();
Ok((atoms::ok(), body).encode(env))
},
Err(err) => {
Ok((atoms::error(), err.to_string()).encode(env))
}
}
}
reqwest::get
を呼んで結果を返しているだけですね。(簡単のためにunwrap
を使っているのであまり良くない)
rust側の実装が終わったらelixir側も変更します。
defmodule Client do
use Rustler, otp_app: :rustler_docker, crate: :client
def get_body(_url), do: exit(:nif_not_loaded)
end
後はこのClient
モジュールを使っていろいろすれば良いですね。
dockerで動かす。
以上のプロジェクトをdockerで動かします。
elixir:alpineを使いたかったのですが、
- reqwestが使っているencoding_rsが比較的新しいrustcを要求している
- alpineでは新しいrustcはedgeを使わないといけない
- elixir:alpineはedgeではない
- alpine:edgeにelixirを入れるのは面倒そう
という問題があり、良い方法が思いつかなかったのでelixir:slimの上で動かしました。
alpine3.8にedgeのrustc, cargoを入れられれば解決しそうですが、今回は見送ります(追記: できました)。
rustライブラリのビルドに必要なものを入れるとイメージが肥大するので、最初のステージでrustライブラリのビルドだけをして、それをelixirプロジェクトに載せる方針でいきます。しかし、rustlerは実行時にrustプロジェクトのコンパイルをしようとするためそのままではうまく行きません。mix.exsに細工をして環境変数によってrustライブラリをコンパイルするか指定します。
defmodule RustlerDocker.MixProject do
use Mix.Project
def project do
[
app: :rustler_docker,
version: "0.1.0",
elixir: "~> 1.7",
compilers: compilers(), # 変更
rustler_crates: rustler_crates(),
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:rustler, "~> 0.18.0"}
]
end
# 追加
defp compilers do
if System.get_env("NO_RUSTLER_COMPILE") do
Mix.compilers
else
[:rustler] ++ Mix.compilers
end
end
defp rustler_crates() do
[
client: [
path: "native/client",
mode: (if Mix.env == :prod, do: :release, else: :debug)
]
]
end
end
FROM elixir:1.7.4-slim
RUN apt update && apt upgrade -y
RUN apt install -y --no-install-recommends curl build-essential libssl-dev pkg-config ca-certificates
RUN apt clean
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable --no-modify-path
ENV PATH=/root/.cargo/bin/:$PATH
WORKDIR /work
COPY native/client .
RUN cargo rustc --release
FROM elixir:1.7.4-slim
RUN apt update && apt upgrade -y
RUN apt install -y ca-certificates libssl-dev
WORKDIR /app
ENV MIX_ENV=prod
ENV NO_RUSTLER_COMPILE=true
COPY mix.exs .
COPY mix.lock .
RUN mix local.hex --force
RUN mix local.rebar --force
RUN mix deps.get
RUN mix deps.compile
COPY . .
RUN mix compile
COPY --from=0 /work/target/release/libclient.so priv/native/libclient.so
イメージのサイズを削減するために最初のステージでrustコンパイラの導入とrustライブラリ(libclient.so)をビルドをして、それをelixirプロジェクトに載せています。記事を書いていて思ったのですが、Cargo.toml, Cargo.lockは先に追加して依存の解決だけしておいた方が良さそう。
/.git
/_build
/deps
/native/client/target
/priv/native
あとはできたイメージを実行してiexを起動し、Client.get_body("https://google.co.jp")
とでも打ってみると動くと思います。
追記
elixir:alpineにalpine:edgeのcargo, rustを入れることができたので詳しくはDockerfileを見てください。この場合nightlyが使えないのでnightlyを使いたい場合は結局rustupを入れる必要がありそうです。
できたイメージの大きさは132MBでした。elixir:alpineのイメージサイズが86.9MBなので結構大きくなってしまいましたが、まぁこんなものでしょう。