diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index fb0a8df76ce..883e3c6efe1 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -425,6 +425,11 @@ 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 readonly Lock prevent_delegate_gc_lock = new Lock (); + static readonly List prevent_delegate_gc = new List (); static List sharedRegistrations = new List (); static bool FastRegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) @@ -562,6 +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); + 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 c023a7c588c..f0d99b17647 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."); + } + } } }