← На главную

Написал свою первую NIF-библиотеку

Недавно мне потребовалось определить load average системы из Erlang. Готового решения найти не удалось. В результате была написана небольшая NIF библиотека для решения этой задачи. NIF (Native Implemented Function) – это функция, написанная на Си, которую можно вызывать из Erlang'а. Эта функция должна находится в динамической библиотеке, которая, соответственно, называется NIF-библиотекой (NIF library).

Как выяснилось, пишутся NIF'ки довольно просто. Заводим новый модуль:

-module(load_average). -export([get/0]). -on_load(init/0). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. init() -> case code:which(load_average) of Filename when is_list(Filename) -> erlang:load_nif(filename:join([filename:dirname(Filename), "..","priv", "load_average"]), []); Reason when is_atom(Reason) -> {error, Reason} end. -spec get() -> {ok, {float(),float(),float()}} | { error, atom() }. % @doc Returns system load average get() -> erlang:nif_error(nif_library_not_loaded). -ifdef(TEST). get_test() -> {ok,{X,Y,Z}} = ?MODULE:get(), true = is_float(X), true = is_float(Y), true = is_float(Z). -endif.

Главное здесь – это вызов функции erlang:load_nif/2. Первым аргументом ей должен быть передан путь к NIF-библиотеке. Путь этот может немного меняться в зависимости от всякого, поэтому используется прием с определением пути к beam-файлу текущего модуля.

Код NIF-библиотеки размещаем в c_src/load_average.c:

#include "erl_nif.h" static ERL_NIF_TERM get( ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { double load_avg[3]; int loads; loads = getloadavg(load_avg, 3); if(loads != 3) { return enif_make_tuple( env, 2, enif_make_atom(env, "error"), enif_make_atom(env, "invalid_result") ); } return enif_make_tuple( env, 2, enif_make_atom(env, "ok"), enif_make_tuple( env, 3, enif_make_double(env, load_avg[0]), enif_make_double(env, load_avg[1]), enif_make_double(env, load_avg[2]) ) ); } static ErlNifFunc nif_funcs[] = { {"get", 0, get} }; ERL_NIF_INIT(load_average, nif_funcs, NULL, NULL, NULL, NULL)

Как видите, вся работа сводится к преобразованию сишных типов в Erlang'овские, а также в вызове функции getloadavg. При написании этого кода мне здорово помог вот этот мануал.

Финальным аккордом дописываем в rebar.config:

{port_specs, [ {"priv/load_average.so", ["c_src/load_average.c"]} ]}.

Компилируем:

rebar compile

... и NIF-библиотека готова! Полную версию исходного кода вы найдете в этом архиве. В целом от открытия мануала до получения готовой библиотеки у меня ушло менее часа. Вообще, в Erlang'е все очень просто и логично устроено, и ко всему есть хорошая документация. Меня, как новичка в данном языке, это ужасно радует.

Как заверил меня более опытный коллега, NIF'ы – они очень быстрые, но когда падают, тянут за собой всю виртуальную машину Erlang'а. И еще они как бы не очень хорошо переносимы. И есть у меня подозрения, что с многопоточностью там не все просто. В данном конкретном случае, например, вместо написания NIF можно было вызвать утилиту uptime с помощью os:cmd/1 и пропарсить ее вывод. Тут есть свои минусы, но в целом подход нормальный.

Дополнение: Особую опасность представляют собой NIF, которые выполняются долго (условно, больше 1-2 мс). Такие NIF останавливают выполнение всех акторов, живущих с ними на одном треде. Возможные решения этой проблемы описаны в секции Long-running NIFs документации к библиотеке erl_nif.

Дополнение: См также мою заметку о трассировке в Erlang.