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
-catch
Klauseln 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, NSSetUncaughtExceptionHandler
die 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:
- Eine Objective-C Ausnahme wird ausgelöst.
- 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. - Die Objective-C Runtime findet keine
@catch
Handler, ruftNSGetUncaughtExceptionHandler
auf und ruft den von Xamarin.iOS/Xamarin.Mac installierten Handler auf. - 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:
- Eine Objective-C Ausnahme wird ausgelöst.
- 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. - 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 immerThrowObjectiveCException
in .NET. Bei älteren Xamarin-Projekten ist diesThrowObjectiveCException
der Fall, wenn sich der GC im kooperativen Modus (watchOS) undUnwindNativeCode
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, wieUnwindNativeCode
.
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 immerThrowManagedException
in .NET. Bei älteren Xamarin-Projekten ist diesThrowManagedException
der Fall, wenn sich der GC im kooperativen Modus (watchOS) undUnwindManagedCode
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
disable
Mit 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).