Condividi tramite


Gestione delle eccezioni

Sia il codice gestito che Objective-C supportano le eccezioni di runtime (clausole try/catch/finally).

Tuttavia, le implementazioni sono diverse, il che significa che le librerie di runtime (i runtime MonoVM/CoreCLR e le librerie di runtime Objective-C) presentano problemi quando si verificano eccezioni da altri runtime.

Questo articolo illustra i problemi che possono verificarsi e le possibili soluzioni.

Include anche un progetto di esempio, Exception Marshaling, che può essere usato per testare vari scenari e le loro 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 l'eccezione Objective-C deve essere gestita in qualche modo quando il processo di disimballaggio dello stack raggiunge un frame gestito.

In passato (pre-.NET), l'azione predefinita era di non fare nulla. Per l'esempio precedente, ciò significa consentire al runtime Objective-C di svolgere i fotogrammi gestiti. Questa azione è problematica perché il runtime di Objective-C non sa come rimuovere i fotogrammi gestiti; ad esempio, non eseguirà alcuna clausola gestita catch o finally, con problemi di individuazione dei bug davvero difficili da trovare.

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 il disaccoppiatore dello stack nel runtime Objective-C può disaccoppiare quei frame. In particolare, eseguirà qualsiasi Objective-C @catch o @finally clausola.

Tuttavia, il dispositivo di srotolamento dello stack Objective-C non è in grado di srotolare correttamente i frame gestiti (frame 4-6): il dispositivo di srotolamento dello stack Objective-C srotolerà i frame gestiti, ma non eseguirà alcuna logica di eccezione gestita, come le clausole catch o 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 il sistema di svolgimento dello stack di Objective-C non conosce la clausola gestita catch e né la clausola finally verrà eseguita.

Quando l'esempio di codice precedente è efficace, è perché Objective-C ha un metodo per ricevere una notifica di eccezioni non gestite Objective-C, , NSSetUncaughtExceptionHandlerche gli SDK .NET usano e a quel punto tenta di convertire eventuali eccezioni Objective-C in eccezioni gestite.

Scenari

Scenario 1 - intercettazione delle eccezioni Objective-C con un gestore di cattura gestito

Nello scenario seguente è possibile intercettare eccezioni Objective-C usando handler gestiti catch.

  1. Viene generata un'eccezione Objective-C.
  2. Il runtime Objective-C percorre lo stack (ma non lo svuota), cercando un gestore nativo @catch in grado di gestire l'eccezione.
  3. Il runtime Objective-C non trova @catch gestori, chiama NSGetUncaughtExceptionHandler e richiama il gestore installato dal SDK .NET.
  4. Il gestore delle eccezioni degli SDK .NET converte l'eccezione Objective-C in un'eccezione gestita e la solleva. Poiché il runtime di Objective-C non ha rilasciato lo stack (è stato solo esaminato), il frame corrente è lo stesso in cui è stata lanciata l'eccezione Objective-C.

In questo caso si verifica un altro problema, perché il runtime di Mono non sa come rimuovere correttamente i fotogrammi Objective-C.

Quando si invoca il callback dell'eccezione non rilevata Objective-C dello .NET SDK, lo stack è simile al seguente:

 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]

In questo caso, gli unici fotogrammi gestiti sono i fotogrammi 8-10, ma l'eccezione gestita viene generata nel fotogramma 0. Ciò significa che il runtime Mono deve eseguire lo scorrimento all'indietro dei frame nativi 0-7, il che causa un problema equivalente a quello discusso in precedenza: sebbene il runtime Mono eseguirà lo scorrimento all'indietro dei frame nativi, non eseguirà alcuna clausola Objective-C @catch 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 gestisce questo frame non lo conosce.

Una variante di questa operazione consiste nel lanciare un'eccezione gestita nel codice gestito e quindi svolgere i frame nativi per raggiungere la 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: 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[])

I fotogrammi da 0 a 1 e da 27 a 30 vengono gestiti, mentre tutti i fotogrammi tra di essi sono nativi. Se Mono si districa attraverso questi fotogrammi, non verranno eseguite le clausole Objective-C @catch o @finally.

Importante

Solo il runtime MonoVM supporta la rimozione di frame nativi durante la gestione delle eccezioni gestite. Il runtime CoreCLR interromperà semplicemente il processo quando si verifica questa situazione (il runtime CoreCLR viene usato per le app macOS, nonché quando NativeAOT è abilitato in qualsiasi piattaforma).

Scenario 2 - non è possibile intercettare eccezioni Objective-C

Nello scenario seguente non è possibile intercettare Objective-C eccezioni usando gestori gestiti catch perché l'eccezione Objective-C è stata gestita in un altro modo:

  1. Si verifica un'eccezione Objective-C.
  2. Il runtime Objective-C scorre lo stack (ma non lo disfa), cercando un gestore nativo @catch in grado di gestire l'eccezione.
  3. Il runtime Objective-C trova un @catch gestore, disfa lo stack e avvia l'esecuzione del @catch gestore.

Questo scenario si trova comunemente in .NET per le app 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 Objective-C non gestita e quindi il callback che converte Objective-C eccezioni in eccezioni gestite non viene mai chiamato.

Ciò è comune anche quando si esegue il debug di app macOS in una versione precedente di macOS rispetto all'ultima versione, 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. La chiamata di tali selettori genererà un NSInvalidArgumentException ("Selettore non riconosciuto inviato a ..."), causando infine il crash del processo.

Per riepilogare, avere il runtime Objective-C o il runtime Mono che scompongono frame che non sono programmati per gestire può portare a comportamenti indefiniti, come arresti anomali, perdite di memoria e altri tipi di comportamenti imprevedibili e anomali.

Suggerimento

Per le app macOS e Mac Catalyst (ma non iOS o tvOS), è possibile fare in modo che il ciclo dell'interfaccia utente non intercetta tutte le eccezioni impostando la NSApplicationCrashOnExceptions proprietà per l'app su true:

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

Tuttavia, si noti che questa proprietà non è documentata da Apple, quindi il comportamento potrebbe cambiare in futuro.

Soluzione

È disponibile il supporto per l'intercettazione di eccezioni gestite e Objective-C in qualsiasi limite nativo gestito e per la conversione di tale eccezione nell'altro tipo.

Nello pseudo-codice l'aspetto è simile al seguente:

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

P/Invoke su 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 (conversione di eccezioni gestite a eccezioni Objective-C).

In .NET, la gestione delle eccezioni per le eccezioni Objective-C è sempre abilitata di default.

La sezione Flag in fase di compilazione illustra come disabilitare l'intercettazione quando è l'impostazione predefinita.

Avvenimenti

Quando viene intercettata un'eccezione, vengono generati due eventi: Runtime.MarshalManagedException e Runtime.MarshalObjectiveCException.

Entrambi gli eventi ricevono un oggetto EventArgs che contiene l'eccezione originale sollevata (la proprietà Exception), e una proprietà ExceptionMode per definire come dovrebbe essere gestita l'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: attualmente è sempre ThrowObjectiveCException. Il valore predefinito potrebbe cambiare in futuro.
  • UnwindNativeCode: non disponibile quando si usa CoreCLR (CoreCLR non supporta la rimozione del codice nativo, interromperà invece il processo).
  • ThrowObjectiveCException: Convertire l'eccezione gestita in un'eccezione Objective-C e generare l'eccezione Objective-C. Si tratta dell'impostazione predefinita in .NET.
  • Abort: interrompe il processo.
  • Disable: Disabilita l'intercettazione dell'eccezione. Non ha senso impostare questo valore nel gestore eventi (quando l'evento viene generato è troppo tardi per disabilitare l'intercettazione dell'eccezione). In ogni caso, se impostato, si comporterà come UnwindNativeCode.

Sono disponibili le seguenti modalità quando si esegue il marshalling delle eccezioni Objective-C nel codice gestito:

  • Default: Al momento, è sempre ThrowManagedException in .NET. Il valore predefinito potrebbe cambiare in futuro.
  • UnwindManagedCode: si tratta del comportamento precedente (non definito).
  • ThrowManagedException: Convertire l'eccezione Objective-C in un'eccezione gestita e lanciare l'eccezione gestita. Si tratta dell'impostazione predefinita in .NET.
  • Abort: interrompe il processo.
  • Disable: Disabilita l'intercettazione dell'eccezione. Non ha senso impostare questo valore nel gestore eventi (quando l'evento viene generato è troppo tardi per disabilitare l'intercettazione dell'eccezione). In ogni caso, se impostato, si comporterà come UnwindManagedCode.

Per visualizzare ogni volta che viene eseguito il marshalling di un'eccezione, è possibile seguire questi passaggi:

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

Suggerimento

Idealmente Objective-C le eccezioni non devono verificarsi in un'app ben comportata (Apple le considera molto più eccezionali rispetto alle eccezioni gestite: "evitare di generare eccezioni [Objective-C] in un'app fornita agli utenti". Un modo per eseguire questa operazione consiste nell'aggiungere un gestore eventi per l'evento Runtime.MarshalObjectiveCException che registra tutte le eccezioni di cui è stato effettuato il marshalling Objective-C usando i dati di telemetria (per le compilazioni di debug/locali potrebbe anche impostare la modalità di eccezione su "Abort" per rilevare tutte queste eccezioni per correggere o evitare tali eccezioni.

Flag Build-Time

È possibile impostare le proprietà MSBuild seguenti, che determineranno se è abilitata l'intercettazione delle eccezioni e impostare l'azione predefinita che deve verificarsi:

  • ModalitàEccezioneGestitaMarshal: "default", "codicenativoannullamento", "lanciareccezioneobjectivec", "terminare", "disabilitare".
  • ModalitàEccezioneObjectiveCMarshal: "default", "unwindmanagedcode", "throwmanagedexception", "abort", "disable".

Esempio:

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

Ad eccezione di disable, questi valori sono identici ai ExceptionMode valori passati agli eventi MarshalManagedException e MarshalObjectiveCException .

L'opzione disable disabiliterà principalmente l'intercettazione, a eccezione delle situazioni in cui intercetteremo le eccezioni senza aggiungere 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.

Limitazioni

Intercettiamo solo i P/Invoke nella famiglia di funzioni objc_msgSend quando si cerca di catturare le eccezioni Objective-C. Ciò significa che un P/Invoke in un'altra funzione C, che genera quindi eventuali eccezioni Objective-C, continuerà a verificarsi nel comportamento precedente e non definito (questo potrebbe essere migliorato in futuro).

Vedere anche