Background
The trimmable typemap currently represents a generic Java peer with one proxy for its open generic definition. On CoreCLR, when the caller supplies a closed targetType, TrimmableTypeMap.CreateInstance() can construct it through reflection. NativeAOT uses CreateInstanceWithoutReflectionFallback(), so the generated open-generic proxy reaches the current unsupported path instead.
For example, given a peer such as:
public partial class GenericPeer<T> : Java.Lang.Object
{
protected GenericPeer (IntPtr handle, JniHandleOwnership transfer)
: base (handle, transfer)
{
}
}
the typemap companion conceptually contains one proxy for the open generic definition:
sealed class GenericPeer_1_Proxy : JavaPeerProxy
{
public override Type TargetType => typeof (GenericPeer<>);
public override IJavaPeerable CreateInstance (IntPtr handle, JniHandleOwnership transfer)
=> throw new NotSupportedException ("Cannot create instance of open generic type.");
}
CoreCLR handles a supplied closed target such as typeof (GenericPeer<string>) using the equivalent of:
var ctor = closedType.GetConstructor (
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
binder: null,
[typeof (IntPtr), typeof (JniHandleOwnership)],
modifiers: null);
return (IJavaPeerable?) ctor?.Invoke ([handle, transfer]);
That fallback is intentionally unavailable to NativeAOT.
macOS precedent
The dotnet/macios registrar solves the equivalent construction problem by adding a static interface factory implementation to each eligible type. Its generated code is conceptually:
interface INSObjectFactory
{
static virtual NSObject? _Xamarin_ConstructNSObject (NativeHandle handle) => null;
}
public partial class GenericObject<T> : NSObject, INSObjectFactory
{
public static NSObject? _Xamarin_ConstructNSObject (NativeHandle handle)
=> new GenericObject<T> (handle);
}
A generic runtime helper can then make a statically resolved call:
static T? ConstructViaFactory<T> (NativeHandle handle)
where T : NSObject, INSObjectFactory
=> T._Xamarin_ConstructNSObject (handle) as T;
This works because the registrar modifies the generic user type. Android's typemap generator currently emits only into a separate companion assembly, so it cannot add the required interface and static factory method to an already compiled type.
Possible Android-generated shape
Explore whether the Android source/binding generator can emit the factory implementation while it is still generating the generic peer. The contract must be non-generic:
internal interface IJavaPeerActivationFactory
{
static abstract IJavaPeerable Create (IntPtr handle, JniHandleOwnership transfer);
}
The generated generic peer implements that contract and returns the non-generic interface type:
public partial class GenericPeer<T> : Java.Lang.Object, IJavaPeerActivationFactory
{
public static IJavaPeerable Create (IntPtr handle, JniHandleOwnership transfer)
=> new GenericPeer<T> (handle, transfer);
}
A constrained generic runtime helper can then make a statically resolved call and cast the result back to the same T:
static T? Create<T> (IntPtr handle, JniHandleOwnership transfer)
where T : class, IJavaPeerable, IJavaPeerActivationFactory
=> T.Create (handle, transfer) as T;
The interface cannot be generic. The closed peer type is the helper generic argument T; T.Create (handle, transfer) as T is what preserves the closed generic instantiation for NativeAOT. The implementation must be generated as part of the generic peer rather than emitted later into the typemap companion, because the companion cannot add the required static interface implementation to T.
This is illustrative, not a proposed final API. The remaining design problem is that today’s activation path has a Type targetType, not a generic T. The investigation must identify where the generic call site can be preserved or generated; resolving a factory from Type with MakeGenericType() would recreate the NativeAOT problem.
Questions to answer
- Which generic peers have a meaningful closed-type activation path, given Java type erasure?
- Where can the current
Type targetType activation path retain or recover a statically known TPeer?
- Can generated factories preserve the concrete generic arguments supplied by the managed caller without
MakeGenericType()?
- Should this be limited to source-generated binding types, or can user-defined generic peers participate?
- What contract should connect the generated static factory implementation to the typemap and runtime activation paths?
- Can the design preserve the current invariant that typemap-generated code never mutates user assemblies?
- How should unsupported open-generic activation continue to fail?
Acceptance criteria for an eventual implementation
- A closed generic Java peer that can be activated under CoreCLR can also be activated under NativeAOT without reflection.
- Open-generic activation remains explicitly unsupported.
- Generated code remains trim-safe and does not require runtime
MakeGenericType(), ConstructorInfo.Invoke(), or Activator.CreateInstance().
- CoreCLR and NativeAOT device tests cover the same closed-generic scenario.
- Existing non-generic and interface/invoker activation paths are unchanged.
Related implementation points:
src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
dotnet/macios/src/Foundation/NSObject2.cs
dotnet/macios/src/ObjCRuntime/Runtime.cs
dotnet/macios/tools/dotnet-linker/AppBundleRewriter.cs
Background
The trimmable typemap currently represents a generic Java peer with one proxy for its open generic definition. On CoreCLR, when the caller supplies a closed
targetType,TrimmableTypeMap.CreateInstance()can construct it through reflection. NativeAOT usesCreateInstanceWithoutReflectionFallback(), so the generated open-generic proxy reaches the current unsupported path instead.For example, given a peer such as:
the typemap companion conceptually contains one proxy for the open generic definition:
CoreCLR handles a supplied closed target such as
typeof (GenericPeer<string>)using the equivalent of:That fallback is intentionally unavailable to NativeAOT.
macOS precedent
The dotnet/macios registrar solves the equivalent construction problem by adding a static interface factory implementation to each eligible type. Its generated code is conceptually:
A generic runtime helper can then make a statically resolved call:
This works because the registrar modifies the generic user type. Android's typemap generator currently emits only into a separate companion assembly, so it cannot add the required interface and static factory method to an already compiled type.
Possible Android-generated shape
Explore whether the Android source/binding generator can emit the factory implementation while it is still generating the generic peer. The contract must be non-generic:
The generated generic peer implements that contract and returns the non-generic interface type:
A constrained generic runtime helper can then make a statically resolved call and cast the result back to the same
T:The interface cannot be generic. The closed peer type is the helper generic argument
T;T.Create (handle, transfer) as Tis what preserves the closed generic instantiation for NativeAOT. The implementation must be generated as part of the generic peer rather than emitted later into the typemap companion, because the companion cannot add the required static interface implementation toT.This is illustrative, not a proposed final API. The remaining design problem is that today’s activation path has a
Type targetType, not a genericT. The investigation must identify where the generic call site can be preserved or generated; resolving a factory fromTypewithMakeGenericType()would recreate the NativeAOT problem.Questions to answer
Type targetTypeactivation path retain or recover a statically knownTPeer?MakeGenericType()?Acceptance criteria for an eventual implementation
MakeGenericType(),ConstructorInfo.Invoke(), orActivator.CreateInstance().Related implementation points:
src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cssrc/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.csdotnet/macios/src/Foundation/NSObject2.csdotnet/macios/src/ObjCRuntime/Runtime.csdotnet/macios/tools/dotnet-linker/AppBundleRewriter.cs