Marshaling de exceção no Xamarin.iOS e Xamarin.Mac
O código gerenciado e Objective-C têm suporte para exceções de runtime (cláusulas try/catch/finally).
No entanto, suas implementações são diferentes, o que significa que as bibliotecas de runtime (o runtime Mono ou CoreCLR e as Objective-C bibliotecas de runtime) têm problemas quando precisam lidar com exceções e, em seguida, executar código escrito em outras linguagens.
Este documento explica os problemas que podem ocorrer e as possíveis soluções.
Ele também inclui um projeto de exemplo, o Marshaling de Exceções, que pode ser usado para testar diferentes cenários e suas soluções.
Problema
O problema ocorre quando uma exceção é gerada e, durante o desenrolamento da pilha, um quadro é encontrado, o que não corresponde ao tipo de exceção que foi gerada.
Um exemplo típico desse problema é quando uma API nativa gera uma exceção Objective-C e, em seguida, essa Objective-C exceção deve de alguma forma ser tratada quando o processo de desenrolamento de pilha atinge um quadro gerenciado.
Para projetos herdados do Xamarin (pre-.NET), a ação padrão é não fazer nada.
Para o exemplo acima, isso significa deixar o Objective-C runtime desenrolar quadros gerenciados. Essa ação é problemática, pois o Objective-C runtime não sabe como desenrolar quadros gerenciados; por exemplo, não executará nenhuma catch
cláusula ou finally
nesse quadro.
Código quebrado
Considere o seguinte exemplo de código:
var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero);
Esse código gerará uma Objective-C NSInvalidArgumentException no código nativo:
NSInvalidArgumentException *** setObjectForKey: key cannot be nil
E o rastreamento de pilha será algo assim:
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 ()
Os quadros 0 a 3 são quadros nativos e o desenrolador de pilha no Objective-C runtime pode desenrolar esses quadros. Em particular, ele executará qualquer Objective-C@catch
cláusula ou @finally
.
No entanto, o Objective-C desenrolador de pilha não é capaz de desenrolar corretamente os quadros gerenciados (quadros 4-6): o Objective-C desenrolador de pilha desenrolará os quadros gerenciados, mas não executará nenhuma lógica de exceção gerenciada (como catch
ou 'finally clauses).
O que significa que geralmente não é possível capturar essas exceções da seguinte maneira:
try {
var dict = new NSMutableDictionary ();
dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
Console.WriteLine (ex);
} finally {
Console.WriteLine ("finally");
}
Isso ocorre porque o Objective-C desenrolador de pilha não sabe sobre a cláusula gerenciada catch
e nem a finally
cláusula será executada.
Quando o exemplo de código acima é eficaz, é porque Objective-C tem um método de ser notificado de exceções sem tratamento Objective-C , NSSetUncaughtExceptionHandler
, que o Xamarin.iOS e o Xamarin.Mac usam e, nesse ponto, tenta converter exceções Objective-C em exceções gerenciadas.
Cenários
Cenário 1 – capturando Objective-C exceções com um manipulador catch gerenciado
No cenário a seguir, é possível capturar Objective-C exceções usando manipuladores gerenciados catch
:
- Uma Objective-C exceção é gerada.
- O Objective-C runtime orienta a pilha (mas não a desenrola), procurando um manipulador nativo
@catch
que possa lidar com a exceção. - O Objective-C runtime não encontra manipuladores
@catch
, chamaNSGetUncaughtExceptionHandler
e invoca o manipulador instalado pelo Xamarin.iOS/Xamarin.Mac. - O manipulador do Xamarin.iOS/Xamarin.Mac converterá a Objective-C exceção em uma exceção gerenciada e a lançará. Como o Objective-C runtime não desenrola a pilha (apenas a andou), o quadro atual é o mesmo em que a Objective-C exceção foi lançada.
Outro problema ocorre aqui, porque o runtime mono não sabe como desenrolar Objective-C quadros corretamente.
Quando o retorno de chamada de exceção não capturada Objective-C do Xamarin.iOS é chamado, a pilha é assim:
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]
Aqui, os únicos quadros gerenciados são quadros de 8 a 10, mas a exceção gerenciada é gerada no quadro 0. Isso significa que o runtime mono deve desenrolar os quadros nativos de 0 a 7, o que causa um problema equivalente ao problema discutido acima: embora o runtime Mono descontraa os quadros nativos, ele não executará nenhuma Objective-C@catch
cláusula ou @finally
.
Exemplo de código:
-(id) setObject: (id) object forKey: (id) key
{
@try {
if (key == nil)
[NSException raise: @"NSInvalidArgumentException"];
} @finally {
NSLog (@"This won't be executed");
}
}
E a @finally
cláusula não será executada porque o runtime mono que desenrola esse quadro não sabe sobre ele.
Uma variação disso é lançar uma exceção gerenciada no código gerenciado e, em seguida, desenrolar por meio de quadros nativos para chegar à primeira cláusula gerenciada 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.");
}
}
}
O método gerenciado UIApplication:Main
chamará o método nativo UIApplicationMain
e, em seguida, o iOS fará muita execução de código nativo antes de eventualmente chamar o método gerenciado AppDelegate:FinishedLaunching
, com ainda muitos quadros nativos na pilha quando a exceção gerenciada for gerada:
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[])
Os quadros 0-1 e 27-30 são gerenciados, enquanto todos os quadros entre eles são nativos.
Se o Mono desenrolar esses quadros, nenhuma Objective-C@catch
cláusula ou @finally
será executada.
Cenário 2 – não é possível capturar Objective-C exceções
No cenário a seguir, não é possível capturar Objective-C exceções usando manipuladores gerenciados catch
porque a Objective-C exceção foi tratada de outra maneira:
- Uma Objective-C exceção é gerada.
- O Objective-C runtime orienta a pilha (mas não a desenrola), procurando um manipulador nativo
@catch
que possa lidar com a exceção. - O Objective-C runtime localiza um
@catch
manipulador, desenrola a pilha e começa a executar o@catch
manipulador.
Esse cenário geralmente é encontrado em aplicativos Xamarin.iOS, pois no thread main geralmente há um código como este:
void UIApplicationMain ()
{
@try {
while (true) {
ExecuteRunLoop ();
}
} @catch (NSException *ex) {
NSLog (@"An unhandled exception occured: %@", exc);
abort ();
}
}
Isso significa que, no thread main, nunca há realmente uma exceção sem tratamento Objective-C e, portanto, nosso retorno de chamada que converte Objective-C exceções em exceções gerenciadas nunca é chamado.
Isso também é comum ao depurar aplicativos Xamarin.Mac em uma versão anterior do macOS compatível com o Xamarin.Mac porque inspecionar a maioria dos objetos de interface do usuário no depurador tentará buscar propriedades que correspondam a seletores que não existem na plataforma em execução (porque o Xamarin.Mac inclui suporte para uma versão mais alta do macOS). Chamar esses seletores gerará um NSInvalidArgumentException
("Seletor não reconhecido enviado para ..."), o que eventualmente fará com que o processo falhe.
Para resumir, ter o Objective-C runtime ou os quadros de desenrolamento do runtime Mono que eles não estão programados para lidar podem levar a comportamentos indefinidos, como falhas, vazamentos de memória e outros tipos de comportamentos imprevisíveis (incorretos).
Solução
No Xamarin.iOS 10 e no Xamarin.Mac 2.10, adicionamos suporte para capturar exceções e Objective-C gerenciadas em qualquer limite nativo gerenciado e para converter essa exceção para o outro tipo.
No pseudocódigo, ele tem esta aparência:
[DllImport (Constants.ObjectiveCLibrary)]
static extern void objc_msgSend (IntPtr handle, IntPtr selector);
static void DoSomething (NSObject obj)
{
objc_msgSend (obj.Handle, Selector.GetHandle ("doSomething"));
}
O P/Invoke para objc_msgSend é interceptado e esse código é chamado em vez disso:
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 algo semelhante é feito para o caso inverso (realizar marshaling de exceções gerenciadas para Objective-C exceções).
Capturar exceções no limite nativo gerenciado não é gratuito, portanto, para projetos herdados do Xamarin (pre-.NET), ele nem sempre é habilitado por padrão:
- Xamarin.iOS/tvOS: a interceptação de Objective-C exceções está habilitada no simulador.
- Xamarin.watchOS: a interceptação é imposta em todos os casos, porque deixar o Objective-C runtime desenrolar quadros gerenciados confundirá o coletor de lixo e o fará travar ou falhar.
- Xamarin.Mac: a interceptação de Objective-C exceções está habilitada para builds de depuração.
No .NET, o marshaling de exceções gerenciadas para Objective-C exceções é sempre habilitado por padrão.
A seção Sinalizadores de tempo de build explica como habilitar a interceptação quando ela não está habilitada por padrão (ou desabilitar a interceptação quando for o padrão).
Eventos
Há dois eventos que são gerados depois que uma exceção é interceptada: Runtime.MarshalManagedException
e Runtime.MarshalObjectiveCException
.
Ambos os eventos são passados por um EventArgs
objeto que contém a exceção original que foi lançada (a Exception
propriedade ) e uma ExceptionMode
propriedade para definir como a exceção deve ser empacotada.
A ExceptionMode
propriedade pode ser alterada no manipulador de eventos para alterar o comportamento de acordo com qualquer processamento personalizado feito no manipulador. Um exemplo seria anular o processo se ocorrer uma determinada exceção.
A alteração da ExceptionMode
propriedade se aplica ao único evento, ela não afeta nenhuma exceção interceptada no futuro.
Os seguintes modos estão disponíveis ao realizar marshaling de exceções gerenciadas para código nativo:
-
Default
: o padrão varia de acordo com a plataforma. Ele está sempreThrowObjectiveCException
no .NET. Para projetos herdados doThrowObjectiveCException
Xamarin, é se o GC está no modo cooperativo (watchOS) eUnwindNativeCode
, caso contrário, (iOS/watchOS/macOS). O padrão pode ser alterado no futuro. -
UnwindNativeCode
: esse é o comportamento anterior (indefinido). Isso não está disponível ao usar o GC no modo cooperativo (que é a única opção no watchOS; portanto, essa não é uma opção válida no watchOS), nem ao usar o CoreCLR, mas é a opção padrão para todas as outras plataformas em projetos Xamarin herdados. -
ThrowObjectiveCException
: converta a exceção gerenciada em uma exceção Objective-C e gere a Objective-C exceção. Esse é o padrão no .NET e no watchOS em projetos herdados do Xamarin. -
Abort
: anula o processo. -
Disable
: desabilita a interceptação de exceção, portanto, não faz sentido definir esse valor no manipulador de eventos, mas depois que o evento é acionado, é tarde demais para desabilitá-lo. De qualquer forma, se definido, ele se comportará comoUnwindNativeCode
.
Os seguintes modos estão disponíveis ao realizar marshaling de Objective-C exceções para código gerenciado:
-
Default
: o padrão varia de acordo com a plataforma. Ele está sempreThrowManagedException
no .NET. Para projetos herdados doThrowManagedException
Xamarin, é se o GC está no modo cooperativo (watchOS) eUnwindManagedCode
, caso contrário, (iOS/tvOS/macOS). O padrão pode ser alterado no futuro. -
UnwindManagedCode
: esse é o comportamento anterior (indefinido). Isso não está disponível ao usar o GC no modo cooperativo (que é o único modo GC válido no watchOS; portanto, essa não é uma opção válida no watchOS), nem ao usar CoreCLR, mas é o padrão para todas as outras plataformas em projetos Xamarin herdados. -
ThrowManagedException
: converta a Objective-C exceção em uma exceção gerenciada e gere a exceção gerenciada. Esse é o padrão no .NET e no watchOS em projetos herdados do Xamarin. -
Abort
: anula o processo. -
Disable
: desabilita a interceptação de exceção, portanto, não faz sentido definir esse valor no manipulador de eventos, mas depois que o evento é acionado, é tarde demais para desabilitá-lo. De qualquer forma, se definido, ele anulará o processo.
Portanto, para ver sempre que uma exceção é empacotada, você pode fazer isso:
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);
};
Sinalizadores de Build-Time
É possível passar as seguintes opções para mtouch (para aplicativos Xamarin.iOS) e mmp (para aplicativos Xamarin.Mac), que determinarão se a interceptação de exceção está habilitada e definirá a ação padrão que deve ocorrer:
--marshal-managed-exceptions=
default
unwindnativecode
throwobjectivecexception
abort
disable
--marshal-objectivec-exceptions=
default
unwindmanagedcode
throwmanagedexception
abort
disable
Com exceção de disable
, esses valores são idênticos aos ExceptionMode
valores passados para os MarshalManagedException
eventos e MarshalObjectiveCException
.
A disable
opção desabilitará principalmente a interceptação, exceto que ainda interceptaremos exceções quando ela não adicionar nenhuma sobrecarga de execução. Os eventos de marshaling ainda são gerados para essas exceções, com o modo padrão sendo o modo padrão para a plataforma em execução.
Limitações
Interceptamos apenas P/Invokes para a objc_msgSend
família de funções ao tentar capturar Objective-C exceções. Isso significa que um P/Invoke para outra função C, que, em seguida, gera quaisquer Objective-C exceções, ainda encontrará o comportamento antigo e indefinido (isso pode ser melhorado no futuro).