C++ のライブラリを Elixir から利用したくなりました。
NIF を使った単純な関数の呼び出しは実装したことがあるのですが、オブジェクトを利用するようなケースは試したことがなかったので、今回調べてみました。
今回のコードではエラーハンドリングなどは省略していますのでご注意ください。
不備がありましたらご指摘いただけるとありがたいです。
またここで実装したファイルは Gist にもアップしていますので合わせてご参照ください。
NIF環境の用意
elixir_make
mix compile
コマンドで C++ のコードがいっしょにコンパイルされるように、elixir_make を導入します。
hex.pm
まず依存パッケージとして 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
を実装してみます。
#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 を用意します。
変数の意味の詳細については 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(), 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++ のクラスを追加します。
ここでは簡単なカウンタクラスを例として用います。
#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
リソースの確保と解放
リソースの確保では次の関数を利用します。
また 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>
#include "counter.h"
ErlNifResourceType* CounterType;
void destruct_counter(ErlNifEnv* caller_env, void* obj) {
enif_fprintf(stdout, "destruct %p\n", obj);
}
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[]) {
void* resource = enif_alloc_resource(CounterType, sizeof(Counter));
enif_fprintf(stdout, "construct %p\n", resource);
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);
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(), 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);
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
);
}
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(), do: :erlang.nif_error(:nif_not_loaded)
def up(), do: :erlang.nif_error(:nif_not_loaded)
def down(), do: :erlang.nif_error(:nif_not_loaded)
def set(, ), do: :erlang.nif_error(:nif_not_loaded)
def get(), 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}
いつか読むはずっと読まない:本を読む
電車での移動中によく本を読むのですが、最近は外出する機会がめっきり減ってしまい、それにともなって本を読む時間も減ってしまいまいました。
外出しないだけ自宅での時間が取れるのだから、その分だけ本を読めるはずなのですが、なかなか思うようにいきません。