Skip to content

[Java.Interop] Remove support for experimental JavaInterop1 codegen #11727

Description

@simonrozsival

Goal

Remove the experimental JavaInterop1 code-generation target and its runtime support. XAJavaInterop1 should be the only supported legacy Android binding/JCW target. The trimmable typemap Java generator remains a separate, targetless pipeline and should not be renamed to another AndroidCodegenTarget value.

Important scope distinction

There are two independent JavaInterop1 switches:

  1. CodeGenerationTarget.JavaInterop1 controls the Java.Interop generator's generated C# binding shape.
  2. JavaPeerStyle.JavaInterop1 controls Java Callable Wrapper (JCW) generation.

The trimmable typemap does not consume either enum. It generates its own Java sources and registers natives through TrimmableTypeMap.RegisterNativeMethods() and mono.android.Runtime.registerNatives(Class).

Current state and prerequisite

PR #12133:

  • switches NativeAOT from JavaPeerStyle.JavaInterop1 to XAJavaInterop1;
  • makes the shared _AndroidJcwCodegenTarget=XAJavaInterop1 default apply to NativeAOT;
  • rejects XamarinAndroid and JavaInterop1 through _AndroidJcwCodegenTarget;
  • removes the NativeAOT-only net.dot.jni.GCUserPeerable bridge path.

After that PR lands, no supported dotnet/android build path should generate JavaInterop1-style JCWs. Java.Interop's standalone generator, JCW emitter, SDK, tests, and expected-output baselines will still contain the legacy implementation and need separate cleanup.

Findings

The direct textual footprint is 123 JavaInterop1 references across 55 files, almost entirely under external/Java.Interop. Including target-specific expected output and conditional test scaffolding, the logical footprint is about 351 files:

  • 305 Java.Interop test/support/baseline files;
  • 40 production generator files;
  • 3 JCW library files;
  • 1 Java.Interop SDK file;
  • 1 outer Android build default;
  • 1 documentation file.

ManagedPeer

JavaPeerStyle.JavaInterop1 JCWs use:

net.dot.jni.ManagedPeer.registerNativeMembers(...)
net.dot.jni.ManagedPeer.construct(...)

XAJavaInterop1 JCWs instead use the mono.android.Runtime registration/activation path. Once PR #12133 lands and the standalone JavaInterop1 JCW emitter is disabled, ManagedPeer is no longer needed as a general JCW bridge.

ManagedPeer also currently bootstraps two built-in Java.Interop proxy classes:

  • JavaProxyObject calls ManagedPeer.registerNativeMembers() to register native equals, hashCode, and toString callbacks.
  • JavaProxyThrowable calls ManagedPeer.registerNativeMembers() with an empty method string, but its managed type has no native-registration callback. The call registers nothing and can be removed without replacement.

The trimmable typemap's TrimmableJavaProxyObject is not a replacement for the normal Java.Interop proxy contract. It intentionally uses Java identity semantics and has no native methods. The normal JavaProxyObject delegates equality, hash code, and string conversion to the wrapped managed object; that behavior should remain for standalone/non-trimmable Java.Interop.

Using an Android [Register] class and app JCW generation for core Java.Interop is not a good fit:

  • Java.Interop is standalone/JVM-agnostic and builds java-interop.jar itself.
  • The core Java.Interop project does not include the Android RegisterAttribute.
  • Depending on the dotnet/android JCW pipeline would introduce an Android-specific/bootstrap dependency.

The smallest behavior-preserving replacement is for managed JavaProxyObject to register its three native methods lazily, immediately before constructing the first proxy instance, while retaining the existing handwritten Java class. The Java static initializer that calls ManagedPeer can then be removed. The trimmable identity proxy remains separate.

Revised implementation plan

1. Land the supported-consumer migration

2. Decouple built-in proxies from ManagedPeer

  • Remove the ManagedPeer.registerNativeMembers() static initializer from Java JavaProxyObject.
  • Add direct managed registration of equals, hashCode, and toString immediately before the first proxy allocation.
  • Preserve delegate lifetime through the existing JniType.RegisterNativeMethods() mechanism.
  • Remove the no-op ManagedPeer.registerNativeMembers() static initializer from Java JavaProxyThrowable.
  • Keep the managed JavaProxyObject and JavaProxyThrowable marshalling types.
  • Keep the trimmable identity proxy implementation separate.

3. Remove remaining ManagedPeer consumers

  • Remove or reject JavaPeerStyle.JavaInterop1 in the standalone JCW generator.
  • Remove/update JCW expected-output tests that emit ManagedPeer.registerNativeMembers() or ManagedPeer.construct().
  • Port or remove hand-authored Java.Interop tests that call ManagedPeer.construct().
  • Verify no generated or checked-in Java source references net/dot/jni/ManagedPeer.

This phase can happen before removing the much larger CodeGenerationTarget.JavaInterop1 C# generator implementation.

4. Delete ManagedPeer and its compatibility plumbing

  • Delete managed Java.Interop.ManagedPeer.
  • Delete net.dot.jni.ManagedPeer.java and remove it from java-interop.jar.
  • Delete Java.Interop.RuntimeFeature.ManagedPeerNativeRegistration.
  • Remove ManagedPeer.Init() and built-in type-manager mappings.
  • Remove the LLVM typemap runtimeconfig switch that enables ManagedPeerNativeRegistration.
  • Remove the ProGuard keep rule for net.dot.jni.ManagedPeer.
  • Remove the trimmable scanner's special exclusion for Java.Interop.ManagedPeer.
  • Remove ManagedPeer entries from the AOT profile.
  • Update Android test fixtures/comments that describe the old compatibility split.

JniRuntime.CreationOptions.JniAddNativeMethodRegistrationAttributePresent is broader than ManagedPeer and should remain.

5. Remove CodeGenerationTarget.JavaInterop1

  • Remove the enum value and javainterop1 CLI option.
  • Remove Java.Interop SDK/build configuration that selects it.
  • Collapse generator/object-model/source-writer branches to the remaining XAJavaInterop1 behavior.
  • Remove JAVA_INTEROP1, TODO_JAVA_INTEROP1, TryJavaInterop1, and related conditional test support.
  • Remove out.ji, expected.ji, and CodeGeneratorExpectedResults/JavaInterop1 baselines.
  • Remove stale documentation describing JavaInterop1.

Compatibility decision

Do not retain a runtime ManagedPeer compatibility shim. Once JavaInterop1 is rejected and removed, previously generated or handwritten Java bytecode that references ManagedPeer is unsupported. Issue #11635 can be resolved in that direction; optional build-time detection may provide a clearer diagnostic, but should not keep the runtime bridge.

Validation

  • Build and run the Java.Interop runtime, generator, and JCW test suites.
  • Add/retain focused coverage that normal JavaProxyObject Java calls delegate equals, hashCode, and toString to the wrapped managed object.
  • Retain exception-transition coverage for JavaProxyThrowable.
  • Run dotnet/android build tests that reject legacy JCW targets.
  • Run representative non-trimmable CoreCLR/MonoVM device tests.
  • Run trimmable CoreCLR and NativeAOT build/device tests.
  • Inspect produced jars/APKs to confirm net/dot/jni/ManagedPeer.class is absent.
  • Confirm repository searches contain no ManagedPeerNativeRegistration, ManagedPeer.construct, or ManagedPeer.registerNativeMembers references.

Completion criteria

  • XAJavaInterop1 is the only supported AndroidCodegenTarget/legacy JCW shape.
  • The trimmable typemap remains an independent Java-generation path.
  • ManagedPeer and ManagedPeerNativeRegistration no longer exist.
  • Built-in proxy marshalling continues to work without ManagedPeer.
  • CodeGenerationTarget.JavaInterop1, JavaPeerStyle.JavaInterop1, and all associated baselines/scaffolding are removed.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions