dotnet / dotnet/android

Investigate eliminating generated ThresholdType and ThresholdClass overrides

Open
#12,679 0 comments 0 reactions 0 assignees View on GitHub
Area: Mono.Android
Dominant language
C#
Stars
2.1k
Forks
579
Avg merge
1d 19h
Merged PRs (30d)
252

Description

### Android framework version

net11.0-android (Preview) / .NET 12 investigation

### Affected platform version

`dotnet/android` main; generated `Mono.Android.dll` and generated Java binding assemblies.

### Description

The XAJavaInterop1 generator currently emits these overrides on nearly every generated bound class and invoker:

```csharp
[DebuggerBrowsable (DebuggerBrowsableState.Never)]
[EditorBrowsable (EditorBrowsableState.Never)]
protected override IntPtr ThresholdClass
=> _members.JniPeerType.PeerReference.Handle;

[DebuggerBrowsable (DebuggerBrowsableState.Never)]
[EditorBrowsable (EditorBrowsableState.Never)]
protected override Type ThresholdType
=> _members.ManagedPeerType;
```

API 37 `Mono.Android.dll` contains approximately:

- 6,776 generated `ThresholdClass` overrides;
- 7,447 generated `ThresholdType` overrides;
- 28,446 associated `DebuggerBrowsable`/`EditorBrowsable` attribute applications;
- roughly 14,000 property rows, getter methods, method-semantics rows, bodies, and related metadata entries.

This appears to be an opportunity of more than 1 MB in the untrimmed framework assembly, but removal must not be attempted without understanding the legacy dispatch contract.

`XAPeerMembers` currently uses the properties to decide virtual versus nonvirtual managed-to-Java dispatch:

```csharp
protected override bool UsesVirtualDispatch (IJavaPeerable value, Type? declaringType)
{
var peerType = GetThresholdType (value);
if (peerType != null)
return peerType == value.GetType ();

return base.UsesVirtualDispatch (value, declaringType);
}
```

It also uses `ThresholdClass` when selecting peer members for nonvirtual dispatch. This historically prevents incorrect recursion or dispatch when managed subclasses override Java virtual methods.

At first glance, modern generated types already expose equivalent information through:

```csharp
value.JniPeerMembers.ManagedPeerType
value.JniPeerMembers.JniPeerType
```

The base `JniPeerMembers.UsesVirtualDispatch()` implementation already compares `value.GetType()` with `value.JniPeerMembers.ManagedPeerType`. We should investigate whether new-format binding assemblies can rely on this information and stop generating the per-type threshold overrides.

This must be considered separately from legacy binding compatibility. Old binding assemblies may:

- contain generated threshold overrides;
- contain hand-written threshold overrides;
- depend on `XAPeerMembers` honoring those values;
- derive from framework binding types across assembly boundaries; or
- use older generator/runtime dispatch assumptions.

A possible compatibility model is:

1. Keep the protected virtual `ThresholdType` and `ThresholdClass` members on `Java.Lang.Object`/`Throwable` so old binaries remain loadable.
2. Stop emitting overrides in newly generated/version-marked bindings.
3. Use `JniPeerMembers.ManagedPeerType` and `JniPeerType` for modern bindings.
4. Detect and continue honoring threshold overrides from legacy binding assemblies, potentially keyed by an assembly/generator-format marker or a cached override-shape check.

The investigation should determine whether this split is correct or whether there are dispatch cases where the explicit threshold values carry information not available through `JniPeerMembers`.

Related size work: #12670.

### Steps to Reproduce

1. Generate `Mono.Android.dll` for API 37 with XAJavaInterop1.
2. Count generated `ThresholdType` and `ThresholdClass` overrides in `obj/.../mcw/*.cs` or the resulting assembly.
3. Trace `XAPeerMembers.UsesVirtualDispatch()` and `GetPeerMembers()` through exact binding types, managed subclasses, Java subclasses, and invoker types.
4. Prototype omitting the generated overrides for a version-marked modern binding format while preserving the base virtual properties.
5. Compare behavior against legacy binding assemblies built by older generators.

The prototype should cover at least:

- an exact generated binding type;
- a managed subclass overriding a Java virtual method;
- a Java-derived runtime type;
- abstract classes and interface invokers;
- cross-assembly inheritance;
- an old binding assembly containing generated threshold overrides;
- a legacy binding with a hand-written/custom threshold override;
- virtual and explicit nonvirtual calls;
- MonoVM, CoreCLR, and NativeAOT/trimmable-typemap configurations where applicable; and
- recursion prevention and correct Java method selection.

Acceptance criteria for an eventual implementation:

- New-format generated bindings omit `ThresholdType`/`ThresholdClass` overrides when they carry no additional information.
- Managed-subclass and Java virtual-dispatch behavior remains unchanged.
- Legacy binding assemblies continue to load and preserve their threshold semantics.
- The protected base API remains binary compatible unless a separate breaking-change decision is made.
- Tests prove that no managed override recursion or wrong JNI nonvirtual class selection is introduced.
- Before/after `Mono.Android.dll` metadata, method, property, attribute, and final trimmed-app sizes are recorded.

### Did you find any workaround?

No workaround is needed for correctness. The current generated properties preserve established behavior but impose repeated assembly-size and metadata costs.

### Relevant log output

```shell
API 37 generated counts:
ThresholdClass overrides: 6,776
ThresholdType overrides: 7,447

Current runtime consumers:
- Android.Runtime.XAPeerMembers.UsesVirtualDispatch()
- Android.Runtime.XAPeerMembers.GetPeerMembers()
- Java.Lang.Object.GetThresholdType()/GetThresholdClass()
- Java.Lang.Throwable.GetThresholdType()/GetThresholdClass()
```

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with generated files under obj/.../mcw/*.cs and trace Android.Runtime.XAPeerMembers.UsesVirtualDispatch(), GetPeerMembers(), and Java.Lang.Object/Throwable threshold accessors. Compare exact, subclass, invoker, cross-assembly, and legacy binding cases across the listed runtimes. Done means establishing whether modern overrides can be omitted without changing dispatch, preserving legacy semantics, and recording the required metadata and size comparisons.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
mobile
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.