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

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

C++のクラスをNIFでElixirにバインドしてみた

C++ のライブラリを Elixir から利用したくなりました。

NIF を使った単純な関数の呼び出しは実装したことがあるのですが、オブジェクトを利用するようなケースは試したことがなかったので、今回調べてみました。

今回のコードではエラーハンドリングなどは省略していますのでご注意ください。 不備がありましたらご指摘いただけるとありがたいです。

またここで実装したファイルは Gist にもアップしていますので合わせてご参照ください。

NIF環境の用意

elixir_make

mix compile コマンドで C++ のコードがいっしょにコンパイルされるように、elixir_make を導入します。

hex.pm

まず依存パッケージとして mix.exselixir_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 は 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}

いつか読むはずっと読まない:本を読む

電車での移動中によく本を読むのですが、最近は外出する機会がめっきり減ってしまい、それにともなって本を読む時間も減ってしまいまいました。 外出しないだけ自宅での時間が取れるのだから、その分だけ本を読めるはずなのですが、なかなか思うようにいきません。