Skip to content

Commit

Permalink
Use mono_unhandled_exception for NET6
Browse files Browse the repository at this point in the history
Context: dotnet/runtime#55904 (comment)
Context: dotnet#4927 (comment)
Context: dotnet#4927 (comment)

Xamarin.Android has been using the `AppDomain.DoUnhandledException` API
since the dawn of time to propagate uncaught Java exceptions to the
managed world.  However, said API (and AppDomains) are gone from the
NET6 MonoVM runtime and we need to switch to something else - the
`mono_unhandled_exception` native API.

This commit introduces an internal call,
`JNIEnv.monodroid_unhandled_exception`, which is used instead of the
older mechanism when targetting NET6 and which calls the native API
mentioned above.

Add a device integration test which makes sure the uncaught exceptions
are propagated as required.
  • Loading branch information
grendello committed Jul 22, 2021
1 parent 1e5bfa3 commit 0f67df4
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 1 deletion.
15 changes: 14 additions & 1 deletion src/Mono.Android/Android.Runtime/JNIEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,9 @@ static void ManualJavaObjectDispose (Java.Lang.Object obj)
}

static Action<Exception> mono_unhandled_exception = null!;
#if !NETCOREAPP
static Action<AppDomain, UnhandledExceptionEventArgs> AppDomain_DoUnhandledException = null!;
#endif // ndef NETCOREAPP

static void Initialize ()
{
Expand All @@ -253,6 +255,7 @@ static void Initialize ()
mono_unhandled_exception = (Action<Exception>) Delegate.CreateDelegate (typeof(Action<Exception>), mono_UnhandledException);
}

#if !NETCOREAPP
if (AppDomain_DoUnhandledException == null) {
var ad_due = typeof (AppDomain)
.GetMethod ("DoUnhandledException",
Expand All @@ -265,8 +268,14 @@ static void Initialize ()
typeof (Action<AppDomain, UnhandledExceptionEventArgs>), ad_due);
}
}
#endif // ndef NETCOREAPP
}

#if NETCOREAPP
[MethodImplAttribute(MethodImplOptions.InternalCall)]
extern static void monodroid_unhandled_exception (Exception javaException);
#endif // def NETCOREAPP

internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPtr, IntPtr javaExceptionPtr)
{
if (!PropagateExceptions)
Expand All @@ -287,14 +296,18 @@ internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPt
try {
var jltp = javaException as JavaProxyThrowable;
Exception? innerException = jltp?.InnerException;
var args = new UnhandledExceptionEventArgs (innerException ?? javaException, isTerminating: true);

Logger.Log (LogLevel.Info, "MonoDroid", "UNHANDLED EXCEPTION:");
Logger.Log (LogLevel.Info, "MonoDroid", javaException.ToString ());

#if !NETCOREAPP
var args = new UnhandledExceptionEventArgs (innerException ?? javaException, isTerminating: true);
// Disabled until Linker error surfaced in https://github.com/xamarin/xamarin-android/pull/4302#issuecomment-596400025 is resolved
//AppDomain.CurrentDomain.DoUnhandledException (args);
AppDomain_DoUnhandledException?.Invoke (AppDomain.CurrentDomain, args);
#else // ndef NETCOREAPP
monodroid_unhandled_exception (innerException ?? javaException);
#endif // def NETCOREAPP
} catch (Exception e) {
Logger.Log (LogLevel.Error, "monodroid", "Exception thrown while raising AppDomain.UnhandledException event: " + e.ToString ());
}
Expand Down
2 changes: 2 additions & 0 deletions src/monodroid/jni/monodroid-glue-internal.hh
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ namespace xamarin::android::internal
}

#if defined (NET6)
static void monodroid_unhandled_exception (MonoObject *java_exception);

MonoClass* get_android_runtime_class ();
#else // def NET6
MonoClass* get_android_runtime_class (MonoDomain *domain);
Expand Down
12 changes: 12 additions & 0 deletions src/monodroid/jni/monodroid-glue.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <mono/metadata/debug-helpers.h>
#include <mono/metadata/mono-config.h>
#include <mono/metadata/mono-debug.h>
#include <mono/metadata/object.h>
#include <mono/utils/mono-dl-fallback.h>
#include <mono/utils/mono-logger.h>

Expand Down Expand Up @@ -1009,6 +1010,9 @@ MonodroidRuntime::init_android_runtime (
{
mono_add_internal_call ("Java.Interop.TypeManager::monodroid_typemap_java_to_managed", reinterpret_cast<const void*>(typemap_java_to_managed));
mono_add_internal_call ("Android.Runtime.JNIEnv::monodroid_typemap_managed_to_java", reinterpret_cast<const void*>(typemap_managed_to_java));
#if defined (NET6)
mono_add_internal_call ("Android.Runtime.JNIEnv::monodroid_unhandled_exception", reinterpret_cast<const void*>(monodroid_unhandled_exception));
#endif // def NET6

struct JnienvInitializeArgs init = {};
init.javaVm = osBridge.get_jvm ();
Expand Down Expand Up @@ -1826,6 +1830,14 @@ MonodroidRuntime::create_and_initialize_domain (JNIEnv* env, jclass runtimeClass
return domain;
}

#if defined (NET6)
void
MonodroidRuntime::monodroid_unhandled_exception (MonoObject *java_exception)
{
mono_unhandled_exception (java_exception);
}
#endif // def NET6

MonoReflectionType*
MonodroidRuntime::typemap_java_to_managed (MonoString *java_type_name)
{
Expand Down
201 changes: 201 additions & 0 deletions tests/MSBuildDeviceIntegration/Tests/UncaughtExceptionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using NUnit.Framework;
using Xamarin.ProjectTools;

namespace Xamarin.Android.Build.Tests
{
[TestFixture]
[Category ("UsesDevice")]
public class UncaughtExceptionTests : DeviceTest
{
class LogcatLine
{
public string Text;
public bool Found = false;
public int SequenceNumber = -1;
public int Count = 0;
};

[Test]
public void EnsureUncaughtExceptionWorks ()
{
AssertHasDevices ();

var lib = new XamarinAndroidBindingProject {
ProjectName = "Scratch.Try",
AndroidClassParser = "class-parse",
};

lib.Imports.Add (
new Import (() => "Directory.Build.targets") {
TextContent = () =>
@"<Project>
<PropertyGroup>
<JavacSourceVersion>1.8</JavacSourceVersion>
<JavacTargetVersion>1.8</JavacTargetVersion>
<Javac>javac</Javac>
<Jar>jar</Jar>
</PropertyGroup>
<ItemGroup>
<JavaSource Include=""java\**\*.java"" />
<AndroidJavaSource Include=""@(JavaSource)"" />
</ItemGroup>
<ItemGroup>
<EmbeddedJar Include=""$(OutputPath)try.jar"" />
</ItemGroup>
<Target Name=""_BuildJar""
AfterTargets=""ResolveAssemblyReferences""
Inputs=""@(JavaSource);$(MSBuildThisFile)""
Outputs=""$(OutputPath)try.jar"">
<PropertyGroup>
<_Classes>$(IntermediateOutputPath)classes</_Classes>
</PropertyGroup>
<RemoveDir Directories=""$(_Classes)""/>
<MakeDir Directories=""$(_Classes)"" />
<Exec Command=""$(Javac) -source $(JavacSourceVersion) -target $(JavacTargetVersion) -d &quot;$(_Classes)&quot; @(JavaSource->'&quot;%(Identity)&quot;', ' ')"" />
<Exec Command=""$(Jar) cf &quot;$(OutputPath)try.jar&quot; -C &quot;$(_Classes)&quot; ."" />
</Target>
</Project>
"
});

lib.Sources.Add (
new BuildItem.NoActionResource ("java\\testing\\Run.java") {
Encoding = new System.Text.UTF8Encoding (encoderShouldEmitUTF8Identifier: false),
TextContent = () =>
@"package testing;
public final class Run {
private Run() {
}
public static interface CatchThrowableHandler {
void onCatch(Throwable t);
}
public static final void tryCatchFinally (Runnable r, CatchThrowableHandler c, Runnable f) {
try {
r.run();
}
catch (Throwable t) {
c.onCatch(t);
}
finally {
f.run();
}
}
}
"
});

var app = new XamarinAndroidApplicationProject {
ProjectName = "Scratch.JMJMException",
};

app.SetDefaultTargetDevice ();
app.AddReference (lib);

app.Sources.Remove (app.GetItem ("MainActivity.cs"));

string mainActivityTemplate = @"using System;
using Android.App;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Testing;
namespace Scratch.JMJMException
{
[Register (""${JAVA_PACKAGENAME}.MainActivity""), Activity (Label = ""${PROJECT_NAME}"", MainLauncher = true, Icon = ""@drawable/icon"")]
public class MainActivity : Activity
{
protected override void OnCreate (Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Button b = new Button (this) {
Text = ""Click Me!"",
};
Testing.Run.TryCatchFinally (
new Java.Lang.Runnable (() => {
Console.WriteLine (""#UET-1# jon: Should be in a Java > Managed [MainActivity.OnCreate] > Java [Run.tryCatchFinally] > Managed [Run] frame. Throwing an exception..."");
Console.WriteLine (new System.Diagnostics.StackTrace(fNeedFileInfo: true).ToString());
throw new Exception (""Should be in a Java > Managed [MainActivity.OnCreate] > Java [Run.tryCatchFinally] > Managed [Run] frame. Throwing an exception..."");
}),
new MyCatchHandler (),
new Java.Lang.Runnable (() => {
Console.WriteLine ($""#UET-3# jon: from Java finally block"");
})
);
SetContentView (b);
}
}
class MyCatchHandler : Java.Lang.Object, Run.ICatchThrowableHandler
{
public void OnCatch (Java.Lang.Throwable t)
{
Console.WriteLine ($""#UET-2# jon: MyCatchHandler.OnCatch: t={t.ToString()}"");
}
}
}
";
string mainActivity = app.ProcessSourceTemplate (mainActivityTemplate);
app.Sources.Add (
new BuildItem.Source ("MainActivity.cs") {
TextContent = () => mainActivity
}
);

var expectedLogLines = new LogcatLine[] {
new LogcatLine { Text = "#UET-1#" },
new LogcatLine { Text = "#UET-2#" },
new LogcatLine { Text = "#UET-3#" },
};

string path = Path.Combine ("temp", TestName);
using (var libBuilder = CreateDllBuilder (Path.Combine (path, lib.ProjectName)))
using (var appBuilder = CreateApkBuilder (Path.Combine (path, app.ProjectName))) {
Assert.True (libBuilder.Build (lib), "Library should have built.");
Assert.IsTrue (appBuilder.Install (app), "Install should have succeeded.");

ClearAdbLogcat ();

AdbStartActivity ($"{app.PackageName}/{app.JavaPackageName}.MainActivity");

string logcatPath = Path.Combine (Root, appBuilder.ProjectDirectory, "logcat.log");
int sequenceCounter = 0;
MonitorAdbLogcat (
(string line) => {
foreach (LogcatLine ll in expectedLogLines) {
if (line.IndexOf (ll.Text, StringComparison.Ordinal) < 0) {
continue;
}
ll.Found = true;
ll.Count++;
ll.SequenceNumber = sequenceCounter++;
break;
}
return false; // we must examine all the lines, and returning `true` aborts the monitoring process
}, logcatPath, 15);
}

AssertValidLine (0, 0);
AssertValidLine (1, 1);
AssertValidLine (2, 2);

void AssertValidLine (int idx, int expectedSequence)
{
LogcatLine ll = expectedLogLines [idx];
Assert.IsTrue (ll.Found, $"Logcat line {idx} was not found");
Assert.IsTrue (ll.Count == 1, $"Logcat line {idx} should have been found only once but it was found {ll.Count} times");
Assert.IsTrue (ll.SequenceNumber == expectedSequence, $"Logcat line {idx} sequence number should be {expectedSequence} but it was {ll.SequenceNumber}");
}
}
}
}

0 comments on commit 0f67df4

Please sign in to comment.