From 9d61050790f615d6e0730c6585869d79269ad470 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:01:13 +0000 Subject: [PATCH 1/5] Initial plan From 6678c285c867302eb6a524df4405fb283a6cd1c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:31:20 +0000 Subject: [PATCH 2/5] Fix [Export] callback delegates not rooted after JNI registration on CoreCLR Root dynamically-created [Export] callback delegates in a static list to prevent CoreCLR's GC from collecting them between JNI registration and first invocation. Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/android/sessions/28ca1465-aa32-4295-9366-82c2a12b6a9b --- src/Mono.Android/Android.Runtime/AndroidRuntime.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index fb0a8df76ce..3cf1f0a3788 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -425,6 +425,10 @@ static Delegate CreateDynamicCallback (MethodInfo method) return (Delegate)dynamic_callback_gen.Invoke (null, new object [] { method })!; } + // [Export] callback delegates are created dynamically via DynamicCallbackCodeGenerator and are not + // cached in static fields (unlike non-[Export] connector delegates). Without rooting them here, + // CoreCLR's GC can collect them between JNI registration and first invocation, causing a crash. + static List prevent_delegate_gc = new List (); static List sharedRegistrations = new List (); static bool FastRegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) @@ -562,6 +566,7 @@ public override void RegisterNativeMembers ( if (minfo == null) throw new InvalidOperationException (FormattableString.Invariant ($"Specified managed method '{mname.ToString ()}' was not found. Signature: {signature.ToString ()}")); callback = CreateDynamicCallback (minfo); + prevent_delegate_gc.Add (callback); needToRegisterNatives = true; } else { Type callbackDeclaringType = type; From 414b356243aec9d42d46f2b9d6431c4ed0269c73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:13:22 +0000 Subject: [PATCH 3/5] Add test: ExportedMembersSurviveGarbageCollection Verifies [Export] delegates survive GC on CoreCLR by forcing aggressive GC cycles between delegate registration and JNI invocation, then calling the method through JNI (Java -> native delegate -> C#) to exercise the exact path that crashes when the delegate is collected. Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/android/sessions/9c9c2212-3532-4407-b394-a1b28d2a2c62 --- .../Tests/MonoAndroidExportTest.cs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/MSBuildDeviceIntegration/Tests/MonoAndroidExportTest.cs b/tests/MSBuildDeviceIntegration/Tests/MonoAndroidExportTest.cs index c023a7c588c..7467572db43 100644 --- a/tests/MSBuildDeviceIntegration/Tests/MonoAndroidExportTest.cs +++ b/tests/MSBuildDeviceIntegration/Tests/MonoAndroidExportTest.cs @@ -106,5 +106,94 @@ protected override void OnCreate (Bundle bundle) Assert.True (b.Uninstall (proj), "Project should have uninstalled."); } } + + [Test] + public void ExportedMembersSurviveGarbageCollection ( + [Values] bool isRelease, + [Values] AndroidRuntime runtime) + { + if (runtime == AndroidRuntime.NativeAOT) { + Assert.Ignore ("NativeAOT does not support Mono.Android.Export"); + } + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + AssertCommercialBuild (); + var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime)) { + IsRelease = isRelease, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.Sources.Add (new BuildItem.Source ("ContainsExportedMethods.cs") { + TextContent = () => @"using System; +using Java.Interop; + +namespace UnnamedProject { + class ContainsExportedMethods : Java.Lang.Object { + [Export] + public void Exported () + { + Console.WriteLine (""# ExportedCallbackInvoked""); + } + } +} +", + }); + proj.MainActivity = @"using System; +using Android.App; +using Android.OS; +using Android.Runtime; + +namespace UnnamedProject +{ + [Activity (Label = ""UnnamedProject"", MainLauncher = true, Icon = ""@drawable/icon"")] + public class MainActivity : Activity { + protected override void OnCreate (Bundle bundle) + { + base.OnCreate (bundle); + + var foo = new ContainsExportedMethods (); + + // Force GC to collect any unrooted delegates + for (int i = 0; i < 10; i++) { + GC.Collect (); + GC.WaitForPendingFinalizers (); + } + + // Invoke the [Export] method through JNI (Java -> native delegate -> C#) + // This path crashes with SIGABRT if the delegate was garbage collected + IntPtr klass = JNIEnv.GetObjectClass (foo.Handle); + IntPtr methodId = JNIEnv.GetMethodID (klass, ""Exported"", ""()V""); + JNIEnv.CallVoidMethod (foo.Handle, methodId); + + Console.WriteLine (""# ExportCallbackSurvivedGC""); + } + } +}"; + proj.SetAndroidSupportedAbis (DeviceAbi); + proj.SetDefaultTargetDevice (); + using (var b = CreateApkBuilder ()) { + b.LatestTargetFrameworkVersion (out string apiLevel); + proj.SupportedOSPlatformVersion = "24.0"; + proj.AndroidManifest = $@" + + + + +"; + Assert.True (b.Install (proj), "Project should have installed."); + RunProjectAndAssert (proj, b, doNotCleanupOnUpdate: true); + Assert.True (WaitForActivityToStart (proj.PackageName, "MainActivity", + Path.Combine (Root, b.ProjectDirectory, "logcat.log"), 30), "Activity should have started."); + string expectedLogcatOutput = "ExportCallbackSurvivedGC"; + Assert.IsTrue (MonitorAdbLogcat ((line) => { + return line.Contains (expectedLogcatOutput); + }, Path.Combine (Root, b.ProjectDirectory, "startup-logcat.log"), 45), $"Output did not contain {expectedLogcatOutput}!"); + Assert.True (b.Uninstall (proj), "Project should have uninstalled."); + } + } } } From abccc786864c3d3e713dc727628ce67a287c26d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:30:59 +0000 Subject: [PATCH 4/5] Address review comments: thread-safe delegate rooting and fix manifest package name - Protect prevent_delegate_gc with a dedicated lock object since RegisterNativeMembers can be called concurrently for different types - Fix manifest package attribute in ExportedMembersSurviveGarbageCollection test: remove erroneous $ prefix that produced invalid package name Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/android/sessions/d78cdf0b-7890-4fe2-b5b3-8b10f5d6f0f4 --- src/Mono.Android/Android.Runtime/AndroidRuntime.cs | 7 +++++-- .../Tests/MonoAndroidExportTest.cs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 3cf1f0a3788..ec1c95eda5b 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -428,7 +428,8 @@ static Delegate CreateDynamicCallback (MethodInfo method) // [Export] callback delegates are created dynamically via DynamicCallbackCodeGenerator and are not // cached in static fields (unlike non-[Export] connector delegates). Without rooting them here, // CoreCLR's GC can collect them between JNI registration and first invocation, causing a crash. - static List prevent_delegate_gc = new List (); + static readonly object prevent_delegate_gc_lock = new object (); + static readonly List prevent_delegate_gc = new List (); static List sharedRegistrations = new List (); static bool FastRegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) @@ -566,7 +567,9 @@ public override void RegisterNativeMembers ( if (minfo == null) throw new InvalidOperationException (FormattableString.Invariant ($"Specified managed method '{mname.ToString ()}' was not found. Signature: {signature.ToString ()}")); callback = CreateDynamicCallback (minfo); - prevent_delegate_gc.Add (callback); + lock (prevent_delegate_gc_lock) { + prevent_delegate_gc.Add (callback); + } needToRegisterNatives = true; } else { Type callbackDeclaringType = type; diff --git a/tests/MSBuildDeviceIntegration/Tests/MonoAndroidExportTest.cs b/tests/MSBuildDeviceIntegration/Tests/MonoAndroidExportTest.cs index 7467572db43..f0d99b17647 100644 --- a/tests/MSBuildDeviceIntegration/Tests/MonoAndroidExportTest.cs +++ b/tests/MSBuildDeviceIntegration/Tests/MonoAndroidExportTest.cs @@ -179,7 +179,7 @@ protected override void OnCreate (Bundle bundle) b.LatestTargetFrameworkVersion (out string apiLevel); proj.SupportedOSPlatformVersion = "24.0"; proj.AndroidManifest = $@" - + From f18729b7b8d4b3f9556ed92da5bd83fca09f5c9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:12:51 +0000 Subject: [PATCH 5/5] Use System.Threading.Lock instead of object for prevent_delegate_gc_lock Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/android/sessions/b3afee17-6668-4279-a4cd-28aa337d19ac --- src/Mono.Android/Android.Runtime/AndroidRuntime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index ec1c95eda5b..883e3c6efe1 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -428,7 +428,7 @@ static Delegate CreateDynamicCallback (MethodInfo method) // [Export] callback delegates are created dynamically via DynamicCallbackCodeGenerator and are not // cached in static fields (unlike non-[Export] connector delegates). Without rooting them here, // CoreCLR's GC can collect them between JNI registration and first invocation, causing a crash. - static readonly object prevent_delegate_gc_lock = new object (); + static readonly Lock prevent_delegate_gc_lock = new Lock (); static readonly List prevent_delegate_gc = new List (); static List sharedRegistrations = new List ();