Marshalling delle eccezioni in Xamarin.iOS e Xamarin.Mac
Sia il codice gestito che Objective-C il supporto per le eccezioni di runtime (clausole try/catch/finally).
Tuttavia, le implementazioni sono diverse, il che significa che le librerie di runtime (il runtime Mono o CoreCLR e le Objective-C librerie di runtime) presentano problemi quando devono gestire le eccezioni e quindi eseguire il codice scritto in altri linguaggi.
Questo documento illustra i problemi che possono verificarsi e le possibili soluzioni.
Include anche un progetto di esempio, il marshalling delle eccezioni, che può essere usato per testare diversi scenari e le relative soluzioni.
Problema
Il problema si verifica quando viene generata un'eccezione e durante la rimozione dello stack viene rilevato un frame che non corrisponde al tipo di eccezione generata.
Un esempio tipico di questo problema è quando un'API nativa genera un'eccezione Objective-C e quindi tale Objective-C eccezione deve essere gestita in qualche modo quando il processo di rimozione dello stack raggiunge un frame gestito.
Per i progetti Xamarin legacy (pre-.NET), l'azione predefinita non prevede alcuna operazione.
Per l'esempio precedente, ciò significa consentire al runtime di rimuovere i Objective-C fotogrammi gestiti. Questa azione è problematica perché il Objective-C runtime non sa come rimuovere i frame gestiti, ad esempio non eseguirà catch
alcuna clausola o finally
in tale frame.
Codice interrotto
Osservare l'esempio di codice seguente:
var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero);
Questo codice genererà un'eccezione Objective-C NSInvalidArgumentException nel codice nativo:
NSInvalidArgumentException *** setObjectForKey: key cannot be nil
E l'analisi dello stack sarà simile alla seguente:
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 ()
I frame 0-3 sono frame nativi e lo stack di rimozione nel Objective-C runtime può rimuovere tali fotogrammi. In particolare, eseguirà qualsiasi Objective-C@catch
clausola o @finally
.
Tuttavia, lo Objective-C stack di rimozione non è in grado di rimuovere correttamente i fotogrammi gestiti (frame 4-6): lo Objective-C stack di rimozione rimuoverà i fotogrammi gestiti, ma non eseguirà alcuna logica di eccezione gestita (ad esempio catch
o "clausole finally").
Ciò significa che in genere non è possibile intercettare queste eccezioni nel modo seguente:
try {
var dict = new NSMutableDictionary ();
dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
Console.WriteLine (ex);
} finally {
Console.WriteLine ("finally");
}
Ciò è dovuto al fatto che lo Objective-C stack di rimozione non conosce la clausola gestita catch
e non verrà eseguita nessuna delle finally
due clausole.
Quando l'esempio di codice precedente è efficace, è perché Objective-C ha un metodo per ricevere una notifica di eccezioni non Objective-C gestite, , NSSetUncaughtExceptionHandler
che Xamarin.iOS e Xamarin.Mac usano e a questo punto tenta di convertire eventuali Objective-C eccezioni in eccezioni gestite.
Scenari
Scenario 1: rilevamento di Objective-C eccezioni con un gestore catch gestito
Nello scenario seguente è possibile intercettare Objective-C le eccezioni usando gestori gestiti catch
:
- Viene generata un'eccezione Objective-C .
- Il Objective-C runtime illustra lo stack (ma non lo rimuove), cercando un gestore nativo
@catch
in grado di gestire l'eccezione. - Il Objective-C runtime non trova
@catch
gestori, chiamaNSGetUncaughtExceptionHandler
e richiama il gestore installato da Xamarin.iOS/Xamarin.Mac. - Il gestore di Xamarin.iOS/Xamarin.Mac convertirà l'eccezione in un'eccezione Objective-C gestita e la genererà. Poiché il Objective-C runtime non ha rimozione dello stack (è stato eseguito solo il passaggio), il frame corrente è identico a quello in cui è stata generata l'eccezione Objective-C .
In questo caso si verifica un altro problema, perché il runtime di Mono non sa come rimuovere Objective-C correttamente i frame.
Quando viene chiamato il callback delle eccezioni non rilevate Objective-C di Xamarin.iOS, lo stack è simile al seguente:
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]
In questo caso, gli unici fotogrammi gestiti sono frame 8-10, ma l'eccezione gestita viene generata nel frame 0. Ciò significa che il runtime di Mono deve rimuovere i fotogrammi nativi 0-7, che causa un problema equivalente al problema descritto in precedenza: anche se il runtime mono rimuoverà i fotogrammi nativi, non eseguirà alcuna Objective-C@catch
clausola o @finally
.
Esempio di codice:
-(id) setObject: (id) object forKey: (id) key
{
@try {
if (key == nil)
[NSException raise: @"NSInvalidArgumentException"];
} @finally {
NSLog (@"This won't be executed");
}
}
E la @finally
clausola non verrà eseguita perché il runtime mono che rimuove questo frame non lo conosce.
Una variante di questa operazione consiste nel generare un'eccezione gestita nel codice gestito e quindi rimuovere i fotogrammi nativi per passare alla prima clausola gestita catch
:
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.");
}
}
}
Il metodo gestito UIApplication:Main
chiamerà il metodo nativo UIApplicationMain
e quindi iOS eseguirà molte esecuzioni di codice nativo prima di chiamare il metodo gestito AppDelegate:FinishedLaunching
, con ancora molti frame nativi nello stack quando viene generata l'eccezione gestita:
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[])
I fotogrammi da 0 a 1 e 27-30 vengono gestiti, mentre tutti i fotogrammi tra sono nativi.
Se Mono si rimuove tramite questi frame, non Objective-C@catch
verranno eseguite clausole o @finally
.
Scenario 2: non è possibile intercettare Objective-C le eccezioni
Nello scenario seguente non è possibile intercettare Objective-C le eccezioni usando gestori gestiti catch
perché l'eccezione Objective-C è stata gestita in un altro modo:
- Viene generata un'eccezione Objective-C .
- Il Objective-C runtime illustra lo stack (ma non lo rimuove), cercando un gestore nativo
@catch
in grado di gestire l'eccezione. - Il Objective-C runtime trova un
@catch
gestore, rimuove lo stack e avvia l'esecuzione del@catch
gestore.
Questo scenario si trova comunemente nelle app Xamarin.iOS, perché nel thread principale è in genere presente codice simile al seguente:
void UIApplicationMain ()
{
@try {
while (true) {
ExecuteRunLoop ();
}
} @catch (NSException *ex) {
NSLog (@"An unhandled exception occured: %@", exc);
abort ();
}
}
Ciò significa che nel thread principale non esiste mai un'eccezione non gestita Objective-C e quindi il callback che converte le Objective-C eccezioni in eccezioni gestite non viene mai chiamato.
Ciò è comune anche quando si esegue il debug di app Xamarin.Mac in una versione macOS precedente rispetto a Xamarin.Mac supportata perché l'ispezione della maggior parte degli oggetti dell'interfaccia utente nel debugger tenterà di recuperare le proprietà corrispondenti ai selettori che non esistono nella piattaforma in esecuzione (perché Xamarin.Mac include il supporto per una versione macOS successiva). La chiamata di tali selettori genererà un'eccezione NSInvalidArgumentException
("Selettore non riconosciuto inviato a ..."), che alla fine causa l'arresto anomalo del processo.
Per riepilogare, avere il Objective-C runtime o i frame di rimozione del runtime Mono che non sono programmati per gestire può causare comportamenti non definiti, ad esempio arresti anomali, perdite di memoria e altri tipi di comportamenti imprevedibili (mis).
Soluzione
In Xamarin.iOS 10 e Xamarin.Mac 2.10 è stato aggiunto il supporto per intercettare sia le eccezioni gestite Objective-C che in qualsiasi limite nativo gestito e per convertire tale eccezione nell'altro tipo.
Nello pseudo-codice l'aspetto è simile al seguente:
[DllImport (Constants.ObjectiveCLibrary)]
static extern void objc_msgSend (IntPtr handle, IntPtr selector);
static void DoSomething (NSObject obj)
{
objc_msgSend (obj.Handle, Selector.GetHandle ("doSomething"));
}
Il P/Invoke per objc_msgSend viene intercettato e questo codice viene chiamato invece:
void
xamarin_dyn_objc_msgSend (id obj, SEL sel)
{
@try {
objc_msgSend (obj, sel);
} @catch (NSException *ex) {
convert_to_and_throw_managed_exception (ex);
}
}
E viene fatto qualcosa di simile per il caso inverso (marshalling delle eccezioni gestite alle Objective-C eccezioni).
L'intercettazione delle eccezioni nel limite nativo gestito non è gratuita, quindi per i progetti Xamarin legacy (pre-.NET), non è sempre abilitata per impostazione predefinita:
- Xamarin.iOS/tvOS: l'intercettazione delle Objective-C eccezioni è abilitata nel simulatore.
- Xamarin.watchOS: l'intercettazione viene applicata in tutti i casi, perché consentendo al runtime di Objective-C rimuovere i frame gestiti confondere il Garbage Collector e bloccarlo o arrestarsi in modo anomalo.
- Xamarin.Mac: l'intercettazione delle Objective-C eccezioni è abilitata per le compilazioni di debug.
In .NET il marshalling delle eccezioni gestite alle Objective-C eccezioni è sempre abilitato per impostazione predefinita.
La sezione Flag in fase di compilazione illustra come abilitare l'intercettazione quando non è abilitata per impostazione predefinita (o disabilitare l'intercettazione quando è l'impostazione predefinita).
evento
Dopo l'intercettazione di un'eccezione, vengono generati due eventi: Runtime.MarshalManagedException
e Runtime.MarshalObjectiveCException
.
Entrambi gli eventi vengono passati a un EventArgs
oggetto che contiene l'eccezione originale generata (la Exception
proprietà) e una ExceptionMode
proprietà per definire la modalità di marshalling dell'eccezione.
La ExceptionMode
proprietà può essere modificata nel gestore eventi per modificare il comportamento in base a qualsiasi elaborazione personalizzata eseguita nel gestore. Un esempio consiste nell'interrompere il processo se si verifica una determinata eccezione.
La modifica della ExceptionMode
proprietà si applica all'evento singolo e non influisce sulle eccezioni intercettate in futuro.
Quando si effettua il marshalling delle eccezioni gestite nel codice nativo, sono disponibili le modalità seguenti:
Default
: il valore predefinito varia in base alla piattaforma. È sempreThrowObjectiveCException
in .NET. Per i progetti Xamarin legacy, èThrowObjectiveCException
se GC è in modalità cooperativa (watchOS) eUnwindNativeCode
in caso contrario (iOS/watchOS/macOS). Il valore predefinito potrebbe cambiare in futuro.UnwindNativeCode
: si tratta del comportamento precedente (non definito). Questa opzione non è disponibile quando si usa GC in modalità cooperativa ,che è l'unica opzione su watchOS, pertanto questa non è un'opzione valida in watchOS, né quando si usa CoreCLR, ma è l'opzione predefinita per tutte le altre piattaforme nei progetti Xamarin legacy.ThrowObjectiveCException
: convertire l'eccezione gestita in un'eccezione Objective-C e generare l'eccezione Objective-C . Questa è l'impostazione predefinita in .NET e in watchOS nei progetti Xamarin legacy.Abort
: interrompe il processo.Disable
: disabilita l'intercettazione dell'eccezione, quindi non ha senso impostare questo valore nel gestore eventi, ma una volta generato l'evento è troppo tardi per disabilitarlo. In ogni caso, se impostato, si comporterà comeUnwindNativeCode
.
Quando si effettua il marshalling delle Objective-C eccezioni al codice gestito, sono disponibili le modalità seguenti:
Default
: il valore predefinito varia in base alla piattaforma. È sempreThrowManagedException
in .NET. Per i progetti Xamarin legacy, èThrowManagedException
se GC è in modalità cooperativa (watchOS) eUnwindManagedCode
in caso contrario (iOS/tvOS/macOS). Il valore predefinito potrebbe cambiare in futuro.UnwindManagedCode
: si tratta del comportamento precedente (non definito). Questa opzione non è disponibile quando si usa GC in modalità cooperativa (che è l'unica modalità GC valida in watchOS, pertanto non è un'opzione valida in watchOS), né quando si usa CoreCLR, ma è l'impostazione predefinita per tutte le altre piattaforme nei progetti Xamarin legacy.ThrowManagedException
: convertire l'eccezione in un'eccezione Objective-C gestita e generare l'eccezione gestita. Questa è l'impostazione predefinita in .NET e in watchOS nei progetti Xamarin legacy.Abort
: interrompe il processo.Disable
: disabilita l'intercettazione dell'eccezione, quindi non ha senso impostare questo valore nel gestore eventi, ma una volta generato l'evento, è troppo tardi per disabilitarlo. In ogni caso, se impostato, interromperà il processo.
Per visualizzare ogni volta che viene eseguito il marshalling di un'eccezione, è possibile eseguire questa operazione:
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);
};
Flag in fase di compilazione
È possibile passare le opzioni seguenti a mtouch (per le app Xamarin.iOS) e mmp (per le app Xamarin.Mac), che determinerà se è abilitata l'intercettazione delle eccezioni e imposta l'azione predefinita che deve verificarsi:
--marshal-managed-exceptions=
default
unwindnativecode
throwobjectivecexception
abort
disable
--marshal-objectivec-exceptions=
default
unwindmanagedcode
throwmanagedexception
abort
disable
Ad eccezione di disable
, questi valori sono identici ai ExceptionMode
valori passati agli MarshalManagedException
eventi e MarshalObjectiveCException
.
L'opzione disable
disabiliterà principalmente l'intercettazione, ad eccezione dei casi in cui non verrà aggiunto alcun sovraccarico di esecuzione. Gli eventi di marshalling vengono comunque generati per queste eccezioni, con la modalità predefinita come modalità predefinita per la piattaforma in esecuzione.
Limiti
I P/Invoke vengono intercettati solo nella objc_msgSend
famiglia di funzioni quando si tenta di intercettare Objective-C le eccezioni. Ciò significa che un P/Invoke in un'altra funzione C, che genera quindi eventuali Objective-C eccezioni, continuerà a verificarsi nel comportamento precedente e non definito (questo potrebbe essere migliorato in futuro).