CoreBus (старое название «Терминальная программа») — кроссплатформенный терминал для работы с COM-портами и TCP-сокетами с поддержкой протоколов Modbus TCP / RTU / ASCII.

Ребрендинг, новые фичи и Native AOT (+ боль и разочарование).


Время идёт, и я уже пишу пятую часть из цикла статей про мой терминал. За это время он обрел довольно обширный функционал. Поэтому позволю себе кратко описать основные возможности CoreBus:

1. Два режима работы: «Без протокола» и «Modbus».

2. «Без протокола»:

  • Работа с данными в строковом или байтовом формате.

  • Поддержка разных кодировок.

  • Три режима отправки: одиночная, цикличная, отправка файла.

3. “Modbus”:

  • Поддержка различных вариаций протокола Modbus: TCP, RTU, ASCII и RTU / ASCII over TCP.

  • Удобная работа с функциями записи.

  • Возможность работы с числами типа float.

  • Возможность работы с бинарными данными.

  • Цикличный опрос.

  • Modbus сканер, который осуществляет поиск устройств на линии связи.

4. Макросы:

  • Отдельные макросы для каждого режима работы.

  • Макрос состоит из неограниченного количества команд (действий).

  • Для Modbus макросов предусмотрена возможность выставления общего Slave ID для всего макроса.

  • Импорт и экспорт макросов.

5. Темная и светлая темы приложения.

6. Пресеты с пользовательскими настройками.

7. Кроссплатформенность: Windows, Linux.


Текущая версия приложения — 3.3.0.

И вот список изменений:

  • Добавлена возможность использовать единый Slave ID для Modbus макроса.

  • Теперь все ошибки, появившиеся при работе макроса, собираются в единое сообщение, а не показываются по отдельности.

  • Добавлены улучшения для более удобной работы с окном макросов.

  • Исправлен баг с получением некорректного пути при выборе папки или файла.

  • Оптимизации.

  • Рефакторинг.

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

Новые возможности

В личных сообщениях и, кажется, тут в комментариях меня просили добавить возможность использования единого Slave ID для Modbus макросов. Согласен, это оказалось очень удобно, особенно когда команд больше пяти.

В режиме «Без протокола» появилась возможность отправки файлов.

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

Из новых возможностей на этом все. Настало время поговорить о главном...


Native AOT

При разработке приложения меня беспокоило два фактора:

  1. Большой размер.

  2. Медленный запуск.

Первая проблема постепенно решается. Одно из решений было применено при релизе версии 3.0.0, когда я перешел с WPF на Avalonia UI. Этот переход сэкономил ~60 Мб на диске (подробнее читайте тут). Но этого все равно недостаточно. И в идеале хотелось бы ужаться до ~10 Мб, но, к сожалению, это пока невозможно.

Вторая проблема раздражала меня особенно на слабых ПК. Причем это касается даже горячего запуска. А если там еще и комбинация из древнего железа и медленного HDD диска, то это просто бррррррррр…

Прочитав это вступление, для любого .NET-разработчика станет очевидно, что в данной ситуации должна помочь компиляция приложения с помощью Native AOT.

Но вот вопрос: а оно того стоит?

Нельзя просто так взять и собрать приложение. При работе с Native AOT существуют некоторые ограничения. Поэтому разберем те, с которыми столкнулся я.

Все что написано ниже актуально для .NET 9.

Проблема №1: использование заранее скомпилированных привязок

По умолчанию привязки не компилируются. Поэтому мы можем пойти по двум путям:

  • Прописывать x:CompileBindings="True" в каждом файле с разметкой.

  • В файле проекта указать такой параметр<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>

Использование заранее скомпилированных привязок вынуждает нас прописывать атрибут x:DataType в корневых элементах (например, в <Window>, <UserControl>). Это нужно для явного указания типа данных контекста привязки. У меня этого атрибута не было совсем… Поэтому мне пришлось долго и упорно прописывать его во всех файлах разметки.

Также важно не забыть проставить атрибут x:DataType у элементов DataTemplate. У меня, например, этот элемент часто встречается в ItemsControl.

Что интересно, если вы вдруг не проставите этот атрибут, то возможно два варианта развития событий:

  • Ваше приложение не соберется. Появится такая ошибка: Dynamic code generation is not supported on this platform. И будет указан файл, на который нужно обратить внимание.

  • Ваше приложение соберется, но упадет сразу после запуска.

Эта ошибка, как по мне, не сразу становится очевидной.

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

Проблема №2: сериализация и десериализация

В моем приложении все настройки и пресеты хранятся в JSON файлах. Для работы с ними я использую стандартное решение от Microsoft – System.Text.Json.

Проблема тут в том, что по умолчанию эти методы используют рефлексию, которая не поддерживается в Native AOT. После сборки компилятор выдаст соответствующие предупреждения. А при попытке использования приложение выдаст исключение типа InvalidOperationException.

Нужно обратить внимание, что у некоторых методов сериализации в System.Text.Json указаны атрибуты RequiresUnreferencedCode и RequiresDynamicCode, которые вызовут предупреждения типа IL2026 и IL3050 соответственно.

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

В моем обобщенном методе сохранения я использую такой код для сериализации:

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    TypeInfoResolver = SerializerContext.Default
};

using var stream = new FileStream(correctFilePath, FileMode.Open);

var jsonTypeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(T), options);

if (jsonTypeInfo == null)
{
    throw new Exception($"Не удалось найти тип {typeof(T)} в объявлении контекста сериализации.");
}

JsonSerializer.Serialize(stream, data, jsonTypeInfo);

С десериализацией всё проще:

var data = (T?)JsonSerializer.Deserialize(stream, typeof(T), SerializerContext.Default);

Более подробно вы можете посмотреть в репозитории этого проекта на GitHub.

Проблема №3: публикация приложения

Я же уже писал, что нельзя просто так взять и собрать приложение под Native AOT? :)

Раньше я как делал? Публиковал обе версии (для Windows и для Linux) из Visual Studio IDE и проблем не знал… Но теперь я знаю два новых правила:

  1. Собирать приложение нужно на той целевой платформе, на которой оно будет запускаться.

  2. Рекомендуется собирать на минимальной поддерживаемой версии ОС.

Первый пункт обсуждать нечего. Стоит только отметить, что тут важен не только тип ОС, но еще и разрядность системы. Нужна сборка под Linux x64 — собирай на этой платформе. Windows x32 — на Windows x32 и т.д.

А вот второй пункт может вызвать сомнения.

В интернете пишут, что, собрав приложение с использованием Native AOT на одной версии ОС, оно может быть несовместимо с более ранними версиями. Поэтому рекомендуется собирать приложение на минимальной версии ОС. Напомню, что с JIT-компиляцией таких проблем нет, так как в случае с ней исполняемый код подстраивается под среду выполнения.

Я провел свои небольшие тесты.

  • Windows
    Приложение было собрано на Windows 11. Была проверена работа на Windows 7 и 10. Все ок.

  • Linux
    Как всегда тут все сложнее… Первым делом, я собрал приложение на Ubuntu 24.10 (версия ядра Linux 6.11.0-26-generic). Все работало отлично. Затем попробовал запустить на Astra Linux Common Edition (версия ядра Linux 5.15.0-70-generic), и меня ждало разочарование. Появилось сообщение о том, что запустить приложение невозможно. Похоже интернет был в чем-то прав…

Поэтому чтобы сохранить совместимость с более ранними версиями ядра Linux, я решил собирать под версией 5.4. К слову, приложение, собранное под 5.15, успешно запустилось на версии 5.4.

Что это? Обратная совместимость в мажорных версиях или мне просто повезло?

Также важно учитывать, что помимо версии ядра Linux играет роль и окружение, в котором происходит сборка приложения.

Я натолкнулся на проблему с библиотекой GNU C Library (glibc). У меня заявлена поддержка Astra Linux Common Edition (тестирую на версии 2.12.46), где используется версия 2.24. А сборку я проводил на Ubuntu 18.04, которая использует версию 2.27. Казалось бы, у минорных версий должна быть обратная совместимость. Но ее нет! Приложение просто не запустилось на Астре.

В итоге, мне пришлось собирать версию под Linux на Астре с версией ядра 5.4.

Теперь немного поговорим об обрезке неиспользуемого кода.

Проблема №4: обрезка неиспользуемого кода

Возникает вопрос: если в решении есть несколько проектов, то нужно ли прописывать настройки Native AOT и обрезки кода в каждом проекте или только в некоторых?

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

Заметил небольшую особенность, что публикация с Native AOT и тримминг увеличивают время компиляции приложения.

Приведу небольшие данные:

JIT

Native AOT

Native AOT + тримминг

~49 сек.

~57 сек.

~70 сек.

Это средние значения. Отклонения были плюс минус 3 секунды. Мой сетап: Intel Core i5 – 13500, 32 Гб DDR5, HDD диск с проектом, SSD NVMe с Windows 11.

Думаю, эти показатели можно масштабировать и на крупные проекты.

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

Чтобы избежать такой ситуации разработчики Avalonia UI рекомендуют добавить сборку со стилями в исключения триммера.

Делается это так:

<ItemGroup>
	<TrimmerRootAssembly Include="Avalonia.Themes.Fluent" />
</ItemGroup>

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

Скрытый текст

В версии Avalonia UI 11.3.х все работает и без этой настройки. Но я решил оставить ее на всякий случай.

Проблема №5: ложные срабатывания антивируса

Я же уже писал, что нельзя просто так взять и собрать приложение под Native AOT? :)

Добавлю еще к этому, что нельзя просто так взять и начать использовать приложение, скомпилированное с помощью Native AOT.

В процессе тестирования, я начал замечать, что Windows Defender почему то активно интересуется моим приложением. То удалит его во время установки, то после скачивания. Раньше при использовании JIT-компиляции такого никогда не было.

Скрытый текст

Версию с установщиком Windows Defender обнаружил как Trojan:Win32/Bearfoos.B!ml, а zip-архив с портативной версией — как Trojan:Script/Wacatac.B!ml.

Почитав в сети об этой проблеме, оказалось, что все дело в тримминге. Причем это довольно древняя проблема, о которой уже писали. Вы даже можете почитать некоторые issue в официальном репозитории .NET Runtime на GitHub: первое, второе и третье.

На ум приходят два решения:

  • Добавить приложение в исключения антивируса.

  • Подписать приложение с помощью сертификата подписи кода.

Но может быть есть и другие решения?

Заключение про Native AOT

Решила ли компиляция приложения с помощью Native AOT два вопроса, поднятых ранее? Да, но частично…

  1. Большой размер приложения.
    Удалось срезать еще немного мегабайт.

    Windows

    Linux

    Было (Мб)

    101

    97.44

    Стало (Мб)

    68.7

    81.59

    Разница (Мб)

    ~32

    ~16

    Приятно, но не слишком существенно.

  2. Медленный запуск приложения.
    Приложение стало запускаться значительно быстрее. Это видно даже невооруженным глазом.
    Но приведу цифры:

    JIT

    Native AOT

    JIT + ReadyToRun

    Native AOT + ReadyToRun

    ~860 мс.

    ~96 мс.

    ~274 мс.

    ~100 мс.

    Я измерял время между вызовом конструктора класса App и появлением события Opened у класса MainWindow.

Но вместе с очевидными плюсами появились и минусы:

  1. Необходимость править код под требования Native AOT. А затем лишний раз тестировать.

  2. Сложность публикации.

  3. Плохая обратная совместимость. Особенно на Linux.

  4. Некоторые сборки могут быть полностью или частично несовместимы с Native AOT.

  5. Ложные срабатывания антивирусного ПО.

Имея в виду всё вышеперечисленное, могу ли я рекомендовать использовать Native AOT для десктопных приложений?

«Однозначно — да», — так было написано в первых вариантах статьи. Но затем моё мнение поменялось.

  • Windows
    Основная проблема - это ложные срабатывания антивируса. И заморачиваться с подписью бесплатного ПО я не собираюсь.

  • Linux
    Основная проблема - это обратная совместимость. JIT-компиляция обеспечивает лучшую обратную совместимость по сравнению с нативной компиляцией. Поэтому, если у вас большой зоопарк из различных дистрибутивов Linux, то ответ на вопрос уместности использования Native AOT становится не таким очевидным.

CoreBus - это, по сути, утилита, которая должна быть небольшой и производительной. А Native AOT как раз и мог бы приблизить её к этому. Но не смотря на все плюсы, минусы все таки перевесили чашу весов. Поэтому я решил пока не использовать Native AOT в этом приложении. Посмотрим, возможно в следующих версиях .NET ситуация улучшится. И как только это произойдет, мне останется раскомментировать всего две строчки в файле проекта.


Планы на развитие

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

Также я получаю различные предложения по улучшению этого приложения на почту или в личных сообщениях тут. Я очень рад, что мое приложение помогает людям в работе. И вместе с моими идеями у меня уже накопился бэклог с фичами, которые хотелось бы реализовать. К сожалению, это не всегда просто сделать. Некоторые вещи довольно сложные или просто объемные. А иногда мне мешают сторонние библиотеки, которые я использую.

Например, вот парочка ограничений от внешних библиотек.

  • System.IO.Ports
    Довольно неторопливая библиотека для работы с последовательным портом, которая любит кушать ресурсы CPU. Есть альтернативы. Например, RJCP.SerialPortStream, которая прекрасно работает на Windows, но пока не имеет части необходимого мне функционала на Linux. Решение от Microsoft работает везде одинаково и стабильно. Но если вы вдруг знайте стабильное кроссплатформенное решение, то дайте мне знать:)

  • Проблема Avalonia UI с Topmost у окна.
    Хотел добавить возможность окну макросов быть всегда сверху, но столкнулся с тем, что модальные окна иногда «залезают» под окно, если у него установлено значение Topmost. Судя по обсуждениям на GitHub, эта проблема появилась в версиях 11.x и универсального решения пока нет.

Итого

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

И напомню, что приложение тестировалось на Windows 10/11, Ubuntu и Astra Linux Common Edition.

Смотрите также:

Комментарии (7)


  1. Nagg
    11.07.2025 20:29

    Native AOT + ReadyToRun -- не очень понятно то имеется ввиду под этим.


    1. AndreyAbdulkayumov Автор
      11.07.2025 20:29

      Это я ради эксперимента попробовал одновременно использовать оба подхода к компиляции в машинный код.

      Вообще, по идее должен был быть конфликт. Потому что Native AOT это полная компиляция решения в машинный код, а ReadyToRun только частичная. Но судя по тому что приложение запустилось и его объем увеличился, можно сказать что эксперимент удался)

      Но публиковать приложение, собранное таким образом, конечно же не стоит)


      1. Nagg
        11.07.2025 20:29

        Их нельзя использовать вместе никак, это два разных рантайма. У вас на выбор 4 опции:

        1. NativeAOT

        2. JIT + R2R (обычно все базовые библиотеки в R2R идут), при этом весь R2R код (если он горячий) по итогу все равно пере-компилируется джитом в Tier1

        3. JIT-only (если насильно отключить R2R).

        4. R2R-only, но тут практически нереально добиться того чтобы джит не вызывался

        Ещё есть интерпретатор новый.


        1. AndreyAbdulkayumov Автор
          11.07.2025 20:29

          А что за интерпретатор? Microsoft добавили что то новое в будущий .NET 10?


          1. Nagg
            11.07.2025 20:29

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


  1. Siemargl
    11.07.2025 20:29

    Нда, с дотнетом прямо совсем с размерами печаль. Самый толстый модбас сканер в мире получился =)


    1. AndreyAbdulkayumov Автор
      11.07.2025 20:29

      Согласен :)

      И пока что не вижу путей как ещё уменьшить размер приложения. Native AOT был последней надеждой.

      Помню, как то пытался перенести часть функционала на Qt. Размер приложения уменьшился. Но я как разработчик не был в восторге при использовании этого фреймворка. Пока что мне больше нравится связка C# и xaml разметки.