Questa pagina può contenere testo tradotto automaticamente.

Come sviluppare applicazioni Native AOT in .NET

.NET 8 introduce un supporto completo per la compilazione ahead-of-time, nota anche come Native AOT. Queste applicazioni in genere si avviano più rapidamente e consumano meno memoria rispetto alle soluzioni gestite.

La pubblicazione .NET AOT produce applicazioni trimmate e self-contained. Tali applicazioni:

  1. Possono essere eseguite su una macchina che non ha installato il runtime .NET.
  2. Puntano a un ambiente di runtime specifico, come Windows x64.
  3. Non contengono codice inutilizzato.

Distribuisci applicazioni .NET AOT

Sviluppiamo la libreria Docotic.Pdf per l'elaborazione PDF. La compatibilità AOT è una delle richieste di funzionalità più popolari nel 2024. Questo articolo descrive il nostro percorso per raggiungere la compatibilità AOT. Imparerai come trovare e correggere i problemi AOT nei progetti .NET.

Nozioni di base sulla pubblicazione .NET AOT

Per pubblicare un'applicazione Native AOT, aggiungi la seguente proprietà al file di progetto:

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

Potresti anche dover installare alcuni prerequisiti. Quindi puoi pubblicare l'app da Visual Studio. Oppure puoi pubblicare il progetto dalla riga di comando usando il comando dotnet publish:

dotnet publish -r linux-x64 -c Release

Compatibilità AOT per le librerie .NET

La pubblicazione Native AOT è disponibile in .NET 7 e .NET 8. In .NET 7 puoi pubblicare solo applicazioni console. In .NET 8 puoi inoltre pubblicare applicazioni ASP.NET Core.

Ma la libreria core Docotic.Pdf prende di mira .NET Standard 2.0. Possiamo renderla compatibile con AOT? Certamente.

Puoi comunque trovare e correggere problemi di compatibilità AOT nelle librerie .NET che prendono di mira .NET Standard, .NET 5 o .NET 6. Le applicazioni Native AOT possono quindi basarsi sulla libreria.

Individua i problemi AOT

La prima richiesta di supporto sulla compatibilità AOT suona così:

Stiamo valutando l'uso di Docotic.Pdf compilato in WebAssembly. Produce avvisi di trimming quando lo compiliamo in AOT:
warning IL2104: Assembly 'BitMiracle.Docotic.Pdf' produced trim warnings. For more information see https://aka.ms/dotnet-illink/libraries

Non molto informativo. Riproduciamo il problema e otteniamo maggiori dettagli su questi avvisi di trimming.

Ottieni gli avvisi di pubblicazione

Pubblicare un'applicazione di test è il modo più flessibile per trovare problemi .NET AOT. Devi:

  1. Creare un progetto console .NET 8 con PublishAot = true
  2. Aggiungere un riferimento al tuo progetto e usare alcuni dei suoi tipi

Ecco la configurazione csproj che usiamo per 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>

Il valore TrimmerSingleWarn = false consente di ottenere informazioni dettagliate sugli avvisi. Altrimenti otterrai solo il messaggio "Assembly 'X' produced trim warnings".

Ed ecco il codice C# in Program.cs:

using BitMiracle.Docotic.Pdf;

using var pdf = new PdfDocument();

Aggiungi del codice per forzare il caricamento dell'assembly in verifica. Non è necessario coprire tutte le funzionalità del progetto per ottenere avvisi di pubblicazione. È sufficiente usare un solo tipo. Ma devi scrivere più codice per intercettare gli errori di runtime.

Il comando dotnet publish -r win-x64 -c Release per questo progetto ha restituito avvisi di trimming e AOT. Ecco la versione breve dell'elenco:

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.

Abbiamo trovato la causa principale. Prima di correggere i problemi, verifichiamo opzioni alternative per individuare i problemi di compatibilità AOT.

Usa gli analizzatori di compatibilità AOT

Microsoft fornisce analizzatori Roslyn per trovare problemi AOT nei progetti .NET 7 o .NET 8. Possono evidenziare i problemi AOT mentre scrivi il codice. Ma la pubblicazione di un'applicazione di test rileva più problemi.

Per usare l'analisi statica, aggiungi la seguente proprietà al file .csproj:

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

Devi destinare il framework .NET 7 o .NET 8 per usare questo tipo di analisi in un progetto .NET Standard 2.0. La configurazione del progetto potrebbe apparire così:

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

Abbiamo applicato la configurazione sopra al nostro grande progetto core e ottenuto 1036 errori di compilazione.

Molti errori di compilazione

Erano tutti collegati alla compatibilità AOT? Avevamo ricompilato il progetto senza la proprietà IsAotCompatible. Eppure, 1036 errori.

Tutti gli errori erano collegati ai miglioramenti in .NET 8 rispetto a .NET Standard 2.0. Nel nostro caso, la maggior parte dei problemi riguardava un supporto migliore per i tipi nullable reference. C'erano anche molti errori CA1500 e CA1512 relativi ai nuovi metodi helper ArgumentOutOfRangeException.ThrowIfLessThan e ArgumentNullException.ThrowIfNull.

Abbiamo corretto tutti gli errori e attivato la proprietà IsAotCompatible. La compilazione del progetto è terminata senza errori AOT. L'analisi statica non ha rilevato nessuno degli avvisi trovati durante il processo di pubblicazione 🙁

Per conferma, abbiamo aggiunto il seguente codice di test alla nostra code base:

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

Questo codice C# è incompatibile al 100% con .NET AOT e trimming. E l'analisi statica ha identificato i seguenti errori:

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.

Quindi, gli analizzatori Roslyn funzionano e possono trovare alcuni problemi. Nel nostro caso, non hanno trovato nulla.

Individua gli errori di runtime

L'articolo Come rendere le librerie compatibili con native AOT afferma il seguente principio:

Se un'applicazione non ha avvisi quando viene pubblicata per AOT, si comporterà allo stesso modo dopo AOT come senza AOT.

In pratica, la versione dopo AOT può comunque comportarsi in modo diverso. Avevamo corretto tutti gli avvisi AOT e di trimming. Tutti i test automatici erano passati. L'applicazione di test elaborava correttamente i documenti PDF. E la versione AOT della stessa applicazione produceva documenti non corretti.

L'applicazione AOT continuava a non funzionare

La pubblicazione AOT aveva rimosso per errore del codice necessario. Abbiamo creato l'issue Optimization removes necessary code. Il team .NET lo ha confermato e corretto rapidamente. E abbiamo aggiunto una soluzione alternativa al nostro codice C# finché la correzione non sarà disponibile.

La lezione di questa storia è che gli avvisi di pubblicazione e di analisi statica potrebbero non essere sufficienti. Usa le funzionalità chiave del tuo progetto nell'applicazione di test. E esegui l'applicazione di test dopo la distribuzione.

L'applicazione di test Docotic.Pdf usa compressione PDF, estrazione di testo e altre funzionalità chiave. La usiamo così:

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

Il test manuale dell'applicazione pubblicata non è comodo. Vale la pena automatizzare il processo.

Rileva automaticamente i problemi AOT

Lo sviluppo di Docotic.Pdf si basa su test automatici. Esistono test per ogni funzionalità o bug. La compatibilità .NET AOT non fa eccezione.

Ci sono due modi per automatizzare i test delle applicazioni Native AOT.

Colloca i test all'esterno dell'applicazione AOT

Qui usi un normale progetto NUnit/xUnit/MSTest. Prima di compilare questo progetto, pubblichi l'applicazione di test AOT. I test eseguono l'app pubblicata usando il metodo Process.Start.

Il vantaggio principale è la separazione dell'infrastruttura di test dall'app AOT. Puoi usare tutte le capacità dei framework di unit testing. Eventuali problemi di compatibilità AOT nel framework di test non contano. È anche più facile riutilizzare i casi di test esistenti.

La configurazione di esempio del progetto di test è disponibile su GitHub. Il progetto Docotic.Tests contiene test automatici per entrambe le versioni, gestita e AOT. AotCompatibility è un'applicazione di test per la pubblicazione AOT.

AotCompatibility.csproj pubblica l'applicazione di test in un evento post-build nella configurazione Release. La condizione '$(PublishProtocol)'=='' disabilita questo passaggio quando usi la procedura guidata "Publish" in Visual Studio.

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

Lo script publish.bat è semplice:

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

Nota che qui usiamo l'argomento --no-build. Il progetto è già compilato prima dell'evento post-build. Non è necessario compilarlo di nuovo.

I test in NativeAot.cs eseguono AotCompatibility.exe pubblicato con argomenti diversi. I progetti Docotic.Tests e AotCompatibility condividono lo stesso codice testato. Questo ci consente di verificare le versioni gestita e AOT senza duplicare il codice.

Colloca i test all'interno dell'applicazione AOT

Un approccio alternativo consiste nello scrivere i test all'interno dell'applicazione di test AOT. A luglio 2024 puoi usare solo l'anteprima iniziale di MS Test. Leggi l'articolo Testing your native AOT Applications per maggiori dettagli.

Il problema principale è che qualsiasi problema AOT nel framework di test renderà i test inaffidabili. E solo un insieme limitato di funzionalità del framework di test è disponibile. Per questo preferiamo collocare l'infrastruttura di test all'esterno dell'applicazione AOT.

Correggi gli avvisi AOT e di trimming

Evita di sopprimere gli avvisi di pubblicazione. Di solito, la soppressione è una soluzione errata. Quando sopprimi avvisi AOT o di trimming, dichiari che il tuo progetto è compatibile con AOT. Tuttavia, la causa principale dell'avviso è ancora presente. Qualsiasi avviso soppresso può infine portare a un errore di runtime dell'applicazione.

Prova a rimuovere o riscrivere il codice associato a ciascun avviso. Anche l'aggiornamento di componenti di terze parti può aiutare.

Rivediamo come abbiamo corretto gli avvisi AOT e di trimming in Docotic.Pdf. Ne hai già visto alcuni nella sezione Ottieni gli avvisi di pubblicazione.

Avvisi di trimming da LibLog

Usavamo la libreria LibLog per la registrazione. LibLog si basa sulla reflection per rilevare e usare framework di logging diffusi. Per progettazione, non è compatibile con AOT.

Al momento, il pacchetto Microsoft.Extensions.Logging è lo standard per il logging in .NET. E lo sviluppo di LibLog è ormai fermo.

Dato questo, abbiamo rimosso completamente LibLog dal nostro codice. Al suo posto abbiamo rilasciato il componente aggiuntivo Docotic.Pdf.Logging. Dipende dalle interfacce Microsoft.Extensions.Logging.Abstractions. Questo ha ridotto le dimensioni della libreria core e corretto tutti gli avvisi di trimming correlati.

Nessun avviso di trimming

Un codice di debug nel componente aggiuntivo Docotic.Layout

Nel componente aggiuntivo layout, usiamo la reflection per il debug interno. Questo ha generato l'avviso di analisi del trimming IL2075.

Il codice corrispondente esiste nella code base, ma i clienti non possono usarlo tramite la API pubblica. La soluzione è escludere il codice dalla configurazione Release. Ora viene usato solo nella configurazione Debug.

Avviso AOT IL3050 da BouncyCastle

La libreria BouncyCastle ci aiuta a firmare i documenti PDF. Il seguente avviso proviene dal suo codice:

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 usa il metodo Enum.GetValues(Type), che non è compatibile con AOT.

La soluzione più semplice sarebbe usare invece l'overload Enum.GetValues<T>(). L'ultima versione di BouncyCastle lo usa già. Sfortunatamente, questo overload è disponibile solo in .NET 5 o successivo. Non è un'opzione per .NET Standard 2.0.

Abbiamo approfondito e analizzato il codice di BouncyCastle. Si è scoperto che BouncyCastle lo usa per impedire l'offuscamento delle costanti dell'enum. Invece, abbiamo disabilitato l'offuscamento per gli enum corrispondenti usando l'attributo [System.Reflection.Obfuscation(Exclude = true)]. E abbiamo rimosso gli usi non più necessari di Enum.GetValues(Type).

Un problema strano nella build offuscata

Usiamo l'offuscamento per proteggere le build di produzione. Sorprendentemente, la distribuzione .NET AOT per la build offuscata restituiva avvisi simili:

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

Tali errori non avevano codici IL30## o IL2### associati. Non era chiaro come identificare il codice correlato.

Gli errori si verificavano solo nella versione offuscata. Potrebbe essere un bug dell'offuscatore. Avevamo aggiornato l'offuscatore, ma gli errori erano ancora presenti.

Dovevamo restringere il campo del problema. Abbiamo usato il seguente processo, basato su una ricerca binaria:

  1. Disabilita l'offuscamento per metà dei namespace. Verifica se si verificano errori. Continua finché trovi i namespace che portano agli errori.
  2. Disabilita l'offuscamento per metà dei tipi nel namespace del passaggio uno. Verifica se si verificano errori. Continua finché trovi i tipi che portano agli errori.
  3. Disabilita l'offuscamento per metà dei membri nel tipo del passaggio due. Verifica se si verificano errori. Continua finché trovi i membri che portano agli errori.
  4. Rivedi il codice del membro del passaggio tre. Commenta le parti insolite e verifica se si verificano errori.

Siamo arrivati al passaggio uno 🙂 Dopo aver disabilitato l'offuscamento per tutti i namespace, gli errori continuavano a verificarsi.

Abbiamo analizzato l'assembly generato con ILSpy. Abbiamo trovato alcuni tipi inaspettati generati dal compilatore. ILSpy ne mostrava gli utilizzi a partire da un codice C# simile:

interface X
{
    void f();
}

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

Questo codice strano proveniva dalla migrazione di codice C vecchio stile a C#. La condizione if (obj.f != null) è completamente ridondante. Abbiamo rimosso tali condizioni e gli errori sono scomparsi.

Non usare GetCallingAssembly

Abbiamo rilevato un altro problema di runtime. La chiamata al metodo LicenseManager.AddLicenseData(string) è fallita dopo la pubblicazione AOT con il seguente errore:

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

Il metodo Assembly.GetCallingAssembly dovrebbe essere implementato per Native AOT in .NET 9. Finché la correzione non è pronta, abbiamo rifattorizzato il nostro codice per ridurre al minimo l'impatto negativo. Ora usiamo gli attributi dell'assembly chiamante solo per convalidare la licenza dell'applicazione. Gli altri tipi di licenza sono compatibili con AOT.

Contrassegna il codice con attributi speciali

Fortunatamente, siamo riusciti a correggere tutti gli avvisi AOT e di trimming nella libreria core Docotic.Pdf e nella maggior parte dei componenti aggiuntivi. Ma potresti non essere in grado di riscrivere tutto il codice incompatibile con AOT.

.NET fornisce attributi speciali per situazioni del genere. Contrassegni la API con attributi per informare i clienti dei problemi AOT noti. Gli utenti della tua API riceveranno un avviso quando chiamano il metodo contrassegnato.

Gli attributi chiave per contrassegnare il codice .NET incompatibile con AOT sono:

  • RequiresDynamicCode
  • RequiresUnreferencedCode
  • DynamicallyAccessedMembers

Puoi trovare maggiori informazioni su questi attributi negli articoli ufficiali Introduction to AOT warnings e Prepare .NET libraries for trimming.

Conclusione

La distribuzione Native AOT è un grande passo avanti nel mondo .NET. Puoi scrivere normale codice C# e ottenere un'applicazione o una libreria nativa. Queste applicazioni sono in genere più veloci e possono essere eseguite senza il runtime .NET installato. Puoi persino usare DLL pubblicate da C, Rust o altri linguaggi di programmazione non .NET.

Pubblica un'applicazione di test .NET 8 per trovare problemi di compatibilità AOT. Esistono anche analizzatori Roslyn, ma rilevano meno problemi.

La libreria core Docotic.Pdf è ora compatibile con AOT e trimming. Anche i seguenti componenti aggiuntivi sono compatibili:

  • Componente aggiuntivo Conformance
  • Componente aggiuntivo Layout
  • Componente aggiuntivo Gdi
  • Componente aggiuntivo Logging

Il componente aggiuntivo HTML to PDF contiene ancora avvisi di trimming. Questo è il nostro prossimo obiettivo nella roadmap di Native AOT.

Sentiti libero di contattarci per domande su Native AOT o sull'elaborazione PDF.