dotnet / dotnet/android

Java-side constructor invocation & Exception Propagation

Open
#7,324 1 comment 0 reactions 1 assignee Claimed by @jonpryor View on GitHub
Area: App Runtime enhancement
Dominant language
C#
Stars
2.1k
Forks
579
Avg merge
1d 20h
Merged PRs (30d)
257

Description

Assume you have an `Activity` subclass:

```csharp
[Activity(Label = "@string/app_name", MainLauncher = true)]
public partial class MainActivity : Activity
{
}
```

At build time, a Java Callable Wrapper is generated.

```java
public class MainActivity extends android.app.Activity implements mono.android.IGCUserPeer
{
/* … */

public MainActivity ()
{
super ();
if (getClass () == MainActivity.class) {
mono.android.TypeManager.Activate ("hw_android_net7.MainActivity, hw-android-net7", "", this, new java.lang.Object[] { });
}
}
}
```

The constructor of the Java Callable Wrapper calls `TypeManager.Activate()`: https://github.com/xamarin/xamarin-android/blob/7c9c24b3614710614c5512d7a3b8272065270dc2/src/Mono.Android/java/mono/android/TypeManager.java#L5-L8

…which eventually invokes `TypeManager.Activate()` in C#: https://github.com/xamarin/xamarin-android/blob/7c9c24b3614710614c5512d7a3b8272065270dc2/src/Mono.Android/Java.Interop/TypeManager.cs#L172-L192

What `TypeManager.Activate()` does is create the corresponding managed-side type, and then invoke the appropriate managed constructor on that instance. This is how when Android creates the `MainActivity` Java Callable Wrapper, an instance of the C# `MainActivity` type is created and the default constructor is invoked.

What happens if the constructor throws an exception?

```csharp
public class MainActivity : Activity
{
public MainActivity() => throw new Exception("lol!");
}
```

This hits the `catch` block in `TypeManager.Activate()`: https://github.com/xamarin/xamarin-android/blob/7c9c24b3614710614c5512d7a3b8272065270dc2/src/Mono.Android/Java.Interop/TypeManager.cs#L184-L191

which does two things:

1. Log that "something went wrong", and
2. Throw a wrapping `NotSupportedException`.

The log message is written to `adb logcat`:

```
W monodroid: Could not activate JNI Handle 0x7ff1f3c970 (key_handle 0x99f05dd) of Java type 'crc6434d9e85eaf140a95/MainActivity' as managed type 'hw_android_net7.MainActivity'.
```

The `NotSupportedException` is visible within the debugger (when debugging the app), and/or is eventually written to `adb logcat` as an unhandled exception:

```
I MonoDroid: Android.Runtime.JavaProxyThrowable: Exception of type 'Android.Runtime.JavaProxyThrowable' was thrown.
I MonoDroid:
I MonoDroid: --- End of managed Android.Runtime.JavaProxyThrowable stack trace ---
I MonoDroid: android.runtime.JavaProxyThrowable: System.NotSupportedException: Could not activate JNI Handle 0x7ff1f3c970 (key_handle 0x99f05dd) of Java type 'crc6434d9e85eaf140a95/MainActivity' as managed type 'hw_android_net7.MainActivity'.
I MonoDroid: ---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
I MonoDroid: ---> System.Exception: lol!
I MonoDroid: at hw_android_net7.MainActivity..ctor()
I MonoDroid: at System.Reflection.ConstructorInvoker.InterpretedInvoke(Object obj, Span`1 args, BindingFlags invokeAttr)
I MonoDroid: --- End of inner exception stack trace ---
I MonoDroid: at System.Reflection.RuntimeConstructorInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
I MonoDroid: at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
I MonoDroid: at Java.Interop.TypeManager.Activate(IntPtr jobject, ConstructorInfo cinfo, Object[] parms)
I MonoDroid: --- End of inner exception stack trace ---
I MonoDroid: at Java.Interop.TypeManager.Activate(IntPtr jobject, ConstructorInfo cinfo, Object[] parms)
I MonoDroid: at Java.Interop.TypeManager.n_Activate(IntPtr jnienv, IntPtr jclass, IntPtr typename_ptr, IntPtr signature_ptr, IntPtr jobject, IntPtr parameters_ptr)
I MonoDroid: at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPLLLL_V(_JniMarshal_PPLLLL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0, IntPtr p1, IntPtr p2, IntPtr p3)
I MonoDroid: at mono.android.TypeManager.n_activate(Native Method)
I MonoDroid: at mono.android.TypeManager.Activate(TypeManager.java:7)
I MonoDroid: at crc6434d9e85eaf140a95.MainActivity.(MainActivity.java:23)
I MonoDroid: at java.lang.Class.newInstance(Native Method)
I MonoDroid: at android.app.AppComponentFactory.instantiateActivity(AppComponentFactory.java:95)
I MonoDroid: at android.app.Instrumentation.newActivity(Instrumentation.java:1285)
I MonoDroid: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3600)
I MonoDroid: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3864)
I MonoDroid: at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
I MonoDroid: at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
I MonoDroid: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
I MonoDroid: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2253)
I MonoDroid: at android.os.Handler.dispatchMessage(Handler.java:106)
I MonoDroid: at android.os.Looper.loopOnce(Looper.java:201)
I MonoDroid: at android.os.Looper.loop(Looper.java:288)
I MonoDroid: at android.app.ActivityThread.main(ActivityThread.java:7870)
I MonoDroid: at java.lang.reflect.Method.invoke(Native Method)
I MonoDroid: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
I MonoDroid: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
I MonoDroid:
I MonoDroid: --- End of managed Android.Runtime.JavaProxyThrowable stack trace ---
```

So far so reasonable. (It's worked this way for ages.)

But…is wrapping *every* exception in `NotSupportedException` *actually* reasonable?

Consider this slight variation:

```csharp
public class MainActivity : Activity
{
public MainActivity()
{
var cursor = ContentResolver.Query(Android.Net.Uri.Parse("content://mms-sms/conversations/"), new string[] { "*" }, null, null, "date DESC");
}
}
```

Here we are "mis-using" the Android API, as we shall see. The resulting unhandled exception is:

```
I MonoDroid: Android.Runtime.JavaProxyThrowable: Exception of type 'Android.Runtime.JavaProxyThrowable' was thrown.
I MonoDroid:
I MonoDroid: --- End of managed Android.Runtime.JavaProxyThrowable stack trace ---
I MonoDroid: android.runtime.JavaProxyThrowable: System.NotSupportedException: Could not activate JNI Handle 0x7ff1f3c970 (key_handle 0x99f05dd) of Java type 'crc6434d9e85eaf140a95/MainActivity' as managed type 'hw_android_net7.MainActivity'.
I MonoDroid: ---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
I MonoDroid: ---> Java.Lang.NullPointerException: Attempt to invoke virtual method 'android.content.ContentResolver android.content.Context.getContentResolver()' on a null object reference
I MonoDroid: at Java.Interop.JniEnvironment.InstanceMethods.CallNonvirtualObjectMethod(JniObjectReference instance, JniObjectReference type, JniMethodInfo method, JniArgumentValue* args)
I MonoDroid: at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualObjectMethod(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters)
I MonoDroid: at Android.Content.ContextWrapper.get_ContentResolver()
I MonoDroid: at hw_android_net7.MainActivity..ctor()
I MonoDroid: at System.Reflection.ConstructorInvoker.InterpretedInvoke(Object obj, Span`1 args, BindingFlags invokeAttr)
I MonoDroid: --- End of managed Java.Lang.NullPointerException stack trace ---
I MonoDroid: java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.ContentResolver android.content.Context.getContentResolver()' on a null object reference
I MonoDroid: at android.content.ContextWrapper.getContentResolver(ContextWrapper.java:110)
I MonoDroid: at mono.android.TypeManager.n_activate(Native Method)
I MonoDroid: at mono.android.TypeManager.Activate(TypeManager.java:7)
I MonoDroid: at crc6434d9e85eaf140a95.MainActivity.(MainActivity.java:23)
I MonoDroid: at java.lang.Class.newInstance(Native Method)
I MonoDroid: at android.app.AppComponentFactory.instantiateActivity(AppComponentFactory.java:95)
I MonoDroid: at android.app.Instrumentation.newActivity(Instrumentation.java:1285)
I MonoDroid: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3600)
I MonoDroid: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3864)
I MonoDroid: at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
I MonoDroid: at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
I MonoDroid: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
I MonoDroid: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2253)
I MonoDroid: at android.os.Handler.dispatchMessage(Handler.java:106)
I MonoDroid: at android.os.Looper.loopOnce(Looper.java:201)
I MonoDroid: at android.os.Looper.loop(Looper.java:288)
I MonoDroid: at android.app.ActivityThread.main(ActivityThread.java:7870)
I MonoDroid: at java.lang.reflect.Method.invoke(Native Method)
I MonoDroid: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
I MonoDroid: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
I MonoDroid:
I MonoDroid: --- End of managed Java.Lang.NullPointerException stack trace ---
```

This is "mis-use" of the API because we're trying to use Android APIs before a base context has been applied; see the docs for the [`ContextThemeWrapper` default constructor](https://developer.android.com/reference/android/view/ContextThemeWrapper#ContextThemeWrapper()):

> **Note**: A base context **must** be attached using [attachBaseContext(android.content.Context)](https://developer.android.com/reference/android/view/ContextThemeWrapper#attachBaseContext(android.content.Context)) before calling any other method on the newly constructed context wrapper.

Meaning *no* `Activity` members can be safely invoked from the `MainActivity` constructor. (Calling `Activity` members from the `OnCreate()` method override is fine, just not the constructor.)

OK, so far so "fine", but… is this *really* fine?

The "topmost" exception is `NotSupportedException` mentioning that the handle couldn't be activated. While correct, it is also misleading, because not all context is immediately available:

image

You need to expand quite a bit to get to the "source" `NullPointerException`:

image

Additionally, no Call Stack is available, because `TypeManager.Activate()` always catches all exceptions, instead of using a "debugger aware exception filter" (e.g. 32cff4383232d5de156bd6c5d10292fcffa66d50), so there is no easy way to *know* that the C# constructor is the source of the crash.

---

Suggestions for improvement:

* Should the `Logger.Log()` calls within `TypeManager.Activate()` *also* print `e.ToString()`? (Why? In case of mono crash; see later comment.)
* Add an exception filter to `TypeManager.Activate()`, so that if a debugger is attached, a "first chance exception" will bring the developer to the original `ContentResolver.Query()` call site.
* *Should* ***all*** exceptions be wrapped in `NotSupportedException`? Or should "Java native" exceptions be passed through?
Related question here is what Java consumers should see. For `Activity`, current behavior is fine, but if Java code had an expectation that constructors could throw `java.lang.IllegalArgumentException`, it is *not possible* to satisfy that requirement. A C# constructor throwing `new Java.Lang.IllegalArgumentException(…)` will raise a `JavaProxyThrowable` to the calling Java code, *always*.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.