In Come trasformare C# in una DLL nativa con Native AOT - Chiamarla da C/C++ via UnmanagedCallersOnly, abbiamo già coperto l’uso di Native AOT per chiamare C# da C/C++. Onestamente, però, sarebbe stato più gentile mettere prima cosa sia effettivamente Native AOT. L’ordine si è invertito un po’.
Le discussioni su Native AOT sono famose perché i termini si confondono subito all’inizio.
- Si tratta di sbarazzarsi del JIT?
- Come differisce da self-contained e single-file?
- È nella stessa famiglia di ReadyToRun?
- Cosa sta succedendo quando appare un fiume di avvisi di trimming?
- WPF / WinForms / ASP.NET Core possono usarlo tutti con lo stesso comfort?
Quando queste cose si fondono, Native AOT inizia a sembrare o “magia che rende tutto più veloce” o, al contrario, “una cosa spaventosa piena di limitazioni”. Entrambe le visioni sono un po’ approssimative.
In questo articolo, assumendo il panorama pratico attuale di .NET 8 e successivi, chiariamo queste quattro cose per prime.
- Cos’è davvero Native AOT
- Cosa si guadagna e dove diventa duro
- Come differisce da ReadyToRun e trimming
- Quali tipi di app sono un primo tentativo gentile
1. La conclusione prima di tutto (in una riga)
- Native AOT è un modello di pubblicazione che compila ahead-of-time un’app .NET in codice nativo al momento della publish e spedisce quello.
- Poiché non usa un runtime JIT, i tempi di avvio e l’impronta di memoria tendono a migliorare, ed è facile distribuirlo su macchine senza .NET runtime installato.
- In cambio, va d’accordo male con reflection senza restrizioni, generazione di codice dinamica, COM built-in e librerie che non supportano trimming.
- In altre parole, piuttosto che magia velocistica, è un modello di pubblicazione che scambia un po’ di mondo dinamico e si inclina verso quello statico, a favore di avvio, distribuzione e vincoli di ambiente di esecuzione.
Native AOT è “un meccanismo per spedire .NET come un’app nativa” — non una semplice checkbox per compilazione più veloce.
2. Le tabelle da guardare per prime
2.1. Il vocabolario intorno a Native AOT
Separare questi termini fin dall’inizio rende tutto ciò che segue più facile.
| Termine | Cosa fa | Relazione con Native AOT |
|---|---|---|
| JIT | Genera codice nativo dall’IL a runtime | Native AOT fa questo lavoro ahead of time |
| self-contained | Spedisce i bit .NET necessari insieme all’app | Native AOT appartiene a questa famiglia di pensiero |
| single-file | Raggruppa la distribuzione in un file | Distinto dall’essenza di Native AOT, ma il risultato finale spesso appare simile |
| trimming | Rimuove codice non usato | Praticamente un prerequisito sotto Native AOT |
| ReadyToRun | Mantiene l’IL ma precarica parte del lavoro del JIT | Suona simile ma è genuinamente diverso |
| source generator | Sposta comportamento dinamico runtime in generazione di codice a build-time | Si abbina bene a Native AOT |
La parte confusa è che Native AOT non è una singola feature, ma più un modello di pubblicazione che lavora di pari passo con self-contained, trimming, source generation e pubblicazione pinnata a RID.
2.2. JIT vs. ReadyToRun vs. Native AOT
Anche questo è più veloce da assorbire come una tabella.
| Aspetto | Esecuzione JIT ordinaria | ReadyToRun | Native AOT |
|---|---|---|---|
| Runtime JIT | Usato | Ancora usato in alcuni casi | Non usato |
| Contenuti distribuzione | Per lo più IL | IL + codice pre-generato | Per lo più un eseguibile nativo |
| Avvio | Baseline | Facile migliorare | Molto facile migliorare |
| Compatibilità | La più ampia | Ampia | Fortemente vincolata |
| Funzionalità dinamiche | Facili da usare | Per lo più facili da usare | Fortemente limitate |
| Meglio adatto a | Sviluppo .NET generale | Un primo passo verso miglioramento avvio | Perseguire aggressivamente avvio, distribuzione e ambienti ristretti |
Se ReadyToRun è la direzione di “rendere la vita del JIT un po’ più facile”, Native AOT è la direzione di “non assumere affatto un runtime JIT a runtime”. Anche se compaiono le stesse lettere A-O-T, la temperatura è piuttosto diversa.
3. Il quadro generale di Native AOT (diagramma)
Grossolanamente abbozzato, Native AOT appare così.
flowchart LR
Src["C# / .NET source code"] --> IL["IL assemblies"]
IL -->|Esecuzione normale| JIT["Runtime JIT"]
JIT --> Run1[App gira]
IL -->|dotnet publish + PublishAot| Analyze["AOT / trim analysis"]
Analyze --> Trim["Rimozione codice non usato"]
Trim --> AOT["Generazione codice nativo"]
AOT --> Run2[Eseguibile RID-specific]
.NET ordinario produce prima IL, poi JIT-compila solo ciò che serve a runtime. Native AOT precarica una buona parte di quello stage successivo al momento della publish.
Il punto cruciale è che al momento della publish, la toolchain “ha bisogno di sapere essenzialmente tutto il codice che servirà a runtime”. È qui che cambia l’atmosfera.
- Scoprire tipi a runtime
- Far crescere codice a runtime
- Caricare assembly a runtime
- Differire la risoluzione con un “tanto si sistema a runtime”
Codice scritto in questi stili improvvisamente smette di andare d’accordo con Native AOT.
4. Cosa ti dà Native AOT
4.1. L’avvio tende ad alleggerirsi
Il payoff più visibile di Native AOT è, non sorprendentemente, l’avvio.
- CLI tools
- Processi short-lived
- Avvio in stile serverless
- Avvio e rollover container
- Tool di monitoraggio e piccoli processi residenti
In questi scenari il costo JIT è facile da vedere, e poiché Native AOT lo precarica, i primi momenti diventano più leggeri.
L’impronta di memoria tende anche a migliorare, il che aiuta quando vuoi stipare più istanze sullo stesso hardware. Specialmente sul lato cloud, dove molte copie dello stesso processo si avviano, questa differenza si accumola costantemente.
4.2. Non devi assumere un runtime pre-installato
Un’app pubblicata con Native AOT è facile da far girare su macchine senza .NET runtime installato.
Questo è silenziosamente significativo.
- Non vuoi dire ai target di deployment “per favore installa prima .NET 9 Runtime”
- Vuoi immagini container più snelle
- Vuoi lasciare cadere un singolo piccolo tool da qualche parte e farlo girare
- Non vuoi — o non puoi — permettere JIT nell’ambiente di esecuzione
In situazioni come queste, semplicemente rimuovere il prerequisito “il runtime deve essere fornito separatamente” rende tutto molto più tranquillo.
Nota che “nessun runtime necessario” qui significa che il target di deployment non ha bisogno di un install .NET separato. Non significa che le parti equivalenti al runtime dentro l’app svaniscano completamente.
4.3. Si adatta bene a ambienti di esecuzione ristretti
Poiché Native AOT non usa un runtime JIT, è più facile eseguirlo in ambienti in cui JIT non è permesso.
Questo beneficio atterra più sul lato cloud, container e mobile che sul desktop. Tuttavia, anche in un contesto di sviluppo Windows, resta un vantaggio genuino nel senso di “ridurre prerequisiti extra nel target di deployment”.
5. Dove Native AOT diventa duro
5.1. Reflection e generazione di codice dinamica
Questo è il cuore dei vincoli di Native AOT.
- Caricamento dinamico come
Assembly.LoadFile - Generazione di codice runtime come
System.Reflection.Emit - Reflection che percorre tipi senza restrizioni a runtime
- Codice che compone generici liberamente a runtime
Questi rendono difficile fissare il codice necessario al momento della publish, quindi diventano terreni fertile per avvisi AOT.
Certo, non è semplice come “una riga di reflection e sei fuori”. Ma la tendenza regge senza eccezioni: più il design si inclina verso “guarda a runtime e decidi”, più le cose diventano dure.
I nomi di avviso che vedrai più spesso sono della famiglia RequiresDynamicCode.
Significano “questa chiamata può rompersi sotto AOT”, quindi è più sicuro non sopprimerli casualmente.
Quando fai Native AOT, un modello mentale utile è: riduci “l’astuzia a runtime” e aumenta “l’esplicitatezza a build-time”.
5.2. Devi pensare per primo al trimming
Native AOT è profondamente intrecciato con il trimming. La cosa facile da perdere è che non solo il tuo codice, ma anche il modo in cui sono scritte le tue librerie di dipendenze conta.
I soliti sospetti sono:
- Serializer basati su reflection
- Setup DI / plugin che raccolgono tipi via scansione runtime
- Meccanismi che cercano tipi per nome stringa e li istanziano
- Librerie che si appoggiano a proxy dinamici o generazione IL
Se avvisi appaiono qui e ti limiti a scrollarti le spalle con “la publish è riuscita, quindi siamo a posto”, il conto arriva in seguito ed è salato. Sotto Native AOT, gli avvisi meritano davvero di essere letti seriamente.
5.3. Pubblicazione per piattaforma
Native AOT pubblica pinnati a un RID (Runtime Identifier).
Cioè, questo non è un mondo in cui qualcosa costruito per win-x64 gira così com’è su linux-x64.
- Windows x64
- Windows Arm64
- Linux x64
- Linux Arm64
- macOS Arm64
Produci un output di publish per ogni target, così.
Questo si sente molto più “app nativa” del .NET framework-dependent ordinario.
5.4. I contesti Windows Desktop / COM richiedono cautela reale
Nella linea di lavoro di KomuraSoft, questa parte conta particolarmente.
Su Windows, Native AOT non ha COM built-in. Inoltre, WPF va d’accordo male con trimming, e WinForms dipende pesantemente da COM marshalling built-in — quindi almeno per ora, nessuno dei due dovrebbe essere in cima alla tua lista dei “primi candidati Native AOT”.
In breve, queste sono cose che tendono a far diventare la stanza silenziosa:
- Convertire direttamente il corpo di un’app WPF / WinForms esistente in Native AOT
- Portare l’interop COM con i tuoi soliti istinti intatti
Viceversa, questi sono target molto più naturali:
- App console
- Worker
- Piccole web API
- Componenti di interop nativa che possono essere appiattiti su un confine di funzioni C
Se serve COM, ci sono casi in cui restare sul JIT, o riprogettare attorno a ComWrappers / source-generated COM, è la strada più saggia.
6. Passi minimi
6.1. Il csproj
Per prima cosa, aggiungi PublishAot al file di progetto.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
net8.0 va bene per gli esempi. Il ragionamento è essenzialmente lo stesso su .NET 9 / 10.
Quel che conta non è aggiungerlo temporaneamente sulla riga di comando dotnet publish, ma tenerlo nel progetto sempre, così vedi l’analisi di build / publish quotidianamente.
Nota che aggiungere <PublishAot>true</PublishAot> non rende all’improvviso le tue esecuzioni locali quotidiane Native AOT.
dotnet run giornaliero e l’esecuzione normale restano sul JIT; la vera compilazione Native AOT avviene al momento della publish.
6.2. Publishing
Per Windows x64, ad esempio:
dotnet publish -c Release -r win-x64
Per Linux x64:
dotnet publish -c Release -r linux-x64
L’output è pinnato a un RID. La mentalità passa da “una DLL .NET che gira ovunque” a “un eseguibile costruito per quel OS / quella architettura”.
Se lo approcci dal lato web API, partire dal template Native AOT è il modo più facile.
dotnet new webapiaot -o MyFirstAotWebApi
Per un worker, questo:
dotnet new worker -o WorkerWithAot --aot
6.3. Come scrivere JSON
Un punto di collisione silenziosamente frequente sotto Native AOT è JSON.
Usato con i tuoi soliti istinti, System.Text.Json tende alla reflection, quindi è più tranquillo appoggiarsi alla source generation.
using System.Text.Json;
using System.Text.Json.Serialization;
[JsonSerializable(typeof(AppConfig))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
public sealed class AppConfig
{
public string? Name { get; init; }
public int RetryCount { get; init; }
}
var config = new AppConfig
{
Name = "sample",
RetryCount = 3
};
string json = JsonSerializer.Serialize(config, AppJsonContext.Default.AppConfig);
In pratica, la regola pratica che raramente sbaglia è meno “rendilo compatibile con Native AOT” e più “non far andare il runtime a caccia di tipi”.
7. Casi in cui si adatta
Native AOT tende a collimare piacevolmente in casi come questi.
- CLI tools in cui l’avvio è il protagonista
- Piccole API distribuite in scala in container
- Worker / servizi in background
- Processi serverless e short-lived
- Piccoli componenti .NET inseriti in app native
- Situazioni in cui non vuoi richiedere un .NET runtime pre-installato
Quel che condividono è che i confini sono relativamente chiari e il machinery dinamico è facile da ridurre.
8. Casi in cui non si adatta
Viceversa, ci sono casi chiari in cui Native AOT non dovrebbe essere il tuo campo di battaglia principale fin dall’inizio.
- Il corpo di un’app WPF / WinForms esistente e grande
- Architetture basate su COM interop built-in
- App in cui il caricamento di plugin a runtime è il centro
- Forte dipendenza da framework che scoprono tipi via reflection
- Librerie che usano
System.Reflection.Emito proxy dinamici di routine - Design con C++/CLI in mezzo
Per questi, .NET JIT ordinario, o ReadyToRun, o ripensare i confini del design sono strade più sagge.
9. Pitfall
Infine, le cose facili da calcare nel primo giro con Native AOT.
- Prendere leggermente gli avvisi di publish
- Come detto sopra, gli avvisi Native AOT meritano di essere letti seriamente.
- Build passa ma publish si rompe
- Al momento della publish, l’analisi gira davvero anche sulle tue dipendenze, e alcune cose diventano visibili solo lì.
- Trattare ReadyToRun e Native AOT con la stessa mentalità
- Le parole sono simili; la forza dei vincoli è abbastanza diversa.
- Partire direttamente dal corpo di un’app desktop
- Console / worker / piccola API prima è molto più tranquillo.
- Scrivere JSON o binding configurazione con i soliti abitudini
- Codice basato su reflection torna indietro a mordere in seguito.
- Distribuire come se fosse indipendente dalla piattaforma
- Gli output Native AOT sono pinnati a un RID.
- Credere “Native AOT = tutto diventa più veloce”
- I protagonisti sono avvio, distribuzione e ambiente di esecuzione. Perdi questo, e le aspettative deragliano.
Sotto Native AOT, dotnet publish agisce più come arbitro di dotnet build.
Inizia a eseguirlo presto, e la parte finale diventa molto meno dolorosa.
10. Riassunto
In una frase, Native AOT è un meccanismo che sposta un’app .NET da un modello di esecuzione dinamica verso un modello di distribuzione che può essere fissato staticamente.
I punti da tenere a mente si riducono a questi cinque.
- Native AOT compila ahead-of-time in codice nativo al momento della publish
- Funziona molto bene per avvio, memoria e distribuzione
- In cambio, è duro con reflection, generazione di codice dinamica, COM built-in e codice incompatibile col trimming
- Come primo target, app console / worker / piccole API sono più tranquille del corpo di un’app desktop
- Verifica presto e spesso tramite publish, risolvendo gli avvisi man mano
Native AOT non è un interruttore standard da accendere su ogni app .NET. Ma dove l’avvio conta, vuoi una distribuzione più leggera e vuoi meno prerequisiti di ambiente di esecuzione, è un’arma genuinamente potente.
Viceversa, nel mondo denso di WPF / WinForms / COM, .NET JIT ordinario è spesso ancora la scelta più saggia. Una volta che sai distinguerli, Native AOT smette di essere “una nuova feature difficile” e diventa un’opzione con un posto chiaramente definito.
11. Riferimenti
- Native AOT deployment overview - .NET
- Native AOT deployment overview - .NET (Japanese)
- Introduction to AOT warnings - .NET
- Prepare .NET libraries for trimming - .NET
- Known trimming incompatibilities - .NET
- How to use source generation in System.Text.Json - .NET
- ASP.NET Core support for Native AOT
- ReadyToRun deployment overview - .NET
- Building native libraries - .NET
- ComWrappers source generation - .NET
- Articolo correlato: Come trasformare C# in una DLL nativa con Native AOT - Chiamarla da C/C++ via UnmanagedCallersOnly
- Articolo correlato: Perché un wrapper C++/CLI è una scelta forte per usare DLL native da C# - Confronto con P/Invoke
Articoli correlati
Articoli recenti con gli stessi tag per approfondire argomenti vicini.
Chiamando un C# Native AOT DLL da C / C++
Esporta i punti di ingresso C da C# con Native AOT e UnmanagedCallersOnly e chiamali da C / C++: impostazione del progetto, gestione dell...
Icone nella system tray e notifiche toast nelle app Windows — le insidie di NotifyIcon e come scegliere l'AppNotification giusta
Una guida pratica per mantenere un'applicazione Windows aziendale residente nella system tray (area di notifica) e avvisare l'utente tram...
Fino a quando funzioneranno le applicazioni VB6? — Lo stato del supporto al runtime e un percorso pratico verso la migrazione a .NET
Fino a quando continueranno a funzionare le applicazioni VB6? Questo articolo chiarisce l'asimmetria tra la politica di supporto del runt...
Era giapponese, festività e date di chiusura nelle applicazioni aziendali — progettazione resiliente ai cambi di era, JapaneseCalendar e calcolo dei giorni lavorativi nella pratica
Mostrare «Reiwa 8» su un documento, calcolare i giorni lavorativi escludendo le festività, pagare entro la fine del mese successivo dopo ...
Capire davvero il modello OSI — sezionare una singola richiesta HTTP nei suoi sette livelli
Capiamo il modello OSI attraverso la pratica reale invece che a memoria. Assembliamo e sezioniamo in C# il frame Ethernet che trasporta u...
Argomenti correlati
Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.
Argomenti tecnici Windows
Portale su sviluppo Windows, analisi dei problemi e valorizzazione delle risorse esistenti.
Interoperabilità a 32 / 64 bit
Compatibilità 32/64 bit, confini nativi e decisioni di progettazione Windows.
Servizi collegati all’argomento
L’articolo è direttamente collegato ai servizi seguenti.
Sviluppo di applicazioni Windows
La scelta di Native AOT influenza distribuzione, bitness, COM e trimming, quindi si adatta bene al supporto di design di app Windows.
Consulenza tecnica e revisione del progetto
Selezionare AOT / JIT / ReadyToRun e pianificare la compatibilità delle dipendenze sono una naturale consulenza tecnica e design review.
Domande frequenti
Domande che ricorrono nelle consulenze sull’argomento dell’articolo.
- Cos'è .NET Native AOT e come differisce dall'esecuzione .NET normale?
- Native AOT è un modello di pubblicazione che compila ahead-of-time un'app .NET in codice nativo al momento della publish e spedisce quello, invece di JIT-compilare l'IL a runtime. Poiché non c'è runtime JIT, i tempi di avvio e l'impronta di memoria tendono a migliorare, e l'app gira su macchine senza .NET runtime installato. In cambio, va d'accordo male con reflection senza restrizioni, generazione di codice dinamica, COM built-in e librerie che non supportano trimming — scambia parte del mondo dinamico per benefici di avvio, distribuzione e ambiente di esecuzione.
- Qual è la differenza tra Native AOT e ReadyToRun?
- ReadyToRun mantiene l'IL e precarica parte del lavoro del JIT, usando ancora il runtime JIT in alcuni casi, quindi mantiene ampia compatibilità ed è un primo passo gentile verso il miglioramento dell'avvio. Native AOT non usa affatto un runtime JIT: la distribuzione è per lo più un eseguibile nativo, l'avvio migliora di più, ma le funzionalità dinamiche sono fortemente limitate e l'output è pinnato a un runtime identifier come win-x64 o linux-x64. I nomi suonano simili, ma la forza dei vincoli è abbastanza diversa.
- Posso usare Native AOT con WPF, WinForms o COM interop?
- Non comodamente, almeno per ora. Su Windows, Native AOT non ha COM built-in, WPF va d'accordo male con trimming, e WinForms dipende pesantemente da COM marshalling built-in, quindi convertire il corpo di un'app desktop esistente non è un buon primo candidato. App console, worker, piccole web API e componenti di interop nativa appiattiti su un confine di funzioni C sono target molto più naturali. Se serve COM, restare sul JIT o riprogettare attorno a ComWrappers e source-generated COM può essere la strada più saggia.
- Posso ignorare gli avvisi AOT e trimming se la publish ha successo?
- No — sotto Native AOT, gli avvisi di publish meritano di essere letti seriamente. Gli avvisi della famiglia RequiresDynamicCode significano che una chiamata può rompersi sotto AOT, e l'analisi copre anche le tue librerie dipendenti, quindi serializer JSON basati su reflection, scansione runtime di tipi e proxy dinamici nelle dipendenze sono colpevoli comuni. Per JSON, usa la source generation di System.Text.Json. La regola pratica è ridurre l'astuzia a runtime e aumentare l'esplicitatezza a build-time, eseguendo dotnet publish presto e spesso.
Profilo dell’autore
Pagina di presentazione dell’autore dell’articolo.
Go Komura
Rappresentante di KomuraSoft LLC
Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.