Совсем недавно я окунулся в мир роботики и решил запрограммировать собственного робота на основе RasPi. Для этого я использовал Elixir, сравнительно новый, к слову сказать, язык программирования, который компилируется в байткод для Erlang VM. У меня сразу же возникла трудность с управлением контактами GPIO. Тогда я нашел библиотеку, которая вроде бы решала все мои проблемы. Однако она была написана как Port, из-за чего каждый вызов ее функций занимал слишком много времени, что влияло на правильность работы моего робота.
Немного подумав, я все-таки решился переписать библиотеку в виде NIF. Так как я не нашел много информации по этому поводу, я решил поделиться своим опытом написания NIF в Elixir с вами. Как пример я буду использовать то, что я создал.
Итак, начнем с того, что я нашел библиотеку в Си, pigpio, в которой были все необходимые мне функции. Затем я создал новый проект с командой:
К стандартным папкам, созданным автоматически программой mix, я добавил:
Моим следующим шагом было написание самого кода NIF в Си. Вначале надо импортировать header функции NIF из VM Erlang:
Потом нужно описать какие именно функции данный NIF будет экспортировать в Elixir. Как пример, в моем случае:
funcs[] — это массив, который содержит в себе структуры из трех элементов. Первый элемент — это название функции в Elixir; второй — это количество параметров, принимаемых функцией; третий — указатель на саму функцию в Си. Сразу скажу, что название этого массива не имеет никакого значения и может быть любым.
К тому же, NIF надо зарегистрировать с помощью макро ERL_NIF_INIT. У меня это выглядит так:
Параметрами этого макро являются:
Я бы хотел показать имплементацию функции get_pwm_range как пример NIF функции.
Все функции NIF должны принимать именно выше указанные параметры и возвращать результат типа ERL_NIF_TERM. Вы сможете найти все подробности на www.erlang.org/doc/man/erl_nif.html.
Итак код в Си готов. Теперь пишем модуль в Elixir. Его основной задачей будет загрузка библиотеку в Си и описание функций, реализуемых в NIF.
Обратите внимание на @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:
В таком Makefile нет ничего особенного. LDFLAGS и флаг "-DEMBEDDED_IN_VM" не требуются для всех NIF и являются специфическими для этого проекта. Переменная ERLANG_PATH, наоборот, есть необходимая вещь для всех NIF.
Теперь мы можем внести последние изменения в mix.exs.
Мы создаем модуль Mix.Tasks.Compile.Pigpio, который поможет нам компилировать библиотеку ex_pigpio.so. Он имплементирует функцию run, которая вызывает команду make с параметром «priv/ex_pigpio.so». Ниже, в функции project, в Keyword мы добавляем элемент «compilers» и указываем там наш модуль на первом месте, перед стандартными. Как вы видите, вместо полного названия модуля мы указали атом :pigpio, который отражает только последнюю часть.
Чтобы скомпилировать, даем команду:
Итак, наш NIF готов! Полный исходный код находится здесь: github.com/briksoftware/ex_pigpio.
Немного подумав, я все-таки решился переписать библиотеку в виде 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)
Параметрами этого макро являются:
- Название модуля в Elixir с приставкой «Elixir.». В моем случае название модуля — это ExPigpio. Приставка нужна, поскольку название модуля меняется при компиляции и приобретает префикс «Elixir.»
- Массив с описанием функций NIF
- Указатели на функции, которые будут вызваны при загрузке, перезагрузке, обновлении и разгрузке библиотеки. Данные функции — это необязательные 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.
Lol4t0
Здорово, но по-моему, по официальной документации написать ниф будет проще, чем по вашей статье.
И вообще, так
делать нельзя
brainnolo Автор
Не могли бы вы поделиться официальной документацией о том, как написать NIF в Elixir? Когда я начал писать мои NIF, я не нашел никакой официальной информации об этом. Заметьте, что цель статьи заключается именно в том, чтобы показать как написать и использовать NIF в Elixir. По этой причине я обратил больше внимания на то, как интегрировать NIF с mix и модулями Elixir.
Это можно делать, так как enif_make_atom является не задокументированным исключением из общего правила. Посмотрите тут: erlang.2086793.n4.nabble.com/erl-nif-environment-in-the-load-function-td4674510.html
Однако так как это поведение не задокументировано, я не упомянул эту часть кода в статье.
Lol4t0
Я имел в виду вот эту: www.erlang.org/doc/man/erl_nif.html и www.erlang.org/doc/tutorial/nif.html
Там все-таки логичнее изложено.
Каждый раз, когда вы используете недокументированные возможности, где-то умирает котенок
brainnolo Автор
Там нет никакой информации о том, как использовать NIF в Elixir и компилировать его с помощью mix. Об этом, собственно говоря, и есть статья. Я сам даю ссылку на официальную документацию NIF в Си, так как Си-код не является главной темой статьи. А вот котов я не люблю и повторно говорю, что я никому не рекомендовал имплементировать load, как это сделал я, и я абсолютно ничего не писал об имплементации этой функции.
Lol4t0
Нет, вы не поняли, это я вам указываю на ошибку в вашей библиотеке, которую необходимо исправить.
Кроме того, если вы показываете код в статье, претендующей на обучающий материал, то вы не должны допускать в нем таких ляпов.