ZEAM開発ログ v.0.4.7 BigNum をどのようにNIFで扱うか考える

ZACKYこと山崎進です。

Elixir の整数は上限・下限が無制限なのが特徴ですが，NIFのAPIでは整数値は64ビット整数(INT64もしくはUINT64)までしか対応していません。64ビット整数の範囲を超える整数値をNIFで扱うにはどうしたらいいかを考えたいと思います。

[「ZEAM開発ログ 目次」はこちら](https://qiita.com/zacky1972/items/70593ab2b70d192813df)

# こんなコードを実行してみた

```c
static
ERL_NIF_TERM asm_1_nif_bi(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
  if(enif_is_number(env, argv[0])) {
    printf("is a number\n");
  } else {
    printf("is not a number\n");
  }
  if(enif_is_map(env, argv[0])) {
    printf("is a map (struct)\n");
  } else {
    printf("is not a map (struct)\n");
  }
  if(enif_is_empty_list(env, argv[0])) {
    printf("is an empty list\n");
  } else {
    printf("is not an empty list\n");
  }
  if(enif_is_list(env, argv[0])) {
    printf("is a list\n");
  } else {
    printf("is not a list\n");
  }
  if(enif_is_tuple(env, argv[0])) {
    printf("is a tuple\n");
  } else {
    printf("is not a tuple\n");
  }
  if(enif_is_ref(env, argv[0])) {
    printf("is a ref\n");
  } else {
    printf("is not a ref\n");
  }
  if(enif_is_atom(env, argv[0])) {
    printf("is an atom\n");
  } else {
    printf("is not an atom\n");
  }
  if(enif_is_binary(env, argv[0])) {
    printf("is a binary\n");
  } else {
    printf("is not a binary\n");
  }

  long l;
  unsigned long u;

  if(enif_get_int64(env, argv[0], &l)) {
    printf("is INT64\n");
  } else if(enif_get_uint64(env, argv[0], &u)) {
    printf("is UINT64\n");
  } else {
    printf("is a bignum\n");
  }


  return enif_make_tuple2(env, error_atom, arithmetic_error_atom);
}
```

```elixir
    try do
      IO.puts asm_1(Asm.max_uint + 1, 1)
    rescue
      error in [ArithmeticError] -> IO.puts "it needs BigNum!: #{Exception.message(error)}"
    end
```

# 実行結果

```
is a number
is not a map (struct)
is not an empty list
is not a list
is not a tuple
is not a ref
is not an atom
is not a binary
is a bignum
it needs BigNum!: bad argument in arithmetic expression
```

実行結果 (is a number) から，`enif_is_number` では true が返ってくることがわかります。
それ以外の基本型にはいずれも該当しません。しかし，`enif_get_int64` と `enif_get_uint64` にはいずれも取得に失敗しています。

このことから，**NIFで標準に用意されているAPIでは64ビット整数の範囲外の整数値は取得できない**ことがわかりました。

ちなみに `ERL_NIF_TERM` はただの整数値，`ErlNifEnv` はメンバーが定義されていない構造体ポインタとして定義されていますので，正攻法では中身をパースすることもできません。これは NIF 側から不用意に Erlang VM 内部をいじらせないためにこのようになっていると考えられます。

# では打ち手はないのか？

打ち手はあります！ 整数値どうか，64ビット整数値の範囲内かどうかは Elixir 側で判別できるので，**該当する場合には，論理演算・シフト演算を使って64ビット整数のリストに分解してから NIF に渡せばいい**です。この方法は，また機会を改めて試したいと思います。

# おまけ: max_uint, min_uint, is_uint64, is_bignum を追加しました

[「ZEAM開発ログ v.0.4.4 INT64判定をマクロで簡単に判定する」](https://qiita.com/zacky1972/items/878489ce88f380f0ae60)で紹介した判定マクロに機能を追加しました。 https://github.com/zeam-vm/asm

* `max_int`: 符号付き64ビット整数の最大値
* `min_int`: 符号付き64ビット整数の最小値
* `max_uint`: 符号なし64ビット整数の最大値
* `min_uint`: 符号なし64ビット整数の最小値
* `is_int64`: INT64判定
* `is_uint64`: UINT64判定
* `is_bignum`: 整数値だが64ビット整数に収まらない場合に`true`

`mix.exs` に次のように記述してください。

```elixir
  defp deps do
    [
      {:asm, "~> 0.0.7"}
    ]
```

この結果，型多相で型安全なNIFは次のようになりました。

`lib/nif_llvm.ex` の一部

```elixir
  def asm_1(a, b) do
    OK.try do
      result <- case {a, b} do
        {a, b} when is_int64(a)   and is_int64(b)   -> asm_1_nif_ii(a, b)
        {a, b} when is_uint64(a)  and is_uint64(b)  -> asm_1_nif_uu(a, b)
        {a, b} when is_integer(a) and is_integer(b) -> 
          IO.puts "need BigNum"
          {:error, :arithmetic_error}

        {a, b} when is_int64(a)   and is_float(b)   -> asm_1_nif_if(a, b)
        {a, b} when is_uint64(a)  and is_float(b)   -> asm_1_nif_uf(a, b)
        {a, b} when is_integer(a) and is_float(b)   ->
          IO.puts "need BigNum"
          {:error, :arithmetic_error}

        {a, b} when is_float(a)   and is_int64(b)   -> asm_1_nif_fi(a, b)
        {a, b} when is_float(a)   and is_uint64(b)  -> asm_1_nif_fu(a, b)
        {a, b} when is_float(a)   and is_integer(b) ->
          IO.puts "need BigNum"
          {:error, :arithmetic_error}

        {a, b} when is_float(a)   and is_float(b)   -> asm_1_nif_ff(a, b)
        _ -> {:error, :arithmetic_error}
      end
    after
      result
    rescue
      :arithmetic_error -> raise ArithmeticError, message: "bad argument in arithmetic expression"
    end
  end

  def asm_1_nif_ii(a, b) when is_int64(a)  and is_int64(b),  do: raise "NIF asm_1_nif_ii/2 not implemented"
  def asm_1_nif_uu(a, b) when is_uint64(a) and is_uint64(b), do: raise "NIF asm_1_nif_uu/2 not implemented"
  def asm_1_nif_if(a, b) when is_int64(a)  and is_float(b),  do: raise "NIF asm_1_nif_if/2 not implemented"
  def asm_1_nif_uf(a, b) when is_uint64(a) and is_float(b),  do: raise "NIF asm_1_nif_uf/2 not implemented"
  def asm_1_nif_fi(a, b) when is_float(a)  and is_int64(b),  do: raise "NIF asm_1_nif_fi/2 not implemented"
  def asm_1_nif_fu(a, b) when is_float(a)  and is_uint64(b), do: raise "NIF asm_1_nif_fu/2 not implemented"
  def asm_1_nif_ff(a, b) when is_float(a)  and is_float(b),  do: raise "NIF asm_1_nif_ff/2 not implemented"
```

[コード全体はこちら(GitHub)](https://github.com/zeam-vm/nif_llvm)

次回は[「ZEAM開発ログ v.0.4.8 INT64判定をGPUベンチマークに組込む」](https://qiita.com/zacky1972/items/0475cb5e48869435dcc5) をお送りします。お楽しみに！

:stars::stars::stars:<b>　お知らせ：Elixirもくもく会（リモート参加OK、入門トラック有）を9月28日に開催します　</b>:stars::stars::stars:

[<b>「fukuoka.ex#14：Elixir／Phoenixもくもく会～入門もあるよ」</b>](https://fukuokaex.connpass.com/event/100659/)を2018年9月28日金曜日に開催します

前回は，ゲリラ的に募った「Zoomによるリモート参加」を，今回から正式に受け付けるようになりましたので，福岡以外の首都圏や地方からでも参加できます（申し込みいただいたら、追ってZoom URLをconnpassメールでお送りします）

また，これまではElixir／Phoenix経験者を対象とした，もくもく会オンリーでしたが，今回から，入門者トラックも併設し，fukuoka.exアドバイザーズ／キャストに質問できるようにアップグレードしました

私，山崎も参加します！ この記事の延長線上のものを作ろうと思っています。

お申込みはコチラから
https://fukuokaex.connpass.com/event/100659/
[![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/3f0fbb56-e5e8-2c4d-7c62-1b0b311d6d02.png)](https://fukuokaex.connpass.com/event/100659/)