From ca78fa282bbecc2790c80069af51d7aec206c94c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Jul 2026 12:53:08 +0200 Subject: [PATCH 01/14] [TrimmableTypeMap] Add AOT-safe array and collection factories Centralize safe runtime construction of arrays and generic Java collection wrappers so JavaConvert and JNIEnv no longer carry the trimmable-specific construction policy inline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 156 +++++++-- src/Mono.Android/Java.Interop/JavaConvert.cs | 91 +---- .../Java.Interop/SafeArrayFactory.cs | 151 +++++++++ .../Java.Interop/SafeJavaCollectionFactory.cs | 319 ++++++++++++++++++ .../TrimmableTypeMapTypeManager.cs | 12 +- src/Mono.Android/Mono.Android.csproj | 2 + .../Android.Runtime/JnienvArrayMarshaling.cs | 16 + .../Java.Interop/JavaConvertTest.cs | 63 +++- 8 files changed, 688 insertions(+), 122 deletions(-) create mode 100644 src/Mono.Android/Java.Interop/SafeArrayFactory.cs create mode 100644 src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 6a2a45012d8..35e8328897f 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -28,26 +28,7 @@ public static partial class JNIEnv { static Array ArrayCreateInstance (Type elementType, int length) { if (RuntimeFeature.TrimmableTypeMap) { - if (RuntimeFeature.IsCoreClrRuntime) { - // CoreCLR runtime type loader can construct any T[] dynamically. - // IsDynamicCodeSupported is a [FeatureGuard] so this branch is - // dead-coded under PublishAot. - return Array.CreateInstance (elementType, length); - } - - if (RuntimeFeature.IsNativeAotRuntime) { - // NativeAOT: resolve via per-rank typemap + generated array proxy. - if (TrimmableTypeMap.Instance.TryGetArrayProxy (elementType, additionalRank: 1, out var arrayProxy)) { - return arrayProxy.CreateManagedArray (length); - } - } - - int arrayRank = GetArrayRank (elementType, out var leafElementType); - throw new NotSupportedException ( - $"No TrimmableTypeMap array proxy entry for element type '{elementType}' " + - $"(leaf element type '{leafElementType}', rank {arrayRank}). " + - $"Array lookups use the leaf element type within the per-rank __ArrayMapRank{arrayRank} typemap group; " + - $"ensure the mapping is emitted for that rank (for example by increasing _AndroidTrimmableTypeMapMaxArrayRank) or report an issue."); + return SafeArrayFactory.CreateInstance (elementType, rank: 1, length); } #pragma warning disable IL3050 // legacy fallback path @@ -55,21 +36,6 @@ static Array ArrayCreateInstance (Type elementType, int length) #pragma warning restore IL3050 } - static int GetArrayRank (Type elementType, out Type leafElementType) - { - int rank = 1; - while (elementType.IsSZArray) { - rank++; - var nestedElementType = elementType.GetElementType (); - if (nestedElementType is null) { - break; - } - elementType = nestedElementType; - } - leafElementType = elementType; - return rank; - } - internal static IntPtr IdentityHash (IntPtr v) { return JniEnvironment.References.GetIdentityHashCode (new JniObjectReference (v)); @@ -605,11 +571,29 @@ static void AssertCompatibleArrayTypes (IntPtr sourceArray, Type destElementType static IntPtr FindArrayClassByElementType (Type elementType) { + var boxedPrimitiveJniClassName = GetBoxedPrimitiveJniClassName (elementType); + if (boxedPrimitiveJniClassName != null) { + return FindClass ("[L" + boxedPrimitiveJniClassName + ";"); + } + int rank = JavaNativeTypeManager.GetArrayInfo (elementType, out elementType) + 1; var typeSignature = JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (elementType).AddArrayRank (rank); return FindClass (typeSignature.Name); } + static string? GetBoxedPrimitiveJniClassName (Type type) + { + if (type == typeof (bool?)) return "java/lang/Boolean"; + if (type == typeof (sbyte?)) return "java/lang/Byte"; + if (type == typeof (char?)) return "java/lang/Character"; + if (type == typeof (short?)) return "java/lang/Short"; + if (type == typeof (int?)) return "java/lang/Integer"; + if (type == typeof (long?)) return "java/lang/Long"; + if (type == typeof (float?)) return "java/lang/Float"; + if (type == typeof (double?)) return "java/lang/Double"; + return null; + } + public static void CopyArray (IntPtr src, bool[] dest) { if (dest == null) @@ -684,6 +668,14 @@ public static void CopyArray (IntPtr src, string[] dest) _GetDoubleArrayRegion (source, index, 1, r); return r [0]; } }, + { typeof (bool?), (type, source, index) => GetNullableArrayElement (source, index, typeof (bool?)) }, + { typeof (sbyte?), (type, source, index) => GetNullableArrayElement (source, index, typeof (sbyte?)) }, + { typeof (char?), (type, source, index) => GetNullableArrayElement (source, index, typeof (char?)) }, + { typeof (short?), (type, source, index) => GetNullableArrayElement (source, index, typeof (short?)) }, + { typeof (int?), (type, source, index) => GetNullableArrayElement (source, index, typeof (int?)) }, + { typeof (long?), (type, source, index) => GetNullableArrayElement (source, index, typeof (long?)) }, + { typeof (float?), (type, source, index) => GetNullableArrayElement (source, index, typeof (float?)) }, + { typeof (double?), (type, source, index) => GetNullableArrayElement (source, index, typeof (double?)) }, { typeof (string), (type, source, index) => { IntPtr elem = GetObjectArrayElement (source, index); if (type == typeof (Java.Lang.String)) @@ -709,6 +701,12 @@ public static void CopyArray (IntPtr src, string[] dest) }; } + static object? GetNullableArrayElement (IntPtr source, int index, [DynamicallyAccessedMembers (Constructors)] Type targetType) + { + IntPtr elem = GetObjectArrayElement (source, index); + return JavaConvert.FromJniHandle (elem, JniHandleOwnership.TransferLocalRef, targetType); + } + static TValue GetConverter(Dictionary dict, Type? elementType, IntPtr array) { TValue? converter; @@ -913,6 +911,14 @@ static Dictionary> CreateCopyManagedToNativeArray () { typeof (long), (source, dest) => CopyArray ((long[]) source, dest) }, { typeof (float), (source, dest) => CopyArray ((float[]) source, dest) }, { typeof (double), (source, dest) => CopyArray ((double[]) source, dest) }, + { typeof (bool?), (source, dest) => CopyManagedObjectArray (source, dest) }, + { typeof (sbyte?), (source, dest) => CopyManagedObjectArray (source, dest) }, + { typeof (char?), (source, dest) => CopyManagedObjectArray (source, dest) }, + { typeof (short?), (source, dest) => CopyManagedObjectArray (source, dest) }, + { typeof (int?), (source, dest) => CopyManagedObjectArray (source, dest) }, + { typeof (long?), (source, dest) => CopyManagedObjectArray (source, dest) }, + { typeof (float?), (source, dest) => CopyManagedObjectArray (source, dest) }, + { typeof (double?), (source, dest) => CopyManagedObjectArray (source, dest) }, { typeof (string), (source, dest) => { var s = source as string[]; if (s != null) { @@ -942,6 +948,16 @@ static Dictionary> CreateCopyManagedToNativeArray () }; } + static void CopyManagedObjectArray (Array source, IntPtr dest) + { + for (int i = 0; i < source.Length; i++) { + JavaConvert.WithLocalJniHandle (source.GetValue (i), lref => { + SetObjectArrayElement (dest, i, lref); + return IntPtr.Zero; + }); + } + } + public static void CopyArray (Array source, Type elementType, IntPtr dest) { if (source == null) @@ -1045,6 +1061,14 @@ public static void CopyArray (T[] src, IntPtr dest) CopyArray (source, r); return r; } }, + { typeof (bool?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, + { typeof (sbyte?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, + { typeof (char?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, + { typeof (short?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, + { typeof (int?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, + { typeof (long?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, + { typeof (float?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, + { typeof (double?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, { typeof (string), (type, source, len) => { if (type != null && typeof (Java.Lang.Object).IsAssignableFrom (type)) { var r = new Java.Lang.String [len]; @@ -1069,6 +1093,16 @@ public static void CopyArray (T[] src, IntPtr dest) }; } + static Array CreateManagedArrayFromObjectArray (Type? elementType, IntPtr source, int len) + { + if (elementType == null) + throw new ArgumentNullException ("elementType"); + + var r = ArrayCreateInstance (elementType, len); + CopyArray (source, r, elementType); + return r; + } + static Array? _GetArray (IntPtr array_ptr, Type? element_type) { if (array_ptr == IntPtr.Zero) @@ -1321,12 +1355,48 @@ static Dictionary> CreateCreateManagedToNativeArray () { typeof (long), (source) => NewArray ((long[]) source) }, { typeof (float), (source) => NewArray ((float[]) source) }, { typeof (double), (source) => NewArray ((double[]) source) }, + { typeof (bool?), (source) => NewObjectArray (source, typeof (bool?)) }, + { typeof (sbyte?), (source) => NewObjectArray (source, typeof (sbyte?)) }, + { typeof (char?), (source) => NewObjectArray (source, typeof (char?)) }, + { typeof (short?), (source) => NewObjectArray (source, typeof (short?)) }, + { typeof (int?), (source) => NewObjectArray (source, typeof (int?)) }, + { typeof (long?), (source) => NewObjectArray (source, typeof (long?)) }, + { typeof (float?), (source) => NewObjectArray (source, typeof (float?)) }, + { typeof (double?), (source) => NewObjectArray (source, typeof (double?)) }, { typeof (string), (source) => NewArray ((string[]) source) }, { typeof (IJavaObject), (source) => NewArray ((IJavaObject[]) source) }, { typeof (Array), (source) => NewArray (source) }, }; } + static IntPtr NewObjectArray (Array value, Type elementType) + { + IntPtr grefArrayElementClass = FindObjectArrayElementClass (elementType); + try { + IntPtr array = IntPtr.Zero; + try { + array = NewObjectArray (value.Length, grefArrayElementClass, IntPtr.Zero); + CopyManagedObjectArray (value, array); + return array; + } catch { + DeleteLocalRef (array); + throw; + } + } finally { + DeleteGlobalRef (grefArrayElementClass); + } + } + + static IntPtr FindObjectArrayElementClass (Type elementType) + { + var boxedPrimitiveJniClassName = GetBoxedPrimitiveJniClassName (elementType); + if (boxedPrimitiveJniClassName != null) { + return FindClass (boxedPrimitiveJniClassName); + } + + return FindClass (elementType); + } + public static IntPtr NewArray (Array value, Type? elementType = null) { if (value == null) @@ -1436,6 +1506,14 @@ static IntPtr NewArray (Array value, Type elementType, IntPtr elementClass) var _value = new[]{(double) value!}; _SetDoubleArrayRegion (dest, index, _value.Length, _value); } }, + { typeof (bool?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, + { typeof (sbyte?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, + { typeof (char?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, + { typeof (short?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, + { typeof (int?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, + { typeof (long?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, + { typeof (float?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, + { typeof (double?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, { typeof (string), (dest, index, value) => { IntPtr s = NewString (value!.ToString ()); try { @@ -1455,6 +1533,14 @@ static IntPtr NewArray (Array value, Type elementType, IntPtr elementClass) }; } + static void SetObjectArrayElementFromManagedValue (IntPtr dest, int index, object? value) + { + JavaConvert.WithLocalJniHandle (value, lref => { + SetObjectArrayElement (dest, index, lref); + return IntPtr.Zero; + }); + } + static unsafe void _SetBooleanArrayRegion (IntPtr array, int start, int length, bool[] buffer) { fixed (bool* p = buffer) diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index 8b8184f9e2a..8aebf70687c 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -57,19 +57,6 @@ static class JavaConvert { } }, }; - static readonly Dictionary ScalarContainerFactories = new Dictionary { - { typeof (bool), JavaPeerContainerFactory.Instance }, - { typeof (byte), JavaPeerContainerFactory.Instance }, - { typeof (sbyte), JavaPeerContainerFactory.Instance }, - { typeof (char), JavaPeerContainerFactory.Instance }, - { typeof (short), JavaPeerContainerFactory.Instance }, - { typeof (int), JavaPeerContainerFactory.Instance }, - { typeof (long), JavaPeerContainerFactory.Instance }, - { typeof (float), JavaPeerContainerFactory.Instance }, - { typeof (double), JavaPeerContainerFactory.Instance }, - { typeof (string), JavaPeerContainerFactory.Instance }, - }; - static Func? GetJniHandleConverter (Type? target) { if (target == null) @@ -89,9 +76,14 @@ static class JavaConvert { if (target.IsGenericType && !target.IsGenericTypeDefinition) { if (RuntimeFeature.TrimmableTypeMap) { - var factoryConverter = TryGetFactoryBasedConverter (target); - if (factoryConverter != null) - return factoryConverter; + if (SafeJavaCollectionFactory.TryGetCollectionType (target, out _)) { + return (h, t) => { + if (SafeJavaCollectionFactory.TryCreateFromJniHandle (target, h, t, out var collection)) { + return collection; + } + return null; + }; + } } else { var factoryConverter = TryMakeGenericCollectionTypeFactory (target); if (factoryConverter != null) @@ -129,73 +121,6 @@ static class JavaConvert { } } - /// - /// AOT-safe converter using from the generated proxy. - /// Avoids MakeGenericType() by using the pre-typed factory from the proxy attribute. - /// - static Func? TryGetFactoryBasedConverter (Type target) - { - if (TryGetSingleGenericArgument (target, typeof (IList<>), typeof (JavaList<>), out var listElementType)) { - var factory = TryGetContainerFactory (listElementType); - if (factory != null) - return (h, t) => factory.CreateList (h, t); - } - - if (TryGetSingleGenericArgument (target, typeof (ICollection<>), typeof (JavaCollection<>), out var collectionElementType)) { - var factory = TryGetContainerFactory (collectionElementType); - if (factory != null) - return (h, t) => factory.CreateCollection (h, t); - } - - if (TryGetDictionaryArguments (target, out var typeArgs)) { - var keyFactory = TryGetContainerFactory (typeArgs [0]); - var valueFactory = TryGetContainerFactory (typeArgs [1]); - if (keyFactory != null && valueFactory != null) - return (h, t) => valueFactory.CreateDictionary (keyFactory, h, t); - } - - return null; - - static bool TryGetSingleGenericArgument (Type target, Type interfaceType, Type wrapperType, [NotNullWhen (true)] out Type? argument) - { - if (target.IsGenericType && !target.IsGenericTypeDefinition) { - var genericDef = target.GetGenericTypeDefinition (); - if (genericDef == interfaceType || genericDef == wrapperType) { - argument = target.GetGenericArguments () [0]; - return true; - } - } - - argument = null; - return false; - } - - static bool TryGetDictionaryArguments (Type target, [NotNullWhen (true)] out Type []? arguments) - { - if (target.IsGenericType && !target.IsGenericTypeDefinition) { - var genericDef = target.GetGenericTypeDefinition (); - if (genericDef == typeof (IDictionary<,>) || genericDef == typeof (JavaDictionary<,>)) { - arguments = target.GetGenericArguments (); - return true; - } - } - - arguments = null; - return false; - } - - static JavaPeerContainerFactory? TryGetContainerFactory (Type elementType) - { - if (ScalarContainerFactories.TryGetValue (elementType, out var scalarFactory)) - return scalarFactory; - - if (typeof (IJavaPeerable).IsAssignableFrom (elementType)) - return TrimmableTypeMap.Instance?.GetContainerFactory (elementType); - - return null; - } - } - static Func GetJniHandleConverterForType ([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t) { MethodInfo m = t.GetMethod ("FromJniHandle", BindingFlags.Static | BindingFlags.Public)!; diff --git a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs new file mode 100644 index 00000000000..effea62d6d0 --- /dev/null +++ b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs @@ -0,0 +1,151 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using Microsoft.Android.Runtime; + +namespace Java.Interop { + static class SafeArrayFactory + { + static readonly Dictionary ValueTypeArrayFactories = new Dictionary { + { typeof (bool), new ArrayFactoryInfo (typeof (bool[]), static length => new bool [length]) }, + { typeof (sbyte), new ArrayFactoryInfo (typeof (sbyte[]), static length => new sbyte [length]) }, + { typeof (char), new ArrayFactoryInfo (typeof (char[]), static length => new char [length]) }, + { typeof (short), new ArrayFactoryInfo (typeof (short[]), static length => new short [length]) }, + { typeof (int), new ArrayFactoryInfo (typeof (int[]), static length => new int [length]) }, + { typeof (long), new ArrayFactoryInfo (typeof (long[]), static length => new long [length]) }, + { typeof (float), new ArrayFactoryInfo (typeof (float[]), static length => new float [length]) }, + { typeof (double), new ArrayFactoryInfo (typeof (double[]), static length => new double [length]) }, + { typeof (bool?), new ArrayFactoryInfo (typeof (bool?[]), static length => new bool? [length]) }, + { typeof (sbyte?), new ArrayFactoryInfo (typeof (sbyte?[]), static length => new sbyte? [length]) }, + { typeof (char?), new ArrayFactoryInfo (typeof (char?[]), static length => new char? [length]) }, + { typeof (short?), new ArrayFactoryInfo (typeof (short?[]), static length => new short? [length]) }, + { typeof (int?), new ArrayFactoryInfo (typeof (int?[]), static length => new int? [length]) }, + { typeof (long?), new ArrayFactoryInfo (typeof (long?[]), static length => new long? [length]) }, + { typeof (float?), new ArrayFactoryInfo (typeof (float?[]), static length => new float? [length]) }, + { typeof (double?), new ArrayFactoryInfo (typeof (double?[]), static length => new double? [length]) }, + }; + + internal static Type GetArrayType (Type elementType, int rank) + { + if (TryGetArrayType (elementType, rank, out var arrayType)) { + return arrayType; + } + + throw CreateUnsupportedArrayException (elementType, rank); + } + + internal static bool TryGetArrayType (Type elementType, int rank, [NotNullWhen (true)] out Type? arrayType) + { + ValidateRank (rank); + + if (elementType == null) + throw new ArgumentNullException (nameof (elementType)); + + if (!TryGetVectorType (elementType, out arrayType)) { + arrayType = null; + return false; + } + + for (int i = 1; i < rank; i++) { + arrayType = MakeReferenceArrayType (arrayType); + } + + return true; + } + + internal static Array CreateInstance (Type elementType, int rank, int length) + { + if (TryCreateInstance (elementType, rank, length, out var array)) { + return array; + } + + throw CreateUnsupportedArrayException (elementType, rank); + } + + internal static bool TryCreateInstance (Type elementType, int rank, int length, [NotNullWhen (true)] out Array? array) + { + ValidateRank (rank); + ArgumentOutOfRangeException.ThrowIfNegative (length); + + if (elementType == null) + throw new ArgumentNullException (nameof (elementType)); + + if (rank == 1 && ValueTypeArrayFactories.TryGetValue (elementType, out var factory)) { + array = factory.CreateInstance (length); + return true; + } + + if (TryGetArrayType (elementType, rank, out var arrayType)) { + array = CreateInstanceFromArrayType (arrayType, length); + return true; + } + + if (RuntimeFeature.TrimmableTypeMap && RuntimeFeature.IsNativeAotRuntime && + TrimmableTypeMap.Instance.TryGetArrayProxy (elementType, rank, out var arrayProxy)) { + array = arrayProxy.CreateManagedArray (length); + return true; + } + + array = null; + return false; + } + + static void ValidateRank (int rank) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero (rank); + } + + static bool TryGetVectorType (Type elementType, [NotNullWhen (true)] out Type? vectorType) + { + if (elementType.IsValueType) { + if (ValueTypeArrayFactories.TryGetValue (elementType, out var factory)) { + vectorType = factory.ArrayType; + return true; + } + + vectorType = null; + return false; + } + + vectorType = MakeReferenceArrayType (elementType); + return true; + } + + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "Reference-type arrays use NativeAOT canonical array support. Value-type vectors are returned from explicit rooted maps before this helper is used.")] + [UnconditionalSuppressMessage ("Trimming", "IL2055:MakeGenericType", + Justification = "Creating an SZ array type does not require member discovery, and value-type vectors are returned from explicit rooted maps.")] + static Type MakeReferenceArrayType (Type elementType) + { + return elementType.MakeArrayType (); + } + + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "The array type is produced by SafeArrayFactory: reference arrays use canonical support, and value-type vectors are explicitly rooted.")] + static Array CreateInstanceFromArrayType (Type arrayType, int length) + { + return Array.CreateInstanceFromArrayType (arrayType, length); + } + + static NotSupportedException CreateUnsupportedArrayException (Type elementType, int rank) + { + return new NotSupportedException ( + $"The array type for element type '{elementType}' and rank '{rank}' is not available for AOT-safe creation."); + } + + sealed class ArrayFactoryInfo + { + public ArrayFactoryInfo (Type arrayType, Func createInstance) + { + ArrayType = arrayType; + CreateInstance = createInstance; + } + + public Type ArrayType { get; } + + public Func CreateInstance { get; } + } + } +} diff --git a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs new file mode 100644 index 00000000000..1d7e99bce94 --- /dev/null +++ b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs @@ -0,0 +1,319 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; + +using Android.Runtime; + +namespace Java.Interop { + static class SafeJavaCollectionFactory + { + const DynamicallyAccessedMemberTypes Constructors = + DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + + static readonly Dictionary ValueTypeArgumentFactories = new Dictionary { + { typeof (bool), CollectionArgumentFactory.Instance }, + { typeof (sbyte), CollectionArgumentFactory.Instance }, + { typeof (char), CollectionArgumentFactory.Instance }, + { typeof (short), CollectionArgumentFactory.Instance }, + { typeof (int), CollectionArgumentFactory.Instance }, + { typeof (long), CollectionArgumentFactory.Instance }, + { typeof (float), CollectionArgumentFactory.Instance }, + { typeof (double), CollectionArgumentFactory.Instance }, + { typeof (bool?), CollectionArgumentFactory.Instance }, + { typeof (sbyte?), CollectionArgumentFactory.Instance }, + { typeof (char?), CollectionArgumentFactory.Instance }, + { typeof (short?), CollectionArgumentFactory.Instance }, + { typeof (int?), CollectionArgumentFactory.Instance }, + { typeof (long?), CollectionArgumentFactory.Instance }, + { typeof (float?), CollectionArgumentFactory.Instance }, + { typeof (double?), CollectionArgumentFactory.Instance }, + }; + + internal static bool TryGetCollectionType (Type targetType, [NotNullWhen (true)] out Type? collectionType) + { + if (targetType == null) + throw new ArgumentNullException (nameof (targetType)); + + if (!TryGetCollectionShape (targetType, out var shape)) { + collectionType = null; + return false; + } + + collectionType = shape.Kind switch { + JavaCollectionKind.List => GetClosedCollectionType (typeof (JavaList<>), shape.Arguments), + JavaCollectionKind.Collection => GetClosedCollectionType (typeof (JavaCollection<>), shape.Arguments), + JavaCollectionKind.Dictionary => GetClosedCollectionType (typeof (JavaDictionary<,>), shape.Arguments), + _ => null, + }; + return collectionType != null; + } + + internal static bool TryCreateFromJniHandle ( + Type targetType, + IntPtr handle, + JniHandleOwnership transfer, + [NotNullWhen (true)] out object? collection) + { + if (targetType == null) + throw new ArgumentNullException (nameof (targetType)); + + if (handle == IntPtr.Zero) { + collection = null; + return true; + } + + if (!TryGetCollectionShape (targetType, out var shape)) { + collection = null; + return false; + } + + if (TryCreateFromMappedValueTypeFactories (shape, handle, transfer, out collection)) { + return true; + } + + if (!TryGetCollectionType (targetType, out var collectionType)) { + collection = null; + return false; + } + + collection = CreateInstance (collectionType, handle, transfer); + return true; + } + + internal static bool TryCreateInstance (Type targetType, object? items, [NotNullWhen (true)] out IJavaObject? collection) + { + if (targetType == null) + throw new ArgumentNullException (nameof (targetType)); + + if (items == null) { + collection = null; + return true; + } + + if (!TryGetCollectionType (targetType, out var collectionType)) { + collection = null; + return false; + } + + var instance = CreateInstance (collectionType, items); + if (instance is not IJavaObject javaObject) { + throw new InvalidOperationException ($"The collection type '{collectionType}' did not create an IJavaObject instance."); + } + + collection = javaObject; + return true; + } + + static bool TryGetCollectionShape (Type targetType, out CollectionShape shape) + { + if (!targetType.IsGenericType || targetType.IsGenericTypeDefinition) { + shape = default; + return false; + } + + var genericDefinition = targetType.GetGenericTypeDefinition (); + if (genericDefinition == typeof (IList<>) || genericDefinition == typeof (JavaList<>)) { + shape = new CollectionShape (JavaCollectionKind.List, targetType.GetGenericArguments ()); + return true; + } + + if (genericDefinition == typeof (ICollection<>) || genericDefinition == typeof (JavaCollection<>)) { + shape = new CollectionShape (JavaCollectionKind.Collection, targetType.GetGenericArguments ()); + return true; + } + + if (genericDefinition == typeof (IDictionary<,>) || genericDefinition == typeof (JavaDictionary<,>)) { + shape = new CollectionShape (JavaCollectionKind.Dictionary, targetType.GetGenericArguments ()); + return true; + } + + shape = default; + return false; + } + + static bool TryCreateFromMappedValueTypeFactories ( + CollectionShape shape, + IntPtr handle, + JniHandleOwnership transfer, + [NotNullWhen (true)] out object? collection) + { + if (shape.Kind == JavaCollectionKind.Dictionary) { + var hasKeyFactory = TryGetValueTypeFactory (shape.Arguments [0], out var keyFactory); + var hasValueFactory = TryGetValueTypeFactory (shape.Arguments [1], out var valueFactory); + + if (hasKeyFactory && hasValueFactory) { + collection = keyFactory.CreateDictionary (valueFactory, handle, transfer); + return true; + } + + if (hasKeyFactory) { + collection = keyFactory.CreateDictionaryWithReferenceValue (shape.Arguments [1], handle, transfer); + return true; + } + + if (hasValueFactory) { + collection = valueFactory.CreateDictionaryWithReferenceKey (shape.Arguments [0], handle, transfer); + return true; + } + + collection = null; + return false; + } + + if (TryGetValueTypeFactory (shape.Arguments [0], out var factory)) { + collection = shape.Kind == JavaCollectionKind.List + ? factory.CreateList (handle, transfer) + : factory.CreateCollection (handle, transfer); + return true; + } + + collection = null; + return false; + } + + static bool TryGetValueTypeFactory (Type type, [NotNullWhen (true)] out CollectionArgumentFactory? factory) + { + if (type.IsValueType) { + return ValueTypeArgumentFactories.TryGetValue (type, out factory); + } + + factory = null; + return false; + } + + [return: DynamicallyAccessedMembers (Constructors)] + static Type? GetClosedCollectionType ( + [DynamicallyAccessedMembers (Constructors)] + Type genericTypeDefinition, + Type[] arguments) + { + foreach (var argument in arguments) { + if (argument.IsValueType && !ValueTypeArgumentFactories.ContainsKey (argument)) { + return null; + } + } + + return MakeGenericType (genericTypeDefinition, arguments); + } + + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "Reference-type generic instantiations use NativeAOT canonical generic support. Value-type arguments are limited to explicitly rooted primitive/nullable factories.")] + [UnconditionalSuppressMessage ("Trimming", "IL2055:MakeGenericType", + Justification = "The generic type definitions are known Java collection wrappers, and value-type instantiations are limited to explicit primitive/nullable mappings.")] + [return: DynamicallyAccessedMembers (Constructors)] + static Type MakeGenericType ( + [DynamicallyAccessedMembers (Constructors)] + Type genericTypeDefinition, + Type[] arguments) + { + return genericTypeDefinition.MakeGenericType (arguments); + } + + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "The collection type is produced by SafeJavaCollectionFactory from known wrappers and explicit value-type mappings.")] + [UnconditionalSuppressMessage ("Trimming", "IL2072:UnrecognizedReflectionPattern", + Justification = "The collection type carries constructor preservation from GetClosedCollectionType.")] + static object CreateInstance ([DynamicallyAccessedMembers (Constructors)] Type collectionType, params object?[] arguments) + { + var instance = Activator.CreateInstance ( + collectionType, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + binder: null, + args: arguments, + culture: CultureInfo.InvariantCulture); + if (instance == null) { + throw new InvalidOperationException ($"Unable to create an instance of collection type '{collectionType}'."); + } + return instance; + } + + readonly struct CollectionShape + { + public CollectionShape (JavaCollectionKind kind, Type[] arguments) + { + Kind = kind; + Arguments = arguments; + } + + public JavaCollectionKind Kind { get; } + + public Type[] Arguments { get; } + } + + enum JavaCollectionKind { + List, + Collection, + Dictionary, + } + + abstract class CollectionArgumentFactory + { + internal abstract IList CreateList (IntPtr handle, JniHandleOwnership transfer); + + internal abstract ICollection CreateCollection (IntPtr handle, JniHandleOwnership transfer); + + internal abstract IDictionary CreateDictionary (CollectionArgumentFactory valueFactory, IntPtr handle, JniHandleOwnership transfer); + + internal abstract IDictionary CreateDictionaryWithReferenceKey (Type keyType, IntPtr handle, JniHandleOwnership transfer); + + internal abstract IDictionary CreateDictionaryWithReferenceValue (Type valueType, IntPtr handle, JniHandleOwnership transfer); + + internal abstract IDictionary CreateDictionaryWithKey<[DynamicallyAccessedMembers (Constructors)] TKey> ( + CollectionArgumentFactory keyFactory, + IntPtr handle, + JniHandleOwnership transfer); + } + + sealed class CollectionArgumentFactory<[DynamicallyAccessedMembers (Constructors)] T> : CollectionArgumentFactory + { + internal static readonly CollectionArgumentFactory Instance = new CollectionArgumentFactory (); + static readonly Type ReferenceKeyDictionaryType = typeof (JavaDictionary); + static readonly Type ReferenceValueDictionaryType = typeof (JavaDictionary); + + CollectionArgumentFactory () + { + } + + internal override IList CreateList (IntPtr handle, JniHandleOwnership transfer) + { + return new JavaList (handle, transfer); + } + + internal override ICollection CreateCollection (IntPtr handle, JniHandleOwnership transfer) + { + return new JavaCollection (handle, transfer); + } + + internal override IDictionary CreateDictionary (CollectionArgumentFactory valueFactory, IntPtr handle, JniHandleOwnership transfer) + { + return valueFactory.CreateDictionaryWithKey (this, handle, transfer); + } + + internal override IDictionary CreateDictionaryWithReferenceKey (Type keyType, IntPtr handle, JniHandleOwnership transfer) + { + _ = ReferenceKeyDictionaryType; + var dictionaryType = MakeGenericType (typeof (JavaDictionary<,>), [keyType, typeof (T)]); + return (IDictionary) CreateInstance (dictionaryType, handle, transfer); + } + + internal override IDictionary CreateDictionaryWithReferenceValue (Type valueType, IntPtr handle, JniHandleOwnership transfer) + { + _ = ReferenceValueDictionaryType; + var dictionaryType = MakeGenericType (typeof (JavaDictionary<,>), [typeof (T), valueType]); + return (IDictionary) CreateInstance (dictionaryType, handle, transfer); + } + + internal override IDictionary CreateDictionaryWithKey<[DynamicallyAccessedMembers (Constructors)] TKey> ( + CollectionArgumentFactory keyFactory, + IntPtr handle, + JniHandleOwnership transfer) + { + return new JavaDictionary (handle, transfer); + } + } + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index e7a73de4e35..1e4f712003b 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -55,9 +55,15 @@ static IEnumerable GetArrayTypes (JniTypeSignature typeSignature, Type ele // We only pre-generate the array types proxy map for Native AOT because we can't manipulate types at runtime. // For CoreCLR, we take advantage of the dynamic runtime and we save app size by not pre-generating the array types proxy map. if (RuntimeFeature.IsNativeAotRuntime) { - return TrimmableTypeMap.Instance.TryGetArrayProxy (elementType, typeSignature.ArrayRank, out var arrayProxy) - ? arrayProxy.GetArrayTypes () - : []; + if (TrimmableTypeMap.Instance.TryGetArrayProxy (elementType, typeSignature.ArrayRank, out var arrayProxy)) { + return arrayProxy.GetArrayTypes (); + } + + if (SafeArrayFactory.TryGetArrayType (elementType, typeSignature.ArrayRank, out var arrayType)) { + return [arrayType]; + } + + return []; } if (RuntimeFeature.IsCoreClrRuntime) { diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 7b5c6f23afd..19bfbe03988 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -325,6 +325,8 @@ + + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs index 247027d1ab8..bfd8d866a7d 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs @@ -206,6 +206,22 @@ public void GetArray_KeycodeEnum () } } + [Test] + public void GetArray_NullableInt32 () + { + var values = new int? [] { 1, null, 3 }; + using (var array = new Java.Lang.Object (JNIEnv.NewArray (values), JniHandleOwnership.TransferLocalRef)) { + Assert.AreEqual ("[Ljava/lang/Integer;", JNIEnv.GetClassNameFromInstance (array.Handle)); + + var copy = JNIEnv.GetArray (array.Handle); + AssertArrays ("GetArray", copy, values); + + Assert.IsNull (JNIEnv.GetArrayItem (array.Handle, 1)); + JNIEnv.SetArrayItem (array.Handle, 1, 2); + Assert.AreEqual ((int?) 2, JNIEnv.GetArrayItem (array.Handle, 1)); + } + } + [Test] public void GetArray_JavaLangStringArrayToJavaLangObjectArray () { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs index 92552cf6033..1140333874d 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Android.App; using Android.Content; @@ -127,6 +128,49 @@ public void MarshalInt23Array () } } + [Test] + public void FromJniHandle_IListNullableInt32 () + { + using (var source = new JavaList ()) { + source.Add (1); + source.Add (null); + source.Add (3); + + var converted = InvokeJavaConvertFromJniHandle (typeof (IList), source.Handle, JniHandleOwnership.DoNotTransfer); + try { + Assert.AreEqual (typeof (JavaList), converted.GetType ()); + + var list = (IList) converted; + Assert.AreEqual (3, list.Count); + Assert.AreEqual ((int?) 1, list [0]); + Assert.IsNull (list [1]); + Assert.AreEqual ((int?) 3, list [2]); + } finally { + (converted as IDisposable)?.Dispose (); + } + } + } + + [Test] + public void FromJniHandle_IDictionaryNullableInt32String () + { + using (var source = new JavaDictionary ()) { + source.Add (1, "one"); + source.Add (null, "null"); + + var converted = InvokeJavaConvertFromJniHandle (typeof (IDictionary), source.Handle, JniHandleOwnership.DoNotTransfer); + try { + Assert.AreEqual (typeof (JavaDictionary), converted.GetType ()); + + var dictionary = (IDictionary) converted; + Assert.AreEqual ("one", dictionary [1]); + Assert.AreEqual ("null", dictionary [null]); + } finally { + (converted as IDisposable)?.Dispose (); + } + } + } + static Java.Util.ArrayList CreateList (params int[][] items) { var list = new Java.Util.ArrayList (); @@ -138,6 +182,23 @@ static Java.Util.ArrayList CreateList (params int[][] items) } return list; } + + static object InvokeJavaConvertFromJniHandle (Type targetType, IntPtr handle, JniHandleOwnership transfer) + { + var javaConvert = typeof (Java.Lang.Object).Assembly.GetType ("Java.Interop.JavaConvert"); + Assert.IsNotNull (javaConvert); + + var method = javaConvert.GetMethod ( + "FromJniHandle", + BindingFlags.Public | BindingFlags.Static, + binder: null, + types: new [] { typeof (IntPtr), typeof (JniHandleOwnership), typeof (Type) }, + modifiers: null); + Assert.IsNotNull (method); + + var value = method.Invoke (null, new object [] { handle, transfer, targetType }); + Assert.IsNotNull (value); + return value; + } } } - From db0469070135e298abc626d338d7b166e365a91d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Jul 2026 13:04:18 +0200 Subject: [PATCH 02/14] [TrimmableTypeMap] Address safe collection factory review Fix nullable annotations on factory Try methods and keep mixed dictionary creation from bypassing the explicit value-type support map. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/SafeJavaCollectionFactory.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs index 1d7e99bce94..62bd39e8f71 100644 --- a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs @@ -56,7 +56,7 @@ internal static bool TryCreateFromJniHandle ( Type targetType, IntPtr handle, JniHandleOwnership transfer, - [NotNullWhen (true)] out object? collection) + out object? collection) { if (targetType == null) throw new ArgumentNullException (nameof (targetType)); @@ -84,7 +84,7 @@ internal static bool TryCreateFromJniHandle ( return true; } - internal static bool TryCreateInstance (Type targetType, object? items, [NotNullWhen (true)] out IJavaObject? collection) + internal static bool TryCreateInstance (Type targetType, object? items, out IJavaObject? collection) { if (targetType == null) throw new ArgumentNullException (nameof (targetType)); @@ -150,12 +150,12 @@ static bool TryCreateFromMappedValueTypeFactories ( return true; } - if (hasKeyFactory) { + if (hasKeyFactory && !shape.Arguments [1].IsValueType) { collection = keyFactory.CreateDictionaryWithReferenceValue (shape.Arguments [1], handle, transfer); return true; } - if (hasValueFactory) { + if (hasValueFactory && !shape.Arguments [0].IsValueType) { collection = valueFactory.CreateDictionaryWithReferenceKey (shape.Arguments [0], handle, transfer); return true; } From 9e0ce62b90469eb1b62b49649a657eba5df80dc5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Jul 2026 13:28:03 +0200 Subject: [PATCH 03/14] [TrimmableTypeMap] Document NativeAOT factory assumptions Expand suppression justifications and comments with the source-grounded NativeAOT behavior for array and generic type construction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/SafeArrayFactory.cs | 23 ++++++++++++-- .../Java.Interop/SafeJavaCollectionFactory.cs | 30 ++++++++++++++++--- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs index effea62d6d0..bb7b504ff3c 100644 --- a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs @@ -8,6 +8,14 @@ namespace Java.Interop { static class SafeArrayFactory { + // NativeAOT can dynamically build SZArray EETypes from canonical templates when the element type + // is a reference type. The path is RuntimeTypeInfo.MakeArrayType() -> + // ExecutionEnvironment.TryGetArrayTypeForElementType() -> TypeLoaderEnvironment/TypeBuilder, + // and StandardCanonicalizationAlgorithm canonicalizes reference DefTypes and arrays to __Canon. + // + // Value-type element arrays are different: value types do not collapse to __Canon, so an exact + // vector EEType/template must be available. These entries intentionally use typeof(T[]) and + // direct new T[length] delegates so ILC roots both the exact vector type and its allocation path. static readonly Dictionary ValueTypeArrayFactories = new Dictionary { { typeof (bool), new ArrayFactoryInfo (typeof (bool[]), static length => new bool [length]) }, { typeof (sbyte), new ArrayFactoryInfo (typeof (sbyte[]), static length => new sbyte [length]) }, @@ -48,6 +56,9 @@ internal static bool TryGetArrayType (Type elementType, int rank, [NotNullWhen ( return false; } + // Additional ranks here are jagged SZArrays, not multidimensional arrays. Once the first + // vector is available, every extra wrapper is an array whose element is itself an array + // type, i.e. a reference type, and follows NativeAOT's canonical reference-array path. for (int i = 1; i < rank; i++) { arrayType = MakeReferenceArrayType (arrayType); } @@ -114,16 +125,22 @@ static bool TryGetVectorType (Type elementType, [NotNullWhen (true)] out Type? v } [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", - Justification = "Reference-type arrays use NativeAOT canonical array support. Value-type vectors are returned from explicit rooted maps before this helper is used.")] + Justification = "NativeAOT's Type.MakeArrayType() is annotated because arbitrary element types can lack an array EEType/template. " + + "This helper is only used for reference element types, or for already-created array types when adding jagged rank. " + + "Those shapes canonicalize to __Canon/reference-array templates in NativeAOT's TypeBuilder. " + + "First-rank value-type vectors are returned from explicit typeof(T[])/new T[length] maps before this helper is used.")] [UnconditionalSuppressMessage ("Trimming", "IL2055:MakeGenericType", - Justification = "Creating an SZ array type does not require member discovery, and value-type vectors are returned from explicit rooted maps.")] + Justification = "Creating an SZArray type does not perform member discovery. " + + "Value-type vectors are not passed here unless they are already array reference types for an outer jagged rank.")] static Type MakeReferenceArrayType (Type elementType) { return elementType.MakeArrayType (); } [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", - Justification = "The array type is produced by SafeArrayFactory: reference arrays use canonical support, and value-type vectors are explicitly rooted.")] + Justification = "Array.CreateInstanceFromArrayType() immediately asks for the array TypeHandle and allocates via RuntimeAugments.NewArray(). " + + "SafeArrayFactory only passes array types that are either NativeAOT-buildable reference-array canonical shapes " + + "or exact primitive/nullable value vectors rooted by the explicit typeof(T[])/new T[length] map above.")] static Array CreateInstanceFromArrayType (Type arrayType, int length) { return Array.CreateInstanceFromArrayType (arrayType, length); diff --git a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs index 62bd39e8f71..88e2ca55db0 100644 --- a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs @@ -14,6 +14,14 @@ static class SafeJavaCollectionFactory const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + // NativeAOT's MakeGenericType() path eventually calls + // ExecutionEnvironment.TryGetConstructedGenericTypeForComponents(), then TypeBuilder.TryBuildGenericType(). + // The builder looks for a template by canonical form: reference type arguments canonicalize to __Canon, + // while value-type arguments stay value-specific. Consequently, JavaList can share the + // JavaList<__Canon> template, but JavaList and JavaList need exact rooted instantiations. + // + // These factories intentionally root the exact primitive/nullable Java collection instantiations through + // direct generic type references and constructors instead of asking MakeGenericType() to invent them. static readonly Dictionary ValueTypeArgumentFactories = new Dictionary { { typeof (bool), CollectionArgumentFactory.Instance }, { typeof (sbyte), CollectionArgumentFactory.Instance }, @@ -150,6 +158,8 @@ static bool TryCreateFromMappedValueTypeFactories ( return true; } + // Mixed dictionaries are safe only when the other side is a reference type. If it is an + // unsupported value type, MakeGenericType() would need an exact unrooted instantiation. if (hasKeyFactory && !shape.Arguments [1].IsValueType) { collection = keyFactory.CreateDictionaryWithReferenceValue (shape.Arguments [1], handle, transfer); return true; @@ -201,9 +211,14 @@ static bool TryGetValueTypeFactory (Type type, [NotNullWhen (true)] out Collecti } [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", - Justification = "Reference-type generic instantiations use NativeAOT canonical generic support. Value-type arguments are limited to explicitly rooted primitive/nullable factories.")] + Justification = "NativeAOT's Type.MakeGenericType() is annotated because arbitrary constructed generics can lack a runtime template. " + + "Callers of this helper restrict the shape to Android.Runtime Java collection wrappers. Reference arguments use NativeAOT's __Canon generic templates. " + + "Value-type arguments are either rejected or handled by explicit primitive/nullable factories that root the exact instantiation. " + + "Mixed reference/value dictionaries additionally root JavaDictionary<__Canon,T> or JavaDictionary through dedicated type tokens.")] [UnconditionalSuppressMessage ("Trimming", "IL2055:MakeGenericType", - Justification = "The generic type definitions are known Java collection wrappers, and value-type instantiations are limited to explicit primitive/nullable mappings.")] + Justification = "The generic type definitions are known Java collection wrappers, not arbitrary user types. " + + "The constructed wrapper constructors are preserved by the return annotation and by the explicit value-type factory references. " + + "The generic element arguments are not activated by this helper; element peer creation still goes through the normal JavaConvert/trimmable typemap path.")] [return: DynamicallyAccessedMembers (Constructors)] static Type MakeGenericType ( [DynamicallyAccessedMembers (Constructors)] @@ -214,9 +229,12 @@ static Type MakeGenericType ( } [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", - Justification = "The collection type is produced by SafeJavaCollectionFactory from known wrappers and explicit value-type mappings.")] + Justification = "Activator.CreateInstance() targets only collection wrapper types produced by SafeJavaCollectionFactory. " + + "Reference-only wrappers use NativeAOT's canonical generic construction, exact value-type wrappers are rooted by CollectionArgumentFactory, " + + "and mixed dictionaries root their reference/value canonical shapes with dedicated type tokens.")] [UnconditionalSuppressMessage ("Trimming", "IL2072:UnrecognizedReflectionPattern", - Justification = "The collection type carries constructor preservation from GetClosedCollectionType.")] + Justification = "The collection type is annotated with DynamicallyAccessedMembers(Constructors) by GetClosedCollectionType/MakeGenericType. " + + "Only the known JavaList, JavaCollection, and JavaDictionary constructors are invoked here.")] static object CreateInstance ([DynamicallyAccessedMembers (Constructors)] Type collectionType, params object?[] arguments) { var instance = Activator.CreateInstance ( @@ -271,6 +289,10 @@ abstract class CollectionArgumentFactory sealed class CollectionArgumentFactory<[DynamicallyAccessedMembers (Constructors)] T> : CollectionArgumentFactory { internal static readonly CollectionArgumentFactory Instance = new CollectionArgumentFactory (); + + // These tokens root the mixed reference/value dictionary canonical shapes. For example, + // JavaDictionary canonicalizes like JavaDictionary<__Canon,int>, so + // JavaDictionary supplies the NativeAOT template when T is a mapped value type. static readonly Type ReferenceKeyDictionaryType = typeof (JavaDictionary); static readonly Type ReferenceValueDictionaryType = typeof (JavaDictionary); From 0c626644ba0a7eaa377fd4c8fbdfff5ac4e3f715 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Jul 2026 13:59:30 +0200 Subject: [PATCH 04/14] [TrimmableTypeMap] Share primitive value type factories Consolidate primitive and nullable value-type rooting for safe arrays and Java collection wrappers into ValueTypeFactory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/SafeArrayFactory.cs | 42 +------ .../Java.Interop/SafeJavaCollectionFactory.cs | 103 ++-------------- .../Java.Interop/ValueTypeFactory.cs | 115 ++++++++++++++++++ src/Mono.Android/Mono.Android.csproj | 1 + 4 files changed, 129 insertions(+), 132 deletions(-) create mode 100644 src/Mono.Android/Java.Interop/ValueTypeFactory.cs diff --git a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs index bb7b504ff3c..ce3c50ba012 100644 --- a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs @@ -1,6 +1,5 @@ #nullable enable using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Android.Runtime; @@ -14,26 +13,8 @@ static class SafeArrayFactory // and StandardCanonicalizationAlgorithm canonicalizes reference DefTypes and arrays to __Canon. // // Value-type element arrays are different: value types do not collapse to __Canon, so an exact - // vector EEType/template must be available. These entries intentionally use typeof(T[]) and - // direct new T[length] delegates so ILC roots both the exact vector type and its allocation path. - static readonly Dictionary ValueTypeArrayFactories = new Dictionary { - { typeof (bool), new ArrayFactoryInfo (typeof (bool[]), static length => new bool [length]) }, - { typeof (sbyte), new ArrayFactoryInfo (typeof (sbyte[]), static length => new sbyte [length]) }, - { typeof (char), new ArrayFactoryInfo (typeof (char[]), static length => new char [length]) }, - { typeof (short), new ArrayFactoryInfo (typeof (short[]), static length => new short [length]) }, - { typeof (int), new ArrayFactoryInfo (typeof (int[]), static length => new int [length]) }, - { typeof (long), new ArrayFactoryInfo (typeof (long[]), static length => new long [length]) }, - { typeof (float), new ArrayFactoryInfo (typeof (float[]), static length => new float [length]) }, - { typeof (double), new ArrayFactoryInfo (typeof (double[]), static length => new double [length]) }, - { typeof (bool?), new ArrayFactoryInfo (typeof (bool?[]), static length => new bool? [length]) }, - { typeof (sbyte?), new ArrayFactoryInfo (typeof (sbyte?[]), static length => new sbyte? [length]) }, - { typeof (char?), new ArrayFactoryInfo (typeof (char?[]), static length => new char? [length]) }, - { typeof (short?), new ArrayFactoryInfo (typeof (short?[]), static length => new short? [length]) }, - { typeof (int?), new ArrayFactoryInfo (typeof (int?[]), static length => new int? [length]) }, - { typeof (long?), new ArrayFactoryInfo (typeof (long?[]), static length => new long? [length]) }, - { typeof (float?), new ArrayFactoryInfo (typeof (float?[]), static length => new float? [length]) }, - { typeof (double?), new ArrayFactoryInfo (typeof (double?[]), static length => new double? [length]) }, - }; + // vector EEType/template must be available. ValueTypeFactory roots typeof(T[]), new T[length], + // and the matching Java collection wrapper instantiations in one shared primitive map. internal static Type GetArrayType (Type elementType, int rank) { @@ -83,8 +64,8 @@ internal static bool TryCreateInstance (Type elementType, int rank, int length, if (elementType == null) throw new ArgumentNullException (nameof (elementType)); - if (rank == 1 && ValueTypeArrayFactories.TryGetValue (elementType, out var factory)) { - array = factory.CreateInstance (length); + if (rank == 1 && ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (elementType, out var factory)) { + array = factory.CreateArray (length); return true; } @@ -111,7 +92,7 @@ static void ValidateRank (int rank) static bool TryGetVectorType (Type elementType, [NotNullWhen (true)] out Type? vectorType) { if (elementType.IsValueType) { - if (ValueTypeArrayFactories.TryGetValue (elementType, out var factory)) { + if (ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (elementType, out var factory)) { vectorType = factory.ArrayType; return true; } @@ -151,18 +132,5 @@ static NotSupportedException CreateUnsupportedArrayException (Type elementType, return new NotSupportedException ( $"The array type for element type '{elementType}' and rank '{rank}' is not available for AOT-safe creation."); } - - sealed class ArrayFactoryInfo - { - public ArrayFactoryInfo (Type arrayType, Func createInstance) - { - ArrayType = arrayType; - CreateInstance = createInstance; - } - - public Type ArrayType { get; } - - public Func CreateInstance { get; } - } } } diff --git a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs index 88e2ca55db0..60d03d31f96 100644 --- a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs @@ -1,6 +1,5 @@ #nullable enable using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -11,7 +10,7 @@ namespace Java.Interop { static class SafeJavaCollectionFactory { - const DynamicallyAccessedMemberTypes Constructors = + internal const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; // NativeAOT's MakeGenericType() path eventually calls @@ -22,24 +21,7 @@ static class SafeJavaCollectionFactory // // These factories intentionally root the exact primitive/nullable Java collection instantiations through // direct generic type references and constructors instead of asking MakeGenericType() to invent them. - static readonly Dictionary ValueTypeArgumentFactories = new Dictionary { - { typeof (bool), CollectionArgumentFactory.Instance }, - { typeof (sbyte), CollectionArgumentFactory.Instance }, - { typeof (char), CollectionArgumentFactory.Instance }, - { typeof (short), CollectionArgumentFactory.Instance }, - { typeof (int), CollectionArgumentFactory.Instance }, - { typeof (long), CollectionArgumentFactory.Instance }, - { typeof (float), CollectionArgumentFactory.Instance }, - { typeof (double), CollectionArgumentFactory.Instance }, - { typeof (bool?), CollectionArgumentFactory.Instance }, - { typeof (sbyte?), CollectionArgumentFactory.Instance }, - { typeof (char?), CollectionArgumentFactory.Instance }, - { typeof (short?), CollectionArgumentFactory.Instance }, - { typeof (int?), CollectionArgumentFactory.Instance }, - { typeof (long?), CollectionArgumentFactory.Instance }, - { typeof (float?), CollectionArgumentFactory.Instance }, - { typeof (double?), CollectionArgumentFactory.Instance }, - }; + // The shared ValueTypeFactory map also roots the exact array vectors for these same value types. internal static bool TryGetCollectionType (Type targetType, [NotNullWhen (true)] out Type? collectionType) { @@ -185,10 +167,10 @@ static bool TryCreateFromMappedValueTypeFactories ( return false; } - static bool TryGetValueTypeFactory (Type type, [NotNullWhen (true)] out CollectionArgumentFactory? factory) + static bool TryGetValueTypeFactory (Type type, [NotNullWhen (true)] out ValueTypeFactory? factory) { if (type.IsValueType) { - return ValueTypeArgumentFactories.TryGetValue (type, out factory); + return ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (type, out factory); } factory = null; @@ -202,7 +184,7 @@ static bool TryGetValueTypeFactory (Type type, [NotNullWhen (true)] out Collecti Type[] arguments) { foreach (var argument in arguments) { - if (argument.IsValueType && !ValueTypeArgumentFactories.ContainsKey (argument)) { + if (argument.IsValueType && !ValueTypeFactory.PrimitiveTypeFactories.ContainsKey (argument)) { return null; } } @@ -220,7 +202,7 @@ static bool TryGetValueTypeFactory (Type type, [NotNullWhen (true)] out Collecti "The constructed wrapper constructors are preserved by the return annotation and by the explicit value-type factory references. " + "The generic element arguments are not activated by this helper; element peer creation still goes through the normal JavaConvert/trimmable typemap path.")] [return: DynamicallyAccessedMembers (Constructors)] - static Type MakeGenericType ( + internal static Type MakeGenericType ( [DynamicallyAccessedMembers (Constructors)] Type genericTypeDefinition, Type[] arguments) @@ -230,12 +212,12 @@ static Type MakeGenericType ( [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", Justification = "Activator.CreateInstance() targets only collection wrapper types produced by SafeJavaCollectionFactory. " + - "Reference-only wrappers use NativeAOT's canonical generic construction, exact value-type wrappers are rooted by CollectionArgumentFactory, " + + "Reference-only wrappers use NativeAOT's canonical generic construction, exact value-type wrappers are rooted by ValueTypeFactory, " + "and mixed dictionaries root their reference/value canonical shapes with dedicated type tokens.")] [UnconditionalSuppressMessage ("Trimming", "IL2072:UnrecognizedReflectionPattern", Justification = "The collection type is annotated with DynamicallyAccessedMembers(Constructors) by GetClosedCollectionType/MakeGenericType. " + "Only the known JavaList, JavaCollection, and JavaDictionary constructors are invoked here.")] - static object CreateInstance ([DynamicallyAccessedMembers (Constructors)] Type collectionType, params object?[] arguments) + internal static object CreateInstance ([DynamicallyAccessedMembers (Constructors)] Type collectionType, params object?[] arguments) { var instance = Activator.CreateInstance ( collectionType, @@ -268,74 +250,5 @@ enum JavaCollectionKind { Dictionary, } - abstract class CollectionArgumentFactory - { - internal abstract IList CreateList (IntPtr handle, JniHandleOwnership transfer); - - internal abstract ICollection CreateCollection (IntPtr handle, JniHandleOwnership transfer); - - internal abstract IDictionary CreateDictionary (CollectionArgumentFactory valueFactory, IntPtr handle, JniHandleOwnership transfer); - - internal abstract IDictionary CreateDictionaryWithReferenceKey (Type keyType, IntPtr handle, JniHandleOwnership transfer); - - internal abstract IDictionary CreateDictionaryWithReferenceValue (Type valueType, IntPtr handle, JniHandleOwnership transfer); - - internal abstract IDictionary CreateDictionaryWithKey<[DynamicallyAccessedMembers (Constructors)] TKey> ( - CollectionArgumentFactory keyFactory, - IntPtr handle, - JniHandleOwnership transfer); - } - - sealed class CollectionArgumentFactory<[DynamicallyAccessedMembers (Constructors)] T> : CollectionArgumentFactory - { - internal static readonly CollectionArgumentFactory Instance = new CollectionArgumentFactory (); - - // These tokens root the mixed reference/value dictionary canonical shapes. For example, - // JavaDictionary canonicalizes like JavaDictionary<__Canon,int>, so - // JavaDictionary supplies the NativeAOT template when T is a mapped value type. - static readonly Type ReferenceKeyDictionaryType = typeof (JavaDictionary); - static readonly Type ReferenceValueDictionaryType = typeof (JavaDictionary); - - CollectionArgumentFactory () - { - } - - internal override IList CreateList (IntPtr handle, JniHandleOwnership transfer) - { - return new JavaList (handle, transfer); - } - - internal override ICollection CreateCollection (IntPtr handle, JniHandleOwnership transfer) - { - return new JavaCollection (handle, transfer); - } - - internal override IDictionary CreateDictionary (CollectionArgumentFactory valueFactory, IntPtr handle, JniHandleOwnership transfer) - { - return valueFactory.CreateDictionaryWithKey (this, handle, transfer); - } - - internal override IDictionary CreateDictionaryWithReferenceKey (Type keyType, IntPtr handle, JniHandleOwnership transfer) - { - _ = ReferenceKeyDictionaryType; - var dictionaryType = MakeGenericType (typeof (JavaDictionary<,>), [keyType, typeof (T)]); - return (IDictionary) CreateInstance (dictionaryType, handle, transfer); - } - - internal override IDictionary CreateDictionaryWithReferenceValue (Type valueType, IntPtr handle, JniHandleOwnership transfer) - { - _ = ReferenceValueDictionaryType; - var dictionaryType = MakeGenericType (typeof (JavaDictionary<,>), [typeof (T), valueType]); - return (IDictionary) CreateInstance (dictionaryType, handle, transfer); - } - - internal override IDictionary CreateDictionaryWithKey<[DynamicallyAccessedMembers (Constructors)] TKey> ( - CollectionArgumentFactory keyFactory, - IntPtr handle, - JniHandleOwnership transfer) - { - return new JavaDictionary (handle, transfer); - } - } } } diff --git a/src/Mono.Android/Java.Interop/ValueTypeFactory.cs b/src/Mono.Android/Java.Interop/ValueTypeFactory.cs new file mode 100644 index 00000000000..3d88b134e3e --- /dev/null +++ b/src/Mono.Android/Java.Interop/ValueTypeFactory.cs @@ -0,0 +1,115 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using Android.Runtime; + +namespace Java.Interop { + abstract class ValueTypeFactory + { + // NativeAOT's MakeGenericType() and MakeArrayType() paths use canonical templates. + // Reference types collapse to __Canon, but value types stay value-specific. This map + // intentionally roots each primitive/nullable value shape through direct typeof(T), + // typeof(T[]), new T[length], and Java collection wrapper constructor references. + internal static readonly Dictionary PrimitiveTypeFactories = new () { + { typeof (bool), new ValueTypeFactory () }, + { typeof (sbyte), new ValueTypeFactory () }, + { typeof (char), new ValueTypeFactory () }, + { typeof (short), new ValueTypeFactory () }, + { typeof (int), new ValueTypeFactory () }, + { typeof (long), new ValueTypeFactory () }, + { typeof (float), new ValueTypeFactory () }, + { typeof (double), new ValueTypeFactory () }, + { typeof (bool?), new ValueTypeFactory () }, + { typeof (sbyte?), new ValueTypeFactory () }, + { typeof (char?), new ValueTypeFactory () }, + { typeof (short?), new ValueTypeFactory () }, + { typeof (int?), new ValueTypeFactory () }, + { typeof (long?), new ValueTypeFactory () }, + { typeof (float?), new ValueTypeFactory () }, + { typeof (double?), new ValueTypeFactory () }, + }; + + public abstract Type ValueType { get; } + + public abstract Type ArrayType { get; } + + public abstract Array CreateArray (int length); + + internal abstract IList CreateList (IntPtr handle, JniHandleOwnership transfer); + + internal abstract ICollection CreateCollection (IntPtr handle, JniHandleOwnership transfer); + + internal abstract IDictionary CreateDictionary (ValueTypeFactory valueFactory, IntPtr handle, JniHandleOwnership transfer); + + internal abstract IDictionary CreateDictionaryWithReferenceKey (Type keyType, IntPtr handle, JniHandleOwnership transfer); + + internal abstract IDictionary CreateDictionaryWithReferenceValue (Type valueType, IntPtr handle, JniHandleOwnership transfer); + + internal abstract IDictionary CreateDictionaryWithKey<[DynamicallyAccessedMembers (SafeJavaCollectionFactory.Constructors)] TKey> ( + ValueTypeFactory keyFactory, + IntPtr handle, + JniHandleOwnership transfer); + } + + sealed class ValueTypeFactory<[DynamicallyAccessedMembers (SafeJavaCollectionFactory.Constructors)] T> : ValueTypeFactory + { + // These tokens root the mixed reference/value dictionary canonical shapes. For example, + // JavaDictionary canonicalizes like JavaDictionary<__Canon,int>, so + // JavaDictionary supplies the NativeAOT template when T is a mapped value type. + static readonly Type ReferenceKeyDictionaryType = typeof (JavaDictionary); + static readonly Type ReferenceValueDictionaryType = typeof (JavaDictionary); + + internal ValueTypeFactory () + { + } + + public override Type ValueType { get; } = typeof (T); + + public override Type ArrayType { get; } = typeof (T[]); + + public override Array CreateArray (int length) + { + return new T [length]; + } + + internal override IList CreateList (IntPtr handle, JniHandleOwnership transfer) + { + return new JavaList (handle, transfer); + } + + internal override ICollection CreateCollection (IntPtr handle, JniHandleOwnership transfer) + { + return new JavaCollection (handle, transfer); + } + + internal override IDictionary CreateDictionary (ValueTypeFactory valueFactory, IntPtr handle, JniHandleOwnership transfer) + { + return valueFactory.CreateDictionaryWithKey (this, handle, transfer); + } + + internal override IDictionary CreateDictionaryWithReferenceKey (Type keyType, IntPtr handle, JniHandleOwnership transfer) + { + _ = ReferenceKeyDictionaryType; + var dictionaryType = SafeJavaCollectionFactory.MakeGenericType (typeof (JavaDictionary<,>), [keyType, typeof (T)]); + return (IDictionary) SafeJavaCollectionFactory.CreateInstance (dictionaryType, handle, transfer); + } + + internal override IDictionary CreateDictionaryWithReferenceValue (Type valueType, IntPtr handle, JniHandleOwnership transfer) + { + _ = ReferenceValueDictionaryType; + var dictionaryType = SafeJavaCollectionFactory.MakeGenericType (typeof (JavaDictionary<,>), [typeof (T), valueType]); + return (IDictionary) SafeJavaCollectionFactory.CreateInstance (dictionaryType, handle, transfer); + } + + internal override IDictionary CreateDictionaryWithKey<[DynamicallyAccessedMembers (SafeJavaCollectionFactory.Constructors)] TKey> ( + ValueTypeFactory keyFactory, + IntPtr handle, + JniHandleOwnership transfer) + { + return new JavaDictionary (handle, transfer); + } + } +} diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 19bfbe03988..4f7d937ba56 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -328,6 +328,7 @@ + From 53d50fbb6269fcf697a44c046eaac049a0507c27 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Jul 2026 14:42:11 +0200 Subject: [PATCH 05/14] [TrimmableTypeMap] Clarify array factory AOT justification Update the suppression text to say first-rank value vectors bypass CreateInstanceFromArrayType and are allocated through ValueTypeFactory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Java.Interop/SafeArrayFactory.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs index ce3c50ba012..09e681a39c4 100644 --- a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs @@ -120,8 +120,9 @@ static Type MakeReferenceArrayType (Type elementType) [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", Justification = "Array.CreateInstanceFromArrayType() immediately asks for the array TypeHandle and allocates via RuntimeAugments.NewArray(). " + - "SafeArrayFactory only passes array types that are either NativeAOT-buildable reference-array canonical shapes " + - "or exact primitive/nullable value vectors rooted by the explicit typeof(T[])/new T[length] map above.")] + "SafeArrayFactory only passes NativeAOT-buildable reference-array canonical shapes here: either arrays whose original element type is a reference type, " + + "or outer jagged array wrappers whose element is already an array reference type. " + + "First-rank primitive/nullable value vectors are allocated directly by ValueTypeFactory.CreateArray() before reaching this helper.")] static Array CreateInstanceFromArrayType (Type arrayType, int length) { return Array.CreateInstanceFromArrayType (arrayType, length); From b959f4a2256f0a535c4f805c1a4fc0ee9724df5f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Jul 2026 17:00:31 +0200 Subject: [PATCH 06/14] [TrimmableTypeMap] Fix NativeAOT warning leak and update apk size baselines The new nullable/collection marshaling paths made JavaConvert.GetJniHandleConverter AOT-reachable, exposing the legacy TryMakeGenericCollectionTypeFactory helper whose MakeGenericType() calls only suppressed IL2055 (trimming) and not IL3050 (AOT), leaking 6 NativeAOT warnings. Add the matching IL3050 suppression; that branch only runs when !RuntimeFeature.TrimmableTypeMap (Mono/CoreCLR), never under NativeAOT. Update BuildReleaseArm64 SimpleDotNet CoreCLR/NativeAOT apkdesc baselines to accept the expected size increase from the rooted array/collection typemap entries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Java.Interop/JavaConvert.cs | 5 +++++ ...ldReleaseArm64SimpleDotNet.CoreCLR.apkdesc | 20 +++++++++---------- ...ReleaseArm64SimpleDotNet.NativeAOT.apkdesc | 6 +++--- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index 8aebf70687c..3e968192a50 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -102,6 +102,11 @@ static class JavaConvert { [UnconditionalSuppressMessage ("ReflectionAnalysis", "IL2055:RequiresUnreferencedCode", Justification = "The target generic type is expected to be preserved by the trimmer as the target type in marshaling.")] + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "This legacy MakeGenericType() path is only reached from the '!RuntimeFeature.TrimmableTypeMap' branch above, " + + "i.e. on the reflection-based Mono/CoreCLR runtimes where dynamic code is available. NativeAOT always enables the trimmable " + + "type map, so under AOT the branch above (SafeJavaCollectionFactory) is used instead and this helper is never executed. " + + "The AOT-safe generic collection construction lives in SafeJavaCollectionFactory.")] static Func? TryMakeGenericCollectionTypeFactory (Type target) { if (target.GetGenericTypeDefinition() == typeof (IDictionary<,>)) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc index 065272635fa..9390d2bf1e3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc @@ -5,34 +5,34 @@ "Size": 3036 }, "classes.dex": { - "Size": 402352 + "Size": 402852 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 2717688 + "Size": 2816776 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2835128 + "Size": 2757816 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4891056 + "Size": 4837240 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 1324320 + "Size": 1264112 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72112 }, "lib/arm64-v8a/libSystem.IO.Compression.Native.so": { - "Size": 1262888 + "Size": 1258776 }, "lib/arm64-v8a/libSystem.Native.so": { - "Size": 101888 + "Size": 99664 }, "lib/arm64-v8a/libSystem.Security.Cryptography.Native.Android.so": { - "Size": 168080 + "Size": 163936 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 20968 + "Size": 23864 }, "res/drawable-hdpi-v4/icon.png": { "Size": 2178 @@ -59,5 +59,5 @@ "Size": 1904 } }, - "PackageSize": 7271867 + "PackageSize": 7337403 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc index d19175aaf76..dcdca9358c9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc @@ -5,10 +5,10 @@ "Size": 3124 }, "classes.dex": { - "Size": 22016 + "Size": 22804 }, "lib/arm64-v8a/libUnnamedProject.so": { - "Size": 5411456 + "Size": 5747648 }, "res/drawable-hdpi-v4/icon.png": { "Size": 2178 @@ -35,5 +35,5 @@ "Size": 1904 } }, - "PackageSize": 2286363 + "PackageSize": 2401051 } \ No newline at end of file From 11430447db17047b9e33bfaa4f57bd8e426ed211 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Jul 2026 23:29:28 +0200 Subject: [PATCH 07/14] [TrimmableTypeMap] Address review: file-scoped namespaces, drop unused API - Remove the unused SafeJavaCollectionFactory.TryCreateInstance(Type, object?, out IJavaObject?) speculative overload (no call sites). - Convert the three new factory files to file-scoped namespaces per repo conventions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/SafeArrayFactory.cs | 204 +++++----- .../Java.Interop/SafeJavaCollectionFactory.cs | 366 ++++++++---------- .../Java.Interop/ValueTypeFactory.cs | 202 +++++----- 3 files changed, 374 insertions(+), 398 deletions(-) diff --git a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs index 09e681a39c4..f9b32b2f470 100644 --- a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs @@ -4,134 +4,134 @@ using Microsoft.Android.Runtime; -namespace Java.Interop { - static class SafeArrayFactory +namespace Java.Interop; + +static class SafeArrayFactory +{ + // NativeAOT can dynamically build SZArray EETypes from canonical templates when the element type + // is a reference type. The path is RuntimeTypeInfo.MakeArrayType() -> + // ExecutionEnvironment.TryGetArrayTypeForElementType() -> TypeLoaderEnvironment/TypeBuilder, + // and StandardCanonicalizationAlgorithm canonicalizes reference DefTypes and arrays to __Canon. + // + // Value-type element arrays are different: value types do not collapse to __Canon, so an exact + // vector EEType/template must be available. ValueTypeFactory roots typeof(T[]), new T[length], + // and the matching Java collection wrapper instantiations in one shared primitive map. + + internal static Type GetArrayType (Type elementType, int rank) { - // NativeAOT can dynamically build SZArray EETypes from canonical templates when the element type - // is a reference type. The path is RuntimeTypeInfo.MakeArrayType() -> - // ExecutionEnvironment.TryGetArrayTypeForElementType() -> TypeLoaderEnvironment/TypeBuilder, - // and StandardCanonicalizationAlgorithm canonicalizes reference DefTypes and arrays to __Canon. - // - // Value-type element arrays are different: value types do not collapse to __Canon, so an exact - // vector EEType/template must be available. ValueTypeFactory roots typeof(T[]), new T[length], - // and the matching Java collection wrapper instantiations in one shared primitive map. - - internal static Type GetArrayType (Type elementType, int rank) - { - if (TryGetArrayType (elementType, rank, out var arrayType)) { - return arrayType; - } - - throw CreateUnsupportedArrayException (elementType, rank); + if (TryGetArrayType (elementType, rank, out var arrayType)) { + return arrayType; } - internal static bool TryGetArrayType (Type elementType, int rank, [NotNullWhen (true)] out Type? arrayType) - { - ValidateRank (rank); + throw CreateUnsupportedArrayException (elementType, rank); + } - if (elementType == null) - throw new ArgumentNullException (nameof (elementType)); + internal static bool TryGetArrayType (Type elementType, int rank, [NotNullWhen (true)] out Type? arrayType) + { + ValidateRank (rank); - if (!TryGetVectorType (elementType, out arrayType)) { - arrayType = null; - return false; - } + if (elementType == null) + throw new ArgumentNullException (nameof (elementType)); - // Additional ranks here are jagged SZArrays, not multidimensional arrays. Once the first - // vector is available, every extra wrapper is an array whose element is itself an array - // type, i.e. a reference type, and follows NativeAOT's canonical reference-array path. - for (int i = 1; i < rank; i++) { - arrayType = MakeReferenceArrayType (arrayType); - } + if (!TryGetVectorType (elementType, out arrayType)) { + arrayType = null; + return false; + } - return true; + // Additional ranks here are jagged SZArrays, not multidimensional arrays. Once the first + // vector is available, every extra wrapper is an array whose element is itself an array + // type, i.e. a reference type, and follows NativeAOT's canonical reference-array path. + for (int i = 1; i < rank; i++) { + arrayType = MakeReferenceArrayType (arrayType); } - internal static Array CreateInstance (Type elementType, int rank, int length) - { - if (TryCreateInstance (elementType, rank, length, out var array)) { - return array; - } + return true; + } - throw CreateUnsupportedArrayException (elementType, rank); + internal static Array CreateInstance (Type elementType, int rank, int length) + { + if (TryCreateInstance (elementType, rank, length, out var array)) { + return array; } - internal static bool TryCreateInstance (Type elementType, int rank, int length, [NotNullWhen (true)] out Array? array) - { - ValidateRank (rank); - ArgumentOutOfRangeException.ThrowIfNegative (length); + throw CreateUnsupportedArrayException (elementType, rank); + } - if (elementType == null) - throw new ArgumentNullException (nameof (elementType)); + internal static bool TryCreateInstance (Type elementType, int rank, int length, [NotNullWhen (true)] out Array? array) + { + ValidateRank (rank); + ArgumentOutOfRangeException.ThrowIfNegative (length); - if (rank == 1 && ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (elementType, out var factory)) { - array = factory.CreateArray (length); - return true; - } + if (elementType == null) + throw new ArgumentNullException (nameof (elementType)); - if (TryGetArrayType (elementType, rank, out var arrayType)) { - array = CreateInstanceFromArrayType (arrayType, length); - return true; - } - - if (RuntimeFeature.TrimmableTypeMap && RuntimeFeature.IsNativeAotRuntime && - TrimmableTypeMap.Instance.TryGetArrayProxy (elementType, rank, out var arrayProxy)) { - array = arrayProxy.CreateManagedArray (length); - return true; - } + if (rank == 1 && ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (elementType, out var factory)) { + array = factory.CreateArray (length); + return true; + } - array = null; - return false; + if (TryGetArrayType (elementType, rank, out var arrayType)) { + array = CreateInstanceFromArrayType (arrayType, length); + return true; } - static void ValidateRank (int rank) - { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero (rank); + if (RuntimeFeature.TrimmableTypeMap && RuntimeFeature.IsNativeAotRuntime && + TrimmableTypeMap.Instance.TryGetArrayProxy (elementType, rank, out var arrayProxy)) { + array = arrayProxy.CreateManagedArray (length); + return true; } - static bool TryGetVectorType (Type elementType, [NotNullWhen (true)] out Type? vectorType) - { - if (elementType.IsValueType) { - if (ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (elementType, out var factory)) { - vectorType = factory.ArrayType; - return true; - } + array = null; + return false; + } - vectorType = null; - return false; + static void ValidateRank (int rank) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero (rank); + } + + static bool TryGetVectorType (Type elementType, [NotNullWhen (true)] out Type? vectorType) + { + if (elementType.IsValueType) { + if (ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (elementType, out var factory)) { + vectorType = factory.ArrayType; + return true; } - vectorType = MakeReferenceArrayType (elementType); - return true; + vectorType = null; + return false; } - [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", - Justification = "NativeAOT's Type.MakeArrayType() is annotated because arbitrary element types can lack an array EEType/template. " + - "This helper is only used for reference element types, or for already-created array types when adding jagged rank. " + - "Those shapes canonicalize to __Canon/reference-array templates in NativeAOT's TypeBuilder. " + - "First-rank value-type vectors are returned from explicit typeof(T[])/new T[length] maps before this helper is used.")] - [UnconditionalSuppressMessage ("Trimming", "IL2055:MakeGenericType", - Justification = "Creating an SZArray type does not perform member discovery. " + - "Value-type vectors are not passed here unless they are already array reference types for an outer jagged rank.")] - static Type MakeReferenceArrayType (Type elementType) - { - return elementType.MakeArrayType (); - } + vectorType = MakeReferenceArrayType (elementType); + return true; + } - [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", - Justification = "Array.CreateInstanceFromArrayType() immediately asks for the array TypeHandle and allocates via RuntimeAugments.NewArray(). " + - "SafeArrayFactory only passes NativeAOT-buildable reference-array canonical shapes here: either arrays whose original element type is a reference type, " + - "or outer jagged array wrappers whose element is already an array reference type. " + - "First-rank primitive/nullable value vectors are allocated directly by ValueTypeFactory.CreateArray() before reaching this helper.")] - static Array CreateInstanceFromArrayType (Type arrayType, int length) - { - return Array.CreateInstanceFromArrayType (arrayType, length); - } + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "NativeAOT's Type.MakeArrayType() is annotated because arbitrary element types can lack an array EEType/template. " + + "This helper is only used for reference element types, or for already-created array types when adding jagged rank. " + + "Those shapes canonicalize to __Canon/reference-array templates in NativeAOT's TypeBuilder. " + + "First-rank value-type vectors are returned from explicit typeof(T[])/new T[length] maps before this helper is used.")] + [UnconditionalSuppressMessage ("Trimming", "IL2055:MakeGenericType", + Justification = "Creating an SZArray type does not perform member discovery. " + + "Value-type vectors are not passed here unless they are already array reference types for an outer jagged rank.")] + static Type MakeReferenceArrayType (Type elementType) + { + return elementType.MakeArrayType (); + } - static NotSupportedException CreateUnsupportedArrayException (Type elementType, int rank) - { - return new NotSupportedException ( - $"The array type for element type '{elementType}' and rank '{rank}' is not available for AOT-safe creation."); - } + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "Array.CreateInstanceFromArrayType() immediately asks for the array TypeHandle and allocates via RuntimeAugments.NewArray(). " + + "SafeArrayFactory only passes NativeAOT-buildable reference-array canonical shapes here: either arrays whose original element type is a reference type, " + + "or outer jagged array wrappers whose element is already an array reference type. " + + "First-rank primitive/nullable value vectors are allocated directly by ValueTypeFactory.CreateArray() before reaching this helper.")] + static Array CreateInstanceFromArrayType (Type arrayType, int length) + { + return Array.CreateInstanceFromArrayType (arrayType, length); + } + + static NotSupportedException CreateUnsupportedArrayException (Type elementType, int rank) + { + return new NotSupportedException ( + $"The array type for element type '{elementType}' and rank '{rank}' is not available for AOT-safe creation."); } } diff --git a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs index 60d03d31f96..75faa734d5e 100644 --- a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs @@ -7,159 +7,125 @@ using Android.Runtime; -namespace Java.Interop { - static class SafeJavaCollectionFactory +namespace Java.Interop; + +static class SafeJavaCollectionFactory +{ + internal const DynamicallyAccessedMemberTypes Constructors = + DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + + // NativeAOT's MakeGenericType() path eventually calls + // ExecutionEnvironment.TryGetConstructedGenericTypeForComponents(), then TypeBuilder.TryBuildGenericType(). + // The builder looks for a template by canonical form: reference type arguments canonicalize to __Canon, + // while value-type arguments stay value-specific. Consequently, JavaList can share the + // JavaList<__Canon> template, but JavaList and JavaList need exact rooted instantiations. + // + // These factories intentionally root the exact primitive/nullable Java collection instantiations through + // direct generic type references and constructors instead of asking MakeGenericType() to invent them. + // The shared ValueTypeFactory map also roots the exact array vectors for these same value types. + + internal static bool TryGetCollectionType (Type targetType, [NotNullWhen (true)] out Type? collectionType) { - internal const DynamicallyAccessedMemberTypes Constructors = - DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - - // NativeAOT's MakeGenericType() path eventually calls - // ExecutionEnvironment.TryGetConstructedGenericTypeForComponents(), then TypeBuilder.TryBuildGenericType(). - // The builder looks for a template by canonical form: reference type arguments canonicalize to __Canon, - // while value-type arguments stay value-specific. Consequently, JavaList can share the - // JavaList<__Canon> template, but JavaList and JavaList need exact rooted instantiations. - // - // These factories intentionally root the exact primitive/nullable Java collection instantiations through - // direct generic type references and constructors instead of asking MakeGenericType() to invent them. - // The shared ValueTypeFactory map also roots the exact array vectors for these same value types. - - internal static bool TryGetCollectionType (Type targetType, [NotNullWhen (true)] out Type? collectionType) - { - if (targetType == null) - throw new ArgumentNullException (nameof (targetType)); - - if (!TryGetCollectionShape (targetType, out var shape)) { - collectionType = null; - return false; - } + if (targetType == null) + throw new ArgumentNullException (nameof (targetType)); - collectionType = shape.Kind switch { - JavaCollectionKind.List => GetClosedCollectionType (typeof (JavaList<>), shape.Arguments), - JavaCollectionKind.Collection => GetClosedCollectionType (typeof (JavaCollection<>), shape.Arguments), - JavaCollectionKind.Dictionary => GetClosedCollectionType (typeof (JavaDictionary<,>), shape.Arguments), - _ => null, - }; - return collectionType != null; + if (!TryGetCollectionShape (targetType, out var shape)) { + collectionType = null; + return false; } - internal static bool TryCreateFromJniHandle ( - Type targetType, - IntPtr handle, - JniHandleOwnership transfer, - out object? collection) - { - if (targetType == null) - throw new ArgumentNullException (nameof (targetType)); - - if (handle == IntPtr.Zero) { - collection = null; - return true; - } + collectionType = shape.Kind switch { + JavaCollectionKind.List => GetClosedCollectionType (typeof (JavaList<>), shape.Arguments), + JavaCollectionKind.Collection => GetClosedCollectionType (typeof (JavaCollection<>), shape.Arguments), + JavaCollectionKind.Dictionary => GetClosedCollectionType (typeof (JavaDictionary<,>), shape.Arguments), + _ => null, + }; + return collectionType != null; + } - if (!TryGetCollectionShape (targetType, out var shape)) { - collection = null; - return false; - } + internal static bool TryCreateFromJniHandle ( + Type targetType, + IntPtr handle, + JniHandleOwnership transfer, + out object? collection) + { + if (targetType == null) + throw new ArgumentNullException (nameof (targetType)); - if (TryCreateFromMappedValueTypeFactories (shape, handle, transfer, out collection)) { - return true; - } + if (handle == IntPtr.Zero) { + collection = null; + return true; + } - if (!TryGetCollectionType (targetType, out var collectionType)) { - collection = null; - return false; - } + if (!TryGetCollectionShape (targetType, out var shape)) { + collection = null; + return false; + } - collection = CreateInstance (collectionType, handle, transfer); + if (TryCreateFromMappedValueTypeFactories (shape, handle, transfer, out collection)) { return true; } - internal static bool TryCreateInstance (Type targetType, object? items, out IJavaObject? collection) - { - if (targetType == null) - throw new ArgumentNullException (nameof (targetType)); + if (!TryGetCollectionType (targetType, out var collectionType)) { + collection = null; + return false; + } - if (items == null) { - collection = null; - return true; - } + collection = CreateInstance (collectionType, handle, transfer); + return true; + } - if (!TryGetCollectionType (targetType, out var collectionType)) { - collection = null; - return false; - } + static bool TryGetCollectionShape (Type targetType, out CollectionShape shape) + { + if (!targetType.IsGenericType || targetType.IsGenericTypeDefinition) { + shape = default; + return false; + } - var instance = CreateInstance (collectionType, items); - if (instance is not IJavaObject javaObject) { - throw new InvalidOperationException ($"The collection type '{collectionType}' did not create an IJavaObject instance."); - } + var genericDefinition = targetType.GetGenericTypeDefinition (); + if (genericDefinition == typeof (IList<>) || genericDefinition == typeof (JavaList<>)) { + shape = new CollectionShape (JavaCollectionKind.List, targetType.GetGenericArguments ()); + return true; + } - collection = javaObject; + if (genericDefinition == typeof (ICollection<>) || genericDefinition == typeof (JavaCollection<>)) { + shape = new CollectionShape (JavaCollectionKind.Collection, targetType.GetGenericArguments ()); return true; } - static bool TryGetCollectionShape (Type targetType, out CollectionShape shape) - { - if (!targetType.IsGenericType || targetType.IsGenericTypeDefinition) { - shape = default; - return false; - } + if (genericDefinition == typeof (IDictionary<,>) || genericDefinition == typeof (JavaDictionary<,>)) { + shape = new CollectionShape (JavaCollectionKind.Dictionary, targetType.GetGenericArguments ()); + return true; + } - var genericDefinition = targetType.GetGenericTypeDefinition (); - if (genericDefinition == typeof (IList<>) || genericDefinition == typeof (JavaList<>)) { - shape = new CollectionShape (JavaCollectionKind.List, targetType.GetGenericArguments ()); - return true; - } + shape = default; + return false; + } - if (genericDefinition == typeof (ICollection<>) || genericDefinition == typeof (JavaCollection<>)) { - shape = new CollectionShape (JavaCollectionKind.Collection, targetType.GetGenericArguments ()); - return true; - } + static bool TryCreateFromMappedValueTypeFactories ( + CollectionShape shape, + IntPtr handle, + JniHandleOwnership transfer, + [NotNullWhen (true)] out object? collection) + { + if (shape.Kind == JavaCollectionKind.Dictionary) { + var hasKeyFactory = TryGetValueTypeFactory (shape.Arguments [0], out var keyFactory); + var hasValueFactory = TryGetValueTypeFactory (shape.Arguments [1], out var valueFactory); - if (genericDefinition == typeof (IDictionary<,>) || genericDefinition == typeof (JavaDictionary<,>)) { - shape = new CollectionShape (JavaCollectionKind.Dictionary, targetType.GetGenericArguments ()); + if (hasKeyFactory && hasValueFactory) { + collection = keyFactory.CreateDictionary (valueFactory, handle, transfer); return true; } - shape = default; - return false; - } - - static bool TryCreateFromMappedValueTypeFactories ( - CollectionShape shape, - IntPtr handle, - JniHandleOwnership transfer, - [NotNullWhen (true)] out object? collection) - { - if (shape.Kind == JavaCollectionKind.Dictionary) { - var hasKeyFactory = TryGetValueTypeFactory (shape.Arguments [0], out var keyFactory); - var hasValueFactory = TryGetValueTypeFactory (shape.Arguments [1], out var valueFactory); - - if (hasKeyFactory && hasValueFactory) { - collection = keyFactory.CreateDictionary (valueFactory, handle, transfer); - return true; - } - - // Mixed dictionaries are safe only when the other side is a reference type. If it is an - // unsupported value type, MakeGenericType() would need an exact unrooted instantiation. - if (hasKeyFactory && !shape.Arguments [1].IsValueType) { - collection = keyFactory.CreateDictionaryWithReferenceValue (shape.Arguments [1], handle, transfer); - return true; - } - - if (hasValueFactory && !shape.Arguments [0].IsValueType) { - collection = valueFactory.CreateDictionaryWithReferenceKey (shape.Arguments [0], handle, transfer); - return true; - } - - collection = null; - return false; + // Mixed dictionaries are safe only when the other side is a reference type. If it is an + // unsupported value type, MakeGenericType() would need an exact unrooted instantiation. + if (hasKeyFactory && !shape.Arguments [1].IsValueType) { + collection = keyFactory.CreateDictionaryWithReferenceValue (shape.Arguments [1], handle, transfer); + return true; } - if (TryGetValueTypeFactory (shape.Arguments [0], out var factory)) { - collection = shape.Kind == JavaCollectionKind.List - ? factory.CreateList (handle, transfer) - : factory.CreateCollection (handle, transfer); + if (hasValueFactory && !shape.Arguments [0].IsValueType) { + collection = valueFactory.CreateDictionaryWithReferenceKey (shape.Arguments [0], handle, transfer); return true; } @@ -167,88 +133,98 @@ static bool TryCreateFromMappedValueTypeFactories ( return false; } - static bool TryGetValueTypeFactory (Type type, [NotNullWhen (true)] out ValueTypeFactory? factory) - { - if (type.IsValueType) { - return ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (type, out factory); - } - - factory = null; - return false; + if (TryGetValueTypeFactory (shape.Arguments [0], out var factory)) { + collection = shape.Kind == JavaCollectionKind.List + ? factory.CreateList (handle, transfer) + : factory.CreateCollection (handle, transfer); + return true; } - [return: DynamicallyAccessedMembers (Constructors)] - static Type? GetClosedCollectionType ( - [DynamicallyAccessedMembers (Constructors)] - Type genericTypeDefinition, - Type[] arguments) - { - foreach (var argument in arguments) { - if (argument.IsValueType && !ValueTypeFactory.PrimitiveTypeFactories.ContainsKey (argument)) { - return null; - } - } + collection = null; + return false; + } - return MakeGenericType (genericTypeDefinition, arguments); + static bool TryGetValueTypeFactory (Type type, [NotNullWhen (true)] out ValueTypeFactory? factory) + { + if (type.IsValueType) { + return ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (type, out factory); } - [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", - Justification = "NativeAOT's Type.MakeGenericType() is annotated because arbitrary constructed generics can lack a runtime template. " + - "Callers of this helper restrict the shape to Android.Runtime Java collection wrappers. Reference arguments use NativeAOT's __Canon generic templates. " + - "Value-type arguments are either rejected or handled by explicit primitive/nullable factories that root the exact instantiation. " + - "Mixed reference/value dictionaries additionally root JavaDictionary<__Canon,T> or JavaDictionary through dedicated type tokens.")] - [UnconditionalSuppressMessage ("Trimming", "IL2055:MakeGenericType", - Justification = "The generic type definitions are known Java collection wrappers, not arbitrary user types. " + - "The constructed wrapper constructors are preserved by the return annotation and by the explicit value-type factory references. " + - "The generic element arguments are not activated by this helper; element peer creation still goes through the normal JavaConvert/trimmable typemap path.")] - [return: DynamicallyAccessedMembers (Constructors)] - internal static Type MakeGenericType ( - [DynamicallyAccessedMembers (Constructors)] - Type genericTypeDefinition, - Type[] arguments) - { - return genericTypeDefinition.MakeGenericType (arguments); - } + factory = null; + return false; + } - [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", - Justification = "Activator.CreateInstance() targets only collection wrapper types produced by SafeJavaCollectionFactory. " + - "Reference-only wrappers use NativeAOT's canonical generic construction, exact value-type wrappers are rooted by ValueTypeFactory, " + - "and mixed dictionaries root their reference/value canonical shapes with dedicated type tokens.")] - [UnconditionalSuppressMessage ("Trimming", "IL2072:UnrecognizedReflectionPattern", - Justification = "The collection type is annotated with DynamicallyAccessedMembers(Constructors) by GetClosedCollectionType/MakeGenericType. " + - "Only the known JavaList, JavaCollection, and JavaDictionary constructors are invoked here.")] - internal static object CreateInstance ([DynamicallyAccessedMembers (Constructors)] Type collectionType, params object?[] arguments) - { - var instance = Activator.CreateInstance ( - collectionType, - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, - binder: null, - args: arguments, - culture: CultureInfo.InvariantCulture); - if (instance == null) { - throw new InvalidOperationException ($"Unable to create an instance of collection type '{collectionType}'."); + [return: DynamicallyAccessedMembers (Constructors)] + static Type? GetClosedCollectionType ( + [DynamicallyAccessedMembers (Constructors)] + Type genericTypeDefinition, + Type[] arguments) + { + foreach (var argument in arguments) { + if (argument.IsValueType && !ValueTypeFactory.PrimitiveTypeFactories.ContainsKey (argument)) { + return null; } - return instance; } - readonly struct CollectionShape - { - public CollectionShape (JavaCollectionKind kind, Type[] arguments) - { - Kind = kind; - Arguments = arguments; - } + return MakeGenericType (genericTypeDefinition, arguments); + } - public JavaCollectionKind Kind { get; } + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "NativeAOT's Type.MakeGenericType() is annotated because arbitrary constructed generics can lack a runtime template. " + + "Callers of this helper restrict the shape to Android.Runtime Java collection wrappers. Reference arguments use NativeAOT's __Canon generic templates. " + + "Value-type arguments are either rejected or handled by explicit primitive/nullable factories that root the exact instantiation. " + + "Mixed reference/value dictionaries additionally root JavaDictionary<__Canon,T> or JavaDictionary through dedicated type tokens.")] + [UnconditionalSuppressMessage ("Trimming", "IL2055:MakeGenericType", + Justification = "The generic type definitions are known Java collection wrappers, not arbitrary user types. " + + "The constructed wrapper constructors are preserved by the return annotation and by the explicit value-type factory references. " + + "The generic element arguments are not activated by this helper; element peer creation still goes through the normal JavaConvert/trimmable typemap path.")] + [return: DynamicallyAccessedMembers (Constructors)] + internal static Type MakeGenericType ( + [DynamicallyAccessedMembers (Constructors)] + Type genericTypeDefinition, + Type[] arguments) + { + return genericTypeDefinition.MakeGenericType (arguments); + } - public Type[] Arguments { get; } + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "Activator.CreateInstance() targets only collection wrapper types produced by SafeJavaCollectionFactory. " + + "Reference-only wrappers use NativeAOT's canonical generic construction, exact value-type wrappers are rooted by ValueTypeFactory, " + + "and mixed dictionaries root their reference/value canonical shapes with dedicated type tokens.")] + [UnconditionalSuppressMessage ("Trimming", "IL2072:UnrecognizedReflectionPattern", + Justification = "The collection type is annotated with DynamicallyAccessedMembers(Constructors) by GetClosedCollectionType/MakeGenericType. " + + "Only the known JavaList, JavaCollection, and JavaDictionary constructors are invoked here.")] + internal static object CreateInstance ([DynamicallyAccessedMembers (Constructors)] Type collectionType, params object?[] arguments) + { + var instance = Activator.CreateInstance ( + collectionType, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + binder: null, + args: arguments, + culture: CultureInfo.InvariantCulture); + if (instance == null) { + throw new InvalidOperationException ($"Unable to create an instance of collection type '{collectionType}'."); } + return instance; + } - enum JavaCollectionKind { - List, - Collection, - Dictionary, + readonly struct CollectionShape + { + public CollectionShape (JavaCollectionKind kind, Type[] arguments) + { + Kind = kind; + Arguments = arguments; } + public JavaCollectionKind Kind { get; } + + public Type[] Arguments { get; } + } + + enum JavaCollectionKind { + List, + Collection, + Dictionary, } + } diff --git a/src/Mono.Android/Java.Interop/ValueTypeFactory.cs b/src/Mono.Android/Java.Interop/ValueTypeFactory.cs index 3d88b134e3e..54bd03772a5 100644 --- a/src/Mono.Android/Java.Interop/ValueTypeFactory.cs +++ b/src/Mono.Android/Java.Interop/ValueTypeFactory.cs @@ -6,110 +6,110 @@ using Android.Runtime; -namespace Java.Interop { - abstract class ValueTypeFactory +namespace Java.Interop; + +abstract class ValueTypeFactory +{ + // NativeAOT's MakeGenericType() and MakeArrayType() paths use canonical templates. + // Reference types collapse to __Canon, but value types stay value-specific. This map + // intentionally roots each primitive/nullable value shape through direct typeof(T), + // typeof(T[]), new T[length], and Java collection wrapper constructor references. + internal static readonly Dictionary PrimitiveTypeFactories = new () { + { typeof (bool), new ValueTypeFactory () }, + { typeof (sbyte), new ValueTypeFactory () }, + { typeof (char), new ValueTypeFactory () }, + { typeof (short), new ValueTypeFactory () }, + { typeof (int), new ValueTypeFactory () }, + { typeof (long), new ValueTypeFactory () }, + { typeof (float), new ValueTypeFactory () }, + { typeof (double), new ValueTypeFactory () }, + { typeof (bool?), new ValueTypeFactory () }, + { typeof (sbyte?), new ValueTypeFactory () }, + { typeof (char?), new ValueTypeFactory () }, + { typeof (short?), new ValueTypeFactory () }, + { typeof (int?), new ValueTypeFactory () }, + { typeof (long?), new ValueTypeFactory () }, + { typeof (float?), new ValueTypeFactory () }, + { typeof (double?), new ValueTypeFactory () }, + }; + + public abstract Type ValueType { get; } + + public abstract Type ArrayType { get; } + + public abstract Array CreateArray (int length); + + internal abstract IList CreateList (IntPtr handle, JniHandleOwnership transfer); + + internal abstract ICollection CreateCollection (IntPtr handle, JniHandleOwnership transfer); + + internal abstract IDictionary CreateDictionary (ValueTypeFactory valueFactory, IntPtr handle, JniHandleOwnership transfer); + + internal abstract IDictionary CreateDictionaryWithReferenceKey (Type keyType, IntPtr handle, JniHandleOwnership transfer); + + internal abstract IDictionary CreateDictionaryWithReferenceValue (Type valueType, IntPtr handle, JniHandleOwnership transfer); + + internal abstract IDictionary CreateDictionaryWithKey<[DynamicallyAccessedMembers (SafeJavaCollectionFactory.Constructors)] TKey> ( + ValueTypeFactory keyFactory, + IntPtr handle, + JniHandleOwnership transfer); +} + +sealed class ValueTypeFactory<[DynamicallyAccessedMembers (SafeJavaCollectionFactory.Constructors)] T> : ValueTypeFactory +{ + // These tokens root the mixed reference/value dictionary canonical shapes. For example, + // JavaDictionary canonicalizes like JavaDictionary<__Canon,int>, so + // JavaDictionary supplies the NativeAOT template when T is a mapped value type. + static readonly Type ReferenceKeyDictionaryType = typeof (JavaDictionary); + static readonly Type ReferenceValueDictionaryType = typeof (JavaDictionary); + + internal ValueTypeFactory () + { + } + + public override Type ValueType { get; } = typeof (T); + + public override Type ArrayType { get; } = typeof (T[]); + + public override Array CreateArray (int length) + { + return new T [length]; + } + + internal override IList CreateList (IntPtr handle, JniHandleOwnership transfer) + { + return new JavaList (handle, transfer); + } + + internal override ICollection CreateCollection (IntPtr handle, JniHandleOwnership transfer) + { + return new JavaCollection (handle, transfer); + } + + internal override IDictionary CreateDictionary (ValueTypeFactory valueFactory, IntPtr handle, JniHandleOwnership transfer) + { + return valueFactory.CreateDictionaryWithKey (this, handle, transfer); + } + + internal override IDictionary CreateDictionaryWithReferenceKey (Type keyType, IntPtr handle, JniHandleOwnership transfer) + { + _ = ReferenceKeyDictionaryType; + var dictionaryType = SafeJavaCollectionFactory.MakeGenericType (typeof (JavaDictionary<,>), [keyType, typeof (T)]); + return (IDictionary) SafeJavaCollectionFactory.CreateInstance (dictionaryType, handle, transfer); + } + + internal override IDictionary CreateDictionaryWithReferenceValue (Type valueType, IntPtr handle, JniHandleOwnership transfer) { - // NativeAOT's MakeGenericType() and MakeArrayType() paths use canonical templates. - // Reference types collapse to __Canon, but value types stay value-specific. This map - // intentionally roots each primitive/nullable value shape through direct typeof(T), - // typeof(T[]), new T[length], and Java collection wrapper constructor references. - internal static readonly Dictionary PrimitiveTypeFactories = new () { - { typeof (bool), new ValueTypeFactory () }, - { typeof (sbyte), new ValueTypeFactory () }, - { typeof (char), new ValueTypeFactory () }, - { typeof (short), new ValueTypeFactory () }, - { typeof (int), new ValueTypeFactory () }, - { typeof (long), new ValueTypeFactory () }, - { typeof (float), new ValueTypeFactory () }, - { typeof (double), new ValueTypeFactory () }, - { typeof (bool?), new ValueTypeFactory () }, - { typeof (sbyte?), new ValueTypeFactory () }, - { typeof (char?), new ValueTypeFactory () }, - { typeof (short?), new ValueTypeFactory () }, - { typeof (int?), new ValueTypeFactory () }, - { typeof (long?), new ValueTypeFactory () }, - { typeof (float?), new ValueTypeFactory () }, - { typeof (double?), new ValueTypeFactory () }, - }; - - public abstract Type ValueType { get; } - - public abstract Type ArrayType { get; } - - public abstract Array CreateArray (int length); - - internal abstract IList CreateList (IntPtr handle, JniHandleOwnership transfer); - - internal abstract ICollection CreateCollection (IntPtr handle, JniHandleOwnership transfer); - - internal abstract IDictionary CreateDictionary (ValueTypeFactory valueFactory, IntPtr handle, JniHandleOwnership transfer); - - internal abstract IDictionary CreateDictionaryWithReferenceKey (Type keyType, IntPtr handle, JniHandleOwnership transfer); - - internal abstract IDictionary CreateDictionaryWithReferenceValue (Type valueType, IntPtr handle, JniHandleOwnership transfer); - - internal abstract IDictionary CreateDictionaryWithKey<[DynamicallyAccessedMembers (SafeJavaCollectionFactory.Constructors)] TKey> ( - ValueTypeFactory keyFactory, - IntPtr handle, - JniHandleOwnership transfer); + _ = ReferenceValueDictionaryType; + var dictionaryType = SafeJavaCollectionFactory.MakeGenericType (typeof (JavaDictionary<,>), [typeof (T), valueType]); + return (IDictionary) SafeJavaCollectionFactory.CreateInstance (dictionaryType, handle, transfer); } - sealed class ValueTypeFactory<[DynamicallyAccessedMembers (SafeJavaCollectionFactory.Constructors)] T> : ValueTypeFactory + internal override IDictionary CreateDictionaryWithKey<[DynamicallyAccessedMembers (SafeJavaCollectionFactory.Constructors)] TKey> ( + ValueTypeFactory keyFactory, + IntPtr handle, + JniHandleOwnership transfer) { - // These tokens root the mixed reference/value dictionary canonical shapes. For example, - // JavaDictionary canonicalizes like JavaDictionary<__Canon,int>, so - // JavaDictionary supplies the NativeAOT template when T is a mapped value type. - static readonly Type ReferenceKeyDictionaryType = typeof (JavaDictionary); - static readonly Type ReferenceValueDictionaryType = typeof (JavaDictionary); - - internal ValueTypeFactory () - { - } - - public override Type ValueType { get; } = typeof (T); - - public override Type ArrayType { get; } = typeof (T[]); - - public override Array CreateArray (int length) - { - return new T [length]; - } - - internal override IList CreateList (IntPtr handle, JniHandleOwnership transfer) - { - return new JavaList (handle, transfer); - } - - internal override ICollection CreateCollection (IntPtr handle, JniHandleOwnership transfer) - { - return new JavaCollection (handle, transfer); - } - - internal override IDictionary CreateDictionary (ValueTypeFactory valueFactory, IntPtr handle, JniHandleOwnership transfer) - { - return valueFactory.CreateDictionaryWithKey (this, handle, transfer); - } - - internal override IDictionary CreateDictionaryWithReferenceKey (Type keyType, IntPtr handle, JniHandleOwnership transfer) - { - _ = ReferenceKeyDictionaryType; - var dictionaryType = SafeJavaCollectionFactory.MakeGenericType (typeof (JavaDictionary<,>), [keyType, typeof (T)]); - return (IDictionary) SafeJavaCollectionFactory.CreateInstance (dictionaryType, handle, transfer); - } - - internal override IDictionary CreateDictionaryWithReferenceValue (Type valueType, IntPtr handle, JniHandleOwnership transfer) - { - _ = ReferenceValueDictionaryType; - var dictionaryType = SafeJavaCollectionFactory.MakeGenericType (typeof (JavaDictionary<,>), [typeof (T), valueType]); - return (IDictionary) SafeJavaCollectionFactory.CreateInstance (dictionaryType, handle, transfer); - } - - internal override IDictionary CreateDictionaryWithKey<[DynamicallyAccessedMembers (SafeJavaCollectionFactory.Constructors)] TKey> ( - ValueTypeFactory keyFactory, - IntPtr handle, - JniHandleOwnership transfer) - { - return new JavaDictionary (handle, transfer); - } + return new JavaDictionary (handle, transfer); } } From d41d0059f253d1de2e58cec552c379c80d58d2b6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Jul 2026 23:35:00 +0200 Subject: [PATCH 08/14] [TrimmableTypeMap] Fix nullable warnings in SafeJavaCollectionFactory Check the value-type factory locals directly instead of via intermediate bools so the compiler tracks the non-null flow, clearing CS8602/CS8604 in the dictionary path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/SafeJavaCollectionFactory.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs index 75faa734d5e..0034cd1c5c7 100644 --- a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs @@ -109,22 +109,24 @@ static bool TryCreateFromMappedValueTypeFactories ( [NotNullWhen (true)] out object? collection) { if (shape.Kind == JavaCollectionKind.Dictionary) { - var hasKeyFactory = TryGetValueTypeFactory (shape.Arguments [0], out var keyFactory); - var hasValueFactory = TryGetValueTypeFactory (shape.Arguments [1], out var valueFactory); + // Null when the argument is not a supported value type; the direct null checks below let the + // compiler track the non-null flow (an intermediate bool would not, producing CS8602/CS8604). + TryGetValueTypeFactory (shape.Arguments [0], out var keyFactory); + TryGetValueTypeFactory (shape.Arguments [1], out var valueFactory); - if (hasKeyFactory && hasValueFactory) { + if (keyFactory != null && valueFactory != null) { collection = keyFactory.CreateDictionary (valueFactory, handle, transfer); return true; } // Mixed dictionaries are safe only when the other side is a reference type. If it is an // unsupported value type, MakeGenericType() would need an exact unrooted instantiation. - if (hasKeyFactory && !shape.Arguments [1].IsValueType) { + if (keyFactory != null && !shape.Arguments [1].IsValueType) { collection = keyFactory.CreateDictionaryWithReferenceValue (shape.Arguments [1], handle, transfer); return true; } - if (hasValueFactory && !shape.Arguments [0].IsValueType) { + if (valueFactory != null && !shape.Arguments [0].IsValueType) { collection = valueFactory.CreateDictionaryWithReferenceKey (shape.Arguments [0], handle, transfer); return true; } From 187d9faea1d5350c97e4cef5788f19e0893e6cfd Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Jul 2026 00:02:45 +0200 Subject: [PATCH 09/14] [TrimmableTypeMap] Address review: value/value dict test, drop unused API, avoid per-element closure - Add FromJniHandle_IDictionaryInt32Int64 to exercise the value/value dictionary branch (JavaDictionary via the generic-virtual CreateDictionaryWithKey), confirming that value-over-value instantiation is rooted under the trimmable/NativeAOT config. - Remove the unused throwing SafeArrayFactory.GetArrayType(Type,int) overload (only TryGetArrayType/CreateInstance are called). - Inline JavaConvert.WithLocalJniHandle in CopyManagedObjectArray to avoid allocating a capturing closure per array element. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 13 +++++++--- .../Java.Interop/SafeArrayFactory.cs | 9 ------- .../Java.Interop/JavaConvertTest.cs | 24 +++++++++++++++++++ 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 35e8328897f..a9d65cf6c37 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -950,11 +950,18 @@ static Dictionary> CreateCopyManagedToNativeArray () static void CopyManagedObjectArray (Array source, IntPtr dest) { + // Inlined equivalent of JavaConvert.WithLocalJniHandle to avoid allocating a capturing + // closure per element (this runs once per array element for both NewObjectArray and the + // nullable-primitive CopyManagedToNativeArray entries). for (int i = 0; i < source.Length; i++) { - JavaConvert.WithLocalJniHandle (source.GetValue (i), lref => { + object? value = source.GetValue (i); + IntPtr lref = JavaConvert.ToLocalJniHandle (value); + try { SetObjectArrayElement (dest, i, lref); - return IntPtr.Zero; - }); + } finally { + DeleteLocalRef (lref); + GC.KeepAlive (value); + } } } diff --git a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs index f9b32b2f470..b0dcaa77561 100644 --- a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs @@ -17,15 +17,6 @@ static class SafeArrayFactory // vector EEType/template must be available. ValueTypeFactory roots typeof(T[]), new T[length], // and the matching Java collection wrapper instantiations in one shared primitive map. - internal static Type GetArrayType (Type elementType, int rank) - { - if (TryGetArrayType (elementType, rank, out var arrayType)) { - return arrayType; - } - - throw CreateUnsupportedArrayException (elementType, rank); - } - internal static bool TryGetArrayType (Type elementType, int rank, [NotNullWhen (true)] out Type? arrayType) { ValidateRank (rank); diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs index 1140333874d..d73d6bbd53b 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs @@ -171,6 +171,30 @@ public void FromJniHandle_IDictionaryNullableInt32String () } } + // Both type arguments are value types: on the trimmable typemap path this builds + // JavaDictionary through the generic-virtual CreateDictionaryWithKey, + // the most NativeAOT-fragile construct in SafeJavaCollectionFactory (no dedicated rooting + // token). This asserts that exact value/value instantiation is rooted at runtime. + [Test] + public void FromJniHandle_IDictionaryInt32Int64 () + { + using (var source = new JavaDictionary ()) { + source.Add (1, 100L); + source.Add (2, 200L); + + var converted = InvokeJavaConvertFromJniHandle (typeof (IDictionary), source.Handle, JniHandleOwnership.DoNotTransfer); + try { + Assert.AreEqual (typeof (JavaDictionary), converted.GetType ()); + + var dictionary = (IDictionary) converted; + Assert.AreEqual (100L, dictionary [1]); + Assert.AreEqual (200L, dictionary [2]); + } finally { + (converted as IDisposable)?.Dispose (); + } + } + } + static Java.Util.ArrayList CreateList (params int[][] items) { var list = new Java.Util.ArrayList (); From cb99cd2eaa23a39276efd1576729a0df38d7ec41 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 13 Jul 2026 18:44:26 +0200 Subject: [PATCH 10/14] [TrimmableTypeMap] Address review: nameof and redundant MakeGenericType Addresses the two remaining PR review comments: - Use `nameof (elementType)` instead of the string literal in `CreateManagedArrayFromObjectArray`. - Gate the trimmable generic-collection converter on a new `SafeJavaCollectionFactory.IsSupportedCollectionType ()` check that does not resolve (or root) the closed collection type. Previously the gate called `TryGetCollectionType ()` (running `MakeGenericType ()`) purely as a boolean probe, and `TryCreateFromJniHandle ()` then resolved the type again, so `MakeGenericType ()` ran twice per uncached `FromJniHandle ()` marshal for reference generic collections. The type is now resolved at most once (reference collections), or not at all (value collections handled by the rooted `ValueTypeFactory` path). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 155743e0-3f7e-489b-b351-80da73edc7d0 --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 2 +- src/Mono.Android/Java.Interop/JavaConvert.cs | 2 +- .../Java.Interop/SafeJavaCollectionFactory.cs | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index f9208ca7acd..0b21eca0c87 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -1118,7 +1118,7 @@ public static void CopyArray (T[] src, IntPtr dest) static Array CreateManagedArrayFromObjectArray (Type? elementType, IntPtr source, int len) { if (elementType == null) - throw new ArgumentNullException ("elementType"); + throw new ArgumentNullException (nameof (elementType)); var r = ArrayCreateInstance (elementType, len); CopyArray (source, r, elementType); diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index ec977caf04d..ae66b1eb52b 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -76,7 +76,7 @@ static class JavaConvert { if (target.IsGenericType && !target.IsGenericTypeDefinition) { if (RuntimeFeature.TrimmableTypeMap) { - if (SafeJavaCollectionFactory.TryGetCollectionType (target, out _)) { + if (SafeJavaCollectionFactory.IsSupportedCollectionType (target)) { return (h, t) => { if (SafeJavaCollectionFactory.TryCreateFromJniHandle (target, h, t, out var collection)) { return collection; diff --git a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs index 0034cd1c5c7..1987f5eee39 100644 --- a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs @@ -43,6 +43,31 @@ internal static bool TryGetCollectionType (Type targetType, [NotNullWhen (true)] return collectionType != null; } + internal static bool IsSupportedCollectionType (Type targetType) + { + if (targetType == null) + throw new ArgumentNullException (nameof (targetType)); + + if (!TryGetCollectionShape (targetType, out var shape)) { + return false; + } + + // Gate GetJniHandleConverter on support without resolving (and rooting) the closed + // collection type. Unsupported value-type arguments are rejected here, before any + // MakeGenericType() call: those would need an exact unrooted instantiation, whereas + // reference and mapped primitive/nullable arguments are all AOT-safe. TryCreateFromJniHandle + // then resolves the type at most once (reference collections) or not at all (value + // collections, handled by the rooted ValueTypeFactory path), instead of resolving it + // once here for the gate and again per marshal. + foreach (var argument in shape.Arguments) { + if (argument.IsValueType && !ValueTypeFactory.PrimitiveTypeFactories.ContainsKey (argument)) { + return false; + } + } + + return true; + } + internal static bool TryCreateFromJniHandle ( Type targetType, IntPtr handle, From e01bc8c64811e09b7328bdd9e81e18bb45579782 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 13 Jul 2026 19:25:11 +0200 Subject: [PATCH 11/14] [TrimmableTypeMap] Fix NativeAOT array-types fallback to return full set The NativeAOT Java-to-managed array reverse lookup (TrimmableTypeMapTypeManager.GetArrayTypes) fell back to returning only the `T[]` vector when no generated array proxy was present. That drops the JavaObjectArray/JavaArray (reference) and JavaArray/JavaPrimitiveArray/ concrete-primitive (e.g. JavaSByteArray) wrapper types that both the generated proxy and the CoreCLR path return, so marshaling a Java array to those wrapper types would fail to resolve once array proxies are no longer pre-generated. Replace the fallback with BuildRuntimeArrayTypes, which reproduces the generated JavaArrayProxy.GetArrayTypes() contract (see the generator's ModelBuilder.GetArrayTypeReferences) at runtime and AOT-safely: - Primitive leaves use PrimitiveArrayInfo's closed `typeof` tokens. - Reference leaves build [JavaObjectArray, JavaArray, T[]] via canonical (__Canon) MakeGenericType/MakeArrayType, the same rooting the trimmable typemap already relies on for reference generics. - Jagged (rank > 1) mirrors ExpandRankOneTypes. Behavioral note: non-primitive value-type leaves (e.g. Nullable) return only the exact rooted vector (int?[]); JavaObjectArray would require an unrooted value-type generic instantiation and the generator never emitted one. Why no test caught this: the existing TryGetArrayProxy_* tests only assert the *proxy* path (gated on generated proxies), and the marshaling round-trip tests only need `T[]`, so the no-proxy runtime fallback was never asserted. BuildRuntimeArrayTypes is a pure function, so the added TrimmableTypeMapTypeManagerTests exercise it directly on every config. Also inline JavaConvert.WithLocalJniHandle in JNIEnv.SetObjectArrayElementFromManagedValue to avoid allocating a capturing closure per element on the SetArrayItem path (matching CopyManagedObjectArray). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 155743e0-3f7e-489b-b351-80da73edc7d0 --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 11 ++- .../Java.Interop/SafeArrayFactory.cs | 15 +++- .../TrimmableTypeMapTypeManager.cs | 77 +++++++++++++++++-- .../TrimmableTypeMapTypeManagerTests.cs | 47 +++++++++++ 4 files changed, 140 insertions(+), 10 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 0b21eca0c87..40a60176f3e 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -1557,10 +1557,15 @@ static IntPtr NewArray (Array value, Type elementType, IntPtr elementClass) static void SetObjectArrayElementFromManagedValue (IntPtr dest, int index, object? value) { - JavaConvert.WithLocalJniHandle (value, lref => { + // Inlined equivalent of JavaConvert.WithLocalJniHandle to avoid allocating a capturing + // closure on every element set (this runs once per element on the SetArrayItem path). + IntPtr lref = JavaConvert.ToLocalJniHandle (value); + try { SetObjectArrayElement (dest, index, lref); - return IntPtr.Zero; - }); + } finally { + DeleteLocalRef (lref); + GC.KeepAlive (value); + } } static unsafe void _SetBooleanArrayRegion (IntPtr array, int start, int length, bool[] buffer) diff --git a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs index b0dcaa77561..00264e118c4 100644 --- a/src/Mono.Android/Java.Interop/SafeArrayFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs @@ -105,11 +105,24 @@ static bool TryGetVectorType (Type elementType, [NotNullWhen (true)] out Type? v [UnconditionalSuppressMessage ("Trimming", "IL2055:MakeGenericType", Justification = "Creating an SZArray type does not perform member discovery. " + "Value-type vectors are not passed here unless they are already array reference types for an outer jagged rank.")] - static Type MakeReferenceArrayType (Type elementType) + internal static Type MakeReferenceArrayType (Type elementType) { return elementType.MakeArrayType (); } + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "NativeAOT's Type.MakeGenericType() is annotated because arbitrary constructed generics can lack a runtime template. " + + "This helper only constructs Java.Interop array wrapper types (JavaObjectArray, JavaArray) over reference arguments, " + + "which canonicalize to __Canon templates in NativeAOT like every other reference generic in the trimmable typemap. " + + "The result is only used as a Java-to-managed marshaling lookup token; the constructed type is not activated here.")] + [UnconditionalSuppressMessage ("Trimming", "IL2055:MakeGenericType", + Justification = "The generic definitions are the known Java.Interop array wrappers, not arbitrary user types, and the type argument " + + "is always a reference type. No members are discovered by constructing the type token.")] + internal static Type MakeReferenceGenericType (Type genericDefinition, Type referenceArgument) + { + return genericDefinition.MakeGenericType (referenceArgument); + } + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", Justification = "Array.CreateInstanceFromArrayType() immediately asks for the array TypeHandle and allocates via RuntimeAugments.NewArray(). " + "SafeArrayFactory only passes NativeAOT-buildable reference-array canonical shapes here: either arrays whose original element type is a reference type, " + diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 5b24e97e3ea..05f0500a9c6 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -56,16 +56,14 @@ static IEnumerable GetArrayTypes (JniTypeSignature typeSignature, Type ele return GetArrayTypesForCoreClr (typeSignature, elementType); } - // NativeAOT: prefer the generated array proxy, otherwise construct the array type at runtime. + // NativeAOT: prefer the generated array proxy; otherwise reconstruct the same set of + // array and wrapper types at runtime so Java-to-managed array marshaling still resolves + // them when array proxies are not pre-generated. if (TrimmableTypeMap.Instance.TryGetArrayProxy (elementType, typeSignature.ArrayRank, out var arrayProxy)) { return arrayProxy.GetArrayTypes (); } - if (SafeArrayFactory.TryGetArrayType (elementType, typeSignature.ArrayRank, out var arrayType)) { - return [arrayType]; - } - - return []; + return BuildRuntimeArrayTypes (elementType, typeSignature.ArrayRank); [UnconditionalSuppressMessage ("Trimming", "IL2026:RequiresUnreferencedCode", Justification = "This API is called as part of Java to .NET type marshalling when the target type is expected as the input " + @@ -131,6 +129,73 @@ static IEnumerable MakeArrayTypes (Type elementType, int rank) } } + // Reconstructs the same array and wrapper types that a generated JavaArrayProxy.GetArrayTypes () + // would return (see the trimmable typemap generator's ModelBuilder.GetArrayTypeReferences), for the + // NativeAOT path when no array proxy was pre-generated. All construction is AOT-safe: primitive + // wrappers are closed typeof tokens from PrimitiveArrayInfo, and every runtime-built type is a + // reference array or a Java.Interop array wrapper over a reference argument, which NativeAOT builds + // from canonical (__Canon) templates. + internal static IReadOnlyList BuildRuntimeArrayTypes (Type elementType, int rank) + { + Debug.Assert (rank > 0, "At least one array rank is expected"); + + if (PrimitiveArrayInfo.TryGetArrayTypes (elementType, out var primitiveRankOneTypes)) { + return ExpandRankOneTypes (primitiveRankOneTypes, rank); + } + + if (elementType.IsValueType) { + // Non-primitive value types (e.g. Nullable) have no generated array proxy and no + // Java.Interop array wrapper: JavaObjectArray/JavaArray over a value type would need + // an unrooted value-type generic instantiation. Only the exact rooted vector is AOT-safe, + // which is all these element types need to marshal. + if (SafeArrayFactory.TryGetArrayType (elementType, rank, out var valueArrayType)) { + return [valueArrayType]; + } + return []; + } + + Type[] referenceRankOneTypes = [ + SafeArrayFactory.MakeReferenceGenericType (typeof (Java.Interop.JavaObjectArray<>), elementType), + SafeArrayFactory.MakeReferenceGenericType (typeof (Java.Interop.JavaArray<>), elementType), + SafeArrayFactory.MakeReferenceArrayType (elementType), + ]; + return ExpandRankOneTypes (referenceRankOneTypes, rank); + + // Mirrors ModelBuilder.ExpandRankOneTypes: for jagged arrays each rank-one type is both wrapped + // in JavaObjectArray<> and given the remaining SZArray ranks. Every input here is a reference type. + static IReadOnlyList ExpandRankOneTypes (IReadOnlyList rankOneTypes, int rank) + { + if (rank == 1) { + return rankOneTypes; + } + + var result = new List (rankOneTypes.Count * 2); + foreach (var type in rankOneTypes) { + result.Add (NestInJavaObjectArray (type, rank - 1)); + result.Add (AddArrayRanks (type, rank - 1)); + } + return result; + } + + static Type NestInJavaObjectArray (Type type, int rank) + { + var result = type; + for (int i = 0; i < rank; i++) { + result = SafeArrayFactory.MakeReferenceGenericType (typeof (Java.Interop.JavaObjectArray<>), result); + } + return result; + } + + static Type AddArrayRanks (Type type, int rank) + { + var result = type; + for (int i = 0; i < rank; i++) { + result = SafeArrayFactory.MakeReferenceArrayType (result); + } + return result; + } + } + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { var builtInType = GetBuiltInTypeForSimpleReference (jniSimpleReference); diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index 8b4abf0d84f..c5aada6a397 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs @@ -290,6 +290,53 @@ public void TryGetArrayProxy_PrimitiveLeaf_ReturnsAllRankTypes () CollectionAssert.Contains (jaggedSbyteArrayProxy.GetArrayTypes (), typeof (JavaObjectArray)); } + // Regression: when no array proxy is pre-generated, the runtime fallback (BuildRuntimeArrayTypes) + // must still return the full set of array and wrapper types — the same contract as the generated + // proxy — not just T[]. Previously it returned only [T[]], and nothing caught it: the + // TryGetArrayProxy_* tests above only cover the proxy path (gated on generated proxies), and the + // marshaling round-trip tests only need T[]. This is a pure function, so it runs on every config. + [Test] + public void BuildRuntimeArrayTypes_ReferenceLeaf_ReturnsProxyContract () + { + var rank1 = TrimmableTypeMapTypeManager.BuildRuntimeArrayTypes (typeof (Java.Lang.Object), rank: 1); + CollectionAssert.AreEquivalent ( + new [] { + typeof (JavaObjectArray), + typeof (Java.Interop.JavaArray), + typeof (Java.Lang.Object[]), + }, + rank1); + + var rank2 = TrimmableTypeMapTypeManager.BuildRuntimeArrayTypes (typeof (Java.Lang.Object), rank: 2); + CollectionAssert.Contains (rank2, typeof (JavaObjectArray>)); + CollectionAssert.Contains (rank2, typeof (Java.Interop.JavaArray[])); + CollectionAssert.Contains (rank2, typeof (Java.Lang.Object[][])); + } + + [Test] + public void BuildRuntimeArrayTypes_PrimitiveLeaf_ReturnsProxyContract () + { + var rank1 = TrimmableTypeMapTypeManager.BuildRuntimeArrayTypes (typeof (sbyte), rank: 1); + CollectionAssert.Contains (rank1, typeof (sbyte[])); + CollectionAssert.Contains (rank1, typeof (Java.Interop.JavaArray)); + CollectionAssert.Contains (rank1, typeof (JavaPrimitiveArray)); + CollectionAssert.Contains (rank1, typeof (JavaSByteArray)); + + var rank2 = TrimmableTypeMapTypeManager.BuildRuntimeArrayTypes (typeof (sbyte), rank: 2); + CollectionAssert.Contains (rank2, typeof (sbyte[][])); + CollectionAssert.Contains (rank2, typeof (JavaObjectArray>)); + CollectionAssert.Contains (rank2, typeof (JavaObjectArray)); + } + + [Test] + public void BuildRuntimeArrayTypes_NullablePrimitiveLeaf_ReturnsVector () + { + // Nullable primitives have no array proxy and no AOT-safe Java.Interop array wrapper, so the + // fallback returns just the exact rooted vector (int?[]) rather than an unrooted value generic. + var rank1 = TrimmableTypeMapTypeManager.BuildRuntimeArrayTypes (typeof (int?), rank: 1); + CollectionAssert.Contains (rank1, typeof (int?[])); + } + static ConcurrentDictionary GetProxyCache (TrimmableTypeMap instance) { var field = typeof (TrimmableTypeMap).GetField ("_proxyCache", BindingFlags.Instance | BindingFlags.NonPublic); From 6eabdc96c19c619b6fe8d7c4747c0c60595271cc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 13 Jul 2026 20:31:13 +0200 Subject: [PATCH 12/14] [TrimmableTypeMap] Restore byte collection support on trimmable path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review: the removed ScalarContainerFactories supported byte-element collections (IList, IDictionary), but ValueTypeFactory only mapped sbyte, so IsSupportedCollectionType returned false for byte and the trimmable GetJniHandleConverter fell through to a silent null — a regression versus the reflection paths (which still handle byte via dynamic MakeGenericType). Add byte and byte? to ValueTypeFactory.PrimitiveTypeFactories. byte marshals to java.lang.Byte bitwise, identical to sbyte, and JavaConvert already has the byte scalar converters, so JavaList/JavaDictionary round-trip correctly (including values > 127). byte[] arrays are unaffected: they are handled by JNIEnv's dedicated primitive-array path (new byte[len]), which never reaches SafeArrayFactory. Add a FromJniHandle_IListByte round-trip test (with an unsigned-range value). Also document, in ValueTypeFactory.CreateDictionaryWithKey, why value/value dictionaries need no dedicated rooting token: every ValueTypeFactory is statically constructed, CreateDictionary is virtually reachable for all of them (instantiating the GVM CreateDictionaryWithKey for every key X), so NativeAOT's generic-virtual-method dependency analysis roots the full JavaDictionary cross-product over the fixed primitive/nullable set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 155743e0-3f7e-489b-b351-80da73edc7d0 --- .../Java.Interop/ValueTypeFactory.cs | 11 +++++++++ .../Java.Interop/JavaConvertTest.cs | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Mono.Android/Java.Interop/ValueTypeFactory.cs b/src/Mono.Android/Java.Interop/ValueTypeFactory.cs index 54bd03772a5..a2543d301d1 100644 --- a/src/Mono.Android/Java.Interop/ValueTypeFactory.cs +++ b/src/Mono.Android/Java.Interop/ValueTypeFactory.cs @@ -14,8 +14,11 @@ abstract class ValueTypeFactory // Reference types collapse to __Canon, but value types stay value-specific. This map // intentionally roots each primitive/nullable value shape through direct typeof(T), // typeof(T[]), new T[length], and Java collection wrapper constructor references. + // `byte` is included alongside `sbyte` (both marshal to java.lang.Byte bitwise) so that + // byte-element collections keep working on the trimmable path, matching the reflection paths. internal static readonly Dictionary PrimitiveTypeFactories = new () { { typeof (bool), new ValueTypeFactory () }, + { typeof (byte), new ValueTypeFactory () }, { typeof (sbyte), new ValueTypeFactory () }, { typeof (char), new ValueTypeFactory () }, { typeof (short), new ValueTypeFactory () }, @@ -24,6 +27,7 @@ abstract class ValueTypeFactory { typeof (float), new ValueTypeFactory () }, { typeof (double), new ValueTypeFactory () }, { typeof (bool?), new ValueTypeFactory () }, + { typeof (byte?), new ValueTypeFactory () }, { typeof (sbyte?), new ValueTypeFactory () }, { typeof (char?), new ValueTypeFactory () }, { typeof (short?), new ValueTypeFactory () }, @@ -110,6 +114,13 @@ internal override IDictionary CreateDictionaryWithReferenceValue (Type valueType IntPtr handle, JniHandleOwnership transfer) { + // Value/value dictionaries need no dedicated rooting token (unlike the mixed reference/value + // cases): the full JavaDictionary cross-product is statically rooted by NativeAOT. + // Every ValueTypeFactory is constructed in PrimitiveTypeFactories, CreateDictionary is a + // virtual call reachable for all of them (so CreateDictionaryWithKey is instantiated for + // every key type X), and this override is a generic virtual method — so NativeAOT's GVM + // dependency analysis emits ValueTypeFactory.CreateDictionaryWithKey (hence + // new JavaDictionary()) for every (X, Y) pair in the fixed primitive/nullable set. return new JavaDictionary (handle, transfer); } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs index d73d6bbd53b..91f347d54d7 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs @@ -195,6 +195,30 @@ public void FromJniHandle_IDictionaryInt32Int64 () } } + // Regression: byte-element collections must stay supported on the trimmable typemap path + // (ValueTypeFactory maps byte alongside sbyte). byte marshals to java.lang.Byte bitwise, so + // values above 127 round-trip through the signed Java byte. + [Test] + public void FromJniHandle_IListByte () + { + using (var source = new JavaList ()) { + source.Add ((byte) 1); + source.Add ((byte) 200); + + var converted = InvokeJavaConvertFromJniHandle (typeof (IList), source.Handle, JniHandleOwnership.DoNotTransfer); + try { + Assert.AreEqual (typeof (JavaList), converted.GetType ()); + + var list = (IList) converted; + Assert.AreEqual (2, list.Count); + Assert.AreEqual ((byte) 1, list [0]); + Assert.AreEqual ((byte) 200, list [1]); + } finally { + (converted as IDisposable)?.Dispose (); + } + } + } + static Java.Util.ArrayList CreateList (params int[][] items) { var list = new Java.Util.ArrayList (); From 2a9cba7482980a1f84f42625ccff6d702eedeada Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 14 Jul 2026 14:26:26 +0200 Subject: [PATCH 13/14] [TrimmableTypeMap] Address collection factory review Add complete nullable byte array marshaling, parse generic collection shapes once per converter, and add focused NativeAOT trimmable coverage for value/value dictionary rooting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98b572c2-766d-43a8-8f1f-3c39df991541 --- .../yaml-templates/stage-package-tests.yaml | 9 ++ src/Mono.Android/Android.Runtime/JNIEnv.cs | 6 ++ src/Mono.Android/Java.Interop/JavaConvert.cs | 10 +-- .../Java.Interop/SafeJavaCollectionFactory.cs | 89 +++++++------------ .../Android.Runtime/JnienvArrayMarshaling.cs | 20 +++++ .../Java.Interop/JavaConvertTest.cs | 16 ++-- 6 files changed, 77 insertions(+), 73 deletions(-) diff --git a/build-tools/automation/yaml-templates/stage-package-tests.yaml b/build-tools/automation/yaml-templates/stage-package-tests.yaml index d4a684b183c..4bb682e74ce 100644 --- a/build-tools/automation/yaml-templates/stage-package-tests.yaml +++ b/build-tools/automation/yaml-templates/stage-package-tests.yaml @@ -178,6 +178,15 @@ stages: artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Mono.Android.NET_Tests-Signed.aab artifactFolder: $(DotNetTargetFramework)-NativeAOT + - template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml + parameters: + configuration: $(XA.Build.Configuration) + testName: Mono.Android.NET_Tests-NativeAOTTrimmable + project: tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj + extraBuildArgs: -p:TestsFlavor=NativeAOTTrimmable -p:PublishAot=true -p:_AndroidTypeMapImplementation=trimmable -p:IncludeCategories=NativeAOTTrimmable + artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Mono.Android.NET_Tests-Signed.aab + artifactFolder: $(DotNetTargetFramework)-NativeAOTTrimmable + - template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml parameters: configuration: $(XA.Build.Configuration) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 40a60176f3e..39041b470d8 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -599,6 +599,7 @@ static IntPtr FindArrayClassByElementType (Type elementType) static string? GetBoxedPrimitiveJniClassName (Type type) { if (type == typeof (bool?)) return "java/lang/Boolean"; + if (type == typeof (byte?)) return "java/lang/Byte"; if (type == typeof (sbyte?)) return "java/lang/Byte"; if (type == typeof (char?)) return "java/lang/Character"; if (type == typeof (short?)) return "java/lang/Short"; @@ -684,6 +685,7 @@ public static void CopyArray (IntPtr src, string[] dest) return r [0]; } }, { typeof (bool?), (type, source, index) => GetNullableArrayElement (source, index, typeof (bool?)) }, + { typeof (byte?), (type, source, index) => GetNullableArrayElement (source, index, typeof (byte?)) }, { typeof (sbyte?), (type, source, index) => GetNullableArrayElement (source, index, typeof (sbyte?)) }, { typeof (char?), (type, source, index) => GetNullableArrayElement (source, index, typeof (char?)) }, { typeof (short?), (type, source, index) => GetNullableArrayElement (source, index, typeof (short?)) }, @@ -927,6 +929,7 @@ static Dictionary> CreateCopyManagedToNativeArray () { typeof (float), (source, dest) => CopyArray ((float[]) source, dest) }, { typeof (double), (source, dest) => CopyArray ((double[]) source, dest) }, { typeof (bool?), (source, dest) => CopyManagedObjectArray (source, dest) }, + { typeof (byte?), (source, dest) => CopyManagedObjectArray (source, dest) }, { typeof (sbyte?), (source, dest) => CopyManagedObjectArray (source, dest) }, { typeof (char?), (source, dest) => CopyManagedObjectArray (source, dest) }, { typeof (short?), (source, dest) => CopyManagedObjectArray (source, dest) }, @@ -1084,6 +1087,7 @@ public static void CopyArray (T[] src, IntPtr dest) return r; } }, { typeof (bool?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, + { typeof (byte?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, { typeof (sbyte?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, { typeof (char?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, { typeof (short?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) }, @@ -1378,6 +1382,7 @@ static Dictionary> CreateCreateManagedToNativeArray () { typeof (float), (source) => NewArray ((float[]) source) }, { typeof (double), (source) => NewArray ((double[]) source) }, { typeof (bool?), (source) => NewObjectArray (source, typeof (bool?)) }, + { typeof (byte?), (source) => NewObjectArray (source, typeof (byte?)) }, { typeof (sbyte?), (source) => NewObjectArray (source, typeof (sbyte?)) }, { typeof (char?), (source) => NewObjectArray (source, typeof (char?)) }, { typeof (short?), (source) => NewObjectArray (source, typeof (short?)) }, @@ -1529,6 +1534,7 @@ static IntPtr NewArray (Array value, Type elementType, IntPtr elementClass) _SetDoubleArrayRegion (dest, index, _value.Length, _value); } }, { typeof (bool?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, + { typeof (byte?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, { typeof (sbyte?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, { typeof (char?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, { typeof (short?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) }, diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index ae66b1eb52b..c02a02e00fe 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -76,14 +76,8 @@ static class JavaConvert { if (target.IsGenericType && !target.IsGenericTypeDefinition) { if (RuntimeFeature.TrimmableTypeMap) { - if (SafeJavaCollectionFactory.IsSupportedCollectionType (target)) { - return (h, t) => { - if (SafeJavaCollectionFactory.TryCreateFromJniHandle (target, h, t, out var collection)) { - return collection; - } - return null; - }; - } + if (SafeJavaCollectionFactory.TryGetFromJniHandleConverter (target, out var collectionConverter)) + return collectionConverter; } else if (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) { var factoryConverter = TryMakeGenericCollectionTypeFactory (target); if (factoryConverter != null) diff --git a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs index 1987f5eee39..b26bbc2b5f1 100644 --- a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs @@ -24,41 +24,34 @@ static class SafeJavaCollectionFactory // direct generic type references and constructors instead of asking MakeGenericType() to invent them. // The shared ValueTypeFactory map also roots the exact array vectors for these same value types. - internal static bool TryGetCollectionType (Type targetType, [NotNullWhen (true)] out Type? collectionType) + internal static bool TryGetFromJniHandleConverter ( + Type targetType, + [NotNullWhen (true)] out Func? converter) { if (targetType == null) throw new ArgumentNullException (nameof (targetType)); if (!TryGetCollectionShape (targetType, out var shape)) { - collectionType = null; + converter = null; return false; } - collectionType = shape.Kind switch { - JavaCollectionKind.List => GetClosedCollectionType (typeof (JavaList<>), shape.Arguments), - JavaCollectionKind.Collection => GetClosedCollectionType (typeof (JavaCollection<>), shape.Arguments), - JavaCollectionKind.Dictionary => GetClosedCollectionType (typeof (JavaDictionary<,>), shape.Arguments), - _ => null, - }; - return collectionType != null; - } - - internal static bool IsSupportedCollectionType (Type targetType) - { - if (targetType == null) - throw new ArgumentNullException (nameof (targetType)); - - if (!TryGetCollectionShape (targetType, out var shape)) { + if (!IsSupportedCollectionShape (shape)) { + converter = null; return false; } - // Gate GetJniHandleConverter on support without resolving (and rooting) the closed - // collection type. Unsupported value-type arguments are rejected here, before any - // MakeGenericType() call: those would need an exact unrooted instantiation, whereas - // reference and mapped primitive/nullable arguments are all AOT-safe. TryCreateFromJniHandle - // then resolves the type at most once (reference collections) or not at all (value - // collections, handled by the rooted ValueTypeFactory path), instead of resolving it - // once here for the gate and again per marshal. + // Capture the parsed shape so GetGenericArguments() runs once when the converter is + // selected, rather than once for the support gate and again for every conversion. + converter = (handle, transfer) => CreateFromJniHandle (shape, handle, transfer); + return true; + } + + static bool IsSupportedCollectionShape (CollectionShape shape) + { + // Unsupported value-type arguments are rejected before any MakeGenericType() call: + // those would need an exact unrooted instantiation, whereas reference and mapped + // primitive/nullable arguments are all AOT-safe. foreach (var argument in shape.Arguments) { if (argument.IsValueType && !ValueTypeFactory.PrimitiveTypeFactories.ContainsKey (argument)) { return false; @@ -68,36 +61,20 @@ internal static bool IsSupportedCollectionType (Type targetType) return true; } - internal static bool TryCreateFromJniHandle ( - Type targetType, + static object? CreateFromJniHandle ( + CollectionShape shape, IntPtr handle, - JniHandleOwnership transfer, - out object? collection) + JniHandleOwnership transfer) { - if (targetType == null) - throw new ArgumentNullException (nameof (targetType)); - if (handle == IntPtr.Zero) { - collection = null; - return true; - } - - if (!TryGetCollectionShape (targetType, out var shape)) { - collection = null; - return false; + return null; } - if (TryCreateFromMappedValueTypeFactories (shape, handle, transfer, out collection)) { - return true; - } - - if (!TryGetCollectionType (targetType, out var collectionType)) { - collection = null; - return false; + if (TryCreateFromMappedValueTypeFactories (shape, handle, transfer, out var collection)) { + return collection; } - collection = CreateInstance (collectionType, handle, transfer); - return true; + return CreateInstance (GetClosedCollectionType (shape), handle, transfer); } static bool TryGetCollectionShape (Type targetType, out CollectionShape shape) @@ -182,18 +159,14 @@ static bool TryGetValueTypeFactory (Type type, [NotNullWhen (true)] out ValueTyp } [return: DynamicallyAccessedMembers (Constructors)] - static Type? GetClosedCollectionType ( - [DynamicallyAccessedMembers (Constructors)] - Type genericTypeDefinition, - Type[] arguments) + static Type GetClosedCollectionType (CollectionShape shape) { - foreach (var argument in arguments) { - if (argument.IsValueType && !ValueTypeFactory.PrimitiveTypeFactories.ContainsKey (argument)) { - return null; - } - } - - return MakeGenericType (genericTypeDefinition, arguments); + return shape.Kind switch { + JavaCollectionKind.List => MakeGenericType (typeof (JavaList<>), shape.Arguments), + JavaCollectionKind.Collection => MakeGenericType (typeof (JavaCollection<>), shape.Arguments), + JavaCollectionKind.Dictionary => MakeGenericType (typeof (JavaDictionary<,>), shape.Arguments), + _ => throw new InvalidOperationException ($"Unsupported Java collection kind '{shape.Kind}'."), + }; } [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs index bfd8d866a7d..b41e8a24e02 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs @@ -222,6 +222,26 @@ public void GetArray_NullableInt32 () } } + [Test] + public void GetArray_NullableByte () + { + var values = new byte? [] { 1, null, 200 }; + using (var array = new Java.Lang.Object (JNIEnv.NewArray (values), JniHandleOwnership.TransferLocalRef)) { + Assert.AreEqual ("[Ljava/lang/Byte;", JNIEnv.GetClassNameFromInstance (array.Handle)); + + var copy = JNIEnv.GetArray (array.Handle); + AssertArrays ("GetArray", copy, values); + + Assert.IsNull (JNIEnv.GetArrayItem (array.Handle, 1)); + JNIEnv.SetArrayItem (array.Handle, 1, 255); + Assert.AreEqual ((byte?) 255, JNIEnv.GetArrayItem (array.Handle, 1)); + + var replacement = new byte? [] { 128, 129, null }; + JNIEnv.CopyArray (replacement, array.Handle); + AssertArrays ("CopyArray", JNIEnv.GetArray (array.Handle), replacement); + } + } + [Test] public void GetArray_JavaLangStringArrayToJavaLangObjectArray () { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs index 91f347d54d7..f62b3c54fdd 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs @@ -171,21 +171,23 @@ public void FromJniHandle_IDictionaryNullableInt32String () } } - // Both type arguments are value types: on the trimmable typemap path this builds - // JavaDictionary through the generic-virtual CreateDictionaryWithKey, - // the most NativeAOT-fragile construct in SafeJavaCollectionFactory (no dedicated rooting - // token). This asserts that exact value/value instantiation is rooted at runtime. + // The non-generic source and assertions intentionally avoid referencing + // JavaDictionary, so NativeAOT must root that exact wrapper through + // ValueTypeFactory.CreateDictionaryWithKey's generic-virtual dispatch. [Test] + [Category ("NativeAOTTrimmable")] public void FromJniHandle_IDictionaryInt32Int64 () { - using (var source = new JavaDictionary ()) { + if (!Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { + Assert.Ignore ("This test validates value/value dictionary rooting on the trimmable typemap path."); + } + + using (var source = new JavaDictionary ()) { source.Add (1, 100L); source.Add (2, 200L); var converted = InvokeJavaConvertFromJniHandle (typeof (IDictionary), source.Handle, JniHandleOwnership.DoNotTransfer); try { - Assert.AreEqual (typeof (JavaDictionary), converted.GetType ()); - var dictionary = (IDictionary) converted; Assert.AreEqual (100L, dictionary [1]); Assert.AreEqual (200L, dictionary [2]); From 0ce5dd16a9e90423eac1fd7d291df41048414df9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 14 Jul 2026 14:56:39 +0200 Subject: [PATCH 14/14] [TrimmableTypeMap] Strengthen factory regression coverage Assert complete runtime array type contracts and exercise collection conversion through JniValueManager without reflection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98b572c2-766d-43a8-8f1f-3c39df991541 --- .../Java.Interop/JavaConvertTest.cs | 79 +++++++++---------- .../TrimmableTypeMapTypeManagerTests.cs | 45 ++++++++--- 2 files changed, 72 insertions(+), 52 deletions(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs index f62b3c54fdd..69a7e0f0858 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using Android.App; using Android.Content; using Android.Runtime; +using Java.Interop; + using NUnit.Framework; using Xamarin.Android.RuntimeTests; @@ -131,20 +132,24 @@ public void MarshalInt23Array () [Test] public void FromJniHandle_IListNullableInt32 () { - using (var source = new JavaList ()) { + // A non-generic source prevents ValueManager from returning the cached peer directly, + // forcing conversion through SafeJavaCollectionFactory. + using (var source = new JavaList ()) { source.Add (1); source.Add (null); source.Add (3); - var converted = InvokeJavaConvertFromJniHandle (typeof (IList), source.Handle, JniHandleOwnership.DoNotTransfer); + var reference = source.PeerReference; + var converted = JniEnvironment.Runtime.ValueManager.GetValue> ( + ref reference, + JniObjectReferenceOptions.Copy); try { Assert.AreEqual (typeof (JavaList), converted.GetType ()); - var list = (IList) converted; - Assert.AreEqual (3, list.Count); - Assert.AreEqual ((int?) 1, list [0]); - Assert.IsNull (list [1]); - Assert.AreEqual ((int?) 3, list [2]); + Assert.AreEqual (3, converted.Count); + Assert.AreEqual ((int?) 1, converted [0]); + Assert.IsNull (converted [1]); + Assert.AreEqual ((int?) 3, converted [2]); } finally { (converted as IDisposable)?.Dispose (); } @@ -154,17 +159,21 @@ public void FromJniHandle_IListNullableInt32 () [Test] public void FromJniHandle_IDictionaryNullableInt32String () { - using (var source = new JavaDictionary ()) { + // A non-generic source prevents ValueManager from returning the cached peer directly, + // forcing conversion through SafeJavaCollectionFactory. + using (var source = new JavaDictionary ()) { source.Add (1, "one"); source.Add (null, "null"); - var converted = InvokeJavaConvertFromJniHandle (typeof (IDictionary), source.Handle, JniHandleOwnership.DoNotTransfer); + var reference = source.PeerReference; + var converted = JniEnvironment.Runtime.ValueManager.GetValue> ( + ref reference, + JniObjectReferenceOptions.Copy); try { Assert.AreEqual (typeof (JavaDictionary), converted.GetType ()); - var dictionary = (IDictionary) converted; - Assert.AreEqual ("one", dictionary [1]); - Assert.AreEqual ("null", dictionary [null]); + Assert.AreEqual ("one", converted [1]); + Assert.AreEqual ("null", converted [null]); } finally { (converted as IDisposable)?.Dispose (); } @@ -186,11 +195,13 @@ public void FromJniHandle_IDictionaryInt32Int64 () source.Add (1, 100L); source.Add (2, 200L); - var converted = InvokeJavaConvertFromJniHandle (typeof (IDictionary), source.Handle, JniHandleOwnership.DoNotTransfer); + var reference = source.PeerReference; + var converted = JniEnvironment.Runtime.ValueManager.GetValue> ( + ref reference, + JniObjectReferenceOptions.Copy); try { - var dictionary = (IDictionary) converted; - Assert.AreEqual (100L, dictionary [1]); - Assert.AreEqual (200L, dictionary [2]); + Assert.AreEqual (100L, converted [1]); + Assert.AreEqual (200L, converted [2]); } finally { (converted as IDisposable)?.Dispose (); } @@ -203,18 +214,22 @@ public void FromJniHandle_IDictionaryInt32Int64 () [Test] public void FromJniHandle_IListByte () { - using (var source = new JavaList ()) { + // A non-generic source prevents ValueManager from returning the cached peer directly, + // forcing conversion through SafeJavaCollectionFactory. + using (var source = new JavaList ()) { source.Add ((byte) 1); source.Add ((byte) 200); - var converted = InvokeJavaConvertFromJniHandle (typeof (IList), source.Handle, JniHandleOwnership.DoNotTransfer); + var reference = source.PeerReference; + var converted = JniEnvironment.Runtime.ValueManager.GetValue> ( + ref reference, + JniObjectReferenceOptions.Copy); try { Assert.AreEqual (typeof (JavaList), converted.GetType ()); - var list = (IList) converted; - Assert.AreEqual (2, list.Count); - Assert.AreEqual ((byte) 1, list [0]); - Assert.AreEqual ((byte) 200, list [1]); + Assert.AreEqual (2, converted.Count); + Assert.AreEqual ((byte) 1, converted [0]); + Assert.AreEqual ((byte) 200, converted [1]); } finally { (converted as IDisposable)?.Dispose (); } @@ -232,23 +247,5 @@ static Java.Util.ArrayList CreateList (params int[][] items) } return list; } - - static object InvokeJavaConvertFromJniHandle (Type targetType, IntPtr handle, JniHandleOwnership transfer) - { - var javaConvert = typeof (Java.Lang.Object).Assembly.GetType ("Java.Interop.JavaConvert"); - Assert.IsNotNull (javaConvert); - - var method = javaConvert.GetMethod ( - "FromJniHandle", - BindingFlags.Public | BindingFlags.Static, - binder: null, - types: new [] { typeof (IntPtr), typeof (JniHandleOwnership), typeof (Type) }, - modifiers: null); - Assert.IsNotNull (method); - - var value = method.Invoke (null, new object [] { handle, transfer, targetType }); - Assert.IsNotNull (value); - return value; - } } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index c5aada6a397..a82a997a5b3 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs @@ -308,24 +308,44 @@ public void BuildRuntimeArrayTypes_ReferenceLeaf_ReturnsProxyContract () rank1); var rank2 = TrimmableTypeMapTypeManager.BuildRuntimeArrayTypes (typeof (Java.Lang.Object), rank: 2); - CollectionAssert.Contains (rank2, typeof (JavaObjectArray>)); - CollectionAssert.Contains (rank2, typeof (Java.Interop.JavaArray[])); - CollectionAssert.Contains (rank2, typeof (Java.Lang.Object[][])); + CollectionAssert.AreEquivalent ( + new [] { + typeof (JavaObjectArray>), + typeof (JavaObjectArray[]), + typeof (JavaObjectArray>), + typeof (Java.Interop.JavaArray[]), + typeof (JavaObjectArray), + typeof (Java.Lang.Object[][]), + }, + rank2); } [Test] public void BuildRuntimeArrayTypes_PrimitiveLeaf_ReturnsProxyContract () { var rank1 = TrimmableTypeMapTypeManager.BuildRuntimeArrayTypes (typeof (sbyte), rank: 1); - CollectionAssert.Contains (rank1, typeof (sbyte[])); - CollectionAssert.Contains (rank1, typeof (Java.Interop.JavaArray)); - CollectionAssert.Contains (rank1, typeof (JavaPrimitiveArray)); - CollectionAssert.Contains (rank1, typeof (JavaSByteArray)); + CollectionAssert.AreEquivalent ( + new [] { + typeof (sbyte[]), + typeof (Java.Interop.JavaArray), + typeof (JavaPrimitiveArray), + typeof (JavaSByteArray), + }, + rank1); var rank2 = TrimmableTypeMapTypeManager.BuildRuntimeArrayTypes (typeof (sbyte), rank: 2); - CollectionAssert.Contains (rank2, typeof (sbyte[][])); - CollectionAssert.Contains (rank2, typeof (JavaObjectArray>)); - CollectionAssert.Contains (rank2, typeof (JavaObjectArray)); + CollectionAssert.AreEquivalent ( + new [] { + typeof (JavaObjectArray), + typeof (sbyte[][]), + typeof (JavaObjectArray>), + typeof (Java.Interop.JavaArray[]), + typeof (JavaObjectArray>), + typeof (JavaPrimitiveArray[]), + typeof (JavaObjectArray), + typeof (JavaSByteArray[]), + }, + rank2); } [Test] @@ -334,7 +354,10 @@ public void BuildRuntimeArrayTypes_NullablePrimitiveLeaf_ReturnsVector () // Nullable primitives have no array proxy and no AOT-safe Java.Interop array wrapper, so the // fallback returns just the exact rooted vector (int?[]) rather than an unrooted value generic. var rank1 = TrimmableTypeMapTypeManager.BuildRuntimeArrayTypes (typeof (int?), rank: 1); - CollectionAssert.Contains (rank1, typeof (int?[])); + CollectionAssert.AreEquivalent (new [] { typeof (int?[]) }, rank1); + + var rank2 = TrimmableTypeMapTypeManager.BuildRuntimeArrayTypes (typeof (int?), rank: 2); + CollectionAssert.AreEquivalent (new [] { typeof (int?[][]) }, rank2); } static ConcurrentDictionary GetProxyCache (TrimmableTypeMap instance)