Freigeben über


Ausnahme-Serialisierung

Sowohl verwalteter Code als auch Objective-C unterstützen Laufzeitausnahmen (try/catch/finally-Klauseln).

Ihre Implementierungen unterscheiden sich jedoch, was bedeutet, dass die Laufzeitbibliotheken (MonoVM/CoreCLR-Runtimes und die Objective-C Laufzeitbibliotheken) Probleme haben, wenn sie Ausnahmen von anderen Laufzeiten haben.

In diesem Artikel werden die Probleme erläutert, die auftreten können, und die möglichen Lösungen.

Es enthält auch ein Beispielprojekt, Exception Marshaling, das verwendet werden kann, um verschiedene Szenarien und deren Lösungen zu testen.

Problem

Das Problem tritt auf, wenn eine Ausnahme ausgelöst wird und beim Abwickeln eines Stapels ein Frame auftritt, der nicht mit dem Typ der ausnahme übereinstimmt, die ausgelöst wurde.

Ein typisches Beispiel für dieses Problem ist, wenn eine systemeigene API eine Objective-C Ausnahme auslöst, und diese Objective-C Ausnahme muss irgendwie behandelt werden, wenn der Stapelentspannvorgang einen verwalteten Frame erreicht.

In der Vergangenheit (pre-.NET) war die Standardaktion, nichts zu unternehmen. Für das obige Beispiel würde dies bedeuten, dass die Objective-C-Laufzeitumgebung verwaltete Frames abwickeln lässt. Diese Aktion ist problematisch, da die Objective-C-Laufzeit nicht weiß, wie verwaltete Frames entwirrt werden. Beispielsweise werden keine verwalteten catch oder finally Klauseln ausgeführt, was zu beeindruckend schwer auffindbaren Fehlern führt.

Fehlerhafter Code

Betrachten Sie das folgende Codebeispiel:

var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero); 

Dieser Code löst eine Objective-C NSInvalidArgumentException in systemeigenem Code aus:

NSInvalidArgumentException *** setObjectForKey: key cannot be nil

Und die Stapelablaufverfolgung sieht ungefähr so aus:

0   CoreFoundation          __exceptionPreprocess + 194
1   libobjc.A.dylib         objc_exception_throw + 52
2   CoreFoundation          -[__NSDictionaryM setObject:forKey:] + 1015
3   libobjc.A.dylib         objc_msgSend + 102
4   TestApp                 ObjCRuntime.Messaging.void_objc_msgSend_IntPtr_IntPtr (intptr,intptr,intptr,intptr)
5   TestApp                 Foundation.NSMutableDictionary.LowlevelSetObject (intptr,intptr)
6   TestApp                 ExceptionMarshaling.Exceptions.ThrowObjectiveCException ()

Frames 0-3 sind systemeigene Frames, und der Stapelentspanner in der Objective-C Laufzeit kann diese Frames abwickeln. Insbesondere werden alle Objective-C @catch oder @finally Klauseln ausgeführt.

Der Objective-C Stack-Entwirrer ist jedoch nicht in der Lage, die verwalteten Frames (Frames 4-6) ordnungsgemäß zu entwirren: Der Objective-C Stack-Entwirrer entwirrt die verwalteten Frames, führt jedoch keine verwaltete Ausnahmelogik (wie catch oder finally Klauseln) aus.

Dies bedeutet, dass es in der Regel nicht möglich ist, diese Ausnahmen auf folgende Weise abzufangen:

try {
    var dict = new NSMutableDictionary ();
    dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
    Console.WriteLine (ex);
} finally {
    Console.WriteLine ("finally");
}

Dies liegt daran, dass der Objective-C Staplauswickler nichts von der verwalteten catch-Klausel weiß und die finally-Klausel auch nicht ausgeführt wird.

Wenn das obige Codebeispiel wirksam ist, liegt das daran, dass Objective-C über eine Methode verfügt, um über nicht behandelte Objective-C-Ausnahmen benachrichtigt zu werden, die von den .NET-SDKs genutzt wird. In diesem Moment versucht das System, alle Objective-C-Ausnahmen in verwaltete Ausnahmen zu konvertieren.

Szenarien

Szenario-1: Abfangen von Objective-C-Ausnahmen mit einem verwalteten Catch-Handler

Im folgenden Szenario ist es möglich, Objective-C Ausnahmen mithilfe von verwalteten catch Handlern abzufangen:

  1. Es wird eine Objective-C Ausnahme ausgelöst.
  2. Die Objective-C Laufzeitumgebung durchläuft den Stapel (aber entspannt ihn nicht) und sucht nach einem nativen @catch Handler, der die Ausnahme behandeln kann.
  3. Die Objective-C Runtime findet @catch keine Handler, ruft NSGetUncaughtExceptionHandler auf und führt den vom .NET SDK installierten Handler aus.
  4. Der .NET-SDKs-Handler konvertiert die Objective-C Ausnahme in eine verwaltete Ausnahme und löst sie aus. Da die Objective-C Laufzeit den Stapel nicht zurückgesetzt hat (nur durchlaufen), ist der aktuelle Rahmen identisch mit dem Zeitpunkt, an dem die Objective-C Ausnahme ausgelöst wurde.

Ein weiteres Problem tritt hier auf, da die Mono-Runtime nicht weiß, wie sie Objective-C Frames ordnungsgemäß abwickeln kann.

Wenn der nicht abgefangene Objective-C-Ausnahmerückruffunktion des .NET-SDKs aufgerufen wird, sieht der Stack wie folgt aus:

 0 TestApp                  exception_handler(exc=name: "NSInvalidArgumentException" - reason: "*** setObjectForKey: key cannot be nil")
 1 CoreFoundation           __handleUncaughtException + 809
 2 libobjc.A.dylib          _objc_terminate() + 100
 3 libc++abi.dylib          std::__terminate(void (*)()) + 14
 4 libc++abi.dylib          __cxa_throw + 122
 5 libobjc.A.dylib          objc_exception_throw + 337
 6 CoreFoundation           -[__NSDictionaryM setObject:forKey:] + 1015
 7 TestApp                  xamarin_dyn_objc_msgSend + 102
 8 TestApp                  ObjCRuntime.Messaging.void_objc_msgSend_IntPtr_IntPtr (intptr,intptr,intptr,intptr)
 9 TestApp                  Foundation.NSMutableDictionary.LowlevelSetObject (intptr,intptr) [0x00000]
10 TestApp                  ExceptionMarshaling.Exceptions.ThrowObjectiveCException () [0x00013]

Hier sind die einzigen verwalteten Frames die Frames 8-10, die verwaltete Ausnahme wird allerdings in Frame 0 ausgelöst. Dies bedeutet, dass die Mono-Runtime die nativen Frames 0-7 zurücksetzen muss, was zu einem Problem führt, das dem oben beschriebenen Problem entspricht: Obwohl die Mono-Runtime die nativen Frames zurücksetzt, werden keine Objective-C @catch oder @finally Klauseln ausgeführt.

Codebeispiel:

-(id) setObject: (id) object forKey: (id) key
{
    @try {
        if (key == nil)
            [NSException raise: @"NSInvalidArgumentException"];
    } @finally {
        NSLog (@"This won't be executed");
    }
}

Und die Klausel wird nicht ausgeführt, da die @finally Mono-Laufzeit, die diesen Frame entspannt, es nicht kennt.

Eine Variante davon besteht darin, eine verwaltete Ausnahme in verwaltetem Code auszuwerfen und dann durch systemeigene Frames zu entspannen, um zur ersten verwalteten catch Klausel zu gelangen:

class AppDelegate : UIApplicationDelegate {
    public override bool FinishedLaunching (UIApplication application, NSDictionary launchOptions)
    {
        throw new Exception ("An exception");
    }
    static void Main (string [] args)
    {
        try {
            UIApplication.Main (args, null, typeof (AppDelegate));
        } catch (Exception ex) {
            Console.WriteLine ("Managed exception caught.");
        }
    }
}

Die verwaltete UIApplication:Main Methode ruft die systemeigene UIApplicationMain Methode auf, und dann führt iOS eine Menge systemeigener Codeausführung durch, bevor schließlich die verwaltete AppDelegate:FinishedLaunching Methode aufgerufen wird, wobei immer noch viele systemeigene Frames im Stapel vorhanden sind, wenn die verwaltete Ausnahme ausgelöst wird:

 0: TestApp                 ExceptionMarshaling.IOS.AppDelegate:FinishedLaunching (UIKit.UIApplication,Foundation.NSDictionary)
 1: TestApp                 (wrapper runtime-invoke) <Module>:runtime_invoke_bool__this___object_object (object,intptr,intptr,intptr) 
 2: TestApp                 mono_jit_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>, error=<unavailable>)
 3: TestApp                 do_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>, error=<unavailable>)
 4: TestApp                 mono_runtime_invoke [inlined] mono_runtime_invoke_checked(method=<unavailable>, obj=<unavailable>, params=<unavailable>, error=0xbff45758)
 5: TestApp                 mono_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>)
 6: TestApp                 xamarin_invoke_trampoline(type=<unavailable>, self=<unavailable>, sel="application:didFinishLaunchingWithOptions:", iterator=<unavailable>), context=<unavailable>)
 7: TestApp                 xamarin_arch_trampoline(state=0xbff45ad4)
 8: TestApp                 xamarin_i386_common_trampoline
 9: UIKit                   -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:]
10: UIKit                   -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:]
11: UIKit                   -[UIApplication _runWithMainScene:transitionContext:completion:]
12: UIKit                   __84-[UIApplication _handleApplicationActivationWithScene:transitionContext:completion:]_block_invoke.3124
13: UIKit                   -[UIApplication workspaceDidEndTransaction:]
14: FrontBoardServices      __37-[FBSWorkspace clientEndTransaction:]_block_invoke_2
15: FrontBoardServices      __40-[FBSWorkspace _performDelegateCallOut:]_block_invoke
16: FrontBoardServices      __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__
17: FrontBoardServices      -[FBSSerialQueue _performNext]
18: FrontBoardServices      -[FBSSerialQueue _performNextFromRunLoopSource]
19: FrontBoardServices      FBSSerialQueueRunLoopSourceHandler
20: CoreFoundation          __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
21: CoreFoundation          __CFRunLoopDoSources0
22: CoreFoundation          __CFRunLoopRun
23: CoreFoundation          CFRunLoopRunSpecific
24: CoreFoundation          CFRunLoopRunInMode
25: UIKit                   -[UIApplication _run]
26: UIKit                   UIApplicationMain
27: TestApp                 (wrapper managed-to-native) UIKit.UIApplication:UIApplicationMain (int,string[],intptr,intptr)
28: TestApp                 UIKit.UIApplication:Main (string[],intptr,intptr)
29: TestApp                 UIKit.UIApplication:Main (string[],string,string)
30: TestApp                 ExceptionMarshaling.IOS.Application:Main (string[])

Frames 0-1 und 27-30 werden verwaltet, während alle Zwischenframes nativ sind. Wenn Mono diese Frames aufrollt, werden keine Objective-C @catch oder @finally Klauseln ausgeführt.

Von Bedeutung

Nur die MonoVM-Runtime unterstützt die Entfaltung nativer Frames bei der Behandlung von verwalteten Ausnahmen. Die CoreCLR-Laufzeit bricht den Prozess einfach ab, wenn diese Situation auftritt (die CoreCLR-Laufzeit wird für macOS-Apps verwendet, sowie wenn NativeAOT auf einer beliebigen Plattform aktiviert ist).

Szenario 2 – nicht in der Lage, Objective-C Ausnahmen abzufangen

Im folgenden Szenario ist es nicht möglich, Objective-C Ausnahmen mithilfe von verwalteten catch Handlern abzufangen, da die Objective-C Ausnahme auf eine andere Weise behandelt wurde:

  1. Es wird eine Objective-C Ausnahme ausgelöst.
  2. Die Objective-C-Laufzeit geht den Stack durch (aber entwirrt ihn nicht), um nach einem nativen @catch Handler zu suchen, der die Ausnahme behandeln kann.
  3. Die Objective-C-Laufzeitumgebung findet einen @catch Handler, wickelt den Stapel ab und startet die Ausführung des @catch Handlers.

Dieses Szenario wird häufig in .NET für iOS-Apps gefunden, da im Hauptthread in der Regel Code wie folgt vorhanden ist:

void UIApplicationMain ()
{
    @try {
        while (true) {
            ExecuteRunLoop ();
        }
    } @catch (NSException *ex) {
        NSLog (@"An unhandled exception occured: %@", exc);
        abort ();
    }
}

Dies bedeutet, dass es im Hauptthread nie wirklich eine unbehandelte Objective-C Ausnahme gibt, und daher wird unser Rückruf, der Objective-C Ausnahmen in verwaltete Ausnahmen konvertiert, nie aufgerufen.

Dies ist auch beim Debuggen von macOS-Apps in einer früheren macOS-Version als der neuesten üblich, da das Überprüfen der meisten UI-Objekte im Debugger versucht, Eigenschaften abzurufen, die Selektoren entsprechen, die auf der Ausführungsplattform nicht vorhanden sind. Das Aufrufen solcher Selektoren löst eine NSInvalidArgumentException ("Nicht erkannter Selektor gesendet an ...") aus, wodurch der Prozess schließlich abstürzt.

Zusammenfassend lässt sich sagen, dass das Objective-C-Runtime oder das Mono-Runtime Frames abwickeln, die nicht für die Verarbeitung programmiert sind, zu nicht definierten Verhaltensweisen wie Abstürzen, Speicherleckagen und anderen Arten unvorhersehbarer (Fehl-)Verhaltensweisen führen können.

Tipp

Für macOS- und Mac Catalyst-Apps (jedoch nicht für iOS oder tvOS) ist es möglich, die UI-Schleife so einzustellen, dass nicht alle Ausnahmen abgefangen werden, indem die NSApplicationCrashOnExceptions-Eigenschaft der App auf true gesetzt wird.

var defs = new NSDictionary ((NSString) "NSApplicationCrashOnExceptions", NSNumber.FromBoolean (true));
NSUserDefaults.StandardUserDefaults.RegisterDefaults (defs);

Beachten Sie jedoch, dass diese Eigenschaft nicht von Apple dokumentiert ist, daher kann sich das Verhalten in Zukunft ändern.

Lösung

Wir unterstützen das Abfangen von Managed- und Objective-C-Ausnahmen an jeder Grenze zwischen verwaltetem und nativem Code und die Umwandlung dieser Ausnahme in den anderen Typ.

In Pseudocode sieht es ungefähr wie folgt aus:

class MyClass {
    [DllImport (Constants.ObjectiveCLibrary)]
    static extern void objc_msgSend (IntPtr handle, IntPtr selector);

    static void DoSomething (NSObject obj)
    {
        objc_msgSend (obj.Handle, Selector.GetHandle ("doSomething"));
    }
}

Der P/Invoke to objc_msgSend wird abgefangen, und dieser Code wird stattdessen aufgerufen:

void
xamarin_dyn_objc_msgSend (id obj, SEL sel)
{
    @try {
        objc_msgSend (obj, sel);
    } @catch (NSException *ex) {
        convert_to_and_throw_managed_exception (ex);
    }
}

Und für den umgekehrten Fall wird eine ähnliche Aktion durchgeführt (verwaltete Ausnahmen werden in Objective-C-Ausnahmen übertragen).

In .NET ist das Marshalling verwalteter Ausnahmen für Objective-C Ausnahmen standardmäßig aktiviert.

Im Abschnitt "Build-Zeit-Flags" wird erläutert, wie Sie das Abfangen deaktivieren, wenn es standardmäßig aktiviert ist.

Ereignisse

Es gibt zwei Ereignisse, die ausgelöst werden, sobald eine Ausnahme abgefangen wird: Runtime.MarshalManagedException und Runtime.MarshalObjectiveCException.

Beide Ereignisse werden an ein EventArgs Objekt übergeben, das die ursprüngliche Ausnahme enthält, die ausgelöst wurde (die Exception Eigenschaft), und eine ExceptionMode Eigenschaft, um zu definieren, wie die Ausnahme gemarstet werden soll.

Die ExceptionMode Eigenschaft kann im Ereignishandler geändert werden, um das Verhalten entsprechend jeder benutzerdefinierten Verarbeitung im Handler zu ändern. Ein Beispiel wäre das Abbrechen des Prozesses, wenn eine bestimmte Ausnahme auftritt.

Das Ändern der ExceptionMode-Eigenschaft gilt für das einzelne Ereignis, es wirkt sich nicht auf Ausnahmen aus, die in Zukunft abgefangen werden.

Die folgenden Modi sind verfügbar, wenn verwaltete Ausnahmen an nativen Code übermittelt werden.

  • Default: Derzeit ist es immer ThrowObjectiveCException. Der Standardwert kann sich in Zukunft ändern.
  • UnwindNativeCode: Dies ist nicht verfügbar, wenn Sie CoreCLR verwenden (CoreCLR unterstützt nicht das Entwirren von systemeigenem Code, stattdessen wird der Prozess abgebrochen).
  • ThrowObjectiveCException: Konvertieren Sie die verwaltete Ausnahme in eine Objective-C Ausnahme, und lösen Sie die Objective-C Ausnahme aus. Dies ist die Standardeinstellung in .NET.
  • Abort: Abbrechen des Prozesses.
  • Disable: Deaktiviert das Abfangen von Ausnahmen. Es ist nicht sinnvoll, diesen Wert im Ereignishandler festzulegen (sobald das Ereignis ausgelöst wurde, ist es zu spät, um das Abfangen der Ausnahme zu deaktivieren). Wenn festgelegt, verhält es sich in jedem Fall wie UnwindNativeCode.

Die folgenden Modi sind verfügbar, wenn Objective-C-Ausnahmen zu verwaltetem Code übertragen werden:

  • Default: Derzeit ist es immer ThrowManagedException in .NET. Der Standardwert kann sich in Zukunft ändern.
  • UnwindManagedCode: Dies ist das vorherige (undefinierte) Verhalten.
  • ThrowManagedException: Konvertieren Sie die Objective-C Ausnahme in eine verwaltete Ausnahme, und lösen Sie die verwaltete Ausnahme aus. Dies ist die Standardeinstellung in .NET.
  • Abort: Abbrechen des Prozesses.
  • Disable: Deaktiviert die Ausnahmeabfangung. Es ist nicht sinnvoll, diesen Wert im Ereignishandler festzulegen (sobald das Ereignis ausgelöst wurde, ist es zu spät, um das Abfangen der Ausnahme zu deaktivieren). Wenn festgelegt, verhält es sich in jedem Fall wie UnwindManagedCode.

Um also jedes Mal zu sehen, wenn eine Ausnahme gemarstet wird, können Sie dies tun:

class MyApp {
    static void Main (string args[])
    {
        Runtime.MarshalManagedException += (object sender, MarshalManagedExceptionEventArgs args) =>
        {
            Console.WriteLine ("Marshaling managed exception");
            Console.WriteLine ("    Exception: {0}", args.Exception);
            Console.WriteLine ("    Mode: {0}", args.ExceptionMode);
            
        };
        Runtime.MarshalObjectiveCException += (object sender, MarshalObjectiveCExceptionEventArgs args) =>
        {
            Console.WriteLine ("Marshaling Objective-C exception");
            Console.WriteLine ("    Exception: {0}", args.Exception);
            Console.WriteLine ("    Mode: {0}", args.ExceptionMode);
        };
        /// ...
    }
}

Tipp

Idealerweise sollten Objective-C Ausnahmen nicht in einer gut verhaltenen App auftreten (Apple betrachtet sie viel außergewöhnlicher als verwaltete Ausnahmen: "Vermeiden Sie das Auslösen von [Objective-C]-Ausnahmen in einer App, die Sie an Benutzer senden". Eine Möglichkeit, dies zu erreichen, wäre das Hinzufügen eines Ereignishandlers für das Runtime.MarshalObjectiveCException-Ereignis , das alle gemarstischen Objective-C Ausnahmen mithilfe von Telemetrie protokollieren würde (für Debug-/lokale Builds möglicherweise auch den Ausnahmemodus auf "Abort" festlegen), um alle solchen Ausnahmen zu erkennen, um sie zu beheben/zu vermeiden.

Flags zur Build-Zeit

Es ist möglich, die folgenden MSBuild-Eigenschaften festzulegen, die bestimmen, ob die Ausnahmeinterception aktiviert ist, und die Standardaktion festlegen, die auftreten soll:

  • MarshalManagedExceptionMode: "default", "relaxnativecode", "throwobjectivecexception", "abort", "disable".
  • MarshalObjectiveCExceptionMode: "default", "relaxmanagedcode", "throwmanagedexception", "abort", "disable".

Beispiel:

<PropertyGroup>
    <MarshalManagedExceptionMode>throwobjectivecexception</MarshalManagedExceptionMode>
    <MarshalObjectiveCExceptionMode>throwmanagedexception</MarshalObjectiveCExceptionMode>
</PropertyGroup>

Mit Ausnahme dieser disableWerte sind diese Werte identisch mit den ExceptionMode Werten, die an die MarshalManagedException - und MarshalObjectiveCException-Ereignisse übergeben werden.

Die disable Option wird das Abfangen größtenteils deaktivieren, jedoch werden Ausnahmen weiterhin abgefangen, solange kein zusätzlicher Ausführungsaufwand entsteht. Die Marshaling-Ereignisse werden für diese Ausnahmen weiterhin ausgelöst, wobei der Standardmodus der Standardmodus für die ausgeführte Plattform ist.

Einschränkungen

Beim Versuch, Objective-C Ausnahmen abzufangen, werden P/Invokes nur für die objc_msgSend Funktionsfamilie abgefangen. Dies bedeutet, dass ein P/Invoke für eine andere C-Funktion, die dann alle Objective-C Ausnahmen auslöst, weiterhin das alte und nicht definierte Verhalten aufweist (dies kann in Zukunft verbessert werden).

Siehe auch