この記事は NervesJP Advent Calendar の22日目のエントリーです。Nerves上のErlang VMからRustで書かれたネイティブ関数を呼び出すための最小限の手順を紹介します。
このスクリーンショットはlib.rs
ファイルに書かれたRustのadd
関数を、ElixirのHello
モジュール経由で呼び出しているところです。
はじめに
Nervesの特徴のひとつに、他の言語で書かれたプログラムと連携させられることがあります。
Nerves Projectの 公式ホームページ より
Scalable
スケーラブルNerves is written in Elixir, but you don’t have to rewrite everything in Elixir to get the advantages of Nerves — simply bring your own code (like C, C++, Python, Rust, and more) and scale up.
NervesはElixirで書かれていますが、Nervesの利点を得るために全てをElixirで書き直す必要はありません。自分のコード(C、C++、Python、Rustなど)を持ち込むだけでスケールアップできます。
この記事ではRustの関数を呼んでみます。Rustの関数をErlang VM(BEAM)のNative Implemented Functions(NIF)として実装し、BEAMプロセス内で実行します。Rustlerというパッケージを使用します。
Rustのコードは開発マシン上でターゲットボードの動的リンクライブラリーへとクロスコンパイルし、Nervesのファームウェアに埋め込みます。
macOSでの手順になります。筆者の環境は以下のとおりです。
筆者の環境
- フレームワーク
- Nerves v1.7.12
- Rustler v0.22.2
- プログラミング言語
- Elixir 1.12.3-otp-24
- Erlang 24.1.5
- Rust 1.57.0
- ターゲットボード
- BeagleBone(オリジナル版。最初に発売された白いボードです)
- 開発マシン
- Mac mini (M1, 2020)
- macOS Big Sur 11.6.1 (arm64)
手順
Nerves開発環境はインストール済みとして進めます。もしまだなら 公式サイトの手順 に従ってインストールしてください。
なお、現時点ではRustlerはクロスコンパイルに完全には対応できてないようで、ワークアラウンドとして、コンパイル時にいくつかのコマンドを手で実行する必要があります。
Rustツールチェーンのインストール
Rustツールチェーンをインストールします。Rustツールチェーンには、Rustコンパイラー(rustc
)、ビルドツール兼パッケージマネージャーのCargo、標準ライブラリーなどが含まれています。
Webブラウザーで https://rustup.rs にアクセスし、表示された手順に従います。macOSでは、ターミナルから以下のコマンドを入力します(2021年12月現在)
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
質問に対しては全てリターンキーを押すことでデフォルトを選択すればOKです。
イントールに成功したら、ツールチェーンがすぐに使えるように、以下のコマンドを実行します。
$ source $HOME/.cargo/env
このコマンドは次回のmacOSログインからは不要です。
なお、通常はこのあとターゲット環境向けのリンカーなどのインストールが必要になるのですが、今回はNervesのツールチェーンに同梱されているものを使用するので、追加のインストールは不要です。
Nervesプロジェクトの作成
Nervesプロジェクトを作成し、依存パッケージをダウンロードします。MIX_TARGET
のところは自分のボードに合ったものを指定してください。
$ mix nerves.new hello_nerves
$ cd hello_nerves
$ export MIX_TARGET=bbb # または rpi3 など
$ mix deps.get
依存パッケージにRustlerを追加する
依存パッケージに Rustler Elixirパッケージ を追加します。
defp deps do
[
# https://hex.pm/packages/rustler
{:rustler, "~> 0.22.2"},
Nervesプロジェクト内にRustパッケージを作成
mix rustler.new
コマンドを使い、Nervesプロジェクト内にRustによるNIFのパッケージを作成します。「Module name」(モジュール名)はElixirのルールに合わせてキャメルケース(頭文字を大文字)にする必要があります。
$ mix deps.get
$ mix rustler.new
...
==> hello_nerves
This is the name of the Elixir module the NIF module will be registered to.
Module name > Hello
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (hello) >
* creating native/hello/.cargo/config
* creating native/hello/README.md
* creating native/hello/Cargo.toml
* creating native/hello/src/lib.rs
* creating native/hello/.gitignore
Ready to go! See ... /hello_nerves/native/hello/README.md for further instructions.
Rustパッケージ内のファイルを編集する
この時点で大まかなディレクトリー構成は以下のようになります。mix rustler.new
コマンドによってnative
ディレクトリーが作られ、その中にRustのhello
パッケージが置かれています。
.
├── _build
├── config
│ ├── config.exs
│ ├── host.exs
│ └── target.exs
├── deps
├── lib
│ ├── hello_nerves
│ │ └── application.ex
│ └── hello_nerves.ex
├── mix.exs
├── mix.lock
├── native # mix rustler.newにより作成された
│ └── hello
│ ├── .cargo
│ │ └── config
│ ├── Cargo.toml
│ ├── README.md
│ └── src
│ └── lib.rs # Rustプログラムのひな形
├── README.md
├── rel
├── rootfs_overlay
└── test
関連するファイルを確認して、そのいくつかを編集していきましょう。
src/lib.rs
native/hello/src/lib.rs
がRustプログラム本体になります。今回は編集せずにこのまま使います。(コメントは筆者が追加しました)
// この関数はNIFとしてエクスポートされる
#[rustler::nif]
fn add(a: i64, b: i64) -> i64 {
a + b
}
// Elixir側のモジュールHelloに、add関数を登録する
rustler::init!("Elixir.Hello", [add]);
add
という関数が定義されており、#[rustler::nif]
属性によって、この関数をNIFとしてエクスポートすることを指示しています。またrustler::init!
を使ってElixir側のモジュールとの結び付けています。
Cargo.toml
native/hello/Cargo.toml
の内容も基本的に編集せずに使えます。crate-type
(クレートタイプ)がcdylib
になってますが、これは動的リンクライブラリーを意味します。
[package]
name = "hello"
version = "0.1.0"
authors = []
edition = "2018" # "2021" に変えてもいいかもしれない
[lib]
name = "hello"
path = "src/lib.rs"
crate-type = ["cdylib"]
[dependencies]
rustler = "0.22.2"
.cargo/config
native/hello/.cargo/config
は修正が必要です。
修正前
[target.x86_64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
修正後
[target.'cfg(target_os = "macos")'] # 変更
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
[target.armv7-unknown-linux-gnueabihf] # 追加
linker = "armv7-nerves-linux-gnueabihf-gcc" # 追加
最初の「変更」のところはmacOSのホスト環境でIntelプロセッサ搭載Macだけでなく、Apple silicon(M1など)搭載Macでも正しく動かすためのものです。
最後の「追加」のところはリンカーとしてNervesに含まれているツールチェーンのGCCを指定します。ここはターゲットボードによって書く内容が変わります。(後日、詳細を追記予定)
lib/hello.ex
以下の内容でlib/hello.ex
を作成します。
defmodule Hello do
use Rustler,
otp_app: :hello_nerves,
crate: :hello,
# Rustパッケージのコンパイルをスキップする
skip_compilation?: true
# 以下はコンパイルをスキップしないときに必要になりそうな設定
# mode: :release,
# target: "armv7-unknown-linux-gnueabihf"
# この関数はNIFで上書きされる
@spec add(integer(), integer()) :: integer()
def add(_a, _b), do: exit(:nif_not_loaded)
end
use Rustler
の各設定項目について、詳細はRustler Elixirパッケージの ドキュメント を参照してください。
skip_compilation?: true
はmix compile
時にRust側のコンパイルをしないようにする設定です。本来はRustlerがmix compile
時にRustコードのコンパイルもしてくれるのですが、後処理に問題があるようで動的リンクライブラリーのリネームで失敗してしまいます。(Rustler v0.22.2)
Rustパッケージのビルド
Rustパッケージをビルドします。cargo build
に--target
を指定することでクロスコンパイル+クロスリンクを行います。armv7_*_linux_gnueabihf
やarmv7-*-linux-gnueabihf
のところはターゲットボードに合わせて変更してください。(後日、詳細を追記予定)
# Nervesによってインストールされたクロスリンク用のツールチェーンにPATHを通す
$ CROSS=$HOME/.nerves/artifacts/nerves_toolchain_armv7_nerves_linux_gnueabihf-darwin_arm-1.4.3
$ export PATH=$PATH:$CROSS/bin:$CROSS/armv7-nerves-linux-gnueabihf/bin
# Rustパッケージ(NIF)をビルドする
$ cd native/hello/
$ cargo build --release --target armv7-unknown-linux-gnueabihf
$ cd ../..
# 動的リンクライブラリーを所定の場所へコピーする
$ mkdir -p priv/native
$ cp -p native/hello/target/armv7-unknown-linux-gnueabihf/release/libhello.so priv/native/
Rustlerのクロスコンパイルの不具合のようなものが解決したら、このあたりの手順は不要になるはずです。
Nervesファームウェアのビルド
Nervesファームウェアをビルドし、ボードに反映します。
$ export MIX_TARGET=bbb
$ mix firmware
# 以下のwarningは無視する。(macOSのErlang VMがlibhello.soをロードできないためにエラーになる)
HH:MM:SS.SSS [warn] The on_load function for module Elixir.Hello returned:
{:error,
{:load_failed,
'Failed to load NIF library: \'dlopen(... /hello_nerves/_build/bbb_dev/lib/hello_nerves/priv/native/libhello.so, 2): no suitable image found. Did find:\n\t ... /hello_nerves/_build/bbb_dev/lib/hello_nerves/priv/native/libhello.so: unknown file type, first eight bytes: 0x7F 0x45 0x4C 0x46 0x01 0x01 0x01 0x00\n\t ... /hello_nerves/_build/bbb_dev/lib/hello_nerves/priv/native/libhello.so: stat() failed with errno=35\''}}
# ボードに反映する
$ mix burn # または mix upload
Nerves上でRustの関数を呼んでみる
ssh
でボードに接続し、NIFが呼べることを確認します。
$ ping nerves.local
$ ssh nerves.local
iex(1)> Hello.add(100, 42)
142
iex(2)>
このように結果が表示されれば成功です おつかれさまでした!
まとめ
かなり駆け足の説明になってしまいました。説明不足のところが多いと思いますが、一応、このとおり手を動かせばうまくいくはずです。(数日前に風邪が少し悪化して発熱してしまい、かなりの時間を失ってしまいました。ごめんなさい)
不明な点などあれば、この記事のコメント欄や、SlackのNervesJPコミュニティーなどで筆者(tatsuya6502)に質問してください。(こちらのページ にSlackへの招待リンクがあります)