|
| 1 | +# Exception Handling |
| 2 | + |
| 3 | +Outside of Xamarin.Android, .NET exception handling |
| 4 | +[when a debugger is attached][0] potentially involves walking the runtime stack |
| 5 | +*twice*, involving three interactions between the runtime and the debugger: |
| 6 | + |
| 7 | + 1. When the exception is first thrown, a *first chance notification* is raised |
| 8 | + in the debugger, which provides the debugger with an opportunity to handle |
| 9 | + breakpoint or single-step exceptions. |
| 10 | + |
| 11 | + 2. If the debugger doesn't handle or continues execution from the first chance |
| 12 | + notification, then: |
| 13 | + |
| 14 | + a. The runtime will attempt to "find a frame-based exception handler that |
| 15 | + handles the exception". |
| 16 | + |
| 17 | + b. If no frame-based exception handler is found, then a |
| 18 | + *last-chance notification* is raised in the debugger. |
| 19 | + |
| 20 | + 3. If the debugger doesn't handle the last chance notification, then execution |
| 21 | + will continue, causing the stack to be unwound. |
| 22 | + |
| 23 | +The first stack walk is step 2(a), while the second stack walk is step (3). |
| 24 | + |
| 25 | +Within Xamarin.Android, if a thread call-stack doesn't involve any calls to |
| 26 | +or from Java code, the same semantics are present. |
| 27 | + |
| 28 | +When a thread call-stack involves calls to or from Java code, the above |
| 29 | +"two-pass" semantics cannot be supported, as the Java Native Interface, which |
| 30 | +is used to support calls to or from Java code, does not support them. |
| 31 | +A cross-VM runtime stack can only be walked *while being unwound*; there is |
| 32 | +no way to ask "is there any method which will handle this exception" before |
| 33 | +code is executed and the stack is unwound. |
| 34 | + |
| 35 | + |
| 36 | +## The Setup |
| 37 | + |
| 38 | +Consider the following Java code: |
| 39 | + |
| 40 | +```java |
| 41 | +// Java |
| 42 | +public class Demo { |
| 43 | + public static void run(Runnable r) { |
| 44 | + /* setup code */ |
| 45 | + try { |
| 46 | + r.run(); |
| 47 | + } finally { |
| 48 | + /* cleanup code */ |
| 49 | + System.out.println("Demo.run() finally block!"); |
| 50 | + } |
| 51 | + } |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +`Demo.run()` is bound as: |
| 56 | + |
| 57 | +```csharp |
| 58 | +partial class Demo { |
| 59 | + public static unsafe void Run (global::Java.Lang.IRunnable p0) |
| 60 | + { |
| 61 | + const string __id = "run.(Ljava/lang/Runnable;)V"; |
| 62 | + JniArgumentValue* __args = stackalloc JniArgumentValue [1]; |
| 63 | + __args [0] = new JniArgumentValue ((p0 == null) ? IntPtr.Zero : ((global::Java.Lang.Object) p0).Handle); |
| 64 | + _members.StaticMethods.InvokeVoidMethod (__id, __args); |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +``` |
| 69 | + |
| 70 | +Now imagine the above Java class has been bound and is used from a |
| 71 | +Xamarin.Android app: |
| 72 | + |
| 73 | +```csharp |
| 74 | +Action a = () => { |
| 75 | + throw new Exception ("Hm…"); |
| 76 | +}; |
| 77 | +Demo.Run(new Java.Lang.Runnable(a)); |
| 78 | +``` |
| 79 | + |
| 80 | + |
| 81 | +## Exception Handling *Without* A Debugger |
| 82 | + |
| 83 | +When a debugger is *not* attached, the following happens: |
| 84 | + |
| 85 | + 1. `Java.Lang.Runnable` has a Java Callable Wrapper generated at app |
| 86 | + build time, `mono.java.lang.Runnable`, which implements the |
| 87 | + `java.lang.Runnable` Java interface type. |
| 88 | + |
| 89 | + 2. When the `Java.Lang.Runnable` is created, a `mono.java.lang.Runnable` |
| 90 | + instance is also created, and the two instances are associated with each other. |
| 91 | + |
| 92 | + 3. The `Demo.Run()` invocation invokes the Java `Demo.run()` method, passing |
| 93 | + along the `mono.java.lang.Runnable` instance created in (2). |
| 94 | + |
| 95 | + 4. The `r.run()` invocation within `Demo.run()` eventually invokes the method |
| 96 | + `Java.Lang.IRunnableInvoker.n_Run()`: |
| 97 | + |
| 98 | + ```csharp |
| 99 | + static void n_Run (IntPtr jnienv, IntPtr native__this) |
| 100 | + { |
| 101 | + var __this = global::Java.Lang.Object.GetObject<Java.Lang.IRunnable> (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; |
| 102 | + __this.Run (); |
| 103 | + } |
| 104 | + ``` |
| 105 | + |
| 106 | + 5. *However*, invocation of `n_Run()` is wrapped in a [runtime-generated][1] |
| 107 | + `try`/`catch` block, which is equivalent to: |
| 108 | + |
| 109 | + ```csharp |
| 110 | + static void Call_n_Run(IntPtr jnienv, IntPtr native__this) |
| 111 | + { |
| 112 | + try { |
| 113 | + IRunnableInvoker.n_Run(jnienv, native__this); |
| 114 | + } |
| 115 | + catch (Exception e) { |
| 116 | + AndroidEnvironment.UnhandledException(e); |
| 117 | + } |
| 118 | + } |
| 119 | + ``` |
| 120 | + |
| 121 | + This runtime-generated `try`/`catch` block is used for *every* |
| 122 | + Java-to-managed call boundary; the method invoked in the `try` block changes. |
| 123 | + |
| 124 | + [`AndroidEnvironment.UnhandledException()`][2] is responsible for calling |
| 125 | + the [`JNIEnv::Throw()`][3] JNI method. |
| 126 | + |
| 127 | + 6. At this point in time, the runtime call-stack is: |
| 128 | + |
| 129 | + * Top-level managed method, which calls > |
| 130 | + * Java `Demo.run()` method, which calls > |
| 131 | + * Runtime-generated `try`/`catch` block, which calls > |
| 132 | + * C# `IRunnableInvoker.n_Run()` method, which calls > |
| 133 | + * C# `Action` delegate. |
| 134 | + |
| 135 | + 7. The `Action` delegate is invoked, causing a C# exception to be thrown. |
| 136 | + |
| 137 | + 8. The exception thrown in (7) is caught by the method in (5). The managed |
| 138 | + exception type is wrapped into a `JavaProxyThrowable` instance, which is |
| 139 | + then raised in the Java code. |
| 140 | + |
| 141 | + 9. The Java `finally` block executes, and then the `Demo.run()` method is |
| 142 | + unwound by the JVM. |
| 143 | + |
| 144 | +10. The `Demo.Run()` binding sees the "pending exception" from the JNI call, |
| 145 | + "unwraps" the `JavaProxyThrowable` to obtain the original `System.Exception` |
| 146 | + then raises the `System.Exception`. |
| 147 | + |
| 148 | +11. If the method calling `Demo.Run()` was invoked from Java code, e.g. |
| 149 | + an `Activity.OnCreate()` method override contained the sample code, then |
| 150 | + the runtime-generated `try`/`catch` block will catch the exception being |
| 151 | + propagated from (10) and likewise wrap it into a `JavaProxyThrowable`. |
| 152 | + |
| 153 | +12. The Java stack frames will unwind, and the Java uncaught exception behavior |
| 154 | + kicks in: [`java.lang.Thread.getUncaughtExceptionHandler()`][4] and |
| 155 | + [`java.lang.Thread.UncaughtExceptionHandler.uncaughtException()`][5] |
| 156 | + are invoked. |
| 157 | + |
| 158 | +13. During process startup, Xamarin.Android inserts itself into Java's |
| 159 | + uncaught exception handler chain. As such, |
| 160 | + [`JNIEnv.PropagateUncaughtException()`][6] is invoked, which will attempt |
| 161 | + to invoke `Debugger.Mono_UnhandledException()`, which gives an attached |
| 162 | + debugger a chance to observe the exception, and |
| 163 | + `AppDomain.DoUnhandledException()` is invoked, which will raise the |
| 164 | + `AppDomain.UnhandledException` event, giving managed code a chance to |
| 165 | + deal with the pending unhandled exception. |
| 166 | + |
| 167 | +14. The process exits, because the `System.Exception` isn't handled. :-) |
| 168 | + |
| 169 | + |
| 170 | +## Exception Handling *With* A Debugger |
| 171 | + |
| 172 | +When the debugger is attached, runtime behavior differs significantly. |
| 173 | +Steps (1) through (4) are the same, then: |
| 174 | + |
| 175 | + 5. Invocation of `n_Run()` is wrapped in a [runtime-generated][1] |
| 176 | + `try`/`catch` block which pulls in the debugger via an |
| 177 | + *exception filter*, and is equivalent to: |
| 178 | + |
| 179 | + ```csharp |
| 180 | + static void Call_n_Run(IntPtr jnienv, IntPtr native__this) |
| 181 | + { |
| 182 | + try { |
| 183 | + IRunnableInvoker.n_Run(jnienv, native__this); |
| 184 | + } |
| 185 | + catch (Exception e) when (Debugger.Mono_UnhandledException(e)) { |
| 186 | + AndroidEnvironment.UnhandledException(e); |
| 187 | + } |
| 188 | + } |
| 189 | + ``` |
| 190 | + |
| 191 | + 6. The runtime call-stack is unchanged relative to execution without a debugger. |
| 192 | + |
| 193 | + 7. The `Action` delegate is invoked, causing a C# exception to be thrown. |
| 194 | + |
| 195 | + 8. No *first chance notification* is raised. Instead, Mono will |
| 196 | + "find a frame-based exception handler that handles the exception," |
| 197 | + and as part of this process will execute any exception filters. This |
| 198 | + causes `Debugger.Mono_UnhandledException()` to be executed, which is what |
| 199 | + triggers the "**System.Exception** has been thrown" message within the |
| 200 | + debugger. |
| 201 | + |
| 202 | + If you look at the *Call Stack* Debug pad within Visual Studio for Mac, |
| 203 | + `System.Diagnostics.Debugger.Mono_UnhandledException_internal()` and |
| 204 | + `System.Diagnostics.Debugger.Mono_UnhandledException()` are the topmost |
| 205 | + call stack entries. |
| 206 | + |
| 207 | + 9. The Java `finally` block within `Demo.run()` *has not executed yet*. |
| 208 | + |
| 209 | + If `Demo.run()` had a `catch(Throwable)` block instead of a `finally` |
| 210 | + block, it likewise (1) would not have executed yet, and (2) will not |
| 211 | + participate in the stack walking to determine whether or not the exception |
| 212 | + is handled or unhandled in the first place. |
| 213 | + |
| 214 | +10. The exception is not yet "pending" in Java either, so it is safe to invoke |
| 215 | + Java code in e.g. the Immediate window. |
| 216 | + |
| 217 | +11. If execution is continued, e.g. via **Continue Debugging**, then |
| 218 | + `AndroidEnvironment.UnhandledException()` will be executed, causing the |
| 219 | + exception to be wrapped and become a "pending exception" within Java code. |
| 220 | + After this point, any invocation of Java code from the debugger will |
| 221 | + *immediately* cause the process to abort: |
| 222 | + |
| 223 | + JNI DETECTED ERROR IN APPLICATION: JNI … called with pending exception android.runtime.JavaProxyThrowable: System.Exception: Hm… |
| 224 | + |
| 225 | +12. ***Furthermore***, *No* Java code can ever again execute within the process. |
| 226 | + Once execution is continued, *Mono* will be unwinding the call stack |
| 227 | + *without involvement of the Java VM*. |
| 228 | + |
| 229 | + The `finally` block within `Debug.run()` hasn't executed yet, and will |
| 230 | + *never* execute. In particular, the `System.out.println()` message isn't |
| 231 | + visible in `adb logcat`! |
| 232 | + |
| 233 | + If `Debug.run()` instead had a `catch` block, it will similarly never be |
| 234 | + executed. |
| 235 | + |
| 236 | +13. Execution then "breaks" at the managed `Debug.Run()` method. At this point |
| 237 | + there is a pending exception within Java; any invocations of Java code from |
| 238 | + the debugger will *immediately* cause the process to abort. |
| 239 | + |
| 240 | + |
| 241 | +14. If execution is continued again, the process will exit. |
| 242 | + |
| 243 | + Unexpectedly (2020-06-26), the exit is *also* due to a JNI error: |
| 244 | + |
| 245 | + JNI DETECTED ERROR IN APPLICATION: JNI NewString called with pending exception android.runtime.JavaProxyThrowable: System.Exception: Hm… |
| 246 | + |
| 247 | + |
| 248 | + |
| 249 | +[0]: https://docs.microsoft.com/en-us/windows/win32/debug/debugger-exception-handling |
| 250 | +[1]: https://github.com/xamarin/xamarin-android/blob/402ae221be90fdb4b48c2aeb29170b745c30f60b/src/Mono.Android/Android.Runtime/JNINativeWrapper.cs#L34-L97 |
| 251 | +[2]: https://github.com/xamarin/xamarin-android/blob/402ae221be90fdb4b48c2aeb29170b745c30f60b/src/Mono.Android/Android.Runtime/AndroidEnvironment.cs#L115-L129 |
| 252 | +[3]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#Throw |
| 253 | +[4]: https://developer.android.com/reference/java/lang/Thread#getUncaughtExceptionHandler() |
| 254 | +[5]: https://developer.android.com/reference/java/lang/Thread.UncaughtExceptionHandler#uncaughtException(java.lang.Thread,%20java.lang.Throwable) |
| 255 | +[6]: https://github.com/xamarin/xamarin-android/blob/4cae5f5e40896c69b7448cb78cf613cf6327c97c/src/Mono.Android/Android.Runtime/JNIEnv.cs#L265-L296 |
| 256 | +
|
| 257 | + |
| 258 | +--- |
| 259 | + |
| 260 | +Internal context: |
| 261 | + |
| 262 | + * https://bugzilla.xamarin.com/show_bug.cgi?id=7634 |
| 263 | + * https://github.com/xamarin/monodroid/commit/b0f85970102d43bab9cd860a8e8884d136d766b3 |
| 264 | + * https://github.com/xamarin/monodroid/commit/a9697ca2ac026b960b347a925fbe414efe3876f7 |
| 265 | + * https://github.com/xamarin/monodroid/commit/12a012e00b4533d586ef31ced33351b63c9de883 |
0 commit comments