Блок-схемы на практике без формалина

Блок-схемы — это наглядный инструмент, который помогает превратить сложный алгоритм в понятную и структурированную последовательность действий. От программирования до управления бизнес-процессами, они служат универсальным языком для визуализации, анализа и оптимизации самых сложных систем.

Представьте себе карту, где вместо дорог — логика, а вместо городов — действия. Это и есть блок-схема — незаменимый инструмент для навигации по самым запутанным процессам.

Пример 1: Упрощённая схема запуска игры
Чтобы понять принцип работы, давайте представим простую схему запуска игры.

Эта схема показывает идеальный сценарий, когда всё происходит без сбоев. Но в реальной жизни всё гораздо сложнее.

Пример 2: Расширенная схема запуска игры с загрузкой данных
Современные игры часто требуют подключения к интернету для загрузки данных пользователя, сохранений или настроек. Давайте добавим эти шаги в нашу схему.

Эта схема уже более реалистична, но что произойдёт, если что-то пойдёт не так?

Как было: Игра, которая “ломалась” при потере интернета

На старте проекта разработчики могли не учесть всех возможных сценариев. Например, они сосредоточились на основной логике игры и не подумали, что произойдет, если у игрока пропадет интернет-соединение.

В такой ситуации блок-схема их кода выглядела бы так:

В этом случае, вместо того чтобы выдать ошибку или корректно закрыться, игра замирала на этапе ожидания данных, которых не получала из-за отсутствия соединения. Это приводило к “чёрному экрану” и зависанию приложения.

Как стало: Исправление по жалобам пользователей

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

Вот как выглядит исправленная блок-схема, где учтены оба сценария:

Благодаря такому подходу, игра теперь корректно информирует пользователя о проблеме, а в некоторых случаях даже может перейти в офлайн-режим, позволяя продолжить игру. Это наглядный пример того, почему блок-схемы так важны: они заставляют разработчика думать не только об идеальном пути выполнения, но и о всех возможных сбоях, делая итоговый продукт гораздо более стабильным и надёжным.

Неопределенное поведение

Зависания и ошибки — это лишь один из примеров непредсказуемого поведения программы. В программировании существует понятие неопределённого поведения (Undefined Behavior) — это ситуация, когда стандарт языка не описывает, как должна вести себя программа в определённом случае.

Это может привести к чему угодно: от случайного “мусора” в выводе до сбоя программы или даже серьёзной уязвимости безопасности. Неопределённое поведение часто возникает при работе с памятью, например, со строками в языке C.

Пример из языка C:

Представьте, что разработчик скопировал строку в буфер, но забыл добавить в конец нулевой символ (`\0`), который отмечает конец строки.

Вот как выглядит код:

#include 

int main() {
char buffer[5];
char* my_string = "hello";

memcpy(buffer, my_string, 5);

printf("%s\n", buffer);
return 0;
}

Ожидаемый результат: “hello”
Реальный результат Непредсказуем.

Почему так происходит? Функция `printf` с спецификатором `%s` ожидает, что строка завершается нулевым символом. Если его нет, она продолжит читать память за пределами выделенного буфера.

Вот блок-схема этого процесса с двумя возможными исходами:

Это наглядный пример того, почему блок-схемы так важны: они заставляют разработчика думать не только об идеальном пути выполнения, но и о всех возможных сбоях, включая такие низкоуровневые проблемы, делая итоговый продукт гораздо более стабильным и надёжным.

Pixel Perfect: Миф или реальность в эпоху декларативности?

В мире разработки интерфейсов существует расхожее понятие – “pixel perfect вёрстка”. Оно подразумевает максимально точное воспроизведение дизайн-макета до мельчайшего пикселя. Долгое время это было золотым стандартом, особенно в эру классического веб-дизайна. Однако с приходом декларативной вёрстки и стремительным ростом разнообразия устройств, принцип “pixel perfect” становится всё более эфемерным. Попробуем разобраться, почему.

Императивный WYSIWYG vs. Декларативный Код: В чём разница?

Традиционно многие интерфейсы, особенно десктопные, создавались с помощью императивных подходов или WYSIWYG (What You See Is What You Get) редакторов. В таких инструментах дизайнер или разработчик напрямую манипулирует элементами, располагая их на холсте с точностью до пикселя. Это похоже на работу с графическим редактором – вы видите, как ваш элемент выглядит, и можете точно его позиционировать. В этом случае достижение “pixel perfect” было вполне реальной целью.

Однако современная разработка всё чаще опирается на декларативную вёрстку. Это означает, что вы не говорите компьютеру “помести эту кнопку сюда”, а описываете, что вы хотите получить. Например, вместо того чтобы указывать конкретные координаты элемента, вы описываете его свойства: “эта кнопка должна быть красной, иметь отступы 16px со всех сторон и находиться в центре контейнера”. Фреймворки вроде React, Vue, SwiftUI или Jetpack Compose как раз и используют этот принцип.

Почему “Pixel Perfect” не работает с декларативной вёрсткой для множества устройств

Представьте себе, что вы создаёте приложение, которое должно одинаково хорошо выглядеть на iPhone 15 Pro Max, Samsung Galaxy Fold, iPad Pro и телевизоре с разрешением 4K. Каждое из этих устройств имеет разное разрешение экрана, плотность пикселей, соотношение сторон и физические размеры.

Когда вы используете декларативный подход, система сама решает, как отобразить ваш описанный интерфейс на конкретном устройстве, учитывая все его параметры. Вы задаёте правила и зависимости, а не жёсткие координаты.

* Адаптивность и Отзывчивость: Основная цель декларативной вёрстки — создать адаптивные и отзывчивые интерфейсы. Это значит, что ваш интерфейс должен автоматически подстраиваться под размер и ориентацию экрана, не ломаясь и сохраняя читаемость. Если бы мы стремились к “pixel perfect” на каждом устройстве, нам пришлось бы создавать бесчисленное количество вариантов одного и того же интерфейса, что полностью нивелирует преимущества декларативного подхода.
* Плотность Пикселей (DPI/PPI): Устройства имеют разную плотность пикселей. Один и тот же элемент, имеющий размер 100 “виртуальных” пикселей, на устройстве с высокой плотностью будет выглядеть гораздо меньше, чем на устройстве с низкой плотностью, если не учитывать масштабирование. Декларативные фреймворки абстрагируются от физических пикселей, работая с логическими единицами.
* Динамический Контент: Контент в современных приложениях часто бывает динамическим – его объём и структура могут меняться. Если бы мы жёстко привязывались к пикселям, любое изменение текста или изображения привело бы к “разваливанию” макета.
* Различные Платформы: Помимо разнообразия устройств, существуют и разные операционные системы (iOS, Android, Web, Desktop). Каждая платформа имеет свои гайдлайны по дизайну, стандартные элементы управления и шрифты. Попытка сделать абсолютно идентичный, “pixel perfect” интерфейс на всех платформах привела бы к неестественному виду и плохому пользовательскому опыту.

Старые Подходы Не Ушли, А Эволюционировали

Важно понимать, что подход к вёрстке интерфейсов не является бинарным выбором между “императивным” и “декларативным”. Исторически для каждой платформы существовали свои инструменты и подходы к созданию интерфейсов.

* Нативные Интерфейсные Файлы: Для iOS это были XIB/Storyboards, для Android – XML-файлы разметки. Эти файлы представляют собой pixel-perfect WYSIWYG верстку, которая затем отображается в рантайме также как и в редакторе. Этот подход никуда не исчез, он продолжает развиваться, интегрируясь с современными декларативными фреймворками. Например, SwiftUI в Apple и Jetpack Compose в Android ступили на путь чисто декларативного кода, но при этом сохранили возможность использовать классическую верстку.
* Гибридные Решения: Часто в реальных проектах используется комбинация подходов. Например, базовая структура приложения может быть реализована декларативно, а для специфических, требующих точного позиционирования элементов, могут применяться более низкоуровневые, императивные методы или же подключаться нативные компоненты, разработанные с учётом специфики платформы.

От Монолита к Адаптивности: Как Эволюция Устройств Сформировала Декларативную Вёрстку

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

* Смартфонах всех форм-факторов и размеров экрана.
* Планшетах с их уникальными режимами ориентации и разделенного экрана.
* Ноутбуках и десктопах с различными разрешениями мониторов.
* Телевизорах и медиацентрах, управляемых дистанционно. Примечательно, что даже для телевизоров, пульты которых могут быть простыми, как Apple TV Remote с минимумом кнопок, или наоборот, перегруженными множеством функций, современные требования к интерфейсам таковы, что код не должен требовать специфической адаптации под эти особенности ввода. Интерфейс должен работать “как бы сам собой”, без дополнительного описания того, “как” именно взаимодействовать с конкретным пультом.
* Умных часах и носимых устройствах с минималистичными экранами.
* Шлемах виртуальной реальности (VR), требующих совершенно нового подхода к пространственному интерфейсу.
* Устройствах дополненной реальности (AR), накладывающих информацию на реальный мир.
* Автомобильных информационно-развлекательных системах.
* И даже бытовой технике: от холодильников с сенсорными экранами и стиральных машин с интерактивными дисплеями до умных духовок и систем “умного дома”.

Каждое из этих устройств имеет свои уникальные особенности: физические размеры, соотношение сторон, плотность пикселей, методы ввода (сенсорный экран, мышь, контроллеры, жесты, голосовые команды) и, что немаловажно, тонкости пользовательского окружения. Например, VR-шлем требует глубокого погружения, а смартфон — быстрой и интуитивной работы на ходу, тогда как интерфейс холодильника должен быть максимально простым и крупным для быстрой навигации.

Классический Подход: Бремя Поддержки Отдельных Интерфейсов

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

* Разработка под iOS часто требовала использования Storyboards или XIB-файлов в Xcode, написания кода на Objective-C или Swift.
* Для Android создавались XML-файлы разметки и код на Java или Kotlin.
* Веб-интерфейсы верстались на HTML/CSS/JavaScript.
* Для C++ приложений на различных десктопных платформах использовались свои специфические фреймворки и инструментарии:
* В Windows это были MFC (Microsoft Foundation Classes), Win32 API с ручной отрисовкой элементов или с использованием ресурсных файлов для диалоговых окон и элементов управления.
* В macOS применялись Cocoa (Objective-C/Swift) или старые Carbon API для прямого управления графическим интерфейсом.
* В Linux/Unix-подобных системах часто использовались библиотеки вроде GTK+ или Qt, которые предоставляли свой набор виджетов и механизмы для создания интерфейсов, нередко через XML-подобные файлы разметки (например, .ui файлы в Qt Designer) или прямое программное создание элементов.

Этот подход обеспечивал максимальный контроль над каждой платформой, позволяя учитывать все её специфические особенности и нативные элементы. Однако у него был огромный недостаток: дублирование усилий и колоссальные затраты на поддержку. Малейшее изменение в дизайне или функциональности требовало внесения правок в несколько, по сути, независимых кодовых баз. Это превращалось в настоящий кошмар для команд разработчиков, замедляя выход новых функций и увеличивая вероятность ошибок.

Декларативная Вёрстка: Единый Язык для Разнообразия

Именно в ответ на это стремительное усложнение и появилась декларативная вёрстка как доминирующая парадигма. Фреймворки вроде React, Vue, SwiftUI, Jetpack Compose и других представляют собой не просто новый способ написания кода, а фундаментальный сдвиг в мышлении.

Основная идея декларативного подхода: вместо того чтобы говорить системе “как” отрисовывать каждый элемент (императивно), мы описываем “что” мы хотим увидеть (декларативно). Мы задаём свойства и состояние интерфейса, а фреймворк сам решает, как наилучшим образом отобразить его на конкретном устройстве.

Это стало возможно благодаря следующим ключевым преимуществам:

1. Абстракция от Деталей Платформы: Декларативные UI фреймворки специально разработаны, чтобы забыть о низкоуровневых деталях отрисовки и специфике каждой платформы. Разработчик описывает компоненты и их взаимосвязи на более высоком уровне абстракции, используя единый, переносимый код.
2. Автоматическая Адаптация и Отзывчивость: Фреймворки берут на себя ответственность за автоматическое масштабирование, изменение макета и адаптацию элементов под различные размеры экранов, плотности пикселей и методы ввода. Это достигается за счёт использования гибких систем компоновки, таких как Flexbox или Grid, и концепций, подобных “логическим пикселям” или “dp” (density-independent pixels).
3. Согласованность Пользовательского Опыта: Несмотря на внешние различия, декларативный подход позволяет поддерживать единую логику поведения и взаимодействия по всему семейству устройств. Это упрощает процесс тестирования и обеспечивает более предсказуемый пользовательский опыт.
4. Ускорение Разработки и Снижение Затрат: С одним и тем же кодом, способным работать на множестве платформ, значительно снижаются время и стоимость разработки и поддержки. Команды могут сосредоточиться на функциональности и дизайне, а не на многократном переписывании одного и того же интерфейса.
5. Готовность к Будущему: Способность абстрагироваться от специфики текущих устройств делает декларативный код более устойчивым к появлению новых типов устройств и форм-факторов. Фреймворки могут быть обновлены для поддержки новых технологий, а ваш уже написанный код получит эту поддержку относительно бесшовно.

Заключение

Декларативная вёрстка — это не просто модное веяние, а необходимый эволюционный шаг, вызванный бурным развитием пользовательских устройств, включая и сферу интернета вещей (IoT) и умной бытовой техники. Она позволяет разработчикам и дизайнерам создавать сложные, адаптивные и единообразные интерфейсы, не утопая в бесконечных специфических реализациях для каждой платформы. Переход от императивного контроля над каждым пикселем к декларативному описанию желаемого состояния — это признание того, что в мире будущего интерфейсы должны быть гибкими, переносимыми и интуитивно понятными вне зависимости от того, на каком экране они отображаются.

Программистам, дизайнерам и пользователям необходимо научиться жить в этом новом мире. Лишние детали “pixel perfect” дизайна, привязанные к конкретному устройству или разрешению, приводят к ненужным временным затратам на разработку и поддержку. Более того, такие жёсткие макеты могут просто не отработать на устройствах с нестандартными интерфейсами, таких как телевизоры с ограниченным вводом, VR- и AR-шлемы, а также другие устройства будущего, о которых мы сегодня ещё даже не догадываемся. Гибкость и адаптивность – вот ключи к созданию успешных интерфейсов в современном мире.

Почему у программистов ничего не получается даже с нейросетями

Сегодня нейросети используются повсеместно. Программисты применяют их для генерации кода, объяснения чужих решений, автоматизации рутинных задач и даже создания целых приложений с нуля. Казалось бы, это должно привести к повышению эффективности, снижению ошибок и ускорению разработки. Но реальность оказывается намного прозаичнее: у многих по-прежнему ничего не получается. Нейросети не решают ключевые проблемы — они лишь подсвечивают глубину незнания.

Полная зависимость от LLM вместо понимания

Главная причина в том, что многие разработчики полностью полагаются на LLM, игнорируя необходимость глубокого понимания инструментов, с которыми они работают. Вместо изучения документации — запрос в чат. Вместо разбора причин ошибки — копирование решения. Вместо архитектурных решений — генерация компонентов по описанию. Всё это может работать на поверхностном уровне, но как только возникает нестандартная задача, требуется интеграция с реальным проектом или необходимость тонкой настройки, всё рассыпается.

Отсутствие контекста и устаревшие практики

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

Избыточность, неэффективность и отсутствие профилирования

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

Нейросеть не проводит профилирование, не учитывает ограничения CPU и GPU, не заботится об утечках памяти. Она не анализирует, насколько эффективно выполняется код на практике. Оптимизация — это по-прежнему ручная работа, требующая анализа и экспертизы. Без неё приложения становятся медленными, нестабильными и ресурсоёмкими, даже если выглядят «правильно» с точки зрения структуры.

Уязвимости и угроза безопасности

Не стоит забывать и про безопасность. Уже известны случаи, когда проекты, частично или полностью созданные с помощью LLM, были успешно взломаны. Причины типичны: использование небезопасных функций, отсутствие проверки входных данных, ошибки в логике авторизации, утечки через внешние зависимости. Нейросеть может сгенерировать уязвимый код просто потому, что он встречался в открытых репозиториях. Без участия специалистов по безопасности и полноценной ревизии такие ошибки легко становятся точками входа для атак.

Закон Парето и суть недоработки

С нейросетями ярко работает закон Парето: 80% результата достигается за счёт 20% усилий. Модель может сгенерировать большое количество кода, создать основу проекта, раскидать структуру, оформить типы, подключить модули. Однако всё это может оказаться устаревшим, несовместимым с текущими версиями библиотек или фреймворков, и потребовать значительной ручной доработки. Автоматизация здесь работает скорее как черновик, который нуждается в проверке, переработке и адаптации под конкретные реалии проекта.

Осторожный оптимизм

Тем не менее, будущее выглядит обнадёживающе. Постоянное обновление обучающих датасетов, интеграция с актуальной документацией, автоматизированные проверки архитектуры, соответствия паттернам проектирования и безопасности — всё это может кардинально изменить правила игры. Возможно, через несколько лет мы действительно сможем писать код быстрее, безопаснее и эффективнее, полагаясь на LLM как на настоящего технического соавтора. Но пока — увы — многое приходится проверять, переписывать и дорабатывать вручную.

Нейросети — мощный инструмент. Но чтобы он работал на вас, а не против вас, нужна база, критическое мышление и готовность в любой момент взять управление в свои руки.

Vibe-кодерские трюки: почему LLM пока не работают с SOLID, DRY и CLEAN

С развитием больших языковых моделей (LLM), таких как ChatGPT, всё больше разработчиков используют их для генерации кода, проектирования архитектуры и ускорения интеграции. Однако при практическом применении становится заметно: классические принципы архитектуры — SOLID, DRY, CLEAN — плохо уживаются с особенностями кодогенерации LLM.

Это не значит, что принципы устарели — напротив, они прекрасно работают при ручной разработке. Но с LLM подход приходится адаптировать.

Почему LLM не справляются с архитектурными принципами

Инкапсуляция

Инкапсуляция требует понимания границ между частями системы, знания о намерениях разработчика, а также следования строгим ограничениям доступа. LLM же часто упрощают структуру, делают поля публичными без причины или дублируют реализацию. Это делает код более уязвимым к ошибкам и нарушает архитектурные границы.

Абстракции и интерфейсы

Паттерны проектирования, такие как абстрактная фабрика или стратегия, требуют целостного взгляда на систему и понимания её динамики. Модели же могут создать интерфейс без ясной цели, не обеспечив его реализацию, или нарушить связь между слоями. Результат — избыточная или нефункциональная архитектура.

DRY (Don’t Repeat Yourself)

LLM не стремятся минимизировать повторяющийся код — напротив, им проще дублировать блоки, чем выносить общую логику. Хотя они могут предложить рефакторинг по запросу, по умолчанию модели склонны генерировать «самодостаточные» фрагменты, даже если это приводит к избыточности.

CLEAN Architecture

CLEAN предполагает строгую иерархию, независимость от фреймворков, направленные зависимости и минимальную связанность между слоями. Генерация такой структуры требует глобального понимания системы — а LLM работают на уровне вероятности слов, а не архитектурной целостности. Поэтому код получается смешанным, с нарушением направлений зависимости и упрощённым делением на уровни.

Что работает лучше при работе с LLM

WET вместо DRY
Подход WET (Write Everything Twice) оказывается практичнее в работе с LLM. Дублирование кода не требует от модели удержания контекста, а значит — результат предсказуемее и легче исправляется вручную. Это также снижает вероятность появления неочевидных связей и багов.

Кроме того, дублирование помогает компенсировать короткую память модели: если определённый фрагмент логики встречается в нескольких местах, LLM с большей вероятностью будет его учитывать при дальнейшей генерации. Это упрощает сопровождение и увеличивает устойчивость к “забыванию”.

Простые структуры вместо инкапсуляции

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

Упрощённая архитектура

Простая, плоская структура проекта с минимальным количеством зависимостей и абстракций даёт более стабильный результат при генерации. Модель легче адаптирует такой код и реже нарушает ожидаемые связи между компонентами.

Интеграция SDK — вручную надёжнее

Большинство языковых моделей обучены на устаревших версиях документации. Поэтому при генерации инструкций по установке SDK часто появляются ошибки: устаревшие команды, неактуальные параметры или ссылки на недоступные ресурсы. Практика показывает: лучше всего использовать официальную документацию и ручную настройку, оставляя LLM вспомогательную роль — например, генерацию шаблонного кода или адаптацию конфигураций.

Почему принципы всё же работают — но при ручной разработке

Важно понимать, что сложности с SOLID, DRY и CLEAN касаются именно кодогенерации через LLM. Когда разработчик пишет код вручную, эти принципы продолжают демонстрировать свою ценность: снижают связанность, упрощают сопровождение, повышают читаемость и гибкость проекта.

Это связано с тем, что человеческое мышление склонно к обобщению. Мы ищем закономерности, выносим повторяющуюся логику в отдельные сущности, создаём паттерны. Вероятно, такое поведение имеет эволюционные корни: сокращение объёма информации экономит когнитивные ресурсы.

LLM же действуют иначе: они не испытывают нагрузки от объёма данных и не стремятся к экономии. Напротив, им проще работать с дублирующей, разрозненной информацией, чем строить и поддерживать сложные абстракции. Именно поэтому им легче справляться с кодом без инкапсуляции, с повторяющимися структурами и минимальной архитектурной строгостью.

Вывод

Большие языковые модели — полезный инструмент в разработке, особенно на ранних стадиях или при создании вспомогательного кода. Но важно адаптировать к ним подход: упростить архитектуру, ограничить абстракции, избегать сложных зависимостей и не полагаться на них при настройке SDK.

Принципы SOLID, DRY и CLEAN по-прежнему актуальны — но наилучший эффект они дают в руках человека. При работе с LLM разумно использовать упрощённый, практичный стиль, позволяющий получать надёжный и понятный код, который легко доработать вручную. А где LLM забывает — дублирование кода помогает ему вспомнить.

Портирование Surreal Engine C++ на WebAssembly

В этой заметке я опишу то как я портировал игровой движок Surreal Engine на WebAssembly.

Surreal Engine – игровой движок который реализует большую часть функционала движка Unreal Engine 1, известные игры на этом движке – Unreal Tournament 99, Unreal, Deus Ex, Undying. Он относится к классическим движкам, которые работали преимущественно в однопоточной среде выполнения.

Изначально у меня была идея взяться за проект который я не смогу выполнить в какой-либо разумный срок, таким образом показав своим подписчикам на Twitch, что есть проекты которые не могу сделать даже я. В первый же стрим я внезапно понял что задача портирования Surreal Engine C++ на WebAssembly с помощью Emscripten выполнима.

Surreal Engine Emscripten Demo

Спустя месяц я могу продемонстрировать свой форк и сборку движка на WebAssembly:
https://demensdeum.com/demos/SurrealEngine/

Управление как и в оригинале, осуществляется на стрелках клавиатуры. Далее планирую адаптацию под мобильное управление (тачи), добавление корректного освещения и прочие графические фишки рендера Unreal Tournament 99.

С чего начать?

Первое о чем хочется сказать, это то что любой проект можно портировать с C++ на WebAssembly с помощью Emscripten, вопрос лишь в том насколько полным получится функционал. Выбирайте проект порты библиотек которого уже доступны для Emscripten, в случае Surreal Engine очень сильно повезло, т.к. движок использует библиотеки SDL 2, OpenAL – они обе портированы под Emscripten. Однако в качестве графического API используется Vulkan, который на данный момент не доступен для HTML5, ведутся работы по реализации WebGPU, но он также находится в стадии черновика, также неизвестно насколько простым будет дальнейший порт из Vulkan на WebGPU, после полной стандартизации оного. Поэтому пришлось написать свой собственный базовый OpenGL-ES / WebGL рендер для Surreal Engine.

Сборка проекта

Система сборки в Surreal Engine – CMake, что тоже упрощает портирование, т.к. Emscripten предоставляет свои нативные сборщики – emcmake, emmake.
За основу порта Surreal Engine брался код моей последней игры на WebGL/OpenGL ES и C++ под названием Death-Mask, из-за этого разработка шла гораздо проще, все необходимые флаги сборки были с собой, примеры кода.

Один из важнейших моментов в CMakeLists.txt это флаги сборки для Emscripten, ниже пример из файла проекта:


-s MAX_WEBGL_VERSION=2 \

-s EXCEPTION_DEBUG \

-fexceptions \

--preload-file UnrealTournament/ \

--preload-file SurrealEngine.pk3 \

--bind \

--use-preload-plugins \

-Wall \

-Wextra \

-Werror=return-type \

-s USE_SDL=2 \

-s ASSERTIONS=1 \

-w \

-g4 \

-s DISABLE_EXCEPTION_CATCHING=0 \

-O3 \

--no-heap-copy \

-s ALLOW_MEMORY_GROWTH=1 \

-s EXIT_RUNTIME=1")

Сам сборочный скрипт:


emmake make -j 16

cp SurrealEngine.data /srv/http/SurrealEngine/SurrealEngine.data

cp SurrealEngine.js /srv/http/SurrealEngine/SurrealEngine.js

cp SurrealEngine.wasm /srv/http/SurrealEngine/SurrealEngine.wasm

cp ../buildScripts/Emscripten/index.html /srv/http/SurrealEngine/index.html

cp ../buildScripts/Emscripten/background.png /srv/http/SurrealEngine/background.png

Далее подготовим index.html, который включает в себя прелоадер файловой системы проекта. Для выкладывания в веб я использовал Unreal Tournament Demo версии 338. Как можно увидеть из файла CMake, распакованная папка игры была добавлена с сборочную директорию и прилинкована как preload-file для Emscripten.

Основные изменения кода

Затем предстояло поменять игровой цикл игры, запускать бесконечный цикл нельзя, это приводит к зависанию браузера, вместо этого нужно использовать emscripten_set_main_loop, об этой особенности я писал в своей заметке 2017 года “Портирование SDL C++ игры на HTML5 (Emscripten)
Код условия выхода из цикла while меняем на if, далее выводим основной класс игрового движка, который содержит игровой луп, в глобальный скоп, и пишем глобальную функцию которая будет вызывать шаг игрового цикла из глобального объекта:


#include <emscripten.h>

Engine *EMSCRIPTEN_GLOBAL_GAME_ENGINE = nullptr;

void emscripten_game_loop_step() {

	EMSCRIPTEN_GLOBAL_GAME_ENGINE->Run();

}

#endif

После этого нужно убедиться что в приложении отсутствуют фоновые потоки, если они есть, то приготовьтесь к переписываю их на однопоточное выполнение, либо использование библиотеки phtread в Emscripten.
Фоновый поток в Surreal Engine используется для проигрывания музыки, из главного потока движка приходят данные о текущем треке, о необходимости проигрывания музыки, либо ее отсутствии, затем фоновый поток по мьютексу получает новое состояние и начинает проигрывать новую музыку, либо приостанавливает. Фоновый поток также используется для буферизации музыки во время проигрывания.
Мои попытки собрать Surreal Engine под Emscripten с pthread не увенчались успехом, по той причине что порты SDL2 и OpenAL были собраны без поддержки pthread, а их пересборкой ради музыки я заниматься не хотел. Поэтому перенес функционал фонового потока музыки в однопоточное выполнение с помощью цикла. Удалив вызовы pthread из C++ кода, я перенес буферизацию, проигрывание музыки в основной поток, чтобы не было задержек я увеличил буфер на несколько секунд.

Далее я опишу уже конкретные реализации графики и звука.

Vulkan не поддерживается!

Да Vulkan не поддерживается в HTML5, хотя все рекламные брошюры выдают кроссплатформенность и широкую поддержку на платформах как основное преимущество Vulkan. По этой причине пришлось написать свой базовый рендер графики для упрощенного типа OpenGL – ES, он используется на мобильных устройствах, иногда не содержит модных фишек современного OpenGL, зато он очень хорошо переносится на WebGL, именно это реализует Emscripten. Написание базового рендера тайлов, bsp рендеринга, для простейшего отображения GUI, и отрисовки моделей + карт, удалось за две недели. Это, пожалуй, была самая сложная часть проекта. Впереди еще очень много работы по имплементации полного функционала рендеринга Surreal Engine, поэтому любая помощь читателей приветствуется в виде кода и pull request’ов.

OpenAL поддерживается!

Большим везением стало то, что Surreal Engine использует OpenAL для вывода звука. Написав простой hello world на OpenAL, и собрав его на WebAssembly c помощью Emscripten, мне стало ясно насколько все просто, и я отправился портировать звук.
После нескольких часов дебага, стало очевидно что в OpenAL реализации Emscripten есть несколько багов, например при инициализации считывания количества моно каналов, метод возвращал бесконечную цифру, а после попытки инициализации вектора бесконечного размера, падает уже C++ с исключением vector::length_error.
Это удалось обойти сделав хардкод количества моно каналов на 2048:


		alcGetIntegerv(alDevice, ALC_STEREO_SOURCES, 1, &stereoSources);



#if __EMSCRIPTEN__

		monoSources = 2048; // for some reason Emscripten's OpenAL gives infinite monoSources count, bug?

#endif



А сеть есть?

Surreal Engine сейчас не поддерживает сетевую игру, игра с ботами поддерживается, но нужен кто-то кто напишет ИИ для этих ботов. Теоретически реализовать сетевую игру на WebAssembly/Emscripten можно с помощью Websockets.

Заключение

В заключение хочется сказать что портирование Surreal Engine получилось достаточно гладким из-за использования библиотек для которых есть порты Emscripten, также мой прошлый опыт реализации игры на C++ для WebAssembly на Emscripten. Ниже ссылки на источники знаний, репозиториев по теме.
M-M-M-MONSTER KILL!

Также если вы хотите помочь проекту, желательно кодом рендера WebGL/OpenGL ES, то пишите мне в Telegram:
https://t.me/demenscave

Ссылки

https://demensdeum.com/demos/SurrealEngine/
https://github.com/demensdeum/SurrealEngine-Emscripten

https://github.com/dpjudas/SurrealEngine

Флеш жив – Interceptor 2021

Недавно оказалось что Adobe Flash достаточно стабильно работает под Wine. На 4-х часовом стриме сделал игру Interceptor 2021, это сиквел игры Interceptor 2020 которая была написана для ZX Spectrum.

Для тех кто в танке – технология Флеш обеспечивала интерактивность в вебе с 2000 по примерно 2015 год. К ее закрытию привело открытое письмо Стива Джобса в котором он написал что Флешу пора на свалку истории, т.к. на айфоне он тормозит. За это время JS стал тормозить еще больше чем флеш, а сам Флеш обернули в JS и теперь его можно запускать на чём угодно благодаря плееру Ruffle.

Поиграть можно тут:
https://demensdeum.com/demos/Interceptor2021

Видео:
https://www.youtube.com/watch?v=-3b5PkBvHQk

Исходный код:
https://github.com/demensdeum/Interceptor-2021

CRUD репозиторий

В этой заметке я опишу основные принципы известного классического паттерна CRUD, реализацию на языке Swift. Swift является открытым, кроссплатформенным языком, доступным для ОС Windows, Linux, macOS, iOS, Android.

Существует множество решений абстрагирования хранилища данных и логики приложения. Одним из таких решений является подход CRUD, это акроним от C – Create, R -Read, U – Update, D – Delete.
Обычно реализация этого принципа обеспечивается с помощью реализации интерфейса к базе данных, в котором работа с элементами происходит с использованием уникального идентификатора, например id. Создается интерфейс по каждой букве CRUD – Create(object, id), Read(id), Update(object, id), Delete(object, id).
Если объект содержит id внутри себя, то аргумент id можно упустить в части методов (Create, Update, Delete), так как туда передается объект целиком вместе со своим полем – id. А вот для – Read требуется id, так как мы хотим получить объект из базы данных по идентификатору.

Все имена вымышлены

Представим что гипотетическое приложение AssistantAI создавалось с использованием бесплатной SDK базы данных EtherRelm, интеграция была простой, API очень удобным, в итоге приложение было выпущено в маркеты.
Внезапно компания-разработчик SDK EtherRelm решает сделать её платной, устанавливая цену в 100$ в год за одного пользователя приложения.
Что? Да! Что же теперь делать разработчикам из AssistantAI, ведь у них уже 1млн активных пользователей! Платить 100 млн долларов?
Вместо этого принимается решение оценить перенос приложения на нативную для платформы базу данных RootData, по оценке программистов такой перенос займет около полугода, это без учета реализации новых фич в приложении. После недолгих раздумий, принимается решение убрать приложение из маркетов, переписать его на другом бесплатном кроссплатформенном фреймворке со встроенной базой данных BueMS, это решит проблему с платностью БД + упростит разработку на другие платформы.
Через год приложение переписано на BueMS, но тут внезапно разработчик фреймворка решает сделать его платным. Получается что команда попала в одну и ту же ловушку дважды, получится ли у них выбраться во второй раз, это уже совершенно другая история.

Абстракция на помощь

Этих проблем удалось бы избежать, если бы разработчики использовали абстракцию интерфейсов внутри приложения. К трем китам ООП – полиморфизму, инкапсуляции, наследованию, не так давно добавили еще одного – абстракцию.
Абстракция данных позволяет описывать идеи, модели в общих чертах, с минимум деталей, при этом достаточно точной для реализации конкретных имплементаций, которые используют для решения бизнес-задач.
Как мы можем абстрагировать работу с базой данных, чтобы логика приложения не зависела от нее? Используем подход CRUD!

Упрощенно UML схема CRUD выглядит так:

Пример с вымышленной базой данных EtherRelm:

Пример с настоящей базой данных SQLite:

Как вы уже заметили, при переключении базы данных, меняется только она, интерфейс CRUD с которым взаимодействует приложение остается неизменным. CRUD является вариантом реализации паттерна GoF – Адаптер, т.к. с помощью него мы адаптируем интерфейсы приложения к любой базе данных, совмещаем несовместимые интерфейсы.
Слова это пустое, покажи мне код
Для реализации абстракций в языках программирования используют интерфейсы/протоколы/абстрактные классы. Все это явления одного порядка, однако на собеседованиях вас могут попросить назвать разницу между ними, я лично считаю что в этом особого смысла нет т.к. единственная цель использования это реализация абстракции данных, в остальном это проверка памяти интервьюируемого.
CRUD часто реализуют в рамках паттерна Репозиторий, репозиторий однако может реализовывать интерфейс CRUD, а может и не реализовывать, всё зависит от изобретательности разработчика.

Рассмотрим достаточно типичный Swift код репозитория структур Book, работающий напрямую с базой данных UserDefaults:


struct Book: Codable {
	let title: String
	let author: String
}

class BookRepository {
	func save(book: Book) {
    		let record = try! JSONEncoder().encode(book)
    		UserDefaults.standard.set(record, forKey: book.title)
	}
    
	func get(bookWithTitle title: String) -> Book? {
    		guard let data = UserDefaults.standard.data(forKey: title) else { return nil }
    		let book = try! JSONDecoder().decode(Book.self, from: data)
    		return book
	}
    
	func delete(book: Book) {
    		UserDefaults.standard.removeObject(forKey: book.title)
	}
}

let book = Book(title: "Fear and Loathing in COBOL", author: "Sir Edsger ZX Spectrum")
let repository = BookRepository()
repository.save(book: book)
print(repository.get(bookWithTitle: book.title)!)
repository.delete(book: book)
guard repository.get(bookWithTitle: book.title) == nil else {
	print("Error: can't delete Book from repository!")
	exit(1)
}

Код выше кажется простым, однако посчитаем количество нарушений принципа DRY (Do not Repeat Yourself) и связанность кода:
Связанность с базой данных UserDefaults
Связанность с энкодерами и декодерами JSON – JSONEncoder, JSONDecoder
Связанность со структурой Book, а нам нужен абстрактный репозиторий чтобы не создавать по классу репозитория для каждой структуры, которую мы будем хранить в базе данных (нарушение DRY)

Такой код CRUD репозитория я встречаю достаточно часто, пользоваться им можно, однако высокая связанность, дублирование кода, приводят к тому что со временем его поддержка очень сильно усложнится. Особенно это будет ощущаться при попытке перейти на другую базу данных, либо при изменении внутренней логики работы с бд во всех созданных в приложении репозиториях.
Вместо того чтобы дублировать код, держать высокую связанность – напишем протокол для CRUD репозитория, таким образом абстрагируя интерфейс базы данных и бизнес-логики приложения, соблюдая DRY, осуществляя низкую связанность:

    typealias Item = Codable
    typealias ItemIdentifier = String
    
    func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws
    func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T
    func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws
    func delete(id: CRUDRepository.ItemIdentifier) async throws
}

Протокол CRUDRepository описывает интерфейсы и ассоциированные типы данных для дальнейшей реализации конкретного CRUD репозитория.

Далее напишем конкретную реализацию для базы данных UserDefaults:

    private typealias RecordIdentifier = String
    
    let tableName: String
    let dataTransformer: DataTransformer
    
    init(
   	 tableName: String = "",
   	 dataTransformer: DataTransformer = JSONDataTransformer()
    ) {
   	 self.tableName = tableName
   	 self.dataTransformer = dataTransformer
    }
    
    private func key(id: CRUDRepository.ItemIdentifier) -> RecordIdentifier {
   	 "database_\(tableName)_item_\(id)"
    }
   	 
    private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
   	 UserDefaults.standard.data(forKey: key(id: id)) != nil
    }
    
    func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
   	 let data = try await dataTransformer.encode(item)
   	 UserDefaults.standard.set(data, forKey: key(id: id))
   	 UserDefaults.standard.synchronize()
    }
    
    func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
   	 guard let data = UserDefaults.standard.data(forKey: key(id: id)) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 let item: T = try await dataTransformer.decode(data: data)
   	 return item
    }
    
    func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
   	 guard try await isExists(id: id) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 let data = try await dataTransformer.encode(item)
   	 UserDefaults.standard.set(data, forKey: key(id: id))
   	 UserDefaults.standard.synchronize()
    }
    
    func delete(id: CRUDRepository.ItemIdentifier) async throws {
   	 guard try await isExists(id: id) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 UserDefaults.standard.removeObject(forKey: key(id: id))
   	 UserDefaults.standard.synchronize()
    }
}

Код выглядит длинным, однако содержит полную конкретную реализацию CRUD репозитория, содержащим слабую связанность, подробности далее.
typealias’ы добавлены для самодокументирования кода.
Слабая связанность и сильная связность
Отвязка от конкретной структуры (struct) реализуется с помощью генерика T, который в свою очередь должен имплементировать протоколы Codable. Codable позволяет производить преобразование структур с помощью классов которые реализуют протоколы TopLevelEncoder и TopLevelDecoder, например JSONEncoder и JSONDecoder, при использовании базовых типов (Int, String, Float и т.д.) нет необходимости писать дополнительный код для преобразования структур.

Отвязка от конкретного энкодера и декодера происходит с помощью абстрагирования в протоколе DataTransformer:

	func encode<T: Encodable>(_ object: T) async throws -> Data
	func decode<T: Decodable>(data: Data) async throws -> T
}

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

Далее приводится код конкретного DataTransformer, а именно для JSON:

	func encode<T>(_ object: T) async throws -> Data where T : Encodable {
    		let data = try JSONEncoder().encode(object)
    		return data
	}
    
	func decode<T>(data: Data) async throws -> T where T : Decodable {
    		let item: T = try JSONDecoder().decode(T.self, from: data)
    		return item
	}
}

А так можно было?

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

Пример клиентский CRUD с конкретным репозиторием, в качестве базы данных выступает UserDefaults, формат данных JSON, структура Client, также пример записи и считывания массива:


print("One item access example")

do {
	let clientRecordIdentifier = "client"
	let clientOne = Client(name: "Chill Client")
	let repository = UserDefaultsRepository(
    	tableName: "Clients Database",
    	dataTransformer: JSONDataTransformer()
	)
	try await repository.create(id: clientRecordIdentifier, item: clientOne)
	var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print("Client Name: \(clientRecord.name)")
	clientRecord.name = "Busy Client"
	try await repository.update(id: clientRecordIdentifier, item: clientRecord)
	let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
	print("Updated Client Name: \(updatedClient.name)")
	try await repository.delete(id: clientRecordIdentifier)
	let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print(removedClientRecord)
}
catch {
	print(error.localizedDescription)
}

print("Array access example")

let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = UserDefaultsRepository(
	tableName: "Clients Database",
	dataTransformer: JSONDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)

При первой проверке CRUD реализована обработка исключения, при которой чтение удаленного айтема будет уже недоступно.

Переключаем базы данных

Теперь я покажу как перенести текущий код на другую базу данных. Для примера возьму код репозитория SQLite который сгенерил ChatGPT:


class SQLiteRepository: CRUDRepository {
    private typealias RecordIdentifier = String
    
    let tableName: String
    let dataTransformer: DataTransformer
    private var db: OpaquePointer?

    init(
   	 tableName: String,
   	 dataTransformer: DataTransformer = JSONDataTransformer()
    ) {
   	 self.tableName = tableName
   	 self.dataTransformer = dataTransformer
   	 self.db = openDatabase()
   	 createTableIfNeeded()
    }
    
    private func openDatabase() -> OpaquePointer? {
   	 var db: OpaquePointer? = nil
   	 let fileURL = try! FileManager.default
   		 .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
   		 .appendingPathComponent("\(tableName).sqlite")
   	 if sqlite3_open(fileURL.path, &db) != SQLITE_OK {
   		 print("error opening database")
   		 return nil
   	 }
   	 return db
    }
    
    private func createTableIfNeeded() {
   	 let createTableString = """
   	 CREATE TABLE IF NOT EXISTS \(tableName) (
   	 id TEXT PRIMARY KEY NOT NULL,
   	 data BLOB NOT NULL
   	 );
   	 """
   	 var createTableStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK {
   		 if sqlite3_step(createTableStatement) == SQLITE_DONE {
       		 print("\(tableName) table created.")
   		 } else {
       		 print("\(tableName) table could not be created.")
   		 }
   	 } else {
   		 print("CREATE TABLE statement could not be prepared.")
   	 }
   	 sqlite3_finalize(createTableStatement)
    }
    
    private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
   	 let queryStatementString = "SELECT data FROM \(tableName) WHERE id = ?;"
   	 var queryStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
   		 sqlite3_bind_text(queryStatement, 1, id, -1, nil)
   		 if sqlite3_step(queryStatement) == SQLITE_ROW {
       		 sqlite3_finalize(queryStatement)
       		 return true
   		 } else {
       		 sqlite3_finalize(queryStatement)
       		 return false
   		 }
   	 } else {
   		 print("SELECT statement could not be prepared.")
   		 throw CRUDRepositoryError.databaseError
   	 }
    }
    
    func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
   	 let insertStatementString = "INSERT INTO \(tableName) (id, data) VALUES (?, ?);"
   	 var insertStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {
   		 let data = try await dataTransformer.encode(item)
   		 sqlite3_bind_text(insertStatement, 1, id, -1, nil)
   		 sqlite3_bind_blob(insertStatement, 2, (data as NSData).bytes, Int32(data.count), nil)
   		 if sqlite3_step(insertStatement) == SQLITE_DONE {
       		 print("Successfully inserted row.")
   		 } else {
       		 print("Could not insert row.")
       		 throw CRUDRepositoryError.databaseError
   		 }
   	 } else {
   		 print("INSERT statement could not be prepared.")
   		 throw CRUDRepositoryError.databaseError
   	 }
   	 sqlite3_finalize(insertStatement)
    }
    
    func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
   	 let queryStatementString = "SELECT data FROM \(tableName) WHERE id = ?;"
   	 var queryStatement: OpaquePointer? = nil
   	 var item: T?
   	 if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
   		 sqlite3_bind_text(queryStatement, 1, id, -1, nil)
   		 if sqlite3_step(queryStatement) == SQLITE_ROW {
       		 let queryResultCol1 = sqlite3_column_blob(queryStatement, 0)
       		 let queryResultCol1Length = sqlite3_column_bytes(queryStatement, 0)
       		 let data = Data(bytes: queryResultCol1, count: Int(queryResultCol1Length))
       		 item = try await dataTransformer.decode(data: data)
   		 } else {
       		 throw CRUDRepositoryError.recordNotFound(id: id)
   		 }
   	 } else {
   		 print("SELECT statement could not be prepared")
   		 throw CRUDRepositoryError.databaseError
   	 }
   	 sqlite3_finalize(queryStatement)
   	 return item!
    }
    
    func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
   	 guard try await isExists(id: id) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 let updateStatementString = "UPDATE \(tableName) SET data = ? WHERE id = ?;"
   	 var updateStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, updateStatementString, -1, &updateStatement, nil) == SQLITE_OK {
   		 let data = try await dataTransformer.encode(item)
   		 sqlite3_bind_blob(updateStatement, 1, (data as NSData).bytes, Int32(data.count), nil)
   		 sqlite3_bind_text(updateStatement, 2, id, -1, nil)
   		 if sqlite3_step(updateStatement) == SQLITE_DONE {
       		 print("Successfully updated row.")
   		 } else {
       		 print("Could not update row.")
       		 throw CRUDRepositoryError.databaseError
   		 }
   	 } else {
   		 print("UPDATE statement could not be prepared.")
   		 throw CRUDRepositoryError.databaseError
   	 }
   	 sqlite3_finalize(updateStatement)
    }
    
    func delete(id: CRUDRepository.ItemIdentifier) async throws {
   	 guard try await isExists(id: id) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 let deleteStatementString = "DELETE FROM \(tableName) WHERE id = ?;"
   	 var deleteStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, deleteStatementString, -1, &deleteStatement, nil) == SQLITE_OK {
   		 sqlite3_bind_text(deleteStatement, 1, id, -1, nil)
   		 if sqlite3_step(deleteStatement) == SQLITE_DONE {
       		 print("Successfully deleted row.")
   		 } else {
       		 print("Could not delete row.")
       		 throw CRUDRepositoryError.databaseError
   		 }
   	 } else {
   		 print("DELETE statement could not be prepared.")
   		 throw CRUDRepositoryError.databaseError
   	 }
   	 sqlite3_finalize(deleteStatement)
    }
}

Или код CRUD репозитория для файловой системы который тоже сгенерила ChatGPT:


class FileSystemRepository: CRUDRepository {
	private typealias RecordIdentifier = String
    
	let directoryName: String
	let dataTransformer: DataTransformer
	private let fileManager = FileManager.default
	private var directoryURL: URL
    
	init(
    	directoryName: String = "Database",
    	dataTransformer: DataTransformer = JSONDataTransformer()
	) {
    	self.directoryName = directoryName
    	self.dataTransformer = dataTransformer
   	 
    	let paths = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
    	directoryURL = paths.first!.appendingPathComponent(directoryName)
   	 
    	if !fileManager.fileExists(atPath: directoryURL.path) {
        	try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
    	}
	}
    
	private func fileURL(id: CRUDRepository.ItemIdentifier) -> URL {
    	return directoryURL.appendingPathComponent("item_\(id).json")
	}
    
	private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
    	return fileManager.fileExists(atPath: fileURL(id: id).path)
	}
    
	func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
    	let data = try await dataTransformer.encode(item)
    	let url = fileURL(id: id)
    	try data.write(to: url)
	}
    
	func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
    	let url = fileURL(id: id)
    	guard let data = fileManager.contents(atPath: url.path) else {
        	throw CRUDRepositoryError.recordNotFound(id: id)
    	}
    	let item: T = try await dataTransformer.decode(data: data)
    	return item
	}
    
	func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
    	guard try await isExists(id: id) else {
        	throw CRUDRepositoryError.recordNotFound(id: id)
    	}
    	let data = try await dataTransformer.encode(item)
    	let url = fileURL(id: id)
    	try data.write(to: url)
	}
    
	func delete(id: CRUDRepository.ItemIdentifier) async throws {
    	guard try await isExists(id: id) else {
        	throw CRUDRepositoryError.recordNotFound(id: id)
    	}
    	let url = fileURL(id: id)
    	try fileManager.removeItem(at: url)
	}
}

Заменяем репозиторий в клиентском коде:


print("One item access example")

do {
	let clientRecordIdentifier = "client"
	let clientOne = Client(name: "Chill Client")
	let repository = FileSystemRepository(
    	directoryName: "Clients Database",
    	dataTransformer: JSONDataTransformer()
	)
	try await repository.create(id: clientRecordIdentifier, item: clientOne)
	var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print("Client Name: \(clientRecord.name)")
	clientRecord.name = "Busy Client"
	try await repository.update(id: clientRecordIdentifier, item: clientRecord)
	let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
	print("Updated Client Name: \(updatedClient.name)")
	try await repository.delete(id: clientRecordIdentifier)
	let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print(removedClientRecord)
}
catch {
	print(error.localizedDescription)
}

print("Array access example")

let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
	directoryName: "Clients Database",
	dataTransformer: JSONDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)

Инициализация UserDefaultsRepository заменена на FileSystemRepository, с соотетствующими аргументами.
После запуска второго варианта клиентского кода, вы обнаружите в папке документов директорию “Clients Database”, которая будет содержать в себе файл сериализованного в JSON массива с одной структурой Client.

Переключаем формат хранения данных

Теперь попросим ChatGPT сгенерить энкодер и декодер для XML:

	let formatExtension = "xml"
    
	func encode<T: Encodable>(_ item: T) async throws -> Data {
    	let encoder = PropertyListEncoder()
    	encoder.outputFormat = .xml
    	return try encoder.encode(item)
	}
    
	func decode<T: Decodable>(data: Data) async throws -> T {
    	let decoder = PropertyListDecoder()
    	return try decoder.decode(T.self, from: data)
	}
}

Благодаря встроенным типам в Swift, задача для нейросети становится элементарной.

Заменяем JSON на XML в клиентском коде:


print("One item access example")

do {
	let clientRecordIdentifier = "client"
	let clientOne = Client(name: "Chill Client")
	let repository = FileSystemRepository(
    	directoryName: "Clients Database",
    	dataTransformer: XMLDataTransformer()
	)
	try await repository.create(id: clientRecordIdentifier, item: clientOne)
	var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print("Client Name: \(clientRecord.name)")
	clientRecord.name = "Busy Client"
	try await repository.update(id: clientRecordIdentifier, item: clientRecord)
	let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
	print("Updated Client Name: \(updatedClient.name)")
	try await repository.delete(id: clientRecordIdentifier)
	let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print(removedClientRecord)
}
catch {
	print(error.localizedDescription)
}

print("Array access example")

let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
	directoryName: "Clients Database",
	dataTransformer: XMLDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)

Клиентский код изменился только на одно выражение JSONDataTransformer -> XMLDataTransformer

Итог

CRUD репозитории один из паттернов проектирования, которые можно использовать для реализации слабой связанности компонентов архитектуры приложения. Еще одно из решений – использование ORM (Объектно-реляционный маппинг), если вкратце то в ОРМ используется подход при котором структуры полностью мапятся на базу данных, и затем изменения с моделями должны отображаться (маппиться(!)) на бд.
Но это уже совсем другая история.

Полная реализация репозиториев CRUD для Swift доступна по ссылке:
https://gitlab.com/demensdeum/crud-example

Кстати Swift давно поддерживается вне macOS, код из статьи был польностью написан и протестирован на Arch Linux.

Источники

https://developer.apple.com/documentation/combine/topleveldecoder
https://developer.apple.com/documentation/combine/toplevelencoder
https://en.wikipedia.org/wiki/Create,_read,_update_and_delete

dd input/output error

Что делать если при копировании нормального диска с помощью dd в Linux вы получили ошибку input/output error?

Ситуевина очень грустная, но решаемая. Скорее всего вы имеете дело со сбойным диском, содержащим bad блоки которые уже невозможно использовать, записывать и считывать данные.

Обязательно проверьте такой диск с помощью S.M.A.R.T., скорее всего он покажет вам ошибки диска. Так было в моем случае, количество сбойных блоков было настолько огромным, что пришлось распрощаться со старым жестким диском и заменить его на новый SSD.

Проблема была в том, что на этом диске была полностью рабочая система с лицензионным софтом, который необходим в работе. Я предпринял попытку использовать partimage, для быстрого копирования данных, но вдруг обнаружил что утилита копирует лишь треть диска, далее завершается толи с segfault, толи с каким-то другим своим веселым Сишным/Сиплюсплюсным приколом.

Далее я попробовал скопировать данные с помощью dd, и обнаружилось что dd доходит примерно тудаже где и partimage, и потом наступает ошибка input/output error. При этом всякие веселые флажки навроде conv=noerr,skip или еще чего-то такого совершенно не помогали.

Зато данные на другой диск удалось скопировать без проблем с помощью утилиты GNU под названием ddrescue.

После этого мои волосы стали шелковистыми, вернулась жена, дети и собака перестала кусать диван.

Большим плюсом ddrescue является наличие встроенного прогрессбара, поэтому не приходится костылять какие-то ухищрения навроде pv и всяких не особо красивых флажков dd. Также ddrescure показывает количество попыток прочитать данные; еще на вики написано что утилита обладает каким-то сверх алгоритмом для считывания поврежденных данных, оставим это на проверку людям которые любят ковыряться в исходниках, мы же не из этих да?

https://ru.wikipedia.org/wiki/Ddrescue
https://www.gnu.org/software/ddrescue/ddrescue_ru.html