Skip to content

[TrimmableTypeMap] Support Java collections of interface element types under NativeAOT (JavaList<TInterface, TInvoker>) #11770

Description

@simonrozsival

Background

PR #11769 (carved out of #11617) makes generated interface peer proxies derive from the non-generic JavaPeerProxy base instead of JavaPeerProxy<T>. This is required because closing JavaPeerProxy<[DynamicallyAccessedMembers(Constructors)] T> over a Java interface (which has no constructors) makes ILC fail to load the type:

Failed to load type 'Java.Interop.JavaPeerProxy`1<...INonMarshalingPreviewCallback>' from assembly 'Mono.Android'

A side effect of that fix: the non-generic JavaPeerProxy.GetContainerFactory() returns null for interface peers, so the AOT-safe container path in JavaConvert.TryGetFactoryBasedConverter cannot build JavaList<IInterface> / JavaCollection<IInterface> / JavaDictionary<…, IInterface>. A binding member that surfaces such a collection (e.g. one returning IList<ISomeListener>) falls back to Type.MakeGenericType(...), which is unavailable under NativeAOT.

Why we can't simply reuse the existing single-parameter container types

Three independent walls block JavaList<IInterface> from existing under ILC:

  1. DAM wallJavaList<T>, JavaCollection<T>, JavaDictionary<TKey,TValue>, and JavaPeerContainerFactory<T> all annotate their type parameter(s) with [DynamicallyAccessedMembers(PublicConstructors | NonPublicConstructors)]. Instantiating any of them over a ctor-less interface fails ILC the same way JavaPeerProxy<IInterface> does.
  2. Reflection wall — the DAM is load-bearing, not redundant: ValueManager.GetPeer reflects on the activation constructor for the cases the typemap cannot proxy (constructed generics). You can't reflect-construct an interface.
  3. Variance wall — substituting the invoker as the single type argument (JavaList<TInvoker> = IList<TInvoker>) does not satisfy the user-facing IList<TInterface>, because C# generics are invariant.

Proposed solution: two-type-parameter container types

Separate the presented element type from the constructed element type:

internal sealed class JavaList<
        TInterface,
        [DynamicallyAccessedMembers (Constructors)] TInvoker
    > : JavaList, IList<TInterface>
    where TInvoker : TInterface
{
    // The DAM is on TInvoker (a concrete invoker that HAS constructors), so ILC can load it.
    // Construction goes through the real ctor (no reflection on the interface), and the value
    // is presented as TInterface (TInvoker : TInterface) so it fits IList<TInterface>.
    internal TInterface? InternalGet (int location)
        => JavaConvert.FromJniHandle<TInvoker> (
               InternalGetReference (location).Handle,
               JniHandleOwnership.TransferLocalRef);
    // ...
}
  • DAM wall → solved: the DAM lands on TInvoker, which has constructors.
  • Reflection wall → solved: JavaConvert.FromJniHandle<TInvoker> constructs the concrete invoker.
  • Variance wall → solved: the type is IList<TInterface>.

The generated JavaPeerContainerFactory for an interface peer already has both types available (the proxy carries TargetType = the interface and InvokerType = the invoker), so it can instantiate JavaList<TInterface, TInvoker> directly. Base-class virtual methods can route to the correctly DAM-annotated concrete type so the single-parameter generic container types are never instantiated over an interface.

Equivalent two-parameter forms are needed for:

  • JavaCollection<TInterface, TInvoker>
  • JavaDictionary<…> for interface keys and/or values (potentially four combinations, or a key/value-invoker pair)

Acceptance criteria

  • JavaList<TInterface, TInvoker> (+ JavaCollection, JavaDictionary variants)
  • Interface-peer JavaPeerContainerFactory produces the two-parameter containers (and GetContainerFactory() no longer returns null for interfaces)
  • A NativeAOT test that round-trips a Java collection of a binding interface element (e.g. IList<ISomeListener>) with no MakeGenericType / reflection on the interface
  • CoreCLR behavior unchanged

Context / references

  • Follow-up to [TrimmableTypeMap] Fix interface-peer proxy generation for NativeAOT #11769 (interface-peer proxy generation for NativeAOT).
  • Relevant code: src/Mono.Android/Java.Interop/JavaPeerProxy.cs, src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs, src/Mono.Android/Java.Interop/JavaConvert.cs (TryGetFactoryBasedConverter), src/Mono.Android/Android.Runtime/JavaList.cs / JavaCollection.cs / JavaDictionary.cs, src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs (GetContainerFactory).

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageIssues that need to be assigned.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions