C++ のライブラリを Elixir から利用したくなりました。
NIF を使った単純な関数の呼び出しは実装したことがあるのですが、オブジェクトを利用するようなケースは試したことがなかったので、今回調べてみました。
今回のコードではエラーハンドリングなどは省略していますのでご注意ください。 不備がありましたらご指摘いただけるとありがたいです。
またここで実装したファイルは Gist にもアップしていますので合わせてご参照ください。
NIF環境の用意
elixir_make
mix compile
コマンドで C++ のコードがいっしょにコンパイルされるように、elixir_make を導入します。
まず依存パッケージとして mix.exs
に elixir_make
を追加します。
このパッケージはコンパイル時のみ利用するので [runtime: false]
のオプションを指定します。
またコンパイラのリストに :elixir_make
を追加して、 mix compile
実行時に合わせて実行されるようにします。
defmodule Counter.MixProject do use Mix.Project def project do [ app: :counter, version: "0.1.0", elixir: "~> 1.11", start_permanent: Mix.env() == :prod, deps: deps(), compilers: [:elixir_make | Mix.compilers()] # 追加 ] end def application do [ extra_applications: [:logger] ] end defp deps do [ {:elixir_make, "~> 0.6", runtime: false} # 追加 ] end end
counter_nif
NIF のコードを C++ で記述します。
ここではまず動作確認のために引数の値をそのまま戻り値として返す identity/1
を実装してみます。
// c_src/counter_nif.cpp #include <erl_nif.h> static ERL_NIF_TERM identity(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { return argv[0]; } static ErlNifFunc nif_funcs[] = { {"identity", 1, identity} }; ERL_NIF_INIT(Elixir.Counter, nif_funcs, nullptr, nullptr, nullptr, nullptr);
Makefile
Makefile を用意します。
変数の意味の詳細については elixir_makeのドキュメント を参照してください。
PREFIX = $(MIX_APP_PATH)/priv BUILD = $(MIX_APP_PATH)/obj NIF = $(PREFIX)/counter_nif.so CFLAGS = -std=c++11 -fpic LDFLAGS = -lpthread -dynamiclib -undefined dynamic_lookup ERL_CFLAGS = -I$(ERL_EI_INCLUDE_DIR) ERL_LDFLAGS = -L$(ERL_EI_LIBDIR) -lei SRC = $(wildcard c_src/*.cpp) HEADERS = $(wildcard c_src/*.h) OBJ = $(SRC:c_src/%.cpp=$(BUILD)/%.o) all: install install: $(PREFIX) $(BUILD) $(NIF) $(OBJ): $(HEADERS) Makefile $(BUILD)/%.o: c_src/%.cpp $(CC) -c $(CFLAGS) $(ERL_CFLAGS) -o $@ $< $(NIF): $(OBJ) $(CC) $(LDFLAGS) $(ERL_LDFLAGS) -o $@ $^ $(PREFIX) $(BUILD): mkdir -p $@ clean: $(RM) $(NIF) $(OBJ) .PHONY: all clean install
Counter module
NIF をバインドする Elixir のモジュールを記述します。
defmodule Counter do @on_load {:load_nif, 0} @compile {:autoload, false} def load_nif do Application.app_dir(:counter, "priv/counter_nif") |> to_charlist() |> :erlang.load_nif(0) end def identity(_term), do: :erlang.nif_error(:nif_not_loaded) end
動作確認
ここまでの編集でファイル構成はこのようになっています。
├── Makefile ├── README.md ├── c_src/ │ └── counter_nif.cpp ├── lib/ │ └── counter.ex ├── mix.exs └── test/ ├── counter_test.exs └── test_helper.exs
mix deps.get
でパッケージを取得し、 iex -S mix
で IEx を起動します。
問題がなければ make が実行され、Makefile に記述した内容で NIF が作成されます。
Counter.identiy/1
を呼び出して NIF が機能していることを確認します。
$ mix deps.get $ iex -S mix iex(1)> Counter.identity(123) 123 iex(2)> Counter.identity("abc") "abc" iex(3)> Counter.identity(:abc) :abc
C++の Counter クラス
C++ のクラスを追加します。
ここでは簡単なカウンタクラスを例として用います。
// c_src/counter.h #ifndef COUNTER_H_ #define COUNTER_H_ class Counter { public: Counter(int count = 0) : count_(count) {} ~Counter() {} int up() { return ++count_; } int down() { return --count_; } void set(int count) { count_ = count; } int get() const { return count_; } private: int count_; }; #endif//COUNTER_H_
リソースの確保と解放
リソースの確保では次の関数を利用します。
enif_open_resource_type
でリソースタイプを登録するenif_alloc_resource
でリソースを確保するenif_make_resource
でハンドルを作成するenif_release_resource
でリソースへの参照を手放す
また enif_open_resource_type
は NIF がロードされたときに呼び出される必要があるので、ロード時に呼び出されるコールバック関数を用意します。
リソースの解放は garbage collection にまかせます。
リソースが解放されるときに呼び出される関数を用意し、 enif_open_resource_type
でその関数を設定します。
counter_nif
c_src/counter_nif.cpp
を次のように変更します。
ここではリソースを確保されたときと解放されたときの様子がわかるように enif_fprintf
を挿入しています。
#include <cstdio> #include <erl_nif.h> // 先に定義した Counter クラスの定義ファイル #include "counter.h" // リソースタイプを保存する変数 ErlNifResourceType* CounterType; // リソースが破棄されるときに呼び出される関数 void destruct_counter(ErlNifEnv* caller_env, void* obj) { // リソースが解放される前に、必要な破棄の操作をここに記述する enif_fprintf(stdout, "destruct %p\n", obj); } // NIF がロードされたときに呼び出される関数 int load(ErlNifEnv* caller_env, void** priv_data, ERL_NIF_TERM load_info) { // リソースタイプを作成する // 第4引数にリソースが破棄されるときに呼び出される関数を指定する CounterType = enif_open_resource_type(caller_env, "Counter", "Counter", destruct_counter, ERL_NIF_RT_CREATE, nullptr); return 0; } // リソースを確保する関数。Elixir から呼び出される ERL_NIF_TERM create(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { // リソースを確保する void* resource = enif_alloc_resource(CounterType, sizeof(Counter)); // 確保されたリソースに対して、必要な構築の操作をここに記述する enif_fprintf(stdout, "construct %p\n", resource); // Erlang がリソースを管理するためのハンドルを作成する // これで garbage collection の対象になる ERL_NIF_TERM handle = enif_make_resource(env, resource); // リソースへの参照を手放す enif_release_resource(resource); // ハンドルを返却する return enif_make_tuple2( env, enif_make_atom(env, "ok"), handle ); } static ErlNifFunc nif_funcs[] = { {"create", 1, create} }; // 第3引数にロード時に呼び出される関数を指定する ERL_NIF_INIT(Elixir.Counter, nif_funcs, load, nullptr, nullptr, nullptr);
Counter module
Counter を次のように変更します。
defmodule Counter do @on_load {:load_nif, 0} @compile {:autoload, false} def load_nif do Application.app_dir(:counter, "priv/counter_nif") |> to_charlist() |> :erlang.load_nif(0) end def create(_init_count), do: :erlang.nif_error(:nif_not_loaded) end
動作確認
IEx から Counter.create/1
を呼び出します。
$ iex -S mix iex(1)> Counter.create(0) construct 0x00007ff929604a78 {:ok, #Reference<0.3631837740.152436741.82065>}
NIF 内に記述したコードによってリソースのポインタが表示されることが確認できます。 またリソースのハンドルは Elixir の環境からは reference として参照されることがわかります。
このリソースは garbage collection によって解放されますが、上記のように実行すると IEx のシェルの環境から参照されているため解放を確認できません。
mix run
コマンドで実行して解放を確認します。
$ mix run -e 'Counter.create(0)' construct 0x00007fb214e033e8 destruct 0x00007fb214e033e8
リソース解放時に呼び出される関数として登録した destruct_counter
が実際に呼び出されていることがわかります。
オブジェクトの構築と破棄
メモリの獲得と解放/分離とオブジェクトの構築/破棄を分離して行うための構文、 placement new と placement delete を利用します。
c_src/counter_nif.cpp
を次のように変更します。
#include <new> #include <erl_nif.h> #include "counter.h" ErlNifResourceType* CounterType; void destruct_counter(ErlNifEnv* caller_env, void* obj) { Counter* counter = static_cast<Counter*>(obj); // placement delete でオブジェクトを破棄する counter->~Counter(); } int load(ErlNifEnv* caller_env, void** priv_data, ERL_NIF_TERM load_info) { CounterType = enif_open_resource_type(caller_env, "Counter", "Counter", destruct_counter, ERL_NIF_RT_CREATE, nullptr); return 0; } ERL_NIF_TERM create(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { int count; enif_get_int(env, argv[0], &count); void* resource = enif_alloc_resource(CounterType, sizeof(Counter)); // placement new でオブジェクトを構築する new(resource) Counter(count); ERL_NIF_TERM handle = enif_make_resource(env, resource); enif_release_resource(resource); return enif_make_tuple2( env, enif_make_atom(env, "ok"), handle ); } static ErlNifFunc nif_funcs[] = { {"create", 1, create} }; ERL_NIF_INIT(Elixir.Counter, nif_funcs, load, nullptr, nullptr, nullptr);
メンバ関数の呼び出し
あとは構築したオブジェクトに対して操作を行う関数を追加するだけです。
Elixir のコードに渡された reference = リソースのハンドルを再び NIF に返すと、enif_get_resource
でリソースのポインタとして受け取ることができます。
void* resource; enif_get_resource(env, argv[0], CounterType, &resource); Counter* counter = static_cast<Counter*>(resource);
もしくは
Counter* counter; enif_get_resource(env, argv[0], CounterType, reinterpret_cast<void**>(&counter));
counter_nif
Counter
のメンバ関数 up
, down
, set
, get
に対応する関数を実装します。
c_src/counter_nif.cpp
を次のように変更します。
#include <new> #include <erl_nif.h> #include "counter.h" ErlNifResourceType* CounterType; void destruct_counter(ErlNifEnv* caller_env, void* obj) { Counter* counter = static_cast<Counter*>(obj); counter->~Counter(); } int load(ErlNifEnv* caller_env, void** priv_data, ERL_NIF_TERM load_info) { CounterType = enif_open_resource_type(caller_env, "Counter", "Counter", destruct_counter, ERL_NIF_RT_CREATE, nullptr); return 0; } ERL_NIF_TERM create(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { int count; enif_get_int(env, argv[0], &count); void* resource = enif_alloc_resource(CounterType, sizeof(Counter)); new(resource) Counter(count); ERL_NIF_TERM handle = enif_make_resource(env, resource); enif_release_resource(resource); return enif_make_tuple2( env, enif_make_atom(env, "ok"), handle ); } ERL_NIF_TERM up(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { void* resource; enif_get_resource(env, argv[0], CounterType, &resource); Counter* counter = static_cast<Counter*>(resource); int result = counter->up(); return enif_make_tuple2( env, enif_make_atom(env, "ok"), enif_make_int(env, result) ); } ERL_NIF_TERM down(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { void* resource; enif_get_resource(env, argv[0], CounterType, &resource); Counter* counter = static_cast<Counter*>(resource); int result = counter->down(); return enif_make_tuple2( env, enif_make_atom(env, "ok"), enif_make_int(env, result) ); } ERL_NIF_TERM set(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { void* resource; enif_get_resource(env, argv[0], CounterType, &resource); Counter* counter = static_cast<Counter*>(resource); int count; enif_get_int(env, argv[1], &count); counter->set(count); return enif_make_atom(env, "ok"); } ERL_NIF_TERM get(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { void* resource; enif_get_resource(env, argv[0], CounterType, &resource); Counter* counter = static_cast<Counter*>(resource); int result = counter->get(); return enif_make_tuple2( env, enif_make_atom(env, "ok"), enif_make_int(env, result) ); } static ErlNifFunc nif_funcs[] = { {"create", 1, create}, {"up", 1, up}, {"down", 1, down}, {"set", 2, set}, {"get", 1, get} }; ERL_NIF_INIT(Elixir.Counter, nif_funcs, load, nullptr, nullptr, nullptr);
Counter module
Counter
にも対応する関数を定義します。
defmodule Counter do @on_load {:load_nif, 0} @compile {:autoload, false} def load_nif do Application.app_dir(:counter, "priv/counter_nif") |> to_charlist() |> :erlang.load_nif(0) end def create(_init_count), do: :erlang.nif_error(:nif_not_loaded) def up(_ref), do: :erlang.nif_error(:nif_not_loaded) def down(_ref), do: :erlang.nif_error(:nif_not_loaded) def set(_ref, _count), do: :erlang.nif_error(:nif_not_loaded) def get(_ref), do: :erlang.nif_error(:nif_not_loaded) end
動作確認
counter $ iex -S mix iex(1)> {:ok, ref} = Counter.create(123) {:ok, #Reference<0.2550255970.1501691911.208805>} iex(2)> Counter.get(ref) {:ok, 123} iex(3)> Counter.up(ref); Counter.up(ref); Counter.up(ref) {:ok, 126} iex(4)> Counter.get(ref) {:ok, 126} iex(5)> Counter.down(ref) {:ok, 125} iex(6)> Counter.get(ref) {:ok, 125} iex(7)> Counter.set(ref, 321) :ok iex(8)> Counter.get(ref) {:ok, 321}
いつか読むはずっと読まない:本を読む
電車での移動中によく本を読むのですが、最近は外出する機会がめっきり減ってしまい、それにともなって本を読む時間も減ってしまいまいました。 外出しないだけ自宅での時間が取れるのだから、その分だけ本を読めるはずなのですが、なかなか思うようにいきません。
- 作者:J・モーティマー・アドラー,V・チャールズ・ドーレン
- 発売日: 1997/10/09
- メディア: 文庫