Совсем недавно я окунулся в мир роботики и решил запрограммировать собственного робота на основе 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)
Параметрами этого макро являются:
- Название модуля в 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.
ссылка на оригинал статьи http://habrahabr.ru/post/259467/
Добавить комментарий