From 908d435fe93b5db0a08dbb19a93200f2176774b6 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 7 Aug 2026 13:10:57 -0500 Subject: [PATCH 1/5] Fix premature JNI handle collection Keep managed JNI handle owners alive until native calls have consumed their handles, and return stable local references for temporary wrappers. Add concurrent collection coverage for ToLocalJniHandle. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6105414-1c2c-4696-8d01-6507bda3e4be --- .../Android.Runtime/AndroidEnvironment.cs | 1 + src/Mono.Android/Android.Runtime/JNIEnv.cs | 8 +-- .../Android.Runtime/JavaObject.cs | 25 +--------- .../Android.Runtime/XmlReaderPullParser.cs | 14 ++++-- .../Android.Widget/AbsListView.cs | 2 + .../Android.Widget/AdapterView.cs | 6 ++- .../Android.Widget/AdapterViewAnimator.cs | 9 +++- .../Android.Widget/ArrayAdapter.cs | 12 ++++- src/Mono.Android/Android.Widget/TextView.cs | 3 +- src/Mono.Android/Java.Interop/JavaConvert.cs | 5 +- .../Java.Interop/JavaObjectExtensions.cs | 8 ++- src/Mono.Android/Java.Interop/Runtime.cs | 4 +- .../AndroidMessageHandler.cs | 1 + .../Java.Interop/JnienvTest.cs | 49 +++++++++++++++++++ 14 files changed, 107 insertions(+), 40 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidEnvironment.cs b/src/Mono.Android/Android.Runtime/AndroidEnvironment.cs index cd487e8e3e4..35c8a951877 100644 --- a/src/Mono.Android/Android.Runtime/AndroidEnvironment.cs +++ b/src/Mono.Android/Android.Runtime/AndroidEnvironment.cs @@ -105,6 +105,7 @@ public static void RaiseThrowable (Java.Lang.Throwable throwable) if (throwable == null) throw new ArgumentNullException ("throwable"); JNIEnv.Throw (throwable.Handle); + GC.KeepAlive (throwable); } internal static void UnhandledException (Exception e) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 2b014fe3a49..681fbbfb87d 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -506,9 +506,9 @@ public static IntPtr ToLocalJniHandle (IJavaObject? value) if (value == null) return IntPtr.Zero; var ex = value as IJavaObjectEx; - if (ex != null) - return ex.ToLocalJniHandle (); - return NewLocalRef (value.Handle); + IntPtr result = ex != null ? ex.ToLocalJniHandle () : NewLocalRef (value.Handle); + GC.KeepAlive (value); + return result; } public static string? GetCharSequence (IntPtr jobject, JniHandleOwnership transfer) @@ -895,6 +895,7 @@ public static void CopyArray (IJavaObject[] src, IntPtr dest) for (int i = 0; i < src.Length; i++) { IJavaObject o = src [i]; JniEnvironment.Arrays.SetObjectArrayElement (new JniObjectReference (dest), i, new JniObjectReference (o == null ? IntPtr.Zero : o.Handle)); + GC.KeepAlive (o); } } @@ -1545,6 +1546,7 @@ static IntPtr NewArray (Array value, Type elementType, IntPtr elementClass) } }, { typeof (IJavaObject), (dest, index, value) => { SetObjectArrayElement (dest, index, value == null ? IntPtr.Zero : ((IJavaObject) value).Handle); + GC.KeepAlive (value); } }, { typeof (Array), (dest, index, value) => { IntPtr _v = NewArray ((Array) value!); diff --git a/src/Mono.Android/Android.Runtime/JavaObject.cs b/src/Mono.Android/Android.Runtime/JavaObject.cs index ec968131f99..e1ddbbecae3 100644 --- a/src/Mono.Android/Android.Runtime/JavaObject.cs +++ b/src/Mono.Android/Android.Runtime/JavaObject.cs @@ -11,29 +11,7 @@ public static IntPtr GetHandle (object obj) if (obj == null) return IntPtr.Zero; - Type type = obj.GetType (); - if (type == typeof (bool)) - return new Java.Lang.Boolean ((bool)obj).Handle; - else if (type == typeof (sbyte)) - return new Java.Lang.Byte ((sbyte)obj).Handle; - else if (type == typeof (char)) - return new Java.Lang.Character ((char)obj).Handle; - else if (type == typeof (short)) - return new Java.Lang.Short ((short)obj).Handle; - else if (type == typeof (int)) - return new Java.Lang.Integer ((int)obj).Handle; - else if (type == typeof (long)) - return new Java.Lang.Long ((long)obj).Handle; - else if (type == typeof (float)) - return new Java.Lang.Float ((float)obj).Handle; - else if (type == typeof (double)) - return new Java.Lang.Double ((double)obj).Handle; - else if (type == typeof (string)) - return JNIEnv.NewString ((string)obj); - else if (typeof (IJavaObject).IsAssignableFrom (type)) - return ((IJavaObject)obj).Handle; - else - return new JavaObject (obj).Handle; + return Java.Interop.JavaConvert.ToLocalJniHandle (obj); } public static object? GetObject (IntPtr handle, JniHandleOwnership transfer) @@ -112,4 +90,3 @@ public override string ToString () } } } - diff --git a/src/Mono.Android/Android.Runtime/XmlReaderPullParser.cs b/src/Mono.Android/Android.Runtime/XmlReaderPullParser.cs index bcf54a12161..d3b9a53439d 100644 --- a/src/Mono.Android/Android.Runtime/XmlReaderPullParser.cs +++ b/src/Mono.Android/Android.Runtime/XmlReaderPullParser.cs @@ -15,8 +15,11 @@ public class XmlReaderResourceParser : XmlReaderPullParser, IXmlResourceParser return IntPtr.Zero; var xpr = value as XmlResourceParserReader; - if (xpr != null) - return JNIEnv.NewLocalRef (xpr.Handle); + if (xpr != null) { + IntPtr result = JNIEnv.NewLocalRef (xpr.Handle); + GC.KeepAlive (xpr); + return result; + } return JNIEnv.ToLocalJniHandle (new Android.Runtime.XmlReaderResourceParser (value)); } @@ -130,8 +133,11 @@ public static IntPtr ToLocalJniHandle (XmlReader? value) return IntPtr.Zero; var xppr = value as XmlPullParserReader; - if (xppr != null) - return JNIEnv.NewLocalRef (xppr.Handle); + if (xppr != null) { + IntPtr result = JNIEnv.NewLocalRef (xppr.Handle); + GC.KeepAlive (xppr); + return result; + } return JNIEnv.ToLocalJniHandle (new Android.Runtime.XmlReaderPullParser (value)); } diff --git a/src/Mono.Android/Android.Widget/AbsListView.cs b/src/Mono.Android/Android.Widget/AbsListView.cs index dbacdcccc2e..c989aec88dc 100644 --- a/src/Mono.Android/Android.Widget/AbsListView.cs +++ b/src/Mono.Android/Android.Widget/AbsListView.cs @@ -79,6 +79,8 @@ public virtual void SetAdapter (Android.Widget.IListAdapter adapter) JNIEnv.CallVoidMethod (Handle, id_setAdapter_Landroid_widget_ListAdapter_, new JValue (adapter)); else JNIEnv.CallNonvirtualVoidMethod (Handle, ThresholdClass, id_setAdapter_Landroid_widget_ListAdapter_, new JValue (adapter)); + GC.KeepAlive (adapter); + GC.KeepAlive (this); } #endif } diff --git a/src/Mono.Android/Android.Widget/AdapterView.cs b/src/Mono.Android/Android.Widget/AdapterView.cs index ec7948632d3..393f81aebbf 100644 --- a/src/Mono.Android/Android.Widget/AdapterView.cs +++ b/src/Mono.Android/Android.Widget/AdapterView.cs @@ -81,6 +81,7 @@ public AdapterView (Android.Content.Context context) JniHandleOwnership.TransferLocalRef); JNIEnv.FinishCreateInstance (Handle, "(Landroid/content/Context;)V", new JValue (context)); } + GC.KeepAlive (context); } static IntPtr id_ctor_Landroid_content_Context_Landroid_util_AttributeSet_; @@ -104,6 +105,8 @@ public AdapterView (Android.Content.Context context, Android.Util.IAttributeSet JniHandleOwnership.TransferLocalRef); JNIEnv.FinishCreateInstance (Handle, "(Landroid/content/Context;Landroid/util/AttributeSet;)V", new JValue (context), new JValue (attrs)); } + GC.KeepAlive (context); + GC.KeepAlive (attrs); } static IntPtr id_ctor_Landroid_content_Context_Landroid_util_AttributeSet_I; @@ -127,6 +130,8 @@ public AdapterView (Android.Content.Context context, Android.Util.IAttributeSet JniHandleOwnership.TransferLocalRef); JNIEnv.FinishCreateInstance (Handle, "(Landroid/content/Context;Landroid/util/AttributeSet;I)V", new JValue (context), new JValue (attrs), new JValue (defStyle)); } + GC.KeepAlive (context); + GC.KeepAlive (attrs); } protected override Java.Lang.Object? RawAdapter { @@ -140,4 +145,3 @@ public abstract T Adapter { } } } - diff --git a/src/Mono.Android/Android.Widget/AdapterViewAnimator.cs b/src/Mono.Android/Android.Widget/AdapterViewAnimator.cs index 3bc26268995..26670405146 100644 --- a/src/Mono.Android/Android.Widget/AdapterViewAnimator.cs +++ b/src/Mono.Android/Android.Widget/AdapterViewAnimator.cs @@ -86,15 +86,18 @@ public Android.Widget.IAdapter? Adapter { get { if (id_getAdapter == IntPtr.Zero) id_getAdapter = JNIEnv.GetMethodID (class_ref, "getAdapter", "()Landroid/widget/Adapter;"); + Android.Widget.IAdapter? result; if (GetType () == ThresholdType) - return Java.Lang.Object.GetObject (JNIEnv.CallObjectMethod (Handle, id_getAdapter), JniHandleOwnership.TransferLocalRef); + result = Java.Lang.Object.GetObject (JNIEnv.CallObjectMethod (Handle, id_getAdapter), JniHandleOwnership.TransferLocalRef); else - return Java.Lang.Object.GetObject ( + result = Java.Lang.Object.GetObject ( JNIEnv.CallNonvirtualObjectMethod ( Handle, ThresholdClass, JNIEnv.GetMethodID (ThresholdClass, "getAdapter", "()Landroid/widget/Adapter;")), JniHandleOwnership.TransferLocalRef); + GC.KeepAlive (this); + return result; } set { if (id_setAdapter_Landroid_widget_Adapter_ == IntPtr.Zero) @@ -108,6 +111,8 @@ public Android.Widget.IAdapter? Adapter { ThresholdClass, JNIEnv.GetMethodID (ThresholdClass, "setAdapter", "(Landroid/widget/Adapter;)V"), new JValue (JNIEnv.ToJniHandle ((IJavaObject?) value))); + GC.KeepAlive (value); + GC.KeepAlive (this); } } diff --git a/src/Mono.Android/Android.Widget/ArrayAdapter.cs b/src/Mono.Android/Android.Widget/ArrayAdapter.cs index 134ea2ab2b0..899710a3f3f 100644 --- a/src/Mono.Android/Android.Widget/ArrayAdapter.cs +++ b/src/Mono.Android/Android.Widget/ArrayAdapter.cs @@ -39,6 +39,7 @@ public ArrayAdapter (Android.Content.Context context, int textViewResourceId) JniHandleOwnership.TransferLocalRef); JNIEnv.FinishCreateInstance (Handle, "(Landroid/content/Context;I)V", new JValue (context), new JValue (textViewResourceId)); } + GC.KeepAlive (context); } static IntPtr id_ctor_Landroid_content_Context_II; @@ -62,6 +63,7 @@ public ArrayAdapter (Android.Content.Context context, int resource, int textView JniHandleOwnership.TransferLocalRef); JNIEnv.FinishCreateInstance (Handle, "(Landroid/content/Context;II)V", new JValue (context), new JValue (resource), new JValue (textViewResourceId)); } + GC.KeepAlive (context); } static IntPtr id_ctor_Landroid_content_Context_IarrayLjava_lang_Object_; @@ -87,6 +89,7 @@ public ArrayAdapter (Android.Content.Context context, int textViewResourceId, T[ JNIEnv.FinishCreateInstance (Handle, "(Landroid/content/Context;I[Ljava/lang/Object;)V", new JValue (context), new JValue (textViewResourceId), new JValue (native_objects)); } JNIEnv.DeleteLocalRef (native_objects); + GC.KeepAlive (context); } static IntPtr id_ctor_Landroid_content_Context_IIarrayLjava_lang_Object_; @@ -112,6 +115,7 @@ public ArrayAdapter (Android.Content.Context context, int resource, int textView JNIEnv.FinishCreateInstance (Handle, "(Landroid/content/Context;II[Ljava/lang/Object;)V", new JValue (context), new JValue (resource), new JValue (textViewResourceId), new JValue (native_objects)); } JNIEnv.DeleteLocalRef (native_objects); + GC.KeepAlive (context); } static IntPtr id_ctor_Landroid_content_Context_ILjava_util_List_; @@ -137,6 +141,7 @@ public ArrayAdapter (Android.Content.Context context, int textViewResourceId, Sy JNIEnv.FinishCreateInstance (Handle, "(Landroid/content/Context;ILjava/util/List;)V", new JValue (context), new JValue (textViewResourceId), new JValue (lrefObjects)); } JNIEnv.DeleteLocalRef (lrefObjects); + GC.KeepAlive (context); } static IntPtr id_ctor_Landroid_content_Context_IILjava_util_List_; @@ -162,6 +167,7 @@ public ArrayAdapter (Android.Content.Context context, int resource, int textView JNIEnv.FinishCreateInstance (Handle, "(Landroid/content/Context;IILjava/util/List;)V", new JValue (context), new JValue (resource), new JValue (textViewResourceId), new JValue (lrefObjects)); } JNIEnv.DeleteLocalRef (lrefObjects); + GC.KeepAlive (context); } static IntPtr id_add_Ljava_lang_Object_; @@ -182,9 +188,11 @@ public void Add (T @object) { if (id_createFromResource_Landroid_content_Context_II == IntPtr.Zero) id_createFromResource_Landroid_content_Context_II = JNIEnv.GetStaticMethodID (class_ref, "createFromResource", "(Landroid/content/Context;II)Landroid/widget/ArrayAdapter;"); - return JavaConvert.FromJniHandle> ( + var result = JavaConvert.FromJniHandle> ( JNIEnv.CallStaticObjectMethod (class_ref, id_createFromResource_Landroid_content_Context_II, new JValue (context), new JValue (textArrayResId), new JValue (textViewResId)), JniHandleOwnership.TransferLocalRef)!; + GC.KeepAlive (context); + return result; } static IntPtr id_getItem_I; @@ -239,6 +247,8 @@ public void Remove (T @object) if (id_sort_Ljava_util_Comparator_ == IntPtr.Zero) id_sort_Ljava_util_Comparator_ = JNIEnv.GetMethodID (class_ref, "sort", "(Ljava/util/Comparator;)V"); JNIEnv.CallNonvirtualVoidMethod (Handle, class_ref, id_sort_Ljava_util_Comparator_, new JValue (comparator)); + GC.KeepAlive (comparator); + GC.KeepAlive (this); } } } diff --git a/src/Mono.Android/Android.Widget/TextView.cs b/src/Mono.Android/Android.Widget/TextView.cs index cef5e050088..5465af9f4b4 100644 --- a/src/Mono.Android/Android.Widget/TextView.cs +++ b/src/Mono.Android/Android.Widget/TextView.cs @@ -13,6 +13,8 @@ void AddTextChangedListener (TextWatcherImplementor watcher) if (id_addTextChangedListener == IntPtr.Zero) id_addTextChangedListener = JNIEnv.GetMethodID (class_ref, "addTextChangedListener", "(Landroid/text/TextWatcher;)V"); JNIEnv.CallVoidMethod (Handle, id_addTextChangedListener, new JValue (watcher)); + GC.KeepAlive (watcher); + GC.KeepAlive (this); } WeakReference? implementor_TextWatcher; @@ -69,4 +71,3 @@ public event EventHandler TextChanged { } } } - diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index 51d16713029..4e305223a7c 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -527,7 +527,10 @@ public static T? FromJavaObject< return JNIEnv.NewArray ((Array) value); } }, { typeof (Android.Runtime.JavaObject), value => { - return value == null ? IntPtr.Zero : JNIEnv.ToLocalJniHandle (new Android.Runtime.JavaObject (value)); + if (value == null) + return IntPtr.Zero; + using (var v = new Android.Runtime.JavaObject (value)) + return JNIEnv.ToLocalJniHandle (v); } }, }; diff --git a/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs b/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs index a992e187779..dd154087d0e 100644 --- a/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs +++ b/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs @@ -81,7 +81,9 @@ internal static TResult? _JavaCast< if (instance.Handle == IntPtr.Zero) throw new ObjectDisposedException (instance.GetType ().FullName); - return (TResult?) Java.Lang.Object.GetObject (instance.Handle, JniHandleOwnership.DoNotTransfer, typeof (TResult)) ?? + var result = (TResult?) Java.Lang.Object.GetObject (instance.Handle, JniHandleOwnership.DoNotTransfer, typeof (TResult)); + GC.KeepAlive (instance); + return result ?? throw new InvalidCastException ( FormattableString.Invariant ($"Unable to convert instance of type '{instance.GetType ().FullName}' to type '{typeof (TResult).FullName}'.")); } @@ -100,7 +102,9 @@ internal static TResult? _JavaCast< if (resultType.IsAssignableFrom (instance.GetType ())) return instance; - return (IJavaObject?) Java.Lang.Object.GetObject (instance.Handle, JniHandleOwnership.DoNotTransfer, resultType) ?? + var result = (IJavaObject?) Java.Lang.Object.GetObject (instance.Handle, JniHandleOwnership.DoNotTransfer, resultType); + GC.KeepAlive (instance); + return result ?? throw new InvalidCastException ( FormattableString.Invariant ($"Unable to convert instance of type '{instance.GetType ().FullName}' to type '{resultType.FullName}'.")); } diff --git a/src/Mono.Android/Java.Interop/Runtime.cs b/src/Mono.Android/Java.Interop/Runtime.cs index 314815a9edb..71e7d018163 100644 --- a/src/Mono.Android/Java.Interop/Runtime.cs +++ b/src/Mono.Android/Java.Interop/Runtime.cs @@ -40,7 +40,9 @@ public static bool IsGCUserPeer (IJavaObject value) { if (value == null) return false; - return IsGCUserPeer (value.Handle); + bool result = IsGCUserPeer (value.Handle); + GC.KeepAlive (value); + return result; } public static bool IsGCUserPeer (IntPtr value) diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs index 08e8e25dffc..67183644780 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs @@ -1746,6 +1746,7 @@ KeyStore GetConfiguredKeyStoreInstance () var keyFactory = KeyFactory.GetInstance (algorithmName) ?? throw new InvalidOperationException ($"Failed to get the KeyFactory instance for algorithm {algorithmName}"); var privateKey = keyFactory.GeneratePrivate (new Java.Security.Spec.PKCS8EncodedKeySpec (key.ExportPkcs8PrivateKey ())); var certificate = Java.Lang.Object.GetObject (clientCertificate.Handle, JniHandleOwnership.DoNotTransfer); + GC.KeepAlive (clientCertificate); if (privateKey is null || certificate is null) { return null; diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs index 917710ab797..1bdaab73703 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs @@ -506,6 +506,55 @@ public void ManagedToJavaTypeMapping () Assert.AreEqual (null, m, "`JnienvTest` does *not* subclass Java.Lang.Object, it should *not* be in the typemap!"); } + [Test] + [Category ("GCBridge")] + [Category ("NativeAOTIgnore")] + public void ToLocalJniHandleSurvivesConcurrentCollection () + { + const int iterations = 1000; + using var done = new CancellationTokenSource (); + var collector = new Thread (() => { + while (!done.IsCancellationRequested) { + GC.Collect (); + GC.WaitForPendingFinalizers (); + Thread.Yield (); + } + }); + collector.Start (); + + try { + for (int i = 0; i < iterations; i++) { + IntPtr handle = JNIEnv.ToLocalJniHandle (new FinalizableHandleOwner ()); + try { + Assert.AreNotEqual (IntPtr.Zero, handle, $"No local reference was returned during iteration {i}."); + Assert.AreEqual ("java/lang/Object", JNIEnv.GetClassNameFromInstance (handle), $"The local reference was invalid during iteration {i}."); + } finally { + JNIEnv.DeleteLocalRef (handle); + } + } + } finally { + done.Cancel (); + collector.Join (); + } + } + + sealed class FinalizableHandleOwner : IJavaObject + { + readonly Java.Lang.Object peer = new Java.Lang.Object (); + + public IntPtr Handle => peer.Handle; + + public void Dispose () + { + peer.Dispose (); + GC.SuppressFinalize (this); + } + ~FinalizableHandleOwner () + { + peer.Dispose (); + } + } + [Test, Category ("GCBridge")] [Ignore ("Failing in NativeAOT: https://github.com/dotnet/android/issues/11690")] public void DoNotLeakWeakReferences () From 1bb71f76df0a74137bb6f4e710c03c68bc62cee0 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 7 Aug 2026 13:34:38 -0500 Subject: [PATCH 2/5] Remove null-forgiving return conversion Use an explicit marshaling failure when ArrayAdapter creation returns null. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6105414-1c2c-4696-8d01-6507bda3e4be --- src/Mono.Android/Android.Widget/ArrayAdapter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mono.Android/Android.Widget/ArrayAdapter.cs b/src/Mono.Android/Android.Widget/ArrayAdapter.cs index 899710a3f3f..d59c1e126b7 100644 --- a/src/Mono.Android/Android.Widget/ArrayAdapter.cs +++ b/src/Mono.Android/Android.Widget/ArrayAdapter.cs @@ -190,9 +190,9 @@ public void Add (T @object) id_createFromResource_Landroid_content_Context_II = JNIEnv.GetStaticMethodID (class_ref, "createFromResource", "(Landroid/content/Context;II)Landroid/widget/ArrayAdapter;"); var result = JavaConvert.FromJniHandle> ( JNIEnv.CallStaticObjectMethod (class_ref, id_createFromResource_Landroid_content_Context_II, new JValue (context), new JValue (textArrayResId), new JValue (textViewResId)), - JniHandleOwnership.TransferLocalRef)!; + JniHandleOwnership.TransferLocalRef); GC.KeepAlive (context); - return result; + return result ?? throw new InvalidOperationException ("Unable to marshal the return value to an Android.Widget.ArrayAdapter instance."); } static IntPtr id_getItem_I; From 97e142469fa187542a0ea4936b2abf9317746e92 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 7 Aug 2026 14:34:18 -0500 Subject: [PATCH 3/5] Fix finalizable JNI test peer construction Use a derived Java peer because Java.Lang.Object does not expose a public constructor. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6105414-1c2c-4696-8d01-6507bda3e4be --- .../Mono.Android-Tests/Java.Interop/JnienvTest.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs index 1bdaab73703..ef24d63a72f 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs @@ -540,7 +540,7 @@ public void ToLocalJniHandleSurvivesConcurrentCollection () sealed class FinalizableHandleOwner : IJavaObject { - readonly Java.Lang.Object peer = new Java.Lang.Object (); + readonly Java.Lang.Object peer = new FinalizableHandlePeer (); public IntPtr Handle => peer.Handle; @@ -555,6 +555,10 @@ public void Dispose () } } + sealed class FinalizableHandlePeer : Java.Lang.Object + { + } + [Test, Category ("GCBridge")] [Ignore ("Failing in NativeAOT: https://github.com/dotnet/android/issues/11690")] public void DoNotLeakWeakReferences () From 36ca65d8e97259392dea95607e06d9adb2d247ce Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 7 Aug 2026 15:40:36 -0500 Subject: [PATCH 4/5] Fix JNI lifetime test owner type Derive the finalizable owner directly from Java.Lang.Object so Android packaging accepts it as an IJavaObject. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6105414-1c2c-4696-8d01-6507bda3e4be --- .../Java.Interop/JnienvTest.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs index ef24d63a72f..85eba525f2f 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs @@ -538,24 +538,7 @@ public void ToLocalJniHandleSurvivesConcurrentCollection () } } - sealed class FinalizableHandleOwner : IJavaObject - { - readonly Java.Lang.Object peer = new FinalizableHandlePeer (); - - public IntPtr Handle => peer.Handle; - - public void Dispose () - { - peer.Dispose (); - GC.SuppressFinalize (this); - } - ~FinalizableHandleOwner () - { - peer.Dispose (); - } - } - - sealed class FinalizableHandlePeer : Java.Lang.Object + sealed class FinalizableHandleOwner : Java.Lang.Object { } From 9d26d1316fa870991104e60c6d1e7b6d190cc275 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 10 Aug 2026 12:41:42 -0500 Subject: [PATCH 5/5] Fix JNI lifetime test regressions Preserve fallback JavaObject peer mappings during collection conversion and validate the generated test peer without assuming its runtime class name. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6105414-1c2c-4696-8d01-6507bda3e4be --- src/Mono.Android/Java.Interop/JavaConvert.cs | 5 +---- .../Mono.Android-Tests/Java.Interop/JnienvTest.cs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index 4e305223a7c..51d16713029 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -527,10 +527,7 @@ public static T? FromJavaObject< return JNIEnv.NewArray ((Array) value); } }, { typeof (Android.Runtime.JavaObject), value => { - if (value == null) - return IntPtr.Zero; - using (var v = new Android.Runtime.JavaObject (value)) - return JNIEnv.ToLocalJniHandle (v); + return value == null ? IntPtr.Zero : JNIEnv.ToLocalJniHandle (new Android.Runtime.JavaObject (value)); } }, }; diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs index 85eba525f2f..409a03ec1f4 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs @@ -527,7 +527,7 @@ public void ToLocalJniHandleSurvivesConcurrentCollection () IntPtr handle = JNIEnv.ToLocalJniHandle (new FinalizableHandleOwner ()); try { Assert.AreNotEqual (IntPtr.Zero, handle, $"No local reference was returned during iteration {i}."); - Assert.AreEqual ("java/lang/Object", JNIEnv.GetClassNameFromInstance (handle), $"The local reference was invalid during iteration {i}."); + Assert.IsNotEmpty (JNIEnv.GetClassNameFromInstance (handle), $"The local reference was invalid during iteration {i}."); } finally { JNIEnv.DeleteLocalRef (handle); }