Skip to content

[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

Merged
merged 3 commits into from
Apr 9, 2025

Conversation

jonpryor
Copy link
Contributor

@jonpryor jonpryor commented Apr 1, 2025

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 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.

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
Copy link

@Copilot Copilot AI left a 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)

@@ -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;
Copy link
Member

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:

Copy link
Contributor Author

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".

@jonpryor jonpryor merged commit 9ad492a into main Apr 9, 2025
1 of 21 checks passed
@jonpryor jonpryor deleted the dev/jonp/jonp-nativeaot-unhandledexception branch April 9, 2025 13:20
@github-actions github-actions bot locked and limited conversation to collaborators May 10, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants