NervesでRustの関数をNIFとして呼び出す最小の手順（Rustlerを使用）

この記事は [NervesJP Advent Calendar][advcal-nervesjp] の22日目のエントリーです。Nerves上のErlang VMからRustで書かれたネイティブ関数を呼び出すための最小限の手順を紹介します。
![hello-nerves-with-rustler.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9516/fbac3b36-4d7e-c747-8cca-372caec18577.png)
このスクリーンショットは`lib.rs`ファイルに書かれたRustの`add`関数を、Elixirの`Hello`モジュール経由で呼び出しているところです。

[advcal-nervesjp]: https://qiita.com/advent-calendar/2021/nervesjp


## はじめに

Nervesの特徴のひとつに、他の言語で書かれたプログラムと連携させられることがあります。

Nerves Projectの [公式ホームページ](https://www.nerves-project.org/) より

> **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][bone-original]（オリジナル版。最初に発売された白いボードです）
- 開発マシン
    - Mac mini (M1, 2020)
    - macOS Big Sur 11.6.1 (arm64)

[bone-original]: https://beagleboard.org/bone-original


## 手順

Nerves開発環境はインストール済みとして進めます。もしまだなら [公式サイトの手順][nerves-installation] に従ってインストールしてください。

なお、現時点ではRustlerはクロスコンパイルに完全には対応できてないようで、ワークアラウンドとして、コンパイル時にいくつかのコマンドを手で実行する必要があります。

[nerves-installation]: https://hexdocs.pm/nerves/installation.html


### Rustツールチェーンのインストール

Rustツールチェーンをインストールします。Rustツールチェーンには、Rustコンパイラー（`rustc`）、ビルドツール兼パッケージマネージャーのCargo、標準ライブラリーなどが含まれています。

Webブラウザーで https://rustup.rs にアクセスし、表示された手順に従います。macOSでは、ターミナルから以下のコマンドを入力します（2021年12月現在）

```console
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

質問に対しては全てリターンキーを押すことでデフォルトを選択すればOKです。

イントールに成功したら、ツールチェーンがすぐに使えるように、以下のコマンドを実行します。

```console
$ source $HOME/.cargo/env
```

このコマンドは次回のmacOSログインからは不要です。

なお、通常はこのあとターゲット環境向けのリンカーなどのインストールが必要になるのですが、今回はNervesのツールチェーンに同梱されているものを使用するので、追加のインストールは不要です。


### Nervesプロジェクトの作成

Nervesプロジェクトを作成し、依存パッケージをダウンロードします。`MIX_TARGET`のところは自分のボードに合ったものを指定してください。

```console
$ mix nerves.new hello_nerves
$ cd hello_nerves
$ export MIX_TARGET=bbb  # または rpi3 など
$ mix deps.get
```

### 依存パッケージにRustlerを追加する

依存パッケージに [Rustler Elixirパッケージ](https://hex.pm/packages/rustler) を追加します。

```elixir:mix.exs
   defp deps do
     [
       # https://hex.pm/packages/rustler
       {:rustler, "~> 0.22.2"},
```

### Nervesプロジェクト内にRustパッケージを作成

`mix rustler.new`コマンドを使い、Nervesプロジェクト内にRustによるNIFのパッケージを作成します。「Module name」（モジュール名）はElixirのルールに合わせてキャメルケース（頭文字を大文字）にする必要があります。

```console
$ 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`パッケージが置かれています。

```console
.
├── _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プログラム本体になります。今回は編集せずにこのまま使います。（コメントは筆者が追加しました）

```rust:native/hello/src/lib.rs
// この関数は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`になってますが、これは動的リンクライブラリーを意味します。

```toml:native/hello/Cargo.toml
[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`は修正が必要です。

**修正前**

```config:native/hello/.cargo/config
[target.x86_64-apple-darwin]
rustflags = [
    "-C", "link-arg=-undefined",
    "-C", "link-arg=dynamic_lookup",
]
```

**修正後**

```config:natime/hello/.cargo/config
[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`を作成します。

```elixir: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パッケージの [ドキュメント](https://hex.pm/packages/rustler) を参照してください。

`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`のところはターゲットボードに合わせて変更してください。（後日、詳細を追記予定）

```console
# 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ファームウェアをビルドし、ボードに反映します。

```console
$ 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が呼べることを確認します。

```console
$ ping nerves.local
$ ssh nerves.local

iex(1)> Hello.add(100, 42)
142
iex(2)> 
```

このように結果が表示されれば成功です :tada:　おつかれさまでした！


## まとめ

かなり駆け足の説明になってしまいました。説明不足のところが多いと思いますが、一応、このとおり手を動かせばうまくいくはずです。（数日前に風邪が少し悪化して発熱してしまい、かなりの時間を失ってしまいました。ごめんなさい）

不明な点などあれば、この記事のコメント欄や、SlackのNervesJPコミュニティーなどで筆者（tatsuya6502）に質問してください。（[こちらのページ](https://nerves-jp.connpass.com/) にSlackへの招待リンクがあります）