четверг, 22 января 2015 г.

Краткое введение в Erlang C-node

Однажды в жизни Erlang проекта может возникнуть необходимость вызывать функции из внешних библиотек(FFI). Увы, как бы нам ни хотелось остаться в рамках функционального программирования.
Erlang предоставляет следующие способы выполнения FFI:
Эти способы являются популярными, но имеют существенный недостаток - оба они являются не безопасными по отношению к работе Erlang VM.


Выполнение кода NIF и port driver не управляется планировщиком VM, а так же VM не выполняет защиту памяти при выполнении такого кода. Отсюда масса проблем - неверное управление выделением памяти или манипуляция данными может привести к порче внутреннего состояния VM, длительное исполнение такого кода ухудшает отзывчивость VM. Обе проблемы как правило приводят к краху виртуальной машины.

По счастью есть и третий способ, являющийся достаточно безопасным - C-node. Если быть совсем уж честными, то C-node это не вызов функций из подключаемой библиотеки, это все тот же message passing, только на C, со всеми плюсами и минусами. Ваше приложение, написанное как C-node, для Erlang кода выглядит как еще одна нода с неким именем и процессами внутри.
Скорее всего для многих задач С-node оказывается overhead решением, но если нам нужно достаточно часто выполнять C-шный код и этот код достаточно сложен(особенно в отладке ;) ), то как мне кажется, эта работа как раз для нее. Документация на C-node достаточно исчерпывающа, поэтому я ограничусь кратким введением.
Итак, перво-наперво необходимо выполнить инициализацию:

erl_init(NULL, 0)
, затем может быть выполнена инициализация ноды:

erl_connect_init(1, "secretcookie", 0)
, для коротких имен, или:

erl_connect_xinit("idril", "cnode", "cnode@idril.ericsson.se", &addr, "secretcookie", 0)
, для длинных.

В случае использования коротких имен, мы не можем задать произвольное имя ноды. Оно генерируется автоматически и будет вида: cX , где X - целое число, которое мы передаем в ф-ции erl_connect_init первым параметром, вторым параметром идет указатель на строку содержащую Erlang magic cookie, и третьим параметром ф-ции следует номер экземпляра ноды(на случай, если вам вздумается запустить несколько экземпляров одной и той же ноды).
Для длинных имен имеется больше пространства для творчества. Тут необходимо указать имя хоста (в примере "idril"), короткое имя ноды (в примере "cnode"), полное имя ноды, указатель на структуру типа in_addr(суть есть IP адрес), куки и все тот же номер экземпляра процесса.

C-нода может работать в двух режимах - как клиент и как сервер. C-node клиент способен подключаться к работающим Erlang нодам при помощи функции:
int erl_connect(char nodename*)
Сервер же для начала должен создать сокет на неком порту и сообщить этот номер порта epmd. Это можно проиллюстрировать вот таким куском кода:

if ((listen = my_listen(port)) <= 0)
        erl_err_quit("my_listen");
if (erl_publish(port) == -1)
     erl_err_quit("erl_publish");
 Функция my_listen в данном примере выполняет bind() и listen() и в случае успешного выполнения, возвращает дескриптор сокета . Затем ожидаем коннекты от других нод:
int erl_accept(int listen, ErlConnect *conn)
Функция возвращает  файловый дескриптор конкретного соединения и инициализирует структуру типа ErlConnect, поля ipaddr и nodename которой, содержать IP-адрес и имя подключившейся ноды, соответственно.
Дальше можно принимать/отправлять сообщения. Принимаем следующим образом:

int erl_receive_msg(int fd, char *buf, int BUFSIZE, ErlMessage *emsg)

Функция читает из указанного дескриптора, кладет полученное сообщение в структуру emsg и возвращает одно из следующих значений::

  • ERL_TICK - в случае если получен пакет проверки работоспособности ноды. Отвечать на такой пакет не требуется;
  • ERL_ERROR - в случае ошибки получения сообщения или в том случае если нода с которой была коммуникация, упала;
  • ERL_MSG - получено обычное сообщение;
Структура типа ErlMessage имеет следующий вид:
typedef struct {
  int type;
  ETERM *msg;
  ETERM *to;
  ETERM *from;
  char to_name[MAXREGLEN];
} ErlMessage
Поле type этой структуры содержит информацию о типе полученного сообщения. Тип может быть следующим:

  • ERL_SEND
  • ERL_REG_SEND
  • ERL_LINK
  • ERL_UNLINK
  • ERL_EXIT
В зависимости от типа сообщения, поля to и from могут содержать различную информацию.
Для типов ERL_SEND и ERL_REG_SEND, поле msg содержит указатель на само сообщение, а поля to и from содержат pid процессов получателя/отправителя. Для ERL_LINK, ERL_UNLINK поля to и from содержат pid процессов для которых выполняются действия link/unlink, а поле msg не используется. Для типа ERL_EXIT, который сигнализирует о том, что линк между процессами "сломался", используются поля to и from, содержащие pid слинкованных процессов, а поле msg содержит причину по которой линк оборван.
Теперь, когда сообщение получено(я рассматриваю случай с типом сообщения ERL_SEND/ERL_REG_SEND), мы можем извлечь необходимые данные из структуры msg, которая имеет тип ETERM. В примере, который дан в документации, в ноду отправляется сообщение содержащее кортеж, первым элементом которого идет pid процесса отправившего сообщение. Ответ отправляется следующим образом:
int erl_send(int fd, ETERM *from, ETERM *resp)
 , где from терм содержащие pid получателя, а resp - терм, который необходимо отправить.
Для отправки на зарегистрированное имя, используется функция:
int erl_reg_send(int fd, char *to, ETERM *msg)
Обе ф-ции возвращают 1 в случае успешной отправки сообщения.
По окончании работы с некоторым термом необходимо высвободить память с помощью функции:
void erl_free_term(ETERM *t)
 Память под термы выделяется функциями инициализации и в том числе, функцией erl_receive_msg().
На этом все. Код примера, который есть в документации, я модернизировал для себя, добавив в него поддержку Posix Thread, а так же добавив исполнение реальных функций, возвращающих в качестве результата ETERM *t. Получилась вполне работающая болванка для небольших реальных проектов. Код можно найти в моем репозитории на Github.
Всяческие конструктивные замечания по поводу содержимого кода и статьи, категорически приветствуются.

Комментариев нет:

Отправить комментарий