使い慣れている言語が C++ なので、ここでは C++ で説明しますが基本的に C 言語でも同じように記述できるはずです。 というか、世の中の NIF の説明は圧倒的に C 言語が多い。
詳細の解説は置いておいて。コードを書いてから呼び出せるところまでを順にたどってゆきます。
(不備など発見されましたらご指摘いただけると幸いです)
C++ のコードを用意する
二つの整数の和を返す add(int, int)
を定義し、これを Elixir から呼び出すインタフェースを書きます。
#include <erl_nif.h> int add(int x1, int x2) { return x1 + x2; } ERL_NIF_TERM add_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { int x1; int x2; if (!enif_get_int(env, argv[0], &x1)) { return enif_make_badarg(env); } if (!enif_get_int(env, argv[1], &x2)) { return enif_make_badarg(env); } int result = add(x1, x2); return enif_make_int(env, result); } ErlNifFunc nif_funcs[] = { {"add", 2, add_nif} }; ERL_NIF_INIT(Elixir.Calc, nif_funcs, nullptr, nullptr, nullptr, nullptr);
add_nif
が Elixir とインタフェースする関数です。
Elixir から呼び出される関数の型はこの形で定義します。
enif_get_int
で引数を整数型として値を取得します。
戻り値は、演算結果を enif_make_int
で Elixir の整数としての値を構築して返します。
また引数エラーの場合は enif_make_badarg
で例外を構築して返します。
ERL_NIF_INIT
で初期化を定義します。
# | 説明 |
---|---|
1 | モジュール名を指定します。Elixir のモジュール名は VM 上では Elixir. という接頭辞つきで扱われます。ここではその接頭辞つきの名前で指定します。 |
2 | 関数の定義の配列を指定します。個々の定義は、Elixir での関数名, 関数のアリティ, C++ の関数のポインタ の配列からなります。 |
3 | ライブラリがロードされたときに呼び出される関数を指定します。 |
4 | OTP 20 以降利用されなくなりました。 |
5 | モジュールのアップグレイドが発生したときに呼び出される関数を指定します。 |
6 | ライブラリがアンロードされたときに呼び出される関数を指定します。 |
詳しくはドキュメントを参照してください。
C++ のコードをコンパイルする
コードをコンパイルする前に。erl_nif.h
のパスを確認します。
ヘッダファイルは :code.root_dir/0
で取得できるパスの下、 usr/include
に配置されています。
$ ls $(elixir -e 'IO.puts(:code.root_dir())')/usr/include driver_int.h ei.h ei_connect.h eicode.h erl_driver.h erl_drv_nif.h erl_fixed_size_int_types.h erl_int_sizes_config.h erl_interface.h erl_memory_trace_parser.h erl_nif.h erl_nif_api_funcs.h
コンパイルしたライブラリの配置について、はっきりとしたルールを見つけることができなかったのですが、種々のリソースを配置している priv
に置くことにしました。
$ mkdir priv $ g++ -I$(elixir -e 'IO.puts(:code.root_dir())')/usr/include \ -fPIC \ -shared \ -undefined dynamic_lookup \ -o priv/calc.so \ c_src/calc.cpp
C++ で作成したライブラリを Elixir で読み込む
作成したライブラリを利用する Elixir 側のコードです。
ライブラリのロードは :erlang.load_nif/2
を利用します。
第 1 引数にはライブラリの拡張子を除いたパスを指定します。
第 2 引数にはライブラリに渡す値を指定します。
この値は ERL_NIF_INIT
の第 3 引数に指定した関数に渡されます。
ライブラリを配置したディレクトリのパスは :code.priv_dir/1
で取得します。
この関数は _build
ディレクトリの下にある priv
の場所を返してくれます。
_build
ディレクトリの下にある priv
は通常はプロジェクト直下にある priv
へのシンボリックリンクですが、リリース時には実際にディレクトリが作成され priv
の内容がコピーされます。
開発時とリリース時で同じように参照できるので :code.priv_dir/1
を利用してライブラリをロードするようにしています。
またモジュールがロードされたときに自動的にライブラリをロードするように @on_load
でモジュールのロードをフックし、そこでライブラリをロードしています。
defmodule Calc do @on_load :load_nifs def load_nifs do :code.priv_dir(:calc) |> Path.join("calc") |> :erlang.load_nif(0) end def add(_, _) do raise "NIF has not been loaded" end end
ここまで準備ができればあとは Calc
モジュールを Elixir で書いたモジュールと同じように利用することができます。
$ iex -S mix iex(1)> Calc.add(111, 222) 333 iex(2)> Calc.add(1.1, 2.2) ** (ArgumentError) argument error (calc) Calc.add(1.1, 2.2)
C++ のコンパイルを mix compile にまかせる
ここまで C++ のコードは Elixir のコードとは別にコンパイルしましたが、mix compile
コマンドなどで Elixir のコードがコンパイルされるときに一緒にコンパイルされると便利です。
C++ のコンパイルを Elixir のコードで記述し、コンパイル時に実行するコードに含めることでそれを実現できます。
まず Mix.Tasks.Compile.Calc
というモジュールを mix.exs
の中で定義し、run/1
という関数を記述します。
その中で System.cmd/3
を利用して C++ のコンパイルを実行します。
次に project/0
で定義しているキーワードリストに compilers
という項目を追加します。この値はデフォルトでは Mix.compilers/0
が返すリストの値になっていますが、今回記述したモジュールをこのリストに追加します。
defmodule Calc.MixProject do use Mix.Project def project do [ # ... compilers: [:calc | Mix.compilers()] ] end # ... end defmodule Mix.Tasks.Compile.Calc do def run(_argv) do erl_path = Path.join(:code.root_dir(), "usr") include_path = Path.join(erl_path, "include") target_path = Path.join("priv", "calc.so") File.mkdir_p("priv") {result, _} = System.cmd( "g++", [ "-I#{include_path}", "-fPIC", "-undefined", "dynamic_lookup", "-shared", "-o", target_path, "c_src/calc.cpp" ], stderr_to_stdout: true ) IO.puts(result) end end
これでライブラリが作成されていない状態からでも mix compile
や iex -S mix
といった Elixir のコードがコンパイルされるのと合わせてライブラリの作成の実行されるようになりました。
コンパイルの手間を減らすパッケージを利用する
C++ のコードのコンパイルは Makefile で管理することが多いと思いますが、Makefile を利用したコンパイルを手助けしてくれるパッケージが提供されています。
いつか読むはずっと読まない:数論は数学の女王
「黒のマティルデ」の数についてはこちらに詳しく書かれています。