How to write your NIF in Elixir
Most recently, I plunged into the world of robotics and decided to program my own robot based on RasPi. For this, I used Elixir, a relatively new, by the way, programming language that compiles into bytecode for Erlang VM. I immediately had difficulty managing GPIO contacts. Then I found a library that seemed to solve all my problems. However, it was written as Port, which is why each call to its functions took too much time, which affected the correct operation of my robot.
After thinking a bit, I still decided to rewrite the library as NIF. Since I did not find much information about this, I decided to share my experience writing NIF in Elixir with you. As an example, I will use what I created.
So, to begin with, I found a library in C, pigpio , which had all the functions I needed. Then I created a new project with the command:
To the standard folders created automatically by the mix program, I added:
My next step was to write the NIF code in C itself. First you need to import the NIF function header from the Erlang VM:
Then you need to describe exactly which functions this NIF will export to Elixir. As an example, in my case:
funcs [] is an array that contains structures of three elements. The first element is the name of the function in Elixir; the second is the number of parameters accepted by the function; the third is a pointer to the function itself in C. I must say right away that the name of this array has no meaning and can be anything.
In addition, NIF must be registered using the macro ERL_NIF_INIT. It looks like this for me:
The parameters of this macro are:
I would like to show the implementation of the get_pwm_range function as an example of a NIF function.
All NIF functions should take exactly the above parameters and return a result of type ERL_NIF_TERM. You can find all the details at www.erlang.org/doc/man/erl_nif.html .
So the code in C is ready. Now we are writing the module in Elixir. Its main task will be to load the library in C and describe the functions implemented in NIF.
Pay attention to @on_load: init. This registers the init function call when the module loads. The init function finds the ex_pigpio.so library in the priv folder. The suffix ".so" is not necessary, as It is added automatically. Finally, the function call: erlang.load_nif loads the library.
For each function from NIF to Elixir, we will write a function with the same name and number of parameters. This function will be called if it fails to load NIF. Typically, the functions described in this Elixir module simply call exit with the parameter: nif_not_loaded. Nevertheless, they can be used for alternative implementation of the final function.
The last step is to compile our project. To do this, we need to create a Makefile and make the required changes to mix.exs.
Makefile example:
There is nothing special about such a Makefile. LDFLAGS and the flag "-DEMBEDDED_IN_VM" are not required for all NIFs and are specific to this project. The variable ERLANG_PATH, on the contrary, is a necessary thing for all NIFs.
Now we can make the latest changes to mix.exs.
We are creating the Mix.Tasks.Compile.Pigpio module, which will help us compile the ex_pigpio.so library. It implements the run function, which calls the make command with the parameter “priv / ex_pigpio.so”. Below, in the project function, in Keyword, we add the “compilers” element and point our module there in the first place, before the standard ones. As you can see, instead of the full name of the module, we specified an atom: pigpio, which reflects only the last part.
To compile, give the command:
So, our NIF is ready! The full source code is here: github.com/briksoftware/ex_pigpio .
After thinking a bit, I still decided to rewrite the library as NIF. Since I did not find much information about this, I decided to share my experience writing NIF in Elixir with you. As an example, I will use what I created.
So, to begin with, I found a library in C, pigpio , which had all the functions I needed. Then I created a new project with the command:
mix new ex_pigpio
To the standard folders created automatically by the mix program, I added:
- src folder : there I put the NIF source code in C
- priv folder : there, when compiling, the ex_pigpio.so library will appear
- file the Makefile : need to compile the library ex_pigpio.so
My next step was to write the NIF code in C itself. First you need to import the NIF function header from the Erlang VM:
#include<erl_nif.h>
Then you need to describe exactly which functions this NIF will export to Elixir. As an example, in my case:
static ErlNifFunc funcs[] = {
{ "set_mode", 2, set_mode },
// ...
{ "get_pwm_range", 1, get_pwm_range }
};
funcs [] is an array that contains structures of three elements. The first element is the name of the function in Elixir; the second is the number of parameters accepted by the function; the third is a pointer to the function itself in C. I must say right away that the name of this array has no meaning and can be anything.
In addition, NIF must be registered using the macro ERL_NIF_INIT. It looks like this for me:
ERL_NIF_INIT(Elixir.ExPigpio, funcs, &load, &reload, &upgrade, &unload)
The parameters of this macro are:
- The name of the module in Elixir with the prefix "Elixir.". In my case, the name of the module is ExPigpio. The prefix is needed because the name of the module changes during compilation and takes on the prefix “Elixir.”
- Array describing NIF functions
- Pointers to the functions that will be called when loading, reloading, updating and unloading the library. These functions are optional callbacks. If any of these callbacks is not needed, then you can specify NULL instead.
I would like to show the implementation of the get_pwm_range function as an example of a NIF function.
static ERL_NIF_TERM get_pwm_range(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ex_pigpio_priv* priv;
priv = enif_priv_data(env);
unsigned gpio;
if (!enif_get_uint(env, argv[0], &gpio)) {
return enif_make_badarg(env);
}
int value = gpioGetPWMrange(gpio);
switch(value) {
case PI_BAD_USER_GPIO:
return enif_make_tuple2(env, priv->atom_error, priv->atom_bad_user_gpio);
default:
return enif_make_tuple2(env, priv->atom_ok, enif_make_int(env, value));
}
}
All NIF functions should take exactly the above parameters and return a result of type ERL_NIF_TERM. You can find all the details at www.erlang.org/doc/man/erl_nif.html .
So the code in C is ready. Now we are writing the module in Elixir. Its main task will be to load the library in C and describe the functions implemented in NIF.
defmodule ExPigpio do
@on_load :init
def init dopath = Application.app_dir(:ex_pigpio, "priv/ex_pigpio") |> String.to_char_list
:ok = :erlang.load_nif(path, 0)
enddef set_mode(_gpio, _mode) doexit(:nif_not_loaded)
end# ...end
Pay attention to @on_load: init. This registers the init function call when the module loads. The init function finds the ex_pigpio.so library in the priv folder. The suffix ".so" is not necessary, as It is added automatically. Finally, the function call: erlang.load_nif loads the library.
For each function from NIF to Elixir, we will write a function with the same name and number of parameters. This function will be called if it fails to load NIF. Typically, the functions described in this Elixir module simply call exit with the parameter: nif_not_loaded. Nevertheless, they can be used for alternative implementation of the final function.
The last step is to compile our project. To do this, we need to create a Makefile and make the required changes to mix.exs.
Makefile example:
MIX = mix
CFLAGS = -O3 -Wall
ERLANG_PATH = $(shell erl -eval 'io:format("~s", [lists:concat([code:root_dir(), "/erts-", erlang:system_info(version), "/include"])])' -s init stop -noshell)
CFLAGS += -I$(ERLANG_PATH)
ifeq ($(wildcard deps/pigpio),)
PIGPIO_PATH = ../pigpio
else
PIGPIO_PATH = deps/pigpio
endif
CFLAGS += -I$(PIGPIO_PATH) -fPIC
LDFLAGS = -lpthread -lrt
.PHONY: all ex_pigpio clean
all: ex_pigpio
ex_pigpio:
$(MIX) compile
priv/ex_pigpio.so: src/ex_pigpio.c
$(MAKE) CFLAGS="-DEMBEDDED_IN_VM" -B -C $(PIGPIO_PATH) libpigpio.a
$(CC) $(CFLAGS) -shared $(LDFLAGS) -o $@ src/ex_pigpio.c $(PIGPIO_PATH)/libpigpio.a
clean:
$(MIX) clean
$(MAKE) -C $(PIGPIO_PATH) clean
$(RM) priv/ex_pigpio.so
There is nothing special about such a Makefile. LDFLAGS and the flag "-DEMBEDDED_IN_VM" are not required for all NIFs and are specific to this project. The variable ERLANG_PATH, on the contrary, is a necessary thing for all NIFs.
Now we can make the latest changes to mix.exs.
defmodule Mix.Tasks.Compile.Pigpio do
@shortdoc "Compiles Pigpio"defrun(_)do
{result, _error_code} = System.cmd("make", ["priv/ex_pigpio.so"], stderr_to_stdout:true)
Mix.shell.info result
:okendend
defmodule ExPigpio.Mixfile do
use Mix.Project
defprojectdo
[app::ex_pigpio,
version:"0.0.1",
elixir:"~> 1.0",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
compilers: [:pigpio, :elixir, :app],
deps: deps]
end# ...end
We are creating the Mix.Tasks.Compile.Pigpio module, which will help us compile the ex_pigpio.so library. It implements the run function, which calls the make command with the parameter “priv / ex_pigpio.so”. Below, in the project function, in Keyword, we add the “compilers” element and point our module there in the first place, before the standard ones. As you can see, instead of the full name of the module, we specified an atom: pigpio, which reflects only the last part.
To compile, give the command:
mix compile
So, our NIF is ready! The full source code is here: github.com/briksoftware/ex_pigpio .