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

27 декабря 2012

Недавно мне потребовалось определить 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.

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.