diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 6a2a45012d8..a9d65cf6c37 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,23 @@ 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++) { + object? value = source.GetValue (i); + IntPtr lref = JavaConvert.ToLocalJniHandle (value); + try { + SetObjectArrayElement (dest, i, lref); + } finally { + DeleteLocalRef (lref); + GC.KeepAlive (value); + } + } + } + public static void CopyArray (Array source, Type elementType, IntPtr dest) { if (source == null) @@ -1045,6 +1068,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 +1100,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 +1362,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 +1513,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 +1540,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..3e968192a50 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) @@ -110,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<,>)) { @@ -129,73 +126,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..b0dcaa77561 --- /dev/null +++ b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs @@ -0,0 +1,128 @@ +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; + +using Microsoft.Android.Runtime; + +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 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; + } + + // 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); + } + + 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 && ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (elementType, out var factory)) { + array = factory.CreateArray (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 (ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (elementType, out var factory)) { + vectorType = factory.ArrayType; + return true; + } + + vectorType = null; + return false; + } + + vectorType = MakeReferenceArrayType (elementType); + return true; + } + + [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 (); + } + + [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 new file mode 100644 index 00000000000..0034cd1c5c7 --- /dev/null +++ b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs @@ -0,0 +1,232 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; + +using Android.Runtime; + +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) + { + 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, + 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; + } + + 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) { + // 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 (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 (keyFactory != null && !shape.Arguments [1].IsValueType) { + collection = keyFactory.CreateDictionaryWithReferenceValue (shape.Arguments [1], handle, transfer); + return true; + } + + if (valueFactory != null && !shape.Arguments [0].IsValueType) { + 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 ValueTypeFactory? factory) + { + if (type.IsValueType) { + return ValueTypeFactory.PrimitiveTypeFactories.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 && !ValueTypeFactory.PrimitiveTypeFactories.ContainsKey (argument)) { + return null; + } + } + + return MakeGenericType (genericTypeDefinition, arguments); + } + + [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); + } + + [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; + } + + 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 new file mode 100644 index 00000000000..54bd03772a5 --- /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/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..4f7d937ba56 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -325,7 +325,10 @@ + + + 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 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..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 @@ -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,73 @@ 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 (); + } + } + } + + // 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 (); @@ -138,6 +206,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; + } } } -