Serialización de excepciones en Xamarin.iOS

Xamarin.iOS contiene nuevos eventos para ayudar a responder a excepciones, especialmente en código nativo.

Tanto el código administrado como Objective-C la compatibilidad con excepciones en tiempo de ejecución (cláusulas try/catch/finally).

Sin embargo, sus implementaciones son diferentes, lo que significa que las bibliotecas en tiempo de ejecución (las bibliotecas de tiempo de ejecución entorno de ejecución Mono y las bibliotecas en tiempo de ejecución) tienen problemas cuando tienen que controlar excepciones y, a continuación, ejecutar código escrito en otros Objective-C lenguajes.

En este documento se explican los problemas que pueden producirse y las posibles soluciones.

También incluye un proyecto de ejemplo, Exception Marshaling, que se puede usar para probar diferentes escenarios y sus soluciones.

Problema

El problema se produce cuando se produce una excepción y, durante el desenredo de la pila, se encuentra un marco que no coincide con el tipo de excepción que se produjo.

Un ejemplo típico de esto para Xamarin.iOS o Xamarin.Mac es cuando una API nativa inicia una excepción y, a continuación, esa excepción se debe controlar de algún modo cuando el proceso de desenredo de la pila llega a un marco Objective-CObjective-C administrado.

La acción predeterminada es no hacer nada. En el ejemplo anterior, esto significa permitir que el Objective-C entorno de ejecución desenreda los fotogramas administrados. Esto es problemático, porque el tiempo de ejecución no sabe cómo desenredar los Objective-C fotogramas administrados; por ejemplo, no ejecutará ninguna cláusula catchfinally o en ese marco.

Código roto

Tenga en cuenta el siguiente código de ejemplo:

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

Esto produce una Objective-C excepción NSInvalidArgumentException en código nativo:

NSInvalidArgumentException *** setObjectForKey: key cannot be nil

Y el seguimiento de la pila será algo parecido a lo siguiente:

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 ()

Los fotogramas 0-3 son fotogramas nativos y el desenredo de pila en Objective-C tiempo de ejecución Objective-C desenredlar esos fotogramas. En concreto, ejecutará las Objective-C@catch@finally cláusulas o .

Sin embargo, el desenredo de pila no es capaz de desenredar correctamente los fotogramas administrados Objective-C (fotogramas 4-6), ya que los fotogramas se desenredarán, pero no se ejecutará la lógica de excepción administrada. Objective-C

Lo que significa que normalmente no es posible detectar estas excepciones de la siguiente manera:

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

Esto se debe a Objective-C que el desenredo de pila no conoce la cláusula catch administrada y tampoco se ejecutará la cláusula finally .

Cuando el ejemplo de código anterior es efectivo, se debe a que tiene un método para recibir notificaciones de excepciones no controladas, , que usan Objective-CNSSetUncaughtExceptionHandler Xamarin.iOS y Xamarin.Mac, y en ese momento intenta convertir las excepciones en excepciones Objective-C administradas.

Escenarios

Escenario 1: Objective-C detecciones de excepciones con un controlador catch administrado

En el escenario siguiente, es posible detectar excepciones Objective-C mediante controladores catch administrados:

  1. Se Objective-C produce una excepción.
  2. El Objective-C tiempo de ejecución recorre la pila (pero no la desenreda), buscando un controlador nativo que pueda controlar la @catch excepción.
  3. El tiempo de ejecución no encuentra ningún controlador, llama a e invoca al controlador instalado Objective-C@catch por NSGetUncaughtExceptionHandler Xamarin.iOS/Xamarin.Mac.
  4. El controlador de Xamarin.iOS/Xamarin.Mac convertirá la excepción en una excepción administrada Objective-C y la producirá. Puesto que Objective-C el tiempo de ejecución no desenredó la pila (solo la recorrió), el marco actual es el mismo en el que se produjo la Objective-C excepción.

Aquí se produce otro problema, porque el entorno de ejecución Mono no sabe cómo desenredar Objective-C los fotogramas correctamente.

Cuando se llama a la devolución de llamada de excepción no detectada de Xamarin.iOS, Objective-C la pila es como esta:

 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]

En este caso, los únicos fotogramas administrados son fotogramas 8-10, pero la excepción administrada se produce en el fotograma 0. Esto significa que el entorno de ejecución Mono debe desenredar los fotogramas nativos 0-7, lo que provoca un problema equivalente al problema descrito anteriormente: aunque el entorno de ejecución Mono desenredará los Objective-C@catch fotogramas nativos, no ejecutará ninguna cláusula o @finally .

Ejemplo de código:

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

Y la @finally cláusula no se ejecutará porque el entorno de ejecución Mono desenreda este marco no lo sabe.

Una variación de esto es producir una excepción administrada en el código administrado y, a continuación, desenredarse a través de fotogramas nativos para llegar a la primera cláusula catch administrada:

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

El método administrado llamará al método nativo y, a continuación, iOS realizará una gran cantidad de ejecución de código nativo antes de llamar finalmente al método administrado, con una gran cantidad de fotogramas nativos en la pila cuando se produce la excepción UIApplication:MainUIApplicationMainAppDelegate:FinishedLaunching administrada:

 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[])

Los fotogramas 0-1 y 27-30 se administran, mientras que todos los entre ellos son nativos. Si Mono se desenreda a través de estos Objective-C@catch fotogramas, no se ejecutará @finally ninguna cláusula o .

Escenario 2: no se pueden detectar Objective-C excepciones

En el escenario siguiente, no es posible detectar excepciones mediante controladores administrados porque la excepción se catchObjective-C controló de otra manera:

  1. Se Objective-C produce una excepción.
  2. El Objective-C tiempo de ejecución recorre la pila (pero no la desenreda), buscando un controlador nativo que pueda controlar la @catch excepción.
  3. El Objective-C tiempo de ejecución encuentra un @catch controlador, desenreda la pila y comienza a ejecutar el @catch controlador.

Este escenario se encuentra normalmente en las aplicaciones de Xamarin.iOS, ya que en el subproceso principal normalmente hay código como este:

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

Esto significa que en el subproceso principal nunca hay realmente una excepción no controlada y, por tanto, nunca se llama a nuestra devolución de llamada que convierte las excepciones en excepciones Objective-CObjective-C administradas.

Esto también es bastante común al depurar aplicaciones de Xamarin.Mac en una versión de macOS anterior a la que admite Xamarin.Mac, ya que la inspección de la mayoría de los objetos de interfaz de usuario del depurador intentará capturar propiedades que corresponden a selectores que no existen en la plataforma en ejecución (porque Xamarin.Mac incluye compatibilidad con una versión de macOS superior). Al llamar a estos selectores, se produce un ("selector no reconocido enviado a ..."), lo que finalmente hace que NSInvalidArgumentException el proceso se bloquea.

En resumen, tener el entorno de ejecución o los marcos de desenredo de entorno de ejecución Mono que no están programados para controlar puede provocar comportamientos indefinidos, como bloqueos, pérdidas de memoria y otros tipos de comportamientos Objective-C imprevisibles (errores).

Solución ##

En Xamarin.iOS 10 y Xamarin.Mac 2.10, hemos agregado compatibilidad para detectar excepciones administradas y en cualquier límite nativo administrado, y para convertir esa excepción al otro Objective-C tipo.

En el pseudocódigo, tiene un aspecto similar al siguiente:

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

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

Se intercepta el comando P/Invoke objc_msgSend y, en su lugar, se llama a esto:

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

Y se hace algo similar para el caso inverso (serializar excepciones administradas a Objective-C excepciones).

Detectar excepciones en el límite nativo administrado no es gratuito, por lo que no siempre está habilitado de forma predeterminada:

  • Xamarin.iOS/tvOS: la interceptación de Objective-C excepciones está habilitada en el simulador.
  • Xamarin.watchOS: la interceptación se aplica en todos los casos, porque permitir que los marcos administrados de desenredo en tiempo de ejecución confundan el recolector de elementos no utilizados y hacer que se bloquea o se Objective-C bloquea.
  • Xamarin.Mac: la interceptación de Objective-C excepciones está habilitada para las compilaciones de depuración.

En la sección Marcas en tiempo de compilación se explica cómo habilitar la interceptación cuando no está habilitada de forma predeterminada.

Events

Hay dos eventos nuevos que se inician una vez interceptada una excepción: Runtime.MarshalManagedException y Runtime.MarshalObjectiveCException .

A ambos eventos se les pasa un objeto que contiene la excepción original que se produjo (la propiedad ) y una propiedad para definir cómo se deben calcular las referencias EventArgsException de la ExceptionMode excepción.

La propiedad se puede cambiar en el controlador de eventos para cambiar el comportamiento según ExceptionMode cualquier procesamiento personalizado realizado en el controlador. Un ejemplo sería anular el proceso si se produce una excepción determinada.

El cambio de la propiedad se aplica al evento único, no afecta a ninguna ExceptionMode excepción interceptada en el futuro.

Están disponibles los siguientes modos:

  • Default: el valor predeterminado varía según la plataforma. Es si ThrowObjectiveCException el GC está en modo cooperativo (watchOS) y, de lo UnwindNativeCode contrario, (iOS/watchOS/macOS). El valor predeterminado puede cambiar en el futuro.
  • UnwindNativeCode: este es el comportamiento anterior (indefinido). Esto no está disponible cuando se usa gc en modo cooperativo (que es la única opción en watchOS; por lo tanto, no es una opción válida en watchOS), pero es la opción predeterminada para todas las demás plataformas.
  • ThrowObjectiveCException: convierte la excepción administrada en una Objective-C excepción y produce la Objective-C excepción. Este es el valor predeterminado en watchOS.
  • Abort: anule el proceso.
  • Disable: deshabilita la interceptación de excepciones, por lo que no tiene sentido establecer este valor en el controlador de eventos, pero una vez que se genera el evento, es demasiado tarde para deshabilitarlo. En cualquier caso, si se establece, se comportará como UnwindNativeCode .

Para serializar Objective-C excepciones en código administrado, están disponibles los siguientes modos:

  • Default: el valor predeterminado varía según la plataforma. Es si ThrowManagedException el GC está en modo cooperativo (watchOS) y, de lo UnwindManagedCode contrario, (iOS/tvOS/macOS). El valor predeterminado puede cambiar en el futuro.
  • UnwindManagedCode: este es el comportamiento anterior (indefinido). Esto no está disponible cuando se usa gc en modo cooperativo (que es el único modo de GC válido en watchOS; por lo tanto, no es una opción válida en watchOS), pero es el valor predeterminado para todas las demás plataformas.
  • ThrowManagedException: convierte la Objective-C excepción en una excepción administrada y produce la excepción administrada. Este es el valor predeterminado en watchOS.
  • Abort: anule el proceso.
  • Disable:D la interceptación de excepciones, por lo que no tiene sentido establecer este valor en el controlador de eventos, pero una vez que se genera el evento, es demasiado tarde para deshabilitarlo. En cualquier caso, si se establece, anulará el proceso.

Por lo tanto, para ver cada vez que se serializa una excepción, puede hacer lo siguiente:

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 posible pasar las siguientes opciones a mtouch (para aplicaciones de Xamarin.iOS) y mmp (para aplicaciones de Xamarin.Mac), que determinarán si está habilitada la interceptación de excepciones y establecerán la acción predeterminada que debe producirse:

  • --marshal-managed-exceptions=

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

    • default
    • unwindmanagedcode
    • throwmanagedexception
    • abort
    • disable

Excepto para disable , estos valores son idénticos a los valores que se pasan a los eventos y ExceptionModeMarshalManagedExceptionMarshalObjectiveCException .

La disable opción disable la interceptación, salvo que todavía interceptaremos excepciones cuando no agregue ninguna sobrecarga de ejecución. Los eventos de serialización se siguen generando para estas excepciones, siendo el modo predeterminado el modo predeterminado para la plataforma en ejecución.

Limitaciones

Solo interceptamos P/Invokes a la objc_msgSend familia de funciones al intentar detectar Objective-C excepciones. Esto significa que una P/Invoke a otra función de C, que luego inicia cualquier excepción, seguirá teniendo el comportamiento anterior e indefinido (esto puede mejorarse en Objective-C el futuro).