Skip to content

[NativeAOT] Use XAJavaInterop1 codegen target and unify GC bridge#12133

Draft
simonrozsival wants to merge 8 commits into
mainfrom
android-nativeaot-use-xajavainterop1-generator
Draft

[NativeAOT] Use XAJavaInterop1 codegen target and unify GC bridge#12133
simonrozsival wants to merge 8 commits into
mainfrom
android-nativeaot-use-xajavainterop1-generator

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Switches NativeAOT builds from the JavaInterop1 code-generation target to XAJavaInterop1, matching CoreCLR and MonoVM.

Because XAJavaInterop1 Java Callable Wrappers implement mono.android.IGCUserPeer instead of net.dot.jni.GCUserPeerable, CoreCLR and NativeAOT can use the same GC bridge processing path.

This also formalizes the legacy XamarinAndroid and JavaInterop1 targets as unsupported: builds now fail with XA4232 if either is selected through _AndroidJcwCodegenTarget, and the documentation identifies XAJavaInterop1 as the only supported code-generation target.

#12121 is merged and included in this branch, so the required NativeAOT trimmable typemap default is in place.

Changes

  • Codegen target: Remove the NativeAOT-specific _AndroidJcwCodegenTarget=JavaInterop1 default. NativeAOT now uses the shared XAJavaInterop1 default in Microsoft.Android.Sdk.DefaultProperties.targets.
  • Configuration validation: Fail early with XA4232 whenever _AndroidJcwCodegenTarget is explicitly set to XamarinAndroid or JavaInterop1, regardless of whether the build uses MonoVM, CoreCLR, or NativeAOT.
  • Documentation: Mark XamarinAndroid and JavaInterop1 as unsupported in the build-property and migration guidance, and update XA4232 documentation from the old deprecation warning to the new error.
  • Native GC bridge: Remove the NativeAOT-only BridgeProcessing implementation that called GCUserPeerable.jiAddManagedReference and jiClearManagedReferences. NativeAOT now uses the shared mono.android.IGCUserPeer path already used by CoreCLR.
  • Bridge cleanup: Collapse BridgeProcessingShared into a single concrete BridgeProcessing class and remove the now-unused virtual maybe_call_gc_user_peerable_* hooks.
  • APK size baseline: Update the NativeAOT release APK description for the generated-wrapper and native-library size changes.

Why the trimmable typemap is required

XAJavaInterop1 changes how generated Java Callable Wrappers register native methods:

  • managed typemap: JCWs use the name-linked Java_mono_android_Runtime_register path provided by the CoreCLR and MonoVM hosts. NativeAOT does not provide that JNI entry point.
  • trimmable typemap: TrimmableTypeMap.RegisterNativeMethods() dynamically registers mono.android.Runtime.registerNatives(Class) to a managed [UnmanagedCallersOnly] callback, so no name-linked JNI symbol is required.

The trimmable typemap is the NativeAOT default after #12121, so this PR builds on its AOT-safe registration path. Rejecting XamarinAndroid and JavaInterop1 as JCW code-generation targets across all runtimes also prevents builds from opting back into legacy JCW shapes that the unified bridge no longer supports.

Follow-up

Remove the now-unused JavaInterop1 codegen and runtime support (ManagedPeer, JavaProxyObject/JavaProxyThrowable, net.dot.jni.GCUserPeerable, and related plumbing) separately.

Verification

  • make prepare
  • make all
  • ./dotnet-local.sh build src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj -c Debug --no-restore
  • ./dotnet-local.sh test bin/TestDebug/net10.0/Xamarin.Android.Build.Tests.dll --filter "Name~UnsupportedJcwCodegenTargetIsRejected"

🤖 Generated with GitHub Copilot CLI

simonrozsival and others added 2 commits July 16, 2026 12:15
Switch NativeAOT builds from the JavaInterop1 code-generation target to
XAJavaInterop1, matching CoreCLR and MonoVM. As a result, the generated
Java Callable Wrappers now implement `mono.android.IGCUserPeer` instead
of `net.dot.jni.GCUserPeerable`, so the GC bridge processing becomes
identical for CoreCLR and NativeAOT.

  * Remove the NativeAOT-specific `_AndroidJcwCodegenTarget=JavaInterop1`
    default so NativeAOT falls through to the shared `XAJavaInterop1`
    default in `Microsoft.Android.Sdk.DefaultProperties.targets`.

  * Delete the NativeAOT-only `BridgeProcessing` override that called the
    `GCUserPeerable.jiAddManagedReference`/`jiClearManagedReferences`
    methods. NativeAOT already compiles the CoreCLR bridge sources and
    already runs `BridgeProcessingShared::initialize_on_runtime_init`
    (via `GCBridge::initialize_on_runtime_init`), which caches the
    `mono.android.IGCUserPeer`/`GCUserPeer` classes. Falling through to
    that shared path makes the native GC bridge identical to CoreCLR.

Dropping the now-unused JavaInterop1 codegen + runtime support
(ManagedPeer, JavaProxyObject/JavaProxyThrowable, GCUserPeerable) is a
follow-up.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8d949279-d51c-4ce0-baf2-0b1d8e919928
Now that both CoreCLR and NativeAOT use the same IGCUserPeer-based GC
bridge, the `maybe_call_gc_user_peerable_*` extension points always
return `false` and no host needs to override them. Remove the abstract
base / derived split:

  * Merge `BridgeProcessingShared` into a single concrete
    `BridgeProcessing` class (delete `bridge-processing-shared.hh`).
  * Drop the two pure-virtual `maybe_call_gc_user_peerable_*` hooks and
    the CoreCLR no-op overrides, and inline the IGCUserPeer path
    directly in `add_reference`/`clear_references`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8d949279-d51c-4ce0-baf2-0b1d8e919928

@simonrozsival simonrozsival left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Skeptical completeness review — this migration looks incomplete; NativeAOT apps will likely fail at runtime.

Verdict: ⚠️ Needs Changes

The two committed pieces are correct and clean:

  • ✅ Codegen flip to XAJavaInterop1.
  • ✅ GC bridge unification (NativeAOT now uses the shared IGCUserPeer path; collapsing BridgeProcessingSharedBridgeProcessing is a nice cleanup).

But flipping the JCW codegen target changes what the generated Java Callable Wrappers call at runtime, and NativeAOT doesn't implement the new registration entry point.

The missing piece: native-method registration

Under XAJavaInterop1, each JCW's static initializer emits mono.android.Runtime.register("<asm-qualified-name>", X.class, methods) (CallableWrapperType.cs:277-289). That is a native method on mono.android.Runtime (src/java-runtime/java/mono/android/Runtime.java).

  • CoreCLR/MonoVM implement it: Java_mono_android_Runtime_register (src/native/clr/host/host-jni.cc:23host.cc:488-507) forwards to the managed RegisterJniNatives callback stored in registerJniNativesFn, which is set only in the CoreCLR/Mono JNIEnvInit.Initialize path (JNIEnvInit.cs:156).
  • NativeAOT implements none of this:
    • src/native/nativeaot/host/host-jni.cc only exposes JNI_OnLoad/OnInit — no register.
    • JNIEnvInit.InitializeNativeAotRuntime never sets args->registerJniNativesFn (JNIEnvInit.cs:104-118).
    • src/native/nativeaot/libnaot-android.map.txt exports nothing (global: is empty).

Today NativeAOT (JavaInterop1) registers natives via net.dot.jni.ManagedPeer.registerNativeMembers — the very class we intend to stop using. After this flip, JCWs stop calling ManagedPeer and start calling mono.android.Runtime.register, which is unresolved on NativeAOT → UnsatisfiedLinkError when the first JCW class loads.

What still needs to be done

  1. Provide a NativeAOT implementation of mono.android.Runtime.register (e.g. an [UnmanagedCallersOnly(EntryPoint="Java_mono_android_Runtime_register")] export, or dynamic RegisterNatives) that routes to RegisterJniNatives.
  2. Set registerJniNativesFn in InitializeNativeAotRuntime (currently only the CoreCLR/Mono path sets it).
  3. Confirm mono.android.TypeManager.Activate/n_activate (used by XAJavaInterop1 ctors, CallableWrapperConstructor.cs:48-55) is actually resolvable on NativeAOT — same registration question.
  4. Sanity-check any other mono.android.Runtime/TypeManager natives the JCWs/runtime now touch, and export them in the NativeAOT version script if name-based JNI linking is used.

CI caveat

The green Android Tools Tests lane only covers MSBuild/tooling. There is likely no NativeAOT device/runtime lane in PR CI, so a green-ish CI here does not validate that apps run. This needs the local device test (make all + the NativeAOT sample / Mono.Android tests) once the registration path exists.

Good news: ManagedPeer/GCUserPeerable/JavaProxyObject removal genuinely does stay a follow-up — but only after the registration path above is in place.

@simonrozsival

Copy link
Copy Markdown
Member Author

🤖 Follow-up / resolution to my registration concern above.

The missing mono.android.Runtime.register entry point is only a problem on the managed typemap. This PR assumes NativeAOT is on the trimmable typemap (default as of #12121), which has its own AOT-safe registration and therefore does not need the CoreCLR-style name-linked register symbol:

  • TrimmableTypeMap.RegisterNativeMethods() dynamically binds mono.android.Runtime.registerNatives(Class) via JniEnvironment.Types.RegisterNatives → managed [UnmanagedCallersOnly] OnRegisterNatives (src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs:133-149, 561-587). No name-based JNI linking, so it works under NativeAOT.
  • TrimmableTypeMapTypeManager.RegisterNativeMembers explicitly documents that natives are registered by the JCW static-initializer blocks (TrimmableTypeMapTypeManager.cs:404-413).
  • It runs on the NativeAOT init path too: InitializeNativeAotRuntimeRegisterTrimmableTypeMapNativeMethodsIfNeededTrimmableTypeMap.RegisterNativeMethods (JNIEnvInit.cs:115, 245-250).

Action taken: documented the dependency in the PR description and kept this PR as draft. It must be rebased on main after #12121 merges (which flips the NativeAOT default to trimmable). No mono.android.Runtime.register implementation for the managed typemap is needed here.

Net: no code change required in this PR for registration — the inline concern is resolved by the #12121 dependency, not by adding a managed-typemap registration path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 70f63eb7-6599-414c-a947-d860705aa0fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 70f63eb7-6599-414c-a947-d860705aa0fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f9b06128-4d84-4113-87e9-558e6561f6d0
@simonrozsival
simonrozsival marked this pull request as ready for review July 17, 2026 08:31
Copilot AI review requested due to automatic review settings July 17, 2026 08:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR switches NativeAOT to use the shared XAJavaInterop1 Java Callable Wrapper (JCW) codegen target (aligning with CoreCLR/MonoVM), and removes the NativeAOT-only GC bridge processing path so NativeAOT and CoreCLR can share the same mono.android.IGCUserPeer-based bridge processing implementation.

Changes:

  • Remove the NativeAOT default override forcing _AndroidJcwCodegenTarget=JavaInterop1, allowing NativeAOT to use the shared XAJavaInterop1 default.
  • Delete the NativeAOT-specific GC bridge processing implementation and unify bridge processing into a single BridgeProcessing class used by both CoreCLR and NativeAOT.
  • Update the NativeAOT release APK size baseline to reflect wrapper/native-library size changes.
Show a summary per file
File Description
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc Updates the NativeAOT Release APK size baseline to match new generated output.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets Removes the NativeAOT-specific JCW codegen default override so NativeAOT follows the shared XAJavaInterop1 default.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets Applies _AndroidJcwCodegenTarget=XAJavaInterop1 default for all runtimes (including NativeAOT).
src/native/nativeaot/include/host/bridge-processing.hh Removes the NativeAOT-only bridge-processing header.
src/native/nativeaot/host/bridge-processing.cc Removes the NativeAOT-only GCUserPeerable-based bridge-processing implementation.
src/native/nativeaot/host/CMakeLists.txt Stops compiling the removed NativeAOT bridge-processing source; continues using CoreCLR shared sources.
src/native/nativeaot/host/host.cc Removes NativeAOT-only bridge-processing init call; relies on shared GCBridge initialization path.
src/native/clr/include/host/bridge-processing.hh Collapses the shared/derived bridge-processing structure into a single concrete BridgeProcessing definition.
src/native/clr/include/host/bridge-processing-shared.hh Removes the now-unused shared base class header.
src/native/clr/host/bridge-processing.cc Renames implementations from BridgeProcessingShared to BridgeProcessing and removes GCUserPeerable hook points.

Copilot's findings

  • Files reviewed: 10/10 changed files
  • Comments generated: 1

Fail early with XA4232 whenever _AndroidJcwCodegenTarget is set to JavaInterop1, regardless of the selected runtime. Reuse the existing obsolete-codegen diagnostic and cover MonoVM, CoreCLR, and NativeAOT.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f9b06128-4d84-4113-87e9-558e6561f6d0
Update the Android code generation property and migration guidance to identify XAJavaInterop1 as the only supported target. Refresh XA4232 documentation for the new _AndroidJcwCodegenTarget error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f9b06128-4d84-4113-87e9-558e6561f6d0
Extend XA4232 to reject both XamarinAndroid and JavaInterop1 when selected through _AndroidJcwCodegenTarget. Update the diagnostic documentation and cover both values across MonoVM, CoreCLR, and NativeAOT.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f9b06128-4d84-4113-87e9-558e6561f6d0
@simonrozsival simonrozsival added drop-libcpp Work to remove the libc++ dependency from Android NativeAOT drop-javainterop1 and removed drop-libcpp Work to remove the libc++ dependency from Android NativeAOT labels Jul 17, 2026
@simonrozsival
simonrozsival marked this pull request as draft July 17, 2026 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants