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