Einführung in AOT-Warnungen

Wenn Sie Ihre Anwendung als Native AOT veröffentlichen, erzeugt der Buildprozess den gesamten nativen Code und die Datenstrukturen, die zur Unterstützung der Anwendung zur Laufzeit erforderlich sind. Dies unterscheidet sich von nicht nativen Bereitstellungen, die die Anwendung aus Formaten ausführen, die die Anwendung abstrakt beschreiben (ein Programm für einen virtuellen Computer) und native Darstellungen bei Bedarf zur Laufzeit erstellen.

Die abstrakten Darstellungen von Programmteilen verfügen nicht über eine 1:1-Zuordnung zur nativen Darstellung. Die abstrakte Beschreibung der generischen List<T>.Add-Methode wird z. B. potenziell unendlichen nativen Methodenkörpern zugeordnet, die für den angegebenen T (z. B. List<int>.Add und List<double>.Add) spezialisiert werden müssen.

Da die Beziehung zwischen abstraktem Code und nativem Code nicht 1:1 besteht, muss der Buildprozess zur Buildzeit eine vollständige Liste der nativen Codetexte und Datenstrukturen erstellen. Es kann schwierig sein, für einige der .NET-APIs diese Liste zur Buildzeit zu erstellen. Wenn die API auf eine Weise verwendet wird, die zur Buildzeit nicht erwartet wurde, wird zur Laufzeit eine Ausnahme ausgelöst.

Um beim Bereitstellen als Native AOT Verhaltensänderungen zu verhindern, bietet das .NET SDK eine statische Analyse der AOT-Kompatibilität durch so genannte „AOT-Warnungen“. AOT-Warnungen werden erstellt, wenn der Build Code findet, der möglicherweise nicht mit AOT kompatibel ist. Wenn nicht AOT-kompatibler Code als Native AOT erstellt wurde, kann er in einer Anwendung Verhaltensänderungen oder sogar Abstürze verursachen. Im Idealfall sollten bei allen Anwendungen, die Native AOT verwenden, keine AOT-Warnungen auftreten. Wenn AOT-Warnungen auftreten, stellen Sie sicher, dass es keine Verhaltensänderungen gibt, indem Sie Ihre App nach dem Erstellen als Native AOT gründlich testen.

Beispiele für AOT-Warnungen

Bei dem meisten C#-Code ist einfach zu bestimmen, welcher native Code generiert werden muss. Der native Compiler kann die Methodentexte durchlaufen und ermitteln, auf welche nativen Code- und Datenstrukturen zugegriffen wird. Leider stellen einige Features wie z. B. die Reflexion ein erhebliches Problem dar. Betrachten Sie folgenden Code:

Type t = typeof(int);
while (true)
{
    t = typeof(GenericType<>).MakeGenericType(t);
    Console.WriteLine(Activator.CreateInstance(t));
}

struct GenericType<T> { }

Das obige Programm hat zwar keinen großen Nutzen, aber es stellt einen Extremfall dar, der erfordert, dass eine unendliche Anzahl von generischen Typen erstellt wird, wenn die Anwendung als Native AOT erstellt wird. Ohne Native AOT würde das Programm ausgeführt werden, bis der Arbeitsspeicher nicht mehr verfügbar ist. Mit Native AOT könnten wir es nicht einmal erstellen, wenn wir alle erforderlichen Typen (die unendliche Anzahl von ihnen) generieren würden.

In diesem Fall gibt der Native AOT-Build die folgende Warnung in der MakeGenericType-Zeile aus:

AOT analysis warning IL3050: Program.<Main>$(String[]): Using member 'System.Type.MakeGenericType(Type[])' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. The native code for this instantiation might not be available at runtime.

Zur Laufzeit löst die Anwendung tatsächlich eine Ausnahme vom MakeGenericType-Aufruf aus.

Reagieren auf AOT-Warnungen

Die AOT-Warnungen sollen die Vorhersagbarkeit für Native AOT-Builds verbessern. Die meisten AOT-Warnungen beziehen sich auf mögliche Laufzeitausnahmen in Situationen, in denen zur Unterstützung des Szenarios kein nativer Code generiert wurde. Die breiteste Kategorie ist RequiresDynamicCodeAttribute.

RequiresDynamicCode

RequiresDynamicCodeAttribute ist einfach und allgemein: Es handelt sich um ein Attribut, das bedeutet, dass der Member als nicht mit AOT kompatibel kommentiert wurde. Diese Anmerkung bedeutet, dass der Member möglicherweise Reflexion oder einen anderen Mechanismus verwendet, um zur Laufzeit neuen nativen Code zu erstellen. Dieses Attribut wird verwendet, wenn Code grundsätzlich nicht mit AOT kompatibel oder die native Abhängigkeit zur statischen Vorhersage zur Buildzeit zu komplex ist. Dies gilt häufig für Methoden, die die Type.MakeGenericType-API verwenden, Reflexion ausgeben, oder für andere Technologien zur Codegenerierung während der Laufzeit. Der folgende Code enthält hierzu ein Beispiel.

[RequiresDynamicCode("Use 'MethodFriendlyToAot' instead")]
void MethodWithReflectionEmit() { ... }

void TestMethod()
{
    // IL3050: Using method 'MethodWithReflectionEmit' which has 'RequiresDynamicCodeAttribute'
    // can break functionality when AOT compiling. Use 'MethodFriendlyToAot' instead.
    MethodWithReflectionEmit();
}

Es gibt nicht viele Problemumgehungen für RequiresDynamicCode. Die beste Lösung ist, den Aufruf der Methode beim Erstellen als Native AOT vollständig zu vermeiden und stattdessen eine AOT-kompatible Methode zu verwenden. Wenn Sie eine Bibliothek schreiben und nicht darüber entscheiden können, ob die Methode aufgerufen wird, können Sie RequiresDynamicCode auch Ihrer eigenen Methode hinzufügen. Dadurch wird Ihre Methode als nicht AOT-kompatibel angemerkt. Wenn Sie RequiresDynamicCode hinzufügen, werden alle AOT-Warnungen in der angemerkten Methode ignoriert. Es wird jedoch weiterhin eine Warnung generiert, wenn die Methode von einer anderen Person aufgerufen wird. Aus diesem Grund ist es vor allem für Bibliotheksautoren nützlich, die Warnung an eine öffentliche API weiterzuleiten.

Wenn Sie feststellen können, dass der Aufruf sicher ist und der gesamte native Code zur Laufzeit zur Verfügung stehen wird, können Sie die Warnung auch durch UnconditionalSuppressMessageAttribute unterdrücken. Beispiel:

[RequiresDynamicCode("Use 'MethodFriendlyToAot' instead")]
void MethodWithReflectionEmit() { ... }

[UnconditionalSuppressMessage("Aot", "IL3050:RequiresDynamicCode",
    Justification = "The unfriendly method is not reachable with AOT")]
void TestMethod()
{
    If (RuntimeFeature.IsDynamicCodeSupported)
        MethodWithReflectionEmit(); // warning suppressed
}

UnconditionalSuppressMessage ist wie SuppressMessage, kann aber von publish und anderen Post-Build-Tools erkannt werden. SuppressMessage- und #pragma-Anweisungen sind nur in der Quelle vorhanden, sodass sie nicht verwendet werden können, um Warnungen vor dem Build zu ignorieren.

Achtung

Unterdrücken Sie AOT-Warnungen mit Vorsicht. Der Aufruf kann jetzt AOT-kompatibel sein, aber wenn Sie Ihren Code aktualisieren, kann sich dies ändern, und Sie vergessen möglicherweise, alle Unterdrückungen zu überprüfen.