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 connue sous le nom de Native AOT. Ces applications sont généralement plus rapides et consomment moins de mémoire que les solutions gérées.

La publication .NET AOT produit des applications réduites et autonomes. De telles applications :

  1. Peut s'exécuter sur une machine sur laquelle le runtime .NET n'est pas installé.
  2. Ciblez un environnement d'exécution spécifique, tel que Windows x64.
  3. Ne contient pas de code inutilisé.

Déployer des applications .NET AOT

Nous développons la bibliothèque Docotic.Pdf pour le traitement des PDF. La compatibilité AOT est l'une des demandes de fonctionnalités les plus populaires en 2024. Cet article décrit notre parcours pour atteindre la compatibilité AOT. Vous apprendrez comment rechercher et résoudre 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 également installer certains prérequis. Ensuite, vous pouvez publier votre application depuis Visual Studio. Ou, vous pouvez publier le projet à partir de 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 également publier des applications ASP.NET Core.

Mais la bibliothèque principale Docotic.Pdf cible .NET Standard 2.0. Pouvons-nous le rendre convivial pour l'AOT ? Bien sûr.

Vous pouvez toujours rechercher et résoudre les problèmes de compatibilité AOT dans les bibliothèques .NET ciblant .NET Standard, .NET 5 ou .NET 6. Les applications Native AOT peuvent alors s'appuyer sur la bibliothèque.

Rechercher des problèmes AOT

Notre première demande d'assistance concernant la compatibilité AOT ressemble à ceci :

Nous envisageons d'utiliser Docotic.Pdf compilé sur WebAssembly. Il produit des avertissements de trim lorsque nous le compilons en AOT :
avertissement IL2104 : l'assemblage 'BitMiracle.Docotic.Pdf' a produit des avertissements de compensation. Pour plus d'informations, voir https://aka.ms/dotnet-illink/libraries

Pas très informatif. Reproduisons le problème et obtenons plus de détails sur ces avertissements de trim.

Recevoir des avertissements de publication

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

  1. Créez un projet de console .NET 8 avec PublishAot = true
  2. Ajoutez une référence à votre projet et utilisez 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>

Le TrimmerSingleWarn = false vous permet d'obtenir des informations détaillées sur les avertissements. Sinon, vous n'obtiendrez que le message « Avertissements de trim produits par l'assemblage 'X' ».

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 toutes les fonctionnalités de votre projet pour recevoir des avertissements de publication. Utiliser un seul type suffit. Mais vous devez écrire plus de code pour capter les erreurs d'exécution.

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

Avertissement d'analyse AOT IL3050: Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): L'utilisation du membre 'System.Enum.GetValues(Type)' qui a 'RequiresDynamicCodeAttribute' peut interrompre la fonctionnalité lors de la compilation AOT.

Avertissement d'analyse de découpage IL2026: LogProviders.Log4NetLogProvider.Log4NetLogger.GetCreateLoggingEvent(ParameterExpression,UnaryExpression,ParameterExpression,UnaryExpression,Type): L’utilisation du membre 'System.Linq.Expressions.Expression.Property(Expression,String)' qui a 'RequiresUnreferencedCodeAttribute' peut interrompre la fonctionnalité lors de la réduction du code d’application.

Avertissement d'analyse de découpage IL2057: LogProviders.LogProviderBase.FindType(String,String[]): Valeur non reconnue passée au paramètre 'typeName' de la méthode 'System.Type.GetType(String)'.

Avertissement d'analyse de découpage IL2070: LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String): L'argument 'this' ne satisfait pas 'DynamicallyAccessedMemberTypes.PublicProperties' dans l'appel à 'System.Type.GetProperty(String)'. Le paramètre 'loggerType' de la méthode 'LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String)' n’a pas d’annotations correspondantes. La valeur source doit déclarer au moins les mêmes exigences que celles déclarées sur l'emplacement cible auquel elle est affectée.

Avertissement d'analyse de découpage IL2075: LogProviders.Log4NetLogProvider.GetOpenNdcMethod(): L'argument 'this' ne satisfait pas 'DynamicallyAccessedMemberTypes.PublicProperties' dans l'appel à 'System.Type.GetProperty(String)'. La valeur de retour de la méthode 'LogProviders.LogProviderBase.FindType(String,String)' n'a pas d'annotations correspondantes. La valeur source doit déclarer au moins les mêmes exigences que celles déclarées sur l'emplacement cible auquel elle est affectée.

Nous avons trouvé la cause profonde. Avant de résoudre les problèmes, vérifions les options alternatives pour trouver les problèmes de compatibilité AOT.

Utiliser des analyseurs de compatibilité AOT

Microsoft fournit des analyseurs Roslyn pour rechercher 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 avons obtenu 1 036 erreurs de construction.

De nombreuses erreurs de construction

Étaient-ils tous liés à la compatibilité AOT ? Nous avions reconstruit le projet sans la propriété IsAotCompatible. Pourtant, 1036 erreurs.

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

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

Pour confirmer, nous avions 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 100% incompatible avec .NET AOT et trimming. Et l'analyse statique a identifié les erreurs suivantes :

erreur IL3050: L'utilisation du membre 'System.Type.MakeGenericType(params Type[])' qui a 'RequiresDynamicCodeAttribute' peut interrompre la fonctionnalité lors de la compilation AOT. Le code natif de cette instanciation peut ne pas être disponible au moment de l'exécution.

erreur IL2057: Valeur non reconnue passée au paramètre 'typeName' de la méthode 'System.Type.GetType(String)'. Il n'est pas possible de garantir la disponibilité du type cible.

Ainsi, les analyseurs Roslyn fonctionnent et peuvent détecter certains problèmes. Dans notre cas, ils n'ont rien trouvé.

Détecter les erreurs d'exécution

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

Si une application ne reçoit aucun avertissement lors de sa publication pour AOT, elle se comportera de la même manière après AOT que sans AOT.

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

L'application AOT ne fonctionne toujours pas

La publication AOT a supprimé par erreur certains codes requis. Nous avons créé le problème L'optimisation supprime le code nécessaire. L'équipe .NET l'a rapidement confirmé et corrigé. Et nous avons ajouté une solution de contournement à notre code C# jusqu'à ce que le correctif soit disponible.

La morale de cette histoire est que les avertissements de publication et d’analyse statique ne suffisent peut-être pas. 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 comme ceci :

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

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

Détecter automatiquement les problèmes AOT

Le développement de Docotic.Pdf est basé sur des tests automatiques. Il existe des tests pour chaque fonctionnalité ou bug. La compatibilité .NET AOT ne fait pas exception.

Il existe deux manières d'automatiser les tests des applications Native AOT.

Placer des tests en dehors de l'application AOT

Ici, vous utilisez un projet NUnit/xUnit/MSTest standard. Avant de créer 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. Les problèmes de compatibilité AOT dans le cadre de test n'ont pas d'importance. Il est également plus facile de réutiliser des cas de tests existants.

L'exemple de configuration du projet de test est disponible sur GitHub. Le projet Docotic.Tests contient des tests automatiques pour les versions gérées et AOT. AotCompatibility est une application de test pour la publication AOT.

Le 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 « Publier » 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à construit avant l'événement post-build. Nous n’avons pas besoin de le reconstruire.

Les tests dans NativeAot.cs exécutent le 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 gérées et AOT sans dupliquer le code.

Placez des tests dans l'application AOT

Une approche alternative consiste à écrire des tests dans l’application de test AOT. En juillet 2024, vous ne pourrez utiliser que l’aperçu anticipé de MS Test. Lisez l'article Test de vos applications Native AOT pour plus de détails.

Le problème clé est que tout problème AOT dans le cadre 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.

Correction des avertissements AOT et trim

Évitez de supprimer les avertissements de publication. Habituellement, la suppression est une solution incorrecte. Lorsque vous supprimez les avertissements AOT ou supprimez, vous dites que votre projet est compatible AOT. Cependant, la cause première de l’avertissement est toujours là. Tout avertissement supprimé peut finalement conduire à un échec d'exécution de votre application.

Essayez de supprimer ou de réécrire le code associé à chaque avertissement. La mise à jour de composants tiers peut également être utile.

Voyons comment nous avons corrigé l'AOT et supprimé les avertissements dans Docotic.Pdf. Vous en avez déjà vu quelques-uns dans la section Recevoir des avertissements de publication.

Supprimer les avertissements de LibLog

Nous avons utilisé la bibliothèque LibLog pour la journalisation. LibLog s'appuie sur la réflexion pour détecter et utiliser les frameworks de journalisation populaires. Il est incompatible avec AOT de par sa conception.

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

Compte tenu de cela, nous avions complètement supprimé LibLog de notre code. Au lieu de cela, nous avions publié le module complémentaire Docotic.Pdf.Logging. Cela 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 suppression associés.

Aucun avertissement de trim

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

Dans le module complémentaire Docotic.Layout, nous utilisons la réflexion pour le débogage interne. Cela a conduit à l’avertissement IL2075.

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

Avertissement AOT IL3050 de BouncyCastle

La bibliothèque BouncyCastle nous aide à signer des documents PDF. L'avertissement suivant provient du code BouncyCastle :

Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): L'utilisation du membre 'System.Enum.GetValues(Type)' qui a 'RequiresDynamicCodeAttribute' peut interrompre la fonctionnalité lors de la compilation AOT. Il n'est peut-être pas possible de créer un tableau de type enum au moment de l'exécution. Utilisez plutôt la surcharge GetValues<TEnum> ou la méthode GetValuesAsUnderlyingType.

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 que dans .NET 5 ou version ultérieure. Ce n'est pas une option pour .NET Standard 2.0.

Nous avons creusé plus profondément et analysé le code de BouncyCastle. Il s'est avéré que BouncyCastle l'utilise pour empêcher l'obscurcissement des constantes d'énumération. Au lieu de cela, nous avons désactivé l'obscurcissement des énumérations correspondantes à l'aide de l'attribut [System.Reflection.Obfuscation(Exclude = true)]. Et suppression des utilisations plus nécessaires de Enum.GetValues(Type).

Un problème étrange dans la version obscurcie

Nous utilisons l'obscurcissement pour protéger les versions de production. Étonnamment, le déploiement .NET AOT pour la version obscurcie a renvoyé des avertissements similaires :

<inconnu>:0: erreur : le symbole '__GenericLookupFromType_BitMiracle_Docotic_Pdf___4<System___Canon__System___Canon__System___Canon__Int32>_TypeHandle___System___Canon' est déjà défini

De telles erreurs n'étaient pas associées à des codes IL30## ou IL2###. On ne savait pas comment identifier le code associé.

Des erreurs se sont produites uniquement dans la version obscurcie. Il s'agit peut-être d'un bug de l'obfuscateur. Nous avions mis à jour l'obfuscateur, mais des erreurs étaient toujours présentes.

Nous devions affiner la portée du problème. Nous avons utilisé le processus suivant, basé sur une recherche binaire :

  1. Désactivez l'obscurcissement pour la moitié des espaces de noms. Vérifiez si des erreurs se produisent. Continuez jusqu'à trouver les espaces de noms qui conduisent aux erreurs.
  2. Désactivez l'obscurcissement pour la moitié des types dans l'espace de noms dès la première étape. Vérifiez si des erreurs se produisent. Continuez jusqu'à trouver les types qui conduisent aux erreurs.
  3. Désactivez l’obscurcissement pour la moitié des membres du type à partir de la deuxième étape. Vérifiez si des erreurs se produisent. Continuez jusqu'à trouver les membres qui conduisent aux erreurs.
  4. Vérifiez le code du membre de la troisième étape. Commentez les parties inhabituelles et vérifiez si des erreurs se produisent.

Nous nous sommes retrouvés à la première étape 🙂 Après avoir désactivé l'obscurcissement pour tous les espaces de noms, des erreurs se produisaient toujours.

Nous avons analysé l'assembly généré avec ILSpy. Trouvé quelques types inattendus générés par le compilateur. ILSpy a montré les utilisations de ces types à partir du code C# :

interface X
{
    void f();
}

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

Ce code étrange provient de la migration du code C à l'ancienne vers C#. La condition if (obj.f != null) est complètement redondante. Nous avons supprimé ces conditions et les erreurs ont disparu.

Ne pas utiliser GetCallingAssembly

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

Assembly.GetCallingAssembly() renvoie System.PlatformNotSupportedException : l'opération n'est pas prise en charge sur cette plate-forme.

La méthode Assembly.GetCallingAssembly devrait être implémentée pour Native AOT dans .NET 9. Jusqu'à ce que le correctif soit prêt, nous avons refactorisé notre code pour minimiser l'impact négatif. Désormais, nous utilisons les attributs de l'assembly appelant pour valider la licence Application uniquement. D'autres types de licences sont compatibles avec AOT.

Marquez le code avec des attributs spéciaux

Heureusement, nous avons pu corriger tous les avertissements AOT et trim dans la bibliothèque principale Docotic.Pdf et la plupart des modules complémentaires. Mais vous ne pourrez peut-être pas réécrire tout le code incompatible avec 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 lors de l'appel de la méthode marquée.

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

  • RequiresDynamicCode
  • RequiresUnreferencedCode
  • DynamicallyAccessedMembers

Vous pouvez trouver plus d'informations sur ces attributs dans les articles officiels Introduction aux avertissements AOT et Préparer les bibliothèques .NET pour le découpage.

Conclusion

Le déploiement Native AOT constitue un grand pas en avant dans le monde .NET. Vous pouvez écrire du code C# standard et obtenir une application ou une bibliothèque native. Ces applications sont généralement plus rapides et peuvent s’exécuter sans que le runtime .NET soit installé. Vous pouvez même utiliser des DLL publiées à partir de 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 également des analyseurs Roslyn, mais ils détectent moins de problèmes.

La bibliothèque principale Docotic.Pdf est désormais compatible avec AOT et trimming. Les modules complémentaires suivants sont également compatibles :

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

Le module complémentaire HTML vers PDF contient toujours des avertissements de découpage. C'est notre prochain objectif sur la feuille de route Native AOT.

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