В 2017м году я начал писать проект на nodejs — реализацию протокола ObjectServer от Weinzierl для доступа к значениям KNX. В процессе написания было изучено: работа с бинарными протоколами, представление данных, работа с сокетами(unix sockets в частности), работа с redis базой данных и pub/sub каналами.
Проект достиг стабильной версии. В это время я потихоньку ковыряю другие языки, в частности Dart и Flutter как его приложение. На полке пылится без действия купленный во времена студенчества справочник Г.Шилдта.
Настойчивая мысль переписать проект на C поселилась в голове. Рассматриваю варианты Go, Rust, отталкивающие иными синтаксическими конструкциями. Начать никак не получается, идея откладывается на время.
В мае этого года решил посмотреть язык D, почему-то уверенный в том, что буква D означает dynamic. Долго гадал откуда и почему эта мысль была в голове, так ответа не нашел. НО это уже не важно, поскольку переписыванием увлекся я на все лето.
Суть проекта
Модули KNX BAOS 830/832/838 подключены через UART к компьютеру, протокол ObjectServer обернут в FT1.2. Приложение устанавливает соединение с /dev/ttyXXX, обрабатывает входящие данные, отправляет туда же конвертированные из JSON сообщения байты пользовательского запроса, приходящего на PUB/SUB канал, либо в очередь заданий на базе списков Redis-а (для nodejs очереди реализованы пакетом bee-queue).
queue.on("job", data => { // предполагая валидное задание: // конвертировать данные, отправить в серийный порт // возвратить промис, который разрешится при входящем ответе }); baos.on("data", data => { // понять, что это: индикация или ответ // если ответ, то разрешить промис из очереди // если индикация - обработать и отправить в pub/sub });
Динамичность
JSON в js — вещь нативная, как обработка происходит в статически типизированных языках я представления не имел. Как оказалось, разницы немного. Для примера взять метод get value. В качестве аргументов он принимает либо число — номер датапоинта, либо массив номеров.
В js выполняются проверки:
if (Array.isArray(payload)) { // получить значения для массива return values; } if (typeof id === "number") { // получить значения одного объекта return value; } throw new Error("Неправильный id");
По сути то же самое на D:
if (payload.type() == JSONType.integer) { // вернуть одно значение } else if (payload.type() === JSONType.array) { // вернуть массив значений } else { throw Errors.wrong_payload_type; }
Почему-то на момент рассмотрения Rust-a меня затормозило именно отсутствие представления о работе с JSON. Другой момент, связанный с динамичностью: массивы. В js привыкаешь к тому, что достаточно вызвать метод push для добавления элемента. На C динамичность реализуется ручным выделением памяти, а лезть туда не очень то и хотелось. Dlang, как оказалось, поддерживает динамические массивы.
ubyte[] res; // хорошая практика - сначала сделать массив больше res.length = 1000; // а после заполнения изменить длину на нужную res.length = count; // чем менять каждый шаг длину массива на 1
Входящие UART данные в js конвертировались в Object. Для этих целей в D отлично подходят структуры, перечисления со значениями и объединения.
enum OS_Services { unknown, GetServerItemReq = 0x01, GetServerItemRes = 0x81, SetServerItemReq = 0x02, SetServerItemRes = 0x82, // ... } // ... struct OS_Message { OS_Services service; OS_MessageDirection direction; bool success; union { // union of possible service returned structs // DatapointDescriptions/DatapointValues/ServerItems/ParameterBytes OS_DatapointDescription[] datapoint_descriptions; OS_DatapointValue[] datapoint_values; OS_ServerItem[] server_items; Exception error; }; }
При входящем сообщении:
ubyte mainService = data.read!ubyte(); ubyte subService = data.read!ubyte(); try { if (mainService == OS_MainService) { switch(subService) { case OS_Services.GetServerItemRes: result.direction = OS_MessageDirection.response; result.service= OS_Services.GetServerItemRes; result.success = true; result.server_items = _processServerItemRes(data); break; case OS_Services.SetServerItemRes: result.direction = OS_MessageDirection.response; // ...
В js я байтовые значения хранил в массиве, при входящих данных делал поиск и возвращал строку с именем сервиса. Структуры, перечисления и объединения выглядят строже.
Работа с массивами байтовых данных
Node.js мне нравится абстракция Buffer. Для примера: преобразования двух байтов в беззнаковое целое удобно выполнять методом readUInt16BE(offset), для записи — writeUInt16BE(value, offset), буфферы активно использовал для работы с бинарным протоколом. Для dlang я изначально начал шерстить репозиторий пакетов на что-либо похожее. Ответ нашелся в стандартной библиотеке std.bitmanip. Для беззнаковых целых длиной 2 байта: ushort start = data.read!ushort(), для записи: result.write!ushort(start, 2);, где 2й аргумент — смещение.
EE, promises, async/await.
Самым тяжелым представлялось программирование без EventEmitter. В node.js просто регистрируются функции слушатели, и при событии они вызываются. Таким образом, сильно думать не надо. В dlang пакетах tinyredis и serialport (зависимости моего приложения) есть неблокирующие методы для обработки сообщений. Решение простое: пока истина получать по очереди сообщения серийного порта и pub/sub канала. В случае входящего пользовательского запроса на pub/sub канал программа должна отправить сообщение в серийный порт, получить результат и отправить пользователю обратно в pub/sub. Методы для серийных запросов решено было сделать блокирующими.
while(!(_responseReceived || _resetInd || _interrupted)) { try { processIncomingData(); processIncomingInterrupts(); if (_resetInd || _interrupted) { _response.success = false; _response.service = OS_Services.unknown; _response.error = Errors.interrupted; _responseReceived = true; _ackReceived = true; } // ... // ... return _response;
В цикле while данные опрашиваются неблокирующим методом processIncomingData(). Так же предусмотрена вероятность того, что KNX модуль может быть перезагружен (отключен и подключен заново к шине KNX или программно). Также обработчик processIncomingInterrupts() проверяет сервисный pub/sub канал на запрос reset. Никаких промисов и асинхронных функций, в отличие от предыдущих реализаций на js. Пришлось подумать над структурой программы (а именно последовательности вызова функций), но, засчет отсутствия лишних абстракций, программировать стало проще. По сути, когда в js коде вызывается await someAsyncMethod — асинхронная функция вызывается как блокирующая, проходя при этом через event loop. Сама возможность языка — это хорошо, но ведь можно обойтись и без нее.
Отличия
Очередь заданий. В node.js реализации для этой цели используется пакет bee-queue. В реализации на D запросы отправляются только через pub/sub.
В остальном все практически идентично.
Оперативной памяти компилируемая версия потребляет в 10 раз меньше, что может быть важно для одноплатных компьютеров.
Компиляция
Компиляция проводилась при помощи ldc на платформе aarch64.
Для установки ldc:
curl -fsS https://dlang.org/install.sh | bash -s ldc
Была собрана плата, состоящая из трех основных компонентов: NanoPi Neo Core2 качестве компьютера, KNX BAOS module 830 для связи с шиной KNX и Silvertel Ag9205 для PoE питания, на которой и осуществлялось программирование.

Заключение
Не буду судить, какой язык лучше или хуже. Каждому свое: js отлично подходит для изучения, уровень абстракций(промисы, эмиттеры) позволяют достаточно легко и быстро строить структуру приложения. К реализации на dlang я подошел уже с ясным, заученным за полтора года, планом что делать. Когда знаешь какие данные необходимо обрабатывать и каким образом, статическая типизация не страшна. Неблокирующие методы позволяют организовать рабочий цикл. Это была первая моя работа на D, работа увлекательная и познавательная.
Насчет выхода из зоны комфорта (как указано в названии): в моем случае — у страха были глаза велики, что долго мешало мне попробовать что-то, помимо nodejs.
Исходные коды открыты и могут быть найдены на github.com/dobaos/dobaos
