Call Erlang/Elixir functions from NIF.
Add nif_call
as a dependency in your mix.exs
file.
defp deps do
[
{:nif_call, "~> 0.1"}
]
end
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
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
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),
};
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.