diff --git a/build-tools/automation/yaml-templates/stage-package-tests.yaml b/build-tools/automation/yaml-templates/stage-package-tests.yaml index d4a684b183c..7cbed90d579 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 + 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/external/Java.Interop/src/Java.Interop/Java.Interop/JniBuiltinMarshalers.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniBuiltinMarshalers.cs index f51f6b2e8f0..b982c14b5ab 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniBuiltinMarshalers.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniBuiltinMarshalers.cs @@ -93,7 +93,7 @@ static bool GetBuiltInTypeSignature (Type type, ref JniTypeSignature signature) signature = GetCachedTypeSignature (ref __BooleanNullableTypeSignature, "java/lang/Boolean"); return true; } - if (type == typeof (SByte?)) { + if (type == typeof (Byte?) || type == typeof (SByte?)) { signature = GetCachedTypeSignature (ref __SByteNullableTypeSignature, "java/lang/Byte"); return true; } diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs index 198cb9c97fd..3a407b527d1 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs @@ -32,7 +32,11 @@ public void GetTypeSignature_Type () AssertGetJniTypeInfoForType (typeof (bool), "Z", true, 0); AssertGetJniTypeInfoForType (typeof (void), "V", true, 0); - AssertGetJniTypeInfoForType (typeof (float?), "java/lang/Float", true, 0); + AssertGetJniTypeInfoForType (typeof (float?), "java/lang/Float", true, 0); + AssertGetJniTypeInfoForType (typeof (byte?), "java/lang/Byte", true, 0); + AssertGetJniTypeInfoForType (typeof (byte?[]), "[Ljava/lang/Byte;", true, 1); + AssertGetJniTypeInfoForType (typeof (byte?[][]), "[[Ljava/lang/Byte;", true, 2); + AssertGetJniTypeInfoForType (typeof (byte?[][][]), "[[[Ljava/lang/Byte;", true, 3); AssertGetJniTypeInfoForType (typeof (JavaObject), "java/lang/Object", false, 0); @@ -136,6 +140,7 @@ static void AssertGetJniTypeInfoForType (Type type, string jniType, bool isKeywo Assert.AreEqual (typeof (float), GetType ("F")); Assert.AreEqual (typeof (double), GetType ("D")); Assert.AreEqual (typeof (string), GetType ("java/lang/String")); + Assert.AreEqual (typeof (sbyte?), GetType ("java/lang/Byte")); Assert.AreEqual (typeof (float?), GetType ("java/lang/Float")); Assert.AreEqual (null, GetType ("com/example/does/not/exist")); Assert.AreEqual (null, GetType ("Lcom/example/does/not/exist;")); diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index b8a4d73914e..39041b470d8 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -32,19 +32,7 @@ static Array ArrayCreateInstance (Type elementType, int length) } if (RuntimeFeature.TrimmableTypeMap) { - 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); } if (RuntimeFeature.ManagedTypeMap) { @@ -61,21 +49,6 @@ Array ArrayCreateInstanceWithSuppression (Type elementType, int length) throw new NotSupportedException ($"It is not possible to create an array with element type '{elementType}'."); } - 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)); @@ -613,11 +586,30 @@ 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 (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"; + 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) @@ -692,6 +684,15 @@ 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 (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?)) }, + { 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)) @@ -717,6 +718,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; @@ -921,6 +928,15 @@ 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 (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) }, + { 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) { @@ -950,6 +966,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) @@ -1053,6 +1086,15 @@ public static void CopyArray (T[] src, IntPtr dest) CopyArray (source, r); 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) }, + { 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]; @@ -1077,6 +1119,16 @@ public static void CopyArray (T[] src, IntPtr dest) }; } + static Array CreateManagedArrayFromObjectArray (Type? elementType, IntPtr source, int len) + { + if (elementType == null) + throw new ArgumentNullException (nameof (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) @@ -1329,12 +1381,49 @@ 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 (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?)) }, + { 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) @@ -1444,6 +1533,15 @@ 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 (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) }, + { 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 { @@ -1463,6 +1561,19 @@ static IntPtr NewArray (Array value, Type elementType, IntPtr elementClass) }; } + static void SetObjectArrayElementFromManagedValue (IntPtr dest, int index, object? value) + { + // 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); + } finally { + DeleteLocalRef (lref); + GC.KeepAlive (value); + } + } + 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 e8ee8741acb..c02a02e00fe 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,8 @@ static class JavaConvert { if (target.IsGenericType && !target.IsGenericTypeDefinition) { if (RuntimeFeature.TrimmableTypeMap) { - var factoryConverter = TryGetFactoryBasedConverter (target); - if (factoryConverter != null) - return factoryConverter; + if (SafeJavaCollectionFactory.TryGetFromJniHandleConverter (target, out var collectionConverter)) + return collectionConverter; } else if (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) { var factoryConverter = TryMakeGenericCollectionTypeFactory (target); if (factoryConverter != null) @@ -141,73 +127,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..f722c1adc80 --- /dev/null +++ b/src/Mono.Android/Java.Interop/SafeArrayFactory.cs @@ -0,0 +1,133 @@ +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; + +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; + } + + 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.")] + 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, " + + "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..b26bbc2b5f1 --- /dev/null +++ b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs @@ -0,0 +1,230 @@ +#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 TryGetFromJniHandleConverter ( + Type targetType, + [NotNullWhen (true)] out Func? converter) + { + if (targetType == null) + throw new ArgumentNullException (nameof (targetType)); + + if (!TryGetCollectionShape (targetType, out var shape)) { + converter = null; + return false; + } + + if (!IsSupportedCollectionShape (shape)) { + converter = null; + return false; + } + + // 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; + } + } + + return true; + } + + static object? CreateFromJniHandle ( + CollectionShape shape, + IntPtr handle, + JniHandleOwnership transfer) + { + if (handle == IntPtr.Zero) { + return null; + } + + if (TryCreateFromMappedValueTypeFactories (shape, handle, transfer, out var collection)) { + return collection; + } + + return CreateInstance (GetClosedCollectionType (shape), handle, transfer); + } + + 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 (CollectionShape shape) + { + 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", + 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..a2543d301d1 --- /dev/null +++ b/src/Mono.Android/Java.Interop/ValueTypeFactory.cs @@ -0,0 +1,126 @@ +#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. + // `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 () }, + { typeof (int), new ValueTypeFactory () }, + { typeof (long), new 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 () }, + { 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) + { + // 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/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index b5b34ea2b09..4ffbf4d4669 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -42,8 +42,7 @@ public class TrimmableTypeMap } /// - /// Initializes the singleton with a single merged typemap universe and optional - /// per-rank array dictionaries (consulted by JNIEnv.ArrayCreateInstance under NativeAOT). + /// Initializes the singleton with a single merged typemap universe. /// public static void Initialize ( IReadOnlyDictionary typeMap, @@ -53,10 +52,10 @@ public static void Initialize ( } /// - /// Initializes the singleton with a single merged typemap universe and optional - /// per-rank array dictionaries (consulted by JNIEnv.ArrayCreateInstance under NativeAOT). + /// Initializes the singleton with a single merged typemap universe and the per-rank array + /// dictionaries emitted by the current generator. /// - /// 0-indexed by (rank - 1); null when no array entries were emitted. + /// 0-indexed by (rank - 1); retained for compatibility with the current generated typemap format. public static void Initialize ( IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap, @@ -493,15 +492,6 @@ internal static bool TargetTypeMatches (Type targetType, Type proxyTargetType) return targetType.IsAssignableFrom (proxyTargetType); } - /// - /// Gets the container factory for a type from its proxy attribute. - /// Used for AOT-safe array/collection/dictionary creation. - /// - internal JavaPeerContainerFactory? GetContainerFactory (Type type) - { - return GetProxyForManagedType (type)?.GetContainerFactory (); - } - /// Lookup of the generated array proxy after adding array rank to the given element type. internal bool TryGetArrayProxy (Type elementType, int additionalRank, [NotNullWhen (true)] out JavaArrayProxy? arrayProxy) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index f2e1406e89a..fd66a333a5c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -56,11 +56,9 @@ static IEnumerable GetArrayTypes (JniTypeSignature typeSignature, Type ele return GetArrayTypesForCoreClr (typeSignature, elementType); } - // 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. - return TrimmableTypeMap.Instance.TryGetArrayProxy (elementType, typeSignature.ArrayRank, out var arrayProxy) - ? arrayProxy.GetArrayTypes () - : []; + // NativeAOT reconstructs the array and wrapper types at runtime instead of using + // generated array proxies. + 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 " + @@ -126,6 +124,71 @@ static IEnumerable MakeArrayTypes (Type elementType, int rank) } } + // Builds the array and wrapper types needed by the NativeAOT marshaling path without generated + // array proxies. 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 AOT-safe 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); @@ -288,6 +351,7 @@ static bool TryGetBuiltInReferenceJniName (Type type, [NotNullWhen (true)] out s { if (type == typeof (string)) { jni = "java/lang/String"; return true; } if (type == typeof (bool?)) { jni = "java/lang/Boolean"; return true; } + if (type == typeof (byte?)) { jni = "java/lang/Byte"; return true; } if (type == typeof (sbyte?)) { jni = "java/lang/Byte"; return true; } if (type == typeof (char?)) { jni = "java/lang/Character"; return true; } if (type == typeof (short?)) { jni = "java/lang/Short"; return true; } diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index adf70272f64..25aee38cdf3 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -324,7 +324,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 0b6a9753cc4..0654830cf97 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 @@ -8,7 +8,7 @@ "Size": 402852 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 2724872 + "Size": 2819928 }, "lib/arm64-v8a/libclrjit.so": { "Size": 2757816 @@ -17,7 +17,7 @@ "Size": 4837240 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 1235528 + "Size": 1220808 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72112 @@ -32,7 +32,7 @@ "Size": 163936 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 21288 + "Size": 23704 }, "res/drawable-hdpi-v4/icon.png": { "Size": 2178 @@ -59,5 +59,5 @@ "Size": 1904 } }, - "PackageSize": 7235003 + "PackageSize": 7325115 } \ 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..dc18a9f5819 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,88 @@ 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_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] + [Category ("NativeAOTTrimmable")] + public void GetArray_NullableByteArrayArray () + { + var values = new [] { + new byte? [] { 1, null, 200 }, + new byte? [] { 255, 128 }, + }; + 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); + Assert.AreEqual (values.Length, copy.Length); + for (int i = 0; i < values.Length; i++) { + AssertArrays ($"GetArray[{i}]", copy [i], values [i]); + } + } + } + + [Test] + [Category ("NativeAOTTrimmable")] + public void GetArray_NullableByteArrayArrayArray () + { + var values = new [] { + new [] { + new byte? [] { 1, null, 200 }, + new byte? [] { 255, 128 }, + }, + new [] { + new byte? [] { null, 42 }, + }, + }; + 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); + Assert.AreEqual (values.Length, copy.Length); + for (int i = 0; i < values.Length; i++) { + Assert.AreEqual (values [i].Length, copy [i].Length); + for (int j = 0; j < values [i].Length; j++) { + AssertArrays ($"GetArray[{i}][{j}]", copy [i][j], values [i][j]); + } + } + } + } + [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..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 @@ -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,99 @@ 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 (); + } + } + } + + // 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 () + { + 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 { + var dictionary = (IDictionary) converted; + Assert.AreEqual (100L, dictionary [1]); + Assert.AreEqual (200L, dictionary [2]); + } finally { + (converted as IDisposable)?.Dispose (); + } + } + } + + // 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 (); @@ -138,6 +232,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; + } } } - 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..74a13f13f2c 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 @@ -15,8 +15,8 @@ namespace Java.InteropTests public class TrimmableTypeMapTypeManagerTests { // Test subclass that allows instantiation without full TrimmableTypeMap initialization. - // GetStaticMethodFallbackTypesCore does not use TrimmableTypeMap.Instance, so the test - // can run without an initialized TrimmableTypeMap singleton. + // Built-in type-signature and static-method fallback lookups do not use + // TrimmableTypeMap.Instance, so those tests can run without an initialized singleton. sealed class TestableTrimmableTypeMapTypeManager : TrimmableTypeMapTypeManager { } @@ -34,6 +34,17 @@ public void GetStaticMethodFallbackTypes_ReturnsDesugarFallbacks (string jniSimp Assert.AreEqual (expectedFallback, fallbacks [1]); } + [TestCase (typeof (byte?[][]), "[[Ljava/lang/Byte;")] + [TestCase (typeof (byte?[][][]), "[[[Ljava/lang/Byte;")] + [Category ("NativeAOTTrimmable")] + public void GetTypeSignature_NullableByteJaggedArray_ReturnsJavaLangByte (Type type, string expected) + { + using var manager = new TestableTrimmableTypeMapTypeManager (); + var signature = manager.GetTypeSignature (type); + + Assert.AreEqual (expected, signature.Name); + } + // Verifies the generic-type-definition fallback in GetProxyForManagedType: // the generator emits one TypeMapAssociation per open generic peer, so a // closed instantiation like JavaList must resolve through its GTD. @@ -290,6 +301,75 @@ public void TryGetArrayProxy_PrimitiveLeaf_ReturnsAllRankTypes () CollectionAssert.Contains (jaggedSbyteArrayProxy.GetArrayTypes (), typeof (JavaObjectArray)); } + // Regression: the runtime replacement (BuildRuntimeArrayTypes) must return the full set of array + // and wrapper types, not just T[]. The TryGetArrayProxy_* tests above only cover the legacy + // generated-proxy path, 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.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.AreEquivalent ( + new [] { + typeof (sbyte[]), + typeof (Java.Interop.JavaArray), + typeof (JavaPrimitiveArray), + typeof (JavaSByteArray), + }, + rank1); + + var rank2 = TrimmableTypeMapTypeManager.BuildRuntimeArrayTypes (typeof (sbyte), rank: 2); + CollectionAssert.AreEquivalent ( + new [] { + typeof (JavaObjectArray), + typeof (sbyte[][]), + typeof (JavaObjectArray>), + typeof (Java.Interop.JavaArray[]), + typeof (JavaObjectArray>), + typeof (JavaPrimitiveArray[]), + typeof (JavaObjectArray), + typeof (JavaSByteArray[]), + }, + rank2); + } + + [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.AreEquivalent (new [] { typeof (int?[]) }, rank1); + + var rank2 = TrimmableTypeMapTypeManager.BuildRuntimeArrayTypes (typeof (int?), rank: 2); + CollectionAssert.AreEquivalent (new [] { typeof (int?[][]) }, rank2); + } + static ConcurrentDictionary GetProxyCache (TrimmableTypeMap instance) { var field = typeof (TrimmableTypeMap).GetField ("_proxyCache", BindingFlags.Instance | BindingFlags.NonPublic);