Pull to refresh

Как написать свой NIF в Elixir

Reading time 4 min
Views 6.5K
Совсем недавно я окунулся в мир роботики и решил запрограммировать собственного робота на основе RasPi. Для этого я использовал Elixir, сравнительно новый, к слову сказать, язык программирования, который компилируется в байткод для Erlang VM. У меня сразу же возникла трудность с управлением контактами GPIO. Тогда я нашел библиотеку, которая вроде бы решала все мои проблемы. Однако она была написана как Port, из-за чего каждый вызов ее функций занимал слишком много времени, что влияло на правильность работы моего робота.

Немного подумав, я все-таки решился переписать библиотеку в виде NIF. Так как я не нашел много информации по этому поводу, я решил поделиться своим опытом написания NIF в Elixir с вами. Как пример я буду использовать то, что я создал.

Итак, начнем с того, что я нашел библиотеку в Си, pigpio, в которой были все необходимые мне функции. Затем я создал новый проект с командой:

mix new ex_pigpio

К стандартным папкам, созданным автоматически программой mix, я добавил:
  • папку src: там я поместил исходный код NIF в Си
  • папку priv: там, при компиляции, появится библиотека ex_pigpio.so
  • файл Makefile: нужен для компиляции библиотеки ex_pigpio.so

Моим следующим шагом было написание самого кода NIF в Си. Вначале надо импортировать header функции NIF из VM Erlang:

#include <erl_nif.h>

Потом нужно описать какие именно функции данный NIF будет экспортировать в Elixir. Как пример, в моем случае:

static ErlNifFunc funcs[] = {
  { "set_mode", 2, set_mode },
  // ...
  { "get_pwm_range", 1, get_pwm_range }
};

funcs[] — это массив, который содержит в себе структуры из трех элементов. Первый элемент — это название функции в Elixir; второй — это количество параметров, принимаемых функцией; третий — указатель на саму функцию в Си. Сразу скажу, что название этого массива не имеет никакого значения и может быть любым.

К тому же, NIF надо зарегистрировать с помощью макро ERL_NIF_INIT. У меня это выглядит так:

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

Параметрами этого макро являются:

  1. Название модуля в Elixir с приставкой «Elixir.». В моем случае название модуля — это ExPigpio. Приставка нужна, поскольку название модуля меняется при компиляции и приобретает префикс «Elixir.»
  2. Массив с описанием функций NIF
  3. Указатели на функции, которые будут вызваны при загрузке, перезагрузке, обновлении и разгрузке библиотеки. Данные функции — это необязательные callback. Если какой-то из этих callback не нужен, то можно указать NULL вместо него.


Я бы хотел показать имплементацию функции get_pwm_range как пример NIF функции.

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));
  }
}

Все функции NIF должны принимать именно выше указанные параметры и возвращать результат типа ERL_NIF_TERM. Вы сможете найти все подробности на www.erlang.org/doc/man/erl_nif.html.

Итак код в Си готов. Теперь пишем модуль в Elixir. Его основной задачей будет загрузка библиотеку в Си и описание функций, реализуемых в 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

Обратите внимание на @on_load :init. Это регистрирует вызов функции init при загрузке модуля. Функция init находит библиотеку ex_pigpio.so в папке priv. Не нужно указывать суффикс ".so", т.к. он добавляется автоматически. Наконец, вызов функции :erlang.load_nif загружает библиотеку.

Для каждой функции из NIF в Elixir мы напишем функцию с таким же названием и количеством параметров. Эта функция будет вызвана в случае, если не получится загрузить NIF. Как правило функции, описанные в этом модуле Elixir, просто вызывают exit с параметром :nif_not_loaded. Тем не менее, их можно использовать и для альтернативной имплементации конечной функции.

Последний шаг — это компилировать наш проект. Для этого нам нужно создавать Makefile и внести требуемые изменения в mix.exs.

Пример 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

В таком Makefile нет ничего особенного. LDFLAGS и флаг "-DEMBEDDED_IN_VM" не требуются для всех NIF и являются специфическими для этого проекта. Переменная ERLANG_PATH, наоборот, есть необходимая вещь для всех NIF.

Теперь мы можем внести последние изменения в 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

Мы создаем модуль Mix.Tasks.Compile.Pigpio, который поможет нам компилировать библиотеку ex_pigpio.so. Он имплементирует функцию run, которая вызывает команду make с параметром «priv/ex_pigpio.so». Ниже, в функции project, в Keyword мы добавляем элемент «compilers» и указываем там наш модуль на первом месте, перед стандартными. Как вы видите, вместо полного названия модуля мы указали атом :pigpio, который отражает только последнюю часть.

Чтобы скомпилировать, даем команду:

mix compile

Итак, наш NIF готов! Полный исходный код находится здесь: github.com/briksoftware/ex_pigpio.
Tags:
Hubs:
+8
Comments 7
Comments Comments 7

Articles