Explore generated factories for NativeAOT activation of closed generic Java peers
- Dominant language
- C#
- Stars
- 2.1k
- Forks
- 579
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 252
Description
## 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:
```csharp
public partial class GenericPeer : Java.Lang.Object
{
protected GenericPeer (IntPtr handle, JniHandleOwnership transfer)
: base (handle, transfer)
{
}
}
```
the typemap companion conceptually contains one proxy for the open generic definition:
```csharp
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)` using the equivalent of:
```csharp
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:
```csharp
interface INSObjectFactory
{
static virtual NSObject? _Xamarin_ConstructNSObject (NativeHandle handle) => null;
}
public partial class GenericObject : NSObject, INSObjectFactory
{
public static NSObject? _Xamarin_ConstructNSObject (NativeHandle handle)
=> new GenericObject (handle);
}
```
A generic runtime helper can then make a statically resolved call:
```csharp
static T? ConstructViaFactory (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:
```csharp
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:
```csharp
public partial class GenericPeer : Java.Lang.Object, IJavaPeerActivationFactory
{
public static IJavaPeerable Create (IntPtr handle, JniHandleOwnership transfer)
=> new GenericPeer (handle, transfer);
}
```
A constrained generic runtime helper can then make a statically resolved call and cast the result back to the same `T`:
```csharp
static T? Create (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`
Contributor guide
No contributing guide indexed for this repository
Research direction
Read src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs and Generator/TypeMapAssemblyEmitter.cs to trace the current Type-based activation path. Compare the macOS precedent in NSObject2.cs, Runtime.cs, and AppBundleRewriter.cs, then determine whether a statically known closed peer can be preserved without mutating user assemblies or using reflection. Done means a documented factory contract, scope, failure behavior, and CoreCLR/NativeAOT test plan.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, java
- Domain
- backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100