From 9fc93e58b29bb15205fc5bc304869b368d96317e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 00:05:27 +0200 Subject: [PATCH 1/8] Use trimmable type map for non-generic collection CopyTo Route non-generic JavaCollection and JavaList CopyTo element conversion through a helper that can use runtime type mapping and trimmable type map peer creation instead of suppressing DAM propagation warnings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/JavaCollection.cs | 11 +-- src/Mono.Android/Android.Runtime/JavaList.cs | 23 ++++--- src/Mono.Android/Java.Interop/JavaConvert.cs | 67 ++++++++++++++++++- src/Mono.Android/metadata | 2 +- .../TrimmableTypeMapRuntimeCoverageTests.cs | 41 ++++++++++++ 5 files changed, 123 insertions(+), 21 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JavaCollection.cs b/src/Mono.Android/Android.Runtime/JavaCollection.cs index 9a41485cf71..e3333016bfe 100644 --- a/src/Mono.Android/Android.Runtime/JavaCollection.cs +++ b/src/Mono.Android/Android.Runtime/JavaCollection.cs @@ -148,11 +148,6 @@ internal Java.Lang.Object[] ToArray () // public void CopyTo (Array array, int array_index) { - [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "JavaCollection constructors are preserved by the MarkJavaObjects trimmer step.")] - [return: DynamicallyAccessedMembers (Constructors)] - static Type GetElementType (Array array) => - array.GetType ().GetElementType (); - if (array == null) throw new ArgumentNullException ("array"); if (array_index < 0) @@ -166,10 +161,10 @@ static Type GetElementType (Array array) => IntPtr lrefArray = JNIEnv.CallObjectMethod (Handle, id_toArray); for (int i = 0; i < Count; i++) array.SetValue ( - JavaConvert.FromJniHandle ( + JavaConvert.ConvertArrayElement ( + array, JNIEnv.GetObjectArrayElement (lrefArray, i), - JniHandleOwnership.TransferLocalRef, - GetElementType (array)), + JniHandleOwnership.TransferLocalRef), array_index + i); JNIEnv.DeleteLocalRef (lrefArray); } diff --git a/src/Mono.Android/Android.Runtime/JavaList.cs b/src/Mono.Android/Android.Runtime/JavaList.cs index 5dd329e71cb..a9f0ba8679b 100644 --- a/src/Mono.Android/Android.Runtime/JavaList.cs +++ b/src/Mono.Android/Android.Runtime/JavaList.cs @@ -27,6 +27,14 @@ public partial class JavaList : Java.Lang.Object, System.Collections.IList { int location, [DynamicallyAccessedMembers (Constructors)] Type? targetType = null) + { + return JavaConvert.FromJniHandle ( + InternalGetJniHandle (location), + JniHandleOwnership.TransferLocalRef, + targetType); + } + + unsafe IntPtr InternalGetJniHandle (int location) { const string id = "get.(I)Ljava/lang/Object;"; JniObjectReference obj; @@ -39,10 +47,7 @@ public partial class JavaList : Java.Lang.Object, System.Collections.IList { throw new ArgumentOutOfRangeException (ex.Message, ex); } - return JavaConvert.FromJniHandle ( - obj.Handle, - JniHandleOwnership.TransferLocalRef, - targetType); + return obj.Handle; } // @@ -269,11 +274,6 @@ public unsafe bool Contains (object? item) public void CopyTo (Array array, int array_index) { - [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "JavaList constructors are preserved by the MarkJavaObjects trimmer step.")] - [return: DynamicallyAccessedMembers (Constructors)] - static Type GetElementType (Array array) => - array.GetType ().GetElementType (); - if (array == null) throw new ArgumentNullException ("array"); if (array_index < 0) @@ -281,10 +281,11 @@ static Type GetElementType (Array array) => if (array.Length < array_index + Count) throw new ArgumentException ("array"); - var targetType = GetElementType (array); int c = Count; for (int i = 0; i < c; i++) - array.SetValue (InternalGet (i, targetType), array_index + i); + array.SetValue ( + JavaConvert.ConvertArrayElement (array, InternalGetJniHandle (i), JniHandleOwnership.TransferLocalRef), + array_index + i); } public IEnumerator GetEnumerator () diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index ebff6c1f765..aa4f1abd4ff 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -162,6 +162,72 @@ static Func GetJniHandleConverterForType ([D typeof (Func), m); } + internal static object? ConvertArrayElement (Array array, IntPtr handle, JniHandleOwnership transfer) + { + var elementType = array.GetType ().GetElementType (); + if (elementType is null || elementType == typeof (object)) + return FromJniHandleWithRuntimeTypeMapping (handle, transfer); + + if (JniHandleConverters.TryGetValue (elementType, out var converter)) + return converter (handle, transfer); + + if (elementType.IsArray) + return JNIEnv.GetArray (handle, transfer, elementType.GetElementType ()); + + if (RuntimeFeature.TrimmableTypeMap) { + return FromJniHandleWithTypeMapping (handle, transfer, elementType); + } else { + return ConvertLegacyArrayElement (handle, transfer, elementType); + } + } + + static object? FromJniHandleWithRuntimeTypeMapping (IntPtr handle, JniHandleOwnership transfer) + { + var converter = GetJniHandleConverter (GetTypeMapping (handle)); + if (converter != null) + return converter (handle, transfer); + return FromJniHandle (handle, transfer); + } + + static object? FromJniHandleWithTypeMapping (IntPtr handle, JniHandleOwnership transfer, Type elementType) + { + bool consumed = false; + try { + var peeked = Java.Lang.Object.PeekObject (handle, elementType); + if (peeked != null) { + consumed = true; + JNIEnv.DeleteRef (handle, transfer); + return peeked; + } + + if (elementType.IsGenericType) { + throw new NotSupportedException ( + FormattableString.Invariant ($"Cannot convert Java collection elements to closed generic array element type '{elementType}'.")); + } + + var reference = new JniObjectReference (handle, JniObjectReferenceType.Local); + var peer = JniEnvironment.Runtime.ValueManager.GetPeer (reference); + if (peer != null) { + consumed = true; + JNIEnv.DeleteRef (handle, transfer); + return peer; + } + + consumed = true; + return FromJniHandle (handle, transfer); + } finally { + if (!consumed) { + JNIEnv.DeleteRef (handle, transfer); + } + } + } + + [UnconditionalSuppressMessage ("Trimming", "IL2067", Justification = "Legacy non-trimmable typemap path preserves constructors by MarkJavaObjects.")] + static object? ConvertLegacyArrayElement (IntPtr handle, JniHandleOwnership transfer, Type elementType) + { + return FromJniHandle (handle, transfer, elementType); + } + public static T? FromJniHandle< [DynamicallyAccessedMembers (Constructors)] T @@ -498,4 +564,3 @@ public static TReturn WithLocalJniHandle(object? value, FuncAndroid.Graphics.Color () where T : Java.Lang.Object { From e438f65d1df6edf2a9cff758fa9f00e8fe067be3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 06:44:36 +0200 Subject: [PATCH 2/8] Address CopyTo array conversion review feedback Hoist array element conversion state out of the CopyTo loops, remove the added trimming suppression, and inline JavaList element retrieval instead of adding a raw-handle helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/JavaCollection.cs | 4 +- src/Mono.Android/Android.Runtime/JavaList.cs | 33 +++++----- src/Mono.Android/Java.Interop/JavaConvert.cs | 62 ++++++++++++------- 3 files changed, 62 insertions(+), 37 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JavaCollection.cs b/src/Mono.Android/Android.Runtime/JavaCollection.cs index e3333016bfe..48bdcd6abd2 100644 --- a/src/Mono.Android/Android.Runtime/JavaCollection.cs +++ b/src/Mono.Android/Android.Runtime/JavaCollection.cs @@ -158,11 +158,11 @@ public void CopyTo (Array array, int array_index) if (id_toArray == IntPtr.Zero) id_toArray = JNIEnv.GetMethodID (collection_class, "toArray", "()[Ljava/lang/Object;"); + var converter = new JavaConvert.ArrayElementConverter (array); IntPtr lrefArray = JNIEnv.CallObjectMethod (Handle, id_toArray); for (int i = 0; i < Count; i++) array.SetValue ( - JavaConvert.ConvertArrayElement ( - array, + converter.FromJniHandle ( JNIEnv.GetObjectArrayElement (lrefArray, i), JniHandleOwnership.TransferLocalRef), array_index + i); diff --git a/src/Mono.Android/Android.Runtime/JavaList.cs b/src/Mono.Android/Android.Runtime/JavaList.cs index a9f0ba8679b..7ebfe3c2bad 100644 --- a/src/Mono.Android/Android.Runtime/JavaList.cs +++ b/src/Mono.Android/Android.Runtime/JavaList.cs @@ -27,14 +27,6 @@ public partial class JavaList : Java.Lang.Object, System.Collections.IList { int location, [DynamicallyAccessedMembers (Constructors)] Type? targetType = null) - { - return JavaConvert.FromJniHandle ( - InternalGetJniHandle (location), - JniHandleOwnership.TransferLocalRef, - targetType); - } - - unsafe IntPtr InternalGetJniHandle (int location) { const string id = "get.(I)Ljava/lang/Object;"; JniObjectReference obj; @@ -47,7 +39,10 @@ unsafe IntPtr InternalGetJniHandle (int location) throw new ArgumentOutOfRangeException (ex.Message, ex); } - return obj.Handle; + return JavaConvert.FromJniHandle ( + obj.Handle, + JniHandleOwnership.TransferLocalRef, + targetType); } // @@ -272,7 +267,7 @@ public unsafe bool Contains (object? item) } } - public void CopyTo (Array array, int array_index) + public unsafe void CopyTo (Array array, int array_index) { if (array == null) throw new ArgumentNullException ("array"); @@ -281,11 +276,21 @@ public void CopyTo (Array array, int array_index) if (array.Length < array_index + Count) throw new ArgumentException ("array"); + var converter = new JavaConvert.ArrayElementConverter (array); + const string id = "get.(I)Ljava/lang/Object;"; int c = Count; - for (int i = 0; i < c; i++) - array.SetValue ( - JavaConvert.ConvertArrayElement (array, InternalGetJniHandle (i), JniHandleOwnership.TransferLocalRef), - array_index + i); + for (int i = 0; i < c; i++) { + JniObjectReference obj; + try { + JniArgumentValue* parameters = stackalloc JniArgumentValue [1] { + new JniArgumentValue (i), + }; + obj = list_members.InstanceMethods.InvokeAbstractObjectMethod (id, this, parameters); + } catch (Java.Lang.IndexOutOfBoundsException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { + throw new ArgumentOutOfRangeException (ex.Message, ex); + } + array.SetValue (converter.FromJniHandle (obj.Handle, JniHandleOwnership.TransferLocalRef), array_index + i); + } } public IEnumerator GetEnumerator () diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index aa4f1abd4ff..362e796fb93 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -162,22 +162,48 @@ static Func GetJniHandleConverterForType ([D typeof (Func), m); } - internal static object? ConvertArrayElement (Array array, IntPtr handle, JniHandleOwnership transfer) + internal readonly struct ArrayElementConverter { - var elementType = array.GetType ().GetElementType (); - if (elementType is null || elementType == typeof (object)) - return FromJniHandleWithRuntimeTypeMapping (handle, transfer); + readonly Type? elementType; + readonly Func? converter; + readonly bool useRuntimeTypeMapping; + + public ArrayElementConverter (Array array) + { + elementType = array.GetType ().GetElementType (); + converter = elementType != null ? GetJniHandleConverter (elementType) : null; + useRuntimeTypeMapping = elementType is null || elementType == typeof (object); + } - if (JniHandleConverters.TryGetValue (elementType, out var converter)) - return converter (handle, transfer); + public object? FromJniHandle (IntPtr handle, JniHandleOwnership transfer) + { + if (useRuntimeTypeMapping) + return FromJniHandleWithRuntimeTypeMapping (handle, transfer); + + if (elementType != null) { + var peeked = Java.Lang.Object.PeekObject (handle, elementType); + if (peeked != null) { + JNIEnv.DeleteRef (handle, transfer); + return peeked; + } + } - if (elementType.IsArray) - return JNIEnv.GetArray (handle, transfer, elementType.GetElementType ()); + if (converter != null) + return converter (handle, transfer); - if (RuntimeFeature.TrimmableTypeMap) { - return FromJniHandleWithTypeMapping (handle, transfer, elementType); - } else { - return ConvertLegacyArrayElement (handle, transfer, elementType); + if (elementType != null && elementType.IsArray) + return JNIEnv.GetArray (handle, transfer, elementType.GetElementType ()); + + if (elementType != null && typeof (IJavaPeerable).IsAssignableFrom (elementType)) { + if (RuntimeFeature.TrimmableTypeMap) + return FromJniHandleWithTrimmableTypeMapping (handle, transfer, elementType); + return Java.Lang.Object.GetObject (handle, transfer, elementType); + } + + var value = FromJniHandleWithRuntimeTypeMapping (handle, transfer); + if (value == null || elementType == null || elementType.IsAssignableFrom (value.GetType ())) + return value; + return Convert.ChangeType (value, elementType, CultureInfo.InvariantCulture); } } @@ -189,7 +215,7 @@ static Func GetJniHandleConverterForType ([D return FromJniHandle (handle, transfer); } - static object? FromJniHandleWithTypeMapping (IntPtr handle, JniHandleOwnership transfer, Type elementType) + static object? FromJniHandleWithTrimmableTypeMapping (IntPtr handle, JniHandleOwnership transfer, Type elementType) { bool consumed = false; try { @@ -213,8 +239,8 @@ static Func GetJniHandleConverterForType ([D return peer; } - consumed = true; - return FromJniHandle (handle, transfer); + throw new NotSupportedException ( + FormattableString.Invariant ($"Cannot convert Java collection element to array element type '{elementType}' using the trimmable type map.")); } finally { if (!consumed) { JNIEnv.DeleteRef (handle, transfer); @@ -222,12 +248,6 @@ static Func GetJniHandleConverterForType ([D } } - [UnconditionalSuppressMessage ("Trimming", "IL2067", Justification = "Legacy non-trimmable typemap path preserves constructors by MarkJavaObjects.")] - static object? ConvertLegacyArrayElement (IntPtr handle, JniHandleOwnership transfer, Type elementType) - { - return FromJniHandle (handle, transfer, elementType); - } - public static T? FromJniHandle< [DynamicallyAccessedMembers (Constructors)] T From a76cfb35dc53252d7f2fa55cba94339431cd6304 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 06:50:14 +0200 Subject: [PATCH 3/8] Handle null Java collection elements in CopyTo Return null for zero JNI handles during array element conversion and add CopyTo coverage for null elements in non-generic JavaCollection and JavaList. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Java.Interop/JavaConvert.cs | 3 +++ .../TrimmableTypeMapRuntimeCoverageTests.cs | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index 362e796fb93..5974c83966a 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -177,6 +177,9 @@ public ArrayElementConverter (Array array) public object? FromJniHandle (IntPtr handle, JniHandleOwnership transfer) { + if (handle == IntPtr.Zero) + return null; + if (useRuntimeTypeMapping) return FromJniHandleWithRuntimeTypeMapping (handle, transfer); diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs index 220f75c7d18..b1df36c5b30 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs @@ -147,10 +147,19 @@ public void NonGenericCollections_CopyTo_UseTrimmableTypeMapForArrayElementConve using (var arrayList = new Java.Util.ArrayList ()) { arrayList.Add (42); + arrayList.Add (null); using (var collection = new JavaCollection (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { - var values = new object [1]; + var values = new object [2]; collection.CopyTo (values, 0); Assert.AreEqual (42, values [0]); + Assert.IsNull (values [1]); + } + + using (var list = new JavaList (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { + var values = new object [2]; + list.CopyTo (values, 0); + Assert.AreEqual (42, values [0]); + Assert.IsNull (values [1]); } } From 72c22d32439acb2d769c8733fe601a1a7bb3d8cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 20:06:36 +0000 Subject: [PATCH 4/8] Remove redundant PeekObject call and add JavaList string[] test Agent-Logs-Url: https://github.com/dotnet/android/sessions/97298485-55ae-4c3c-a1ea-aa1a0b113cbc Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> --- src/Mono.Android/Java.Interop/JavaConvert.cs | 7 ------- .../Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs | 6 ++++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index 5974c83966a..3ce54cad7c5 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -222,13 +222,6 @@ public ArrayElementConverter (Array array) { bool consumed = false; try { - var peeked = Java.Lang.Object.PeekObject (handle, elementType); - if (peeked != null) { - consumed = true; - JNIEnv.DeleteRef (handle, transfer); - return peeked; - } - if (elementType.IsGenericType) { throw new NotSupportedException ( FormattableString.Invariant ($"Cannot convert Java collection elements to closed generic array element type '{elementType}'.")); diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs index b1df36c5b30..1cddee0df62 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs @@ -170,6 +170,12 @@ public void NonGenericCollections_CopyTo_UseTrimmableTypeMapForArrayElementConve collection.CopyTo (values, 0); Assert.AreEqual ("alpha", values [0]); } + + using (var list = new JavaList (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { + var values = new string [1]; + list.CopyTo (values, 0); + Assert.AreEqual ("alpha", values [0]); + } } } From 24e9650b90d61ec4e88278dd349258af4254fc3b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 22 May 2026 13:06:52 +0200 Subject: [PATCH 5/8] Share JavaList get-handle logic with CopyTo Split InternalGet so CopyTo can reuse the List.get local-reference path while keeping array element conversion separate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/JavaList.cs | 44 +++++++------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JavaList.cs b/src/Mono.Android/Android.Runtime/JavaList.cs index 7ebfe3c2bad..73323903d7e 100644 --- a/src/Mono.Android/Android.Runtime/JavaList.cs +++ b/src/Mono.Android/Android.Runtime/JavaList.cs @@ -23,26 +23,29 @@ public partial class JavaList : Java.Lang.Object, System.Collections.IList { // // https://developer.android.com/reference/java/util/List.html?hl=en#get(int) // - internal unsafe object? InternalGet ( + internal object? InternalGet ( int location, [DynamicallyAccessedMembers (Constructors)] Type? targetType = null) + { + var obj = InternalGetReference (location); + return JavaConvert.FromJniHandle ( + obj.Handle, + JniHandleOwnership.TransferLocalRef, + targetType); + } + + internal unsafe JniObjectReference InternalGetReference (int location) { const string id = "get.(I)Ljava/lang/Object;"; - JniObjectReference obj; try { JniArgumentValue* parameters = stackalloc JniArgumentValue [1] { new JniArgumentValue (location), }; - obj = list_members.InstanceMethods.InvokeAbstractObjectMethod (id, this, parameters); + return list_members.InstanceMethods.InvokeAbstractObjectMethod (id, this, parameters); } catch (Java.Lang.IndexOutOfBoundsException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { throw new ArgumentOutOfRangeException (ex.Message, ex); } - - return JavaConvert.FromJniHandle ( - obj.Handle, - JniHandleOwnership.TransferLocalRef, - targetType); } // @@ -277,18 +280,9 @@ public unsafe void CopyTo (Array array, int array_index) throw new ArgumentException ("array"); var converter = new JavaConvert.ArrayElementConverter (array); - const string id = "get.(I)Ljava/lang/Object;"; int c = Count; for (int i = 0; i < c; i++) { - JniObjectReference obj; - try { - JniArgumentValue* parameters = stackalloc JniArgumentValue [1] { - new JniArgumentValue (i), - }; - obj = list_members.InstanceMethods.InvokeAbstractObjectMethod (id, this, parameters); - } catch (Java.Lang.IndexOutOfBoundsException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { - throw new ArgumentOutOfRangeException (ex.Message, ex); - } + var obj = InternalGetReference (i); array.SetValue (converter.FromJniHandle (obj.Handle, JniHandleOwnership.TransferLocalRef), array_index + i); } } @@ -743,19 +737,9 @@ public JavaList (IEnumerable items) : this () // // https://developer.android.com/reference/java/util/List.html?hl=en#get(int) // - internal unsafe T? InternalGet (int location) + internal T? InternalGet (int location) { - const string id = "get.(I)Ljava/lang/Object;"; - JniObjectReference obj; - try { - JniArgumentValue* parameters = stackalloc JniArgumentValue [1] { - new JniArgumentValue (location), - }; - obj = list_members.InstanceMethods.InvokeAbstractObjectMethod (id, this, parameters); - } catch (Java.Lang.IndexOutOfBoundsException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { - throw new ArgumentOutOfRangeException (ex.Message, ex); - } - + var obj = InternalGetReference (location); return JavaConvert.FromJniHandle ( obj.Handle, JniHandleOwnership.TransferLocalRef); From a9e06760481b81373c9727cd4fb2650c4fa91236 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 22 May 2026 13:06:52 +0200 Subject: [PATCH 6/8] Split non-generic collection CopyTo coverage Break the combined trimmable typemap CopyTo coverage into focused tests while exercising both JavaCollection and JavaList paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapRuntimeCoverageTests.cs | 82 +++++++++++-------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs index 1cddee0df62..f16e3384f1f 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs @@ -124,7 +124,7 @@ public void ClosedGenericJavaList_CanWrapJavaCreatedArrayListHandle () } [Test] - public void NonGenericCollections_CopyTo_UseTrimmableTypeMapForArrayElementConversion () + public void NonGenericCollection_CopyTo_ViewArray_UsesTrimmableTypeMapForArrayElementConversion () { AssumeTrimmableTypeMapEnabled (); @@ -132,50 +132,66 @@ public void NonGenericCollections_CopyTo_UseTrimmableTypeMapForArrayElementConve using (var arrayList = new Java.Util.ArrayList ()) { arrayList.Add (view); - using (var collection = new JavaCollection (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { - var values = new View [1]; - collection.CopyTo (values, 0); - Assert.AreEqual (view.Handle, values [0].Handle); - } + var values = new View [1]; + CopyToJavaCollection (arrayList, values); + Assert.AreEqual (view.Handle, values [0].Handle); - using (var list = new JavaList (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { - var values = new View [1]; - list.CopyTo (values, 0); - Assert.AreEqual (view.Handle, values [0].Handle); - } + values = new View [1]; + CopyToJavaList (arrayList, values); + Assert.AreEqual (view.Handle, values [0].Handle); } + } + + [Test] + public void NonGenericCollection_CopyTo_ObjectArray_PreservesNullElement () + { + AssumeTrimmableTypeMapEnabled (); using (var arrayList = new Java.Util.ArrayList ()) { arrayList.Add (42); arrayList.Add (null); - using (var collection = new JavaCollection (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { - var values = new object [2]; - collection.CopyTo (values, 0); - Assert.AreEqual (42, values [0]); - Assert.IsNull (values [1]); - } - using (var list = new JavaList (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { - var values = new object [2]; - list.CopyTo (values, 0); - Assert.AreEqual (42, values [0]); - Assert.IsNull (values [1]); - } + var values = new object [2]; + CopyToJavaCollection (arrayList, values); + Assert.AreEqual (42, values [0]); + Assert.IsNull (values [1]); + + values = new object [2]; + CopyToJavaList (arrayList, values); + Assert.AreEqual (42, values [0]); + Assert.IsNull (values [1]); } + } + + [Test] + public void NonGenericCollection_CopyTo_StringArray_ConvertsJavaString () + { + AssumeTrimmableTypeMapEnabled (); using (var arrayList = new Java.Util.ArrayList ()) { arrayList.Add ("alpha"); - using (var collection = new JavaCollection (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { - var values = new string [1]; - collection.CopyTo (values, 0); - Assert.AreEqual ("alpha", values [0]); - } - using (var list = new JavaList (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { - var values = new string [1]; - list.CopyTo (values, 0); - Assert.AreEqual ("alpha", values [0]); - } + var values = new string [1]; + CopyToJavaCollection (arrayList, values); + Assert.AreEqual ("alpha", values [0]); + + values = new string [1]; + CopyToJavaList (arrayList, values); + Assert.AreEqual ("alpha", values [0]); + } + } + + static void CopyToJavaCollection (Java.Util.ArrayList arrayList, Array values) + { + using (var collection = new JavaCollection (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { + collection.CopyTo (values, 0); + } + } + + static void CopyToJavaList (Java.Util.ArrayList arrayList, Array values) + { + using (var list = new JavaList (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { + list.CopyTo (values, 0); } } From 80414aac87a7613520324e22c30b11e35c316ea2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 22 May 2026 13:48:46 +0200 Subject: [PATCH 7/8] Centralize trimmable typemap peer creation Move proxy lookup, direct CreateInstance activation, and created-peer state setup into TrimmableTypeMap so JavaConvert can use the trimmable activation path without owning those details. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Java.Interop/JavaConvert.cs | 3 +- .../JavaMarshalValueManager.cs | 40 +------------ .../TrimmableTypeMap.cs | 57 +++++++++++++++++++ .../TrimmableTypeMapRuntimeCoverageTests.cs | 34 +++++++---- 4 files changed, 83 insertions(+), 51 deletions(-) diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index 3ce54cad7c5..205f5668814 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -227,8 +227,7 @@ public ArrayElementConverter (Array array) FormattableString.Invariant ($"Cannot convert Java collection elements to closed generic array element type '{elementType}'.")); } - var reference = new JniObjectReference (handle, JniObjectReferenceType.Local); - var peer = JniEnvironment.Runtime.ValueManager.GetPeer (reference); + var peer = TrimmableTypeMap.Instance.CreateInstance (handle, elementType); if (peer != null) { consumed = true; JNIEnv.DeleteRef (handle, transfer); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 210f6f343a0..716c7b06765 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -520,21 +520,8 @@ void ProcessContext (HandleContext* context) var resolvedTargetType = ResolvePeerType (targetType); var typeMap = TrimmableTypeMap.Instance; - var proxy = typeMap.GetProxyForJavaObject (reference.Handle, resolvedTargetType); - - // Open-generic proxies cannot instantiate closed targets. - IJavaPeerable? peer; - if (ShouldActivateClosedGenericTarget (proxy, resolvedTargetType)) { - peer = ActivateUsingReflection (resolvedTargetType, reference.Handle, JniHandleOwnership.DoNotTransfer); - } else { - peer = proxy?.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer); - } + var peer = typeMap.CreateInstance (reference.Handle, resolvedTargetType); if (peer is not null) { - var peerState = peer.JniManagedPeerState | JniManagedPeerStates.Replaceable; - if (global::Java.Interop.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) { - peerState |= JniManagedPeerStates.Activatable; - } - peer.SetJniManagedPeerState (peerState); return peer; } @@ -583,31 +570,6 @@ void ProcessContext (HandleContext* context) return type; } - static bool ShouldActivateClosedGenericTarget ( - [NotNullWhen (true)] JavaPeerProxy? proxy, - [NotNullWhen (true)] Type? resolvedTargetType) - { - return proxy is not null && - proxy.TargetType.IsGenericTypeDefinition && - resolvedTargetType is not null && - resolvedTargetType.IsGenericType && - !resolvedTargetType.IsGenericTypeDefinition; - } - - static IJavaPeerable? ActivateUsingReflection ( - [DynamicallyAccessedMembers (Constructors)] - Type closedType, - IntPtr handle, - JniHandleOwnership transfer) - { - var ctor = closedType.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); - if (ctor is null) { - return null; - } - - return (IJavaPeerable) ctor.Invoke ([handle, transfer]); - } - /// /// Returns true when 's Java class is not assignable from /// . Throws when has no usable mapping. diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index a574c00a066..89d5478c52e 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -383,8 +383,65 @@ static JniMethodInfo GetClassGetInterfacesMethod () } } + internal IJavaPeerable? CreateInstance ( + IntPtr handle, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + var proxy = GetProxyForJavaObject (handle, targetType); + + IJavaPeerable? peer; + if (ShouldActivateClosedGenericTarget (proxy, targetType)) { + peer = ActivateUsingReflection (targetType, handle, JniHandleOwnership.DoNotTransfer); + } else { + peer = proxy?.CreateInstance (handle, JniHandleOwnership.DoNotTransfer); + } + if (peer is not null) { + MarkCreatedPeer (peer); + } + return peer; + } + const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; + + static bool ShouldActivateClosedGenericTarget ( + [NotNullWhen (true)] JavaPeerProxy? proxy, + [NotNullWhen (true)] Type? targetType) + { + return proxy is not null && + proxy.TargetType.IsGenericTypeDefinition && + targetType is not null && + targetType.IsGenericType && + !targetType.IsGenericTypeDefinition; + } + + static IJavaPeerable? ActivateUsingReflection ( + [DynamicallyAccessedMembers (Constructors)] + Type closedType, + IntPtr handle, + JniHandleOwnership transfer) + { + var ctor = closedType.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); + if (ctor is null) { + return null; + } + + return (IJavaPeerable) ctor.Invoke ([handle, transfer]); + } + + static void MarkCreatedPeer (IJavaPeerable peer) + { + var peerState = peer.JniManagedPeerState | JniManagedPeerStates.Replaceable; + if (global::Java.Interop.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) { + peerState |= JniManagedPeerStates.Activatable; + } + peer.SetJniManagedPeerState (peerState); + } + /// /// Match the proxy's stored target type against a hint from the caller. /// The proxy's target type is the open generic definition for generic peers diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs index f16e3384f1f..bfd4175bab4 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs @@ -128,17 +128,25 @@ public void NonGenericCollection_CopyTo_ViewArray_UsesTrimmableTypeMapForArrayEl { AssumeTrimmableTypeMapEnabled (); - using (var view = new View (Android.App.Application.Context)) using (var arrayList = new Java.Util.ArrayList ()) { - arrayList.Add (view); - - var values = new View [1]; - CopyToJavaCollection (arrayList, values); - Assert.AreEqual (view.Handle, values [0].Handle); - - values = new View [1]; - CopyToJavaList (arrayList, values); - Assert.AreEqual (view.Handle, values [0].Handle); + var viewClass = JniEnvironment.Types.FindClass ("android/view/View"); + var viewHandle = IntPtr.Zero; + try { + var constructor = JNIEnv.GetMethodID (viewClass.Handle, "", "(Landroid/content/Context;)V"); + viewHandle = JNIEnv.NewObject (viewClass.Handle, constructor, new JValue (Android.App.Application.Context.Handle)); + AddToJavaCollection (arrayList, viewHandle); + + var values = new View [1]; + CopyToJavaCollection (arrayList, values); + Assert.IsTrue (JNIEnv.IsSameObject (viewHandle, values [0].Handle)); + + values = new View [1]; + CopyToJavaList (arrayList, values); + Assert.IsTrue (JNIEnv.IsSameObject (viewHandle, values [0].Handle)); + } finally { + JNIEnv.DeleteLocalRef (viewHandle); + JniObjectReference.Dispose (ref viewClass); + } } } @@ -195,6 +203,12 @@ static void CopyToJavaList (Java.Util.ArrayList arrayList, Array values) } } + static void AddToJavaCollection (Java.Util.ArrayList arrayList, IntPtr handle) + { + var add = JNIEnv.GetMethodID (arrayList.Class.Handle, "add", "(Ljava/lang/Object;)Z"); + JNIEnv.CallBooleanMethod (arrayList.Handle, add, new JValue (handle)); + } + static T CreateFromJava () where T : Java.Lang.Object { From 62635ea1b8764ba592e2211be62e2066b54f1560 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 22 May 2026 14:08:36 +0200 Subject: [PATCH 8/8] Avoid reflection fallback for CopyTo typemap activation Use the no-reflection trimmable typemap creation path for typed array element conversion so CopyTo does not require DAM annotations on array element types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Java.Interop/JavaConvert.cs | 7 ++++++- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index 205f5668814..ba46b155c00 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -227,7 +227,12 @@ public ArrayElementConverter (Array array) FormattableString.Invariant ($"Cannot convert Java collection elements to closed generic array element type '{elementType}'.")); } - var peer = TrimmableTypeMap.Instance.CreateInstance (handle, elementType); + // This path intentionally avoids the reflection fallback used by TrimmableTypeMap.CreateInstance () + // because passing array element types there would require DAM annotations. Closed generic element + // types cannot be supported without that fallback: creating a non-generic base peer would not be + // assignable to the requested closed generic array element type. If the requested element type is + // already a non-generic base type, the typemap lookup can still select that base mapping. + var peer = TrimmableTypeMap.Instance.CreateInstanceWithoutReflectionFallback (handle, elementType); if (peer != null) { consumed = true; JNIEnv.DeleteRef (handle, transfer); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 89d5478c52e..915daa0e248 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -402,6 +402,15 @@ static JniMethodInfo GetClassGetInterfacesMethod () return peer; } + internal IJavaPeerable? CreateInstanceWithoutReflectionFallback (IntPtr handle, Type? targetType = null) + { + var peer = GetProxyForJavaObject (handle, targetType)?.CreateInstance (handle, JniHandleOwnership.DoNotTransfer); + if (peer is not null) { + MarkCreatedPeer (peer); + } + return peer; + } + const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;