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 sono generalmente più veloci e consumano meno memoria rispetto alle soluzioni gestite.
La pubblicazione AOT .NET produce applicazioni ridotte e autonome. Tali applicazioni:
- Può essere eseguito su un computer su cui non è installato il runtime .NET.
- Scegli come target un ambiente runtime specifico, come Windows x64.
- Non contenere codice inutilizzato.
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:
- Crea un progetto console .NET 8 con
PublishAot = true
- 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.
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.
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=""$(SolutionDir)Scripts\publish.bat"" />
</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.
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:
- 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.
- 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.
- 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.
- 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.