Repository navigation
[runtime] Extend JNI remapping support - #12847
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved findings remain around JNI owner preservation and remapping generator ABI/table support.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 6
Open (9)
Preserve declaring class for static field accessors · New Preserve target owner for base-remapped static methods · New Update generator and parser for the new remapping ABI · New Emit dedicated remapping count symbols · New Emit reverse type and field remapping symbols · New Emit all remapping count symbols, including empty tables · New Sort generated names using UTF-8 byte ordering · New Parse and emit target method signatures · New Add XML support for reverse-type and replace-field · New
What changed in this PR
Extends JNI remapping support across Java.Interop, Mono.Android, MonoVM, CoreCLR, and NativeAOT.
Changes:
- Adds reverse type, method, constructor, and field remapping.
- Extends native tables, lookup APIs, and P/Invoke infrastructure.
- Adds focused managed, native, and Java.Interop tests.
| File | Summary |
|---|---|
tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml |
Adds remapping test fixtures. |
tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs |
Tests reverse type lookup precedence. |
src/native/nativeaot/include/runtime-base/internal-pinvokes.hh |
Declares NativeAOT remapping entry points. |
src/native/nativeaot/host/jni-remapping-tables-stub.cc |
Provides empty NativeAOT remapping tables. |
src/native/nativeaot/host/internal-pinvoke-stubs.cc |
Updates NativeAOT P/Invoke stubs. |
src/native/nativeaot/host/host.cc |
Propagates NativeAOT remapping state. |
src/native/nativeaot/host/CMakeLists.txt |
Builds NativeAOT remapping sources. |
src/native/native.targets |
Tracks shared native inputs. |
src/native/mono/xamarin-app-stub/xamarin-app.hh |
Extends the Mono remapping ABI. |
src/native/mono/xamarin-app-stub/application_dso_stub.cc |
Updates Mono fixture tables. |
src/native/mono/runtime-base/internal-pinvokes.hh |
Declares Mono remapping entry points. |
src/native/mono/pinvoke-override/pinvoke-tables.include |
Registers remapping P/Invoke entries. |
src/native/mono/pinvoke-override/generate-pinvoke-tables.cc |
Adds generated P/Invoke entry names. |
src/native/mono/monodroid/monodroid-glue.cc |
Loads new Mono remapping tables and counts. |
src/native/mono/monodroid/jni-remapping.hh |
Declares reverse and field lookups. |
src/native/mono/monodroid/jni-remapping.cc |
Implements Mono remapping lookups. |
src/native/mono/monodroid/internal-pinvokes.cc |
Bridges Mono remapping APIs. |
src/native/clr/xamarin-app-stub/application_dso_stub.cc |
Updates CoreCLR fixture tables. |
src/native/clr/runtime-base/jni-remapping.cc |
Implements CoreCLR indexed lookups. |
src/native/clr/pinvoke-override/precompiled.cc |
Resolves CoreCLR remapping P/Invokes. |
src/native/clr/include/xamarin-app.hh |
Defines the CoreCLR remapping ABI. |
src/native/clr/include/runtime-base/jni-remapping.hh |
Declares shared CoreCLR lookup APIs. |
src/native/clr/include/runtime-base/internal-pinvokes.hh |
Declares CoreCLR remapping entry points. |
src/native/clr/host/internal-pinvokes-shared.cc |
Implements shared CoreCLR bridges. |
src/native/clr/host/internal-pinvokes-clr.cc |
Removes duplicate CoreCLR bridges. |
src/native/clr/host/host.cc |
Uses shared remapping state. |
src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs |
Applies remapped cast targets. |
src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs |
Adds reverse, member, and field remapping. |
src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs |
Applies remapping during proxy resolution. |
src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs |
Marshals remapping data. |
src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs |
Declares native remapping imports. |
src/Mono.Android/Android.Runtime/AndroidRuntime.cs |
Connects field remapping. |
external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java |
Adds a derived Java field fixture. |
external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java |
Adds a base Java field fixture. |
external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs |
Tests remapped fields and members. |
external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs |
Supplies Java.Interop remapping test data. |
external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj |
Includes Java test fixtures. |
external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt |
Records new public APIs. |
external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs |
Adds non-throwing field lookup. |
external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs |
Adds field remapping override. |
external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs |
Adds field remapping API. |
external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs |
Adds base static-method remapping. |
external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs |
Adds static-field remapping. |
external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs |
Adds constructor and base-method remapping. |
external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs |
Adds instance-field remapping. |
external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs |
Adds shared base member helpers. |
ed89425 to
9e84534
Compare
## Summary Remove the experimental build-time managed-assembly rewriting approach for R8 so any future assembly-rewriting design can start from a clean foundation. This cleanup is related to #12535 and the original prototype in #12575. ## What this PR undoes This removes the managed rewriting implementation developed across the R8 obfuscation stack: - #12629 added the PE metadata rebuild substrate. - #12630 added managed JNI metadata rewriting from R8 mappings. - #12631 added rewriting for generated trimmable type-map assemblies. - #12632 and #12634 integrated and tested that rewriting approach for CoreCLR and NativeAOT. Concretely, this PR removes the rewrite task, rewrite-only metadata/IL utilities, dedicated tests and fixtures, and the XA4325/XA4326 resources and documentation. The intent is to abandon this implementation rather than preserve an unused rewriting stack that a future design would need to work around. ## What remains in place - The generic `R8Mapping` parser introduced in #12628 remains, along with its tests and the `MSBuildDeviceIntegration` consumer. It is independently useful for reading R8 mapping files. - The ordinary R8 configuration and `private-members` obfuscation/optimization policy from #12668 remain unchanged. This PR does **not** disable R8 or remove private-member obfuscation. - Existing D8/R8 packaging, ProGuard rule handling, and non-rewriting build behavior remain unchanged. - Runtime remapping remains active as the stacked follow-up work in #12847, #12848, #12692, and #12844. Those PRs implement the alternative opt-in strategy without managed assembly rewriting and are not part of this cleanup diff. We may revisit managed assembly rewriting in .NET 12 based on customer feedback and performance data, but with a fresh design rather than this implementation. ## Validation - Built `Xamarin.Android.Build.Tasks` - Ran `Microsoft.Android.Build.Tasks.Tests` - Ran `Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests` - Built `MSBuildDeviceIntegration` - Ran focused `R8MappingTests`
9e84534 to
5c8b4b1
Compare
|
/review |
|
✅ Android PR Reviewer completed successfully!
|
There was a problem hiding this comment.
❌ Reject — 1 error.
NativeAOT does not populate the new JNI remapping-data argument, so remapping remains disabled for that runtime. The MonoVM/CoreCLR wiring and consolidation of lookup logic otherwise look coherent, and CI is green.
Generated by Android PR Reviewer for #12847 · copilot · gpt56 · 694 AIC · ⌖ 11.1 AIC · ⊞ 25.9K
Comment /review to run again
5c5717a to
d6a5b00
Compare
|
/review |
|
✅ Android PR Reviewer completed successfully!
|
There was a problem hiding this comment.
✅ LGTM — 0 errors, 0 warnings, 1 suggestion.
The cross-runtime JNI remapping plumbing, generated-table ABI, owner preservation, and regression coverage are coherent. All 44 current checks, including the Azure DevOps dotnet-android build, are green. I left one non-blocking suggestion to cover an internal binary-search miss in the lookup tests.
Generated by Android PR Reviewer for #12847 · copilot · gpt56 · 628.8 AIC · ⌖ 11.1 AIC · ⊞ 25.9K
Comment /review to run again
|
@dalexsoto review |
dalexsoto
left a comment
There was a problem hiding this comment.
I found two blockers on this revision:
-
Incoming renamed peers are not reverse-mapped in the nontrimmable type map (
src/Mono.Android/Android.Runtime/AndroidRuntime.cs:331-349,src/Mono.Android/Java.Interop/TypeManager.cs:264-276,303-319). Outgoing lookup now maps a declared JNI class such asFooto its runtime namea, but incoming objects namedapass unchanged through bothAndroidTypeManagerlookup methods into the native Java-to-managed typemap, which is generated from the declared names without the remapping XML. The legacyTypeManager.CreateInstancepath likewise calls the native lookup unchanged. Areverse-typeentry can therefore exist in the new descriptor while an incomingaobject still fails to materialize asFoo(and may instead become a superclass peer). The deferred R8 ingestion in #12848 is not the concern here: this is runtime consumption of an already generated reverse entry. Apply the reverse lookup before the nontrimmable and legacy Java-to-managed lookups, or provide equivalent remapped typemap keys, and exercise incoming peer creation under LLVM-IR typemaps. -
The public method-remap result loses its source identity (
src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs:224-233). The shipped string-basedJniTypeManager.GetReplacementMethodInfopreviously populatedSourceJniType,SourceJniMethodName, andSourceJniMethodSignatureon a successful lookup; the new initializer leaves all three null. Those source properties have no UTF-8 alternatives, so callers inspecting or formatting the result lose its origin, and equality/hash comparisons can no longer distinguish entries by source. Preserve those fields for the public string lookup without sacrificing the internal UTF-8 fast path, and cover the returned values in a test.
Addressed in
The public string overload now preserves |
Co-authored-by: Copilot App <[email protected]>
Co-authored-by: Copilot App <[email protected]>
Port the managed remapping data ABI from the closed runtime stack, remove the native lookup implementations and P/Invokes, and retain the extended reverse-type, field, signature, and declaring-owner behavior. Co-authored-by: Copilot App <[email protected]>
Co-authored-by: Copilot App <[email protected]>
Dispose cached instance-method redirects and cover the managed generated-table lookup paths for Unicode ordering and member specificity. Co-authored-by: Copilot App <[email protected]>
Dispose remapped member and subclass-constructor candidates that lose ConcurrentDictionary publication races, and document the single-hop type-remapping contract. Co-authored-by: Copilot App <[email protected]>
Forward field remapping through AndroidTypeManager and use an XML-safe high Unicode key for the lookup boundary test. Co-authored-by: Copilot App <[email protected]>
Provide a weak empty remapping descriptor for runtime-only links while allowing application-generated remapping data to override it. Co-authored-by: Copilot App <[email protected]>
Co-authored-by: Copilot App <[email protected]>
Preserve source identity for public method remap results and cover one-hop reverse lookup with LLVM-IR typemaps. Co-authored-by: Copilot App <[email protected]>
Fix the LLVM-IR remapping test imports and keep runtime-name registrations ahead of reverse typemap fallback. Co-authored-by: Copilot App <[email protected]>
Preserve main's removal of legacy timing P/Invokes while rebasing JNI remapping support. Co-authored-by: Copilot App <[email protected]>
Separate explicit registrations from typemap caching and retain complete public method remap results. Co-authored-by: Copilot App <[email protected]>
Gate managed remapping behind build-derived feature switches so apps with empty remap tables do not pay the linked-code cost. Co-authored-by: Copilot App <[email protected]>
1f2226d to
39004c2
Compare
Resolve APK descriptor conflicts using main's measured trimmable baselines. Co-authored-by: Copilot App <[email protected]>
Use the measured descriptors from dotnet-android build 1618183 for the Simple/CoreCLR tests with and without R8. Preserve the existing regression thresholds. Co-authored-by: Copilot App <[email protected]>
Merge 3f356b7 while preserving the measured Simple/CoreCLR APK baselines and runtime remapping changes. Co-authored-by: Copilot App <[email protected]>
Include the merged #12976 trimmable typemap scan fix while preserving the runtime foundation and measured APK baselines. Co-authored-by: Copilot App <[email protected]>
|
@dalexsoto stamp |
dalexsoto
left a comment
There was a problem hiding this comment.
Stamping as requested by @simonrozsival; no code review performed.
Depends on #12847. This layer adds the producer-side implementation for R8 JNI runtime remapping. It parses R8 mappings, scans linked managed metadata read-only to retain surviving entries, supports NativeAOT post-ILC retention, and generates forward/reverse type, method, field, descriptor, indexed, count, and empty LLVM lookup tables. Managed assemblies are never rewritten or reconstructed by this change. This intentionally contains no public product mode, policy, pipeline target wiring, NativeAOT deferred-link orchestration, documentation, or end-to-end device tests; that wiring remains for the later #12692 layer.
Context: #12535 This is the product policy and build-orchestration layer for opt-in R8 runtime JNI remapping. It is stacked on: - #12846 — removes the abandoned managed-assembly rewriting implementation - #12847 — adds generic runtime JNI remapping support - #12848 — parses the final R8 mapping and generates native runtime lookup tables A final dependent PR, #12844, moves the .NET-only R8 mapping task into `Microsoft.Android.Build.Tasks`. This PR does not rewrite managed assemblies. Managed bindings retain their original JNI names; the lower layers generate native tables that translate lookups to the names and descriptors emitted by R8. ## Opt-in For a trimmed CoreCLR or NativeAOT application: ```xml <PropertyGroup Condition="'$(Configuration)' == 'Release'"> <AndroidLinkTool>r8</AndroidLinkTool> <AndroidTypeMapImplementation>trimmable</AndroidTypeMapImplementation> <PublishTrimmed>true</PublishTrimmed> <AndroidR8ObfuscationMode>runtime-remapping</AndroidR8ObfuscationMode> </PropertyGroup> ``` `runtime-remapping` is the sole opt-in; there is no separate enable property. It does not apply to library projects. The existing `private-members` and `disabled` behavior remains unchanged. Unknown mode values report XA1050, while incompatible runtime-remapping configurations report XA4329. ## Build orchestration - Runs R8 once, after ILLink or all per-RID NativeAOT ILC compilations. - Converts that final mapping into runtime remapping tables before native linking. - Defers NativeAOT linking until the shared R8 pass completes, then relinks each RID without rerunning ILC. - Applies the runtime-remapping-specific bootstrap, native-callback, manifest, resource, and JNI safety keep rules. - Preserves existing MAM remapping precedence and asset handling. - Tracks mapping inputs, generated XML, native table sources, task assemblies, native-link inputs, configuration changes, missing-output recovery, and opt-out for incremental builds. - Preserves generated ProGuard rule timestamps when their contents are unchanged so managed-only rebuilds do not rerun R8 unnecessarily. ## Coverage in this layer Host tests cover mode defaults, configuration validation, keep-rule policy, NativeAOT ProGuard configuration, packaging metadata, and incremental behavior. Build/device integration tests cover renamed types and members, constructors, overloads, inherited lookups, peer activation, single-pass ordering, multi-RID builds, missing-output recovery, and opt-out for CoreCLR and NativeAOT. The generic lookup semantics and mapping/table-generation tests live in #12847 and #12848 respectively. ## Experimental limitations NativeAOT literal matching is conservative and can retain extra entries. Arbitrarily computed JNI names may require explicit remaps or keep rules. Conservative public/nested-class, interface, bootstrap, and native-callback keeps limit obfuscation. Existing Intune/R8 conflict handling is not full remapping-chain composition, and ambiguous reverse mappings for merged classes are omitted. This remains an experimental opt-in, not a production-readiness claim. ---- - [x] Useful description of why the change is necessary. - [x] Links to related issues and dependent layers. - [x] Unit and integration coverage. Fixes: #12535
…mbly (#12844) ## Summary This PR is stacked on and depends on #12692. That parent is built on the focused lower chain #12846, #12847, and #12848. This final layer is only a task-assembly boundary change for the runtime-R8 remapping producer. It moves `GenerateR8JniRemapping` and the .NET-only helpers it directly requires into `Microsoft.Android.Build.Tasks`: - `JniRemappingAssemblyScanner` - `R8Mapping` - `JniDescriptorText` - `NativeAotJniRetention` The generic runtime table producer remains in `Xamarin.Android.Build.Tasks`: `GenerateJniRemappingNativeCode` and `JniRemappingNativeCodeGenerator` are unchanged and continue to serve both MAM and R8 remapping. The relocation also: - loads only `GenerateR8JniRemapping` from the modern assembly using the existing Full/Core `UsingTask` pattern - tracks both modern and legacy task assemblies in mixed incremental targets - links only the metadata helpers required by the read-only scanner - packages `ELFSharp.dll` beside the modern task assembly under `tools/net` - keeps net10 `MSBuildDeviceIntegration` compatible by linking `R8Mapping.cs` instead of referencing the net11 task project - preserves a test-only aliased legacy task reference to validate generated XML through `MergeRemapXml` and `GenerateJniRemappingNativeCode` This PR does not restore or relocate the removed managed assembly rewriter, rewrite planners/rebuilders, rewrite-only tests, or `XA4325`/`XA4326` behavior. ## Validation - `Microsoft.Android.Build.Tasks` build - `Xamarin.Android.Build.Tasks` build - 86 focused modern R8 remapping tests - 41 trimmable typemap integration tests - `MSBuildDeviceIntegration` project build - verified packaged `tools/net/ELFSharp.dll` - final read-only review found no blocking issues


Depends on #12846.
This is the consolidated runtime foundation for the R8 runtime-remapping stack. It supersedes the earlier split runtime implementation in #12796 and #12817 without abandoning that workstream.
It adds the generic JNI remapping support shared by Java.Interop, Mono.Android, MonoVM, CoreCLR, and NativeAOT, including forward/reverse type remapping, descriptor-aware method and constructor remapping, field remapping, Java hiding/fallback behavior, and focused tests.
The generated application object contains read-only remapping tables and a single
jni_remapping_datadescriptor. Native startup passes that descriptor to managed initialization; type, reverse-type, method, and field lookup algorithms remain inJniRemappingLookup.cs. No C++ remapping lookup implementation or lookup P/Invokes are introduced.This PR intentionally contains no R8 build orchestration and no managed assembly rewriting. Producer-side R8 mapping ingestion and generated-table wiring are in the next stack layer, #12848; public mode and build orchestration follow in #12692, with the modern task-assembly boundary in #12844.
APK size and disabled-remapping footprint
The measured Simple/CoreCLR baselines were refreshed from the complete test attachments in CI build 1618183:
The test failure was the per-file threshold on
libxamarin-app.so, not a large APK regression: that library grew from 11,744 to 12,368 bytes (+624, 5.05%). Isolated re-links using the archived CI object files attribute this exactly:.textsection remains 36 bytes in all four variants; this is fixed data/configuration overhead, not added remapping executable code.For this no-remapping application, all four table counts are zero and both remapping switches are configured
false. Inspection of the actual linkedMono.Android.Runtime.dllconfirms thatJniRemappingLookupis absent. The earlier approximately 100 KiB cost from retaining the managed remapping implementation has not returned.The full baseline comparison also crosses the .NET 11 RC2 to .NET 12 alpha update inherited from
mainin #12939, so its other changes must not be attributed wholesale to this PR. The APK's CoreCLR, JIT, globalization, and System.Native binaries are byte-identical to the new runtime pack; the cached previous runtime pack matches the old reference sizes. For example,libcoreclr.soshrank 103,792 uncompressed bytes, whilelibclrjit.sogrew 15,896 bytes. The assembly store grew 23,880 bytes andlibmonodroid.soshrank 2,896 bytes. Summed across all entries, uncompressed contents actually shrank 65,868 bytes.APK size measures the compressed and signed archive, not the sum of those uncompressed sizes. The current APK uses DEFLATE for its native libraries, an 8 KiB signing block, and 4 KiB signing alignment. The nearest retained pre-upgrade comparison, CI build 1617831, shows the larger assembly store (+23,216 compressed bytes) and JIT (+6,970) almost offset by the smaller CoreCLR (-28,694) and other entries: compressed payload grows just 1,034 bytes, while ZIP/signing/alignment overhead grows 3,062 bytes, producing the observed 4 KiB APK step.
libxamarin-app.soitself adds only 107 compressed bytes in that comparison.Comparison limitation: the exact reference APK was generated locally before the runtime update and was not retained. Build 1617831 has slightly different dex/store/monodroid entries, although its total APK size matches the reference. The compressed/padding breakdown is therefore an explicitly identified historical CI comparison, not an exact reconstruction of the local reference or a same-toolchain
main-versus-PR A/B test. The isolated 400/224-byte native attribution and removal of the managed lookup are independently confirmed.