Skip to content

Commit 0f67df4

Browse files
committed
Use mono_unhandled_exception for NET6
Context: dotnet/runtime#55904 (comment) Context: #4927 (comment) Context: #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.
1 parent 1e5bfa3 commit 0f67df4

File tree

4 files changed

+229
-1
lines changed

4 files changed

+229
-1
lines changed

src/Mono.Android/Android.Runtime/JNIEnv.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,9 @@ static void ManualJavaObjectDispose (Java.Lang.Object obj)
242242
}
243243

244244
static Action<Exception> mono_unhandled_exception = null!;
245+
#if !NETCOREAPP
245246
static Action<AppDomain, UnhandledExceptionEventArgs> AppDomain_DoUnhandledException = null!;
247+
#endif // ndef NETCOREAPP
246248

247249
static void Initialize ()
248250
{
@@ -253,6 +255,7 @@ static void Initialize ()
253255
mono_unhandled_exception = (Action<Exception>) Delegate.CreateDelegate (typeof(Action<Exception>), mono_UnhandledException);
254256
}
255257

258+
#if !NETCOREAPP
256259
if (AppDomain_DoUnhandledException == null) {
257260
var ad_due = typeof (AppDomain)
258261
.GetMethod ("DoUnhandledException",
@@ -265,8 +268,14 @@ static void Initialize ()
265268
typeof (Action<AppDomain, UnhandledExceptionEventArgs>), ad_due);
266269
}
267270
}
271+
#endif // ndef NETCOREAPP
268272
}
269273

274+
#if NETCOREAPP
275+
[MethodImplAttribute(MethodImplOptions.InternalCall)]
276+
extern static void monodroid_unhandled_exception (Exception javaException);
277+
#endif // def NETCOREAPP
278+
270279
internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPtr, IntPtr javaExceptionPtr)
271280
{
272281
if (!PropagateExceptions)
@@ -287,14 +296,18 @@ internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPt
287296
try {
288297
var jltp = javaException as JavaProxyThrowable;
289298
Exception? innerException = jltp?.InnerException;
290-
var args = new UnhandledExceptionEventArgs (innerException ?? javaException, isTerminating: true);
291299

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

303+
#if !NETCOREAPP
304+
var args = new UnhandledExceptionEventArgs (innerException ?? javaException, isTerminating: true);
295305
// Disabled until Linker error surfaced in https://github.com/xamarin/xamarin-android/pull/4302#issuecomment-596400025 is resolved
296306
//AppDomain.CurrentDomain.DoUnhandledException (args);
297307
AppDomain_DoUnhandledException?.Invoke (AppDomain.CurrentDomain, args);
308+
#else // ndef NETCOREAPP
309+
monodroid_unhandled_exception (innerException ?? javaException);
310+
#endif // def NETCOREAPP
298311
} catch (Exception e) {
299312
Logger.Log (LogLevel.Error, "monodroid", "Exception thrown while raising AppDomain.UnhandledException event: " + e.ToString ());
300313
}

src/monodroid/jni/monodroid-glue-internal.hh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ namespace xamarin::android::internal
261261
}
262262

263263
#if defined (NET6)
264+
static void monodroid_unhandled_exception (MonoObject *java_exception);
265+
264266
MonoClass* get_android_runtime_class ();
265267
#else // def NET6
266268
MonoClass* get_android_runtime_class (MonoDomain *domain);

src/monodroid/jni/monodroid-glue.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <mono/metadata/debug-helpers.h>
2828
#include <mono/metadata/mono-config.h>
2929
#include <mono/metadata/mono-debug.h>
30+
#include <mono/metadata/object.h>
3031
#include <mono/utils/mono-dl-fallback.h>
3132
#include <mono/utils/mono-logger.h>
3233

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

10131017
struct JnienvInitializeArgs init = {};
10141018
init.javaVm = osBridge.get_jvm ();
@@ -1826,6 +1830,14 @@ MonodroidRuntime::create_and_initialize_domain (JNIEnv* env, jclass runtimeClass
18261830
return domain;
18271831
}
18281832

1833+
#if defined (NET6)
1834+
void
1835+
MonodroidRuntime::monodroid_unhandled_exception (MonoObject *java_exception)
1836+
{
1837+
mono_unhandled_exception (java_exception);
1838+
}
1839+
#endif // def NET6
1840+
18291841
MonoReflectionType*
18301842
MonodroidRuntime::typemap_java_to_managed (MonoString *java_type_name)
18311843
{
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text.RegularExpressions;
5+
using NUnit.Framework;
6+
using Xamarin.ProjectTools;
7+
8+
namespace Xamarin.Android.Build.Tests
9+
{
10+
[TestFixture]
11+
[Category ("UsesDevice")]
12+
public class UncaughtExceptionTests : DeviceTest
13+
{
14+
class LogcatLine
15+
{
16+
public string Text;
17+
public bool Found = false;
18+
public int SequenceNumber = -1;
19+
public int Count = 0;
20+
};
21+
22+
[Test]
23+
public void EnsureUncaughtExceptionWorks ()
24+
{
25+
AssertHasDevices ();
26+
27+
var lib = new XamarinAndroidBindingProject {
28+
ProjectName = "Scratch.Try",
29+
AndroidClassParser = "class-parse",
30+
};
31+
32+
lib.Imports.Add (
33+
new Import (() => "Directory.Build.targets") {
34+
TextContent = () =>
35+
@"<Project>
36+
<PropertyGroup>
37+
<JavacSourceVersion>1.8</JavacSourceVersion>
38+
<JavacTargetVersion>1.8</JavacTargetVersion>
39+
<Javac>javac</Javac>
40+
<Jar>jar</Jar>
41+
</PropertyGroup>
42+
<ItemGroup>
43+
<JavaSource Include=""java\**\*.java"" />
44+
<AndroidJavaSource Include=""@(JavaSource)"" />
45+
</ItemGroup>
46+
<ItemGroup>
47+
<EmbeddedJar Include=""$(OutputPath)try.jar"" />
48+
</ItemGroup>
49+
<Target Name=""_BuildJar""
50+
AfterTargets=""ResolveAssemblyReferences""
51+
Inputs=""@(JavaSource);$(MSBuildThisFile)""
52+
Outputs=""$(OutputPath)try.jar"">
53+
<PropertyGroup>
54+
<_Classes>$(IntermediateOutputPath)classes</_Classes>
55+
</PropertyGroup>
56+
<RemoveDir Directories=""$(_Classes)""/>
57+
<MakeDir Directories=""$(_Classes)"" />
58+
<Exec Command=""$(Javac) -source $(JavacSourceVersion) -target $(JavacTargetVersion) -d &quot;$(_Classes)&quot; @(JavaSource->'&quot;%(Identity)&quot;', ' ')"" />
59+
<Exec Command=""$(Jar) cf &quot;$(OutputPath)try.jar&quot; -C &quot;$(_Classes)&quot; ."" />
60+
</Target>
61+
</Project>
62+
"
63+
});
64+
65+
lib.Sources.Add (
66+
new BuildItem.NoActionResource ("java\\testing\\Run.java") {
67+
Encoding = new System.Text.UTF8Encoding (encoderShouldEmitUTF8Identifier: false),
68+
TextContent = () =>
69+
@"package testing;
70+
71+
public final class Run {
72+
private Run() {
73+
}
74+
75+
public static interface CatchThrowableHandler {
76+
void onCatch(Throwable t);
77+
}
78+
79+
public static final void tryCatchFinally (Runnable r, CatchThrowableHandler c, Runnable f) {
80+
try {
81+
r.run();
82+
}
83+
catch (Throwable t) {
84+
c.onCatch(t);
85+
}
86+
finally {
87+
f.run();
88+
}
89+
}
90+
}
91+
"
92+
});
93+
94+
var app = new XamarinAndroidApplicationProject {
95+
ProjectName = "Scratch.JMJMException",
96+
};
97+
98+
app.SetDefaultTargetDevice ();
99+
app.AddReference (lib);
100+
101+
app.Sources.Remove (app.GetItem ("MainActivity.cs"));
102+
103+
string mainActivityTemplate = @"using System;
104+
using Android.App;
105+
using Android.OS;
106+
using Android.Runtime;
107+
using Android.Views;
108+
using Android.Widget;
109+
using Testing;
110+
111+
namespace Scratch.JMJMException
112+
{
113+
[Register (""${JAVA_PACKAGENAME}.MainActivity""), Activity (Label = ""${PROJECT_NAME}"", MainLauncher = true, Icon = ""@drawable/icon"")]
114+
public class MainActivity : Activity
115+
{
116+
protected override void OnCreate (Bundle savedInstanceState)
117+
{
118+
base.OnCreate(savedInstanceState);
119+
Button b = new Button (this) {
120+
Text = ""Click Me!"",
121+
};
122+
123+
Testing.Run.TryCatchFinally (
124+
new Java.Lang.Runnable (() => {
125+
Console.WriteLine (""#UET-1# jon: Should be in a Java > Managed [MainActivity.OnCreate] > Java [Run.tryCatchFinally] > Managed [Run] frame. Throwing an exception..."");
126+
Console.WriteLine (new System.Diagnostics.StackTrace(fNeedFileInfo: true).ToString());
127+
throw new Exception (""Should be in a Java > Managed [MainActivity.OnCreate] > Java [Run.tryCatchFinally] > Managed [Run] frame. Throwing an exception..."");
128+
}),
129+
new MyCatchHandler (),
130+
new Java.Lang.Runnable (() => {
131+
Console.WriteLine ($""#UET-3# jon: from Java finally block"");
132+
})
133+
);
134+
135+
SetContentView (b);
136+
}
137+
}
138+
139+
class MyCatchHandler : Java.Lang.Object, Run.ICatchThrowableHandler
140+
{
141+
public void OnCatch (Java.Lang.Throwable t)
142+
{
143+
Console.WriteLine ($""#UET-2# jon: MyCatchHandler.OnCatch: t={t.ToString()}"");
144+
}
145+
}
146+
}
147+
";
148+
string mainActivity = app.ProcessSourceTemplate (mainActivityTemplate);
149+
app.Sources.Add (
150+
new BuildItem.Source ("MainActivity.cs") {
151+
TextContent = () => mainActivity
152+
}
153+
);
154+
155+
var expectedLogLines = new LogcatLine[] {
156+
new LogcatLine { Text = "#UET-1#" },
157+
new LogcatLine { Text = "#UET-2#" },
158+
new LogcatLine { Text = "#UET-3#" },
159+
};
160+
161+
string path = Path.Combine ("temp", TestName);
162+
using (var libBuilder = CreateDllBuilder (Path.Combine (path, lib.ProjectName)))
163+
using (var appBuilder = CreateApkBuilder (Path.Combine (path, app.ProjectName))) {
164+
Assert.True (libBuilder.Build (lib), "Library should have built.");
165+
Assert.IsTrue (appBuilder.Install (app), "Install should have succeeded.");
166+
167+
ClearAdbLogcat ();
168+
169+
AdbStartActivity ($"{app.PackageName}/{app.JavaPackageName}.MainActivity");
170+
171+
string logcatPath = Path.Combine (Root, appBuilder.ProjectDirectory, "logcat.log");
172+
int sequenceCounter = 0;
173+
MonitorAdbLogcat (
174+
(string line) => {
175+
foreach (LogcatLine ll in expectedLogLines) {
176+
if (line.IndexOf (ll.Text, StringComparison.Ordinal) < 0) {
177+
continue;
178+
}
179+
ll.Found = true;
180+
ll.Count++;
181+
ll.SequenceNumber = sequenceCounter++;
182+
break;
183+
}
184+
return false; // we must examine all the lines, and returning `true` aborts the monitoring process
185+
}, logcatPath, 15);
186+
}
187+
188+
AssertValidLine (0, 0);
189+
AssertValidLine (1, 1);
190+
AssertValidLine (2, 2);
191+
192+
void AssertValidLine (int idx, int expectedSequence)
193+
{
194+
LogcatLine ll = expectedLogLines [idx];
195+
Assert.IsTrue (ll.Found, $"Logcat line {idx} was not found");
196+
Assert.IsTrue (ll.Count == 1, $"Logcat line {idx} should have been found only once but it was found {ll.Count} times");
197+
Assert.IsTrue (ll.SequenceNumber == expectedSequence, $"Logcat line {idx} sequence number should be {expectedSequence} but it was {ll.SequenceNumber}");
198+
}
199+
}
200+
}
201+
}

0 commit comments

Comments
 (0)