Cette page peut contenir du texte traduit automatiquement.

Comment développer des applications Native AOT dans .NET

.NET 8 apporte une prise en charge complète de la compilation anticipée, également appelée Native AOT. De telles applications démarrent généralement plus vite et consomment moins de mémoire que les solutions managées.

La publication AOT de .NET produit des applications tronquées et autonomes. De telles applications :

  1. Peuvent s’exécuter sur une machine qui n’a pas le runtime .NET installé.
  2. Ciblent un environnement d’exécution spécifique, comme Windows x64.
  3. Ne contiennent pas de code inutilisé.

Déployer des applications .NET AOT

Nous développons la bibliothèque Docotic.Pdf pour le traitement de PDF. La compatibilité AOT est l’une des demandes de fonctionnalités les plus populaires en 2024. Cet article décrit notre parcours vers la compatibilité AOT. Vous apprendrez à trouver et corriger les problèmes AOT dans les projets .NET.

Bases de la publication .NET AOT

Pour publier une application Native AOT, ajoutez la propriété suivante à votre fichier projet :

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

Vous devrez peut-être aussi installer certains prérequis. Ensuite, vous pouvez publier votre application depuis Visual Studio. Ou bien, vous pouvez publier le projet depuis la ligne de commande à l’aide de la commande dotnet publish :

dotnet publish -r linux-x64 -c Release

Compatibilité AOT pour les bibliothèques .NET

La publication Native AOT est disponible dans .NET 7 et .NET 8. Dans .NET 7, vous pouvez uniquement publier des applications console. Dans .NET 8, vous pouvez en plus publier des applications ASP.NET Core.

Mais la bibliothèque principale Docotic.Pdf cible .NET Standard 2.0. Pouvons-nous la rendre compatible AOT ? Bien sûr.

Vous pouvez toujours détecter et corriger les problèmes de compatibilité AOT dans les bibliothèques .NET ciblant .NET Standard, .NET 5 ou .NET 6. Les applications Native AOT peuvent ensuite s’appuyer sur la bibliothèque.

Trouver les problèmes AOT

La première demande de support sur la compatibilité AOT ressemble à ceci :

Nous envisageons d’utiliser Docotic.Pdf compilé en WebAssembly. Il produit des avertissements de troncature lorsque nous le compilons en AOT :
warning IL2104: Assembly 'BitMiracle.Docotic.Pdf' produced trim warnings. For more information see https://aka.ms/dotnet-illink/libraries

Ce n’est pas très informatif. Reproduisons le problème et obtenons plus de détails sur ces avertissements de troncature.

Obtenir les avertissements de publication

La publication d’une application de test est le moyen le plus flexible pour trouver les problèmes AOT .NET. Vous devez :

  1. Créer un projet console .NET 8 avec PublishAot = true
  2. Ajouter une référence à votre projet et utiliser certains de ses types

Voici la configuration csproj que nous utilisons pour 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 vous permet d’obtenir des informations détaillées sur les avertissements. Sinon, vous n’obtiendrez que le message "Assembly 'X' produced trim warnings".

Et voici le code C# dans Program.cs :

using BitMiracle.Docotic.Pdf;

using var pdf = new PdfDocument();

Ajoutez du code pour forcer le chargement de l’assembly en cours de vérification. Il n’est pas nécessaire de couvrir toute la fonctionnalité de votre projet pour obtenir des avertissements de publication. L’utilisation d’un seul type suffit. Mais vous devez écrire davantage de code pour capturer les erreurs d’exécution.

La commande dotnet publish -r win-x64 -c Release pour ce projet a renvoyé des avertissements de troncature et AOT. Voici la version courte de la liste :

AOT analysis warning IL3050: Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): Using member 'System.Enum.GetValues(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling.

Trim analysis warning IL2026: LogProviders.Log4NetLogProvider.Log4NetLogger.GetCreateLoggingEvent(ParameterExpression,UnaryExpression,ParameterExpression,UnaryExpression,Type): Using member 'System.Linq.Expressions.Expression.Property(Expression,String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code.

Trim analysis warning IL2057: LogProviders.LogProviderBase.FindType(String,String[]): Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'.

Trim analysis warning IL2070: LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The parameter 'loggerType' of method 'LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Trim analysis warning IL2075: LogProviders.Log4NetLogProvider.GetOpenNdcMethod(): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The return value of method 'LogProviders.LogProviderBase.FindType(String,String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Nous avons trouvé la cause racine. Avant de corriger les problèmes, examinons d’autres options pour détecter les problèmes de compatibilité AOT.

Utiliser les analyseurs de compatibilité AOT

Microsoft fournit des analyseurs Roslyn pour détecter les problèmes AOT dans les projets .NET 7 ou .NET 8. Ils peuvent mettre en évidence les problèmes AOT lorsque vous écrivez le code. Mais la publication d’une application de test détecte davantage de problèmes.

Pour utiliser l’analyse statique, ajoutez la propriété suivante au fichier .csproj :

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

Vous devez cibler le framework .NET 7 ou .NET 8 pour utiliser une telle analyse dans un projet .NET Standard 2.0. La configuration du projet peut ressembler à ceci :

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

Nous avons appliqué la configuration ci-dessus à notre grand projet principal et obtenu 1036 erreurs de compilation.

Nombreuses erreurs de compilation

Étaient-elles toutes liées à la compatibilité AOT ? Nous avons recompilé le projet sans la propriété IsAotCompatible. Toujours 1036 erreurs.

Toutes les erreurs étaient liées aux améliorations de .NET 8 par rapport à .NET Standard 2.0. Dans notre cas, la plupart des problèmes concernaient la meilleure prise en charge des types de référence nullable. Il y avait aussi de nombreuses erreurs CA1500 et CA1512 à propos des nouvelles méthodes d’assistance ArgumentOutOfRangeException.ThrowIfLessThan et ArgumentNullException.ThrowIfNull.

Nous avons corrigé toutes les erreurs puis activé la propriété IsAotCompatible. La compilation du projet s’est terminée sans erreurs AOT. L’analyse statique n’a détecté aucun des avertissements trouvés pendant le processus de publication 🙁

Pour confirmer, nous avons ajouté le code de test suivant à notre base de code :

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);
}

Ce code C# est incompatible à 100 % avec .NET AOT et la troncature. Et l’analyse statique a identifié les erreurs suivantes :

error IL3050: Using member 'System.Type.MakeGenericType(params Type[])' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. The native code for this instantiation might not be available at runtime.

error IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'. It's not possible to guarantee the availability of the target type.

Donc, les analyseurs Roslyn fonctionnent et peuvent trouver certains problèmes. Dans notre cas, ils n’ont rien trouvé.

Capturer les erreurs d’exécution

L’article Comment rendre les bibliothèques compatibles avec native AOT énonce le principe suivant :

Si une application n’a aucun avertissement lors de sa publication pour AOT, elle se comportera de la même façon après AOT que sans AOT.

En pratique, la version après AOT peut quand même fonctionner différemment. Nous avions corrigé tous les avertissements AOT et de troncature. Tous les tests automatiques ont réussi. L’application de test traitait correctement les documents PDF. Et la version AOT de la même application produisait des documents incorrects.

L’application AOT ne fonctionnait toujours pas

La publication AOT a supprimé par erreur du code nécessaire. Nous avons créé le problème Optimization removes necessary code. L’équipe .NET l’a rapidement confirmé et corrigé. Et nous avons ajouté un contournement à notre code C# jusqu’à ce que la correction soit disponible.

La morale de cette histoire est que les avertissements de publication et d’analyse statique peuvent ne pas suffire. Utilisez les fonctionnalités clés de votre projet dans l’application de test. Et exécutez l’application de test après le déploiement.

L’application de test Docotic.Pdf utilise la compression PDF, l’extraction de texte et d’autres fonctionnalités clés. Nous l’utilisons ainsi :

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

Le test manuel de l’application publiée n’est pas pratique. Il vaut la peine d’automatiser le processus.

Détecter automatiquement les problèmes AOT

Le développement de Docotic.Pdf repose sur des tests automatiques. Il existe des tests pour chaque fonctionnalité ou bogue. La compatibilité .NET AOT n’est pas une exception.

Il existe deux façons d’automatiser les tests des applications Native AOT.

Placer les tests en dehors de l’application AOT

Ici, vous utilisez un projet NUnit/xUnit/MSTest classique. Avant de compiler ce projet, vous publiez l’application de test AOT. Les tests exécutent l’application publiée à l’aide de la méthode Process.Start.

Le principal avantage est la séparation de l’infrastructure de test de l’application AOT. Vous pouvez utiliser toutes les capacités des frameworks de tests unitaires. Tout problème de compatibilité AOT dans le framework de test est sans importance. Il est aussi plus facile de réutiliser les cas de test existants.

La configuration d’exemple du projet de test est disponible sur GitHub. Le projet Docotic.Tests contient des tests automatiques pour les versions managée et AOT. AotCompatibility est une application de test pour la publication AOT.

Le fichier AotCompatibility.csproj publie l’application de test lors d’un événement post-build dans la configuration Release. La condition '$(PublishProtocol)'=='' désactive cette étape lorsque vous utilisez l’assistant "Publish" dans Visual Studio.

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

Le script publish.bat est simple :

@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"

Notez que nous utilisons ici l’argument --no-build. Le projet est déjà compilé avant l’événement post-build. Nous n’avons pas besoin de le compiler à nouveau.

Les tests dans NativeAot.cs exécutent AotCompatibility.exe publié avec différents arguments. Les projets Docotic.Tests et AotCompatibility partagent le même code testé. Cela nous permet de vérifier les versions managée et AOT sans dupliquer le code.

Placer les tests à l’intérieur de l’application AOT

Une approche alternative consiste à écrire les tests à l’intérieur de l’application de test AOT. En juillet 2024, vous ne pouvez utiliser que la préversion anticipée de MS Test. Lisez l’article Tester vos applications native AOT pour plus de détails.

Le problème principal est que tout problème AOT dans le framework de test rendra vos tests peu fiables. Et seul un ensemble limité de fonctionnalités du framework de test est disponible. C’est pourquoi nous préférons placer l’infrastructure de test en dehors de l’application AOT.

Corriger les avertissements AOT et de troncature

Évitez de supprimer les avertissements de publication. En général, la suppression est une mauvaise solution. Lorsque vous supprimez des avertissements AOT ou de troncature, vous déclarez que votre projet est compatible AOT. Cependant, la cause racine de l’avertissement est toujours présente. Tout avertissement supprimé peut finalement conduire à une défaillance à l’exécution de votre application.

Essayez de supprimer ou de réécrire le code associé à chaque avertissement. La mise à jour de composants tiers peut aussi aider.

Passons en revue comment nous avons corrigé les avertissements AOT et de troncature dans Docotic.Pdf. Vous en avez déjà vu certains dans la section Obtenir les avertissements de publication.

Avertissements de troncature provenant de LibLog

Nous utilisions la bibliothèque LibLog pour la journalisation. LibLog s’appuie sur la réflexion pour détecter et utiliser les frameworks de journalisation populaires. Elle est incompatible avec AOT par conception.

Actuellement, le package Microsoft.Extensions.Logging est la norme pour la journalisation dans .NET. Et le développement de LibLog est désormais gelé.

Sur cette base, nous avons complètement supprimé LibLog de notre code. À la place, nous avons publié le module complémentaire Docotic.Pdf.Logging. Il dépend des interfaces Microsoft.Extensions.Logging.Abstractions. Cela a réduit la taille de la bibliothèque principale et corrigé tous les avertissements de troncature associés.

Aucun avertissement de troncature

Un code de débogage dans le module complémentaire Docotic.Layout

Dans le module complémentaire de mise en page, nous utilisons la réflexion pour le débogage interne. Cela a conduit à l’avertissement d’analyse de troncature IL2075.

Le code correspondant existe dans la base de code, mais les clients ne peuvent pas l’utiliser via l’API publique. La solution consiste à exclure le code de la configuration Release. Il n’est désormais utilisé que dans la configuration Debug.

Avertissement AOT IL3050 provenant de BouncyCastle

La bibliothèque BouncyCastle nous aide à signer des documents PDF. L’avertissement suivant provient de son code :

Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): Using member 'System.Enum.GetValues(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. It might not be possible to create an array of the enum type at runtime. Use the GetValues<TEnum> overload or the GetValuesAsUnderlyingType method instead.

BouncyCastle utilise la méthode Enum.GetValues(Type), qui n’est pas compatible AOT.

La solution la plus simple serait d’utiliser à la place la surcharge Enum.GetValues<T>(). La dernière version de BouncyCastle l’utilise déjà. Malheureusement, cette surcharge n’est disponible qu’à partir de .NET 5. Ce n’est pas une option pour .NET Standard 2.0.

Nous avons approfondi l’analyse du code BouncyCastle. Il s’est avéré que BouncyCastle l’utilise pour empêcher l’obfuscation des constantes d’énumération. À la place, nous avons désactivé l’obfuscation pour les énumérations correspondantes à l’aide de l’attribut [System.Reflection.Obfuscation(Exclude = true)]. Et nous avons supprimé les utilisations de Enum.GetValues(Type) qui n’étaient plus nécessaires.

Un problème étrange dans la version obfusquée

Nous utilisons l’obfuscation pour protéger les versions de production. Étonnamment, le déploiement .NET AOT pour la version obfusquée renvoyait des avertissements similaires :

<unknown>:0: error: symbol '__GenericLookupFromType_BitMiracle_Docotic_Pdf___4<System___Canon__System___Canon__System___Canon__Int32>_TypeHandle___System___Canon' is already defined

De telles erreurs n’avaient pas de codes associés IL30## ou IL2###. Il n’était pas clair comment identifier le code concerné.

Les erreurs se produisaient uniquement dans la version obfusquée. Cela pourrait être un bogue de l’obfuscateur. Nous avons mis à jour l’obfuscateur, mais les erreurs étaient toujours présentes.

Nous devions réduire le périmètre du problème. Nous avons utilisé le processus suivant, basé sur une recherche binaire :

  1. Désactiver l’obfuscation pour la moitié des espaces de noms. Vérifier si des erreurs se produisent. Continuer jusqu’à trouver les espaces de noms qui provoquent les erreurs.
  2. Désactiver l’obfuscation pour la moitié des types de l’espace de noms de l’étape 1. Vérifier si des erreurs se produisent. Continuer jusqu’à trouver les types qui provoquent les erreurs.
  3. Désactiver l’obfuscation pour la moitié des membres du type de l’étape 2. Vérifier si des erreurs se produisent. Continuer jusqu’à trouver les membres qui provoquent les erreurs.
  4. Examiner le code du membre de l’étape 3. Commenter les parties inhabituelles et vérifier si des erreurs se produisent.

Nous sommes finalement arrivés à l’étape 1 🙂 Après avoir désactivé l’obfuscation pour tous les espaces de noms, des erreurs se produisaient encore.

Nous avons analysé l’assembly généré avec ILSpy. Nous avons trouvé quelques types générés par le compilateur inattendus. ILSpy affichait leurs utilisations à partir d’un code C# similaire :

interface X
{
    void f();
}

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

Ce code étrange provenait de la migration d’un ancien code C vers C#. La condition if (obj.f != null) est complètement redondante. Nous avons supprimé de telles conditions et les erreurs ont disparu.

Ne pas utiliser GetCallingAssembly

Nous avons détecté un autre problème d’exécution. L’appel de méthode LicenseManager.AddLicenseData(string) a échoué après la publication AOT avec l’erreur suivante :

Assembly.GetCallingAssembly() throws System.PlatformNotSupportedException: Operation is not supported on this platform.

La méthode Assembly.GetCallingAssembly devrait être implémentée pour Native AOT dans .NET 9. En attendant la correction, nous avons refactoré notre code pour minimiser l’impact négatif. Désormais, nous utilisons les attributs de l’assembly appelant uniquement pour valider la licence d’application. Les autres types de licence sont compatibles avec AOT.

Marquer le code avec des attributs spéciaux

Heureusement, nous avons pu corriger tous les avertissements AOT et de troncature dans la bibliothèque principale Docotic.Pdf et la plupart des modules complémentaires. Mais il se peut que vous ne puissiez pas réécrire tout le code incompatible AOT.

.NET fournit des attributs spéciaux pour de telles situations. Vous marquez l’API avec des attributs pour informer les clients des problèmes AOT connus. Les utilisateurs de votre API recevront un avertissement lorsqu’ils appelleront la méthode marquée.

Les attributs clés pour marquer du code .NET incompatible AOT sont :

  • RequiresDynamicCode
  • RequiresUnreferencedCode
  • DynamicallyAccessedMembers

Vous pouvez trouver plus d’informations sur ces attributs dans les articles officiels Introduction to AOT warnings et Prepare .NET libraries for trimming.

Conclusion

Le déploiement Native AOT constitue une avancée importante dans l’écosystème .NET. Vous pouvez écrire du code C# classique et obtenir une application ou une bibliothèque native. De telles applications sont généralement plus rapides et peuvent s’exécuter sans le runtime .NET installé. Vous pouvez même utiliser des DLL publiées depuis C, Rust ou d’autres langages de programmation non .NET.

Publiez une application de test .NET 8 pour détecter les problèmes de compatibilité AOT. Il existe aussi des analyseurs Roslyn, mais ils détectent moins de problèmes.

La bibliothèque principale Docotic.Pdf est maintenant compatible avec AOT et la troncature. Les modules complémentaires suivants sont aussi compatibles :

  • Module complémentaire Conformance
  • Module complémentaire Layout
  • Module complémentaire Gdi
  • Module complémentaire Logging

Le module complémentaire HTML vers PDF contient encore des avertissements de troncature. C’est notre prochain objectif sur la feuille de route Native AOT.

N’hésitez pas à nous poser des questions sur Native AOT ou le traitement de PDF.