Skip to content

Investigate eliminating generated ThresholdType and ThresholdClass overrides #12679

Description

@simonrozsival

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:

[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:

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:

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

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()

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: Mono.AndroidIssues with the Android API binding (Mono.Android.dll).

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions