[NativeAOT] Use XAJavaInterop1 codegen target and unify GC bridge#12133
[NativeAOT] Use XAJavaInterop1 codegen target and unify GC bridge#12133simonrozsival wants to merge 8 commits into
Conversation
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
left a comment
There was a problem hiding this comment.
🤖 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
IGCUserPeerpath; collapsingBridgeProcessingShared→BridgeProcessingis 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:23→host.cc:488-507) forwards to the managedRegisterJniNativescallback stored inregisterJniNativesFn, which is set only in the CoreCLR/MonoJNIEnvInit.Initializepath (JNIEnvInit.cs:156). - NativeAOT implements none of this:
src/native/nativeaot/host/host-jni.cconly exposesJNI_OnLoad/OnInit— noregister.JNIEnvInit.InitializeNativeAotRuntimenever setsargs->registerJniNativesFn(JNIEnvInit.cs:104-118).src/native/nativeaot/libnaot-android.map.txtexports 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
- Provide a NativeAOT implementation of
mono.android.Runtime.register(e.g. an[UnmanagedCallersOnly(EntryPoint="Java_mono_android_Runtime_register")]export, or dynamicRegisterNatives) that routes toRegisterJniNatives. - Set
registerJniNativesFninInitializeNativeAotRuntime(currently only the CoreCLR/Mono path sets it). - Confirm
mono.android.TypeManager.Activate/n_activate(used by XAJavaInterop1 ctors,CallableWrapperConstructor.cs:48-55) is actually resolvable on NativeAOT — same registration question. - Sanity-check any other
mono.android.Runtime/TypeManagernatives 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.
|
🤖 Follow-up / resolution to my registration concern above. The missing
Action taken: documented the dependency in the PR description and kept this PR as draft. It must be rebased on 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
There was a problem hiding this comment.
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 sharedXAJavaInterop1default. - Delete the NativeAOT-specific GC bridge processing implementation and unify bridge processing into a single
BridgeProcessingclass 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
Summary
Switches NativeAOT builds from the JavaInterop1 code-generation target to XAJavaInterop1, matching CoreCLR and MonoVM.
Because XAJavaInterop1 Java Callable Wrappers implement
mono.android.IGCUserPeerinstead ofnet.dot.jni.GCUserPeerable, CoreCLR and NativeAOT can use the same GC bridge processing path.This also formalizes the legacy
XamarinAndroidandJavaInterop1targets as unsupported: builds now fail with XA4232 if either is selected through_AndroidJcwCodegenTarget, and the documentation identifiesXAJavaInterop1as 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
_AndroidJcwCodegenTarget=JavaInterop1default. NativeAOT now uses the sharedXAJavaInterop1default inMicrosoft.Android.Sdk.DefaultProperties.targets._AndroidJcwCodegenTargetis explicitly set toXamarinAndroidorJavaInterop1, regardless of whether the build uses MonoVM, CoreCLR, or NativeAOT.XamarinAndroidandJavaInterop1as unsupported in the build-property and migration guidance, and update XA4232 documentation from the old deprecation warning to the new error.BridgeProcessingimplementation that calledGCUserPeerable.jiAddManagedReferenceandjiClearManagedReferences. NativeAOT now uses the sharedmono.android.IGCUserPeerpath already used by CoreCLR.BridgeProcessingSharedinto a single concreteBridgeProcessingclass and remove the now-unused virtualmaybe_call_gc_user_peerable_*hooks.Why the trimmable typemap is required
XAJavaInterop1 changes how generated Java Callable Wrappers register native methods:
managedtypemap: JCWs use the name-linkedJava_mono_android_Runtime_registerpath provided by the CoreCLR and MonoVM hosts. NativeAOT does not provide that JNI entry point.trimmabletypemap:TrimmableTypeMap.RegisterNativeMethods()dynamically registersmono.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
XamarinAndroidandJavaInterop1as 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 preparemake 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