Flow
Библиотека для декларативного связывания состояний
Мы разрабатываем продукты для платформ Linux, Эльбрус и Windows с использованием C++/Qt/Qml и C#/WPF (реже JVM языки) используя последние доступные для целевых платформ версии языков и фреймворков: C++ до 20, Qt до новейшего 6.x выпуска, C# до 9.x, .NET Core до 3.x. Применяем OpenCV, CUDA/OpenCL/Vulkan или даже чистый OpenGL для быстрой обработки изображений. MKL, скомпилированные библиотеки Matlab и др. для мат. обработки.

Как у каждой софтверной компании у нас есть собственная библиотека покрывающая большинство типовых задач, с которыми мы встречаемся на практике. Ниже кратко представлено описание наиболее интересных частей. Большая часть из описанного применима как к C++/Qt/Qml, так и к C#/WPF, но в последнее время мы больше работаем с первым стеком.
Декларативный подход бывает очень полезен в определенных аспектах разработки программного обеспечения. В отличии от типичного императивного способа мышления он позволяет создавать программы "сверху-вниз", от конечного результата до отдельных простых шагов его достижения.

Рассмотрим пример. Требуется создать компонент для интерактивного просмотра изображений на OpenGL с возможностью обработки в реальном времени. Начнем с конечного результата, т.е. рендеринга обработанного изображения на экране. Для этого нам требуется лишь матрица преобразований и текстура с обработанным изображением. Можно уже начинать писать реализацию использую (пока не существующие) источники этих двух компонент. Матрица преобразований — это просто комбинация заданных пользователем перемещения, увеличения и геометрических преобразований. OpenGL текстура зависит от исходного изображения и настроек обработки. Исходное изображение есть лишь функция от имени файла. И так далее, вверх по графу зависимостей.

Выстраивание алгоритма путем соединения таких блоков сверху вниз не только упрощает жизнь разработчику, но и позволяет впоследствии легко заменять отдельные части. Например, мы можем легко заменить источник исходного изображения на видеопоток с камеры без изменения остального кода. Или мы можем 60 раз в секунду изменять имя файла и получить на экране интерактивное видео с пост-обработкой.

Обычно так разрабатывают программные продукты в функциональном стиле — все есть лишь комбинация различных функций. Стоит отметить "ленивость" функций графа, вычисления производятся только когда они востребованы, скажем при исполнении "эффектов". Интерактивность может быть добавлена к этой схеме путем введения конвейера обработки изменений для информирования верхнего уровня о необходимости обновления.

Для решения наших повседневных задач мы разработали библиотеку названную "the Flow". Основным строительным блоком в ней является "источник", который поставляет потребителю результаты и оповещает об изменениях. Различные источники легко комбинируется вместе и используются как входные аргументы для других источников. Все оповещения распространяются по графу автоматически. Как было упомянуто выше, все вычисления являются ленивыми и выполняются только по запросу.

Данный подход имеет сходства со свойствами в Qt, но объединяет данные и систему оповещения вместе, что существенно упрощает комбинирование функций и передачу их по всему приложению. Больше никаких ручных вызовов update методов и соединения сигналов и слотов. Такой подход будет знаком адептам React с той разницей, что Flow базируется именно на текущем состоянии системы, а не на потоках данных.

Библиотека имеем целых ряд удобных возможностей. В частности, встроенный контроль жизни гарантирует существование источника пока он используется хоть одним потребителем. В С++ для этого используется std::shared_ptr, C# обеспечивает это по-умолчанию.

Подписчики долгоживущих источников не должны волноваться о своевременной подписке/отписке от событий, этим занимается ядро библиотеки. Помимо удобства, это помогает избавится от достаточно типичных причин утечек памяти. В C++ для этого используется std::weak_ptr, в C# — ConditionalWeakTable. При этом возможно и ручное переключение между различными источниками или даже разрыв связей благодаря использованию специальных компонентов.

Flow поощряет стиль работы с неизменяемыми структурами данных вместо отдельный значений. Если несколько значений интересуют внешние модули лишь как единое целое, то предпочтительно использовать именно такие структуры для уменьшения количества кода и взаимосвязей.

Библиотека полностью поддерживает принадлежность объектов к потокам в Qt и естественным образом обеспечивает потокобезопасность. Изменения любого источника могут быть осуществлены из любого потока без необходимости думать о lock'ах. Все действие и триггеры ("эффекты" в понимании FP) автоматически выполняются в нужных потоках. Можно также явно указать конкретный поток выполнения или даже выполнять расчеты в фоне. Это полезно в ряде случаев, включая работу с OpenGL, которая в значительной степени однопоточна.

Код, выполняемый в других потоках, может выбрасывать исключения. Flow перехватывает все такие случаи и обрабатывает их центральном обработчике, задаваемым разработчиком. При этом нет никаких препятствий для ручной обработки необходимых событий на месте.

Кроме встроенной ленивости Flow использует умное кеширование результатов вычислений. Так же легко реализовать собственный ограничитель кеша, настроенный на определенные применения. Например, можно сделать ограничитель для хранения численных результатов, и отдельную политику работы с GPU текстурами.

Также библиотека снабжена рядом вспомогательных классов. Конвертеры обеспечивают двустороннее связанное преобразование данных, ограничители нагрузки гибко регулируют поток сообщений об изменениях, барьеры предотвращают нежелательные промежуточные реакции на многократные изменения и пр.

Flow используется по всех наших проектах и распространяется под лицензией LGPL v3.0.
Метасистема, интеграция с Qml и сериализация

В отличие от C#, в C++ отсутствует рефлексия и, как следствие, данные о полях и методах класса недоступны во время выполнения. Метасистема фреймворка Qt частично решает эту проблему, но ограничена заложенным в нее функционалом. Также сложные типы данных, такие как умные указатели, абстрактные классы, динамические списки не имеют встроенной интеграции.

По этой причине в C++ версии Flow мы реализовали собственную метасистему для обеспечения более гибкой поддержки Qml, автоматической поддержки Json/Bson и пр. С помощью лишь одной строки кода любое поле или метод становятся доступными в Qml и могут быть автоматически [де]сериализованы. Система по определению динамическая, поскольку базируется на Flow.

Метасистема поддерживает собственные типы и большинство встроенных типов Qt, поддержка любых других сторонних типов может быть также легко добавлена. Она развивает идею интеграции с Qt путем создания "мостов" между C++ и Qml объектами. Например, числовые и строковые поля отображаются в Qml напрямую, но QVector<T> автоматически становится динамическим QAbstractItemModel. Поэтому можно сразу работать с ним как с полноценным списком — отображать, добавлять и удалять элементы без написания какого-либо кода. Реализация достаточно умна, чтобы отследить изменения отдельных элементов (сложность O(N)) и не обновлять список целиком. Или можно определить абстрактный класс с несколькими наследниками с разным поведением. Тогда их сразу можно использовать напрямую из Qml, получая доступ к их типу и отображая соответствующие части UI.

Встроенная поддержка вложенных объектов позволяет преобразовывать в Json весь граф объектов одной командой или получать доступ к объекту любого уровня вложенности из Qml через точку. Встроенная система версионирования поддерживает автоматические миграции данных при загрузке.

C# версия библиотеки намного проще, благодаря наличию полноценной рефлексии и включает в себя лишь интеграцию Flow с WPF, которая выглядит аналогично. В плане [де]сериализации мы клонировали Newtonsoft.Json и добавили в него поддержку версионирования.