[Из песочницы] Как написать свой NIF в Elixir
Совсем недавно я окунулся в мир роботики и решил запрограммировать собственного робота на основе 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_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.