Эта страница может содержать автоматически переведенный текст.

Как разрабатывать Native AOT приложения в .NET

.NET 8 обеспечивает комплексную поддержку предварительной компиляции, также известной как Native AOT. Такие приложения обычно запускаются быстрее и потребляют меньше памяти, чем управляемые решения.

Публикация .NET AOT создает урезанные автономные приложения. Такие приложения:

  1. Могут работать на компьютере, на котором не установлена среда выполнения .NET.
  2. Ориентированы на конкретную среду выполнения, например, Windows x64.
  3. Не содержат неиспользуемого кода.

Развертывание приложений .NET AOT

Мы разрабатываем библиотеку Docotic.Pdf для обработки PDF. Совместимость с AOT — один из самых популярных запросов на добавление функций в 2024 году. В этой статье описывается наш путь к достижению совместимости с AOT. Вы узнаете, как находить и устранять проблемы AOT в проектах .NET.

Основы публикации .NET AOT

Чтобы опубликовать приложение Native AOT, добавьте в файл проекта следующее свойство:

<PropertyGroup>
    <PublishAot>true</PublishAot>
</PropertyGroup>

Вам также может потребоваться установить некоторые необходимые компоненты. Затем вы можете опубликовать свое приложение из Visual Studio. Или вы можете опубликовать проект из командной строки, используя команду dotnet publish:

dotnet publish -r linux-x64 -c Release

Совместимость AOT для библиотек .NET

Публикация Native AOT доступна в .NET 7 и .NET 8. В .NET 7 можно публиковать только консольные приложения. В .NET 8 вы можете дополнительно публиковать приложения ASP.NET Core.

Но основная библиотека Docotic.Pdf предназначена для .NET Standard 2.0. Можем ли мы сделать его дружественным к AOT? Конечно.

Вы по-прежнему можете находить и устранять проблемы совместимости AOT в .NET библиотеках, ориентированных на .NET Standard, .NET 5 или .NET 6. В этом случае Native AOT приложения могут полагаться на эту библиотеку.

Найти проблемы AOT

Первый запрос в службу поддержки по поводу совместимости AOT звучит так:

Мы рассматриваем возможность использования Docotic.Pdf, скомпилированного в WebAssembly. Он выдает предупреждения об обрезке, когда мы компилируем его в AOT:
предупреждение IL2104: сборка 'BitMiracle.Docotic.Pdf' выдала предупреждения об обрезке. Для получения дополнительной информации см. https://aka.ms/dotnet-illink/libraries.

Не очень информативно. Давайте воспроизведем проблему и получим более подробную информацию об этих предупреждениях об обрезке.

Получить предупреждения при публикации

Публикация тестового приложения — это наиболее гибкий способ обнаружения проблем .NET AOT. Вам нужно:

  1. Создать консольный проект .NET 8 с параметром PublishAot = true.
  2. Добавить ссылку на свой проект и использовать некоторые его типы.

Вот конфигурация csproj, которую мы используем для Docotic.Pdf:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <PublishAot>true</PublishAot>
        <TrimmerSingleWarn>false</TrimmerSingleWarn>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
        <WarningsAsErrors />
    </PropertyGroup>

    <ItemGroup>
        <ProjectReference Include="..\Docotic\Docotic.csproj" />
    </ItemGroup>
</Project>

TrimmerSingleWarn = false позволяет получить подробную информацию о предупреждениях. В противном случае вы получите только сообщение «Предупреждения об обрезке сборки 'X'».

А вот С# код в файле Program.cs:

using BitMiracle.Docotic.Pdf;

using var pdf = new PdfDocument();

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

Команда dotnet publish -r win-x64 -c Release для этого проекта возвращала предупреждения об обрезке и AOT. Вот сокращенный список:

Предупреждение анализа AOT IL3050: Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): Использование метода 'System.Enum.GetValues(Type)', который имеет 'RequiresDynamicCodeAttribute', может нарушить функциональность при компиляции AOT.

Предупреждение анализа обрезки IL2026: LogProviders.Log4NetLogProvider.Log4NetLogger.GetCreateLoggingEvent(ParameterExpression,UnaryExpression,ParameterExpression,UnaryExpression,Type): Использование метода 'System.Linq.Expressions.Expression.Property(Expression,String)', который имеет 'RequiresUnreferencedCodeAttribute', может нарушить функциональность при обрезке кода приложения.

Предупреждение анализа обрезки IL2057: LogProviders.LogProviderBase.FindType(String,String[]): В параметр 'typeName' метода 'System.Type.GetType(String)' передано нераспознанное значение.

Предупреждение анализа обрезки IL2070: LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String): Аргумент 'this' не удовлетворяет требованиям 'DynamicallyAccessedMemberTypes.PublicProperties' при вызове 'System.Type.GetProperty(String)'. Параметр 'loggerType' метода 'LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String)' не имеет соответствующих аннотаций. Исходное значение должно декларировать по крайней мере те же требования, что и требования, объявленные в целевом расположении, которому оно назначено.

Предупреждение анализа обрезки IL2075: LogProviders.Log4NetLogProvider.GetOpenNdcMethod(): Аргумент 'this' не удовлетворяет требованиям 'DynamicallyAccessedMemberTypes.PublicProperties' при вызове 'System.Type.GetProperty(String)'. Возвращаемое значение метода 'LogProviders.LogProviderBase.FindType(String,String)' не имеет соответствующих аннотаций. Исходное значение должно декларировать по крайней мере те же требования, что и требования, объявленные в целевом расположении, которому оно назначено.

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

Использовать анализаторы совместимости AOT

Microsoft предоставляет анализаторы Roslyn для поиска проблем AOT в проектах .NET 7 или .NET 8. Они могут выявить проблемы AOT во время написания кода. Но с помощью публикации тестового приложения можно обнаружить больше проблем.

Чтобы использовать статический анализ, добавьте в файл .csproj следующее свойство:

<PropertyGroup>
    <IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

Чтобы использовать такой анализ в проекте .NET Standard 2.0, вам необходимо настроить платформу .NET 7 или .NET 8. Конфигурация проекта может выглядеть так:

<PropertyGroup>
    <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
    <IsAotCompatible Condition="'$(TargetFramework)'=='net8.0'">true</IsAotCompatible>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <WarningsAsErrors />
</PropertyGroup>

Мы применили вышеуказанную конфигурацию к нашему большому основному проекту и получили 1036 ошибок сборки.

Много ошибок сборки

Все ли они связаны с совместимостью AOT? Мы пересобрали проект без свойства IsAotCompatible. По-прежнему 1036 ошибок.

Все ошибки связаны с улучшениями .NET 8 по сравнению с .NET Standard 2.0. В нашем случае большинство проблем заключалось в лучшей поддержке ссылочных типов, допускающих значение null. Также было много ошибок CA1500 и CA1512 в отношении новых вспомогательных методов ArgumentOutOfRangeException.ThrowIfLessThan и ArgumentNullException.ThrowIfNull.

Мы исправили все ошибки и включили свойство IsAotCompatible. Сборка проекта завершилась без ошибок AOT. Статический анализ не уловил ни одного предупреждения, обнаруженного в процессе публикации 🙁

Для подтверждения мы добавили в нашу базу кода следующий тестовый код:

Type t = typeof(int);
t = typeof(List<>).MakeGenericType(t);
Console.WriteLine(Activator.CreateInstance(t));

string s = Console.ReadLine() ?? string.Empty;
Type? type = Type.GetType(s);
if (type != null)
{
    foreach (var m in type.GetMethods())
        Console.WriteLine(m.Name);
}

Этот код C# на 100% несовместим с .NET AOT и обрезкой. И статический анализ выявил следующие ошибки:

ошибка IL3050: Использование метода 'System.Type.MakeGenericType(params Type[])', который имеет 'RequiresDynamicCodeAttribute', может нарушить функциональность при компиляции AOT. Нативный код для этого экземпляра может быть недоступен во время выполнения.

ошибка IL2057: В параметр 'typeName' метода 'System.Type.GetType(String)' передано нераспознанное значение. Невозможно гарантировать доступность целевого типа.

Итак, анализаторы Roslyn работают и могут найти некоторые проблемы. В нашем случае они ничего не нашли.

Обнаружить ошибки во время выполнения

В статье [Как сделать библиотеки совместимыми с native AOT]](!https://devblogs.microsoft.com/dotnet/creating-aot-compatible-libraries/) сформулирован следующий принцип:

Если приложение не имеет предупреждений при публикации для AOT, после AOT оно будет вести себя так же, как и без AOT.

На практике версия после AOT все еще может работать по-другому. Мы исправили все предупреждения AOT и триммера. Все автоматические тесты пройдены. Тестовое приложение правильно обрабатывало PDF документы. А версия AOT того же приложения выдавала неправильные документы.

Приложение AOT по-прежнему не работало

Публикация AOT по ошибке удалила часть необходимого кода. Мы создали проблему Оптимизация удаляет необходимый код. Команда .NET быстро подтвердила и исправила её. И мы добавили обходной путь в наш код C# до тех пор, пока исправление не станет доступным.

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

Тестовое приложение Docotic.Pdf использует сжатие PDF-файлов, извлечение текста и другие ключевые функции. Мы используем его следующим образом:

AotCompatibility.exe compress "C:\compressed.pdf" "C:\input.pdf" "optional-password"

Ручное тестирование опубликованного приложения неудобно. Стоит автоматизировать процесс.

Автоматическое обнаружение проблем AOT

Разработка Docotic.Pdf основана на автоматических тестах. Есть тесты для каждой функции или ошибки. Совместимость .NET AOT не является исключением.

Есть два способа автоматизировать тестирование приложений Native AOT.

Разместить тесты вне приложения AOT

Здесь вы используете обычный проект NUnit/xUnit/MSTest. Прежде чем создавать этот проект, вы публикуете тестовое приложение AOT. Тесты запускают опубликованное приложение с помощью метода Process.Start.

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

Пример конфигурации тестового проекта доступен на GitHub. Проект Docotic.Tests содержит автоматические тесты как для управляемых версий, так и для AOT. AotCompatibility — это тестовое приложение для публикации AOT.

AotCompatibility.csproj публикует тестовое приложение в событии после сборки в конфигурации Release. Условие '$(PublishProtocol)'=='' отключает этот шаг при использовании мастера публикации в Visual Studio.

<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Release' And '$(PublishProtocol)'==''">
    <Exec Command="&quot;$(SolutionDir)Scripts\publish.bat&quot;" />
</Target>

Скрипт publish.bat прост:

@echo off

SET AOT_TEST_DIR=%~dp0\..\AotCompatibility
dotnet publish "%TRIM_TEST_DIR%\AotCompatibility.csproj" --no-build /warnaserror /p:PublishProfile="%AOT_TEST_DIR%\Properties\PublishProfiles\win-x64-installer.pubxml"

Обратите внимание, что здесь мы используем аргумент --no-build. Проект уже собран до события после сборки. Нам не нужно собирать его заново.

Тесты в NativeAot.cs запускают опубликованный файл AotCompatibility.exe с разными аргументами. Проекты Docotic.Tests и AotCompatibility используют один и тот же протестированный код. Это позволяет нам проверять управляемые версии и версии AOT без дублирования кода.

Разместить тесты внутри приложения AOT

Альтернативный подход — написание тестов внутри тестового приложения AOT. В июле 2024 г. вы сможете использовать только раннюю предварительную версию MS Test. Более подробную информацию можно найти в статье Тестирование ваших native AOT приложений.

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

Исправление предупреждений AOT и обрезки

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

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

Давайте рассмотрим, как мы исправили AOT и обрезали предупреждения в Docotic.Pdf. Некоторые из них вы уже видели в разделе Получить предупреждения при публикации.

Предупреждения обрезки от LibLog

Для логирования мы использовали библиотеку LibLog. LibLog использует рефлексию для обнаружения и использования популярных платформ ведения журналов. Он несовместим с AOT по определению.

В настоящее время пакет Microsoft.Extensions.Logging является стандартом для входа в систему .NET. А разработка LibLog сейчас заморожена.

Учитывая это, мы полностью удалили LibLog из нашего кода. Вместо этого мы выпустили дополнение Docotic.Pdf.Logging. Оно зависит от интерфейсов Microsoft.Extensions.Logging.Abstractions. Это уменьшило размер основной библиотеки и исправило все связанные с ней предупреждения об обрезке.

Нет предупреждений об обрезке

Отладочный код в дополнении Docotic.Layout

В дополнении Docotic.Layout мы используем отражение для внутренней отладки. Это привело к предупреждению IL2075.

Соответствующий код существует в базе кода, но клиенты не могут использовать его через общедоступный API. Решение состоит в том, чтобы исключить код из конфигурации Release. Теперь он используется только в конфигурации Debug.

Предупреждение AOT IL3050 от BouncyCastle

Библиотека BouncyCastle помогает нам подписывать PDF документы. Следующее предупреждение исходит из кода BouncyCastle:

Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): Использование члена 'System.Enum.GetValues(Type)' с атрибутом 'RequiresDynamicCodeAttribute' может нарушить функциональность при компиляции AOT. Во время выполнения может оказаться невозможным создать массив типа перечисления. Вместо этого используйте перегрузку GetValues<TEnum> или метод GetValuesAsUnderlyingType.

BouncyCastle использует метод Enum.GetValues(Type), который несовместим с AOT.

Самым простым решением было бы использовать вместо этого перегрузку Enum.GetValues<T>(). Последняя версия BouncyCastle уже использует его. К сожалению, эта перегрузка доступна только в .NET 5 или новее. Это не вариант для .NET Standard 2.0.

Мы копнули глубже и проанализировали код BouncyCastle. Оказалось, что BouncyCastle использует его для предотвращения обфускации констант перечисления. Вместо этого мы отключили обфускацию для соответствующих перечислений, используя атрибут [System.Reflection.Obfuscation(Exclude = true)]. И удалили ненужное использование Enum.GetValues(Type).

Странная проблема в обфусцированной сборке

Мы используем обфускацию для защиты производственных сборок. Удивительно, но развертывание .NET AOT для запутанной сборки вернуло подобные предупреждения:

<неизвестно>:0: ошибка: символ '__GenericLookupFromType_BitMiracle_Docotic_Pdf___4<System___Canon__System___Canon__System___Canon__Int32>_TypeHandle___System___Canon' уже определен

Такие ошибки не имели связанных с ними кодов IL30## или IL2###. Было неясно, как идентифицировать связанный код.

Ошибки возникали только в обфусцированной версии. Возможно, это ошибка в обфускаторе. Мы обновили обфускатор, но ошибки все равно присутствовали.

Нам нужно было сузить масштаб проблемы. Мы использовали следующий процесс, основанный на двоичном поиске:

  1. Отключите обфускацию для половины пространств имен. Проверьте, возникают ли ошибки. Продолжайте, пока не найдете пространства имен, которые приводят к ошибкам.
  2. Отключите обфускацию для половины типов в пространстве имен с первого шага. Проверьте, возникают ли ошибки. Продолжайте, пока не найдете типы, которые приводят к ошибкам.
  3. Отключите обфускацию для половины членов типа из второго шага. Проверьте, возникают ли ошибки. Продолжайте, пока не найдете члены, которые приводят к ошибкам.
  4. Просмотрите код участника из третьего шага. Закомментируйте необычные части и проверьте, возникают ли ошибки.

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

Мы проанализировали сгенерированную сборку с помощью ILSpy. Обнаружено несколько неожиданных типов, сгенерированных компилятором. ILSpy показал использование этих типов в таком коде C#:

interface X
{
    void f();
}

X obj = ..;
if (obj.f != null)
    ..

Этот странный код появился в результате миграции кода C старого стиля на C#. Условие if (obj.f != null) совершенно избыточно. Мы убрали такие условия и ошибки исчезли.

Не использовать GetCallingAssembly

Мы обнаружили еще одну проблему во время выполнения. Вызов метода LicenseManager.AddLicenseData(string) завершился неудачей после публикации AOT со следующей ошибкой:

Assembly.GetCallingAssembly() выдает System.PlatformNotSupportedException: Операция не поддерживается на этой платформе.

Метод Assembly.GetCallingAssembly должен быть реализован для Native AOT в .NET 9. Пока исправление не будет готово, мы отрефакторили наш код, чтобы минимизировать негативное влияние. Теперь мы используем атрибуты вызывающей сборки только для проверки лицензии Application. Другие типы лицензий совместимы с AOT.

Разметить код специальными атрибутами

К счастью, нам удалось исправить все предупреждения AOT и обрезки в основной библиотеке Docotic.Pdf и большинстве надстроек. Но, возможно, вы не сможете переписать весь код, несовместимый с AOT.

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

Ключевые атрибуты для маркировки кода .NET, несовместимого с AOT:

  • RequiresDynamicCode
  • RequiresUnreferencedCode
  • DynamicallyAccessedMembers

Дополнительную информацию об этих атрибутах можно найти в официальных статьях Общие сведения о предупреждениях AOT и Подготовка библиотек .NET для обрезки.

Заключение

Развертывание Native AOT — это большой шаг вперед в мире .NET. Вы можете написать обычный код C# и получить нативное приложение или библиотеку. Такие приложения обычно работают быстрее и могут работать без установленной среды выполнения .NET. Вы даже можете использовать опубликованные библиотеки DLL из C, Rust или других языков программирования, отличных от .NET.

Публикуйте тестовое приложение .NET 8, чтобы обнаружить проблемы совместимости AOT. Есть еще анализаторы Roslyn, но они обнаруживают меньше проблем.

Основная библиотека Docotic.Pdf теперь совместима с AOT и обрезкой. Также совместимы следующие адд-оны:

  • Дополнение Layout
  • Дополнение Gdi
  • Дополнение Logging

Дополнение HTML в PDF по-прежнему содержит предупреждения об обрезке. Это наша следующая цель в дорожной карте Native AOT.

Не стесняйтесь задавать нам вопросы о Native AOT или обработке PDF.