-
Notifications
You must be signed in to change notification settings - Fork 555
[NativeAOT] Add DefaultUncaughtExceptionHandler #9994
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Context: dotnet/runtime#102730 Context: 1aa0ea7 What should happen when an exception is thrown and not caught? partial class MainActivity { protected override void OnCreate(Bundle? savedInstanceState) => throw new Exception("Uncaught exception"); } What *previously* happened is that the app would exit, with an `AndroidRuntime` tag containing the Java-side exception, which will contain *some* managed info courtesy of 1aa0ea7. I ActivityManager: Start proc 6911:net.dot.hellonativeaot/u0a205 for top-activity {net.dot.hellonativeaot/my.MainActivity} … D NativeAotRuntimeProvider: NativeAotRuntimeProvider() D NativeAotRuntimeProvider: NativeAotRuntimeProvider.attachInfo(): calling JavaInteropRuntime.init()… D JavaInteropRuntime: Loading NativeAOT.so... I JavaInteropRuntime: JNI_OnLoad() … D NativeAotRuntimeProvider: NativeAotRuntimeProvider.onCreate() D NativeAOT: Application..ctor(7fff01a958, DoNotTransfer) D NativeAOT: Application.OnCreate() … D NativeAOT: MainActivity.OnCreate() … D NativeAOT: MainActivity.OnCreate() ColorStateList: ColorStateList{mThemeAttrs=nullmChangingConfigurations=0mStateSpecs=[[0, 1]]mColors=[0, 1]mDefaultColor=0} D AndroidRuntime: Shutting down VM E AndroidRuntime: FATAL EXCEPTION: main E AndroidRuntime: Process: net.dot.hellonativeaot, PID: 6911 E AndroidRuntime: net.dot.jni.internal.JavaProxyThrowable: System.InvalidOperationException: What happened? E AndroidRuntime: at NativeAOT.MainActivity.OnCreate(Bundle savedInstanceState) + 0x2f4 E AndroidRuntime: at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) + 0xc8 E AndroidRuntime: at my.MainActivity.n_onCreate(Native Method) E AndroidRuntime: at my.MainActivity.onCreate(MainActivity.java:28) E AndroidRuntime: at android.app.Activity.performCreate(Activity.java:8595) E AndroidRuntime: at android.app.Activity.performCreate(Activity.java:8573) E AndroidRuntime: at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1456) E AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3805) E AndroidRuntime: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3963) E AndroidRuntime: at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103) E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139) E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96) E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2484) E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:106) E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:205) E AndroidRuntime: at android.os.Looper.loop(Looper.java:294) E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8225) E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:573) E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1049) W ActivityTaskManager: Force finishing activity net.dot.hellonativeaot/my.MainActivity What *won't* happen is that the [`AppDomain.UnhandledException`][0] event will ***not*** be raised This is less than ideal, and will cause the `InstallAndRunTests.SubscribeToAppDomainUnhandledException()` test to fail, once that test is enabled for NativeAOT. *Begin* to address this by setting `Java.Lang.Thread.DefaultUncaughtExceptionHandler` to a `Thread.IUncaughtExceptionHandler` instance which at least prints the exception to `adb logcat`. Update `samples/NativeAOT` to demonstrate this, by using a new boolean `thrown` extra; if true, then `OnCreate()` throws: adb shell am start --ez throw 1 net.dot.hellonativeaot/my.MainActivity `adb logcat` continues to have the `FATAL EXCEPTION` message from `AndroidRuntime`, as shown above, and `adb logcat` now also contains: F DOTNET : FATAL UNHANDLED EXCEPTION: System.InvalidOperationException: What happened? F DOTNET : at NativeAOT.MainActivity.OnCreate(Bundle savedInstanceState) + 0x2f4 F DOTNET : at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) + 0xc8 which prints out the managed exception that we would expect to be raised by `AppDomain.UnhandledException`, once that integration works. TODO: once dotnet/runtime#102730 is fixed, update `UncaughtExceptionMarshaler` to do whatever it needs to do to cause the `AppDomain.UnhandledException` event to be raised. Update `JavaInteropRuntime.init()` to marshal excpetions back to Java, so that the process appropriately terminates if `init()` fails. [0]: https://learn.microsoft.com/dotnet/api/system.appdomain.unhandledexception?view=net-9.0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces a default uncaught exception handler for NativeAOT on Android, addressing the issue where unhandled exceptions were not triggering the AppDomain.UnhandledException event. Key changes include:
- Adding a new UncaughtExceptionMarshaler class to log unhandled exceptions.
- Updating JavaInteropRuntime to set the new default uncaught exception handler and propagate exceptions via JniTransition.
- Modifying the NativeAOT sample to optionally throw an exception to test the new behavior.
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
File | Description |
---|---|
src/Microsoft.Android.Runtime.NativeAOT/UncaughtExceptionMarshaler.cs | New exception marshaler that logs exceptions before deferring. |
src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs | Update to set the new default exception handler and manage exception transitions. |
samples/NativeAOT/MainActivity.cs | Modified sample to optionally throw an exception for testing. |
Comments suppressed due to low confidence (1)
src/Microsoft.Android.Runtime.NativeAOT/UncaughtExceptionMarshaler.cs:5
- [nitpick] Consider specifying an explicit access modifier (e.g., public) for the primary constructor parameter to clarify the intended accessibility and align with coding conventions.
class UncaughtExceptionMarshaler (Java.Lang.Thread.IUncaughtExceptionHandler? OriginalHandler)
src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs
Show resolved
Hide resolved
src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs
Show resolved
Hide resolved
@@ -33,6 +33,7 @@ static void JNI_OnUnload (IntPtr vm, IntPtr reserved) | |||
[UnmanagedCallersOnly (EntryPoint="Java_net_dot_jni_nativeaot_JavaInteropRuntime_init")] | |||
static void init (IntPtr jnienv, IntPtr klass) | |||
{ | |||
JniTransition transition = default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this is a struct, and no need to check if it's "empty"? Seems like the methods are no-op:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, methods are a no-op if the struct
"default constructor" is used. The non-default constructor can't be used until after a JniRuntime
exists, which made structure "weird".
…ot-unhandledexception
Context: dotnet/runtime#102730
Context: 1aa0ea7
What should happen when an exception is thrown and not caught?
What previously happened is that the app would exit, with an
AndroidRuntime
tag containing the Java-side exception, which will contain some managed info courtesy of 1aa0ea7.What won't happen is that the
AppDomain.UnhandledException
event will not be raisedThis is less than ideal, and will cause the
InstallAndRunTests.SubscribeToAppDomainUnhandledException()
test to fail, once that test is enabled for NativeAOT.Begin to address this by setting
Java.Lang.Thread.DefaultUncaughtExceptionHandler
to aThread.IUncaughtExceptionHandler
instance which at least prints the exception toadb logcat
.Update
samples/NativeAOT
to demonstrate this, by using a new booleanthrown
extra; if true, thenOnCreate()
throws:adb logcat
continues to have theFATAL EXCEPTION
message fromAndroidRuntime
, as shown above, andadb logcat
now also contains:which prints out the managed exception that we would expect to be raised by
AppDomain.UnhandledException
, once that integration works.TODO: once dotnet/runtime#102730 is fixed, update
UncaughtExceptionMarshaler
to do whatever it needs to do to cause theAppDomain.UnhandledException
event to be raised.Update
JavaInteropRuntime.init()
to marshal excpetions back to Java, so that the process appropriately terminates ifinit()
fails.