Xamarin 中的异常封送处理Exception Marshaling in Xamarin.iOS

Xamarin 包含可帮助响应异常的新事件,特别是在本机代码中。Xamarin.iOS contains new events to help respond to exceptions, particularly in native code.

托管代码和目标-C 都支持运行时异常(try/catch/finally 子句)。Both managed code and Objective-C have support for runtime exceptions (try/catch/finally clauses).

但是,它们的实现是不同的,这意味着运行时库(Mono 运行时和目标 C 运行时库)必须处理异常,然后运行用其他语言编写的代码。However, their implementations are different, which means that the runtime libraries (the Mono runtime and the Objective-C runtime libraries) have problems when they have to handle exceptions and then run code written in other languages.

本文档介绍了可能出现的问题以及可能的解决方案。This document explains the problems that can occur, and the possible solutions.

它还包括一个可用于测试不同方案及其解决方案的示例项目,即异常封送处理It also includes a sample project, Exception Marshaling, which can be used to test different scenarios and their solutions.

问题Problem

如果引发异常,则会出现此问题,在堆栈展开过程中,遇到的帧与引发的异常的类型不匹配。The problem occurs when an exception is thrown, and during stack unwinding a frame is encountered which does not match the type of exception that was thrown.

用于 Xamarin 或 Xamarin 的典型示例是当本机 API 引发目标 C 异常时,如果堆栈展开进程到达托管帧,则必须以某种方式处理目标 C 异常。A typical example of this for Xamarin.iOS or Xamarin.Mac is when a native API throws an Objective-C exception, and then that Objective-C exception must somehow be handled when the stack unwinding process reaches a managed frame.

默认操作是不执行任何操作。The default action is to do nothing. 对于上述示例,这意味着允许目标-C 运行时展开托管的帧。For the sample above, this means letting the Objective-C runtime unwind managed frames. 这是有问题的,因为目标-C 运行时不知道如何展开托管帧;例如,它不会执行该帧中的任何 catchfinally 子句。This is problematic, because the Objective-C runtime does not know how to unwind managed frames; for example it won't execute any catch or finally clauses in that frame.

中断的代码Broken code

请考虑以下代码示例:Consider the following code example:

var dict = new NSMutableDictionary ();
dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero); 

这将在本机代码中引发 NSInvalidArgumentException:This will throw an Objective-C NSInvalidArgumentException in native code:

NSInvalidArgumentException *** setObjectForKey: key cannot be nil

堆栈跟踪将如下所示:And the stack trace will be something like this:

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 是本机帧,而目标-C 运行时中的 stack 展开器_可以_展开这些帧。Frames 0-3 are native frames, and the stack unwinder in the Objective-C runtime can unwind those frames. 具体而言,它将执行任何目标 @catch@finally 子句。In particular, it will execute any Objective-C @catch or @finally clauses.

但是,目标-C stack 展开器_不_能正确地展开托管帧(框架4-6),因为这些帧将展开,但不会执行托管异常逻辑。However, the Objective-C stack unwinder is not capable of properly unwinding the managed frames (frames 4-6), in that the frames will be unwound, but managed exception logic will not be executed.

这意味着通常不能通过以下方式捕获这些异常:Which means that it's usually not possible to catch these exceptions in the following manner:

try {
    var dict = new NSMutableDictionary ();
    dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
    Console.WriteLine (ex);
} finally {
    Console.WriteLine ("finally");
}

这是因为目标-C stack 展开器不知道托管的 catch 子句,也不会执行 finally 子句。This is because the Objective-C stack unwinder does not know about the managed catch clause, and neither will the finally clause be executed.

当上面的_代码示例生效时,_ 这是因为,客观-c 有一种方法收到未经处理的客观-C 异常, NSSetUncaughtExceptionHandler,其中的 Xamarin 和 xamarin 使用,此时尝试转换任何目标-c托管异常的异常。When the above code sample is effective, it is because Objective-C has a method of being notified of unhandled Objective-C exceptions, NSSetUncaughtExceptionHandler, which Xamarin.iOS and Xamarin.Mac use, and at that point tries to convert any Objective-C exceptions to managed exceptions.

方案Scenarios

方案 1-使用托管 catch 处理程序捕获目标为 C 的异常Scenario 1 - catching Objective-C exceptions with a managed catch handler

在下面的方案中,可以使用托管的 catch 处理程序来捕获目标 C 异常:In the following scenario, it is possible to catch Objective-C exceptions using managed catch handlers:

  1. 引发目标 C 异常。An Objective-C exception is thrown.
  2. 目标 C 运行时将遍历堆栈(但不展开堆栈),并查找可处理异常的本机 @catch 处理程序。The Objective-C runtime walks the stack (but does not unwind it), looking for a native @catch handler that can handle the exception.
  3. 目标-C 运行时找不到任何 @catch 处理程序,调用 NSGetUncaughtExceptionHandler,并调用 Xamarin/Xamarin 安装的处理程序。The Objective-C runtime doesn't find any @catch handlers, calls NSGetUncaughtExceptionHandler, and invokes the handler installed by Xamarin.iOS/Xamarin.Mac.
  4. Xamarin/Xamarin. Mac 的处理程序会将目标 C 异常转换为托管异常,并引发异常。Xamarin.iOS/Xamarin.Mac's handler will convert the Objective-C exception into a managed exception, and throw it. 由于目标-C 运行时未展开堆栈(仅遍历堆栈),因此当前帧与引发目标 C 异常的帧相同。Since the Objective-C runtime didn't unwind the stack (only walked it), the current frame is the same one where the Objective-C exception was thrown.

这里出现了另一个问题,因为 Mono 运行时不知道如何正确展开目标 C 帧。Another problem occurs here, because the Mono runtime does not know how to unwind Objective-C frames properly.

当已不捕获 Xamarin 的目标-C 异常回调时,堆栈如下所示:When Xamarin.iOS' uncaught Objective-C exception callback is called, the stack is like this:

 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中引发。Here, the only managed frames are frames 8-10, but the managed exception is thrown in frame 0. 这意味着 Mono 运行时必须展开本机帧0-7,这会导致与上述问题等效的问题:尽管 Mono 运行时将展开本机帧,但不会执行任何 @catch@finally 子句。This means that the Mono runtime must unwind the native frames 0-7, which causes a problem equivalent to the problem discussed above: although the Mono runtime will unwind the native frames, it won't execute any Objective-C @catch or @finally clauses.

代码示例:Code example:

-(id) setObject: (id) object forKey: (id) key
{
    @try {
        if (key == nil)
            [NSException raise: @"NSInvalidArgumentException"];
    } @finally {
        NSLog (@"This will not be executed");
    }
}

不会执行 @finally 子句,因为展开此帧的 Mono 运行时并不知道它。And the @finally clause will not be executed because the Mono runtime that unwinds this frame does not know about it.

这种情况的一种变化是在托管代码中引发托管异常,然后在本机帧中展开以转到第一个托管的 catch 子句:A variation of this is to throw a managed exception in managed code, and then unwinding through native frames to get to the first managed catch clause:

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.");
        }
    }
}

托管的 UIApplication:Main 方法将调用本机 UIApplicationMain 方法,然后在最终调用托管的 AppDelegate:FinishedLaunching 方法之前,iOS 将执行大量的本机代码,在引发托管异常时,堆栈上仍有大量的本机帧:The managed UIApplication:Main method will call the native UIApplicationMain method, and then iOS will do a lot of native code execution before eventually calling the managed AppDelegate:FinishedLaunching method, with still a lot of native frames on the stack when the managed exception is thrown:

 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 是托管的,而它们之间的所有这些都是本机。Frames 0-1 and 27-30 are managed, while all those in between are native. 如果通过这些帧展开 Mono,则不会执行目标为 C 的 @catch@finally 子句。If Mono unwinds through these frames, no Objective-C @catch or @finally clauses will be executed.

方案 2-无法捕捉目标 C 异常Scenario 2 - not able to catch Objective-C exceptions

在下面的方案中,_无法_使用托管的 catch 处理程序捕获 o c o o o o o o o o o o o o o o o o o o o o o o o o o o 异常In the following scenario, it is not possible to catch Objective-C exceptions using managed catch handlers because the Objective-C exception was handled in another way:

  1. 引发目标 C 异常。An Objective-C exception is thrown.
  2. 目标 C 运行时将遍历堆栈(但不展开堆栈),并查找可处理异常的本机 @catch 处理程序。The Objective-C runtime walks the stack (but does not unwind it), looking for a native @catch handler that can handle the exception.
  3. 目标 C 运行时查找 @catch 处理程序,展开堆栈,并开始执行 @catch 处理程序。The Objective-C runtime finds a @catch handler, unwinds the stack, and starts executing the @catch handler.

这种情况通常出现在 Xamarin iOS 应用程序中,因为在主线程上通常会出现如下代码:This scenario is commonly found in Xamarin.iOS apps, because on the main thread there is usually code like this:

void UIApplicationMain ()
{
    @try {
        while (true) {
            ExecuteRunLoop ();
        }
    } @catch (NSException *ex) {
        NSLog (@"An unhandled exception occured: %@", exc);
        abort ();
    }
}

这意味着,在主线程上绝不会真正导致未处理的目标 C 异常,因此永远不会调用将 o o o o o o o o o o o C o o o o o o o o o o o oThis means that on the main thread there's never really an unhandled Objective-C exception, and thus our callback that converts Objective-C exceptions to managed exceptions is never called.

在早期 macOS 版本上调试 Xamarin 应用程序时,这一点也很常见,因为在早期版本上,由于检查调试器中的大部分 UI 对象将尝试获取与执行平台上不存在的选择器相对应的属性(因为 Xamarin 支持更高的 macOS 版本)。This is also quite common when debugging Xamarin.Mac apps on an earlier macOS version than Xamarin.Mac supports because inspecting most UI objects in the debugger will try to fetch properties that correspond to selectors that don't exist on the executing platform (because Xamarin.Mac includes support for a higher macOS version). 调用此类选择器将引发 NSInvalidArgumentException ("发送到 ..." 的未识别选择器),这最终会导致进程崩溃。Calling such selectors will throw an NSInvalidArgumentException ("Unrecognized selector sent to ..."), which eventually causes the process to crash.

总而言之,使目标 C 运行时或 Mono 运行时展开帧无法进行处理可能导致未定义的行为,例如,崩溃、内存泄漏和其他类型的不可预测(mis)行为。To summarize, having either the Objective-C runtime or the Mono runtime unwind frames that they are not programmed to handle can lead to undefined behaviors, such as crashes, memory leaks, and other types of unpredictable (mis)behaviors.

## 解决方案Solution

在 Xamarin 10 和 Xamarin. Mac 2.10 中,我们添加了对任何托管本机边界上都捕获托管和目标-C 异常的支持,并添加了用于将该异常转换为其他类型的支持。In Xamarin.iOS 10 and Xamarin.Mac 2.10, we've added support for catching both managed and Objective-C exceptions on any managed-native boundary, and for converting that exception to the other type.

在伪代码中,它如下所示:In pseudo-code, it looks something like this:

[DllImport ("libobjc.dylib")]
static extern void objc_msgSend (IntPtr handle, IntPtr selector);

static void DoSomething (NSObject obj)
{
    objc_msgSend (obj.Handle, Selector.GetHandle ("doSomething"));
}

Objc_msgSend 的 P/Invoke 被截获,而这称为:The P/Invoke to objc_msgSend is intercepted, and this is called instead:

void
xamarin_dyn_objc_msgSend (id obj, SEL sel)
{
    @try {
        objc_msgSend (obj, sel);
    } @catch (NSException *ex) {
        convert_to_and_throw_managed_exception (ex);
    }
}

对于反向,还会执行类似操作(将托管异常封送到目标-C 异常)。And something similar is done for the reverse case (marshaling managed exceptions to Objective-C exceptions).

托管本机边界上的捕获异常并不收费,因此默认情况下不会始终启用:Catching exceptions on the managed-native boundary is not cost-free, so it's not always enabled by default:

  • Xamarin/tvOS:在模拟器中启用了目标-C 例外。Xamarin.iOS/tvOS: interception of Objective-C exceptions is enabled in the simulator.
  • 在所有情况下都强制使用 watchOS:截获,因为允许目标-C 运行时展开托管帧会使垃圾回收器混乱,并使其挂起或崩溃。Xamarin.watchOS: interception is enforced in all cases, because letting the Objective-C runtime unwind managed frames will confuse the garbage collector, and either make it hang or crash.
  • Xamarin:针对调试版本启用了目标-C 例外。Xamarin.Mac: interception of Objective-C exceptions is enabled for debug builds.

"生成时标志" 部分说明了如何在默认情况下启用拦截。The Build-time flags section explains how to enable interception when it's not enabled by default.

事件Events

截获异常后,将引发两个新事件: Runtime.MarshalManagedExceptionRuntime.MarshalObjectiveCExceptionThere are two new events that are raised once an exception is intercepted: Runtime.MarshalManagedException and Runtime.MarshalObjectiveCException.

这两个事件均被传递 EventArgs 对象,该对象包含引发的原始异常(Exception 属性)和一个 ExceptionMode 属性,用于定义应如何封送异常。Both events are passed an EventArgs object that contains the original exception that was thrown (the Exception property), and an ExceptionMode property to define how the exception should be marshaled.

可以在事件处理程序中更改 ExceptionMode 属性,以根据处理程序中执行的任何自定义处理更改行为。The ExceptionMode property can be changed in the event handler to change the behavior according to any custom processing done in the handler. 一个示例是在发生特定异常时中止进程。One example would be to abort the process if a certain exception occurs.

更改 ExceptionMode 属性适用于单个事件,而不会影响将来截获的任何异常。Changing the ExceptionMode property applies to the single event, it does not affect any exceptions intercepted in the future.

可用的模式如下:The following modes are available:

  • Default:默认值因平台而异。Default: The default varies by platform. 如果 GC 处于协作模式(watchOS),则 ThrowObjectiveCException; 否则 UnwindNativeCode (iOS/watchOS/macOS)。It is ThrowObjectiveCException if the GC is in cooperative mode (watchOS), and UnwindNativeCode otherwise (iOS / watchOS / macOS). 将来可能会更改默认值。The default may change in the future.
  • UnwindNativeCode:这是以前的(未定义)行为。UnwindNativeCode: This is the previous (undefined) behavior. 如果在协作模式(这是 watchOS 上的唯一选项,这是 watchOS 上的唯一选项)中使用 GC,则此选项不可用,但它是所有其他平台的默认选项。This is not available when using the GC in cooperative mode (which is the only option on watchOS; thus, this is not a valid option on watchOS), but it's the default option for all other platforms.
  • ThrowObjectiveCException:将托管异常转换为目标 C 异常,并引发目标 C 异常。ThrowObjectiveCException: Convert the managed exception into an Objective-C exception and throw the Objective-C exception. 这是 watchOS 上的默认值。This is the default on watchOS.
  • Abort:中止进程。Abort: Abort the process.
  • Disable:禁用异常截获,因此在事件处理程序中设置此值并不合理,但一旦引发事件,则无法将其禁用。Disable: Disables the exception interception, so it doesn't make sense to set this value in the event handler, but once the event is raised it's too late to disable it. 在任何情况下,如果设置,它将表现为 UnwindNativeCodeIn any case, if set, it will behave as UnwindNativeCode.

对于向托管代码封送目标为 C 的异常,可以使用以下模式:For marshaling Objective-C exceptions to managed code, the following modes are available:

  • Default:默认值因平台而异。Default: The default varies by platform. 如果 GC 处于协作模式(watchOS),则 ThrowManagedException; 否则 UnwindManagedCode (iOS/tvOS/macOS)。It is ThrowManagedException if the GC is in cooperative mode (watchOS), and UnwindManagedCode otherwise (iOS / tvOS / macOS). 将来可能会更改默认值。The default may change in the future.
  • UnwindManagedCode:这是以前的(未定义)行为。UnwindManagedCode: This is the previous (undefined) behavior. 如果在协作模式(这是 watchOS 上的唯一有效 GC 模式,这是 watchOS 上的无效选项)中使用 GC,则此选项不可用,但它是所有其他平台的默认选项。This is not available when using the GC in cooperative mode (which is the only valid GC mode on watchOS; thus this is not a valid option on watchOS), but it's the default for all other platforms.
  • ThrowManagedException:将目标为 C 的异常转换为托管异常,并引发托管异常。ThrowManagedException: Convert the Objective-C exception to a managed exception and throw the managed exception. 这是 watchOS 上的默认值。This is the default on watchOS.
  • Abort:中止进程。Abort: Abort the process.
  • Disable:D isables 异常截获,因此在事件处理程序中设置此值并不合理,但一旦引发事件,则无法将其禁用。Disable:Disables the exception interception, so it doesn't make sense to set this value in the event handler, but once the event is raised, it's too late to disable it. 在任何情况下,如果设置,它将中止进程。In any case if set, it will abort the process.

因此,若要查看每次封送异常,可以执行以下操作:So, to see every time an exception is marshaled, you can do this:

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);
};

生成时标志Build-Time Flags

可以将以下选项传递给mtouch (适用于 xamarin iOS 应用)和mmp (适用于 xamarin),这会确定是否启用了异常拦截,并设置了应发生的默认操作:It's possible to pass the following options to mtouch (for Xamarin.iOS apps) and mmp (for Xamarin.Mac apps), which will determine if exception interception is enabled, and set the default action that should occur:

  • --marshal-managed-exceptions=

    • default
    • unwindnativecode
    • throwobjectivecexception
    • abort
    • disable
  • --marshal-objectivec-exceptions=

    • default
    • unwindmanagedcode
    • throwmanagedexception
    • abort
    • disable

除了 disable之外,这些值与传递到 MarshalManagedExceptionMarshalObjectiveCException 事件的 ExceptionMode 值相同。Except for disable, these values are identical to the ExceptionMode values that are passed to the MarshalManagedException and MarshalObjectiveCException events.

disable 选项将_主要_禁用截获,但当不添加任何执行开销时,我们仍会截获异常。The disable option will mostly disable interception, except we'll still intercept exceptions when it does not add any execution overhead. 对于这些异常,仍会引发封送处理事件,默认模式为执行平台的默认模式。The marshaling events are still raised for these exceptions, with the default mode being the default mode for the executing platform.

限制Limitations

在尝试捕获目标 C 异常时,只会截获对 objc_msgSend 系列函数的 P/Invoke。We only intercept P/Invokes to the objc_msgSend family of functions when trying to catch Objective-C exceptions. 这意味着,对于另一个 C 函数的 P/Invoke,后者随后会引发任何目标为 C 的异常,仍将会遇到旧的和未定义的行为(将来可能会改进)。This means that a P/Invoke to another C function, which then throws any Objective-C exceptions, will still run into the old and undefined behavior (this may be improved in the future).