はじめに
AtomVM 上の Elixir で色々試そうとすると、C で書かれた NIF を扱えることが前提になる場面がありました。
そこで今回は、固定の整数 1234 を返すだけの最小 NIF を自作し、ESP32-S3 向けの AtomVM ファームウェアに組み込んで、Elixir から呼び出すところまでをやってみました。
忘れないうちにメモを残します。
※ 写真はイメージです
対象環境
- マイコン
- ESP32-S3
- ホスト PC
- Debian 系 Linux
- 主なソフトウェア
- AtomVM(0.7.0-dev 系)
- ESP-IDF(v5.5)
- Elixir 1.17(Erlang/OTP 27)
- Python (3.10 以上)
※ 以降の手順は「ESP-IDF の環境がセットアップ済みで idf.py が動く」前提です。
※ Erlang / Elixir / Python は mise でインストールしました。
できたもの
- 固定値
1234を返すだけの C 製 NIF(Elixir 側ではhello/0) - NIF を ESP-IDF のコンポーネントとして組み込んだ、ESP32-S3 向けのカスタム AtomVM ファームウェア
-
hello/0を呼び出すだけの最小 Elixir アプリケーション
最終的にログにはこんな形で出力されます。
Starting application...
NIF said: 1234
Return value: ok
成果は以下のリポジトリにまとめました。
C 側
固定値を返すだけの最小 NIF です。
ここで押さえておきたい点は「NIF 名の文字列が一致しているか」と「Kconfig の設定で登録が有効になっているか」です。
#include "sample_app_hello.h"
#include <context.h>
#include <nifs.h>
#include <portnifloader.h>
#include <term.h>
#include <string.h>
// 追跡ログが欲しいときだけ有効化する(必要なときだけ)
// #define ENABLE_TRACE
#include <trace.h>
// AtomVM は NIF 名を文字列で解決する。
// 形式は "Elixir.モジュール:関数/アリティ"
static term hello_0(Context *ctx, int argc, term argv[])
{
(void) ctx;
(void) argc;
(void) argv;
// 返り値は term_* 系で組み立てる(今回は整数のみ)
return term_from_int(1234);
}
static const struct Nif hello_0_nif = {
.base.type = NIFFunctionType,
.nif_ptr = hello_0
};
// NIF の解決は *_get_nif で行う(名前が一致したら struct Nif を返す)
const struct Nif *sample_app_hello_get_nif(const char *nifname)
{
TRACE("Locating NIF %s ...\n", nifname);
if (strcmp("Elixir.SampleApp.Hello:hello/0", nifname) == 0) {
TRACE("Resolved NIF %s\n", nifname);
return &hello_0_nif;
}
return NULL;
}
static void sample_app_hello_init(GlobalContext *global)
{
(void) global;
}
static void sample_app_hello_destroy(GlobalContext *global)
{
(void) global;
}
// Kconfig の config FOO は C 側では CONFIG_FOO になる。
// ここが一致しないと登録されない。
#ifdef CONFIG_AVM_SAMPLE_APP_HELLO_NIF_ENABLE
REGISTER_NIF_COLLECTION(
sample_app_hello,
sample_app_hello_init,
sample_app_hello_destroy,
sample_app_hello_get_nif
)
#endif
Elixir 側
NIF 以外の要因で落ちないように、Elixir 側は最小限にして切り分けしやすくします。
defmodule SampleApp.Hello do
@moduledoc """
AtomVM 上では `SampleApp.Hello.hello/0` が C の NIF
"Elixir.SampleApp.Hello:hello/0" に差し替わる想定。
"""
@spec hello() :: integer() | :nif_not_loaded
# NIF が読み込まれていない場合はこの値のまま返る。
def hello, do: :nif_not_loaded
end
呼び出し側は、hello/0 を呼ぶだけです。
defmodule SampleApp do
@moduledoc false
def start, do: loop()
defp loop do
IO.puts("NIF said: #{inspect(SampleApp.Hello.hello())}")
Process.sleep(1000)
loop()
end
end
AtomVM 側
AtomVM の ESP32 向けソース(src/platforms/esp32)に、NIF を ESP-IDF のコンポーネントとして追加します。
AtomVM 本体は共通の場所に置き、NIF 側だけをシンボリックリンクで差し込む方針にしました。
このやり方で良いのかどうかは知りません。
my_atomvm_esp32_path="$HOME/Projects/atomvm/AtomVM/src/platforms/esp32"
my_nif_src="$HOME/Projects/atomvm_hello_nif"
my_nif_dest="$my_atomvm_esp32_path/components/atomvm_hello_nif"
# 既存があっても置き換える(-s: symlink, -f: force, -n: treat dest as normal file)
ln -sfn "$my_nif_src" "$my_nif_dest"
ESP-IDF の環境を読み込み、AtomVM(ESP32 向け)をビルドします。
cd "$my_atomvm_esp32_path"
source ~/esp/esp-idf/export.sh
idf.py set-target esp32s3
idf.py build
※ ESP-IDF を公式手順で入れている場合、~/esp/esp-idf 配下に export.sh、export.fish 等が用意されています。
Elixir アプリケーションをビルドする
まずは Elixir 側の .avm(packbeam された成果物)を作ります。
cd ~/Projects/atomvm_hello_nif/examples/hello_nif_elixir
mix deps.get
mix atomvm.packbeam
書き込みは mix atomvm.esp32.flash でもできますが、環境によっては esptool.py の実行権限や参照先の違いで失敗することがありました。
その場合は、次のいずれかで回避できます。
# IDF_PATH の影響を避けて mix atomvm.esp32.flash を使う
env -u IDF_PATH mix atomvm.esp32.flash --port /dev/ttyACM0 --baud 115200
# esptool.py を Python で直接実行する
python ~/esp/esp-idf/components/esptool_py/esptool/esptool.py \
--chip esp32s3 \
--port /dev/ttyACM0 \
-b 115200 \
--before default_reset \
--after hard_reset \
write_flash \
--flash_mode keep \
--flash_freq keep \
--flash_size detect \
0x250000 \
sample_app.avm
動作確認(ログを見る)
書き込み後はシリアルログを見て動作を確認します。ESP-IDF のモニターを使う場合はこちら。
cd ~/Projects/atomvm/AtomVM/src/platforms/esp32
source ~/esp/esp-idf/export.sh
idf.py -p /dev/ttyACM0 monitor
# 終了: Ctrl + ]
ESP-IDF を介さずに軽く見るだけなら picocom 等でも十分です。
picocom /dev/ttyACM0
# 終了: Ctrl + a → Ctrl + x
おわりに
AtomVM の ESP32 向けファームウェアに C 側の最小 NIF を組み込み、Elixir から呼び出すところまでを最小構成で確認しました。
ここまでできたらもうなんでもできるはず。
🎌 🎌 🎌

