Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
Управляемый код и 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 это еслиThrowObjectiveCExceptionGC находится в совместном режиме (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 это еслиThrowManagedExceptionGC находится в совместном режиме (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=defaultunwindnativecodethrowobjectivecexceptionabortdisable
--marshal-objectivec-exceptions=defaultunwindmanagedcodethrowmanagedexceptionabortdisable
disableКроме того, эти значения идентичны ExceptionMode значениям, передаваемым MarshalManagedException в события и MarshalObjectiveCException события.
Этот disable параметр будет в основном отключать перехват, за исключением того, что мы по-прежнему перехватим исключения, если они не добавляют никаких затрат на выполнение. События маршалинга по-прежнему создаются для этих исключений, при этом режим по умолчанию является режимом по умолчанию для исполняемой платформы.
Ограничения
При попытке перехвата Objective-C исключений выполняется перехват P/Invokes в objc_msgSend семейство функций. Это означает, что функция P/Invoke в другую функцию C, которая затем вызывает любые Objective-C исключения, по-прежнему будет выполняться в старом и неопределенном поведении (это может быть улучшено в будущем).