Skip to content

Commit f7f3ebc

Browse files
authored
[docs] Document Exception Handling semantics (#4877)
1 parent 100d8c5 commit f7f3ebc

File tree

1 file changed

+265
-0
lines changed

1 file changed

+265
-0
lines changed
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
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: JNIcalled 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

Comments
 (0)