dotnet / dotnet/android

[Java.Interop] Remove support for experimental `JavaInterop1` codegen

Open
#11,727 0 comments 0 reactions 0 assignees View on GitHub
drop-javainterop1
Dominant language
C#
Stars
2.1k
Forks
579
Avg merge
1d 21h
Merged PRs (30d)
257

Description

## 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 https://github.com/dotnet/android/pull/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:

```java
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

- [ ] Land PR #12133.
- [ ] Confirm NativeAOT, CoreCLR, and MonoVM all select `XAJavaInterop1` for the legacy JCW pipeline.
- [ ] Keep the trimmable typemap generator independent of `_AndroidJcwCodegenTarget`.

### 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 https://github.com/dotnet/android/issues/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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reviewing PR #12133 and the JavaProxyObject, JavaProxyThrowable, CodeGenerationTarget.JavaInterop1, and JavaPeerStyle.JavaInterop1 entry points described in the issue. Then inspect the Java.Interop runtime, generator, and JCW test suites plus their expected-output baselines. Done means JavaInterop1 and ManagedPeer support are removed while proxy behavior, trimmable typemap independence, and the listed Android validation tests remain passing.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, csharp, java
Domain
build-system, mobile-dev, tooling
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.