dotnet / android

.NET for Android provides open-source bindings of the Android SDK for use with .NET managed languages such as C#
MIT License
1.92k stars 526 forks source link

Java-side constructor invocation & Exception Propagation #7324

Open jonpryor opened 2 years ago

jonpryor commented 2 years ago

Assume you have an Activity subclass:

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

At build time, a Java Callable Wrapper is generated.

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?

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.<init>(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:

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.<init>(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) 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:

jonpryor commented 2 years ago

…and about that mono crash? With .NET 6:

% dotnet new android -n android-hw-net6

% cat <<EOF | patch -p1
diff --git a/MainActivity.cs b/MainActivity.cs
index 3beae44..043b2b3 100644
--- a/MainActivity.cs
+++ b/MainActivity.cs
@@ -3,6 +3,11 @@ namespace android_hw_net6;
 [Activity(Label = "@string/app_name", MainLauncher = true)]
 public class MainActivity : Activity
 {
+    public MainActivity()
+    {
+        var cursor = ContentResolver.Query(Android.Net.Uri.Parse("content://mms-sms/conversations/"), new string[] { "*" }, null, null, "date DESC");
+    }
+
     protected override void OnCreate(Bundle? savedInstanceState)
     {
         base.OnCreate(savedInstanceState);
EOF

% dotnet build -t:Install
% adb shell setprop debug.mono.log gref
% dotnet build -t:Run

Expected behavior: the expected Unhandled Exception "crash".

Actual behavior: mono aborts!

E android_hw_net: * Assertion: should not be reached at /__w/1/s/src/mono/mono/mini/mini-exceptions.c:456
…
I crash_dump64: performing dump of process 10718 (target tid = 10718)
E DEBUG   : failed to read /proc/uptime: Permission denied
F DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
F DEBUG   : Build fingerprint: 'google/raven/raven:12/SQ3A.220705.004/8836240:user/release-keys'
F DEBUG   : Revision: 'MP1.0'
F DEBUG   : ABI: 'arm64'
F DEBUG   : Timestamp: 2022-08-31 14:36:02.523194803-0400
F DEBUG   : Process uptime: 0s
F DEBUG   : Cmdline: com.companyname.android_hw_net6
F DEBUG   : pid: 10718, tid: 10718, name: android_hw_net6  >>> com.companyname.android_hw_net6 <<<
F DEBUG   : uid: 10278
F DEBUG   : tagged_addr_ctrl: 0000000000000001
F DEBUG   : signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
F DEBUG   :     x0  0000000000000000  x1  00000000000029de  x2  0000000000000006  x3  0000007ff1f3abb0
F DEBUG   :     x4  67626064711f6461  x5  67626064711f6461  x6  67626064711f6461  x7  7f7f7f7f7f7f7f7f
F DEBUG   :     x8  00000000000000f0  x9  000000711b0a30b0  x10 0000000000000000  x11 ffffff80fffffbdf
F DEBUG   :     x12 0000000000000001  x13 0000000000000059  x14 0000007ff1f39a50  x15 00001c0ca925bd07
F DEBUG   :     x16 000000711b140050  x17 000000711b11dbd0  x18 000000711fdf2000  x19 00000000000029de
F DEBUG   :     x20 00000000000029de  x21 00000000ffffffff  x22 0000007ff1f3bee0  x23 0000007ff1f3b6e0
F DEBUG   :     x24 0000000000000000  x25 0000007ff1f3b9f0  x26 b400006fbdecb430  x27 0000000000000000
F DEBUG   :     x28 0000000000000000  x29 0000007ff1f3ac30
F DEBUG   :     lr  000000711b0d072c  sp  0000007ff1f3ab90  pc  000000711b0d075c  pst 0000000000001000
F DEBUG   : backtrace:
F DEBUG   :       #00 pc 000000000004f75c  /apex/com.android.runtime/lib64/bionic/libc.so (abort+168) (BuildId: 53a228529316d67f22e241dd17ea9b9e)
F DEBUG   :       #01 pc 00000000000309c0  /data/app/~~-9u-MryjEfwWJIeVGn07vQ==/com.companyname.android_hw_net6-LAyawSCT5cLDklg8Q_cmjg==/lib/arm64/libmonodroid.so (xamarin::android::internal::MonodroidRuntime::mono_log_handler(char const*, char const*, char const*, int, void*)+144) (BuildId: 0615bbc115b094c7682d18118b1ea0c19b27ba97)
F DEBUG   :       #02 pc 0000000000264074  /data/app/~~-9u-MryjEfwWJIeVGn07vQ==/com.companyname.android_hw_net6-LAyawSCT5cLDklg8Q_cmjg==/lib/arm64/libmonosgen-2.0.so (BuildId: ca0b0b9194e7cf921e407ed4dc579808ea4e788a)
F DEBUG   :       #03 pc 00000000002641a0  /data/app/~~-9u-MryjEfwWJIeVGn07vQ==/com.companyname.android_hw_net6-LAyawSCT5cLDklg8Q_cmjg==/lib/arm64/libmonosgen-2.0.so (BuildId: ca0b0b9194e7cf921e407ed4dc579808ea4e788a)
F DEBUG   :       #04 pc 0000000000264200  /data/app/~~-9u-MryjEfwWJIeVGn07vQ==/com.companyname.android_hw_net6-LAyawSCT5cLDklg8Q_cmjg==/lib/arm64/libmonosgen-2.0.so (BuildId: ca0b0b9194e7cf921e407ed4dc579808ea4e788a)
F DEBUG   :       #05 pc 00000000001e5d7c  /data/app/~~-9u-MryjEfwWJIeVGn07vQ==/com.companyname.android_hw_net6-LAyawSCT5cLDklg8Q_cmjg==/lib/arm64/libmonosgen-2.0.so (BuildId: ca0b0b9194e7cf921e407ed4dc579808ea4e788a)
F DEBUG   :       #06 pc 00000000001e5b18  /data/app/~~-9u-MryjEfwWJIeVGn07vQ==/com.companyname.android_hw_net6-LAyawSCT5cLDklg8Q_cmjg==/lib/arm64/libmonosgen-2.0.so (BuildId: ca0b0b9194e7cf921e407ed4dc579808ea4e788a)
F DEBUG   :       #07 pc 00000000001e6de4  /data/app/~~-9u-MryjEfwWJIeVGn07vQ==/com.companyname.android_hw_net6-LAyawSCT5cLDklg8Q_cmjg==/lib/arm64/libmonosgen-2.0.so (BuildId: ca0b0b9194e7cf921e407ed4dc579808ea4e788a)
F DEBUG   :       #08 pc 00000000001e6b54  /data/app/~~-9u-MryjEfwWJIeVGn07vQ==/com.companyname.android_hw_net6-LAyawSCT5cLDklg8Q_cmjg==/lib/arm64/libmonosgen-2.0.so (BuildId: ca0b0b9194e7cf921e407ed4dc579808ea4e788a)
F DEBUG   :       #09 pc 0000000000006198  <anonymous:710cc77000>

Apparent assert location is: https://github.com/dotnet/runtime/blob/release/6.0/src/mono/mono/mini/mini-exceptions.c#L456

This doesn't appear to happen on .NET 7.