Skip to content

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

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

[NativeAOT] Use XAJavaInterop1 codegen target and unify GC bridge#12133
simonrozsival wants to merge 2 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), the GC bridge processing becomes identical for CoreCLR and NativeAOT.

⚠️ Depends on #12121 — keep as draft until it merges, then rebase on main.
This PR assumes NativeAOT uses the trimmable typemap (the default introduced by #12121). See "Why this depends on the trimmable typemap" below — it does not work on the managed typemap.

Changes

  • Codegen target: Remove the NativeAOT-specific _AndroidJcwCodegenTarget=JavaInterop1 default. NativeAOT now falls through to the shared XAJavaInterop1 default in Microsoft.Android.Sdk.DefaultProperties.targets.
  • Native GC bridge: Delete the NativeAOT-only BridgeProcessing override that called GCUserPeerable.jiAddManagedReference/jiClearManagedReferences. 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.
  • Bridge cleanup: With both runtimes on the same IGCUserPeer path, the maybe_call_gc_user_peerable_* virtual hooks are always no-ops, so BridgeProcessingShared is collapsed into a single concrete BridgeProcessing.

Why this depends on the trimmable typemap (#12121)

Flipping the JCW codegen target changes how the generated Java Callable Wrappers register their native methods at runtime — their static initializers now call into mono.android.Runtime instead of net.dot.jni.ManagedPeer. The registration mechanism that backs this differs by typemap:

  • managed typemap (old NativeAOT default): JCWs would rely on Java_mono_android_Runtime_register, a name-linked native symbol implemented only in the CoreCLR/MonoVM hosts (src/native/clr/host/host-jni.cc, host.cc) and wired through registerJniNativesFn (set only in JNIEnvInit.Initialize). NativeAOT implements none of this (src/native/nativeaot/host/host-jni.cc has no register, InitializeNativeAotRuntime never sets registerJniNativesFn, and libnaot-android.map.txt exports nothing) → UnsatisfiedLinkError.
  • trimmable typemap (NativeAOT default after [NativeAOT] Make the trimmable typemap the default #12121): Provides its own AOT-safe registration. TrimmableTypeMap.RegisterNativeMethods() dynamically registers mono.android.Runtime.registerNatives(Class) via JniEnvironment.Types.RegisterNatives pointing at the managed [UnmanagedCallersOnly] OnRegisterNatives (src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs:133-149,561-587) — no name-linked JNI symbol required. TrimmableTypeMapTypeManager.RegisterNativeMembers even asserts that natives are registered by the JCW static initializers.

So this migration only works when NativeAOT is on the trimmable typemap. #12121 makes that the default; this PR builds on it.

Follow-up (not in this PR)

Once this migration is verified on the trimmable typemap, drop the now-unused JavaInterop1 codegen + runtime support: ManagedPeer, JavaProxyObject/JavaProxyThrowable, net.dot.jni.GCUserPeerable, and related plumbing.

Verification

Draft until #12121 merges and this branch is rebased on main. Then: make all + NativeAOT sample / Mono.Android device tests on the trimmable typemap.

🤖 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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant