Skip to content

Commit fcf2793

Browse files
grendellojonpryor
authored andcommitted
[Mono.Android] Use mono_unhandled_exception for NET6 (#6104)
Context: dotnet/runtime#55904 (comment) Context: #4927 (comment) Context: #4927 (comment) Context: https://github.com/xamarin/monodroid/commit/7d62dec18ee1010fa9c96c206589827480e8d49c Context: dotnet/runtime@15ab9f9 Xamarin.Android has been using the `AppDomain.DoUnhandledException()` internal API since 2015 (xamarin/monodroid@7d62dec1) to propagate uncaught Java exceptions to the managed world. However, `AppDomain.DoUnhandledException()` was an internal API which was *removed* from MonoVM as part of the .NET 6 effort in dotnet/runtime@15ab9f98. For .NET 6, instead of calling the no-longer-present `AppDomain.DoUnhandledException()` method, call the [`mono_unhandled_exception()` embedding API][0], which in turn raises the `AppDomain.UnhandledException` event. Add a new internal call, `JNIEnv.monodroid_unhandled_exception()`, which is used on .NET 6 to invoke `mono_unhandled_exception()`. Add an on-device integration test which ensures that the uncaught exceptions are propagated as required. [0]: http://docs.go-mono.com/index.aspx?link=xhtml%3adeploy%2fmono-api-exc.html
1 parent 8ca2895 commit fcf2793

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)