Маршалирование исключений в 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 обработчиков:

  1. Objective-C Создается исключение.
  2. Среда Objective-C выполнения проходит стек (но не удаляет его), ищет собственный @catch обработчик, который может обрабатывать исключение.
  3. Среда Objective-C выполнения не находит @catch обработчиков, вызовов NSGetUncaughtExceptionHandlerи вызывает обработчик, установленный Xamarin.iOS/Xamarin.Mac.
  4. Обработчик 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 исключение было обработано другим способом:

  1. Objective-C Создается исключение.
  2. Среда Objective-C выполнения проходит стек (но не удаляет его), ищет собственный @catch обработчик, который может обрабатывать исключение.
  3. Среда 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 исключения, по-прежнему будет выполняться в старом и неопределенном поведении (это может быть улучшено в будущем).