[runtime] Move JNI remapping lookup to managed code - #12796
simonrozsival wants to merge 20 commits into
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The stale NativeAOT source entry and missing managed lookup regression coverage must be addressed.
Get a fresh assessment by requesting another Copilot review.
Review tier: Lite
Findings: 1
Open (2)
What changed in this PR
This pull request moves JNI remapping lookups from native runtime code into managed code across Mono, CoreCLR, and NativeAOT.
Changes:
- Adds shared remapping metadata and managed UTF-8 binary-search lookups.
- Passes remapping data through runtime initialization.
- Removes obsolete native lookup implementations and P/Invoke registrations.
- Updates generators, runtime stubs, build files, and tests.
| File | Summary |
|---|---|
src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs |
Emits aggregate remapping data. |
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs |
Updates generated-symbol and ordering assertions. |
src/native/nativeaot/include/runtime-base/internal-pinvokes.hh |
Updates NativeAOT internal P/Invoke declarations. |
src/native/nativeaot/host/jni-remapping-tables-stub.cc |
Supplies NativeAOT remapping table stubs. |
src/native/nativeaot/host/host.cc |
Wires remapping data into the NativeAOT host. |
src/native/nativeaot/host/CMakeLists.txt |
Updates NativeAOT build sources. |
src/native/native.targets |
Updates runtime inputs; retains a stale deleted-header entry. |
src/native/mono/xamarin-app-stub/xamarin-app.hh |
Updates the Mono application interface. |
src/native/mono/xamarin-app-stub/application_dso_stub.cc |
Provides Mono remapping data. |
src/native/mono/runtime-base/internal-pinvokes.hh |
Updates Mono internal P/Invoke declarations. |
src/native/mono/pinvoke-override/pinvoke-tables.include |
Updates generated P/Invoke table entries. |
src/native/mono/pinvoke-override/generate-pinvoke-tables.cc |
Updates P/Invoke table generation. |
src/native/mono/monodroid/monodroid-glue.cc |
Wires remapping data during initialization. |
src/native/mono/monodroid/jni-remapping.hh |
Removes obsolete native remapping declarations. |
src/native/mono/monodroid/jni-remapping.cc |
Removes the native lookup implementation. |
src/native/mono/monodroid/internal-pinvokes.cc |
Removes obsolete native P/Invoke implementations. |
src/native/mono/monodroid/CMakeLists.txt |
Updates Mono build sources. |
src/native/common/include/managed-interface.hh |
Defines the shared remapping-data ABI. |
src/native/clr/xamarin-app-stub/application_dso_stub.cc |
Provides CoreCLR remapping data. |
src/native/clr/runtime-base/jni-remapping.cc |
Removes the CoreCLR native lookup implementation. |
src/native/clr/runtime-base/CMakeLists.txt |
Updates CoreCLR build sources. |
src/native/clr/pinvoke-override/precompiled.cc |
Updates P/Invoke override compilation. |
src/native/clr/include/xamarin-app.hh |
Updates the CoreCLR application interface. |
src/native/clr/include/runtime-base/jni-remapping.hh |
Removes the obsolete remapping header. |
src/native/clr/include/runtime-base/internal-pinvokes.hh |
Updates CoreCLR internal P/Invoke declarations. |
src/native/clr/host/internal-pinvokes-shared.cc |
Updates shared host P/Invoke wiring. |
src/native/clr/host/host.cc |
Wires shared remapping data into the host. |
src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs |
Implements managed remapping lookups; runtime coverage is still needed. |
src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs |
Removes obsolete P/Invoke declarations. |
src/Mono.Android/Android.Runtime/JNIEnvInit.cs |
Passes remapping data during initialization. |
05ff47f to
5c655e9
Compare
5c655e9 to
bc65226
Compare
477ca05 to
3203cc6
Compare
accf258 to
2e82c37
Compare
2e82c37 to
8a702d3
Compare
Carry stable UTF-8 replacement type and method-name pointers through remapping results and use them directly for JNI lookup while preserving string fallbacks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Document the unmanaged memory ownership and lifetime contract for remapping pointers, and exercise pointer-backed instance/static method lookups with signature fallback in the JVM tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Carry replacement type, method name, and signature pointers through JniPeerMembers and JNI lookup without eagerly decoding native memory. Preserve string-based compatibility paths and defer decoding to explicit diagnostics and Debug metadata access. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Require replacement pointers to remain stable for the associated runtime because cached JNI metadata may retain them, and use UTF-8-first precedence when formatting failed lookup diagnostics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The UTF-8-signature compatibility case relies on the test fixture's custom JniTypeManager, which Android does not support. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep the mixed representation coverage in the host type manager while registering the equivalent generated remap for Android test runs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
8a702d3 to
4b1489e
Compare
Return string and UTF-8 replacement representations from one virtual lookup so pointer-aware managers can report handled misses without triggering a duplicate string-table search. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Route string replacement queries through the combined lookup and make non-zero UTF-8 pointers authoritative when an override supplies both representations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Emit the existing Intune remapping tables as a self-describing data block, pass it through JNIEnv initialization, and share UTF-8 binary lookup across MonoVM and CoreCLR without per-call P/Invokes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use System.Text.Ascii for the common UTF-8/UTF-16 comparison path and a streaming Rune fallback for non-ASCII names. Keep replacement types and exact method signatures in generated native memory, avoid source metadata strings, and cache generator UTF-8 sort keys. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Exercise allocation-free UTF-8/UTF-16 comparison for a non-ASCII replacement type and method name through the CoreCLR device remapping path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Compare UTF-8 and UTF-16 ASCII names in vector-sized chunks so binary-search ordering rescans at most one chunk after Ascii.Equals reports a mismatch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The native assembler populates the JniRemappingData table pointers during code generation, so suppress CS0649 for those two fields just like the existing generated methods pointer. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Update the Xamarin.Forms CoreCLR R8 Release APK description from the matching failed CI test attachment. macOS and Windows produced the same package size; their assembly stores differed by only 16 bytes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use the generator UTF-8 sort-key cache when computing serialized string lengths so each distinct remapping string is encoded only once. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use Encoding.UTF8.GetByteCount for serialized lengths so signatures and other non-sort values are not retained in the sort-key cache. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Override the combined replacement-type lookup in both Android type managers so generated UTF-8 table misses do not fall through into a second compatibility string search. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
4b1489e to
2ab1f9c
Compare
## Motivation The existing Intune/MAM JNI method-remapping path stores target type and method names as stable NUL-terminated UTF-8 strings in generated native data. The old path materialized those names as managed UTF-16 strings and then encoded them back to UTF-8 for `FindClass` and `GetMethodID`. That round trip is unnecessary and becomes more important as remapping is reused by larger consumers such as R8. ## Approach - Add pointer-backed target type, method-name, and method-signature values to `ReplacementMethodInfo`. - Keep pointer and string representations independent so reading a compatibility string property does not implicitly decode native memory. - Add `JniType` lookup paths for pointer/pointer and mixed pointer/span member names and signatures. - Let `JniPeerMembers` retain a replacement type pointer and use it directly for `FindClass` and later member-remapping lookups. - Keep native pointers in `JniMethodInfo` Debug metadata and decode them only if `Name`, `Signature`, or `ToString()` is explicitly requested. - Retain the existing string/span paths for custom `JniTypeManager` implementations. - Document UTF-8 encoding, NUL termination, ownership, and lifetime requirements for every pointer API. The successful generated-remapping path therefore passes the pregenerated UTF-8 type, method name, and optional signature directly to JNI without copying them or converting them to a managed string. This PR is method-only and does not add R8, field remapping, reverse-type mapping, inherited-member fallback, or NativeAOT remapping support. ## Relationship to other PRs - This PR is based directly on `main` and provides the Java.Interop representation and JNI lookup primitives. - #12796 supplies the generated pointers, performs managed table search, and removes the native remapping P/Invokes. - #12692 can build on this stack to add the R8-specific field, reverse-type, inherited-member, and NativeAOT pieces. ## Validation - Java.Interop Debug build. - Java.Interop `JniPeerMembersTests`: 12 passed, 1 skipped. - The JVM fixture exercises stable unmanaged UTF-8 type/name/signature storage, signature fallback, instance/static lookup, and instance-to-static remapping.
2ab1f9c to
25652d2
Compare
|
/review |
|
✅ Android PR Reviewer completed successfully!
|
There was a problem hiding this comment.
⚠️ Needs Changes
Findings: 0 errors, 0 warnings, 1 suggestion.
The managed/native ABI stays aligned across MonoVM and CoreCLR, the generated UTF-8 ordering matches the managed binary-search comparator, and stable generated pointers preserve the required Java.Interop lifetimes. The added device scenario also validates non-ASCII type and method remapping. I left one inline suggestion for focused 16-byte comparator-boundary coverage.
CI is not yet complete: 41 checks have passed, 2 are running, and the aggregate dotnet-android check is queued. No failing checks were reported, but this cannot be considered green until the remaining matrix finishes.
Generated by Android PR Reviewer for #12796 · copilot · gpt56 · 272.9 AIC · ⌖ 9.79 AIC · ⊞ 17.6K
Comment /review to run again
Exercise 15-, 16-, and 17-byte method names and adjacent names that first differ in the second comparison chunk through the generated remapping table and JNI lookup path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
25652d2 to
d338ba5
Compare
|
/review |
|
✅ Android PR Reviewer completed successfully!
|
There was a problem hiding this comment.
⚠️ Needs Changes
Findings: 0 errors, 0 warnings, 1 suggestion.
The managed/native ABI remains aligned, the UTF-8 comparison and binary-search paths preserve the generated ordering, pointer lifetimes are documented and backed by process-lifetime generated data, and all 44 CI checks passed. I left one inline testing suggestion: the new aggregate log assertions do not prove that each boundary source name maps to its intended target, so a swapped second-chunk result can pass unnoticed.
Generated by Android PR Reviewer for #12796 · copilot · gpt56 · 230.7 AIC · ⌖ 8.26 AIC · ⊞ 18.1K
Comment /review to run again
Require the ASCII chunk-boundary target messages to appear in invocation order so swapped adjacent lookup results cannot satisfy the regression test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>


Motivation
The existing Intune/MAM JNI remapping implementation performs a managed-to-native transition for every type and method lookup and maintains separate linear-search implementations in MonoVM and CoreCLR. Native replacement strings are then decoded to managed UTF-16 and encoded back to UTF-8 before JNI receives them.
The generated tables are already sorted and live for the process lifetime. Shared managed lookup can read them directly, avoid the repeated P/Invoke boundary, and keep successful remaps in native UTF-8 memory through JNI.
Approach
jni_remapping_datastructure directly in generated LLVM IR. It contains the existing type-replacement and method-index table pointers and counts.JnienvInitializeArgsinstead of a boolean remapping flag.System.Text.Ascii.IsValid()and chunkedAscii.Equals(ReadOnlySpan<byte>, ReadOnlySpan<char>)handle the common ASCII path without transcoding or allocation.Rune.DecodeFromUtf8()/Rune.DecodeFromUtf16()fallback preserves correct ordering and equality for non-ASCII names without creating a full converted buffer.SourceJniMethodNameorSourceJniMethodSignature; the requesting spans remain available to the caller for fallback and diagnostics.The search is not hash-based. Tables are ordered lexicographically by raw UTF-8 bytes. UTF-8 byte order and Unicode scalar order are equivalent, so the mixed UTF-8/UTF-16 comparator preserves the generated sort order.
Scope and relationship to #12692
This PR is based on #12795 and covers the existing Intune/MAM type and method remapping contract. It intentionally does not contain R8-specific field remapping, reverse-type mapping, inherited-member fallback, or NativeAOT remapping support.
#12692 can be rebased on this stack and add those R8-specific extensions rather than this optimization depending on #12692.
Performance characterization
Direct search benchmark
The R8 sample-content table contains 1,014 methods across 186 types. A linear control used identical UTF-8 data and comparisons without hashing.
205.8 nslinear vs.204.6 nsbinary).30.5%faster.70.5%faster.83.0%faster.Matched R2R startup benchmark
To benchmark the two PRs without depending on #12692's conflicting implementation, the real R8 sample's 483 method mappings and two type mappings were converted to identity remaps. Baseline and treatment APKs therefore ran identical unobfuscated MAUI sample-content code with the same large lookup-table distribution.
CoreCLR APKs were restored and built with
PublishReadyToRun=true. Measurements used a Samsung Galaxy A16 (Android 16, arm64-v8a), fresh installs, two warmups per install, five measured launches, animations disabled, and counterbalanced ordering.The initial mixed comparator rescanned an entire common prefix after
Ascii.Equals()reported a mismatch and measured+0.81%(95% CI+0.04%..+1.57%). The final implementation compares vector-sized chunks so a mismatch rescans at most one chunk.Final 160-launch A/B (80 launches per variant, 16 paired blocks):
am start -WTotalTimeFinal paired difference:
+0.22%, 95% CI-0.87%..+1.32%. The final implementation has no statistically measurable whole-app startup regression.A separate 180-launch R2R control matrix compared runtime remapping with equivalent R8 private-member optimization:
+0.02%, 95% CI-1.14%..+1.19%.Stack
Depends on #12795. The bottom PR is based on
main.Validation
JniPeerMembersTests: 12 passed, 1 skipped.TypeAndMemberRemapping: Debug and Release passed; NativeAOT remains intentionally skipped because remapping is not supported there onmain.