dotnet / dotnet/android

Explore generated factories for NativeAOT activation of closed generic Java peers

Open
#12,563 1 comment 0 reactions 0 assignees View on GitHub
needs-triage
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.