Маршалирование исключений в Xamarin.iOS и Xamarin.Mac
Управляемый код и Objective-C поддерживают исключения среды выполнения (предложения try/catch/finally).
Однако их реализации отличаются, что означает, что библиотеки среды выполнения (среда выполнения Mono или CoreCLR и Objective-C библиотеки среды выполнения) имеют проблемы, когда они должны обрабатывать исключения, а затем запускать код, написанный на других языках.
В этом документе описываются проблемы, которые могут возникнуть, и возможные решения.
Он также включает пример проекта, маршалинг исключений, который можно использовать для тестирования различных сценариев и их решений.
Проблема
Проблема возникает при возникновении исключения и во время очистки кадра стека, который не соответствует типу создаваемого исключения.
Типичный пример этой проблемы заключается в том, что собственный API создает Objective-C исключение, а затем это Objective-C исключение должно как-то обрабатываться, когда процесс очистки стека достигает управляемого кадра.
Для устаревших проектов Xamarin (pre-.NET), действие по умолчанию не выполняется.
В приведенном выше примере это означает, что среда выполнения отменяет Objective-C управляемые кадры. Это действие проблематично, так как Objective-C среда выполнения не знает, как отменить управляемые кадры. Например, она не будет выполнять какие-либо finally
catch
или предложения в этом кадре.
Сломанный код
Рассмотрим следующий пример кода.
var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero);
Этот код вызовет Objective-C NSInvalidArgumentException в машинном коде:
NSInvalidArgumentException *** setObjectForKey: key cannot be nil
И трассировка стека будет примерно такой:
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 ()
Кадры 0-3 — это собственные кадры, а стек распаковки в Objective-C среде выполнения может снять эти кадры. В частности, он будет выполнять любые Objective-C@catch
или @finally
предложения.
Тем не менее, Objective-C стек очистки не способен правильно расположить управляемые кадры (кадры 4-6): Objective-C стек очистки будет отключает управляемые кадры, но не будет выполнять логику управляемых исключений (например catch
, или "предложения, наконец).
Это означает, что обычно невозможно поймать эти исключения следующим образом:
try {
var dict = new NSMutableDictionary ();
dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
Console.WriteLine (ex);
} finally {
Console.WriteLine ("finally");
}
Это связано с тем, что Objective-C стек очистки не знает об управляемом catch
предложении, и ни при finally
этом предложение не будет выполняться.
Если приведенный выше пример кода эффективен, он связан с тем, что Objective-C имеет метод уведомления об необработанных Objective-C исключениях, NSSetUncaughtExceptionHandler
которые используются Xamarin.iOS и Xamarin.Mac, и на этом этапе пытается преобразовать все Objective-C исключения в управляемые исключения.
Сценарии
Сценарий 1. Перехват Objective-C исключений с помощью управляемого обработчика перехвата
В следующем сценарии можно перехватывать Objective-C исключения с помощью управляемых catch
обработчиков:
- Objective-C Создается исключение.
- Среда Objective-C выполнения проходит стек (но не удаляет его), ищет собственный
@catch
обработчик, который может обрабатывать исключение. - Среда Objective-C выполнения не находит
@catch
обработчиков, вызововNSGetUncaughtExceptionHandler
и вызывает обработчик, установленный Xamarin.iOS/Xamarin.Mac. - Обработчик Xamarin.iOS/Xamarin.Mac преобразует Objective-C исключение в управляемое исключение и выдает его. Objective-C Так как среда выполнения не распаковывает стек (только пошаговая), текущий кадр совпадает с тем, где Objective-C было создано исключение.
Другая проблема возникает здесь, так как среда выполнения Mono не знает, как правильно снять Objective-C кадры.
При вызове обратного Objective-C вызова исключения Xamarin.iOS стек выглядит следующим образом:
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]
Здесь единственными управляемыми кадрами являются кадры 8-10, но управляемое исключение создается в кадре 0. Это означает, что среда выполнения Mono должна отключать собственные кадры 0-7, что приводит к проблеме, эквивалентной приведенной выше проблеме: хотя среда выполнения Mono развернет собственные кадры, она не будет выполнять какие-либо Objective-C@catch
предложения или @finally
предложения.
Пример кода:
-(id) setObject: (id) object forKey: (id) key
{
@try {
if (key == nil)
[NSException raise: @"NSInvalidArgumentException"];
} @finally {
NSLog (@"This won't be executed");
}
}
@finally
И предложение не будет выполнено, так как среда выполнения Mono, которая раскручивает этот кадр, не знает об этом.
Вариантом этого является создание управляемого исключения в управляемом коде, а затем очистка с помощью собственных кадров для получения первого управляемого 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.");
}
}
}
Управляемый метод вызовет собственный UIApplicationMain
метод, а затем iOS выполнит много выполнения машинного кода, прежде чем в конечном итоге вызывать управляемый UIApplication:Main
метод, при возникновении управляемого AppDelegate:FinishedLaunching
исключения все еще много собственных кадров в стеке:
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[])
Кадры 0-1 и 27-30 управляются, а все кадры между ними являются собственными.
Если Mono отключает эти кадры, никакие Objective-C@catch
или @finally
предложения не будут выполняться.
Сценарий 2. Не удается перехватывать Objective-C исключения
В следующем сценарии невозможно перехватывать Objective-C исключения с помощью управляемых catch
обработчиков, так как Objective-C исключение было обработано другим способом:
- Objective-C Создается исключение.
- Среда Objective-C выполнения проходит стек (но не удаляет его), ищет собственный
@catch
обработчик, который может обрабатывать исключение. - Среда Objective-C выполнения находит
@catch
обработчик, распаковывает стек и запускает выполнение обработчика@catch
.
Этот сценарий обычно встречается в приложениях Xamarin.iOS, так как в основном потоке обычно код выглядит следующим образом:
void UIApplicationMain ()
{
@try {
while (true) {
ExecuteRunLoop ();
}
} @catch (NSException *ex) {
NSLog (@"An unhandled exception occured: %@", exc);
abort ();
}
}
Это означает, что в основном потоке никогда не существует необработанного Objective-C исключения, поэтому обратный вызов, который преобразует Objective-C исключения в управляемые исключения, никогда не вызывается.
Это также часто происходит при отладке приложений Xamarin.Mac на более ранней версии macOS, чем Xamarin.Mac, так как проверка большинства объектов пользовательского интерфейса в отладчике попытается получить свойства, соответствующие селекторам, которые не существуют на исполняемой платформе (так как Xamarin.Mac включает поддержку более высокой версии macOS). Вызов таких селекторов вызовет NSInvalidArgumentException
исключение ("Нераспознанный селектор, отправленный в ..." ), что в конечном итоге приводит к сбою процесса.
Чтобы свести к сведению, если Objective-C среда выполнения или среда выполнения Mono отменяют кадры, которые они не запрограммированы для обработки, могут привести к неопределенным поведению, таким как сбои, утечки памяти и другие типы непредсказуемых (неправильно)поведения.
Решение
В Xamarin.iOS 10 и Xamarin.Mac 2.10 мы добавили поддержку перехвата управляемых и Objective-C исключений на любой управляемой границе, а также для преобразования этого исключения в другой тип.
В псевдокоде выглядит примерно так:
[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 to objc_msgSend перехватывается, и вместо этого вызывается следующий код:
void
xamarin_dyn_objc_msgSend (id obj, SEL sel)
{
@try {
objc_msgSend (obj, sel);
} @catch (NSException *ex) {
convert_to_and_throw_managed_exception (ex);
}
}
И что-то подобное выполняется для обратного регистра (маршалинг управляемых исключений в Objective-C исключения).
Перехват исключений на границе управляемого собственного кода не является дорогостоящим, поэтому для устаревших проектов Xamarin (pre-.NET), он не всегда включен по умолчанию:
- Xamarin.iOS/tvOS: перехват исключений Objective-C включен в симуляторе.
- Xamarin.watchOS: перехват применяется во всех случаях, так как позволить Objective-C среде выполнения отменить управляемые кадры, будут путать сборщик мусора, и либо сделать его зависанием или сбоем.
- Xamarin.Mac: перехват исключений Objective-C включен для отладочных сборок.
В .NET маршалинг управляемых исключений Objective-C в исключения всегда включен по умолчанию.
В разделе "Флаги времени сборки " объясняется, как включить перехват, если он не включен по умолчанию (или отключить перехват при использовании по умолчанию).
События
После перехвата исключения возникают два события: Runtime.MarshalManagedException
и Runtime.MarshalObjectiveCException
.
Оба события передают EventArgs
объект, содержащий исходное исключение, которое было создано ( Exception
свойство), и ExceptionMode
свойство, определяющее, как следует маршалировать исключение.
Свойство ExceptionMode
можно изменить в обработчике событий, чтобы изменить поведение в соответствии с любой пользовательской обработкой, выполняемой в обработчике. Одним из примеров будет прерывание процесса при возникновении определенного исключения.
ExceptionMode
Изменение свойства применяется к одному событию, оно не влияет на какие-либо исключения, перехватанные в будущем.
При маршалинге управляемых исключений в машинный код доступны следующие режимы:
Default
: значение по умолчанию зависит от платформы. Он всегдаThrowObjectiveCException
находится в .NET. Для устаревших проектов Xamarin это еслиThrowObjectiveCException
GC находится в совместном режиме (watchOS) иUnwindNativeCode
в противном случае (iOS/ watchOS / macOS). Значение по умолчанию может измениться в будущем.UnwindNativeCode
: это предыдущее (неопределенное) поведение. Это недоступно при использовании GC в кооперативном режиме (который является единственным вариантом в watchOS; таким образом, это не является допустимым вариантом в watchOS), а также при использовании CoreCLR, но это параметр по умолчанию для всех других платформ в устаревших проектах Xamarin.ThrowObjectiveCException
: преобразуйте управляемое Objective-C исключение в исключение и создайте Objective-C исключение. Это значение по умолчанию в .NET и watchOS в устаревших проектах Xamarin.Abort
: прерывание процесса.Disable
: отключает перехват исключений, поэтому не имеет смысла задать это значение в обработчике событий, но когда событие вызывается, оно слишком поздно, чтобы отключить его. В любом случае, если задано, он будет вести себя какUnwindNativeCode
.
При маршалинге Objective-C исключений в управляемый код доступны следующие режимы:
Default
: значение по умолчанию зависит от платформы. Он всегдаThrowManagedException
находится в .NET. Для устаревших проектов Xamarin это еслиThrowManagedException
GC находится в совместном режиме (watchOS) иUnwindManagedCode
в противном случае (iOS/ tvOS / macOS). Значение по умолчанию может измениться в будущем.UnwindManagedCode
: это предыдущее (неопределенное) поведение. Это недоступно при использовании GC в кооперативном режиме (который является единственным допустимым режимом GC в watchOS; таким образом, это не является допустимым вариантом в watchOS), а также при использовании CoreCLR, но это по умолчанию для всех других платформ в устаревших проектах Xamarin.ThrowManagedException
: преобразуйте исключение в Objective-C управляемое исключение и создайте управляемое исключение. Это значение по умолчанию в .NET и watchOS в устаревших проектах Xamarin.Abort
: прерывание процесса.Disable
: отключает перехват исключений, поэтому не имеет смысла задать это значение в обработчике событий, но после того, как событие вызывается, это слишком поздно, чтобы отключить его. В любом случае, если задано, процесс прерывается.
Таким образом, чтобы каждый раз маршалировать исключение, можно сделать следующее:
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);
};
Флаги времени сборки
Можно передать следующие параметры mtouch (для приложений Xamarin.iOS) и mmp (для приложений Xamarin.Mac), которые будут определять, включен ли перехват исключений, и задать действие по умолчанию, которое должно произойти:
--marshal-managed-exceptions=
default
unwindnativecode
throwobjectivecexception
abort
disable
--marshal-objectivec-exceptions=
default
unwindmanagedcode
throwmanagedexception
abort
disable
disable
Кроме того, эти значения идентичны ExceptionMode
значениям, передаваемым MarshalManagedException
в события и MarshalObjectiveCException
события.
Этот disable
параметр будет в основном отключать перехват, за исключением того, что мы по-прежнему перехватим исключения, если они не добавляют никаких затрат на выполнение. События маршалинга по-прежнему создаются для этих исключений, при этом режим по умолчанию является режимом по умолчанию для исполняемой платформы.
Ограничения
При попытке перехвата Objective-C исключений выполняется перехват P/Invokes в objc_msgSend
семейство функций. Это означает, что функция P/Invoke в другую функцию C, которая затем вызывает любые Objective-C исключения, по-прежнему будет выполняться в старом и неопределенном поведении (это может быть улучшено в будущем).