138 строки
5.5 KiB
Markdown
138 строки
5.5 KiB
Markdown
History
|
|
====================
|
|
|
|
Historically we've attempted to convert Objective-C exceptions to managed exceptions
|
|
by adding a handler to be notified of uncaught Objective-C exceptions [1], but that one
|
|
major problem: it did not handle *caught* Objective-C exceptions, where the exception
|
|
handler would just abort.
|
|
|
|
This is common in iOS apps, where the main runloop adds an Objective-C @try/@catch
|
|
handler that just aborts, preventing us from being notified of the Objectice-C exception.
|
|
|
|
This approach also has another problem: the Objective-C exception handling mechanism
|
|
could unwind through managed code, which would result in all kinds of inexplicable
|
|
behavior (if there was an Objective-C exception handler that _didn't_ abort the app):
|
|
|
|
try {
|
|
doSomethingThatCausesAnObjectiveCException ();
|
|
} finally {
|
|
Console.WriteLine ("Done!");
|
|
}
|
|
|
|
If there happened to be an Objective-C exception handler in a stack frame further
|
|
up, the Console.WriteLine would never be executed.
|
|
|
|
Another frequent problem would occur in an IDE debugger if it tried to evaluate
|
|
every property for an object. If there were properties not supported on the
|
|
executing platform, it would throw an Objective-C exception (selector not found)
|
|
that would terminate the app.
|
|
|
|
The reverse problem also existed: throwing a managed exception that the native-to-managed
|
|
boundary didn't catch, would make the mono runtime unwind through native frames,
|
|
causing a similar set of inexplicable behavior:
|
|
|
|
[obj selectorThatManagedCodeHasOverriddenAndThrowAManagedException];
|
|
NSLog (@"done!");
|
|
|
|
The NSLog statement would never be executed.
|
|
|
|
[1] By using [NSSetUncaughtExceptionHandler](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/#//apple_ref/c/func/NSSetUncaughtExceptionHandler).
|
|
|
|
Exception marshaling
|
|
====================
|
|
|
|
There are two parts to exception marshaling: Objective-C exceptions
|
|
and managed exceptions.
|
|
|
|
In both cases we offer a managed event the app can subscribe to to
|
|
be notified, and then the app can choose what to do:
|
|
|
|
1. Convert the managed/Objective-C exception to its counterpart
|
|
(i.e. managed exceptions are converted to an Objective-C exception,
|
|
and Objective-C exceptions are converted to a managed exception).
|
|
|
|
2. Abort.
|
|
|
|
3. Rethrow the original exception (this is not available when
|
|
using the Coop GC, which is the only option on watchOS).
|
|
|
|
Managed exception marshaling
|
|
----------------------------
|
|
|
|
This is the simplest case, we just make sure to catch any managed
|
|
exceptions that reaches native code. There are a few sources:
|
|
|
|
1. When we call mono_runtime_invoke in our trampolines.
|
|
|
|
In this case mono_runtime_invoke will catch any exceptions
|
|
and return them to us (without unwinding anything else).
|
|
|
|
2. When managed code gives a delegate to native code, and
|
|
native code calls that delegate.
|
|
|
|
This is still a TODO, we need to use this new API to be
|
|
notified when the mono runtime detects this case:
|
|
https://github.com/mono/mono/pull/2948
|
|
|
|
3. When we call any managed code from our runtime. This occurs
|
|
quite frequently when Objective-C calls a selector with a
|
|
managed information.
|
|
|
|
In all cases we must handle exceptions properly - this is
|
|
done by having the managed delegates in our runtime catch
|
|
all exceptions, and returning a GCHandle with the caught
|
|
exception. Then the caller must check if any exceptions
|
|
were thrown, and decide how to handle it appropriately.
|
|
|
|
Objective-C exception marshaling
|
|
--------------------------------
|
|
|
|
This is more complicated, because there must be an Objective-C
|
|
exception handler on the managed-to-native boundary frame,
|
|
catching the Objective-C exception.
|
|
|
|
We have two approaches for this:
|
|
|
|
1. Custom wrappers of the objc_msgSend functions with an
|
|
Objective-C exception handler. These have to be written
|
|
in assembly code, since it's not possible to do it in C.
|
|
At runtime we inject a dllmap into the process, which
|
|
redirects any P/invoke to the objc_msgSend functions to
|
|
these custom wrappers.
|
|
|
|
Since we can't write assembly code for bitcode targets
|
|
(watchOS), another approach is still required though.
|
|
|
|
This is the used for iOS/tvOS/watchOS simulator builds
|
|
(both 32-bit and 64-bit) and Mac/64 bits. It does not
|
|
work on Mac/32 bits because that platform does not use
|
|
0-cost exceptions like the other platforms (it uses
|
|
setjmp/longjmp instead, and would require a very
|
|
different implementation in assembly code).
|
|
|
|
The assembly code for these wrappers is in the
|
|
trampolines-[arch]-objc_msgSend* files.
|
|
|
|
The documentation about 0-cost exceptions is scarce,
|
|
but here are a few documents which were helpful:
|
|
|
|
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Exceptions/Articles/Exceptions64Bit.html
|
|
https://sourceware.org/binutils/docs/as/CFI-directives.html
|
|
http://mentorembedded.github.io/cxx-abi/abi-eh.html
|
|
http://llvm.org/docs/ExceptionHandling.html
|
|
http://www.opensource.apple.com/source/llvmCore/llvmCore-3425.0.33/docs/ExceptionHandling.rst
|
|
http://stackoverflow.com/a/7535848
|
|
http://www.darlinghq.org/for-developers/exception-handling-on-os-x
|
|
|
|
The wrappers themselves were first created by writing
|
|
something similar in C, and then telling clang to
|
|
dump the corresponding assembly code (by passing
|
|
`--save-temps -fverbose-asm` to clang).
|
|
|
|
2. At build time generate custom wrappers of every P/Invoke
|
|
to the objc_msgSend functions, and modify the P/Invoke to
|
|
call these generated wrappers.
|
|
|
|
This is used for iOS/tvOS/watchOS device builds, and
|
|
Mac/32 bits.
|