Questa pagina può contenere testo tradotto automaticamente.

Come sviluppare applicazioni Native AOT in .NET

.NET 8 offre il supporto completo per la compilazione anticipata, nota anche come Native AOT. Tali applicazioni solitamente si avviano più velocemente e consumano meno memoria rispetto alle soluzioni gestite.

La pubblicazione AOT .NET produce applicazioni ridotte e autonome. Tali applicazioni:

  1. Può essere eseguito su un computer su cui non è installato il runtime .NET.
  2. Scegli come target un ambiente runtime specifico, come Windows x64.
  3. Non contenere codice inutilizzato.

Distribuire applicazioni AOT .NET

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

Nozioni di base sulla pubblicazione AOT .NET

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

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

Potrebbe anche essere necessario installare alcuni prerequisiti. Quindi puoi pubblicare la tua app da Visual Studio. In alternativa, puoi pubblicare il progetto dalla riga di comando utilizzando 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 è possibile pubblicare solo applicazioni console. In .NET 8 è inoltre possibile pubblicare applicazioni ASP.NET Core.

Ma la libreria principale Docotic.Pdf è destinata a .NET Standard 2.0. Possiamo renderlo AOT friendly? Ovviamente.

È comunque possibile trovare e risolvere i problemi di compatibilità AOT nelle librerie .NET destinate a .NET Standard, .NET 5 o .NET 6. Le applicazioni Native AOT possono quindi fare affidamento sulla libreria.

Trova problemi AOT

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

Stiamo valutando l'utilizzo di Docotic.Pdf compilato in WebAssembly. Produce avvisi di assetto quando lo compiliamo in AOT:
avviso IL2104: l'assieme 'BitMiracle.Docotic.Pdf' produceva avvisi di rifinitura. Per ulteriori informazioni vedere https://aka.ms/dotnet-illink/libraries

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

Ricevi avvisi di pubblicazione

La pubblicazione di un'applicazione di test è il modo più flessibile per individuare i problemi AOT di .NET. Devi:

  1. Crea un progetto console .NET 8 con PublishAot = true
  2. Aggiungi un riferimento al tuo progetto e utilizza alcuni dei suoi tipi

Ecco la configurazione csproj che utilizziamo 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 TrimmerSingleWarn = false consente di ottenere informazioni dettagliate sugli avvisi. Altrimenti riceverai solo il messaggio "Avvisi di assetto prodotto gruppo 'X'".

Ed ecco il codice C# nel Program.cs:

using BitMiracle.Docotic.Pdf;

using var pdf = new PdfDocument();

Aggiungere del codice per forzare il caricamento dell'assembly da controllare. Non è necessario coprire tutte le funzionalità del progetto per ricevere avvisi di pubblicazione. È sufficiente utilizzare un solo tipo. Ma è necessario scrivere più codice per intercettare errori di runtime.

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

Avviso analisi AOT IL3050: Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): L'uso del membro 'System.Enum.GetValues(Type)' che ha 'RequiresDynamicCodeAttribute' può interrompere la funzionalità durante la compilazione AOT.

Avviso di analisi del taglio IL2026: LogProviders.Log4NetLogProvider.Log4NetLogger.GetCreateLoggingEvent(ParameterExpression,UnaryExpression,ParameterExpression,UnaryExpression,Type): L'uso del membro 'System.Linq.Expressions.Expression.Property(Expression,String)' che ha 'RequiresUnreferencedCodeAttribute' può interrompere la funzionalità durante il taglio del codice dell'applicazione.

Avviso di analisi del taglio IL2057: LogProviders.LogProviderBase.FindType(String,String[]): Valore non riconosciuto passato al parametro 'typeName' del metodo 'System.Type.GetType(String)'.

Avviso di analisi del taglio IL2070: LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String): L'argomento 'this' non soddisfa 'DynamicallyAccessedMemberTypes.PublicProperties' nella chiamata a 'System.Type.GetProperty(String)'. Il parametro 'loggerType' del metodo 'LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String)' non dispone di annotazioni corrispondenti. Il valore di origine deve dichiarare almeno gli stessi requisiti dichiarati nella posizione di destinazione a cui è assegnato.

Avviso di analisi del taglio IL2075: LogProviders.Log4NetLogProvider.GetOpenNdcMethod(): L'argomento 'this' non soddisfa 'DynamicallyAccessedMemberTypes.PublicProperties' nella chiamata a 'System.Type.GetProperty(String)'. Il valore restituito del metodo 'LogProviders.LogProviderBase.FindType(String,String)' non dispone di annotazioni corrispondenti. Il valore di origine deve dichiarare almeno gli stessi requisiti dichiarati nella posizione di destinazione a cui è assegnato.

Abbiamo trovato la causa principale. Prima di risolvere i problemi, controlliamo le opzioni alternative per trovare problemi di compatibilità AOT.

Utilizzare analizzatori compatibili con AOT

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

Per utilizzare l'analisi statica, aggiungi la seguente proprietà al file con estensione csproj:

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

È necessario scegliere come target .NET 7 o .NET Framework 8 per usare tale analisi in un progetto .NET Standard 2,0. La configurazione del progetto potrebbe assomigliare a questa:

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

Abbiamo applicato la configurazione di cui sopra al nostro grande progetto principale e abbiamo riscontrato 1036 errori di compilazione.

Molti errori di costruzione

Si riferivano tutti alla compatibilità AOT? Avevamo ricostruito il progetto senza la proprietà IsAotCompatible. Tuttavia, 1036 errori.

Tutti gli errori sono correlati ai miglioramenti di .NET 8 rispetto a .NET Standard 2,0. Nel nostro caso, la maggior parte dei problemi riguardava il migliore supporto per i tipi di riferimento nullable. C'erano anche molti errori CA1500 e CA1512 sui 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 rilevati durante il processo di pubblicazione 🙁

Per confermare, abbiamo aggiunto il seguente codice di test alla nostra codebase:

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 ritaglio. E l'analisi statica ha identificato i seguenti errori:

errore IL3050: L'uso del membro 'System.Type.MakeGenericType(params Type[])' che ha 'RequiresDynamicCodeAttribute' può interrompere la funzionalità durante la compilazione AOT. Il codice nativo per questa creazione di istanze potrebbe non essere disponibile in fase di esecuzione.

errore IL2057: Valore non riconosciuto passato al parametro 'typeName' del metodo 'System.Type.GetType(String)'. Non è possibile garantire la disponibilità del tipo di destinazione.

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

Rileva gli errori di runtime

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

Se un'applicazione non presenta avvisi durante la pubblicazione per AOT, si comporterà allo stesso modo dopo AOT come senza AOT.

In pratica, la versione dopo l'AOT può ancora funzionare in modo diverso. Avevamo corretto tutti gli avvisi di AOT e assetto. Tutti i test automatici sono stati superati. L'applicazione di prova ha elaborato correttamente i documenti PDF. E la versione AOT della stessa applicazione produceva documenti errati.

L'applicazione AOT continuava a non funzionare

La pubblicazione AOT ha rimosso erroneamente parte del codice richiesto. Abbiamo creato il problema L'ottimizzazione rimuove il codice necessario. Il team .NET lo ha rapidamente confermato e risolto. E abbiamo aggiunto una soluzione alternativa al nostro codice C# finché la correzione non sarà disponibile.

La morale di questa storia è che la pubblicazione e gli avvertimenti sull’analisi statica potrebbero non essere sufficienti. Utilizza le funzionalità chiave del tuo progetto nell'applicazione di prova. Ed esegui l'applicazione di test dopo la distribuzione.

L'applicazione di test Docotic.Pdf utilizza la compressione PDF, l'estrazione del testo e altre funzionalità chiave. Lo usiamo in questo modo:

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

Il test manuale dell'applicazione pubblicata non è conveniente. 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.

Esistono due modi per automatizzare il test delle applicazioni Native AOT.

Posizionare i test all'esterno dell'applicazione AOT

In questo caso utilizzi un normale progetto NUnit/xUnit/MSTest. Prima di creare questo progetto, pubblichi l'applicazione di test AOT. I test eseguono l'app pubblicata utilizzando il metodo Process.Start.

Il vantaggio principale è la separazione dell'infrastruttura di test dall'app AOT. È possibile utilizzare tutte le funzionalità dei framework di unit test. Eventuali problemi di compatibilità AOT nel framework di test non hanno importanza. È anche più semplice riutilizzare i casi di test esistenti.

La configurazione di esempio del progetto di test è disponibile su GitHub. Il progetto Docotic.Tests contiene test automatici sia per la versione gestita che per quella AOT. AotCompatibility è un'applicazione di prova per la pubblicazione AOT.

AotCompatibility.csproj pubblica l'applicazione di test in un evento post-compilazione nella configurazione Release. La condizione '$(PublishProtocol)'=='' disabilita questo passaggio quando si utilizza la procedura guidata "Pubblica" 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"

Tieni presente che qui utilizziamo l'argomento --no-build. Il progetto è già creato prima dell'evento di post-compilazione. Non abbiamo bisogno di ricostruirlo di nuovo.

I test in NativeAot.cs eseguono il file AotCompatibility.exe pubblicato con argomenti diversi. I progetti Docotic.Tests e AotCompatibility condividono lo stesso codice testato. Ciò ci consente di controllare le versioni gestite e AOT senza duplicare il codice.

Posiziona i test all'interno dell'applicazione AOT

Un approccio alternativo consiste nel scrivere test all'interno dell'applicazione di test AOT. A luglio 2024 potrai utilizzare solo l'anteprima anticipata di MS Test. Leggi l'articolo Test delle tue applicazioni Native AOT per maggiori dettagli.

Il problema principale è che qualsiasi problema AOT nel framework di test renderà i tuoi test inaffidabili. Ed è disponibile solo un insieme limitato di funzionalità del framework di test. Ecco perché preferiamo mettere l'infrastruttura di test al di fuori dell'applicazione AOT.

Risolti gli avvisi di AOT e assetto

Evitare di sopprimere gli avvisi di pubblicazione. Di solito, sopprimere è una soluzione errata. Quando sopprimi l'AOT o gli avvisi di taglio, dici che il tuo progetto è compatibile con AOT. Tuttavia, la causa principale dell’avviso è ancora lì. Qualsiasi avviso soppresso potrebbe 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 potrebbe essere d'aiuto.

Rivediamo come abbiamo corretto gli avvisi AOT e trim in Docotic.Pdf. Ne hai già visti alcuni nella sezione Ricevi avvisi di pubblicazione.

Elimina gli avvisi da LibLog

Abbiamo utilizzato la libreria LibLog per la registrazione. LibLog si basa sulla riflessione per rilevare e utilizzare i framework di registrazione più diffusi. È incompatibile con AOT in base alla progettazione.

Attualmente, il pacchetto Microsoft.Extensions.Logging è lo standard per l'accesso a .NET. E lo sviluppo di LibLog è ora congelato.

Detto questo, abbiamo completamente rimosso LibLog dal nostro codice. Invece, abbiamo rilasciato il componente aggiuntivo Docotic.Pdf.Logging. Dipende dalle interfacce Microsoft.Extensions.Logging.Abstractions. Ciò ha ridotto le dimensioni della libreria principale e corretto tutti gli avvisi di ritaglio correlati.

Nessun avviso di assetto

Un codice di debug nel componente aggiuntivo Docotic.Layout

Nel componente aggiuntivo Docotic.Layout, utilizziamo la riflessione per il debug interno. Ciò ha portato all'avviso IL2075.

Il codice corrispondente esiste nella codebase, ma i client non possono utilizzarlo tramite l'API pubblica. La soluzione è escludere il codice dalla configurazione Release. Ora è utilizzato solo nella configurazione Debug.

Avviso AOT IL3050 da BouncyCastle

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

Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): L'uso del membro 'System.Enum.GetValues(Type)' che ha 'RequiresDynamicCodeAttribute' può interrompere la funzionalità durante la compilazione AOT. Potrebbe non essere possibile creare un array del tipo enum in fase di esecuzione. Utilizzare invece l'overload GetValues<TEnum> o il metodo GetValuesAsUnderlyingType.

BouncyCastle utilizza il metodo Enum.GetValues(Type), che non è compatibile con AOT.

La soluzione più semplice sarebbe invece utilizzare l'overload Enum.GetValues<T>(). L'ultima versione di BouncyCastle lo utilizza già. Sfortunatamente, questo sovraccarico è disponibile solo in .NET 5 o versioni successive. Questa non è un'opzione per .NET Standard 2.0.

Abbiamo scavato più a fondo e analizzato il codice BouncyCastle. Si è scoperto che BouncyCastle lo usa per prevenire l'offuscamento delle costanti enum. Abbiamo invece disabilitato l'offuscamento per le enumerazioni corrispondenti utilizzando l'attributo [System.Reflection.Obfuscation(Exclude = true)]. E sono stati rimossi gli utilizzi non più necessari di Enum.GetValues(Type).

Un problema strano nella build offuscata

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

<sconosciuto>:0: errore: il simbolo '__GenericLookupFromType_BitMiracle_Docotic_Pdf___4<System___Canon__System___Canon__System___Canon__Int32>_TypeHandle___System___Canon' è già definito

A tali errori non erano associati codici IL30## o IL2###. Non era chiaro come identificare il codice associato.

Si sono verificati errori solo nella versione offuscata. Potrebbe essere un bug nell'offuscatore. Avevamo aggiornato l'offuscatore, ma gli errori erano ancora presenti.

Bisognava restringere la portata del problema. Abbiamo utilizzato il seguente processo, basato su una ricerca binaria:

  1. Disabilitare l'offuscamento per metà degli spazi dei nomi. Controllare se si verificano errori. Continuare fino a trovare gli spazi dei nomi che causano gli errori.
  2. Disabilitare l'offuscamento per metà dei tipi nello spazio dei nomi dal primo passaggio. Controllare se si verificano errori. Continuare fino a trovare i tipi che portano agli errori.
  3. Disabilitare l'offuscamento per metà dei membri nel tipo del passaggio due. Controllare se si verificano errori. Continuare fino a trovare i membri che causano gli errori.
  4. Rivedi il codice del membro dal passaggio tre. Commenta le parti insolite e controlla se si verificano errori.

Siamo finiti al primo passaggio 🙂 Dopo aver disabilitato l'offuscamento per [tutti] gli spazi dei nomi, si verificavano ancora errori.

Abbiamo analizzato l'assembly generato con ILSpy. Sono stati trovati alcuni tipi imprevisti generati dal compilatore. L'ILSpy ha mostrato gli utilizzi di questi tipi da tale codice C#:

interface X
{
    void f();
}

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

Questo strano codice deriva dalla migrazione del codice C vecchio stile in C#. La condizione if (obj.f != null) è completamente ridondante. Abbiamo rimosso tali condizioni e gli errori erano scomparsi.

Non utilizzare GetCallingAssembly

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

Assembly.GetCallingAssembly() genera System.PlatformNotSupportedException: l'operazione non è supportata su questa piattaforma.

Il metodo Assembly.GetCallingAssembly dovrebbe essere implementato per Native AOT in .NET 9. Fino a quando la correzione non sarà pronta, abbiamo effettuato il refactoring del nostro codice per ridurre al minimo l'impatto negativo. Ora utilizziamo gli attributi dell'assembly chiamante solo per convalidare la licenza Application. 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 taglio nella libreria principale Docotic.Pdf e nella maggior parte dei componenti aggiuntivi. Tuttavia, potresti non essere in grado di riscrivere tutto il codice AOT incompatibile.

.NET fornisce attributi speciali per tali situazioni. Contrassegni l'API con attributi per informare i clienti sui 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

È possibile trovare ulteriori informazioni su questi attributi negli articoli ufficiali Introduzione agli avvisi AOT e Preparare le librerie .NET per il taglio.

Conclusione

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

Pubblicare un'applicazione .NET 8 di prova per individuare problemi di compatibilità AOT. Esistono anche gli analizzatori Roslyn, ma rilevano meno problemi.

La libreria principale Docotic.Pdf è ora compatibile con AOT e ritaglio. Sono compatibili anche i seguenti componenti aggiuntivi:

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

Il componente aggiuntivo da HTML a PDF contiene ancora avvisi di ritaglio. Questo è il nostro prossimo obiettivo sulla tabella di marcia Native AOT.

Sentiti libero di farci domande sull'Native AOT o sull'elaborazione PDF.