Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Tanto el código administrado como Objective-C tienen 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 (los entornos de ejecución de MonoVM/CoreCLR y las bibliotecas en tiempo de ejecución de Objective-C) tienen problemas cuando se producen excepciones de otros entornos de ejecución.
En este artículo se explican los problemas que pueden producirse y las posibles soluciones.
También incluye un proyecto de ejemplo, Serialización de excepciones, 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 desenredado de la pila, se encuentra un marco que no coincide con el tipo de excepción que se produjo.
Un ejemplo típico de este problema es cuando una API nativa produce una excepción de Objective-C y, a continuación, esa excepción Objective-C debe controlarse de alguna manera cuando el proceso de desenredado de la pila alcanza un marco administrado.
En el pasado (pre-.NET), la acción predeterminada era no hacer nada. Para el ejemplo anterior, esto significaría permitir que el runtime Objective-C desenrollara marcos administrados. Esta acción es problemática, ya que el entorno de ejecución de Objective-C no sabe cómo desempaquetar marcos gestionados; por ejemplo, no ejecutará ninguna catch
o finally
cláusula gestionada, lo que da lugar a errores extremadamente difíciles de encontrar.
Código roto
Tenga en cuenta el siguiente código de ejemplo:
var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero);
Este código iniciará un Objective-C NSInvalidArgumentException en código nativo:
NSInvalidArgumentException *** setObjectForKey: key cannot be nil
El rastro de la pila será algo así:
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 marcos 0-3 son marcos nativos y el desenrollador de pila en el entorno de ejecución de Objective-C puede desenrollar esos marcos. En concreto, ejecutará cualquier cláusula Objective-C @catch
o @finally
.
Sin embargo, el desapilador de Objective-C no es capaz de desapilar correctamente los marcos administrados (marcos 4-6): el desapilador de Objective-C desapilará los marcos administrados, pero no ejecutará ninguna lógica de excepciones administradas (como las cláusulas catch
o finally
).
Esto 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 que el desenredador de Objective-C pila no conoce la cláusula administrada catch
y tampoco se ejecutará la finally
cláusula .
Cuando el ejemplo de código anterior es efectivo, se debe a que Objective-C tiene un método para recibir una notificación de excepciones de Objective-C no controladas, NSSetUncaughtExceptionHandler
, que usan los SDK de .NET y, en ese momento, intenta convertir las excepciones de Objective-C en excepciones administradas.
Escenarios
Escenario 1: detección de excepciones de Objective-C con un controlador catch administrado
En el siguiente escenario, es posible capturar excepciones Objective-C usando controladores administrados catch
.
- Se produce una excepción de Objective-C.
- El entorno de ejecución Objective-C recorre la pila (pero no la desenrolla), buscando un controlador nativo
@catch
que pueda gestionar la excepción. - El tiempo de ejecución de Objective-C no encuentra ningún
@catch
manejador, llama aNSGetUncaughtExceptionHandler
e invoca el manejador instalado por el SDK de .NET. - El controlador de los SDKs de .NET convertirá la excepción Objective-C en una excepción administrada y la lanzará. Dado que el entorno de ejecución de Objective-C no desenrolló la pila (solo lo recorrió), el marco de pila actual es el mismo que aquel en el que se lanzó la excepción Objective-C.
Se produce otro problema aquí, ya que el entorno de ejecución de Mono no sabe cómo desenrollar correctamente los marcos Objective-C.
Cuando se invoca la función de retorno en caso de excepción no detectada Objective-C en los SDK de .NET, la pila es como esta:
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]
Aquí, los únicos fotogramas administrados son los fotogramas 8-10, pero la excepción administrada se produce en el fotograma 0. Esto significa que el tiempo de ejecución de Mono debe desenredar los fotogramas nativos 0-7, lo que provoca un problema equivalente al problema descrito anteriormente: aunque el tiempo de ejecución de Mono desenredará los fotogramas nativos, no ejecutará ninguna Objective-C @catch
o @finally
cláusulas.
Ejemplo de código:
-(id) setObject: (id) object forKey: (id) key
{
@try {
if (key == nil)
[NSException raise: @"NSInvalidArgumentException"];
} @finally {
NSLog (@"This won't be executed");
}
}
Y la @finally
cláusula no se ejecutará porque el entorno de ejecución de Mono que desenreda este marco no lo sabe.
Una variación de esto es iniciar una excepción administrada en código administrado y, a continuación, desenredar a través de fotogramas nativos para llegar a la primera cláusula administrada 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.");
}
}
}
El método administrado UIApplication:Main
llamará al método nativo UIApplicationMain
, y luego iOS ejecutará mucho código nativo antes de eventualmente llamar al método administrado AppDelegate:FinishedLaunching
, con todavía muchos marcos nativos en la pila cuando se produzca la excepción 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: 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[])
Los fotogramas 0-1 y 27-30 están gestionados, mientras que todos los fotogramas entre ellos son nativos.
Si Mono se desenrolla a través de estos fotogramas, no se ejecutarán Objective-C @catch
ni @finally
cláusulas.
Importante
Solo el entorno de ejecución de MonoVM admite el desenrollamiento de fotogramas nativos durante el manejo de excepciones gestionadas. El entorno de ejecución de CoreCLR simplemente anulará el proceso al encontrar esta situación (el entorno de ejecución de CoreCLR se usa para aplicaciones macOS, así como cuando NativeAOT está habilitado en cualquier plataforma).
Escenario 2: no se pueden detectar excepciones de Objective-C
En el escenario siguiente, no es posible detectar excepciones de Objective-C mediante controladores administrados catch
porque la excepción Objective-C se controló de otra manera:
- Se produce una excepción de Objective-C.
- El Objective-C tiempo de ejecución recorre la pila (pero no la desenreda), buscando un controlador nativo
@catch
que pueda controlar la excepción. - El entorno de ejecución de Objective-C busca un
@catch
controlador, desenreda la pila y comienza a ejecutar el@catch
controlador.
Este escenario se encuentra normalmente en .NET para aplicaciones de iOS, ya que en el subproceso principal normalmente hay código similar al siguiente:
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 de Objective-C y, por tanto, nuestra devolución de llamada, que convierte las excepciones Objective-C en excepciones administradas, nunca se llama.
Esto también es habitual al depurar aplicaciones macOS en una versión anterior de macOS que la más reciente, 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. Al llamar a estos selectores, se producirá un NSInvalidArgumentException
("Selector no reconocido enviado a ..."), lo que finalmente hará que el proceso se bloquee.
En resumen, tener el entorno de ejecución de Objective-C o los fotogramas de desenredado mono que no están programados para controlar pueden provocar comportamientos indefinidos, como bloqueos, fugas de memoria y otros tipos de comportamientos impredecibles (incorrectos).
Sugerencia
Para las aplicaciones de macOS y Mac Catalyst (pero no para iOS ni tvOS), es posible que el ciclo de la interfaz de usuario no capture todas las excepciones al establecer el valor de la propiedad NSApplicationCrashOnExceptions
de la aplicación a true
:
var defs = new NSDictionary ((NSString) "NSApplicationCrashOnExceptions", NSValue.FromBoolean (true));
NSUserDefaults.StandardUserDefaults.RegisterDefaults (defs);
Sin embargo, tenga en cuenta que Apple no documenta esta propiedad, por lo que el comportamiento puede cambiar en el futuro.
Solución
Contamos con soporte tanto para detectar excepciones administradas como Objective-C en cualquier límite entre nativo y administrado, y para convertir esa excepción al otro tipo.
En pseudocódigo, tiene un aspecto similar al siguiente:
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"));
}
}
Se intercepta la instrucción P/Invoke a objc_msgSend
y se llama a este código en su lugar:
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 realiza algo similar para el caso inverso (manejo de excepciones administradas hacia excepciones Objective-C).
En .NET, el manejo de excepciones administradas a excepciones del tipo Objective-C está habilitado por defecto.
En la sección indicadores de tiempo de compilación se explica cómo deshabilitar la interceptación cuando es el valor por defecto.
Eventos
Hay dos eventos que se generan una vez que se intercepta una excepción: Runtime.MarshalManagedException y Runtime.MarshalObjectiveCException.
A ambos eventos se les pasa un objeto EventArgs
que contiene la excepción original que se produjo (la propiedad Exception
) y una propiedad ExceptionMode
para definir cómo se debe serializar la excepción.
La ExceptionMode
propiedad se puede cambiar en el controlador de eventos para cambiar el comportamiento según 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 ExceptionMode
propiedad se aplica al evento único, no afecta a ninguna excepción interceptada en el futuro.
Los siguientes modos están disponibles al transferir excepciones administradas a código nativo:
-
Default
: Actualmente, siempre esThrowObjectiveCException
. El valor predeterminado puede cambiar en el futuro. -
UnwindNativeCode
: Esto no está disponible cuando se utiliza CoreCLR (CoreCLR no soporta el desenrollado de código nativo, en su lugar, abortará el proceso). -
ThrowObjectiveCException
: Convertir la excepción administrada en una excepción de Objective-C y lanzar la excepción Objective-C. Este es el valor predeterminado en .NET. -
Abort
: anula el proceso. -
Disable
: deshabilita la interceptación de excepciones. No tiene sentido establecer este valor en el controlador de eventos (una vez que se genera el evento, es demasiado tarde para deshabilitar la interceptación de la excepción). En cualquier caso, si se establece, se comportará comoUnwindNativeCode
.
Los siguientes modos están disponibles al gestionar las excepciones Objective-C hacia el código gestionado:
-
Default
: Actualmente, siempre está enThrowManagedException
dentro de .NET. El valor predeterminado puede cambiar en el futuro. -
UnwindManagedCode
: este es el comportamiento anterior (indefinido). -
ThrowManagedException
: Convierta la excepción de Objective-C en una excepción administrada y lance la excepción administrada. Este es el valor predeterminado en .NET. -
Abort
: anula el proceso. -
Disable
: deshabilita la interceptación de excepciones. No tiene sentido establecer este valor en el controlador de eventos (una vez que se genera el evento, es demasiado tarde para deshabilitar la interceptación de la excepción). En cualquier caso, si se establece, se comportará comoUnwindManagedCode
.
Por lo tanto, para ver cada vez que se canaliza una excepción, puede hacerlo:
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);
};
/// ...
}
}
Sugerencia
Lo ideal es que las excepciones Objective-C no se produzcan en una aplicación bien comportada (Apple las considera mucho más excepcionales que las excepciones administradas: "evite iniciar excepciones [Objective-C] en una aplicación que envíe a los usuarios". Una manera de lograr esto sería agregar un controlador de eventos para el evento Runtime.MarshalObjectiveCException que registre todas las excepciones marcadas de Objective-C mediante telemetría (para configuraciones locales o de depuración, también puede establecer el modo de excepción en "Abortar") para detectar todas estas excepciones con el fin de corregirlas o evitarlas.
Banderas de Build-Time
Es posible establecer las siguientes propiedades de MSBuild, que determinarán si la interceptación de excepciones está habilitada y establecer la acción predeterminada que debe producirse:
- MarshalManagedExceptionMode: "default", "unwindnativecode", "throwobjectivecexception", "abort", "disable".
- MarshalObjectiveCExceptionMode: "default", "unwindmanagedcode", "throwmanagedexception", "abort", "disable".
Ejemplo:
<PropertyGroup>
<MarshalManagedExceptionMode>throwobjectivecexception</MarshalManagedExceptionMode>
<MarshalObjectiveCExceptionMode>throwmanagedexception</MarshalObjectiveCExceptionMode>
</PropertyGroup>
Excepto para disable
, estos valores son idénticos a los valores que se pasan a los ExceptionMode
eventos MarshalManagedException y MarshalObjectiveCException .
La disable
opción deshabilitará principalmente la interceptación, salvo que se interceptarán las excepciones cuando no agregue ninguna sobrecarga de ejecución. Los eventos de agregación se siguen generando para estas excepciones, y el modo predeterminado es el modo predeterminado para la plataforma en ejecución.
Limitaciones
Solo interceptamos P/Invokes al conjunto de funciones objc_msgSend
al intentar detectar excepciones Objective-C. Esto significa que un P/Invoke a otra función en C, que después lanza cualquier excepción Objective-C, seguirá encontrándose con el comportamiento antiguo e indefinido (esto puede mejorarse en el futuro).