Adding custom Nifs, Ports, and third-party components を読みつつ進めていきます。
ドキュメントを読んでわかることは
- NIF を追加するドキュメントは ESP32 向けにしかない
- Generic UNIX 向け NIF 対応は執筆時点ではありませんでした
- 追加する NIF は BEAM の場合と異なり静的リンクになる
- BEAM の場合は共有オブジェクト *.so なので、動的リンクです
- ESP32 向けの AtomVM はターゲットに書き込むので、静的リンクとするのは自然です
Generic UNIX 向けの AtomVM で NIF 対応ができるとホストで動作確認できる範囲が増えるので嬉しさはあります。しかし、実装する上での判断として動的リンク・静的リンクにするか、どのようなディレクトリ構造をとるかは考えなければなりませんし、AtomVM のメインターゲットはホストでないので、対応されるかはわかりません。
この記事ではESP32向けのAtomVMにNIFを追加する方法を掘っていきいたいと思います。
ESP-IDF の components ディレクトリ
Adding the custom Nif or Port to the components directory of the AtomVM source tree.
とAtomVM のドキュメントにあることから、 ESP-IDF の components ディレクトリの利用の仕方を学ぶ必要がありそうです。
ESP-IDF の Build System のドキュメントを読んでいけばいいのですが量が多いので気になる部分だけかいつまみます。
EXTRA_COMPONENT_DIRS
作成する NIF は component 扱いとしますが、AtomVM のソースとは別で管理したいです。
なぜなら、ソース管理を分けられれば、AtomVM と NIF をそれぞれ独立にバージョンアップをさせることができるからです。
調べてみると EXTRA_COMPONENT_DIRS を使うことができそうです。
EXTRA_COMPONENT_DIRS: Optional list of additional directories to search for components. Paths can be relative to the project directory, or absolute.
ref. Optional Project Variables
AtomVM をビルドする際に以下のようにすることで指定できるようです。
idf.py \
-DEXTRA_COMPONENT_DIRS="/path/components1;/path/components2;/path/components3" \
build
この -D は CMake のオプションです。
ref. https://cmake.org/cmake/help/v3.30/manual/cmake.1.html#cmdoption-cmake-D
これが使えるかはおいてあとで確認することにして、まずは NIF の実装とコンポーネント化をします。
NIF 実装とコンポーネント化
Adding a custom AtomVM Nif では書き方が分からないので、実際に使われている NIF 実装を調べてみます。
上記ドキュメントより、 NIF として登録しているのであれば REGISTER_NIF_COLLECTION を呼んでいるので、これを grep します。
$ pwd
/home/pojiro/Sandbox/AtomVM/src/platforms/esp32
$ rg REGISTER_NIF_COLLECTION .
...
components/avm_builtins/ledc_nif.c
625:REGISTER_NIF_COLLECTION(ledc, NULL, NULL, ledc_nif_get_nif)
...
出力から NIF 実装がいくつもあることがわかりますが、ファイル名が一番とっつきやすそうな ledc_nif.c を参考にして最小の NIF を書いてみます。
:ok アトムを返すだけの最小 NIF
my_atomvm_nif というディレクトリに NIF 実装である my_atomvm_nif.c を作ります。
$ pwd
/home/pojiro/Sandbox
$ mkdir my_atomvm_nif
$ touch my_atomvm_nif/my_atomvm_nif.c
まずは、 :ok アトムを返す関数を実装します。
// OK_ATOM が定義されているヘッダ
#include <defaultatoms.h>
// Nif 構造体が定義されているヘッダ
#include <nifs.h>
// REGISTER_NIF_COLLECTION が定義されているヘッダ
#include <portnifloader.h>
// strcmp が定義されているヘッダ
#include <string.h>
// term 型が定義されているヘッダ
#include <term.h>
// NIF 関数実体
static term my_atomvm_nif_ok(Context *ctx, int argc, term argv[])
{
return OK_ATOM;
}
// NIF 関数を保持する構造体
static const struct Nif my_atomvm_nif_ok_nif = {
.base.type = NIFFunctionType,
.nif_ptr = my_atomvm_nif_ok // NIF 関数実体のポインタ
};
// NIF の呼び出しと関数実体を紐付ける関数
const struct Nif *my_atomvm_nif_get_nif(const char *nifname)
{
if (strcmp("my_atomvm_nif:ok/0", nifname) == 0 ||
strcmp("Elixir.MyAtomvmNif:ok/0", nifname) == 0) {
return &my_atomvm_nif_ok_nif;
}
return NULL;
}
// NIF を登録するマクロ
REGISTER_NIF_COLLECTION(my_atomvm_nif, NULL, NULL, my_atomvm_nif_get_nif)
必要最低限の実装です。
コンポーネント化
ESP-IDF は CMake を利用しているので、 コンポーネントにするために CMakeLists.txt を書く必要があります。
$ pwd
/home/pojiro/Sandbox
$ touch my_atomvm_nif/CMakeLists.txt
これも /home/pojiro/Sandbox/AtomVM/src/platforms/esp32/components/avm_builtins/CMakeLists.txt を参考に必要最低限を書きます。
idf_component_register(
# ソース
SRCS "my_atomvm_nif.c"
# 依存関係
PRIV_REQUIRES "libatomvm"
# オブジェクトファイルを強制的にリンクするオプション
WHOLE_ARCHIVE
# 実装した NIF は外部 API を持たず AtomVM から呼び出されないため、
# リンカがオブジェクトファイルを不要と判断してリンクされません。
)
WHOLE_ARCHIVE を指定すると、
if specified, the component library is surrounded by -Wl,--whole-archive
--whole-archive オプションがリンカに渡されます。
ここまでで NIF 実装とコンポーネント化ができました。ビルドします。
AtomVM のビルド
NIF をビルドするということは AtomVM の場合は NIF を静的リンクで AtomVM 自体に組み込むことを意味します。よって、 AtomVM の再ビルドが必要です。
$ cd /home/pojiro/Sandbox/AtomVM/src/platforms/esp32/
$ source "/home/pojiro/.espressif/tools/activate_idf_v5.5.5.sh"
$ idf.py -DEXTRA_COMPONENT_DIRS="/home/pojiro/Sandbox/my_atomvm_nif" build
Executing action: all (aliases: build)
Running ninja in directory /home/pojiro/Sandbox/AtomVM/src/platforms/esp32/build
Executing "ninja all"...
[0/1] Re-running CMake...
...
-- Component idf::my_atomvm_nif will be linked with -Wl,--whole-archive
-- Component idf::avm_builtins will be linked with -Wl,--whole-archive
...
[4/5] Completed 'bootloader'
Project build complete. To flash, run:
idf.py flash
or
idf.py -p PORT flash
or
...
以下の表示があり、
-- Component idf::my_atomvm_nif will be linked with -Wl,--whole-archive
-DEXTRA_COMPONENT_DIRS="/home/pojiro/Sandbox/my_atomvm_nif" オプションにより、コンポーネントが認識されていること、 WHOLE_ARCHIVE が効いていることがわかります。
指示通り書き込み、
$ idf.py -p /dev/ttyACM0 flash
...
出力を確認します。(結線はM5STACK CORE S3 での開発手順のままです。
$ source "/home/pojiro/.espressif/tools/activate_idf_v5.5.5.sh"
...
$ idf-monitor -p /dev/ttyUSB0
...
AtomVM init.
I (702) sys: Loaded BEAM partition main.avm at address 0x250000 (size=1048576 bytes)
Starting application...
Hello World
Console.puts() and Console.print() work with binary or charlist strings.
Return value: ok
I (732) AtomVM: AtomVM application terminated. Going to sleep forever ...
出力はAtomVM を ESP32S3 向けにビルドして動かすから変化はありません。
なぜなら、AtomVM を書き換えただけであり、呼び出し側アプリの avm は書き換えてないからです。
アプリから NIF を呼び出す
AtomVM を ESP32S3 向けにビルドして動かす際のコードを以下のように更新します。
$ pwd
/home/pojiro/Sandbox/my_atomvm_project
$ vim lib/my_atomvm_project.ex
Erlang 形式の呼び出しとElixir 形式の呼び出しをして、結果を出力するようにしています。
--- a/lib/my_atomvm_project.ex
+++ b/lib/my_atomvm_project.ex
@@ -1,11 +1,9 @@
defmodule MyAtomvmProject do
# this compiler option is to suppress warnings when compiling the VM
# it is not needed or recommended for user apps.
- @compile {:no_warn_undefined, [Console]}
+ @compile {:no_warn_undefined, [Console, :my_atomvm_nif, MyAtomvmNif]}
def start() do
- Console.print("Hello World\n")
- Console.puts("Console.puts() and Console.print() work with binary ")
- Console.puts(~c"or charlist strings.\n")
- Console.flush()
+ ":my_atomvm_nif.ok() returns #{inspect(:my_atomvm_nif.ok())}\n" |> Console.puts()
+ "MyAtomvmNif.ok() returns #{inspect(MyAtomvmNif.ok())}\n" |> Console.puts()
end
end
AtomVM 向け Elixir Project を作るの追記の mix flash を使って M5STACK CORE S3 にアプリを書き込みます。
$ mix flash
mix flash
Compiling 1 file (.ex)
Generated my_atomvm_project app
Warning: following modules or functions are not available on AtomVM:
* Elixir.MyAtomvmNif:ok/0
* my_atomvm_nif:ok/0
(Using them may not be supported; make sure ExAtomVM is fully updated.)
...
Writing at 0x00348000... (100 %)
Wrote 1032192 bytes at 0x00250000 in 8.8 seconds (933.7 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
my_atomvm_nif.beam や Elixir.MyAtomvmNif.beam が my_atomvm_project.avm にないため以下が表示されています。
Warning: following modules or functions are not available on AtomVM:
* Elixir.MyAtomvmNif:ok/0
* my_atomvm_nif:ok/0
呼び出し結果はどうなったでしょうか、 idf-monitor -p /dev/ttyUSB0 実行中のターミナルを見ましょう。
AtomVM init.
I (702) sys: Loaded BEAM partition main.avm at address 0x250000 (size=1048576 bytes)
Starting application...
:my_atomvm_nif.ok() returns :ok
MyAtomvmNif.ok() returns :ok
Return value: ok
I (732) AtomVM: AtomVM application terminated. Going to sleep forever ...
NIF が呼び出せており、 :ok アトムが返っていることが確認できます。
Warning: following modules or functions are not available on AtomVM:
* Elixir.MyAtomvmNif:ok/0
* my_atomvm_nif:ok/0
が表示されていましたが、名前は解決されて呼び出せています。
defmodule MyAtomvmNif do
def ok, do: :nif_not_loaded
end
を定義して、パックすれば
$ mix atomvm.packbeam
Warning: following modules or functions are not available on AtomVM:
* my_atomvm_nif:ok/0
...
Warning を減らすことができますが、 avm 内の beam が増えるだけであまり意味がないように思います。どのような beam が avm に含まれているかは packbeam コマンドを使って確認できます。
$ pwd
/home/pojiro/Sandbox/my_atomvm_project
$ /home/pojiro/Sandbox/AtomVM/build/tools/packbeam/packbeam list my_atomvm_project.avm
...
Elixir.MyAtomvmNif.beam [576]
...
今回は AtomVM に NIF を追加して実行することができました。
今日はここまで。