エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

C++ による NIF ことはじめ

使い慣れている言語が 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 compileiex -S mix といった Elixir のコードがコンパイルされるのと合わせてライブラリの作成の実行されるようになりました。

コンパイルの手間を減らすパッケージを利用する

C++ のコードのコンパイルMakefile で管理することが多いと思いますが、Makefile を利用したコンパイルを手助けしてくれるパッケージが提供されています。

いつか読むはずっと読まない:数論は数学の女王

数の女王

数の女王

  • 作者:川添 愛
  • 出版社/メーカー: 東京書籍
  • 発売日: 2019/07/16
  • メディア: 単行本(ソフトカバー)

「黒のマティルデ」の数についてはこちらに詳しく書かれています。