Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Mono.Android/Android.Runtime/AndroidRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Delegate> prevent_delegate_gc = new List<Delegate> ();
static List<JniNativeMethodRegistration> sharedRegistrations = new List<JniNativeMethodRegistration> ();
Comment on lines +428 to 433

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prevent_delegate_gc is a shared mutable List<Delegate> which is appended to from RegisterNativeMembers(). List<T> is not thread-safe; RegisterNativeMembers() can be invoked concurrently for different types, and concurrent Add() calls can corrupt the list or throw, leading to app crashes. Consider protecting the collection with a lock, or switching to a thread-safe collection (e.g., ConcurrentBag<Delegate>), and ensure the chosen approach matches expected access patterns.

Copilot uses AI. Check for mistakes.

static bool FastRegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan<char> methods)
Expand Down Expand Up @@ -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;
Expand Down
89 changes: 89 additions & 0 deletions tests/MSBuildDeviceIntegration/Tests/MonoAndroidExportTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = $@"<?xml version=""1.0"" encoding=""utf-8""?>
<manifest xmlns:android=""http://schemas.android.com/apk/res/android"" android:versionCode=""1"" android:versionName=""1.0"" package=""{proj.PackageName}"">
<uses-sdk android:targetSdkVersion=""{apiLevel}"" />
<application android:label=""${{PROJECT_NAME}}"">
</application >
</manifest>";
Comment on lines +181 to +186

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manifest string uses package="${proj.PackageName}" inside an interpolated string, which will emit a leading $ in the package name (e.g., $com.example...). This produces an invalid manifest package and can cause build/install failures. Use package="{proj.PackageName}" (no $ inside), consistent with other device tests (e.g., DebuggingTest.cs).

Copilot uses AI. Check for mistakes.
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.");
}
}
}
}