Diese Seite kann automatisch übersetzten Text enthalten.

Wie man Native-AOT-Anwendungen in .NET entwickelt

.NET 8 bringt umfassende Unterstützung für Ahead-of-Time-Kompilierung, auch bekannt als Native AOT. Solche Anwendungen starten in der Regel schneller und verbrauchen weniger Speicher als verwaltete Lösungen.

Die .NET-AOT-Veröffentlichung erzeugt getrimmte, eigenständige Anwendungen. Solche Anwendungen:

  1. Können auf einem Rechner ausgeführt werden, auf dem die .NET-Runtime nicht installiert ist.
  2. Zielen auf eine bestimmte Laufzeitumgebung ab, z. B. Windows x64.
  3. Enthalten keinen ungenutzten Code.

Bereitstellung von .NET-AOT-Anwendungen

Wir entwickeln die Docotic.Pdf-Bibliothek für die PDF-Verarbeitung. Die AOT-Kompatibilität ist einer der meistgefragten Punkte im Jahr 2024. Dieser Artikel beschreibt unseren Weg zur AOT-Kompatibilität. Sie erfahren, wie man AOT-Probleme in .NET-Projekten findet und behebt.

Grundlagen der .NET-AOT-Veröffentlichung

Um eine Native-AOT-Anwendung zu veröffentlichen, fügen Sie die folgende Eigenschaft zu Ihrer Projektdatei hinzu:

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

Möglicherweise müssen Sie auch einige Voraussetzungen installieren. Dann können Sie Ihre App aus Visual Studio veröffentlichen. Oder Sie veröffentlichen das Projekt über die Befehlszeile mit dem dotnet publish-Befehl:

dotnet publish -r linux-x64 -c Release

AOT-Kompatibilität für .NET-Bibliotheken

Die Native-AOT-Veröffentlichung ist in .NET 7 und .NET 8 verfügbar. In .NET 7 können Sie nur Konsolenanwendungen veröffentlichen. In .NET 8 können Sie zusätzlich ASP.NET-Core-Anwendungen veröffentlichen.

Aber die Docotic.Pdf-Kernbibliothek zielt auf .NET Standard 2.0 ab. Können wir sie AOT-freundlich machen? Natürlich.

Sie können AOT-Kompatibilitätsprobleme weiterhin in .NET-Bibliotheken finden und beheben, die auf .NET Standard, .NET 5 oder .NET 6 abzielen. Native-AOT-Anwendungen können dann auf die Bibliothek zurückgreifen.

AOT-Probleme finden

Die erste Supportanfrage zur AOT-Kompatibilität klingt ungefähr so:

Wir ziehen in Betracht, Docotic.Pdf für WebAssembly kompiliert zu verwenden. Beim Kompilieren nach AOT erzeugt es Trim-Warnungen:
warning IL2104: Assembly 'BitMiracle.Docotic.Pdf' produced trim warnings. For more information see https://aka.ms/dotnet-illink/libraries

Nicht sehr aufschlussreich. Lassen Sie uns das Problem reproduzieren und mehr Details zu diesen Trim-Warnungen erhalten.

Veröffentlichungswarnungen abrufen

Das Veröffentlichen einer Testanwendung ist der flexibelste Weg, .NET-AOT-Probleme zu finden. Sie müssen:

  1. Ein .NET-8-Konsolenprojekt mit PublishAot = true erstellen
  2. Einen Verweis auf Ihr Projekt hinzufügen und einige seiner Typen verwenden

Hier ist die csproj-Konfiguration, die wir für Docotic.Pdf verwenden:

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

TrimmerSingleWarn = false ermöglicht es, detaillierte Informationen zu den Warnungen zu erhalten. Andernfalls erhalten Sie nur die Meldung „Assembly 'X' produced trim warnings“.

Und hier ist der C#-Code in Program.cs:

using BitMiracle.Docotic.Pdf;

using var pdf = new PdfDocument();

Fügen Sie etwas Code hinzu, um das Laden der zu prüfenden Assembly zu erzwingen. Es ist nicht erforderlich, die gesamte Funktionalität Ihres Projekts abzudecken, um Veröffentlichungswarnungen zu erhalten. Die Verwendung eines einzelnen Typs reicht aus. Aber Sie müssen mehr Code schreiben, um Laufzeitfehler abzufangen.

Der Befehl dotnet publish -r win-x64 -c Release für dieses Projekt lieferte Trim- und AOT-Warnungen. Hier ist die Kurzfassung der Liste:

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.

Wir haben die Ursache gefunden. Bevor wir die Probleme beheben, sehen wir uns alternative Möglichkeiten an, AOT-Kompatibilitätsprobleme zu finden.

AOT-Kompatibilitäts-Analyzatoren verwenden

Microsoft stellt Roslyn-Analyzatoren bereit, um AOT-Probleme in .NET-7- oder .NET-8-Projekten zu finden. Sie können AOT-Probleme hervorheben, wenn Sie den Code schreiben. Das Veröffentlichen einer Testanwendung erkennt jedoch mehr Probleme.

Um statische Analyse zu verwenden, fügen Sie die folgende Eigenschaft zur .csproj-Datei hinzu:

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

Sie müssen das .NET-7- oder .NET-8-Framework anvisieren, um eine solche Analyse in einem .NET-Standard-2.0-Projekt zu verwenden. Die Projektkonfiguration könnte so aussehen:

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

Wir haben die obige Konfiguration auf unser großes Kernprojekt angewendet und 1036 Buildfehler erhalten.

Viele Buildfehler

Bezog sich das alles auf die AOT-Kompatibilität? Wir hatten das Projekt erneut ohne die Eigenschaft IsAotCompatible erstellt. Trotzdem 1036 Fehler.

Alle Fehler bezogen sich auf Verbesserungen in .NET 8 gegenüber .NET Standard 2.0. In unserem Fall betrafen die meisten Probleme die bessere Unterstützung für Nullable-Referenztypen. Es gab auch viele CA1500- und CA1512-Fehler zu den neuen Hilfsmethoden ArgumentOutOfRangeException.ThrowIfLessThan und ArgumentNullException.ThrowIfNull.

Wir haben alle Fehler behoben und die Eigenschaft IsAotCompatible aktiviert. Der Projektbuild wurde ohne AOT-Fehler abgeschlossen. Die statische Analyse hat keine der Warnungen erkannt, die während des Veröffentlichungsprozesses gefunden wurden 🙁

Zur Bestätigung haben wir den folgenden Testcode zu unserer Codebasis hinzugefügt:

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

Dieser C#-Code ist zu 100 % inkompatibel mit .NET AOT und Trimmen. Und die statische Analyse hat die folgenden Fehler identifiziert:

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.

Die Roslyn-Analyzatoren funktionieren also und können einige Probleme finden. In unserem Fall fanden sie jedoch nichts.

Laufzeitfehler abfangen

Der Artikel Wie man Bibliotheken mit Native AOT kompatibel macht nennt das folgende Prinzip:

Wenn eine Anwendung beim Veröffentlichen für AOT keine Warnungen hat, verhält sie sich nach AOT genauso wie ohne AOT.

In der Praxis kann die Version nach AOT dennoch anders funktionieren. Wir hatten alle AOT- und Trim-Warnungen behoben. Alle automatischen Tests bestanden. Die Testanwendung verarbeitete PDF-Dokumente korrekt. Und die AOT-Version derselben Anwendung erzeugte falsche Dokumente.

Die AOT-Anwendung funktionierte immer noch nicht

Die AOT-Veröffentlichung hatte versehentlich einigen erforderlichen Code entfernt. Wir haben das Issue Optimization removes necessary code erstellt. Das .NET-Team hat es schnell bestätigt und behoben. Und wir haben einen Workaround in unseren C#-Code aufgenommen, bis der Fix verfügbar ist.

Die Lehre aus dieser Geschichte ist, dass Veröffentlichungs- und statische Analysewarnungen möglicherweise nicht ausreichen. Verwenden Sie die zentralen Funktionen Ihres Projekts in der Testanwendung. Und führen Sie die Testanwendung nach der Bereitstellung aus.

Die Testanwendung von Docotic.Pdf verwendet PDF-Komprimierung, Textextraktion und andere Kernfunktionen. Wir verwenden sie so:

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

Das manuelle Testen der veröffentlichten Anwendung ist unpraktisch. Es lohnt sich, den Prozess zu automatisieren.

AOT-Probleme automatisch erkennen

Die Entwicklung von Docotic.Pdf basiert auf automatischen Tests. Für jede Funktion und jeden Fehler gibt es Tests. Die .NET-AOT-Kompatibilität ist keine Ausnahme.

Es gibt zwei Möglichkeiten, das Testen von Native-AOT-Anwendungen zu automatisieren.

Tests außerhalb der AOT-Anwendung platzieren

Hier verwenden Sie ein reguläres NUnit-/xUnit-/MSTest-Projekt. Vor dem Build dieses Projekts veröffentlichen Sie die AOT-Testanwendung. Die Tests führen die veröffentlichte App mit der Methode Process.Start aus.

Der Hauptvorteil ist die Trennung der Testinfrastruktur von der AOT-App. Sie können die vollen Möglichkeiten von Unit-Test-Frameworks nutzen. AOT-Kompatibilitätsprobleme im Testframework spielen keine Rolle. Außerdem lassen sich vorhandene Testfälle einfacher wiederverwenden.

Die Beispielkonfiguration des Testprojekts ist auf GitHub verfügbar. Das Projekt Docotic.Tests enthält automatische Tests für sowohl verwaltete als auch AOT-Versionen. AotCompatibility ist eine Testanwendung für die AOT-Veröffentlichung.

Die Datei AotCompatibility.csproj veröffentlicht die Testanwendung in einem Post-Build-Ereignis in der Konfiguration Release. Die Bedingung '$(PublishProtocol)'=='' deaktiviert diesen Schritt, wenn Sie den „Veröffentlichen“-Assistenten in Visual Studio verwenden.

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

Das Skript publish.bat ist einfach:

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

Beachten Sie, dass wir hier das Argument --no-build verwenden. Das Projekt ist vor dem Post-Build-Ereignis bereits erstellt worden. Wir müssen es nicht erneut bauen.

Die Tests in NativeAot.cs führen die veröffentlichte AotCompatibility.exe mit verschiedenen Argumenten aus. Die Projekte Docotic.Tests und AotCompatibility verwenden denselben getesteten Code. Dadurch können wir verwaltete und AOT-Versionen überprüfen, ohne Code zu duplizieren.

Tests innerhalb der AOT-Anwendung platzieren

Ein alternativer Ansatz besteht darin, Tests innerhalb der AOT-Testanwendung zu schreiben. Im Juli 2024 können Sie dafür nur die frühe Vorschau von MS Test verwenden. Lesen Sie den Artikel Testing your native AOT Applications für weitere Details.

Das Kernproblem ist, dass jedes AOT-Problem im Testframework Ihre Tests unzuverlässig macht. Außerdem ist nur ein begrenzter Satz an Testframework-Funktionen verfügbar. Deshalb bevorzugen wir es, die Testinfrastruktur außerhalb der AOT-Anwendung zu platzieren.

AOT- und Trim-Warnungen beheben

Vermeiden Sie das Unterdrücken von Veröffentlichungswarnungen. In der Regel ist das Unterdrücken die falsche Lösung. Wenn Sie AOT- oder Trim-Warnungen unterdrücken, sagen Sie damit, dass Ihr Projekt AOT-kompatibel ist. Die Ursache der Warnung ist jedoch weiterhin vorhanden. Jede unterdrückte Warnung kann schließlich zu einem Laufzeitfehler Ihrer Anwendung führen.

Versuchen Sie, den mit jeder Warnung verbundenen Code zu entfernen oder umzuschreiben. Auch das Aktualisieren von Komponenten von Drittanbietern kann helfen.

Sehen wir uns an, wie wir AOT- und Trim-Warnungen in Docotic.Pdf behoben haben. Einige davon haben Sie bereits im Abschnitt Veröffentlichungswarnungen abrufen gesehen.

Trim-Warnungen von LibLog

Wir verwendeten die LibLog-Bibliothek für Logging. LibLog nutzt Reflexion, um gängige Logging-Frameworks zu erkennen und zu verwenden. Sie ist von Haus aus nicht mit AOT kompatibel.

Derzeit ist das Paket Microsoft.Extensions.Logging der Standard für Logging in .NET. Und die Entwicklung von LibLog ist nun eingefroren.

Vor diesem Hintergrund haben wir LibLog vollständig aus unserem Code entfernt. Stattdessen haben wir das Add-on Docotic.Pdf.Logging veröffentlicht. Es hängt von den Schnittstellen Microsoft.Extensions.Logging.Abstractions ab. Das reduzierte die Größe der Kernbibliothek und beseitigte alle zugehörigen Trim-Warnungen.

Keine Trim-Warnungen

Ein Debug-Code im Docotic.Layout-Add-on

Im Layout-Add-on verwenden wir Reflexion für internes Debugging. Das führte zur Trim-Analysewarnung IL2075.

Der entsprechende Code ist in der Codebasis vorhanden, aber Kunden können ihn nicht über die öffentliche API verwenden. Die Lösung besteht darin, den Code aus der Konfiguration Release auszuschließen. Er wird jetzt nur noch in der Konfiguration Debug verwendet.

AOT-Warnung IL3050 von BouncyCastle

Die BouncyCastle-Bibliothek hilft uns, PDF-Dokumente zu signieren. Die folgende Warnung stammt aus ihrem Code:

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 verwendet die Methode Enum.GetValues(Type), die nicht AOT-kompatibel ist.

Die einfachste Lösung wäre, stattdessen die Überladung Enum.GetValues<T>() zu verwenden. Die neueste BouncyCastle-Version verwendet sie bereits. Leider ist diese Überladung erst in .NET 5 oder neuer verfügbar. Das ist für .NET Standard 2.0 keine Option.

Wir sind tiefer eingestiegen und haben den BouncyCastle-Code analysiert. Dabei stellte sich heraus, dass BouncyCastle sie verwendet, um die Verschleierung von Enum-Konstanten zu verhindern. Stattdessen haben wir die Verschleierung für die entsprechenden Enums mit dem Attribut [System.Reflection.Obfuscation(Exclude = true)] deaktiviert. Und nicht mehr benötigte Verwendungen von Enum.GetValues(Type) entfernt.

Ein seltsames Problem im verschleierten Build

Wir verwenden Verschleierung, um Produktiv-Builds zu schützen. Überraschenderweise meldete die .NET-AOT-Bereitstellung für den verschleierten Build ähnliche Warnungen:

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

Solche Fehler hatten keinen zugehörigen Code IL30## oder IL2###. Es war unklar, wie der zugehörige Code identifiziert werden sollte.

Die Fehler traten nur in der verschleierten Version auf. Es könnte ein Fehler im Obfuskator sein. Wir haben den Obfuskator aktualisiert, aber die Fehler waren weiterhin vorhanden.

Wir mussten den Problemumfang eingrenzen. Dafür verwendeten wir den folgenden, auf binärer Suche basierenden Prozess:

  1. Deaktivieren Sie die Verschleierung für die Hälfte der Namespaces. Prüfen Sie, ob Fehler auftreten. Fahren Sie fort, bis Sie die Namespaces finden, die zu den Fehlern führen.
  2. Deaktivieren Sie die Verschleierung für die Hälfte der Typen im Namespace aus Schritt eins. Prüfen Sie, ob Fehler auftreten. Fahren Sie fort, bis Sie die Typen finden, die zu den Fehlern führen.
  3. Deaktivieren Sie die Verschleierung für die Hälfte der Member im Typ aus Schritt zwei. Prüfen Sie, ob Fehler auftreten. Fahren Sie fort, bis Sie die Member finden, die zu den Fehlern führen.
  4. Prüfen Sie den Code des Members aus Schritt drei. Kommentieren Sie ungewöhnliche Teile aus und prüfen Sie, ob Fehler auftreten.

Wir landeten bei Schritt eins 🙂 Nachdem wir die Verschleierung für alle Namespaces deaktiviert hatten, traten die Fehler weiterhin auf.

Wir analysierten die erzeugte Assembly mit ILSpy. Dort fanden wir einige unerwartete, vom Compiler erzeugte Typen. ILSpy zeigte ihre Verwendungen anhand eines ähnlichen C#-Codes:

interface X
{
    void f();
}

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

Dieser seltsame Code stammte aus der Migration von altem C-Code zu C#. Die Bedingung if (obj.f != null) ist vollständig redundant. Wir entfernten solche Bedingungen, und die Fehler verschwanden.

GetCallingAssembly nicht verwenden

Wir haben ein weiteres Laufzeitproblem abgefangen. Der Aufruf der Methode LicenseManager.AddLicenseData(string) schlug nach der AOT-Veröffentlichung mit dem folgenden Fehler fehl:

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

Die Methode Assembly.GetCallingAssembly sollte für Native AOT implementiert sein in .NET 9. Bis der Fix bereit ist, haben wir unseren Code so refaktoriert, dass die negativen Auswirkungen minimiert werden. Jetzt verwenden wir die Attribute der aufrufenden Assembly nur noch zur Validierung der Application License. Andere Lizenztypen sind mit AOT kompatibel.

Den Code mit speziellen Attributen markieren

Glücklicherweise konnten wir alle AOT- und Trim-Warnungen in der Docotic.Pdf-Kernbibliothek und den meisten Add-ons beheben. Aber möglicherweise können Sie nicht den gesamten AOT-inkompatiblen Code umschreiben.

.NET stellt für solche Situationen spezielle Attribute bereit. Sie markieren die API mit Attributen, um Clients über bekannte AOT-Probleme zu informieren. Benutzer Ihrer API erhalten eine Warnung, wenn sie die markierte Methode aufrufen.

Die wichtigsten Attribute zum Markieren von AOT-inkompatiblem .NET-Code sind:

  • RequiresDynamicCode
  • RequiresUnreferencedCode
  • DynamicallyAccessedMembers

Weitere Informationen zu diesen Attributen finden Sie in den offiziellen Artikeln Introduction to AOT warnings und Prepare .NET libraries for trimming.

Abschluss

Die Native-AOT-Bereitstellung ist ein großer Schritt nach vorn in der .NET-Welt. Sie können normalen C#-Code schreiben und eine native Anwendung oder Bibliothek erhalten. Solche Anwendungen sind in der Regel schneller und können ohne installierte .NET-Runtime ausgeführt werden. Sie können veröffentlichte DLLs sogar aus C, Rust oder anderen Nicht-.NET-Programmiersprachen verwenden.

Veröffentlichen Sie eine .NET-8-Testanwendung, um AOT-Kompatibilitätsprobleme zu finden. Es gibt auch Roslyn-Analyzatoren, aber sie erkennen weniger Probleme.

Die Docotic.Pdf-Kernbibliothek ist jetzt mit AOT und Trimmen kompatibel. Auch die folgenden Add-ons sind kompatibel:

  • Conformance-Add-on
  • Layout-Add-on
  • Gdi-Add-on
  • Logging-Add-on

Das HTML-to-PDF-Add-on enthält weiterhin Trim-Warnungen. Das ist unser nächstes Ziel auf der Native-AOT-Roadmap.

Stellen Sie uns gerne Fragen zu Native AOT oder zur PDF-Verarbeitung.