ZACKYこと山崎進です。
Elixir/NIF間でBigNumをやりとりするの2回目は,NIF側について説明したいと思います。
書いたコード
#include "erl_nif.h"
typedef struct vector {
int size;
unsigned long *value;
} VECTOR;
int enif_get_big_num(ErlNifEnv *env, ERL_NIF_TERM term, unsigned *is_negative, VECTOR **value)
{
int tuple_arity;
const ERL_NIF_TERM *tuple;
if(__builtin_expect((enif_get_tuple(env, term, &tuple_arity, &tuple) == 0), 0)) {
return 0;
}
if(__builtin_expect(tuple_arity != 2, 0)) {
return 0;
}
if(__builtin_expect((enif_get_uint(env, tuple[0], is_negative) == 0), 0)) {
return 0;
}
unsigned int length;
if(__builtin_expect((enif_get_list_length(env, tuple[1], &length) == 0), 0)) {
return 0;
}
if(__builtin_expect((length == 0), 0)) {
return 0;
}
ERL_NIF_TERM head, tail;
if(__builtin_expect((enif_get_list_cell(env, tuple[1], &head, &tail) == 0), 0)) {
return 0;
}
unsigned long *v = (unsigned long *)enif_alloc(sizeof(unsigned long) * length);
if(__builtin_expect((v == NULL), 0)) {
return 0;
}
*value = (VECTOR *)enif_alloc(sizeof(VECTOR));
if(__builtin_expect((*value == NULL), 0)) {
enif_free(v);
return 0;
}
for(int i = 0; i < length; i++) {
if(__builtin_expect((enif_get_uint64(env, head, &v[i]) == 0), 0)) {
enif_free(v);
return 0;
}
if(__builtin_expect((enif_get_list_cell(env, tail, &head, &tail) == 0), 0)) {
if(i + 1 < length) {
enif_free(v);
return 0;
}
}
}
(*value)->size = length;
(*value)->value = v;
return 1;
}
ERL_NIF_TERM enif_make_big_num(ErlNifEnv *env, const unsigned is_negative, const VECTOR *value)
{
ERL_NIF_TERM term_is_negative = enif_make_uint(env, is_negative);
ERL_NIF_TERM *term_array = enif_alloc(sizeof(ERL_NIF_TERM) * value->size);
for(int i = 0; i < value->size; i++) {
term_array[i] = enif_make_uint64(env, value->value[i]);
}
ERL_NIF_TERM term_list = enif_make_list_from_array(env, term_array, value->size);
enif_free(term_array);
return enif_make_tuple2(env, term_is_negative, term_list);
}
enif_get_big_num
は型が厳密に合っていない場合などには 0 を返すようにしています。また型安全になるように最善の努力をしています。
分岐予測を最適化するためのコードが入っているので,読みにくいですね。
スッキリさせるとこんな感じです。
#include "erl_nif.h"
typedef struct vector {
int size;
unsigned long *value;
} VECTOR;
int enif_get_big_num(ErlNifEnv *env, ERL_NIF_TERM term, unsigned *is_negative, VECTOR **value)
{
int tuple_arity;
const ERL_NIF_TERM *tuple;
if(!enif_get_tuple(env, term, &tuple_arity, &tuple
|| tuple_arity != 2
|| !enif_get_uint(env, tuple[0], is_negative)) {
return 0;
}
unsigned int length;
if(!enif_get_list_length(env, tuple[1], &length)
|| length == 0) {
return 0;
}
ERL_NIF_TERM head, tail;
if(!enif_get_list_cell(env, tuple[1], &head, &tail)) {
return 0;
}
unsigned long *v = (unsigned long *)enif_alloc(sizeof(unsigned long) * length);
if(v == NULL) {
return 0;
}
*value = (VECTOR *)enif_alloc(sizeof(VECTOR));
if(*value == NULL) {
enif_free(v);
return 0;
}
for(int i = 0; i < length; i++) {
if(!enif_get_uint64(env, head, &v[i])) {
enif_free(v);
return 0;
}
if(!enif_get_list_cell(env, tail, &head, &tail)) {
if(i + 1 < length) {
enif_free(v);
return 0;
}
}
}
(*value)->size = length;
(*value)->value = v;
return 1;
}
ERL_NIF_TERM enif_make_big_num(ErlNifEnv *env, const unsigned is_negative, const VECTOR *value)
{
ERL_NIF_TERM term_is_negative = enif_make_uint(env, is_negative);
ERL_NIF_TERM *term_array = enif_alloc(sizeof(ERL_NIF_TERM) * value->size);
for(int i = 0; i < value->size; i++) {
term_array[i] = enif_make_uint64(env, value->value[i]);
}
ERL_NIF_TERM term_list = enif_make_list_from_array(env, term_array, value->size);
enif_free(term_array);
return enif_make_tuple2(env, term_is_negative, term_list);
}
元のコードで順に説明していきます。
int enif_get_big_num(ErlNifEnv *env, ERL_NIF_TERM term, unsigned *is_negative, VECTOR **value)
NIFの関数と同じように名前付けと引数・戻り値の型を決めてみました。
呼び出す時にはこんな感じにします。
static
ERL_NIF_TERM func(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
unsinged int a_neg;
VECTOR *a;
if(enif_get_big_num(env, argv[0], &a_neg, &a) {
// a_neg, a に第1引数を読み取った結果が入っている
}
int tuple_arity;
const ERL_NIF_TERM *tuple;
if(__builtin_expect((enif_get_tuple(env, term, &tuple_arity, &tuple) == 0), 0)) {
return 0;
}
if(__builtin_expect(tuple_arity != 2, 0)) {
return 0;
}
enif_get_tuple
はタプルを読み込みます。tuple_arity
にはタプルの引数の数,tuple
にはERL_NIF_TERMの配列として読み込まれたタプルが入ります。タプル型でなかったり,tuple_arity
が2でなかった場合には 0 を返します。
if(__builtin_expect((enif_get_uint(env, tuple[0], is_negative) == 0), 0)) {
return 0;
}
タプルの第1引数であるis_negative
を読み込みます。真偽値ではなく整数値にしたので,扱いがシンプルになります。
unsigned int length;
if(__builtin_expect((enif_get_list_length(env, tuple[1], &length) == 0), 0)) {
return 0;
}
if(__builtin_expect((length == 0), 0)) {
return 0;
}
タプルの第2引数であるリストの長さを読み込んでlength
に入れます。リストではなかったりlength
が0のときには0を返します。
このAPIをみると,リストの長さはUINT32の範囲までですね。これを超える場合は,後述するenif_get_list_cell
を使って一つずつ取り出すしかないですね。あとで対策を考えよう。
ERL_NIF_TERM head, tail;
if(__builtin_expect((enif_get_list_cell(env, tuple[1], &head, &tail) == 0), 0)) {
return 0;
}
Elixirでいうところの hd
と tl
の取り出しです。head
に hd
,tail
にtl
が入ります。
unsigned long *v = (unsigned long *)enif_alloc(sizeof(unsigned long) * length);
if(__builtin_expect((v == NULL), 0)) {
return 0;
}
配列のメモリを確保します。enif_alloc
は NIF で用意されているメモリ確保の関数です。ErlangVMのメモリアロケーターを利用するので,malloc
より高速とのことです。メモリの解放は enif_free
です。
*value = (VECTOR *)enif_alloc(sizeof(VECTOR));
if(__builtin_expect((*value == NULL), 0)) {
enif_free(v);
return 0;
}
構造体のメモリを確保します。失敗した時にはすでに確保している配列のメモリも解放します。
for(int i = 0; i < length; i++) {
if(__builtin_expect((enif_get_uint64(env, head, &v[i]) == 0), 0)) {
enif_free(v);
return 0;
}
if(__builtin_expect((enif_get_list_cell(env, tail, &head, &tail) == 0), 0)) {
if(i + 1 < length) {
enif_free(v);
return 0;
}
}
}
リストから配列への読み込みです。tail
を次々読み込むのはリストの読み込みの定番です。終了処理がちょっとトリッキーですが,i == length - 1 のときに enif_get_list_cell
を実行すると 0 が返ってきてしまうためです。
(*value)->size = length;
(*value)->value = v;
return 1;
}
結果として返却する構造体にデータを格納して 1 を返します。
ERL_NIF_TERM enif_make_big_num(ErlNifEnv *env, const unsigned is_negative, const VECTOR *value)
{
ERL_NIF_TERM term_is_negative = enif_make_uint(env, is_negative);
ERL_NIF_TERM *term_array = enif_alloc(sizeof(ERL_NIF_TERM) * value->size);
for(int i = 0; i < value->size; i++) {
term_array[i] = enif_make_uint64(env, value->value[i]);
}
ERL_NIF_TERM term_list = enif_make_list_from_array(env, term_array, value->size);
enif_free(term_array);
return enif_make_tuple2(env, term_is_negative, term_list);
}
enif_get_big_num
に比べると enif_make_big_num
はごくシンプルです。ただし,value
や value->value
が NULL
だったり is_negative
や value->size
が不正な値だったり,term_array
で得た値が NULL
だったり,といった場合のチェックはサボっています。要改善。
ERL_NIF_TERM term_is_negative = enif_make_uint(env, is_negative);
is_negative
を変換しています。
ERL_NIF_TERM *term_array = enif_alloc(sizeof(ERL_NIF_TERM) * value->size);
for(int i = 0; i < value->size; i++) {
term_array[i] = enif_make_uint64(env, value->value[i]);
}
ERL_NIF_TERM term_list = enif_make_list_from_array(env, term_array, value->size);
enif_free(term_array);
unsined long
の配列をリストに変換しています。最初,enif_make_list_from_array
の term_array
と書いているところに直に unsigned long
の配列を突っ込んで,セグメンテーションフォールトを発生させてしまいました。ただしくは ERL_NIF_TERM
の配列を与えてやる必要があります。
return enif_make_tuple2(env, term_is_negative, term_list);
まとめてタプルにして返します。
次回は「ZEAM開発ログ v.0.4.14.2 Elixir / NIF 間で BigNum をやりとりする (Elixir での使用例)」をお送りします。お楽しみに!