diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 1c35c0a3db9..ec76a3f091f 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -104,7 +104,7 @@ internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPt if (RuntimeFeature.IsMonoRuntime) { MonoDroidUnhandledException (innerException ?? javaException); } else if (RuntimeFeature.IsCoreClrRuntime) { - // TODO: what to do here? + ExceptionHandling.RaiseAppDomainUnhandledExceptionEvent (innerException ?? javaException); } else { throw new NotSupportedException ("Internal error: unknown runtime not supported"); } diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index d932148e7f3..8314aa2708c 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -35,6 +35,7 @@ internal struct JnienvInitializeArgs { public bool marshalMethodsEnabled; public IntPtr grefGCUserPeerable; public bool managedMarshalMethodsLookupEnabled; + public IntPtr propagateUncaughtExceptionFn; } #pragma warning restore 0649 @@ -49,6 +50,12 @@ internal struct JnienvInitializeArgs { internal static JniRuntime? androidRuntime; + [UnmanagedCallersOnly] + static void PropagateUncaughtException (IntPtr env, IntPtr javaThread, IntPtr javaException) + { + JNIEnv.PropagateUncaughtException (env, javaThread, javaException); + } + [UnmanagedCallersOnly] static unsafe void RegisterJniNatives (IntPtr typeName_ptr, int typeName_len, IntPtr jniClass, IntPtr methods_ptr, int methods_len) { @@ -157,6 +164,8 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) xamarin_app_init (args->env, getFunctionPointer); } + args->propagateUncaughtExceptionFn = (IntPtr)(delegate* unmanaged)&PropagateUncaughtException; + SetSynchronizationContext (); } diff --git a/src/native/clr/host/host-jni.cc b/src/native/clr/host/host-jni.cc index b3fa81df164..a41c1c507cc 100644 --- a/src/native/clr/host/host-jni.cc +++ b/src/native/clr/host/host-jni.cc @@ -47,9 +47,9 @@ Java_mono_android_Runtime_initInternal (JNIEnv *env, jclass klass, jstring lang, } JNIEXPORT void -JNICALL Java_mono_android_Runtime_propagateUncaughtException ([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jclass klass, [[maybe_unused]] jobject javaThread, [[maybe_unused]] jthrowable javaException) +JNICALL Java_mono_android_Runtime_propagateUncaughtException (JNIEnv *env, [[maybe_unused]] jclass klass, jobject javaThread, jthrowable javaException) { - // TODO: implement or remove + Host::propagate_uncaught_exception (env, javaThread, javaException); } JNIEXPORT void diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index 1a0dc8caebc..7d19c7e9175 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -560,6 +560,10 @@ void Host::Java_mono_android_Runtime_initInternal ( log_debug (LOG_DEFAULT, "Calling into managed runtime init"sv); FastTiming::time_call ("JNIEnv.Initialize UCO"sv, initialize, &init); + // PropagateUncaughtException is returned from Initialize to avoid an extra create_delegate call + jnienv_propagate_uncaught_exception = init.propagateUncaughtExceptionFn; + abort_unless (jnienv_propagate_uncaught_exception != nullptr, "Failed to obtain unmanaged-callers-only function pointer to the PropagateUncaughtException method."); + if (FastTiming::enabled ()) [[unlikely]] { internal_timing.end_event (); // native to managed internal_timing.end_event (); // total init time @@ -613,3 +617,13 @@ auto HostCommon::Java_JNI_OnLoad (JavaVM *vm, [[maybe_unused]] void *reserved) n AndroidSystem::init_max_gref_count (); return JNI_VERSION_1_6; } + +void Host::propagate_uncaught_exception (JNIEnv *env, jobject javaThread, jthrowable javaException) noexcept +{ + if (jnienv_propagate_uncaught_exception == nullptr) { + log_warn (LOG_DEFAULT, "propagate_uncaught_exception called before JNIEnvInit.PropagateUncaughtException was initialized"sv); + return; + } + + jnienv_propagate_uncaught_exception (env, javaThread, javaException); +} diff --git a/src/native/clr/include/host/host.hh b/src/native/clr/include/host/host.hh index a05601b9bf6..5e873b1a54a 100644 --- a/src/native/clr/include/host/host.hh +++ b/src/native/clr/include/host/host.hh @@ -20,6 +20,7 @@ namespace xamarin::android { jstring runtimeNativeLibDir, jobjectArray appDirs, jint localDateTimeOffset, jobject loader, jobjectArray assembliesJava, jboolean isEmulator, jboolean haveSplitApks) noexcept; static void Java_mono_android_Runtime_register (JNIEnv *env, jstring managedType, jclass nativeClass, jstring methods) noexcept; + static void propagate_uncaught_exception (JNIEnv *env, jobject javaThread, jthrowable javaException) noexcept; static auto get_timing () -> std::shared_ptr { @@ -53,6 +54,7 @@ namespace xamarin::android { static inline std::shared_ptr _timing{}; static inline bool found_assembly_store = false; static inline jnienv_register_jni_natives_fn jnienv_register_jni_natives = nullptr; + static inline jnienv_propagate_uncaught_exception_fn jnienv_propagate_uncaught_exception = nullptr; static inline jclass java_TimeZone = nullptr; diff --git a/src/native/common/include/managed-interface.hh b/src/native/common/include/managed-interface.hh index f40690d4030..b590fb6210e 100644 --- a/src/native/common/include/managed-interface.hh +++ b/src/native/common/include/managed-interface.hh @@ -14,6 +14,8 @@ namespace xamarin::android { Signals = 0x08, }; + using jnienv_propagate_uncaught_exception_fn = void (*)(JNIEnv *env, jobject javaThread, jthrowable javaException); + // NOTE: Keep this in sync with managed side in src/Mono.Android/Android.Runtime/JNIEnvInit.cs struct JnienvInitializeArgs { JavaVM *javaVm; @@ -33,6 +35,7 @@ namespace xamarin::android { bool marshalMethodsEnabled; jobject grefGCUserPeerable; bool managedMarshalMethodsLookupEnabled; + jnienv_propagate_uncaught_exception_fn propagateUncaughtExceptionFn; }; // Keep the enum values in sync with those in src/Mono.Android/AndroidRuntime/BoundExceptionType.cs diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index b0a1594b404..aed9aa41a21 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -213,15 +213,9 @@ void Button_ViewTreeObserver_GlobalLayout (object sender, EventArgs e) }, Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log"), 60), $"Output did not contain {expectedLogcatOutput}!"); } - // TODO: check if AppDomain.CurrentDomain.UnhandledException even works in CoreCLR [Test] public void SubscribeToAppDomainUnhandledException ([Values (AndroidRuntime.MonoVM, AndroidRuntime.CoreCLR)] AndroidRuntime runtime) { - if (runtime == AndroidRuntime.CoreCLR) { - Assert.Ignore ("AppDomain.CurrentDomain.UnhandledException doesn't work in CoreCLR"); - return; - } - proj = new XamarinAndroidApplicationProject () { IsRelease = true, }; @@ -243,7 +237,13 @@ public void SubscribeToAppDomainUnhandledException ([Values (AndroidRuntime.Mono Assert.IsTrue (builder.Install (proj), "Install should have succeeded."); RunProjectAndAssert (proj, builder); - string expectedLogcatOutput = "# Unhandled Exception: sender=System.Object; e.IsTerminating=True; e.ExceptionObject=System.Exception: CRASH"; + string? expectedSender = runtime switch + { + AndroidRuntime.MonoVM => "System.Object", // MonoVM passes the current domain as the sender + AndroidRuntime.CoreCLR => null, // CoreCLR explicitly passes a `null` sender + _ => throw new NotImplementedException($"Test does not support runtime {runtime}"), + }; + string expectedLogcatOutput = $"# Unhandled Exception: sender={expectedSender}; e.IsTerminating=True; e.ExceptionObject=System.Exception: CRASH"; Assert.IsTrue ( MonitorAdbLogcat (CreateLineChecker (expectedLogcatOutput), logcatFilePath: Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log"), timeout: 60),