4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AtomVM: 最小 NIF を自作して動かしてみる

Last updated at Posted at 2025-12-13

はじめに

AtomVM 上の Elixir で色々試そうとすると、C で書かれた NIF を扱えることが前提になる場面がありました。

そこで今回は、固定の整数 1234 を返すだけの最小 NIF を自作し、ESP32-S3 向けの AtomVM ファームウェアに組み込んで、Elixir から呼び出すところまでをやってみました。

忘れないうちにメモを残します。

piyopiyo-board-2025-12.png

※ 写真はイメージです

対象環境

  • マイコン
    • 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 の設定で登録が有効になっているか」です。

nifs/sample_app_hello.c
#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.shexport.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 から呼び出すところまでを最小構成で確認しました。

ここまでできたらもうなんでもできるはず。

atomvm-hello-nif.png

🎌 🎌 🎌

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?