Exception Marshalling in Xamarin.iOS und Xamarin.Mac

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

Ihre Implementierungen sind jedoch unterschiedlich, was bedeutet, dass die Laufzeitbibliotheken (die Mono-Runtime oder CoreCLR und die Objective-C Laufzeitbibliotheken) Probleme haben, wenn sie Ausnahmen behandeln und dann code ausführen müssen, der in anderen Sprachen geschrieben wurde.

In diesem Dokument werden die möglichen Probleme und mögliche Lösungen erläutert.

Es enthält auch das Beispielprojekt Exception Marshaling, das zum Testen verschiedener Szenarien und deren Lösungen verwendet werden kann.

Problem

Das Problem tritt auf, wenn eine Ausnahme ausgelöst wird und beim Entladen des Stapels ein Frame auftritt, der nicht mit dem Typ der ausgelösten Ausnahme übereinstimmt.

Ein typisches Beispiel für dieses Problem ist, wenn eine native API eine Objective-C Ausnahme auslöst. Diese Ausnahme muss dann Objective-C irgendwie behandelt werden, wenn der Stapelentladungsprozess einen verwalteten Frame erreicht.

Bei älteren Xamarin-Projekten (pre-.NET) besteht die Standardaktion darin, nichts zu tun. Für das obige Beispiel bedeutet dies, dass die Objective-C Laufzeit verwaltete Frames entlädt. Diese Aktion ist problematisch, da die Objective-C Laufzeit nicht weiß, wie verwaltete Frames entladen werden. Beispielsweise führt sie keine - oder finally -catchKlauseln in diesem Frame aus.

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 im systemeigenen Code aus:

NSInvalidArgumentException *** setObjectForKey: key cannot be nil

Und die Stapelüberwachung sieht in etwa wie folgt 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 native Frames, und der Stapelentladungsmodul in der Objective-C Laufzeit kann diese Frames entladen. Insbesondere werden alle Objective-C@catch - oder @finally -Klauseln ausgeführt.

Der Stapelentladungser ist jedoch nicht in der Lage, Objective-C die verwalteten Frames (Frames 4-6) ordnungsgemäß zu entladen: Der Objective-C Stapelentladungser entlädt die verwalteten Frames, führt jedoch keine verwaltete Ausnahmelogik aus (zcatch. B. oder "finally-Klauseln").

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 Stapelentladunger die verwaltete catch Klausel nicht kennt, und auch die finally -Klausel wird nicht ausgeführt.

Wenn das obige Codebeispiel wirksam ist , liegt es daran, dass Objective-C eine Methode zur Benachrichtigung über nicht behandelte Objective-C Ausnahmen verfügt, NSSetUncaughtExceptionHandlerdie Xamarin.iOS und Xamarin.Mac verwenden, und an diesem Punkt versucht, 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, Ausnahmen mithilfe von verwalteten catch Handlern abzufangenObjective-C:

  1. Eine Objective-C Ausnahme wird ausgelöst.
  2. Die Objective-C Laufzeit durchläuft den Stapel (entlädt ihn jedoch nicht), und sucht nach einem nativen @catch Handler, der die Ausnahme behandeln kann.
  3. Die Objective-C Runtime findet keine @catch Handler, ruft NSGetUncaughtExceptionHandlerauf und ruft den von Xamarin.iOS/Xamarin.Mac installierten Handler auf.
  4. Der Xamarin.iOS/Xamarin.Mac-Handler konvertiert die Objective-C Ausnahme in eine verwaltete Ausnahme und löst sie aus. Da die Objective-C Runtime den Stapel nicht entladen hat (nur durchlaufen), ist der aktuelle Frame derselbe wie der Ort, an dem die Objective-C Ausnahme ausgelöst wurde.

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

Wenn der Xamarin.iOS-Ausnahmerückruf Objective-C aufgerufen wird, sieht der Stapel wie folgt aus:

 0 libxamarin-debug.dylib   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 libxamarin-debug.dylib   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 frames 8-10 die einzigen verwalteten Frames, aber die verwaltete Ausnahme wird in Frame 0 ausgelöst. Dies bedeutet, dass die Mono-Runtime die nativen Frames 0-7 entladen muss, was ein Problem verursacht, das dem oben beschriebenen Problem entspricht: Obwohl die Mono-Runtime die nativen Frames entlädt, führt sie keine Objective-C@catch - oder @finally -Klauseln aus.

Codebeispiel:

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

Und die @finally -Klausel wird nicht ausgeführt, da die Mono-Runtime, die diesen Frame entlädt, nichts davon weiß.

Eine Variante davon besteht darin, eine verwaltete Ausnahme in verwaltetem Code auszulösen und dann durch native Frames zu entladen, 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 native UIApplicationMain Methode auf, und iOS führt dann eine Menge nativer Codeausführungen durch, bevor die verwaltete AppDelegate:FinishedLaunching Methode schließlich aufgerufen wird, mit immer noch vielen nativen Frames im Stapel, 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: libmonosgen-2.0.dylib   mono_jit_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>, error=<unavailable>)
 3: libmonosgen-2.0.dylib   do_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>, error=<unavailable>)
 4: libmonosgen-2.0.dylib   mono_runtime_invoke [inlined] mono_runtime_invoke_checked(method=<unavailable>, obj=<unavailable>, params=<unavailable>, error=0xbff45758)
 5: libmonosgen-2.0.dylib   mono_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>)
 6: libxamarin-debug.dylib  xamarin_invoke_trampoline(type=<unavailable>, self=<unavailable>, sel="application:didFinishLaunchingWithOptions:", iterator=<unavailable>), context=<unavailable>)
 7: libxamarin-debug.dylib  xamarin_arch_trampoline(state=0xbff45ad4)
 8: libxamarin-debug.dylib  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[])

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

Szenario 2: Ausnahmen können nicht abfangen Objective-C

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

  1. Eine Objective-C Ausnahme wird ausgelöst.
  2. Die Objective-C Laufzeit durchläuft den Stapel (entlädt ihn jedoch nicht), und sucht nach einem nativen @catch Handler, der die Ausnahme behandeln kann.
  3. Die Objective-C Runtime findet einen @catch Handler, entlädt den Stapel und beginnt mit der Ausführung des @catch Handlers.

Dieses Szenario ist häufig in Xamarin.iOS-Apps zu finden, da im Standard Thread in der Regel Code wie der folgende vorhanden ist:

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

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

Dies ist auch beim Debuggen von Xamarin.Mac-Apps auf einer früheren macOS-Version als Xamarin.Mac üblich, da beim Überprüfen der meisten UI-Objekte im Debugger versucht wird, Eigenschaften abzurufen, die Selektoren entsprechen, die auf der ausführenden Plattform nicht vorhanden sind (da Xamarin.Mac Unterstützung für eine höhere macOS-Version enthält). Durch den Aufruf solcher Selektoren wird ein NSInvalidArgumentException ("Unrecognized selector sent to ...") ausgelöst, was schließlich dazu führt, dass der Prozess abstürzt.

Zusammenfassend kann es zu undefinierten Verhaltensweisen wie Abstürze, Speicherverlusten und anderen Arten von unvorhersehbaren (fehl)Verhaltensweisen führen, wenn entweder die Objective-C Runtime oder die Mono-Runtime entlädt, für die sie nicht programmiert sind.

Lösung

In Xamarin.iOS 10 und Xamarin.Mac 2.10 haben wir Unterstützung für das Abfangen von verwalteten und Objective-C ausnahmen an jeder verwalteten nativen Grenze und für die Konvertierung dieser Ausnahme in den anderen Typ hinzugefügt.

Im Pseudocode sieht dies in etwa wie folgt aus:

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

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

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 etwas Ähnliches wird für den umgekehrten Fall (Marshalling verwalteter Ausnahmen zu Objective-C Ausnahmen) durchgeführt.

Das Abfangen von Ausnahmen an der verwalteten nativen Grenze ist nicht kostenlos, sodass es für Ältere Xamarin-Projekte (pre-.NET) nicht immer standardmäßig aktiviert ist:

  • Xamarin.iOS/tvOS: Das Abfangen von Objective-C Ausnahmen ist im Simulator aktiviert.
  • Xamarin.watchOS: Interception wird in allen Fällen erzwungen, da das Entladen von verwalteten Frames zur Objective-C Laufzeit den Garbage Collector verwirren und entweder hängen bleibt oder abstürzt.
  • Xamarin.Mac: Das Abfangen von Objective-C Ausnahmen ist für Debugbuilds aktiviert.

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

Im Abschnitt Buildzeitflags wird erläutert, wie Das Abfangen aktiviert wird, wenn es nicht standardmäßig aktiviert ist (oder die Abfangenfunktion deaktiviert wird, wenn es sich um die Standardeinstellung handelt).

Ereignisse

Es gibt zwei Ereignisse, die ausgelöst werden, sobald eine Ausnahme abgefangen wurde: 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 gemarshallt 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. Dies wirkt sich nicht auf ausnahmen aus, die in zukunft abgefangen werden.

Die folgenden Modi sind verfügbar, wenn verwaltete Ausnahmen in systemeigenen Code gemarshallt werden:

  • Default: Die Standardeinstellung variiert je nach Plattform. Es ist immer ThrowObjectiveCException in .NET. Bei älteren Xamarin-Projekten ist dies ThrowObjectiveCException der Fall, wenn sich der GC im kooperativen Modus (watchOS) und UnwindNativeCode andernfalls (iOS/watchOS/macOS) befindet. Der Standardwert kann sich in Zukunft ändern.
  • UnwindNativeCode: Dies ist das vorherige (undefinierte) Verhalten. Dies ist nicht verfügbar, wenn Sie gc im kooperativen Modus verwenden (dies ist die einzige Option unter watchOS, daher ist dies keine gültige Option unter watchOS), noch bei Verwendung von CoreCLR, aber es ist die Standardoption für alle anderen Plattformen in älteren Xamarin-Projekten.
  • 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 und unter watchOS in älteren Xamarin-Projekten.
  • Abort: Abbrechen des Prozesses.
  • Disable: Deaktiviert die Ausnahmeabnahme, sodass es nicht sinnvoll ist, diesen Wert im Ereignishandler festzulegen, aber sobald das Ereignis ausgelöst wurde, ist es zu spät, ihn zu deaktivieren. In jedem Fall verhält es sich, wenn festgelegt, wie UnwindNativeCode.

Die folgenden Modi sind verfügbar, wenn Ausnahmen für verwalteten Code gemarshallt Objective-C werden:

  • Default: Die Standardeinstellung variiert je nach Plattform. Es ist immer ThrowManagedException in .NET. Bei älteren Xamarin-Projekten ist dies ThrowManagedException der Fall, wenn sich der GC im kooperativen Modus (watchOS) und UnwindManagedCode andernfalls (iOS/tvOS/macOS) befindet. Der Standardwert kann sich in Zukunft ändern.
  • UnwindManagedCode: Dies ist das vorherige (undefinierte) Verhalten. Dies ist nicht verfügbar, wenn gc im kooperativen Modus verwendet wird (der einzige gültige GC-Modus unter watchOS, daher ist dies keine gültige Option unter watchOS), noch bei Verwendung von CoreCLR, aber es ist die Standardeinstellung für alle anderen Plattformen in älteren Xamarin-Projekten.
  • 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 und unter watchOS in älteren Xamarin-Projekten.
  • Abort: Abbrechen des Prozesses.
  • Disable: Deaktiviert die Ausnahmeabnahme, sodass es nicht sinnvoll ist, diesen Wert im Ereignishandler festzulegen, aber sobald das Ereignis ausgelöst wurde, ist es zu spät, ihn zu deaktivieren. In jedem Fall, wenn festgelegt, wird der Prozess abgebrochen.

Wenn Sie also jedes Mal sehen möchten, dass eine Ausnahme gemarshallt wird, können Sie folgendes tun:

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

Build-Time Flags

Es ist möglich, die folgenden Optionen an mtouch (für Xamarin.iOS-Apps) und mmp (für Xamarin.Mac-Apps) zu übergeben, um zu bestimmen, ob die Ausnahmeinterception aktiviert ist, und die auszuführende Standardaktion festzulegen:

  • --marshal-managed-exceptions=

    • default
    • unwindnativecode
    • throwobjectivecexception
    • abort
    • disable
  • --marshal-objectivec-exceptions=

    • default
    • unwindmanagedcode
    • throwmanagedexception
    • abort
    • disable

disableMit Ausnahme von sind diese Werte identisch mit den ExceptionMode Werten, die an die MarshalManagedException Ereignisse und MarshalObjectiveCException übergeben werden.

Mit disable der Option wird die Abfangfunktion größtenteils deaktiviert, mit der Ausnahme, dass weiterhin Ausnahmen abgefangen werden, wenn sie keinen Ausführungsaufwand verursacht. Die Marshallingereignisse werden für diese Ausnahmen weiterhin ausgelöst, wobei der Standardmodus der Standardmodus für die ausführende Plattform ist.

Einschränkungen

Wir fangen nur P/Invokes für die objc_msgSend Funktionsfamilie ab, wenn wir versuchen, Ausnahmen abzufangen Objective-C . 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).