diff --git a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs index b26bbc2b5f1..d56410c1d4b 100644 --- a/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs +++ b/src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs @@ -9,222 +9,230 @@ namespace Java.Interop; +/// +/// Produces the Java-handle-to-managed converters used by the trimmable typemap when the marshaling +/// target is a Java collection wrapper (, , +/// , or the // +/// interfaces they implement). +/// +/// +/// NativeAOT's 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<string> can share the JavaList<__Canon> +/// template, but JavaList<int> and JavaList<int?> need exact rooted instantiations. +/// +/// The per-container construction therefore splits into explicit, non-overlapping paths: +/// +/// +/// reference element arguments ride the __Canon template: the wrapper definition is +/// reflectively closed and its activation constructor invoked, kept alive by a concrete-literal +/// IJavaPeerable rooting branch in the same method; +/// primitive/nullable value-type arguments go through , +/// which roots the exact instantiation with a direct new; +/// other value types use the corresponding untyped collection wrapper because +/// their exact generic instantiations are not rooted. +/// +/// 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. + /// Binding flags used to find the activation constructor of the Java collection wrappers. + internal const BindingFlags ActivationConstructorBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; internal static bool TryGetFromJniHandleConverter ( Type targetType, [NotNullWhen (true)] out Func? converter) { - if (targetType == null) - throw new ArgumentNullException (nameof (targetType)); + ArgumentNullException.ThrowIfNull (targetType); - if (!TryGetCollectionShape (targetType, out var shape)) { + // Reject open and partially-open constructed types (e.g. IList<>, IDictionary). These have + // ContainsGenericParameters == true and would produce an open wrapper from MakeGenericType (), whose + // activation would throw ArgumentException. Fail cleanly here rather than crash during construction. + if (targetType.ContainsGenericParameters) { converter = null; return false; } - if (!IsSupportedCollectionShape (shape)) { - converter = null; - return false; + if (targetType.IsGenericType && !targetType.IsGenericTypeDefinition) { + var genericDefinition = targetType.GetGenericTypeDefinition (); + if (IsKnownContainerDefinition (genericDefinition)) { + // Capture the parsed arguments so GetGenericArguments () runs once when the converter is + // selected, rather than again for every conversion. + var arguments = targetType.GetGenericArguments (); + if (genericDefinition == typeof (IList<>) || genericDefinition == typeof (JavaList<>)) { + var elementType = arguments [0]; + if (elementType.IsValueType) { + if (!ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (elementType, out var listFactory)) { + converter = GetUntypedFromJniHandleConverter (genericDefinition); + return true; + } + converter = (handle, transfer) => handle == IntPtr.Zero ? null : listFactory.CreateList (handle, transfer); + return true; + } + converter = (handle, transfer) => CreateReferenceListFromJniHandle (elementType, handle, transfer); + return true; + } + + if (genericDefinition == typeof (ICollection<>) || genericDefinition == typeof (JavaCollection<>)) { + var elementType = arguments [0]; + if (elementType.IsValueType) { + if (!ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (elementType, out var collectionFactory)) { + converter = GetUntypedFromJniHandleConverter (genericDefinition); + return true; + } + converter = (handle, transfer) => handle == IntPtr.Zero ? null : collectionFactory.CreateCollection (handle, transfer); + return true; + } + converter = (handle, transfer) => CreateReferenceCollectionFromJniHandle (elementType, handle, transfer); + return true; + } + + var keyType = arguments [0]; + var valueType = arguments [1]; + ValueTypeFactory? keyFactory = null; + ValueTypeFactory? valueFactory = null; + if ((keyType.IsValueType && !ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (keyType, out keyFactory)) + || (valueType.IsValueType && !ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (valueType, out valueFactory))) { + converter = GetUntypedFromJniHandleConverter (genericDefinition); + return true; + } + if (keyFactory != null) { + converter = valueFactory != null + ? (handle, transfer) => handle == IntPtr.Zero ? null : keyFactory.CreateDictionary (valueFactory, handle, transfer) + : (handle, transfer) => handle == IntPtr.Zero ? null : keyFactory.CreateDictionaryWithReferenceValue (valueType, handle, transfer); + return true; + } + if (valueFactory != null) { + converter = (handle, transfer) => handle == IntPtr.Zero ? null : valueFactory.CreateDictionaryWithReferenceKey (keyType, handle, transfer); + return true; + } + converter = (handle, transfer) => CreateReferenceDictionaryFromJniHandle (keyType, valueType, handle, transfer); + return true; + } } - // 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; + converter = null; + return false; } - 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; - } - } + static bool IsKnownContainerDefinition (Type genericDefinition) + => genericDefinition == typeof (IList<>) || genericDefinition == typeof (JavaList<>) + || genericDefinition == typeof (ICollection<>) || genericDefinition == typeof (JavaCollection<>) + || genericDefinition == typeof (IDictionary<,>) || genericDefinition == typeof (JavaDictionary<,>); - return true; + static Func GetUntypedFromJniHandleConverter (Type genericDefinition) + { + if (genericDefinition == typeof (IList<>) || genericDefinition == typeof (JavaList<>)) + return (handle, transfer) => JavaList.FromJniHandle (handle, transfer); + if (genericDefinition == typeof (ICollection<>) || genericDefinition == typeof (JavaCollection<>)) + return (handle, transfer) => JavaCollection.FromJniHandle (handle, transfer); + return (handle, transfer) => JavaDictionary.FromJniHandle (handle, transfer); } - static object? CreateFromJniHandle ( - CollectionShape shape, - IntPtr handle, - JniHandleOwnership transfer) + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "MakeGenericType () and Activator.CreateInstance () are annotated because arbitrary constructed generics can lack a runtime template. " + + "elementType is always a reference type here (value types are diverted to ValueTypeFactory above), so JavaList canonicalizes to the " + + "JavaList<__Canon> template whose activation constructor is rooted by the direct JavaList construction in the other branch.")] + [UnconditionalSuppressMessage ("Trimming", "IL2071:MakeGenericType", + Justification = "IL2071 fires because MakeGenericType () cannot statically prove the runtime elementType satisfies the DynamicallyAccessedMembers(Constructors) " + + "requirement that JavaList<[DAM(Constructors)] TElement> places on its element parameter. That requirement exists only for the dynamic-code path, where the wrapper " + + "reflectively activates element peers from their constructors. On the trimmable typemap path the wrapper never activates its elements — element peer creation goes " + + "through JavaConvert and the typemap's registered activation constructors — so the unsatisfied element requirement is never exercised.")] + [UnconditionalSuppressMessage ("Trimming", "IL2072:UnrecognizedReflectionPattern", + Justification = "The dynamically constructed JavaList rides the JavaList canonical template whose activation constructor is rooted by the " + + "concrete-literal branch. Only the known JavaList activation constructor is invoked here.")] + static object? CreateReferenceListFromJniHandle (Type elementType, 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 (elementType == typeof (IJavaPeerable)) { + // Concrete-literal rooting branch. Taken only when marshaling the JavaList shape itself + // (uncommon), its main purpose is to statically root the (IntPtr, JniHandleOwnership) constructor on the + // JavaList<__Canon> template for the trimmer/ILC. The else branch reuses that same canonical constructor + // for every other JavaList. + return new JavaList (handle, transfer); } - if (genericDefinition == typeof (ICollection<>) || genericDefinition == typeof (JavaCollection<>)) { - shape = new CollectionShape (JavaCollectionKind.Collection, targetType.GetGenericArguments ()); - return true; + var listType = typeof (JavaList<>).MakeGenericType (elementType); + var result = Activator.CreateInstance (listType, ActivationConstructorBinding, binder: null, args: [handle, transfer], culture: CultureInfo.InvariantCulture); + if (result == null) { + throw new InvalidOperationException ($"Unable to create a JavaList instance for element type '{elementType}'."); } - - if (genericDefinition == typeof (IDictionary<,>) || genericDefinition == typeof (JavaDictionary<,>)) { - shape = new CollectionShape (JavaCollectionKind.Dictionary, targetType.GetGenericArguments ()); - return true; - } - - shape = default; - return false; + return result; } - static bool TryCreateFromMappedValueTypeFactories ( - CollectionShape shape, - IntPtr handle, - JniHandleOwnership transfer, - [NotNullWhen (true)] out object? collection) + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "MakeGenericType () and Activator.CreateInstance () are annotated because arbitrary constructed generics can lack a runtime template. " + + "elementType is always a reference type here (value types are diverted to ValueTypeFactory above), so JavaCollection canonicalizes to the " + + "JavaCollection<__Canon> template whose activation constructor is rooted by the direct JavaCollection construction in the other branch.")] + [UnconditionalSuppressMessage ("Trimming", "IL2071:MakeGenericType", + Justification = "IL2071 fires because MakeGenericType () cannot statically prove the runtime elementType satisfies the DynamicallyAccessedMembers(Constructors) " + + "requirement that JavaCollection<[DAM(Constructors)] TElement> places on its element parameter. That requirement exists only for the dynamic-code path, where the " + + "wrapper reflectively activates element peers from their constructors. On the trimmable typemap path the wrapper never activates its elements — element peer creation " + + "goes through JavaConvert and the typemap's registered activation constructors — so the unsatisfied element requirement is never exercised.")] + [UnconditionalSuppressMessage ("Trimming", "IL2072:UnrecognizedReflectionPattern", + Justification = "The dynamically constructed JavaCollection rides the JavaCollection canonical template whose activation constructor is rooted " + + "by the concrete-literal branch. Only the known JavaCollection activation constructor is invoked here.")] + static object? CreateReferenceCollectionFromJniHandle (Type elementType, IntPtr handle, JniHandleOwnership transfer) { - 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 (handle == IntPtr.Zero) { + return null; } - if (TryGetValueTypeFactory (shape.Arguments [0], out var factory)) { - collection = shape.Kind == JavaCollectionKind.List - ? factory.CreateList (handle, transfer) - : factory.CreateCollection (handle, transfer); - return true; + if (elementType == typeof (IJavaPeerable)) { + // Concrete-literal rooting branch. Taken only when marshaling the JavaCollection shape + // itself (uncommon), its main purpose is to statically root the (IntPtr, JniHandleOwnership) constructor + // on the JavaCollection<__Canon> template for the trimmer/ILC. The else branch reuses that same canonical + // constructor for every other JavaCollection. + return new JavaCollection (handle, transfer); } - collection = null; - return false; - } - - static bool TryGetValueTypeFactory (Type type, [NotNullWhen (true)] out ValueTypeFactory? factory) - { - if (type.IsValueType) { - return ValueTypeFactory.PrimitiveTypeFactories.TryGetValue (type, out factory); + var collectionType = typeof (JavaCollection<>).MakeGenericType (elementType); + var result = Activator.CreateInstance (collectionType, ActivationConstructorBinding, binder: null, args: [handle, transfer], culture: CultureInfo.InvariantCulture); + if (result == null) { + throw new InvalidOperationException ($"Unable to create a JavaCollection instance for element type '{elementType}'."); } - - 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}'."), - }; + return result; } [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.")] + Justification = "MakeGenericType () and Activator.CreateInstance () are annotated because arbitrary constructed generics can lack a runtime template. " + + "The reflective branch is reached only when both arguments are reference types, so JavaDictionary canonicalizes to the " + + "JavaDictionary<__Canon,__Canon> template whose activation constructor is rooted by the direct JavaDictionary construction in the other branch.")] + [UnconditionalSuppressMessage ("Trimming", "IL2071:MakeGenericType", + Justification = "IL2071 fires because MakeGenericType () cannot statically prove the runtime keyType/valueType satisfy the DynamicallyAccessedMembers(Constructors) " + + "requirement that JavaDictionary<[DAM(Constructors)] TKey, [DAM(Constructors)] TValue> places on its element parameters. That requirement exists only for the " + + "dynamic-code path, where the wrapper reflectively activates key/value peers from their constructors. On the trimmable typemap path the wrapper never activates its " + + "elements — element peer creation goes through JavaConvert and the typemap's registered activation constructors — so the unsatisfied element requirement is never exercised.")] [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) + Justification = "The dynamically constructed JavaDictionary rides the JavaDictionary canonical template whose activation " + + "constructor is rooted by the concrete-literal branch. Only the known JavaDictionary activation constructor is invoked here.")] + static object? CreateReferenceDictionaryFromJniHandle (Type keyType, Type valueType, IntPtr handle, JniHandleOwnership transfer) { - 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}'."); + if (handle == IntPtr.Zero) { + return null; } - return instance; - } - readonly struct CollectionShape - { - public CollectionShape (JavaCollectionKind kind, Type[] arguments) - { - Kind = kind; - Arguments = arguments; + // Both arguments are reference types: JavaDictionary rides the __Canon template. + if (keyType == typeof (IJavaPeerable) && valueType == typeof (IJavaPeerable)) { + // Concrete-literal rooting branch. Taken only when marshaling the JavaDictionary shape itself (uncommon), its main purpose is to statically root the (IntPtr, + // JniHandleOwnership) constructor on the JavaDictionary<__Canon,__Canon> template for the trimmer/ILC. + // The else branch reuses that same canonical constructor for every other reference pair. + return new JavaDictionary (handle, transfer); } - public JavaCollectionKind Kind { get; } - - public Type[] Arguments { get; } - } - - enum JavaCollectionKind { - List, - Collection, - Dictionary, + var dictionaryType = typeof (JavaDictionary<,>).MakeGenericType (keyType, valueType); + var result = Activator.CreateInstance (dictionaryType, ActivationConstructorBinding, binder: null, args: [handle, transfer], culture: CultureInfo.InvariantCulture); + if (result == null) { + throw new InvalidOperationException ($"Unable to create a JavaDictionary instance for key type '{keyType}' and value type '{valueType}'."); + } + return result; } - } diff --git a/src/Mono.Android/Java.Interop/ValueTypeFactory.cs b/src/Mono.Android/Java.Interop/ValueTypeFactory.cs index a2543d301d1..7466ea4ac63 100644 --- a/src/Mono.Android/Java.Interop/ValueTypeFactory.cs +++ b/src/Mono.Android/Java.Interop/ValueTypeFactory.cs @@ -2,7 +2,10 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; using Android.Runtime; @@ -12,8 +15,8 @@ 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. + // 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 () { @@ -61,12 +64,6 @@ abstract class ValueTypeFactory 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 () { } @@ -97,16 +94,62 @@ internal override IDictionary CreateDictionary (ValueTypeFactory valueFactory, I 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); + // JavaDictionary canonicalizes like JavaDictionary<__Canon, T>; the + // JavaDictionary exemplar roots that template with its constructors. + return CreateReferenceMixedDictionary> ([keyType, typeof (T)], 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); + // JavaDictionary canonicalizes like JavaDictionary; the + // JavaDictionary exemplar roots that template with its constructors. + return CreateReferenceMixedDictionary> ([typeof (T), valueType], handle, transfer); + } + + /// + /// Builds a mixed reference/value whose reference argument rides the + /// __Canon template rooted by the instantiation. + /// + /// + /// The JavaDictionary<IJavaPeerable, T> / JavaDictionary<T, IJavaPeerable> exemplar. + /// Its roots the constructors of the canonical + /// template that resolves to (the exact-value argument stays value-specific). + /// + /// The [key, value] generic arguments, exactly one of which is a reference type. + [UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode", + Justification = "NativeAOT's Type.MakeGenericType() and Activator.CreateInstance() are annotated because arbitrary constructed generics can lack a runtime template. " + + "This helper only closes JavaDictionary<,> over one reference argument and the mapped value type T. " + + "The reference argument canonicalizes to __Canon and T stays value-specific, so the result shares the JavaDictionary / JavaDictionary " + + "template that TExemplar already roots.")] + [UnconditionalSuppressMessage ("Trimming", "IL2055:MakeGenericType", + Justification = "IL2055 is raised because the runtime key/value arguments cannot be statically proven to satisfy the DynamicallyAccessedMembers(Constructors) " + + "requirement that JavaDictionary<[DAM(Constructors)] TKey, [DAM(Constructors)] TValue> places on its element type parameters. That requirement exists for the " + + "dynamic-code path, where the wrapper reflectively activates key/value peers from their constructors. On the trimmable typemap path taken here the wrapper never " + + "activates its elements: element peer creation goes through JavaConvert and the typemap's registered activation constructors, so the unmet element requirement is " + + "never exercised. The wrapper's own constructor is separately rooted by the DynamicallyAccessedMembers(Constructors) annotation on TExemplar.")] + [UnconditionalSuppressMessage ("Trimming", "IL2072:UnrecognizedReflectionPattern", + Justification = "The constructed dictionary type rides the TExemplar canonical template whose constructors are rooted by DynamicallyAccessedMembers(Constructors). " + + "Only the known JavaDictionary constructor is invoked here.")] + static IDictionary CreateReferenceMixedDictionary<[DynamicallyAccessedMembers (SafeJavaCollectionFactory.Constructors)] TExemplar> ( + Type[] arguments, + IntPtr handle, + JniHandleOwnership transfer) + { + var exemplarType = typeof (TExemplar); + Debug.Assert (exemplarType.IsGenericType && !exemplarType.IsGenericTypeDefinition); + + var definition = exemplarType.GetGenericTypeDefinition (); + var dictionaryType = definition.MakeGenericType (arguments); + var instance = Activator.CreateInstance ( + dictionaryType, + SafeJavaCollectionFactory.ActivationConstructorBinding, + binder: null, + args: [handle, transfer], + culture: CultureInfo.InvariantCulture); + if (instance == null) { + throw new InvalidOperationException ($"Unable to create an instance of collection type '{dictionaryType}'."); + } + return (IDictionary) instance; } internal override IDictionary CreateDictionaryWithKey<[DynamicallyAccessedMembers (SafeJavaCollectionFactory.Constructors)] TKey> ( diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs index f62b3c54fdd..f59d79b9aa6 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 @@ -221,6 +221,58 @@ public void FromJniHandle_IListByte () } } + // Keep the expected wrapper types open so NativeAOT must construct the closed reference-type + // wrappers through SafeJavaCollectionFactory's rooted canonical templates. + [TestCase (typeof (IList), typeof (JavaList<>), false)] + [TestCase (typeof (ICollection), typeof (JavaCollection<>), false)] + [TestCase (typeof (IDictionary), typeof (JavaDictionary<,>), true)] + public void FromJniHandle_ReferenceArgumentsUseCanonicalTemplate (Type targetType, Type expectedWrapperDefinition, bool dictionary) + { + Java.Lang.Object source = dictionary ? new JavaDictionary () : new JavaList (); + using (source) { + var converted = InvokeJavaConvertFromJniHandle (targetType, source.Handle, JniHandleOwnership.DoNotTransfer); + try { + var convertedType = converted.GetType (); + Assert.AreEqual (expectedWrapperDefinition, convertedType.GetGenericTypeDefinition ()); + CollectionAssert.AreEqual (targetType.GetGenericArguments (), convertedType.GetGenericArguments ()); + Assert.IsTrue (targetType.IsInstanceOfType (converted)); + } finally { + (converted as IDisposable)?.Dispose (); + } + } + } + + [TestCase (typeof (IList), false)] + [TestCase (typeof (ICollection), false)] + [TestCase (typeof (IDictionary), true)] + [TestCase (typeof (IDictionary), true)] + [TestCase (typeof (IList), false)] + [TestCase (typeof (ICollection), false)] + [TestCase (typeof (IDictionary), true)] + [TestCase (typeof (IDictionary), true)] + [Category ("NativeAOTTrimmable")] + public void FromJniHandle_UnsupportedValueTypeUsesUntypedFallback (Type targetType, bool dictionary) + { + if (!Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { + Assert.Ignore ("This test validates unsupported value-type container fallback on the trimmable typemap path."); + } + + Java.Lang.Object source = dictionary ? new JavaDictionary () : new JavaList (); + using (source) { + var converted = InvokeJavaConvertFromJniHandle (targetType, source.Handle, JniHandleOwnership.DoNotTransfer); + try { + Assert.AreEqual (dictionary ? typeof (JavaDictionary) : typeof (JavaList), converted.GetType ()); + Assert.IsFalse (targetType.IsInstanceOfType (converted)); + } finally { + (converted as IDisposable)?.Dispose (); + } + } + } + + readonly struct UnsupportedValueType + { + } + static Java.Util.ArrayList CreateList (params int[][] items) { var list = new Java.Util.ArrayList ();