ZEAM開発ログ v.0.4.6 OKを使ってNIFのエラー処理をエレガントに書いてみる

ZACKYこと山崎進です。

Tsuyoshi Yamaguchi @Tsuyoshi84 さんが書いた[「OKを使ってElixirの :ok, :error タプルをエレガントに処理」](https://qiita.com/Tsuyoshi84/items/5a020ef185b094778d92)に刺激を受けて，さっそく型多相・型安全なNIFコードにOKを使ってみました。

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

# さっそくコードを紹介

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

mix.exs

```elixir
 defmodule NifLlvm.MixProject do
  use Mix.Project

  def project do
    [
      app: :nif_llvm,
      version: "0.1.0",
      elixir: "~> 1.6",
      compilers: [:nif_llvm] ++ Mix.compilers,
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:asm, "~> 0.0.4"},
      {:ok, "~> 2.0"}
    ]
  end
end

defmodule Mix.Tasks.Compile.NifLlvm do
  def run(_) do
    if match? {:win32, _}, :os.type do
      # libpostal does not support Windows unfortunately.
      IO.warn("Windows is not supported.")
      exit(1)
    else
      File.mkdir_p("priv")
      {result, _error_code} = System.cmd("make", ["priv/libnifllvm.so"], stderr_to_stdout: true)
      IO.binwrite result
    end
    :ok
  end
end
```

`defp deps` に `{:ok, "~> 2.0"}`を追加しています。

lib/nif_llvm.ex

```elixir
defmodule NifLlvm do
  require OK
  require Asm
  import Asm

  @on_load :load_nifs

  def load_nifs do
    :erlang.load_nif('./priv/libnifllvm', 0)
  end

  @moduledoc """
  Documentation for NifLlvm.
  """

  def main do
    IO.puts asm_1(1, 2)
    IO.puts asm_1(1.0, 2)
    IO.puts asm_1(1, 2.0)
    IO.puts asm_1(1.0, 2.0)
    IO.puts asm_1(Asm.max_int, 0)
    IO.puts asm_1(Asm.min_int, 0)
    try do
      IO.puts asm_1(Asm.max_int, 1)
    rescue
      error in [ArithmeticError] -> IO.puts "it needs BigNum!: #{Exception.message(error)}"
    end
    try do
      IO.puts asm_1(Asm.max_int + 1, 1)
    rescue
      error in [ArithmeticError] -> IO.puts "it needs BigNum!: #{Exception.message(error)}"
    end
  end

  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_int64(a) and is_float(b) -> asm_1_nif_if(a, b)
        {a, b} when is_float(a) and is_int64(b) -> asm_1_nif_fi(a, b)
        {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_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_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_ff(a, b) when is_float(a) and is_float(b), do: raise "NIF asm_1_nif_ff/2 not implemented"

end
```

かなりスッキリ書けましたね。

native/lib.c

```c
#include <limits.h>
#include "erl_nif.h"
#include "loader.c"

static ERL_NIF_TERM arithmetic_error;
static ERL_NIF_TERM ok_atom;
static ERL_NIF_TERM error_atom;
static ERL_NIF_TERM arithmetic_error_atom;

static void init_nif_llvm(ErlNifEnv *env)
{
  arithmetic_error = enif_raise_exception(env, enif_make_atom(env, "ArithmeticError"));
  ok_atom = enif_make_atom(env, "ok");
  error_atom = enif_make_atom(env, "error");
  arithmetic_error_atom = enif_make_atom(env, "arithmetic_error");
}

static
ERL_NIF_TERM asm_1_nif_ii(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
  long a, b;
  if(__builtin_expect((enif_get_int64(env, argv[0], &a) == 0), 0)) {
    goto error;
  }
  if(__builtin_expect((enif_get_int64(env, argv[1], &b) == 0), 0)) {
    goto error;
  }

  long result;

  if(__builtin_expect(__builtin_saddl_overflow(a, b, &result), 0)) {
    goto error2;
  }

  return enif_make_tuple2(env, ok_atom, enif_make_int64(env, result));
error:
  return arithmetic_error;
error2:
  return enif_make_tuple2(env, error_atom, arithmetic_error_atom);
}

static
ERL_NIF_TERM asm_1_nif_if(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
  long a;
  double b;
  if(__builtin_expect((enif_get_int64(env, argv[0], &a) == 0), 0)) {
    goto error;
  }
  if(__builtin_expect((enif_get_double(env, argv[1], &b) == 0), 0)) {
    goto error;
  }
  double result = ((double)a) + b;
  return enif_make_tuple2(env, ok_atom, enif_make_double(env, result));
error:
  return arithmetic_error;
}

static
ERL_NIF_TERM asm_1_nif_fi(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
  double a;
  long b;
  if(__builtin_expect((enif_get_double(env, argv[0], &a) == 0), 0)) {
    goto error;
  }
  if(__builtin_expect((enif_get_int64(env, argv[1], &b) == 0), 0)) {
    goto error;
  }
  double result = a + ((double) b);
  return enif_make_tuple2(env, ok_atom, enif_make_double(env, result));
error:
  return arithmetic_error;
}

static
ERL_NIF_TERM asm_1_nif_ff(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
  double a, b;
  if(__builtin_expect((enif_get_double(env, argv[0], &a) == 0), 0)) {
    goto error;
  }
  if(__builtin_expect((enif_get_double(env, argv[1], &b) == 0), 0)) {
    goto error;
  }
  double result = a + b;
  return enif_make_tuple2(env, ok_atom, enif_make_double(env, result));
error:
  return arithmetic_error;
}

static
ErlNifFunc nif_funcs[] =
{
  // {erl_function_name, erl_function_arity, c_function}
  {"asm_1_nif_ii", 2, asm_1_nif_ii},
  {"asm_1_nif_if", 2, asm_1_nif_if},
  {"asm_1_nif_fi", 2, asm_1_nif_fi},
  {"asm_1_nif_ff", 2, asm_1_nif_ff}
};

ERL_NIF_INIT(Elixir.NifLlvm, nif_funcs, &load, &reload, &upgrade, &unload)
```

NIFコードの変更点は，`:ok`, `:error` を第1引数に，計算結果もしくは `:arithmetic_error` を第2引数にした2引数のタプルを返すようにした点です。

次回は[「ZEAM開発ログ v.0.4.7 BigNum をどのようにNIFで扱うか考える」](https://qiita.com/zacky1972/items/e2858597de64ec2449dc)です。お楽しみに！

: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/)