Совсем недавно я окунулся в мир роботики и решил запрограммировать собственного робота на основе 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.

Комментарии (5)


  1. Lol4t0
    04.06.2015 09:16
    +2

    Здорово, но по-моему, по официальной документации написать ниф будет проще, чем по вашей статье.

    И вообще, так

    static int load(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) {
      ex_pigpio_priv* data = enif_alloc(sizeof(ex_pigpio_priv));
      //...
      data->atom_ok = enif_make_atom(env, "ok");
      data->atom_error = enif_make_atom(env, "error");
    


    делать нельзя
    ErlNifEnv represents an environment that can host Erlang terms. All terms in an environment are valid as long as the environment is valid. ErlNifEnv is an opaque type and pointers to it can only be passed on to API functions. There are two types of environments; process bound and process independent.

    A process bound environment is passed as the first argument to all NIFs. All function arguments passed to a NIF will belong to that environment. The return value from a NIF must also be a term belonging to the same environment. In addition a process bound environment contains transient information about the calling Erlang process. The environment is only valid in the thread where it was supplied as argument until the NIF returns. It is thus useless and dangerous to store pointers to process bound environments between NIF calls.


    1. brainnolo Автор
      04.06.2015 20:19

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

      data->atom_ok = enif_make_atom(env, "ok");
      

      Это можно делать, так как enif_make_atom является не задокументированным исключением из общего правила. Посмотрите тут: erlang.2086793.n4.nabble.com/erl-nif-environment-in-the-load-function-td4674510.html

      Normally the lifetime of a term is determined by its environment.
      However, atoms are an exception to this rule, which allows you to
      prefabricate atoms in static variables.

      This exceptions is undocumented but widely used (by myself included in
      crypto). There is a small risk that an introduction of atom garbage
      collection in some non predictable future release will have to break
      this feature.

      Однако так как это поведение не задокументировано, я не упомянул эту часть кода в статье.


      1. Lol4t0
        04.06.2015 22:33

        Не могли бы вы поделиться официальной документацией о том, как написать NIF в Elixir?

        Я имел в виду вот эту: www.erlang.org/doc/man/erl_nif.html и www.erlang.org/doc/tutorial/nif.html

        Там все-таки логичнее изложено.

        enif_make_atom является не задокументированным исключением из общего правила.

        Каждый раз, когда вы используете недокументированные возможности, где-то умирает котенок


        1. brainnolo Автор
          04.06.2015 22:45

          Там нет никакой информации о том, как использовать NIF в Elixir и компилировать его с помощью mix. Об этом, собственно говоря, и есть статья. Я сам даю ссылку на официальную документацию NIF в Си, так как Си-код не является главной темой статьи. А вот котов я не люблю и повторно говорю, что я никому не рекомендовал имплементировать load, как это сделал я, и я абсолютно ничего не писал об имплементации этой функции.


          1. Lol4t0
            04.06.2015 23:05
            +1

            Си-код не является главной темой статьи
            И тем не менее, C-код составляет половину статьи

            А вот котов я не люблю и повторно говорю, что я никому не рекомендовал имплементировать load, как это сделал я
            Нет, вы не поняли, это я вам указываю на ошибку в вашей библиотеке, которую необходимо исправить.
            Кроме того, если вы показываете код в статье, претендующей на обучающий материал, то вы не должны допускать в нем таких ляпов.