/nif_call

Call Erlang/Elixir functions from NIF.

Primary LanguageElixirApache License 2.0Apache-2.0

nif_call

Hex.pm

Call Erlang/Elixir functions from NIF.

Usage

1. Add nif_call as a dependency

Add nif_call as a dependency in your mix.exs file.

defp deps do
  [
    {:nif_call, "~> 0.1"}
  ]
end

2. Get the header file

It's recommended to use the nif_call's mix task to get the bundled header file. Assuming you're currently in the root directory of your project, run the following command:

mix nif_call.put_header

By default, the header file will be put in the c_src directory. It may look like this:

.
├── Makefile
├── c_src
│   ├── demo_nif.cpp
│   └── nif_call.h          <-- From this repository
├── lib
│   └── demo
│       ├── application.ex
│       └── demo.ex
├── mix.exs
└── mix.lock

You can also change the directory by passing the --dir option.

mix nif_call.put_header --dir lib/nif_call

If there's already a nif_call.h file in the target directory, you may want to overwrite it by passing the --overwrite option.

mix nif_call.put_header --overwrite

3. Add runner processes to your supervision tree

In applications that use nif_call, you need to add runner processes to the supervision tree. The runner processes are responsible for evaluating the Elixir functions called from NIF. In this demo project, we will add a runner process named Demo.Runner by adding the following code to the lib/demo/application.ex file.

{NifCall.Runner, runner_opts: [nif_module: Demo.NIF, on_evaluated: :nif_call_evaluated], name: Demo.Runner}

The application.ex file should look like this:

defmodule Demo.Application do
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      {NifCall.Runner,
       runner_opts: [nif_module: Demo.NIF, on_evaluated: :nif_call_evaluated], name: Demo.Runner}
    ]

    opts = [strategy: :one_for_one, name: Demo.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Demo.NIF is the module that contains your NIF functions. The on_evaluated option is the name of the callback function that will be called by nif_call to send the evaluated result back to the caller. The default name is nif_call_evaluated.

To send the evaluated result back to the caller, nif_call needs to inject one NIF function to do that.

# lib/demo/nif.ex
defmodule Demo.NIF do
  use NifCall.NIF
end

If you have changed the name of the callback function for the runner process, you need to specify it in the on_evaluated option.

# lib/demo/nif.ex
defmodule Demo.NIF do
  use NifCall.NIF, on_evaluated: :my_evaluated
end

4. Prepare C code

In your NIF code, include nif_call.h and define the NIF_CALL_IMPLEMENTATION macro before including it.

// c_src/demo_nif.cpp
#define NIF_CALL_IMPLEMENTATION
#include "nif_call.h"

And remember to initialize nif_call in the onload function.

// c_src/demo_nif.cpp
static int on_load(ErlNifEnv *env, void **, ERL_NIF_TERM) {
  // initialize nif_call
  return nif_call_onload(env);
}

Lastly, inject the NIF function:

// c_src/demo_nif.cpp
static ErlNifFunc nif_functions[] = {
  // ... your other NIF functions

  // inject nif_call functions
  // `nif_call_evaluated` is the name of the callback function that will be called by nif_call
  NIF_CALL_NIF_FUNC(nif_call_evaluated),

  // of course, you can change the name of the callback function
  // but remember to change it in the Elixir code as well
  // NIF_CALL_NIF_FUNC(my_evaluated),
};

5. Call Erlang/Elixir functions from NIF

Let's try to implement a simple function that adds 1 to the given value and sends the intermediate result to Elixir for further processing. The result of the Elixir callback function is returned as the final result.

Firstly, implement the add_one function in the Elixir code.

# lib/demo/demo.ex
defmodule Demo do
  @doc """
  Add 1 to the `value` in NIF and send the intermediate result to
  Elixir for further processing using the `callback` function.

  The result of the `callback` function is returned as the final result.

  ## Examples

      iex> Demo.add_one(1, fn result -> result * 2 end)
      4

  """
  def add_one(value, callback) do
    # remember to change the name of the Evaluator module if you have changed it
    # and pass both the evaluator and the callback function to the NIF
    evaluator = Process.whereis(Demo.Evaluator)
    Demo.NIF.add_one(value, evaluator, callback)
  end
  def add_one(value, callback) do
    # Use `NifCall.run/3` to call the NIF function
    # 
    # - The second argument is the callback function that will be called from the NIF
    #
    # - The third argument is the function that can invoke somes NIF functions,
    #   this is where you normally call the NIF function
    #
    #   notice that the third argument is a function that takes a `tag` as an argument
    #   the `tag` is used as a reference to the callback function in your `Demo.Runner` process
    NifCall.run(Demo.Runner, callback, fn tag ->
      Demo.NIF.add_one(value, tag)
    end)
  end
end

After that, implement the add_one function in the NIF C code.

// c_src/demo_nif.cpp
static ERL_NIF_TERM add_one(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  ErlNifSInt64 a;
  ERL_NIF_TERM tag = argv[1];

  if (!enif_get_int64(env, argv[0], &a)) return enif_make_badarg(env);
  ERL_NIF_TERM result_term = enif_make_int64(env, a + 1);

  // send the intermediate result to Elixir for further processing
  // `make_nif_call` will return the result of the callback function
  // which is the final result in this case
  NifCallResult result = make_nif_call(env, tag, result_term);
  return result.is_ok() ? result.get_value() : enif_make_tuple2(env, enif_make_atom(env, "error"), result.get_err());
}

Most importantly, don't forget to add the NIF function to the nif_functions array, and they have to be marked as dirty NIF functions.

// c_src/demo_nif.cpp
static ErlNifFunc nif_functions[] = {
  // ... your other NIF functions

  // inject nif_call functions
  NIF_CALL_NIF_FUNC(nif_call_evaluated),

  // add the NIF function
  // NIF functions that calls Elixir functions have to be marked as dirty
  // either ERL_NIF_DIRTY_JOB_CPU_BOUND or ERL_NIF_DIRTY_JOB_IO_BOUND
  {"add_one", 3, add_one, ERL_NIF_DIRTY_JOB_CPU_BOUND},
};

Now, you can call the add_one function from Elixir.

iex> Demo.add_one(1, fn result -> result * 2 end)
4

Congratulations! You have successfully called an Elixir function from NIF.

There's a slightly more complex example in the example directory, which shows that you can make multiple calls to Elixir functions from NIF and use the intermediate results in the next call.