📜 ⬆️ ⬇️

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 is compiled into bytecode for Erlang VM. I immediately had difficulty managing my GPIO contacts. Then I found a library that seemed to solve all my problems. However, it was written as a Port, because of which each call to its functions took too much time, which affected the correct operation of my robot.

A little thought, I still decided to rewrite the library in the form of NIF. Since I did not find much information on this subject, I decided to share my experience of writing NIF in Elixir with you. As an example, I will use what I have created.

So let's start with the fact that I found the library in C, the 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:

My next step was to write the NIF code itself in C. First you need to import the header of the NIF function from VM Erlang:
')
 #include <erl_nif.h> 

Then you need to describe exactly what features 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 three-element structures. 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 that the name of this array does not matter and can be anything.

In addition, NIF must be registered using the macro ERL_NIF_INIT. It looks like this to me:

 ERL_NIF_INIT(Elixir.ExPigpio, funcs, &load, &reload, &upgrade, &unload) 

The parameters of this macro are:

  1. The name of the module in Elixir with the prefix "Elixir.". In my case, the module name is ExPigpio. The prefix is ​​needed because the name of the module changes upon compilation and acquires the prefix "Elixir."
  2. An array describing the functions of the NIF
  3. Pointers to functions that will be called upon loading, reloading, updating, and unloading the library. These functions are optional callbacks. If any of these callbacks are 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 the 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 must take exactly the above parameters and return the 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 write the module in Elixir. Its main task will be to load the library in C and the description of the functions implemented in the NIF.

 defmodule ExPigpio do @on_load :init def init do path = Application.app_dir(:ex_pigpio, "priv/ex_pigpio") |> String.to_char_list :ok = :erlang.load_nif(path, 0) end def set_mode(_gpio, _mode) do exit(:nif_not_loaded) end # ... end 

Notice @on_load: init. This logs the init function call when the module is loaded. The init function finds the ex_pigpio.so library in the priv folder. No need to specify the suffix ".so", because 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 in case it fails to load the NIF. As a rule, the functions described in this Elixir module simply call exit with the parameter: nif_not_loaded. However, they can also 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 the mix.exs.

Example Makefile:

 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 this Makefile. The LDFLAGS and "-DEMBEDDED_IN_VM" flag 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" def run(_) do {result, _error_code} = System.cmd("make", ["priv/ex_pigpio.so"], stderr_to_stdout: true) Mix.shell.info result :ok end end defmodule ExPigpio.Mixfile do use Mix.Project def project do [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 create 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 “priv / ex_pigpio.so” parameter. Below, in the project function, in the Keyword we add the element “compilers” and indicate our module in the first place, before the standard ones. As you can see, instead of the full name of the module, we indicated the 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 .

Source: https://habr.com/ru/post/259467/


All Articles