From d3bb4e8612c3b3062c8dc70c4214f914d2016f78 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 5 Sep 2026 21:01:35 +0200 Subject: [PATCH 01/14] [Xamarin.Android.Build.Tasks] Add opt-in R8 runtime remapping Context: https://github.com/dotnet/android/issues/12535 R8 obfuscation changes JNI names that managed bindings still use. Add an experimental alternative to assembly rewriting by translating those names through the existing runtime remapping machinery. Run a naming-only R8 seed before ILLink or ILC and apply its mapping during final R8. Select CoreCLR remaps from linked assemblies and NativeAOT remaps from retained ELF literals, then link the NativeAOT table after ILC. Extend lookups for reverse types, descriptors, fields, and peers. Expose AndroidEnableR8Obfuscation, defaulting to false, and AndroidR8ObfuscationMode, defaulting to runtime-remapping. Reserve experimental-rewriting with a clear error until its separate pipeline is available. Diagnose incompatible configurations with XA4329. Preserve JNI bootstrap and resource keep rules, track incremental table and native-link inputs, and support switching obfuscation back off. Include task, configuration, device regression tests, and documentation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/TOC.yml | 6 + .../building-apps/build-properties.md | 51 ++ Documentation/docs-mobile/messages/index.md | 3 + Documentation/docs-mobile/messages/xa4327.md | 59 ++ Documentation/docs-mobile/messages/xa4328.md | 44 ++ Documentation/docs-mobile/messages/xa4329.md | 38 + .../JniPeerMembers.JniInstanceFields.cs | 18 +- .../JniPeerMembers.JniInstanceMethods.cs | 39 +- .../JniPeerMembers.JniStaticFields.cs | 18 +- .../JniPeerMembers.JniStaticMethods.cs | 4 +- .../Java.Interop/JniPeerMembers.cs | 95 ++- .../Java.Interop/JniRuntime.JniTypeManager.cs | 80 ++ .../JniRuntime.ReflectionJniTypeManager.cs | 2 + .../src/Java.Interop/Java.Interop/JniType.cs | 52 ++ .../src/Java.Interop/PublicAPI.Unshipped.txt | 24 + .../Java.Interop/JavaVMFixture.cs | 26 + .../Java.Interop/JniPeerMembersTests.cs | 60 ++ .../Android.Runtime/AndroidRuntime.cs | 5 + .../Android.Runtime/RuntimeNativeMethods.cs | 8 + .../JniRemappingLookup.cs | 72 +- .../TrimmableTypeMap.cs | 7 +- .../TrimmableTypeMapTypeManager.cs | 34 +- .../TrimmableTypeMapValueManager.cs | 3 +- .../Android/Xamarin.Android.Aapt2.targets | 6 - .../Microsoft.Android.Sdk.NativeAOT.targets | 6 +- ...crosoft.Android.Sdk.R8JniRemapping.targets | 295 ++++++++ ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 3 +- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 27 +- ...id.Sdk.TypeMap.Trimmable.NativeAOT.targets | 3 +- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 4 + .../Properties/Resources.Designer.cs | 180 +++++ .../Properties/Resources.resx | 103 +++ .../proguard_trimmable_nativeaot.cfg | 10 +- .../Resources/proguard_xamarin.cfg | 10 + .../Tasks/GenerateJniRemappingNativeCode.cs | 68 +- .../GenerateNativeAotProguardConfiguration.cs | 5 +- .../Tasks/GenerateProguardConfiguration.cs | 8 +- ...erateR8JniManifestProguardConfiguration.cs | 105 +++ .../Tasks/GenerateR8JniRemapping.cs | 470 ++++++++++++ src/Xamarin.Android.Build.Tasks/Tasks/R8.cs | 129 +++- .../InvalidConfigTests.cs | 69 ++ .../GenerateJniRemappingNativeCodeTests.cs | 260 +++++++ .../Tasks/GenerateR8JniRemappingTests.cs | 714 ++++++++++++++++++ .../Tasks/GenerateTrimmableTypeMapTests.cs | 15 +- .../Tasks/R8Tests.cs | 45 +- .../JniRemapping/JniAssemblyRewriter.cs | 3 + .../JniRemapping/JniDescriptorText.cs | 53 ++ .../JniRemapping/NativeAotJniRetention.cs | 252 +++++++ .../Utilities/JniRemapping/R8Mapping.cs | 161 +++- .../JniRemappingAssemblyGenerator.cs | 420 +++++++++-- .../Xamarin.Android.Common.targets | 81 +- .../Xamarin.Android.D8.targets | 6 +- src/native/clr/host/host.cc | 3 +- src/native/clr/host/internal-pinvokes-clr.cc | 13 - .../clr/host/internal-pinvokes-shared.cc | 24 + .../include/runtime-base/internal-pinvokes.hh | 2 + .../clr/include/runtime-base/jni-remapping.hh | 23 +- src/native/clr/include/xamarin-app.hh | 39 +- .../clr/pinvoke-override/precompiled.cc | 6 + src/native/clr/runtime-base/jni-remapping.cc | 224 ++++-- .../xamarin-app-stub/application_dso_stub.cc | 48 ++ .../mono/monodroid/internal-pinvokes.cc | 19 + .../generate-pinvoke-tables.cc | 2 + .../pinvoke-override/pinvoke-tables.include | 10 +- .../mono/runtime-base/internal-pinvokes.hh | 2 + .../xamarin-app-stub/application_dso_stub.cc | 2 + .../mono/xamarin-app-stub/xamarin-app.hh | 13 +- src/native/native.targets | 4 + src/native/nativeaot/host/CMakeLists.txt | 2 + src/native/nativeaot/host/host.cc | 2 + .../nativeaot/host/internal-pinvoke-stubs.cc | 13 - .../host/jni-remapping-tables-stub.cc | 16 + .../include/runtime-base/internal-pinvokes.hh | 2 + .../Tests/R8RuntimeRemappingTests.cs | 161 ++++ 74 files changed, 4653 insertions(+), 236 deletions(-) create mode 100644 Documentation/docs-mobile/messages/xa4327.md create mode 100644 Documentation/docs-mobile/messages/xa4328.md create mode 100644 Documentation/docs-mobile/messages/xa4329.md create mode 100644 src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.R8JniRemapping.targets create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniManifestProguardConfiguration.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/NativeAotJniRetention.cs create mode 100644 src/native/nativeaot/host/jni-remapping-tables-stub.cc create mode 100644 tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs diff --git a/Documentation/docs-mobile/TOC.yml b/Documentation/docs-mobile/TOC.yml index fbac40449c2..77954d490cc 100644 --- a/Documentation/docs-mobile/TOC.yml +++ b/Documentation/docs-mobile/TOC.yml @@ -370,6 +370,12 @@ href: messages/xa4325.md - name: XA4326 href: messages/xa4326.md + - name: XA4327 + href: messages/xa4327.md + - name: XA4328 + href: messages/xa4328.md + - name: XA4329 + href: messages/xa4329.md - name: "XA5xxx: GCC and toolchain" items: - name: "XA5xxx: GCC and toolchain" diff --git a/Documentation/docs-mobile/building-apps/build-properties.md b/Documentation/docs-mobile/building-apps/build-properties.md index 475a57bbbdd..695a1531b61 100644 --- a/Documentation/docs-mobile/building-apps/build-properties.md +++ b/Documentation/docs-mobile/building-apps/build-properties.md @@ -469,6 +469,33 @@ removing the existing one(s) and adding your own AOT profiles. This property is `False` by default. +## AndroidEnableR8Obfuscation + +A boolean property that opts an Android application into R8 name obfuscation. +The default is `false`; setting +[`$(AndroidR8ObfuscationMode)`](#androidr8obfuscationmode) alone does not enable it. +This feature is experimental. + +The current implementation requires `AndroidLinkTool=r8`, +`AndroidTypeMapImplementation=trimmable`, `PublishTrimmed=true`, and either the +CoreCLR or NativeAOT runtime. Explicit incompatible settings produce +[XA4329](../messages/xa4329.md) rather than being silently changed. +This property has no effect on library projects. + +For example: + +```xml + + r8 + trimmable + true + true + runtime-remapping + +``` + +Added in .NET 11. + ## AndroidEnableRestrictToAttributes An enum-style property with valid values of `obsolete` and `disable`. @@ -1095,6 +1122,30 @@ r8 dex-compiler and shrinker. The default value is a path into the .NET for Android workload installation. For further information see our documentation on [D8 and R8][d8-r8]. +## AndroidR8ObfuscationMode + +Selects how managed JNI references are reconciled with R8's obfuscated Java +names. It is only used when +[`$(AndroidEnableR8Obfuscation)`](#androidenabler8obfuscation) is `true`. +The default is `runtime-remapping`. + +| Value | Behavior | +|---|---| +| `runtime-remapping` | Keeps managed assemblies unchanged and translates JNI type/member lookups using generated native remapping tables. Available for trimmed CoreCLR and NativeAOT applications. | +| `experimental-rewriting` | Reserved for the separate managed-assembly rewriting implementation. This SDK does not yet include its build pipeline; selecting it reports [XA4329](../messages/xa4329.md). | + +The runtime-remapping mode runs a naming-only R8 pass before ILLink or ILC and +applies that mapping in the final R8 pass. CoreCLR selects remaps from linked +assemblies. NativeAOT selects remaps from retained JNI literals in ILC's native +object and statically links the table afterward. + +Runtime-generated JNI names may require explicit remapping or keep rules. +Conservative keep rules still protect native callbacks, bootstrap code, and +resource-referenced names. Neither mode is selected as a fallback for another +mode; unrecognized values report XA4329 when obfuscation is enabled. + +Added in .NET 11. + ## AndroidResgenExtraArgs Specifies diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index 4b708ca2c39..529fc04cc8b 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -257,6 +257,9 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA4324](xa4324.md): [{arch}] Unable to delete source file '{file}'. + [XA4325](xa4325.md): Failed to rewrite managed JNI names for R8. {message} + [XA4326](xa4326.md): Unable to safely rewrite a JNI member lookup because its class handle does not have one structurally unambiguous `JNIEnv.FindClass` source. ++ [XA4327](xa4327.md): Failed to generate the R8 JNI remapping data. {message} ++ [XA4328](xa4328.md): The R8 JNI remapping data is incomplete. {message} ++ [XA4329](xa4329.md): Invalid or unsupported R8 obfuscation configuration. ## XA5xxx: GCC and toolchain diff --git a/Documentation/docs-mobile/messages/xa4327.md b/Documentation/docs-mobile/messages/xa4327.md new file mode 100644 index 00000000000..19dc8698184 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4327.md @@ -0,0 +1,59 @@ +--- +title: .NET for Android error XA4327 +description: XA4327 error code +ms.date: 09/04/2026 +f1_keywords: + - "XA4327" +--- + +# .NET for Android error XA4327 + +## Example messages + +``` +error XA4327: Failed to generate the R8 JNI remapping data. The R8 seed mapping file 'obj/Release/net11.0/android-arm64/r8-jni-seed/mapping.txt' was not found. +``` + +``` +error XA4327: Failed to generate the R8 JNI remapping data. The Android manifest 'obj/Release/net11.0/android/AndroidManifest.xml' does not have a element with a 'package' attribute. +``` + +## Issue + +The build could not produce the data that lets the runtime translate the +original JNI names in the managed assemblies into the names R8 chose. + +This only happens when R8 obfuscation is enabled with +`$(AndroidEnableR8Obfuscation)=true` and +`$(AndroidR8ObfuscationMode)=runtime-remapping`. The remapping is built from a +naming-only "seed" R8 pass that runs before trimming; the error means either +that pass did not produce a usable mapping file, or the merged +`AndroidManifest.xml` it depends on could not be read. On NativeAOT, this also +reports a missing or invalid ILC native object: remapping data is selected from +the surviving JNI literals in that object before the final native link. + +NativeAOT filtering supports normal generated JNI bindings whose class names, +member names, and descriptors are literal strings. It inspects the initialized +data of the 32-bit or 64-bit ILC ELF object, including UTF-16 literals and UTF-8 +metadata. Shared strings can retain extra mappings; they do not make arbitrary +runtime-constructed JNI names safe. JNI names or descriptors constructed at +runtime require explicit remapping XML or R8 keep rules that preserve the +affected Java types and members. + +## Solution + +The message names the specific file that is missing or unreadable. + +* Build with `-v:diag` (or check the binary log) for the output of the seed R8 + pass that should have produced the mapping file, and address any failure it + reports. +* Delete the `obj` directory and rebuild if the intermediate output is in an + inconsistent state. +* For NativeAOT, ensure ILC completed and its `NativeObject` output exists before + remapping runs. Pre-ILC assemblies and dependency graphs cannot substitute for + that object. Missing or invalid retention data fails the build instead of + falling back to an unfiltered mapping. +* If the failure persists, [report an issue][report-issue] and include the full + error, a binary log, and, if possible, a project that reproduces it. + +[report-issue]: https://github.com/dotnet/android/issues/new/choose diff --git a/Documentation/docs-mobile/messages/xa4328.md b/Documentation/docs-mobile/messages/xa4328.md new file mode 100644 index 00000000000..1cbf713afd2 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4328.md @@ -0,0 +1,44 @@ +--- +title: .NET for Android warning XA4328 +description: XA4328 warning code +ms.date: 09/04/2026 +f1_keywords: + - "XA4328" +--- + +# .NET for Android warning XA4328 + +## Example message + +``` +warning XA4328: The R8 JNI remapping data is incomplete. The 'replace-type' entry for 'T com/contoso/MainActivity' was not emitted: another JNI remapping input already maps it to 'com/contoso/Renamed', which conflicts with 'a/b'. +``` + +## Issue + +The R8 JNI runtime remapping is generated from the R8 seed mapping file and is +merged with every other JNI remapping input in the build, such as the Intune +(MAM) mapping. + +An entry produced from the R8 mapping described the same type or member as an +entry that another input already contributed, but mapped it somewhere else. The +pre-existing input wins and the conflicting entry is not emitted. + +When the conflict is on a type, the type's reverse mapping and all of its +members are left to the other input as well, so the type named in the message is +not remapped for R8 at all. + +The warning is also emitted when a Java signature in the R8 mapping file cannot +be converted to a JNI descriptor. That entry is skipped as well. + +## Solution + +Only one remapping input can own a given type or member. + +* If the app uses the Intune (MAM) mapping, exclude the affected types from the + R8 renaming with a `-keep` rule in a `@(ProguardConfiguration)` file so the + seed R8 pass does not rename them. +* If the conflict is unexpected, [report an issue][report-issue] and include the + full warning, the R8 seed mapping file, and the other remapping input. + +[report-issue]: https://github.com/dotnet/android/issues/new/choose diff --git a/Documentation/docs-mobile/messages/xa4329.md b/Documentation/docs-mobile/messages/xa4329.md new file mode 100644 index 00000000000..05d587c7a14 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4329.md @@ -0,0 +1,38 @@ +--- +title: .NET for Android error XA4329 +description: XA4329 error code +ms.date: 09/05/2026 +f1_keywords: + - "XA4329" +--- + +# .NET for Android error XA4329 + +## Example messages + +``` +Invalid value for AndroidEnableR8Obfuscation: 'yes'. Valid values are: true, false. +``` + +``` +AndroidR8ObfuscationMode='experimental-rewriting' is not available in this SDK. Use 'runtime-remapping' or set AndroidEnableR8Obfuscation=false. +``` + +## Issue + +An R8 obfuscation property has an invalid value, the selected mode is unavailable, +or the application's build configuration is incompatible with obfuscation. + +## Solution + +Set `AndroidEnableR8Obfuscation` to `true` or `false`. When enabled, use +`AndroidR8ObfuscationMode=runtime-remapping` (the default), `AndroidLinkTool=r8`, +`AndroidTypeMapImplementation=trimmable`, and `PublishTrimmed=true` with CoreCLR +or NativeAOT. + +The `experimental-rewriting` value is reserved for a separate implementation +whose build pipeline is not included in this SDK. It does not fall back to +runtime remapping. Setting a mode alone does not enable obfuscation. + +See [AndroidEnableR8Obfuscation](../building-apps/build-properties.md#androidenabler8obfuscation) +and [AndroidR8ObfuscationMode](../building-apps/build-properties.md#androidr8obfuscationmode). diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs index 43c33bdf95d..0346b47c2f7 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs @@ -27,8 +27,24 @@ public JniFieldInfo GetFieldInfo (string encodedMember) return InstanceFields.GetOrAdd (encodedMember, static (member, fields) => { string field, signature; JniPeerMembers.GetNameAndSignature (member, out field, out signature); - return fields.Members.JniPeerType.GetInstanceField (field, signature); + return fields.GetFieldInfo (field, signature); }, this); } + + JniFieldInfo GetFieldInfo (string field, string signature) + { + var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerOriginalTypeName, Members.JniPeerTypeName, Members.ManagedPeerType, field, signature); + if (newField.HasValue) { + var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName; + var fieldName = newField.Value.TargetJniFieldName ?? field; + var fieldSig = newField.Value.TargetJniFieldSignature ?? signature; + + using var t = new JniType (typeName); + if (t.TryGetInstanceField (fieldName, fieldSig, out var f)) { + return f; + } + } + return Members.JniPeerType.GetInstanceField (field, signature); + } }} } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs index 90ababdb74b..5bdad4129b7 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs @@ -24,12 +24,25 @@ internal JniInstanceMethods (JniPeerMembers members) declaringType.FullName)); DeclaringType = declaringType; - jniPeerType = new JniType (info.Name); + // The managed type declares its original JNI name; the peer must be looked up under the + // name it has in the packaged application, but member replacements stay keyed by the + // original one. + originalJniTypeName = jvm.TypeManager.GetOriginalType (info.SimpleReference) ?? info.SimpleReference; + targetJniTypeName = jvm.TypeManager.GetReplacementType (originalJniTypeName) ?? info.Name; + jniPeerType = new JniType (targetJniTypeName); jniPeerType.RegisterWithRuntime (); } JniPeerMembers? members; JniType? jniPeerType; + readonly string? originalJniTypeName; + readonly string? targetJniTypeName; + + // The JNI type name member replacements are keyed by... + string SourceJniTypeName => originalJniTypeName ?? Members.JniPeerOriginalTypeName; + + // ...and the one members are actually looked up on. + string TargetJniTypeName => targetJniTypeName ?? Members.JniPeerTypeName; internal JniPeerMembers Members => members ?? throw new InvalidOperationException (); @@ -59,7 +72,23 @@ public JniMethodInfo GetConstructor (string signature) if (signature == null) throw new ArgumentNullException (nameof (signature)); return InstanceMethods.GetOrAdd (signature, static (member, methods) => - methods.JniPeerType.GetConstructor (member), this); + methods.GetConstructorCore (member), this); + } + + JniMethodInfo GetConstructorCore (string signature) + { + // Constructors are never renamed, but their parameter types can be, so the descriptor + // still has to be translated. + var newMethod = JniPeerMembers.GetReplacementMethodInfo (SourceJniTypeName, TargetJniTypeName, DeclaringType, "", signature, searchBaseTypes: false); + var targetSignature = newMethod?.TargetJniMethodSignature; + if (targetSignature != null && !string.Equals (targetSignature, signature, StringComparison.Ordinal)) { + var typeName = newMethod?.TargetJniType ?? TargetJniTypeName; + using var t = new JniType (typeName); + if (t.TryGetInstanceMethod ("", targetSignature, out var m)) { + return m; + } + } + return JniPeerType.GetConstructor (signature); } internal JniInstanceMethods GetConstructorsForType (Type declaringType) @@ -104,9 +133,9 @@ public JniMethodInfo GetMethodInfo (string encodedMember) JniMethodInfo GetMethodInfo (string method, string signature) { var m = (JniMethodInfo?) null; - var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature); + var newMethod = JniPeerMembers.GetReplacementMethodInfo (SourceJniTypeName, TargetJniTypeName, DeclaringType, method, signature); if (newMethod.HasValue) { - var typeName = newMethod.Value.TargetJniType ?? Members.JniPeerTypeName; + var typeName = newMethod.Value.TargetJniType ?? TargetJniTypeName; var methodName = newMethod.Value.TargetJniMethodName ?? method; var methodSig = newMethod.Value.TargetJniMethodSignature ?? signature; @@ -120,7 +149,7 @@ JniMethodInfo GetMethodInfo (string method, string signature) if (t.TryGetInstanceMethod (methodName, methodSig, out m)) { return m; } - Console.Error.WriteLine ($"warning: For declared method `{Members.JniPeerTypeName}.{method}.{signature}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!"); + Console.Error.WriteLine ($"warning: For declared method `{SourceJniTypeName}.{method}.{signature}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!"); } return JniPeerType.GetInstanceMethod (method, signature); } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs index f0c490460ab..4d4fb7f0457 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs @@ -22,10 +22,26 @@ public JniFieldInfo GetFieldInfo (string encodedMember) return StaticFields.GetOrAdd (encodedMember, static (member, fields) => { string field, signature; JniPeerMembers.GetNameAndSignature (member, out field, out signature); - return fields.Members.JniPeerType.GetStaticField (field, signature); + return fields.GetFieldInfo (field, signature); }, this); } + JniFieldInfo GetFieldInfo (string field, string signature) + { + var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerOriginalTypeName, Members.JniPeerTypeName, Members.ManagedPeerType, field, signature); + if (newField.HasValue) { + var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName; + var fieldName = newField.Value.TargetJniFieldName ?? field; + var fieldSig = newField.Value.TargetJniFieldSignature ?? signature; + + using var t = new JniType (typeName); + if (t.TryGetStaticField (fieldName, fieldSig, out var f)) { + return f; + } + } + return Members.JniPeerType.GetStaticField (field, signature); + } + internal void Dispose () { StaticFields.Clear (); diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs index 379d6f21c52..6e3a3388c49 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs @@ -34,7 +34,7 @@ public JniMethodInfo GetMethodInfo (string encodedMember) JniMethodInfo GetMethodInfo (string method, string signature) { var m = (JniMethodInfo?) null; - var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature); + var newMethod = JniPeerMembers.GetReplacementMethodInfo (Members.JniPeerOriginalTypeName, Members.JniPeerTypeName, Members.ManagedPeerType, method, signature); if (newMethod.HasValue) { using var t = new JniType (newMethod.Value.TargetJniType ?? Members.JniPeerTypeName); if (t.TryGetStaticMethod ( @@ -66,7 +66,7 @@ JniType GetMethodDeclaringType (JniMethodInfo method) JniMethodInfo? FindInFallbackTypes (string method, string signature) { - var fallbackTypes = JniEnvironment.Runtime.TypeManager.GetStaticMethodFallbackTypes (Members.JniPeerTypeName); + var fallbackTypes = JniEnvironment.Runtime.TypeManager.GetStaticMethodFallbackTypes (Members.JniPeerOriginalTypeName); if (fallbackTypes == null) { return null; } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs index 1b64266f9c8..d5e976a9a23 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs @@ -12,27 +12,31 @@ public partial class JniPeerMembers { private bool isInterface; public JniPeerMembers (string jniPeerTypeName, Type managedPeerType, bool isInterface) - : this (jniPeerTypeName = GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: true, isInterface: isInterface) + : this (jniPeerTypeName, GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: true, isInterface: isInterface) { } public JniPeerMembers (string jniPeerTypeName, Type managedPeerType) - : this (jniPeerTypeName = GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: true, isInterface: false) + : this (jniPeerTypeName, GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: true, isInterface: false) { } static string GetReplacementType (string jniPeerTypeName) { + if (jniPeerTypeName == null) + throw new ArgumentNullException (nameof (jniPeerTypeName)); var replacement = JniEnvironment.Runtime.TypeManager.GetReplacementType (jniPeerTypeName); if (replacement != null) return replacement; return jniPeerTypeName; } - JniPeerMembers (string jniPeerTypeName, Type managedPeerType, bool checkManagedPeerType, bool isInterface = false) + JniPeerMembers (string originalJniPeerTypeName, string jniPeerTypeName, Type managedPeerType, bool checkManagedPeerType, bool isInterface = false) { if (jniPeerTypeName == null) throw new ArgumentNullException (nameof (jniPeerTypeName)); + if (originalJniPeerTypeName == null) + throw new ArgumentNullException (nameof (originalJniPeerTypeName)); if (checkManagedPeerType) { if (managedPeerType == null) @@ -41,8 +45,10 @@ static string GetReplacementType (string jniPeerTypeName) throw new ArgumentException ("'managedPeerType' must implement the IJavaPeerable interface.", nameof (managedPeerType)); #if DEBUG + // The managed type still declares its *original* JNI name, so compare against that + // and not against the (possibly remapped) name used to look the type up. var signatureFromType = JniEnvironment.Runtime.TypeManager.GetTypeSignature (managedPeerType); - if (signatureFromType.SimpleReference != jniPeerTypeName) { + if (signatureFromType.SimpleReference != originalJniPeerTypeName) { Debug.WriteLine ("WARNING-Java.Interop: ManagedPeerType <=> JniTypeName Mismatch! javaVM.GetJniTypeInfoForType(typeof({0})).JniTypeName=\"{1}\" != \"{2}\"", managedPeerType.FullName, signatureFromType.SimpleReference, @@ -53,6 +59,7 @@ static string GetReplacementType (string jniPeerTypeName) } JniPeerTypeName = jniPeerTypeName; + JniPeerOriginalTypeName = originalJniPeerTypeName; ManagedPeerType = managedPeerType; this.isInterface = isInterface; @@ -65,7 +72,7 @@ static string GetReplacementType (string jniPeerTypeName) static JniPeerMembers CreatePeerMembers (string jniPeerTypeName, Type managedPeerType) { - return new JniPeerMembers (jniPeerTypeName, managedPeerType, checkManagedPeerType: false); + return new JniPeerMembers (jniPeerTypeName, GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: false); } JniType? jniPeerType; @@ -75,7 +82,14 @@ static JniPeerMembers CreatePeerMembers (string jniPeerTypeName, Type managedPee JniStaticFields staticFields; public Type ManagedPeerType {get; private set;} + + /// The JNI type name used to look the peer type up at runtime. This is the + /// remapped name when the type was renamed in the packaged application. public string JniPeerTypeName {get; private set;} + + /// The JNI type name the managed peer type declares. Member replacements are keyed + /// by it, because the mapping describes the original names. + internal string JniPeerOriginalTypeName {get; private set;} public JniType JniPeerType { get { var t = JniType.GetCachedJniType (ref jniPeerType, JniPeerTypeName); @@ -141,6 +155,77 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value) return isInterface ? this : value.JniPeerMembers; } + // + // Member replacements are described in terms of the JNI names the managed code declares, so + // `sourceJniTypeName` is the natural key. Remapping inputs which predate type renaming being + // applied to member entries - the Intune/MAM mapping - instead key them by the replaced + // name, so that is tried as well. + // + internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo ( + string sourceJniTypeName, + string effectiveJniTypeName, + Type managedPeerType, + string method, + string signature, + bool searchBaseTypes = true) + { + var typeManager = JniEnvironment.Runtime.TypeManager; + var info = typeManager.GetReplacementMethodInfo (sourceJniTypeName, method, signature); + if (info == null && !string.Equals (sourceJniTypeName, effectiveJniTypeName, StringComparison.Ordinal)) { + info = typeManager.GetReplacementMethodInfo (effectiveJniTypeName, method, signature); + } + if (info == null && searchBaseTypes) { + for (Type? baseType = managedPeerType.BaseType; baseType != null; baseType = baseType.BaseType) { + var baseSignature = typeManager.GetTypeSignature (baseType); + string? effectiveBaseType = baseSignature.SimpleReference; + if (effectiveBaseType == null) { + continue; + } + string sourceBaseType = typeManager.GetOriginalType (effectiveBaseType) ?? effectiveBaseType; + info = typeManager.GetReplacementMethodInfo (sourceBaseType, method, signature); + if (info == null && !string.Equals (sourceBaseType, effectiveBaseType, StringComparison.Ordinal)) { + info = typeManager.GetReplacementMethodInfo (effectiveBaseType, method, signature); + } + if (info != null) { + break; + } + } + } + return info; + } + + internal static JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo ( + string sourceJniTypeName, + string effectiveJniTypeName, + Type managedPeerType, + string field, + string signature) + { + var typeManager = JniEnvironment.Runtime.TypeManager; + var info = typeManager.GetReplacementFieldInfo (sourceJniTypeName, field, signature); + if (info == null && !string.Equals (sourceJniTypeName, effectiveJniTypeName, StringComparison.Ordinal)) { + info = typeManager.GetReplacementFieldInfo (effectiveJniTypeName, field, signature); + } + if (info == null) { + for (Type? baseType = managedPeerType.BaseType; baseType != null; baseType = baseType.BaseType) { + var baseSignature = typeManager.GetTypeSignature (baseType); + string? effectiveBaseType = baseSignature.SimpleReference; + if (effectiveBaseType == null) { + continue; + } + string sourceBaseType = typeManager.GetOriginalType (effectiveBaseType) ?? effectiveBaseType; + info = typeManager.GetReplacementFieldInfo (sourceBaseType, field, signature); + if (info == null && !string.Equals (sourceBaseType, effectiveBaseType, StringComparison.Ordinal)) { + info = typeManager.GetReplacementFieldInfo (effectiveBaseType, field, signature); + } + if (info != null) { + break; + } + } + } + return info; + } + internal static void AssertSelf (IJavaPeerable self) { if (self == null) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs index 6612957175d..dc9f22c0f21 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs @@ -77,6 +77,61 @@ public override string ToString () public static bool operator!=(ReplacementMethodInfo a, ReplacementMethodInfo b) => !a.Equals (b); } + [SuppressMessage ("Design", "CA1034:Nested types should not be visible", + Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`; see 045b8af7.")] + public struct ReplacementFieldInfo : IEquatable + { + public string? SourceJniType {get; set;} + public string? SourceJniFieldName {get; set;} + public string? SourceJniFieldSignature {get; set;} + public string? TargetJniType {get; set;} + public string? TargetJniFieldName {get; set;} + public string? TargetJniFieldSignature {get; set;} + + public override bool Equals (object? obj) + { + if (obj is ReplacementFieldInfo o) { + return Equals (o); + } + return false; + } + + public bool Equals (ReplacementFieldInfo other) + { + return string.Equals (SourceJniType, other.SourceJniType) && + string.Equals (SourceJniFieldName, other.SourceJniFieldName) && + string.Equals (SourceJniFieldSignature, other.SourceJniFieldSignature) && + string.Equals (TargetJniType, other.TargetJniType) && + string.Equals (TargetJniFieldName, other.TargetJniFieldName) && + string.Equals (TargetJniFieldSignature, other.TargetJniFieldSignature); + } + + public override int GetHashCode () + { + return (SourceJniType?.GetHashCode () ?? 0) ^ + (SourceJniFieldName?.GetHashCode () ?? 0) ^ + (SourceJniFieldSignature?.GetHashCode () ?? 0) ^ + (TargetJniType?.GetHashCode () ?? 0) ^ + (TargetJniFieldName?.GetHashCode () ?? 0) ^ + (TargetJniFieldSignature?.GetHashCode () ?? 0); + } + + public override string ToString () + { + return $"{nameof (ReplacementFieldInfo)} {{ " + + $"{nameof (SourceJniType)} = \"{SourceJniType}\"" + + $", {nameof (SourceJniFieldName)} = \"{SourceJniFieldName}\"" + + $", {nameof (SourceJniFieldSignature)} = \"{SourceJniFieldSignature}\"" + + $", {nameof (TargetJniType)} = \"{TargetJniType}\"" + + $", {nameof (TargetJniFieldName)} = \"{TargetJniFieldName}\"" + + $", {nameof (TargetJniFieldSignature)} = \"{TargetJniFieldSignature}\"" + + $"}}"; + } + + public static bool operator==(ReplacementFieldInfo a, ReplacementFieldInfo b) => a.Equals (b); + public static bool operator!=(ReplacementFieldInfo a, ReplacementFieldInfo b) => !a.Equals (b); + } + /// public partial class JniTypeManager : IDisposable, ISetRuntime { @@ -250,6 +305,15 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type) protected virtual string? GetReplacementTypeCore (string jniSimpleReference) => null; + internal string? GetOriginalType (string jniSimpleReference) + { + AssertValid (); + AssertSimpleReference (jniSimpleReference, nameof (jniSimpleReference)); + return GetOriginalTypeCore (jniSimpleReference); + } + + protected virtual string? GetOriginalTypeCore (string jniSimpleReference) => null; + public IReadOnlyList? GetStaticMethodFallbackTypes (string jniSimpleReference) { AssertValid (); @@ -274,6 +338,22 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type) protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, string jniMethodName, string jniMethodSignature) => null; + public ReplacementFieldInfo? GetReplacementFieldInfo (string jniSimpleReference, string jniFieldName, string jniFieldSignature) + { + AssertValid (); + AssertSimpleReference (jniSimpleReference, nameof (jniSimpleReference)); + if (string.IsNullOrEmpty (jniFieldName)) { + throw new ArgumentNullException (nameof (jniFieldName)); + } + if (string.IsNullOrEmpty (jniFieldSignature)) { + throw new ArgumentNullException (nameof (jniFieldSignature)); + } + + return GetReplacementFieldInfoCore (jniSimpleReference, jniFieldName, jniFieldSignature); + } + + protected virtual ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSimpleReference, string jniFieldName, string jniFieldSignature) => null; + // Default implementation is a no-op. Derived classes (e.g. `ReflectionJniTypeManager`) // provide reflection-based registration. Override to provide custom registration. public virtual void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs index 8f8a47e3f9b..f515c53d536 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs @@ -342,6 +342,8 @@ IEnumerable CreateGetTypesForSimpleReferenceEnumerator (string jniSimpleRe protected override ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, string jniMethodName, string jniMethodSignature) => null; + protected override ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSimpleReference, string jniFieldName, string jniFieldSignature) => null; + public override void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) { TryRegisterNativeMembers (nativeClass, type, methods); diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs index e6a39706223..40e3cf257a0 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs @@ -209,6 +209,58 @@ public JniFieldInfo GetInstanceField (string name, string signature) return JniEnvironment.InstanceFields.GetFieldID (PeerReference, name, signature); } + internal bool TryGetInstanceField (string name, string signature, [NotNullWhen(true)] out JniFieldInfo? field) + { + AssertValid (); + + var env = JniEnvironment.EnvironmentPointer; + var id = RawGetFieldID (env, name, signature, isStatic: false, out var thrown); + return TryCreateFieldInfo (env, name, signature, id, thrown, isStatic: false, out field); + } + + internal bool TryGetStaticField (string name, string signature, [NotNullWhen(true)] out JniFieldInfo? field) + { + AssertValid (); + + var env = JniEnvironment.EnvironmentPointer; + var id = RawGetFieldID (env, name, signature, isStatic: true, out var thrown); + return TryCreateFieldInfo (env, name, signature, id, thrown, isStatic: true, out field); + } + + IntPtr RawGetFieldID (IntPtr env, string name, string signature, bool isStatic, out IntPtr thrown) + { + var _name = Marshal.StringToCoTaskMemUTF8 (name); + var _sig = Marshal.StringToCoTaskMemUTF8 (signature); + try { + var id = isStatic + ? JniNativeMethods.GetStaticFieldID (env, PeerReference.Handle, _name, _sig) + : JniNativeMethods.GetFieldID (env, PeerReference.Handle, _name, _sig); + thrown = JniNativeMethods.ExceptionOccurred (env); + return id; + } + finally { + Marshal.ZeroFreeCoTaskMemUTF8 (_name); + Marshal.ZeroFreeCoTaskMemUTF8 (_sig); + } + } + + static bool TryCreateFieldInfo (IntPtr env, string name, string signature, IntPtr id, IntPtr thrown, bool isStatic, [NotNullWhen(true)] out JniFieldInfo? field) + { + field = null; + if (thrown != IntPtr.Zero) { + JniEnvironment.Exceptions.ExceptionClear (); + JniEnvironment.References.RawDeleteLocalRef (env, thrown); + return false; + } + Debug.Assert (id != IntPtr.Zero); + if (id == IntPtr.Zero) { + // …huh? Should only happen if `thrown != IntPtr.Zero`, handled above. + return false; + } + field = new JniFieldInfo (name, signature, id, isStatic); + return true; + } + public JniFieldInfo GetCachedInstanceField ([NotNull] ref JniFieldInfo? cachedField, string name, string signature) { AssertValid (); diff --git a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt index 48024f01427..5f4294251ac 100644 --- a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt @@ -118,3 +118,27 @@ override Java.Interop.JniRuntime.ReflectionJniTypeManager.RegisterNativeMembers( override Java.Interop.JniRuntime.ReflectionJniTypeManager.RegisterNativeMembers(Java.Interop.JniType! nativeClass, System.Type! type, System.ReadOnlySpan methods) -> void virtual Java.Interop.JniRuntime.ReflectionJniValueManager.TryConstructPeer(Java.Interop.IJavaPeerable! self, ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options, System.Type! type) -> bool virtual Java.Interop.JniRuntime.ReflectionJniValueManager.CreateNonArrayListValue(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options, System.Type! targetType) -> object? +Java.Interop.JniRuntime.JniTypeManager.GetReplacementFieldInfo(string! jniSimpleReference, string! jniFieldName, string! jniFieldSignature) -> Java.Interop.JniRuntime.ReplacementFieldInfo? +Java.Interop.JniRuntime.ReplacementFieldInfo +Java.Interop.JniRuntime.ReplacementFieldInfo.Equals(Java.Interop.JniRuntime.ReplacementFieldInfo other) -> bool +Java.Interop.JniRuntime.ReplacementFieldInfo.ReplacementFieldInfo() -> void +Java.Interop.JniRuntime.ReplacementFieldInfo.SourceJniFieldName.get -> string? +Java.Interop.JniRuntime.ReplacementFieldInfo.SourceJniFieldName.set -> void +Java.Interop.JniRuntime.ReplacementFieldInfo.SourceJniFieldSignature.get -> string? +Java.Interop.JniRuntime.ReplacementFieldInfo.SourceJniFieldSignature.set -> void +Java.Interop.JniRuntime.ReplacementFieldInfo.SourceJniType.get -> string? +Java.Interop.JniRuntime.ReplacementFieldInfo.SourceJniType.set -> void +Java.Interop.JniRuntime.ReplacementFieldInfo.TargetJniFieldName.get -> string? +Java.Interop.JniRuntime.ReplacementFieldInfo.TargetJniFieldName.set -> void +Java.Interop.JniRuntime.ReplacementFieldInfo.TargetJniFieldSignature.get -> string? +Java.Interop.JniRuntime.ReplacementFieldInfo.TargetJniFieldSignature.set -> void +Java.Interop.JniRuntime.ReplacementFieldInfo.TargetJniType.get -> string? +Java.Interop.JniRuntime.ReplacementFieldInfo.TargetJniType.set -> void +override Java.Interop.JniRuntime.ReflectionJniTypeManager.GetReplacementFieldInfoCore(string! jniSimpleReference, string! jniFieldName, string! jniFieldSignature) -> Java.Interop.JniRuntime.ReplacementFieldInfo? +override Java.Interop.JniRuntime.ReplacementFieldInfo.Equals(object? obj) -> bool +override Java.Interop.JniRuntime.ReplacementFieldInfo.GetHashCode() -> int +override Java.Interop.JniRuntime.ReplacementFieldInfo.ToString() -> string! +static Java.Interop.JniRuntime.ReplacementFieldInfo.operator !=(Java.Interop.JniRuntime.ReplacementFieldInfo a, Java.Interop.JniRuntime.ReplacementFieldInfo b) -> bool +static Java.Interop.JniRuntime.ReplacementFieldInfo.operator ==(Java.Interop.JniRuntime.ReplacementFieldInfo a, Java.Interop.JniRuntime.ReplacementFieldInfo b) -> bool +virtual Java.Interop.JniRuntime.JniTypeManager.GetReplacementFieldInfoCore(string! jniSimpleReference, string! jniFieldName, string! jniFieldSignature) -> Java.Interop.JniRuntime.ReplacementFieldInfo? +virtual Java.Interop.JniRuntime.JniTypeManager.GetOriginalTypeCore(string! jniSimpleReference) -> string? diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs index 99004f98c2b..66090d7ad1a 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs @@ -130,8 +130,34 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type) // NOTE: key must use *post-renamed* value, not pre-renamed value // NOTE: SourceSignature lacking return type; "closer in spirit" to what `remapping-config.json` allows [("net/dot/jni/test/RenameClassBase2", "hashCode", "()")] = ("net/dot/jni/test/RenameClassBase2", "myNewHashCode", null, null, false), + + // Renamed parameter types: the target descriptor is pinned explicitly, which is what + // `target-method-signature` carries. + [("java/lang/StringBuilder", "", "(Lnet/dot/jni/test/RenamedInt;)V")] = (null, "", "(I)V", null, false), + [("java/lang/StringBuilder", "indexOf", "(Lnet/dot/jni/test/RenamedString;)I")] = (null, "indexOf", "(Ljava/lang/String;)I", null, false), + }; + + Dictionary<(string SourceType, string SourceName, string? SourceSignature), (string? TargetType, string? TargetName, string? TargetSignature)> ReplacementFields = new() { + [("java/lang/Math", "remappedToPi", "D")] = (null, "PI", null), + [("java/io/ByteArrayInputStream", "remappedToPos", "I")] = (null, "pos", null), }; + protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, string jniFieldName, string jniFieldSignature) + { + if (!ReplacementFields.TryGetValue ((jniSourceType, jniFieldName, jniFieldSignature), out var r) && + !ReplacementFields.TryGetValue ((jniSourceType, jniFieldName, null), out r)) { + return null; + } + return new JniRuntime.ReplacementFieldInfo { + SourceJniType = jniSourceType, + SourceJniFieldName = jniFieldName, + SourceJniFieldSignature = jniFieldSignature, + TargetJniType = r.TargetType ?? jniSourceType, + TargetJniFieldName = r.TargetName ?? jniFieldName, + TargetJniFieldSignature = r.TargetSignature ?? jniFieldSignature, + }; + } + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) { // Console.Error.WriteLine ($"# jonp: looking for replacement method for (\"{jniSourceType}\", \"{jniMethodName}\", \"{jniMethodSignature}\")"); diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs index 45b2a3cb6e1..97cd8caff55 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs @@ -75,6 +75,48 @@ public void MethodLookupForNonexistentStaticMethodWillTryFallbacks () } } + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void ReplaceStaticFieldName () + { + // Resolves `java.lang.Math.PI`, not the nonexistent `remappedToPi`. + var info = JavaLangRemappingTestMath._members.StaticFields.GetFieldInfo ("remappedToPi.D"); + Assert.IsNotNull (info); + Assert.IsTrue (info.IsStatic); + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void ReplaceInstanceFieldName () + { + // Resolves `java.io.ByteArrayInputStream.pos`, not the nonexistent `remappedToPos`. + var info = JavaIoRemappingTestStream._members.InstanceFields.GetFieldInfo ("remappedToPos.I"); + Assert.IsNotNull (info); + Assert.IsFalse (info.IsStatic); + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void ReplacementConstructorUsesTargetSignature () + { + // The declared parameter type does not exist; the replacement pins `([C)V` instead. + var ctor = JavaLangRemappingTestStringBuilder._members.InstanceMethods.GetConstructor ("(Lnet/dot/jni/test/RenamedInt;)V"); + Assert.IsNotNull (ctor); + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void ReplacementMethodUsesTargetSignature () + { + // The declared parameter type does not exist; the replacement pins `(Ljava/lang/String;)I` instead. + var method = JavaLangRemappingTestStringBuilder._members.InstanceMethods.GetMethodInfo ("indexOf.(Lnet/dot/jni/test/RenamedString;)I"); + Assert.IsNotNull (method); + } + [Test] [Category ("NativeAOTIgnore")] [Category ("TrimmableTypeMapUnsupported")] @@ -217,6 +259,24 @@ public unsafe int remappedToStaticHashCode () } } + [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] + class JavaLangRemappingTestMath : JavaObject { + internal const string JniTypeName = "java/lang/Math"; + internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaLangRemappingTestMath)); + } + + [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] + class JavaIoRemappingTestStream : JavaObject { + internal const string JniTypeName = "java/io/ByteArrayInputStream"; + internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaIoRemappingTestStream)); + } + + [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] + class JavaLangRemappingTestStringBuilder : JavaObject { + internal const string JniTypeName = "java/lang/StringBuilder"; + internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaLangRemappingTestStringBuilder)); + } + [JniTypeSignature (JavaLangRemappingTestRuntime.JniTypeName, GenerateJavaPeer=false)] internal class JavaLangRemappingTestRuntime : JavaObject { internal const string JniTypeName = "java/lang/Runtime"; diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index ee2772e654d..e976e300bbe 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -381,6 +381,11 @@ protected override IEnumerable GetSimpleReferences (Type type) return JniRemappingLookup.GetReplacementType (jniSimpleReference); } + protected override string? GetOriginalTypeCore (string jniSimpleReference) + { + return JniRemappingLookup.GetReverseType (jniSimpleReference); + } + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) { return JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); diff --git a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs index a2a59d33ba4..a8bddadf4d7 100644 --- a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs +++ b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs @@ -68,6 +68,14 @@ internal unsafe static partial class RuntimeNativeMethods [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] internal static partial IntPtr _monodroid_lookup_replacement_method_info (string jniSourceType, string jniMethodName, string jniMethodSignature); + [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)] + [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] + internal static partial IntPtr _monodroid_lookup_reverse_type (string jniSimpleReference); + + [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)] + [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] + internal static partial IntPtr _monodroid_lookup_replacement_field_info (string jniSourceType, string jniFieldName, string jniFieldSignature); + [LibraryImport (RuntimeConstants.InternalDllName)] [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs index 4f0c5eeea99..7fe7172f701 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs @@ -11,12 +11,23 @@ namespace Microsoft.Android.Runtime; static class JniRemappingLookup { #pragma warning disable CS0649 // Field 'JniRemappingLookup.JniRemappingReplacementMethod.target_type' is never assigned to, and will always have its default value null + // Keep in sync with `JniRemappingReplacementMethod` in src/native/clr/include/xamarin-app.hh struct JniRemappingReplacementMethod { public string? target_type; public string? target_name; + public string? target_signature; + [MarshalAs (UnmanagedType.I1)] public bool is_static; } + + // Keep in sync with `JniRemappingReplacementField` in src/native/clr/include/xamarin-app.hh + struct JniRemappingReplacementField + { + public string? target_type; + public string? target_name; + public string? target_signature; + } #pragma warning restore CS0649 internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSimpleReference, bool useReplacementTypes) @@ -56,6 +67,24 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi return Marshal.PtrToStringAnsi (ret); } + /// + /// Maps a JNI type name as it exists in the packaged application back onto the name the managed + /// code declares. Used by Java-to-managed lookups. + /// + internal static string? GetReverseType (string? jniSimpleReference) + { + if (jniSimpleReference is null || !JNIEnvInit.jniRemappingInUse) { + return null; + } + + IntPtr ret = RuntimeNativeMethods._monodroid_lookup_reverse_type (jniSimpleReference); + if (ret == IntPtr.Zero) { + return null; + } + + return Marshal.PtrToStringAnsi (ret); + } + internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (string jniSourceType, string jniMethodName, string jniMethodSignature) { if (!JNIEnvInit.jniRemappingInUse) { @@ -72,12 +101,17 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi $"JNI remapping entry for `{jniSourceType}.{jniMethodName}{jniMethodSignature}` is missing a target type."); var targetName = method.target_name ?? throw new InvalidOperationException ( $"JNI remapping entry for `{jniSourceType}.{jniMethodName}{jniMethodSignature}` is missing a target method name."); - var newSignature = jniMethodSignature; + // The mapping may pin the target descriptor explicitly (its parameter and return types can + // have been renamed too). When it does not, the source signature is kept, which is what + // remapping inputs predating `target-method-signature` rely on. + var newSignature = method.target_signature ?? jniMethodSignature; int? paramCount = null; if (method.is_static) { paramCount = JniMemberSignature.GetParameterCountFromMethodSignature (jniMethodSignature) + 1; - newSignature = $"(L{jniSourceType};" + jniMethodSignature.Substring ("(".Length); + if (method.target_signature is null) { + newSignature = $"(L{jniSourceType};" + jniMethodSignature.Substring ("(".Length); + } } if (Logger.LogAssembly) { @@ -98,4 +132,38 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi TargetJniMethodInstanceToStatic = method.is_static, }; } + + internal static JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo (string jniSourceType, string jniFieldName, string jniFieldSignature) + { + if (!JNIEnvInit.jniRemappingInUse) { + return null; + } + + IntPtr retInfo = RuntimeNativeMethods._monodroid_lookup_replacement_field_info (jniSourceType, jniFieldName, jniFieldSignature); + if (retInfo == IntPtr.Zero) { + return null; + } + + var field = Marshal.PtrToStructure (retInfo); + var targetType = field.target_type ?? throw new InvalidOperationException ( + $"JNI remapping entry for `{jniSourceType}.{jniFieldName}` is missing a target type."); + var targetName = field.target_name ?? throw new InvalidOperationException ( + $"JNI remapping entry for `{jniSourceType}.{jniFieldName}` is missing a target field name."); + var targetSignature = field.target_signature ?? jniFieldSignature; + + if (Logger.LogAssembly) { + var message = $"Remapping field `{jniSourceType}.{jniFieldName}:{jniFieldSignature}` to " + + $"`{targetType}.{targetName}:{targetSignature}`"; + Logger.Log (LogLevel.Debug, "monodroid-assembly", message); + } + + return new JniRuntime.ReplacementFieldInfo { + SourceJniType = jniSourceType, + SourceJniFieldName = jniFieldName, + SourceJniFieldSignature = jniFieldSignature, + TargetJniType = targetType, + TargetJniFieldName = targetName, + TargetJniFieldSignature = targetSignature, + }; + } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index ca6be17f1c5..b4a0db29ad9 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -173,6 +173,7 @@ internal static JavaPeerProxy[] GetProxyArrayCacheEntry (object cacheEntry) /// JavaPeerProxy? GetProxyForJniClass (string className, Type? targetType) { + className = JniRemappingLookup.GetReverseType (className) ?? className; var cacheEntry = GetProxyCacheEntryForJniName (className); if (cacheEntry is JavaPeerProxy singleProxy) { return targetType is null || TargetTypeMatches (targetType, singleProxy.TargetType) @@ -267,7 +268,8 @@ bool TryResolveProxyFromSealedTargetType ( var targetClass = default (JniObjectReference); try { - targetClass = JniEnvironment.Types.FindClass (targetProxy.JniName); + string runtimeJniName = JniRemappingLookup.GetReplacementType (targetProxy.JniName) ?? targetProxy.JniName; + targetClass = JniEnvironment.Types.FindClass (runtimeJniName); var reference = new JniObjectReference (handle); if (JniEnvironment.Types.IsInstanceOf (reference, targetClass)) { proxy = targetProxy; @@ -403,7 +405,8 @@ static JniMethodInfo GetClassGetInterfacesMethod () try { objClass = JniEnvironment.Types.GetObjectClass (selfRef); try { - targetClass = JniEnvironment.Types.FindClass (targetJniName); + string runtimeJniName = JniRemappingLookup.GetReplacementType (targetJniName) ?? targetJniName; + targetClass = JniEnvironment.Types.FindClass (runtimeJniName); } catch (Java.Lang.ClassNotFoundException) { // FindClass throws for managed types whose Java peer class is // not present in the APK (e.g. test types annotated with diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 3f0528a034a..a53a02ee4dd 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -199,6 +199,14 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl foreach (var type in TrimmableTypeMap.Instance.GetTargetTypes (jniSimpleReference)) { yield return type; } + + // The type map is keyed by the JNI names the managed code declares, so a name that was + // renamed in the packaged application has to be translated back first. + if (GetOriginalSimpleReference (jniSimpleReference) is string originalReference) { + foreach (var type in TrimmableTypeMap.Instance.GetTargetTypes (originalReference)) { + yield return type; + } + } } protected override Type? GetTypeForSimpleReference (string jniSimpleReference) @@ -214,9 +222,24 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl return type; } + if (GetOriginalSimpleReference (jniSimpleReference) is string originalReference && + TrimmableTypeMap.Instance.TryGetTargetType (originalReference, out type)) { + return type; + } + return null; } + static string? GetOriginalSimpleReference (string jniSimpleReference) + { + var original = JniRemappingLookup.GetReverseType (jniSimpleReference); + if (original is null || string.Equals (original, jniSimpleReference, StringComparison.Ordinal)) { + return null; + } + + return original; + } + // Lookup of the built-in managed type for a JNI simple reference, e.g., string, bool?, int?, etc. static Type? GetBuiltInTypeForSimpleReference (string jniSimpleReference) { @@ -271,7 +294,8 @@ static JniTypeSignature GetTypeSignatureUncached (Type type) while (currentType is not null) { if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (currentType, out var jniName)) { - return new (jniName, rank, keyword: false); + string runtimeJniName = JniRemappingLookup.GetReplacementType (jniName) ?? jniName; + return new (runtimeJniName, rank, keyword: false); } currentType = currentType.BaseType; @@ -370,7 +394,7 @@ protected override IEnumerable GetTypeSignaturesCore (Type typ return signature.IsValid ? [signature] : []; } - // Remapping APIs for InTune support + // Remapping APIs, used by the Intune/MAM mapping and by R8 JNI runtime remapping protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) => JniRemappingLookup.GetStaticMethodFallbackTypes (jniSimpleReference, useReplacementTypes: true); @@ -378,9 +402,15 @@ protected override IEnumerable GetTypeSignaturesCore (Type typ protected override string? GetReplacementTypeCore (string jniSimpleReference) => JniRemappingLookup.GetReplacementType (jniSimpleReference); + protected override string? GetOriginalTypeCore (string jniSimpleReference) + => JniRemappingLookup.GetReverseType (jniSimpleReference); + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) => JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); + protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, string jniFieldName, string jniFieldSignature) + => JniRemappingLookup.GetReplacementFieldInfo (jniSourceType, jniFieldName, jniFieldSignature); + // The rest of the APIs are unsupported - they are not needed internally anywhere anyway protected override Type? GetInvokerTypeCore (Type type) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs index b2344f8fd2a..483ee7b3d22 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs @@ -192,7 +192,8 @@ static bool IsIncompatibleCast ( var instanceClass = JniEnvironment.Types.GetObjectClass (reference); JniObjectReference targetClass = default; try { - targetClass = JniEnvironment.Types.FindClass (targetJniName); + string runtimeJniName = JniRemappingLookup.GetReplacementType (targetJniName) ?? targetJniName; + targetClass = JniEnvironment.Types.FindClass (runtimeJniName); if (!JniEnvironment.Types.IsAssignableFrom (instanceClass, targetClass)) { // Match the legacy cast diagnostic when assembly logging is enabled. diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets index eed1d4765df..8cdb8db6242 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets @@ -209,7 +209,6 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. <_ProtobufFormat Condition=" '$(AndroidPackageFormat)' == 'aab' ">True <_ProtobufFormat Condition=" '$(_ProtobufFormat)' == '' ">False - <_Aapt2ProguardRules Condition=" '$(AndroidLinkTool)' != '' ">$(IntermediateOutputPath)aapt_rules.txt <_OutputFileDir>$([System.IO.Path]::GetDirectoryName ('$(_PackagedResources)')) @@ -244,10 +243,5 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. UncompressedFileExtensions="$(AndroidStoreUncompressedFileExtensions)" ProguardRuleOutput="$(_Aapt2ProguardRules)" /> - - - - - diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets index aa9074cb6ff..7424ec4db2a 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets @@ -241,6 +241,7 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. DebugBuild="$(AndroidIncludeDebugSymbols)" WorkingDirectory="$(_NativeAssemblySourceDir)" AndroidBinUtilsDirectory="$(AndroidBinUtilsDirectory)" /> + @@ -263,8 +264,8 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. --> <_AndroidNativeAotSharedLibrary>$(NativeOutputPath)$(NativeBinaryPrefix)$(TargetName).so @@ -319,6 +320,7 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. <_NativeAotLinkLibraries Include="@(NativeLibrary)" /> <_NativeAotAdditionalObjects Include="@(_PrivateJniInitFuncsNativeObjectFile)" /> <_NativeAotAdditionalObjects Include="@(_PrivateEnvironmentNativeObjectFile)" /> + <_NativeAotAdditionalObjects Include="@(_AndroidNativeAotR8RemappingObject)" /> <_NativeAotSystemLibraries Include="dl" /> <_NativeAotSystemLibraries Include="z" /> <_NativeAotSystemLibraries Include="log" /> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.R8JniRemapping.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.R8JniRemapping.targets new file mode 100644 index 00000000000..335b874a164 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.R8JniRemapping.targets @@ -0,0 +1,295 @@ + + + + + + + + + + <_AndroidR8JniSeedDirectory>$(_TypeMapBaseOutputDir)r8-jni-seed/ + <_AndroidR8JniSeedMapping>$(_AndroidR8JniSeedDirectory)mapping.txt + <_AndroidR8JniManifestProguardConfiguration>$(_AndroidR8JniSeedDirectory)manifest_rules.txt + <_AndroidR8JniSeedXamarinConfiguration>$(_AndroidR8JniSeedDirectory)xamarin.cfg + <_AndroidR8JniSeedJavaClassDirectory>$(_AndroidR8JniSeedDirectory)classes/ + <_AndroidR8JniSeedJavaStamp>$(_AndroidR8JniSeedDirectory)compile-java.stamp + <_AndroidR8JniRemappingXml>$(_TypeMapBaseOutputDir)r8-jni-remap.xml + + <_PreTrimTypeMapAcwMapOutputFile Condition=" '$(_AndroidRuntime)' == 'CoreCLR' and '$(_PreTrimTypeMapAcwMapOutputFile)' == '' ">$(_AndroidR8JniSeedDirectory)acw-map.txt + <_PreTrimTypeMapApplicationRegistrationOutputFile Condition=" '$(_AndroidRuntime)' == 'CoreCLR' and '$(_PreTrimTypeMapApplicationRegistrationOutputFile)' == '' ">$(_AndroidR8JniSeedDirectory)java/net/dot/android/ApplicationRegistration.java + <_AndroidR8JniSeedAcwMap>$(_PreTrimTypeMapAcwMapOutputFile) + <_AndroidR8JniSeedApplicationRegistration>$(_PreTrimTypeMapApplicationRegistrationOutputFile) + <_AndroidR8JniSeedAcwMap Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">$(_TypeMapBaseOutputDir)acw-map.txt + <_AndroidR8JniSeedApplicationRegistration Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">$(_TypeMapBaseOutputDir)android/src/net/dot/android/ApplicationRegistration.java + + + + + <_AndroidR8JniSeedJavaSource Include="$(_TypeMapJavaOutputDirectory)/**/*.java" /> + <_AndroidR8JniSeedJavaSource Include="$(_AndroidR8JniSeedApplicationRegistration)" + Condition=" '$(_AndroidR8JniSeedApplicationRegistration)' != '' " /> + + + + + + + + + + + + + + + + + + + + + + <_AndroidR8JniMergedManifest>$(_TypeMapBaseOutputDir)AndroidManifest.xml + <_AndroidR8JniMergedManifest Condition=" '$(AndroidManifestMerger)' == 'manifestmerger.jar' ">$(IntermediateOutputPath)android/AndroidManifest.xml + + + + + + + + + + + + + + + <_AndroidR8JniTaskAssembly>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', '$(_XamarinAndroidBuildTasksAssembly)')) + + + <_AndroidR8JniSeedClassFile Include="$(_AndroidR8JniSeedJavaClassDirectory)**\*.class" /> + <_AndroidR8JniSeedManifestProguardConfiguration + Include="$(_AndroidR8JniManifestProguardConfiguration)" + Condition=" '$(ProguardConfigFiles)' == '' and Exists('$(_AndroidR8JniManifestProguardConfiguration)') " /> + + <_AndroidR8JniSeedProguardConfiguration Include="$(ProguardConfigFiles)" Condition=" '$(ProguardConfigFiles)' != '' " /> + + <_AndroidR8JniSeedProguardConfiguration + Include="$(MSBuildThisFileDirectory)..\tools\proguard-android.txt" + Condition=" '$(ProguardConfigFiles)' == '' " /> + <_AndroidR8JniSeedProguardConfiguration + Include="@(ProguardConfiguration)" + Condition=" '$(ProguardConfigFiles)' == '' and '%(ProguardConfiguration.AndroidGeneratedProguardConfiguration)' != 'true' " /> + <_AndroidR8JniSeedProguardConfiguration + Include="@(_AndroidR8JniSeedManifestProguardConfiguration)" + Condition=" '$(ProguardConfigFiles)' == '' " /> + <_AndroidR8JniSeedMapDiagnostics Condition=" '$(AndroidR8IgnoreWarnings)' == 'true' " Include="warning" To="info" /> + + + + + + + + + + + + + + + + + + + + + + + <_AndroidR8JniExistingRemapMembers Include="@(_AndroidRemapMembers)" + Condition=" '%(Identity)' != '$(_AndroidR8JniRemappingXml)' " /> + + + + + + + + + <_AndroidRemapMembers Include="$(_AndroidR8JniRemappingXml)" /> + + + + + + + + <_AndroidNativeAotR8RemappingDirectory>$(NativeIntermediateOutputPath)jni-remap/ + <_AndroidNativeAotR8GeneratedRemappingXml>$(_AndroidNativeAotR8RemappingDirectory)r8-jni-generated-remap.xml + <_AndroidNativeAotR8RemappingXml>$(_AndroidNativeAotR8RemappingDirectory)r8-jni-remap.xml + + + + + + <_AndroidNativeAotR8RemappingObject Include="@(_AndroidNativeAotR8RemappingSource->'$([System.IO.Path]::ChangeExtension('%(Identity)', '.o'))')"> + %(_AndroidNativeAotR8RemappingSource.abi) + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets index 7bb826023f8..07fabaf1631 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets @@ -446,10 +446,11 @@ diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index b12f80fdc8f..455bc9967ba 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -7,10 +7,6 @@ <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">mono.MonoRuntimeProvider - - <_GenerateProguardAfterTargets Condition=" '$(_GenerateProguardAfterTargets)' == '' ">ILLink - - - + <_LinkedAssemblyForProguard Remove="@(_LinkedAssemblyForProguard)" /> <_LinkedAssemblyForProguard Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> - + + + <_AndroidR8JniRemappingAssembly Remove="@(_AndroidR8JniRemappingAssembly)" /> + <_AndroidR8JniRemappingAssembly Include="@(_LinkedAssemblyForProguard)" /> + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index a485ef744fe..f91d4526340 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -220,7 +220,7 @@ @@ -237,6 +237,7 @@ NativeAotDgmlFiles="@(_TrimmableNativeAotDgmlFiles)" AcwMapFile="$(IntermediateOutputPath)acw-map.txt" TrimJavaCallableWrappers="$(_AndroidTrimmableTypemapTrimJavaCode)" + EnableObfuscation="$(_AndroidR8RuntimeRemappingEnabled)" OutputFile="$(_ProguardProjectConfiguration)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index df55572c604..d7da5d70000 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -539,4 +539,8 @@ + + + diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 1707d8f8507..5e8cbb2a2c9 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1977,6 +1977,186 @@ public static string XA4326 { } } + /// + /// Looks up a localized string similar to Failed to generate the R8 JNI remapping data. {0}. + /// + public static string XA4327 { + get { + return ResourceManager.GetString("XA4327", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The seed R8 pass requires a mapping file output.. + /// + public static string XA4327_SeedMappingOutputRequired { + get { + return ResourceManager.GetString("XA4327_SeedMappingOutputRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The R8 seed mapping file '{0}' was not found.. + /// + public static string XA4327_SeedMappingNotFound { + get { + return ResourceManager.GetString("XA4327_SeedMappingNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The R8 seed mapping file '{0}' could not be read: {1}. + /// + public static string XA4327_MappingDataFailure { + get { + return ResourceManager.GetString("XA4327_MappingDataFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Android manifest '{0}' could not be read: {1}. + /// + public static string XA4327_ManifestReadFailure { + get { + return ResourceManager.GetString("XA4327_ManifestReadFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to NativeAOT JNI retention requires an existing post-ILC NativeAotObjectFile; '{0}' was not found.. + /// + public static string XA4327_NativeAotObjectRequired { + get { + return ResourceManager.GetString("XA4327_NativeAotObjectRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The NativeAOT retention object '{0}' could not be read: {1}. + /// + public static string XA4327_NativeAotObjectReadFailure { + get { + return ResourceManager.GetString("XA4327_NativeAotObjectReadFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to NativeAotObjectFile requires NativeAot=true.. + /// + public static string XA4327_NativeAotModeRequired { + get { + return ResourceManager.GetString("XA4327_NativeAotModeRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Expected a 32-bit or 64-bit little-endian relocatable NativeAOT ELF object.. + /// + public static string XA4327_NativeAotObjectFormat { + get { + return ResourceManager.GetString("XA4327_NativeAotObjectFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The NativeAOT ELF object contains an invalid section extent.. + /// + public static string XA4327_NativeAotInvalidSection { + get { + return ResourceManager.GetString("XA4327_NativeAotInvalidSection", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The NativeAOT ELF object contains truncated section data.. + /// + public static string XA4327_NativeAotTruncatedSection { + get { + return ResourceManager.GetString("XA4327_NativeAotTruncatedSection", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The NativeAOT object must contain allocated __managedcode and initialized data sections.. + /// + public static string XA4327_NativeAotMissingSections { + get { + return ResourceManager.GetString("XA4327_NativeAotMissingSections", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Android manifest '{0}' does not have a <manifest> element with a 'package' attribute.. + /// + public static string XA4327_ManifestPackageMissing { + get { + return ResourceManager.GetString("XA4327_ManifestPackageMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The R8 JNI remapping data is incomplete. {0}. + /// + public static string XA4328 { + get { + return ResourceManager.GetString("XA4328", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The '{0}' entry for '{1}' was not emitted: another JNI remapping input already maps it to '{2}', which conflicts with '{3}'.. + /// + public static string XA4328_ConflictingEntry { + get { + return ResourceManager.GetString("XA4328_ConflictingEntry", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The entry for '{0}' was not emitted: its signature '{1}' could not be converted to a JNI descriptor.. + /// + public static string XA4328_UnsupportedSignature { + get { + return ResourceManager.GetString("XA4328_UnsupportedSignature", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid value for {0}: '{1}'. Valid values are: {2}.. + /// + public static string XA4329 { + get { + return ResourceManager.GetString("XA4329", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to AndroidR8ObfuscationMode='experimental-rewriting' is not available in this SDK. Use 'runtime-remapping' or set AndroidEnableR8Obfuscation=false.. + /// + public static string XA4329_RewritingUnavailable { + get { + return ResourceManager.GetString("XA4329_RewritingUnavailable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to AndroidEnableR8Obfuscation=true requires $({0}) to be '{1}', but it is {2}.. + /// + public static string XA4329_RequiredProperty { + get { + return ResourceManager.GetString("XA4329_RequiredProperty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to AndroidEnableR8Obfuscation=true is not supported for runtime '{0}'. Supported runtimes are CoreCLR and NativeAOT.. + /// + public static string XA4329_UnsupportedRuntime { + get { + return ResourceManager.GetString("XA4329_UnsupportedRuntime", resourceCulture); + } + } + /// /// Looks up a localized string similar to Missing Android NDK toolchains directory '{0}'. Please install the Android NDK.. /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index b8d6772bec5..f33261845c0 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -901,6 +901,109 @@ Remove the '{0}' reference from your project and add the '{1}' NuGet package ins Unable to safely rewrite a JNI member lookup because its class handle does not have one structurally unambiguous JNIEnv.FindClass source. The following are literal API names and should not be translated: JNI, JNIEnv.FindClass. + + Failed to generate the R8 JNI remapping data. {0} + The following are literal names and should not be translated: R8, JNI. +{0} - A sentence describing the specific failure. It is supplied by one of the XA4327_* resources. + + + The seed R8 pass requires a mapping file output. + The following are literal names and should not be translated: R8. + + + The R8 seed mapping file '{0}' was not found. + The following are literal names and should not be translated: R8. +{0} - The path of the missing seed mapping file. + + + The R8 seed mapping file '{0}' could not be read: {1} + The following are literal names and should not be translated: R8. +{0} - The path of the seed mapping file. +{1} - The underlying message describing why the file could not be read. It is not localized. + + + The Android manifest '{0}' could not be read: {1} + The following are literal names and should not be translated: Android. +{0} - The path of the Android manifest. +{1} - The underlying message describing why the manifest could not be read. It is not localized. + + + NativeAOT JNI retention requires an existing post-ILC NativeAotObjectFile; '{0}' was not found. + The following are literal names and should not be translated: NativeAOT, JNI, ILC, NativeAotObjectFile. +{0} - The path of the missing ILC native object, or an empty string if none was supplied. + + + The NativeAOT retention object '{0}' could not be read: {1} + The following is a literal name and should not be translated: NativeAOT. +{0} - The path of the ILC native object. +{1} - The underlying message describing why the object could not be read. + + + NativeAotObjectFile requires NativeAot=true. + The following are literal names and should not be translated: NativeAotObjectFile, NativeAot=true. + + + Expected a 32-bit or 64-bit little-endian relocatable NativeAOT ELF object. + The following are literal names and should not be translated: NativeAOT, ELF. + + + The NativeAOT ELF object contains an invalid section extent. + The following are literal names and should not be translated: NativeAOT, ELF. + + + The NativeAOT ELF object contains truncated section data. + The following are literal names and should not be translated: NativeAOT, ELF. + + + The NativeAOT object must contain allocated __managedcode and initialized data sections. + The following are literal names and should not be translated: NativeAOT, __managedcode. + + + The Android manifest '{0}' does not have a <manifest> element with a 'package' attribute. + The following are literal names and should not be translated: Android, <manifest>, 'package'. +{0} - The path of the Android manifest. + + + The R8 JNI remapping data is incomplete. {0} + The following are literal names and should not be translated: R8, JNI. +{0} - A sentence describing the specific omission. It is supplied by one of the XA4328_* resources. + + + The '{0}' entry for '{1}' was not emitted: another JNI remapping input already maps it to '{2}', which conflicts with '{3}'. + The following are literal names and should not be translated: JNI. +{0} - The XML element name of the conflicting entry, such as replace-type. +{1} - The source type or member the entry describes. +{2} - The target the pre-existing input maps the source to. +{3} - The target this entry would have mapped the source to. + + + The entry for '{0}' was not emitted: its signature '{1}' could not be converted to a JNI descriptor. + The following are literal names and should not be translated: JNI. +{0} - The member the entry describes. +{1} - The Java signature which could not be converted. + + + Invalid value for {0}: '{1}'. Valid values are: {2}. + {0} - The MSBuild property name. +{1} - The invalid property value. +{2} - A comma-separated list of valid literal values. Do not translate these values. + + + AndroidR8ObfuscationMode='experimental-rewriting' is not available in this SDK. Use 'runtime-remapping' or set AndroidEnableR8Obfuscation=false. + The following are literal names and should not be translated: AndroidR8ObfuscationMode, experimental-rewriting, runtime-remapping, AndroidEnableR8Obfuscation, false, SDK. + + + AndroidEnableR8Obfuscation=true requires $({0}) to be '{1}', but it is {2}. + The following are literal names and should not be translated: AndroidEnableR8Obfuscation, true. +{0} - The required MSBuild property name. +{1} - The required literal value. +{2} - The actual value, including quotes. + + + AndroidEnableR8Obfuscation=true is not supported for runtime '{0}'. Supported runtimes are CoreCLR and NativeAOT. + The following are literal names and should not be translated: AndroidEnableR8Obfuscation, true, CoreCLR, NativeAOT. +{0} - The runtime name. + Missing Android NDK toolchains directory '{0}'. Please install the Android NDK. {0} - The path of the missing directory diff --git a/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg b/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg index c12ac57637c..9e59546314c 100644 --- a/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg +++ b/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg @@ -3,10 +3,18 @@ -dontobfuscate -keep class net.dot.jni.** { *; (...); } +-keep class net.dot.android.ApplicationRegistration { *; (...); } -keep class net.dot.android.crypto.** { *; (...); } -# NativeAOT resolves these interface methods through JNI during startup. +# NativeAOT resolves these fields, constructors and interface methods through JNI during startup. +-keep class mono.android.Runtime { *; } +-keep class mono.android.GCUserPeer { (); } -keep class mono.android.IGCUserPeer { *; } +# Keep the seed and final graphs consistent for interface dispatch and resource class names. +-keepclassmembernames interface * { *; } +-keepnames public class * +-keepnames class **$* + -keepclassmembers class * extends android.view.View { *** set*(...); } diff --git a/src/Xamarin.Android.Build.Tasks/Resources/proguard_xamarin.cfg b/src/Xamarin.Android.Build.Tasks/Resources/proguard_xamarin.cfg index 9b16fefd6cf..1e6585b7288 100644 --- a/src/Xamarin.Android.Build.Tasks/Resources/proguard_xamarin.cfg +++ b/src/Xamarin.Android.Build.Tasks/Resources/proguard_xamarin.cfg @@ -7,6 +7,8 @@ -keep class mono.MonoRuntimeProvider* { *; (...); } -keep class mono.MonoPackageManager { *; (...); } -keep class mono.MonoPackageManager_Resources { *; (...); } +# MonoPackageManager calls this package-private helper directly. +-keep class mono.NativeLibraryHelper { *; (...); } -keep class mono.android.** { *; (...); } -keep class mono.java.** { *; (...); } -keep class mono.javax.** { *; (...); } @@ -22,8 +24,16 @@ -keepclassmembers class md52ce486a14f4bcd95899665e9d932190b.** { *; (...); } # .NET runtime +-keep class net.dot.android.ApplicationRegistration { *; (...); } -keep class net.dot.android.crypto.** { *; (...); } +# R8 must keep interface dispatch names aligned across seed and final graphs. +-keepclassmembernames interface * { *; } + +# Binary Android resources and Java package access require these class names to stay stable. +-keepnames public class * +-keepnames class **$* + # Android's template misses fluent setters... -keepclassmembers class * extends android.view.View { *** set*(...); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs index 16cf42c3533..492df8dcec2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs @@ -19,11 +19,16 @@ internal sealed class JniRemappingNativeCodeInfo { public int ReplacementTypeCount { get; } public int ReplacementMethodIndexEntryCount { get; } + public int ReverseTypeCount { get; } + public int ReplacementFieldIndexEntryCount { get; } - public JniRemappingNativeCodeInfo (int replacementTypeCount, int replacementMethodIndexEntryCount) + public JniRemappingNativeCodeInfo (int replacementTypeCount, int replacementMethodIndexEntryCount, + int reverseTypeCount = 0, int replacementFieldIndexEntryCount = 0) { ReplacementTypeCount = replacementTypeCount; ReplacementMethodIndexEntryCount = replacementMethodIndexEntryCount; + ReverseTypeCount = reverseTypeCount; + ReplacementFieldIndexEntryCount = replacementFieldIndexEntryCount; } } @@ -39,6 +44,11 @@ public JniRemappingNativeCodeInfo (int replacementTypeCount, int replacementMeth public bool GenerateEmptyCode { get; set; } + /// Table sizes produced by the last run; exposed for tests and for consumers + /// which cannot reach the registered task object (for example the per-RID NativeAOT + /// build). + internal JniRemappingNativeCodeInfo? NativeCodeInfo { get; private set; } + public override bool RunTask () { if (!GenerateEmptyCode) { @@ -56,13 +66,15 @@ public override bool RunTask () void GenerateEmpty () { - Generate (new JniRemappingAssemblyGenerator (Log), typeReplacementsCount: 0); + Generate (new JniRemappingAssemblyGenerator (Log)); } void Generate (string remappingXmlFilePath) { var typeReplacements = new List (); + var reverseTypeReplacements = new List (); var methodReplacements = new List (); + var fieldReplacements = new List (); var readerSettings = new XmlReaderSettings { XmlResolver = null, @@ -72,14 +84,14 @@ void Generate (string remappingXmlFilePath) if (reader.MoveToContent () != XmlNodeType.Element || reader.LocalName != "replacements") { Log.LogCodedError ("XA1045", Properties.Resources.XA1045, remappingXmlFilePath); } else { - ReadXml (reader, typeReplacements, methodReplacements, remappingXmlFilePath); + ReadXml (reader, typeReplacements, reverseTypeReplacements, methodReplacements, fieldReplacements, remappingXmlFilePath); } } - Generate (new JniRemappingAssemblyGenerator (Log, typeReplacements, methodReplacements), typeReplacements.Count); + Generate (new JniRemappingAssemblyGenerator (Log, typeReplacements, reverseTypeReplacements, methodReplacements, fieldReplacements)); } - void Generate (JniRemappingAssemblyGenerator jniRemappingComposer, int typeReplacementsCount) + void Generate (JniRemappingAssemblyGenerator jniRemappingComposer) { LLVMIR.LlvmIrModule module = jniRemappingComposer.Construct (); @@ -94,14 +106,25 @@ void Generate (JniRemappingAssemblyGenerator jniRemappingComposer, int typeRepla } } + NativeCodeInfo = new JniRemappingNativeCodeInfo ( + jniRemappingComposer.ReplacementTypeCount, + jniRemappingComposer.ReplacementMethodIndexEntryCount, + jniRemappingComposer.ReverseTypeCount, + jniRemappingComposer.ReplacementFieldIndexEntryCount + ); + BuildEngine4.RegisterTaskObjectAssemblyLocal ( ProjectSpecificTaskObjectKey (JniRemappingNativeCodeInfoKey), - new JniRemappingNativeCodeInfo (typeReplacementsCount, jniRemappingComposer.ReplacementMethodIndexEntryCount), + NativeCodeInfo, RegisteredTaskObjectLifetime.Build ); } - void ReadXml (XmlReader reader, List typeReplacements, List methodReplacements, string remappingXmlFilePath) + void ReadXml (XmlReader reader, List typeReplacements, + List reverseTypeReplacements, + List methodReplacements, + List fieldReplacements, + string remappingXmlFilePath) { bool haveAllAttributes; @@ -119,6 +142,14 @@ void ReadXml (XmlReader reader, List typeReplacemen } typeReplacements.Add (new JniRemappingTypeReplacement (from, to)); + } else if (MonoAndroidHelper.StringEquals ("reverse-type", reader.LocalName)) { + haveAllAttributes &= GetRequiredAttribute ("from", out string from); + haveAllAttributes &= GetRequiredAttribute ("to", out string to); + if (!haveAllAttributes) { + continue; + } + + reverseTypeReplacements.Add (new JniRemappingTypeReplacement (from, to)); } else if (MonoAndroidHelper.StringEquals ("replace-method", reader.LocalName)) { haveAllAttributes &= GetRequiredAttribute ("source-type", out string sourceType); haveAllAttributes &= GetRequiredAttribute ("source-method-name", out string sourceMethodName); @@ -136,10 +167,31 @@ void ReadXml (XmlReader reader, List typeReplacemen } string sourceMethodSignature = reader.GetAttribute ("source-method-signature"); + // Optional: inputs which predate it (for example the Intune/MAM mapping) keep + // the source signature on the target method. + string targetMethodSignature = reader.GetAttribute ("target-method-signature"); methodReplacements.Add ( new JniRemappingMethodReplacement ( sourceType, sourceMethodName, sourceMethodSignature, - targetType, targetMethodName, isStatic + targetType, targetMethodName, targetMethodSignature, isStatic + ) + ); + } else if (MonoAndroidHelper.StringEquals ("replace-field", reader.LocalName)) { + haveAllAttributes &= GetRequiredAttribute ("source-type", out string sourceType); + haveAllAttributes &= GetRequiredAttribute ("source-field-name", out string sourceFieldName); + haveAllAttributes &= GetRequiredAttribute ("target-type", out string targetType); + haveAllAttributes &= GetRequiredAttribute ("target-field-name", out string targetFieldName); + + if (!haveAllAttributes) { + continue; + } + + string sourceFieldSignature = reader.GetAttribute ("source-field-signature"); + string targetFieldSignature = reader.GetAttribute ("target-field-signature"); + fieldReplacements.Add ( + new JniRemappingFieldReplacement ( + sourceType, sourceFieldName, sourceFieldSignature, + targetType, targetFieldName, targetFieldSignature ) ); } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs index b8369374be9..45123a0223f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs @@ -29,6 +29,8 @@ public class GenerateNativeAotProguardConfiguration : AndroidTask // this avoids generating and processing the very large ILC dependency graph. public bool TrimJavaCallableWrappers { get; set; } = true; + public bool EnableObfuscation { get; set; } + public override bool RunTask () { var dir = Path.GetDirectoryName (OutputFile); @@ -61,8 +63,9 @@ public override bool RunTask () using var writer = new StringWriter (); writer.WriteLine ("# ACWs retained by NativeAOT ILC"); + string keepOption = EnableObfuscation ? "-keep,allowobfuscation" : "-keep"; foreach (var javaTypeName in javaTypes) { - writer.WriteLine ($"-keep class {javaTypeName} {{ *; }}"); + writer.WriteLine ($"{keepOption} class {javaTypeName} {{ *; }}"); } Files.CopyIfStringChanged (writer.ToString (), OutputFile); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateProguardConfiguration.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateProguardConfiguration.cs index 6c3b683d6c5..0869601d685 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateProguardConfiguration.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateProguardConfiguration.cs @@ -19,6 +19,8 @@ public class GenerateProguardConfiguration : AndroidTask [Required] public string OutputFile { get; set; } = ""; + public bool EnableObfuscation { get; set; } + public override bool RunTask () { var dir = Path.GetDirectoryName (OutputFile); @@ -100,8 +102,10 @@ void ProcessType (MetadataReader reader, TypeDefinition type, TextWriter writer) if (javaTypeName == null) return; - writer.WriteLine ($"-keep class {javaTypeName}"); - writer.WriteLine ($"-keepclassmembers class {javaTypeName} {{"); + string keepOption = EnableObfuscation ? "-keep,allowobfuscation" : "-keep"; + string keepMembersOption = EnableObfuscation ? "-keepclassmembers,allowobfuscation" : "-keepclassmembers"; + writer.WriteLine ($"{keepOption} class {javaTypeName}"); + writer.WriteLine ($"{keepMembersOption} class {javaTypeName} {{"); foreach (var methodHandle in type.GetMethods ()) { ProcessMethod (reader, methodHandle, writer); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniManifestProguardConfiguration.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniManifestProguardConfiguration.cs new file mode 100644 index 00000000000..ea2ca22295c --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniManifestProguardConfiguration.cs @@ -0,0 +1,105 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using System.Xml.Linq; + +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; + +namespace Xamarin.Android.Tasks; + +/// +/// Emits keep rules for the types the merged AndroidManifest.xml names, so the naming-only +/// seed R8 pass cannot rename them. The final R8 pass keeps the same names via the AAPT-generated +/// rules, so pinning them up front is what keeps the seed mapping applicable with +/// -applymapping. +/// +public sealed class GenerateR8JniManifestProguardConfiguration : AndroidTask +{ + static readonly XNamespace AndroidNamespace = "http://schemas.android.com/apk/res/android"; + + public override string TaskPrefix => "GRJMPC"; + + [Required] + public string AndroidManifestFile { get; set; } = ""; + + [Required] + public string OutputFile { get; set; } = ""; + + public override bool RunTask () + { + XDocument manifest; + try { + manifest = XDocument.Load (AndroidManifestFile, LoadOptions.None); + } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is XmlException) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_ManifestReadFailure, AndroidManifestFile, ex.Message)); + return false; + } + + XElement? root = manifest.Root; + string? packageName = root?.Attribute ("package")?.Value; + if (root?.Name.LocalName != "manifest" || packageName.IsNullOrWhiteSpace ()) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_ManifestPackageMissing, AndroidManifestFile)); + return false; + } + + var classes = new SortedSet (StringComparer.Ordinal); + foreach (XElement element in root.DescendantsAndSelf ()) { + switch (element.Name.LocalName) { + case "application": + AddClass (classes, packageName, element, "name"); + AddClass (classes, packageName, element, "backupAgent"); + AddClass (classes, packageName, element, "appComponentFactory"); + AddClass (classes, packageName, element, "zygotePreloadName"); + break; + case "activity": + case "service": + case "receiver": + case "provider": + case "instrumentation": + case "process": + AddClass (classes, packageName, element, "name"); + break; + case "activity-alias": + // android:name on an is an alias, not a real type; only the + // targetActivity names a class that must survive with its name intact. + AddClass (classes, packageName, element, "targetActivity"); + break; + } + } + + string content = string.Join ("\n", classes.Select (name => $"-keep class {name} {{ (); }}")); + if (content.Length > 0) { + content += "\n"; + } + + string? directory = Path.GetDirectoryName (OutputFile); + if (!directory.IsNullOrEmpty ()) { + Directory.CreateDirectory (directory); + } + File.WriteAllText (OutputFile, content, Files.UTF8withoutBOM); + return !Log.HasLoggedErrors; + } + + static void AddClass (ISet classes, string packageName, XElement element, string attributeName) + { + string? value = element.Attribute (AndroidNamespace + attributeName)?.Value; + if (value.IsNullOrWhiteSpace () || value [0] == '@' || value [0] == '?') { + return; + } + if (value [0] == '.') { + classes.Add (packageName + value); + } else if (value.IndexOf ('.') < 0) { + classes.Add (packageName + "." + value); + } else { + classes.Add (value); + } + } + + void LogR8JniRemappingError (string detail) => + Log.LogCodedError ("XA4327", Properties.Resources.XA4327, detail); +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs new file mode 100644 index 00000000000..9b9016b0bc9 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs @@ -0,0 +1,470 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Xml; + +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; + +using Xamarin.Android.Tasks.JniRemapping; + +namespace Xamarin.Android.Tasks +{ + /// + /// Converts the naming-only R8 seed mapping.txt into a JNI remapping XML document that + /// the existing @(_AndroidRemapMembers) -> MergeRemapXml -> + /// GenerateJniRemappingNativeCode pipeline consumes. + /// + /// Managed assemblies are *not* rewritten on this path, so they keep the original JNI names. + /// The generated document is what teaches the runtime how those original names map onto the + /// obfuscated names R8 produced, and how the obfuscated names map back for Java-to-managed + /// lookups. + /// + /// The document extends the existing schema in a backward-compatible way: + /// + /// + /// <replace-type from to /> - unchanged, one per renamed class. + /// <replace-method ... /> - unchanged attributes, plus the new optional + /// target-method-signature carrying the JNI descriptor after its parameter and + /// return types were themselves renamed. + /// <reverse-type from to /> - new; obfuscated-to-original class name, for + /// Java-to-managed lookup. Only emitted when the reverse direction is unambiguous. + /// <replace-field ... /> - new; field renames and rewritten field + /// descriptors. + /// + /// + /// Existing consumers ignore the new elements and attributes, and existing remapping inputs + /// (for example the Intune/MAM mapping) are composed with rather than overridden: an entry that + /// collides with one already contributed by another input is dropped, with a warning. + /// + public class GenerateR8JniRemapping : AndroidTask + { + public override string TaskPrefix => "GR8JR"; + + /// The naming-only R8 seed mapping file. + [Required] + public string MappingFile { get; set; } = ""; + + [Required] + public string OutputFile { get; set; } = ""; + + /// + /// Remapping XML documents already contributed by other features. Entries colliding with + /// these are not emitted, so the pre-existing inputs keep winning. + /// + public ITaskItem []? ExistingRemapXmlFiles { get; set; } + + public ITaskItem []? LinkedAssemblies { get; set; } + + /// Use post-ILC retention instead of treating pre-ILC assemblies as linked output. + public bool NativeAot { get; set; } + + /// + /// ILC's NativeObject, before native linking. Generated JNI identifiers must remain literal + /// strings; runtime-constructed names require explicit remapping in ExistingRemapXmlFiles. + /// + public string? NativeAotObjectFile { get; set; } + + readonly Dictionary existingEntries = new Dictionary (StringComparer.Ordinal); + + // Types another remapping input already describes. Everything about such a type - its + // reverse mapping and its members - is left to that input. + readonly HashSet externallyOwnedTypes = new HashSet (StringComparer.Ordinal); + + public override bool RunTask () + { + if (!File.Exists (MappingFile)) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_SeedMappingNotFound, MappingFile)); + return false; + } + + R8Mapping mapping; + try { + mapping = R8Mapping.Load (MappingFile); + } catch (FormatException ex) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_MappingDataFailure, MappingFile, ex.Message)); + return false; + } catch (IOException ex) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_MappingDataFailure, MappingFile, ex.Message)); + return false; + } catch (UnauthorizedAccessException ex) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_MappingDataFailure, MappingFile, ex.Message)); + return false; + } + + ReadExistingEntries (); + + HashSet? requiredEntries; + if (NativeAot) { + if (NativeAotObjectFile.IsNullOrEmpty () || !File.Exists (NativeAotObjectFile)) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_NativeAotObjectRequired, NativeAotObjectFile ?? "")); + return false; + } + try { + requiredEntries = NativeAotJniRetention.GetRequiredEntries (NativeAotObjectFile, mapping); + } catch (Exception ex) when (ex is IOException || ex is InvalidDataException || ex is UnauthorizedAccessException) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_NativeAotObjectReadFailure, NativeAotObjectFile, ex.Message)); + return false; + } + Log.LogDebugMessage ($"Post-ILC NativeAOT JNI retention selected {requiredEntries.Count} mapping entries."); + } else { + if (!NativeAotObjectFile.IsNullOrEmpty ()) { + LogR8JniRemappingError (Properties.Resources.XA4327_NativeAotModeRequired); + return false; + } + ScanLinkedAssemblies (mapping); + requiredEntries = LinkedAssemblies?.Length > 0 + ? new HashSet (mapping.AccessedEntries, StringComparer.Ordinal) + : null; + } + if (Log.HasLoggedErrors) { + return false; + } + string content = GenerateContent (mapping, requiredEntries); + string? directory = Path.GetDirectoryName (OutputFile); + if (!directory.IsNullOrEmpty ()) { + Directory.CreateDirectory (directory); + } + File.WriteAllText (OutputFile, content, Files.UTF8withoutBOM); + + return !Log.HasLoggedErrors; + } + + void ScanLinkedAssemblies (R8Mapping mapping) + { + if (LinkedAssemblies == null) { + return; + } + + var seen = new HashSet (StringComparer.OrdinalIgnoreCase); + foreach (ITaskItem assembly in LinkedAssemblies) { + string path = assembly.ItemSpec; + if (!seen.Add (path) || !File.Exists (path)) { + continue; + } + + try { + using var stream = File.OpenRead (path); + using var peReader = new PEReader (stream); + if (!peReader.HasMetadata) { + continue; + } + MetadataReader reader = peReader.GetMetadataReader (); + + JniAssemblyRewriter.ScanAssembly (peReader, reader, mapping, Log); + } catch (BadImageFormatException ex) { + Log.LogDebugMessage ($"Could not read assembly '{path}': {ex.Message}"); + } catch (JniRewriteException ex) { + LogR8JniRemappingError ($"The linked assembly '{path}' could not be scanned: {ex.Message}"); + } + } + } + + string GenerateContent (R8Mapping mapping, HashSet? requiredEntries) + { + var allClassMappings = new List (mapping.EnumerateClassMappings ()); + var classMappings = new List (); + foreach (R8ClassMapping classMapping in allClassMappings) { + if (requiredEntries == null || requiredEntries.Contains (R8Mapping.BuildClassEntry (classMapping.OriginalJniName))) { + classMappings.Add (classMapping); + } + } + var classRenames = new Dictionary (StringComparer.Ordinal); + foreach (R8ClassMapping classMapping in allClassMappings) { + classRenames [classMapping.OriginalJniName] = classMapping.ObfuscatedJniName; + } + string? RenameClass (string className) + => classRenames.TryGetValue (className, out string? renamed) ? renamed : null; + + var settings = new XmlWriterSettings { + Encoding = Files.UTF8withoutBOM, + Indent = true, + IndentChars = " ", + NewLineChars = "\n", + OmitXmlDeclaration = true, + }; + + var output = new StringBuilder (); + using (var writer = XmlWriter.Create (output, settings)) { + writer.WriteStartElement ("replacements"); + var skippedClasses = new HashSet (StringComparer.Ordinal); + foreach (R8ClassMapping classMapping in classMappings) { + if (!WriteClass (writer, mapping, classMapping)) { + skippedClasses.Add (classMapping.OriginalJniName); + } + } + foreach (R8ClassMapping classMapping in classMappings) { + if (skippedClasses.Contains (classMapping.OriginalJniName)) { + continue; + } + foreach (R8FieldMapping field in classMapping.Fields) { + if (requiredEntries != null && + !requiredEntries.Contains (R8Mapping.BuildFieldEntry (classMapping.OriginalJniName, field.OriginalName))) { + continue; + } + WriteField (writer, classMapping, field, RenameClass); + } + foreach (R8MethodMapping method in classMapping.Methods) { + string methodKey = R8Mapping.BuildMethodKey (method.OriginalName, method.JavaParameterTypes, method.JavaReturnType); + if (requiredEntries != null && + !requiredEntries.Contains (R8Mapping.BuildMethodEntry (classMapping.OriginalJniName, methodKey))) { + continue; + } + WriteMethod (writer, classMapping, method, RenameClass); + } + } + writer.WriteEndElement (); + } + output.Append ('\n'); + return output.ToString (); + } + + /// + /// Writes the class-level entries. Returns false when another remapping input owns this + /// type, in which case its members must be left to that input as well. + /// + bool WriteClass (XmlWriter writer, R8Mapping mapping, R8ClassMapping classMapping) + { + bool ownedExternally = externallyOwnedTypes.Contains (BuildTypeKey (classMapping.OriginalJniName)); + if (classMapping.IsRenamed) { + if (TryClaimEntry ( + "replace-type", + BuildTypeKey (classMapping.OriginalJniName), + classMapping.ObfuscatedJniName)) { + writer.WriteStartElement ("replace-type"); + writer.WriteAttributeString ("from", classMapping.OriginalJniName); + writer.WriteAttributeString ("to", classMapping.ObfuscatedJniName); + writer.WriteEndElement (); + } else { + ownedExternally = true; + } + } + + if (ownedExternally) { + return false; + } + + // R8 class merging can map several original classes onto one residual class; the + // reverse direction is then ambiguous and must not be described at all. + if (!classMapping.IsRenamed || + !mapping.TryGetOriginalClass (classMapping.ObfuscatedJniName, out string originalJniName) || + !string.Equals (originalJniName, classMapping.OriginalJniName, StringComparison.Ordinal)) { + return true; + } + + if (TryClaimEntry ( + "reverse-type", + BuildReverseTypeKey (classMapping.ObfuscatedJniName), + classMapping.OriginalJniName)) { + writer.WriteStartElement ("reverse-type"); + writer.WriteAttributeString ("from", classMapping.ObfuscatedJniName); + writer.WriteAttributeString ("to", classMapping.OriginalJniName); + writer.WriteEndElement (); + } + return true; + } + + void WriteField (XmlWriter writer, R8ClassMapping classMapping, R8FieldMapping field, Func renameClass) + { + if (field.JavaFieldType.Length == 0) { + return; + } + + string sourceSignature; + try { + sourceSignature = JniDescriptorText.JavaSourceTypeToJniTypeToken (field.JavaFieldType); + } catch (ArgumentException) { + LogR8JniRemappingWarning (string.Format ( + Properties.Resources.XA4328_UnsupportedSignature, + $"{classMapping.OriginalJniName}.{field.OriginalName}", + field.JavaFieldType)); + return; + } + + JniDescriptorText.TryRewriteDescriptor (sourceSignature, renameClass, out string targetSignature); + if (!classMapping.IsRenamed && !field.IsRenamed && + string.Equals (sourceSignature, targetSignature, StringComparison.Ordinal)) { + return; + } + + if (!TryClaimEntry ( + "replace-field", + BuildFieldKey (classMapping.OriginalJniName, field.OriginalName), + $"{classMapping.ObfuscatedJniName}\t{field.ObfuscatedName}\t{targetSignature}")) { + return; + } + + writer.WriteStartElement ("replace-field"); + writer.WriteAttributeString ("source-type", classMapping.OriginalJniName); + writer.WriteAttributeString ("source-field-name", field.OriginalName); + writer.WriteAttributeString ("source-field-signature", sourceSignature); + writer.WriteAttributeString ("target-type", classMapping.ObfuscatedJniName); + writer.WriteAttributeString ("target-field-name", field.ObfuscatedName); + writer.WriteAttributeString ("target-field-signature", targetSignature); + writer.WriteEndElement (); + } + + void WriteMethod (XmlWriter writer, R8ClassMapping classMapping, R8MethodMapping method, Func renameClass) + { + string sourceSignature; + try { + sourceSignature = JniDescriptorText.JavaSourceTypesToMethodDescriptor (method.JavaParameterTypes, method.JavaReturnType); + } catch (ArgumentException) { + LogR8JniRemappingWarning (string.Format ( + Properties.Resources.XA4328_UnsupportedSignature, + $"{classMapping.OriginalJniName}.{method.OriginalName}", + string.Join (",", method.JavaParameterTypes))); + return; + } + + JniDescriptorText.TryRewriteDescriptor (sourceSignature, renameClass, out string targetSignature); + if (!classMapping.IsRenamed && !method.IsRenamed && + string.Equals (sourceSignature, targetSignature, StringComparison.Ordinal)) { + return; + } + + // The source signature is part of the key, so overloads stay distinct entries. + if (!TryClaimEntry ( + "replace-method", + BuildMethodKey (classMapping.OriginalJniName, method.OriginalName, sourceSignature), + $"{classMapping.ObfuscatedJniName}\t{method.ObfuscatedName}\t{targetSignature}")) { + return; + } + + writer.WriteStartElement ("replace-method"); + writer.WriteAttributeString ("source-type", classMapping.OriginalJniName); + writer.WriteAttributeString ("source-method-name", method.OriginalName); + writer.WriteAttributeString ("source-method-signature", sourceSignature); + writer.WriteAttributeString ("target-type", classMapping.ObfuscatedJniName); + writer.WriteAttributeString ("target-method-name", method.ObfuscatedName); + writer.WriteAttributeString ("target-method-signature", targetSignature); + writer.WriteAttributeString ("target-method-instance-to-static", "false"); + writer.WriteEndElement (); + } + + /// + /// Records an entry, reporting a conflict when another remapping input already described + /// the same source. Returns false when the entry must not be emitted. + /// + bool TryClaimEntry (string elementName, string key, string target) + { + if (!existingEntries.TryGetValue (key, out string? existingTarget)) { + existingEntries [key] = target; + return true; + } + + if (string.Equals (existingTarget, target, StringComparison.Ordinal)) { + Log.LogDebugMessage ($"Skipping duplicate `{elementName}` entry for `{key.Replace ('\t', ' ')}`."); + return false; + } + + LogR8JniRemappingWarning (string.Format ( + Properties.Resources.XA4328_ConflictingEntry, + elementName, + key.Replace ('\t', ' '), + existingTarget.Replace ('\t', ' '), + target.Replace ('\t', ' '))); + return false; + } + + void ReadExistingEntries () + { + if (ExistingRemapXmlFiles == null) { + return; + } + + var readerSettings = new XmlReaderSettings { + XmlResolver = null, + }; + + foreach (ITaskItem item in ExistingRemapXmlFiles) { + string file = item.ItemSpec; + if (string.Equals (Path.GetFullPath (file), Path.GetFullPath (OutputFile), StringComparison.OrdinalIgnoreCase)) { + continue; + } + if (!File.Exists (file)) { + // MergeRemapXml reports missing inputs (XA4316) later in the build. + Log.LogDebugMessage ($"Existing remapping input `{file}` does not exist yet."); + continue; + } + + try { + using var reader = XmlReader.Create (File.OpenRead (file), readerSettings); + ReadExistingEntries (reader); + } catch (Exception ex) when (ex is XmlException || ex is IOException || ex is UnauthorizedAccessException) { + // MergeRemapXml reports unreadable inputs (XA4318) later in the build. + Log.LogDebugMessage ($"Existing remapping input `{file}` could not be read: {ex.Message}"); + } + } + } + + void ReadExistingEntries (XmlReader reader) + { + while (reader.Read ()) { + if (reader.NodeType != XmlNodeType.Element) { + continue; + } + + switch (reader.LocalName) { + case "replace-type": + AddExistingEntry ( + BuildTypeKey (reader.GetAttribute ("from")), + reader.GetAttribute ("to"), + externallyOwnedType: true); + break; + case "reverse-type": + AddExistingEntry ( + BuildReverseTypeKey (reader.GetAttribute ("from")), + reader.GetAttribute ("to")); + break; + case "replace-field": + AddExistingEntry ( + BuildFieldKey (reader.GetAttribute ("source-type"), reader.GetAttribute ("source-field-name")), + $"{reader.GetAttribute ("target-type")}\t{reader.GetAttribute ("target-field-name")}\t{reader.GetAttribute ("target-field-signature")}"); + break; + case "replace-method": + AddExistingEntry ( + BuildMethodKey ( + reader.GetAttribute ("source-type"), + reader.GetAttribute ("source-method-name"), + reader.GetAttribute ("source-method-signature")), + $"{reader.GetAttribute ("target-type")}\t{reader.GetAttribute ("target-method-name")}\t{reader.GetAttribute ("target-method-signature")}"); + break; + } + } + } + + void AddExistingEntry (string key, string? target, bool externallyOwnedType = false) + { + if (key.Length == 0) { + return; + } + existingEntries [key] = target ?? ""; + if (externallyOwnedType) { + externallyOwnedTypes.Add (key); + } + } + + static string BuildTypeKey (string? from) => from.IsNullOrEmpty () ? "" : $"T\t{from}"; + + static string BuildReverseTypeKey (string? from) => from.IsNullOrEmpty () ? "" : $"R\t{from}"; + + static string BuildFieldKey (string? sourceType, string? fieldName) + => sourceType.IsNullOrEmpty () || fieldName.IsNullOrEmpty () ? "" : $"F\t{sourceType}\t{fieldName}"; + + // A method's source signature is part of its identity: overloads must not collapse. + static string BuildMethodKey (string? sourceType, string? methodName, string? signature) + => sourceType.IsNullOrEmpty () || methodName.IsNullOrEmpty () ? "" : $"M\t{sourceType}\t{methodName}\t{signature}"; + + void LogR8JniRemappingError (string detail) + => Log.LogCodedError ("XA4327", Properties.Resources.XA4327, detail); + + void LogR8JniRemappingWarning (string detail) + => Log.LogCodedWarning ("XA4328", Properties.Resources.XA4328, detail); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs index 0985f923d17..a66a3c70e98 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -34,6 +34,25 @@ public class R8 : D8 public string? ProguardGeneratedApplicationConfiguration { get; set; } public string? ProguardCommonXamarinConfiguration { get; set; } public string? ProguardMappingFileOutput { get; set; } + + /// + /// A mapping file applied with -applymapping, so this R8 run reproduces the names an + /// earlier (seed) run chose. + /// + public string? ProguardMappingFileInput { get; set; } + + /// + /// Runs R8 as a naming-only seed pass: no tree shaking, no optimization, mapping output only. + /// + public bool GenerateSeedMapping { get; set; } + + /// + /// Allows R8 to rename types and members by omitting the SDK-generated + /// -dontobfuscate, and by letting the generated Java Callable Wrapper keep rules + /// retain their types without pinning their names. + /// + public bool EnableObfuscation { get; set; } + public string? BuildMetadataFileOutput { get; set; } public ITaskItem []? ProguardConfigurationFiles { get; set; } public bool UseTrimmableNativeAotProguardConfiguration { get; set; } @@ -49,6 +68,10 @@ public class R8 : D8 public override bool RunTask () { try { + if (GenerateSeedMapping && ProguardMappingFileOutput.IsNullOrEmpty ()) { + Log.LogCodedError ("XA4327", Properties.Resources.XA4327, Properties.Resources.XA4327_SeedMappingOutputRequired); + return false; + } return base.RunTask (); } finally { foreach (var temp in tempFiles) { @@ -159,7 +182,26 @@ protected override string CreateResponseFile () } } - if (EnableShrinking) { + if (GenerateSeedMapping) { + // Naming-only seed pass: choose the names, keep everything else intact. The mapping + // this produces is applied to the final R8 run with -applymapping. + WriteArg (response, "--no-tree-shaking"); + var seedConfiguration = new List { + "-dontoptimize", + "-dontpreverify", + "-keepattributes **", + $"-printmapping \"{Path.GetFullPath (GetRequiredSeedMappingOutput ())}\"", + }; + if (IgnoreWarnings) { + seedConfiguration.Add ("-ignorewarnings"); + } + WriteConfiguration (response, seedConfiguration); + GenerateCommonXamarinConfiguration (); + if (!ProguardCommonXamarinConfiguration.IsNullOrEmpty ()) { + WriteArg (response, "--pg-conf"); + WriteArg (response, ProguardCommonXamarinConfiguration); + } + } else if (EnableShrinking) { if (UseTrimmableNativeAotProguardConfiguration && !ProguardGeneratedApplicationConfiguration.IsNullOrEmpty ()) { // ACW keep rules come from the DGML/acw-map-driven proguard_project_references.cfg on // the trimmable path. User-authored AndroidJavaSource (Bind != true) has no managed peer @@ -168,7 +210,7 @@ protected override string CreateResponseFile () using (var appcfg = File.CreateText (ProguardGeneratedApplicationConfiguration)) { appcfg.WriteLine ("# ACW keep rules are generated from NativeAOT ILC metadata."); foreach (var java in GetUserJavaTypes ()) { - appcfg.WriteLine ($"-keep class {java} {{ *; }}"); + appcfg.WriteLine ($"{KeepOption} class {java} {{ *; }}"); } } } else if (!AcwMapFile.IsNullOrEmpty ()) { @@ -180,34 +222,16 @@ protected override string CreateResponseFile () javaTypes.Sort (StringComparer.Ordinal); using (var appcfg = File.CreateText (ProguardGeneratedApplicationConfiguration)) { foreach (var java in javaTypes) { - appcfg.WriteLine ($"-keep class {java} {{ *; }}"); + appcfg.WriteLine ($"{KeepOption} class {java} {{ *; }}"); } // User-authored AndroidJavaSource (Bind != true) has no managed peer and is absent // from the acw-map, so keep it explicitly; otherwise shrinking removes it. foreach (var java in GetUserJavaTypes ()) { - appcfg.WriteLine ($"-keep class {java} {{ *; }}"); - } - } - } - if (!ProguardCommonXamarinConfiguration.IsNullOrWhiteSpace ()) { - using (var xamcfg = File.CreateText (ProguardCommonXamarinConfiguration)) { - if (UseTrimmableNativeAotProguardConfiguration) { - using var stream = GetEmbeddedResourceStream ("proguard_trimmable_nativeaot.cfg"); - stream.CopyTo (xamcfg.BaseStream); - } else { - using var stream = GetEmbeddedResourceStream ("proguard_xamarin.cfg"); - stream.CopyTo (xamcfg.BaseStream); - } - if (IgnoreWarnings) { - xamcfg.WriteLine ("-ignorewarnings"); - } - if (!ProguardMappingFileOutput.IsNullOrEmpty ()) { - xamcfg.WriteLine ("-keepattributes SourceFile"); - xamcfg.WriteLine ("-keepattributes LineNumberTable"); - xamcfg.WriteLine ($"-printmapping \"{Path.GetFullPath (ProguardMappingFileOutput)}\""); + appcfg.WriteLine ($"{KeepOption} class {java} {{ *; }}"); } } } + GenerateCommonXamarinConfiguration (); } else { //NOTE: we may be calling r8 *only* for multi-dex, and all shrinking is disabled WriteArg (response, "--no-tree-shaking"); @@ -232,6 +256,11 @@ protected override string CreateResponseFile () WriteArg (response, "--pg-conf"); WriteArg (response, temp); } + if (!ProguardMappingFileInput.IsNullOrEmpty ()) { + WriteConfiguration (response, new [] { + $"-applymapping \"{Path.GetFullPath (ProguardMappingFileInput)}\"", + }); + } if (ProguardConfigurationFiles != null) { foreach (var item in ProguardConfigurationFiles) { var file = item.ItemSpec; @@ -252,6 +281,60 @@ protected override string CreateResponseFile () return responseFile; } + /// + /// The keep option used for the generated Java Callable Wrapper keep rules. When the JNI + /// names are remapped at runtime the wrappers must survive shrinking but stay renameable, + /// otherwise a plain -keep pins their names and -applymapping has no effect. + /// + internal string KeepOption => EnableObfuscation ? "-keep,allowobfuscation" : "-keep"; + + string GetRequiredSeedMappingOutput () + { + string? output = ProguardMappingFileOutput; + if (output.IsNullOrEmpty ()) { + throw new InvalidOperationException (Properties.Resources.XA4327_SeedMappingOutputRequired); + } + return output; + } + + internal void GenerateCommonXamarinConfiguration () + { + if (ProguardCommonXamarinConfiguration.IsNullOrWhiteSpace ()) { + return; + } + + using var xamcfg = File.CreateText (ProguardCommonXamarinConfiguration); + string resourceName = UseTrimmableNativeAotProguardConfiguration ? "proguard_trimmable_nativeaot.cfg" : "proguard_xamarin.cfg"; + using (Stream resource = GetEmbeddedResourceStream (resourceName)) + using (var reader = new StreamReader (resource)) { + while (reader.ReadLine () is string line) { + // The only SDK-generated option dropped when obfuscation is enabled. Every + // other rule in the configuration still applies. + if (EnableObfuscation && string.Equals (line.Trim (), "-dontobfuscate", StringComparison.OrdinalIgnoreCase)) { + continue; + } + xamcfg.WriteLine (line); + } + } + if (IgnoreWarnings) { + xamcfg.WriteLine ("-ignorewarnings"); + } + if (!ProguardMappingFileOutput.IsNullOrEmpty ()) { + xamcfg.WriteLine ("-keepattributes SourceFile"); + xamcfg.WriteLine ("-keepattributes LineNumberTable"); + xamcfg.WriteLine ($"-printmapping \"{Path.GetFullPath (ProguardMappingFileOutput)}\""); + } + } + + void WriteConfiguration (StreamWriter response, IEnumerable lines) + { + var temp = Path.GetTempFileName (); + File.WriteAllLines (temp, lines); + tempFiles.Add (temp); + WriteArg (response, "--pg-conf"); + WriteArg (response, temp); + } + // ProGuard "global" options that affect the whole build and are not allowed inside // a library's proguard.txt (the file packaged inside an .aar's root). AGP 9.0 // introduced the same restriction — see "Behavior changes" in the AGP 9.0 release diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs index 2714c6090a2..5b70f8192b9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs @@ -67,5 +67,74 @@ public void UnsupportedJcwCodegenTargetIsRejected ( } } + [TestCase (null, null, "false", "runtime-remapping", "false")] + [TestCase (null, "runtime-remapping", "false", "runtime-remapping", "false")] + [TestCase (null, "experimental-rewriting", "false", "experimental-rewriting", "false")] + [TestCase ("false", "unknown", "false", "unknown", "false")] + [TestCase ("true", null, "true", "runtime-remapping", "true")] + [TestCase ("true", "runtime-remapping", "true", "runtime-remapping", "true")] + public void R8ObfuscationDefaults (string? enabled, string? mode, string expectedEnabled, string expectedMode, string expectedRemapping) + { + var project = new XamarinAndroidApplicationProject { IsRelease = true }; + project.SetRuntime (AndroidRuntime.CoreCLR); + project.SetProperty ("AndroidLinkTool", "r8"); + project.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + if (enabled != null) { + project.SetProperty ("AndroidEnableR8Obfuscation", enabled); + } + if (mode != null) { + project.SetProperty ("AndroidR8ObfuscationMode", mode); + } + project.Imports.Add (new Import ("R8Options.targets") { + TextContent = () => """ + + + + + + """, + }); + using var builder = CreateApkBuilder (); + builder.Target = "ReportR8Options"; + Assert.IsTrue (builder.Build (project)); + StringAssertEx.Contains ($"R8_OPTIONS={expectedEnabled}|{expectedMode}|{expectedRemapping}", builder.LastBuildOutput); + } + + [TestCase ("AndroidEnableR8Obfuscation", "yes", "AndroidEnableR8Obfuscation")] + [TestCase ("AndroidR8ObfuscationMode", "unknown", "AndroidR8ObfuscationMode")] + [TestCase ("AndroidR8ObfuscationMode", "experimental-rewriting", "not available in this SDK")] + [TestCase ("AndroidLinkTool", "d8", "AndroidLinkTool")] + [TestCase ("AndroidLinkTool", "", "AndroidLinkTool")] + [TestCase ("AndroidTypeMapImplementation", "llvm-ir", "AndroidTypeMapImplementation")] + [TestCase ("PublishTrimmed", "false", "PublishTrimmed")] + [TestCase ("_AndroidRuntime", "MonoVM", "Supported runtimes are CoreCLR and NativeAOT")] + public void R8ObfuscationInvalidConfiguration (string property, string value, string expectedMessage) + { + var project = new XamarinAndroidApplicationProject { IsRelease = true }; + project.SetRuntime (AndroidRuntime.CoreCLR); + project.SetProperty ("AndroidEnableR8Obfuscation", "true"); + project.SetProperty ("RunAOTCompilation", "false"); + project.SetProperty ("AndroidLinkTool", "r8"); + project.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + project.SetProperty (property, value); + using var builder = CreateApkBuilder (); + builder.Target = "_ValidateAndroidR8Obfuscation"; + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (project)); + StringAssertEx.Contains ("error XA4329:", builder.LastBuildOutput); + StringAssertEx.Contains (expectedMessage, builder.LastBuildOutput); + } + + [Test] + public void R8ObfuscationDoesNotEnableLibraries () + { + var project = new XamarinAndroidLibraryProject (); + project.SetProperty ("AndroidEnableR8Obfuscation", "true"); + project.SetProperty ("AndroidR8ObfuscationMode", "experimental-rewriting"); + using var builder = CreateDllBuilder (); + builder.Target = "_ValidateAndroidR8Obfuscation"; + Assert.IsTrue (builder.Build (project), "Application obfuscation settings must not affect referenced libraries."); + } + } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs new file mode 100644 index 00000000000..0518f753320 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs @@ -0,0 +1,260 @@ +#nullable enable + +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Microsoft.Build.Framework; +using NUnit.Framework; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests.Tasks { + + [TestFixture] + public class GenerateJniRemappingNativeCodeTests : BaseTest { + + List? errors; + List? warnings; + MockBuildEngine? engine; + string? directory; + + const string Abi = "arm64-v8a"; + + [SetUp] + public void Setup () + { + errors = new List (); + warnings = new List (); + engine = new MockBuildEngine (TestContext.Out, errors, warnings); + directory = Path.Combine (Root, "temp", TestName); + if (Directory.Exists (directory)) { + Directory.Delete (directory, recursive: true); + } + Directory.CreateDirectory (directory); + } + + string TestDirectory { + get { + Assert.IsNotNull (directory); + return directory!; + } + } + + List Errors { + get { + Assert.IsNotNull (errors); + return errors!; + } + } + + string RunTask (string remappingXml) + { + string xmlPath = Path.Combine (TestDirectory, "remap.xml"); + File.WriteAllText (xmlPath, remappingXml); + + var task = new GenerateJniRemappingNativeCode { + BuildEngine = engine, + OutputDirectory = TestDirectory, + SupportedAbis = [Abi], + RemappingXmlFilePath = new Microsoft.Build.Utilities.TaskItem (xmlPath), + }; + + Assert.IsTrue (task.Execute (), $"Task should have succeeded. Errors: {string.Join ("; ", Errors.Select (e => e.Message))}"); + LastNativeCodeInfo = task.NativeCodeInfo; + + return File.ReadAllText (Path.Combine (TestDirectory, $"jni_remap.{Abi}.ll")); + } + + GenerateJniRemappingNativeCode.JniRemappingNativeCodeInfo? LastNativeCodeInfo { get; set; } + + GenerateJniRemappingNativeCode.JniRemappingNativeCodeInfo Info { + get { + Assert.IsNotNull (LastNativeCodeInfo); + return LastNativeCodeInfo!; + } + } + + [Test] + public void EmptyCodeEmitsAllTablesAndZeroCounts () + { + var task = new GenerateJniRemappingNativeCode { + BuildEngine = engine, + OutputDirectory = TestDirectory, + SupportedAbis = [Abi], + GenerateEmptyCode = true, + }; + + Assert.IsTrue (task.Execute (), "Task should have succeeded."); + + string ll = File.ReadAllText (Path.Combine (TestDirectory, $"jni_remap.{Abi}.ll")); + foreach (string symbol in new [] { + "jni_remapping_type_replacements", + "jni_remapping_reverse_type_replacements", + "jni_remapping_method_replacement_index", + "jni_remapping_field_replacement_index", + }) { + StringAssert.Contains ($"@{symbol}", ll, $"`{symbol}` must always be emitted."); + } + + foreach (string counter in new [] { + "jni_remapping_type_replacement_count", + "jni_remapping_reverse_type_replacement_count", + "jni_remapping_method_replacement_index_count", + "jni_remapping_field_replacement_index_count", + }) { + StringAssert.Contains ($"@{counter} = dso_local local_unnamed_addr constant i32 0", ll, $"`{counter}` must be zero."); + } + + var info = task.NativeCodeInfo; + Assert.IsNotNull (info); + Assert.AreEqual (0, info!.ReplacementTypeCount); + Assert.AreEqual (0, info.ReverseTypeCount); + Assert.AreEqual (0, info.ReplacementMethodIndexEntryCount); + Assert.AreEqual (0, info.ReplacementFieldIndexEntryCount); + } + + [Test] + public void CountsMatchGeneratedTables () + { + RunTask ( + """ + + + + + + + + + """); + + Assert.AreEqual (2, Info.ReplacementTypeCount, "replace-type count"); + Assert.AreEqual (1, Info.ReverseTypeCount, "reverse-type count"); + Assert.AreEqual (2, Info.ReplacementMethodIndexEntryCount, "replace-method type count"); + Assert.AreEqual (1, Info.ReplacementFieldIndexEntryCount, "replace-field type count"); + } + + [Test] + public void ReverseTypesAreEmittedSeparatelyFromForwardTypes () + { + string ll = RunTask ( + """ + + + + + """); + + int forward = ll.IndexOf ("@jni_remapping_type_replacements"); + int reverse = ll.IndexOf ("@jni_remapping_reverse_type_replacements"); + Assert.Greater (forward, -1, "Forward table must be emitted."); + Assert.Greater (reverse, -1, "Reverse table must be emitted."); + Assert.AreEqual (1, Info.ReplacementTypeCount); + Assert.AreEqual (1, Info.ReverseTypeCount); + } + + [Test] + public void MissingTargetMethodSignatureIsBackwardCompatible () + { + // The Intune/MAM mapping shape: no `target-method-signature`, wildcard source signature. + string ll = RunTask ( + """ + + + + + """); + + Assert.AreEqual (1, Info.ReplacementTypeCount); + Assert.AreEqual (0, Info.ReverseTypeCount, "No reverse entries in a legacy document."); + Assert.AreEqual (1, Info.ReplacementMethodIndexEntryCount); + Assert.AreEqual (0, Info.ReplacementFieldIndexEntryCount); + StringAssert.Contains ("com/microsoft/intune/MAMActivity", ll); + // The wildcard signature is emitted as a zero-length string, and the absent target + // signature as a null pointer. + StringAssert.Contains ("ptr null", ll, "An absent target-method-signature must be a null pointer."); + } + + [Test] + public void TypeTablesAreSortedForBinarySearch () + { + string ll = RunTask ( + """ + + + + + + + + + """); + + AssertOrdered (ll, "aa/First", "mm/Middle", "zz/Last"); + Assert.AreEqual (3, Info.ReplacementTypeCount); + Assert.AreEqual (3, Info.ReverseTypeCount); + } + + [Test] + public void MethodsAndFieldsAreSortedByNameThenSignature () + { + string ll = RunTask ( + """ + + + + + + + + """); + + // Overloads keep a stable (name, signature) order so the runtime can binary-search the + // name and scan the equal-name run. + AssertOrdered (ll, "c\"alpha", "c\"(I)V", "c\"(J)V", "c\"zeta"); + AssertOrdered (ll, "c\"af", "c\"zf"); + Assert.AreEqual (1, Info.ReplacementMethodIndexEntryCount); + Assert.AreEqual (1, Info.ReplacementFieldIndexEntryCount); + } + + [Test] + public void Utf8OrderingMatchesNativeMemcmp () + { + // '_' (0x5F) sorts after 'Z' (0x5A) but before 'a' (0x61); a culture-sensitive + // comparison would order these differently, and the native binary search would break. + Assert.Less (JniRemappingAssemblyGenerator.CompareUtf8 (Utf8 ("Z"), Utf8 ("_")), 0); + Assert.Less (JniRemappingAssemblyGenerator.CompareUtf8 (Utf8 ("_"), Utf8 ("a")), 0); + Assert.Less (JniRemappingAssemblyGenerator.CompareUtf8 (Utf8 ("a"), Utf8 ("ab")), 0); + Assert.AreEqual (0, JniRemappingAssemblyGenerator.CompareUtf8 (Utf8 ("a/B"), Utf8 ("a/B"))); + + static byte [] Utf8 (string s) => System.Text.Encoding.UTF8.GetBytes (s); + } + + static void AssertOrdered (string haystack, params string [] needles) + { + int previous = -1; + string previousNeedle = ""; + foreach (string needle in needles) { + int index = haystack.IndexOf (needle, previous + 1, System.StringComparison.Ordinal); + Assert.Greater (index, previous, $"`{needle}` must appear after `{previousNeedle}`."); + previous = index; + previousNeedle = needle; + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs new file mode 100644 index 00000000000..2d3b2ab8f9b --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs @@ -0,0 +1,714 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using NUnit.Framework; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests.Tasks { + + [TestFixture] + public class GenerateR8JniRemappingTests : BaseTest { + + List? errors; + List? warnings; + MockBuildEngine? engine; + string? directory; + + [SetUp] + public void Setup () + { + errors = new List (); + warnings = new List (); + engine = new MockBuildEngine (TestContext.Out, errors, warnings); + directory = Path.Combine (Root, "temp", TestName); + if (Directory.Exists (directory)) { + Directory.Delete (directory, recursive: true); + } + Directory.CreateDirectory (directory); + } + + string TestDirectory { + get { + Assert.IsNotNull (directory); + return directory; + } + } + + List Errors { + get { + Assert.IsNotNull (errors); + return errors; + } + } + + List Warnings { + get { + Assert.IsNotNull (warnings); + return warnings; + } + } + + string WriteMapping (string content, string fileName = "mapping.txt") + { + var path = Path.Combine (TestDirectory, fileName); + File.WriteAllText (path, content); + return path; + } + + string WriteRemapXml (string content, string fileName = "existing.xml") + { + var path = Path.Combine (TestDirectory, fileName); + File.WriteAllText (path, content); + return path; + } + + string Run (string mappingContent, params string [] existingRemapXmlFiles) + => Run (mappingContent, null, existingRemapXmlFiles); + + string Run (string mappingContent, string []? linkedAssemblies, string [] existingRemapXmlFiles, string? nativeAotObjectFile = null) + { + var outputFile = Path.Combine (TestDirectory, "r8-jni-remap.xml"); + var task = new GenerateR8JniRemapping { + BuildEngine = engine, + MappingFile = WriteMapping (mappingContent), + OutputFile = outputFile, + ExistingRemapXmlFiles = existingRemapXmlFiles + .Select (f => (ITaskItem) new TaskItem (f)) + .ToArray (), + LinkedAssemblies = linkedAssemblies? + .Select (f => (ITaskItem) new TaskItem (f)) + .ToArray (), + NativeAot = nativeAotObjectFile != null, + NativeAotObjectFile = nativeAotObjectFile, + }; + Assert.IsTrue (task.Execute (), "Task should have succeeded."); + Assert.AreEqual (0, Errors.Count, "Task should have no errors."); + FileAssert.Exists (outputFile); + return File.ReadAllText (outputFile); + } + + string WriteNativeObject (string [] literals, bool utf8 = false, bool dehydrated = false, + string []? debugLiterals = null, bool managedCode = true, bool elf32 = false) + { + byte [] Encode (string [] values) + { + using var data = new MemoryStream (); + foreach (string value in values) { + byte [] bytes = (utf8 ? Encoding.UTF8 : Encoding.Unicode).GetBytes (value); + int start = dehydrated && bytes [0] == 0 ? 1 : 0; + int end = bytes.Length - (dehydrated && bytes [bytes.Length - 1] == 0 ? 1 : 0); + data.Write (bytes, start, end - start); + data.WriteByte (0xFF); + data.WriteByte (0xFF); + } + return data.ToArray (); + } + + var sections = new [] { + (Name: "", Flags: 0UL, Type: 0U, Bytes: new byte [0]), + (Name: ".shstrtab", Flags: 0UL, Type: 3U, Bytes: new byte [0]), + (Name: managedCode ? "__managedcode" : ".text", Flags: 6UL, Type: 1U, + Bytes: elf32 ? new byte [] { 0x1E, 0xFF, 0x2F, 0xE1 } : new byte [] { 0xC0, 0x03, 0x5F, 0xD6 }), + (Name: ".rodata", Flags: 2UL, Type: 1U, Bytes: Encode (literals)), + (Name: ".debug_info", Flags: 0UL, Type: 1U, Bytes: Encode (debugLiterals ?? [])), + }; + sections [1].Bytes = Encoding.UTF8.GetBytes (string.Join ("\0", sections.Select (s => s.Name)) + "\0"); + var offsets = new long [sections.Length]; + using var image = new MemoryStream (); + using var writer = new BinaryWriter (image); + void WriteWord (ulong value) + { + if (elf32) { + writer.Write (checked ((uint) value)); + } else { + writer.Write (value); + } + } + writer.Write (new byte [] { 0x7F, (byte) 'E', (byte) 'L', (byte) 'F', elf32 ? (byte) 1 : (byte) 2, 1, 1, 0 }); + writer.Write (0UL); + writer.Write ((ushort) 1); // ET_REL + writer.Write (elf32 ? (ushort) 40 : (ushort) 183); // ARM or AArch64 + writer.Write (1U); + WriteWord (0); // entry point + WriteWord (0); // program headers + WriteWord (0); // section headers, filled below + writer.Write (0U); + writer.Write (elf32 ? (ushort) 52 : (ushort) 64); + writer.Write ((ushort) 0); + writer.Write ((ushort) 0); + writer.Write (elf32 ? (ushort) 40 : (ushort) 64); + writer.Write ((ushort) sections.Length); + writer.Write ((ushort) 1); + for (int i = 1; i < sections.Length; i++) { + offsets [i] = image.Position; + writer.Write (sections [i].Bytes); + } + long sectionHeaders = image.Position; + int nameIndex = 0; + for (int i = 0; i < sections.Length; i++) { + writer.Write (nameIndex); + writer.Write (sections [i].Type); + WriteWord (sections [i].Flags); + WriteWord (0); + WriteWord ((ulong) offsets [i]); + WriteWord ((ulong) sections [i].Bytes.Length); + writer.Write (0U); // link + writer.Write (0U); // info + WriteWord (i == 0 ? 0UL : 1UL); + WriteWord (0); + nameIndex += Encoding.UTF8.GetByteCount (sections [i].Name) + 1; + } + image.Position = elf32 ? 32 : 40; + WriteWord ((ulong) sectionHeaders); + string path = Path.Combine (TestDirectory, "app.o"); + File.WriteAllBytes (path, image.ToArray ()); + return path; + } + + [TestCase (false, false, false)] + [TestCase (false, true, false)] + [TestCase (true, false, false)] + [TestCase (false, false, true)] + [TestCase (false, true, true)] + [TestCase (true, false, true)] + public void NativeAotFiltersMembersAndOverloadsOfRetainedType (bool utf8, bool dehydrated, bool elf32) + { + var nativeObject = WriteNativeObject ( + ["com/contoso/Peer", "run.(I)V", "value.I", "callback:()V:n_Callback"], + utf8, dehydrated, + debugLiterals: ["removed.()V", "run.(Ljava/lang/String;)V", "unused.I", "com/contoso/Unused"], + elf32: elf32); + var xml = Run ( + """ + com.contoso.Peer -> a.b: + void run(int) -> c + void run(java.lang.String) -> d + void removed() -> e + void callback() -> f + int value -> g + int unused -> h + com.contoso.Unused -> a.i: + void run(int) -> j + """, null, [], nativeObject); + + StringAssert.Contains (Method ("com/contoso/Peer", "run", "(I)V", "a/b", "c", "(I)V"), xml); + StringAssert.Contains (Method ("com/contoso/Peer", "callback", "()V", "a/b", "f", "()V"), xml); + StringAssert.Contains (Field ("com/contoso/Peer", "value", "I", "a/b", "g", "I"), xml); + StringAssert.DoesNotContain ("removed", xml); + StringAssert.DoesNotContain ("unused", xml); + StringAssert.DoesNotContain ("Unused", xml); + StringAssert.DoesNotContain ("Ljava/lang/String;", xml); + } + + [Test] + public void NativeAotRetainsConstructorsAndDescriptorOnlyTypes () + { + var nativeObject = WriteNativeObject (["com/contoso/Peer", "([Lcom/contoso/Argument;)V", + "run.([Lcom/contoso/Argument;)Lcom/contoso/Result;"]); + var xml = Run ( + """ + com.contoso.Peer -> a.b: + void (com.contoso.Argument[]) -> + void (int) -> + com.contoso.Result run(com.contoso.Argument[]) -> c + com.contoso.Argument -> a.d: + com.contoso.Result -> a.e: + """, null, [], nativeObject); + + StringAssert.Contains (Method ("com/contoso/Peer", "<init>", "([Lcom/contoso/Argument;)V", + "a/b", "<init>", "([La/d;)V"), xml); + StringAssert.Contains (Method ("com/contoso/Peer", "run", "([Lcom/contoso/Argument;)Lcom/contoso/Result;", + "a/b", "c", "([La/d;)La/e;"), xml); + StringAssert.Contains ("""""", xml); + StringAssert.Contains ("""""", xml); + StringAssert.DoesNotContain ("(I)V", xml); + } + + [Test] + public void NativeAotSharedGenericAndInlinedLiteralsConservativelyRetainEveryOwner () + { + // Generic instantiations and inlined methods do not need distinct compiled method + // symbols. A shared Java-erased member ID is sufficient for both reachable owners. + var nativeObject = WriteNativeObject (["com/contoso/Generic", "com/contoso/Generic$Nested", + "get.(Ljava/lang/Object;)Ljava/lang/Object;"]); + var xml = Run ( + """ + com.contoso.Generic -> a.b: + java.lang.Object get(java.lang.Object) -> c + int get(int) -> d + com.contoso.Generic$Nested -> a.e: + java.lang.Object get(java.lang.Object) -> f + """, null, [], nativeObject); + + StringAssert.Contains (Method ("com/contoso/Generic", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", + "a/b", "c", "(Ljava/lang/Object;)Ljava/lang/Object;"), xml); + StringAssert.Contains (Method ("com/contoso/Generic$Nested", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", + "a/e", "f", "(Ljava/lang/Object;)Ljava/lang/Object;"), xml); + StringAssert.DoesNotContain ("(I)I", xml); + } + + [TestCase (false)] + [TestCase (true)] + public void NativeAotRetainsUnicodeIdentifiers (bool dehydrated) + { + var nativeObject = WriteNativeObject (["com/contoso/例", "Āction.()V", "café.()V"], dehydrated: dehydrated); + var xml = Run ( + """ + com.contoso.例 -> a.b: + void Āction() -> c + void café() -> d + """, null, [], nativeObject); + StringAssert.Contains ("Āction", xml); + StringAssert.Contains ("café", xml); + } + + [Test] + public void NativeAotEncodingCollisionsConservativelyRetainBothMembers () + { + // UTF-8 U+0100 and UTF-16 U+80C4 have the same bytes. Neither interpretation + // may overwrite the other in the retention index. + var nativeObject = WriteNativeObject (["com/contoso/Peer", "\u0100.()V"], utf8: true); + var xml = Run ("com.contoso.Peer -> a.b:\n void \u0100() -> c\n void \u80C4() -> d\n", + null, [], nativeObject); + StringAssert.Contains ("\u0100", xml); + StringAssert.Contains ("\u80C4", xml); + } + + [Test] + public void NativeAotEmptySelectionDoesNotFallBackToFullMapping () + { + var nativeObject = WriteNativeObject (["unrelated literal"]); + var xml = Run ("com.contoso.Unused -> a.b:\n void unused() -> c\n", + [Path.Combine (TestDirectory, "PreIlc.dll")], [], nativeObject); + StringAssert.DoesNotContain ("com/contoso/Unused", xml); + StringAssert.DoesNotContain ("replace-method", xml); + } + + [TestCase ("missing")] + [TestCase ("empty-path")] + [TestCase ("empty-file")] + [TestCase ("truncated")] + [TestCase ("unrelated-object")] + [TestCase ("invalid-section")] + [TestCase ("wrong-endianness")] + [TestCase ("linked-library")] + [TestCase ("graph")] + public void InvalidNativeAotRetentionIsReportedAsXA4327 (string kind) + { + string path = Path.Combine (TestDirectory, "missing.o"); + switch (kind) { + case "empty-path": + path = ""; + break; + case "empty-file": + File.WriteAllBytes (path, []); + break; + case "truncated": + File.WriteAllBytes (path, [0x7F, (byte) 'E', (byte) 'L', (byte) 'F', 2, 1, 1]); + break; + case "unrelated-object": + path = WriteNativeObject (["com/contoso/Peer"], managedCode: false); + break; + case "invalid-section": + path = WriteNativeObject (["com/contoso/Peer"]); + using (var file = File.Open (path, FileMode.Open, FileAccess.ReadWrite)) { + using var reader = new BinaryReader (file, Encoding.UTF8, leaveOpen: true); + using var writer = new BinaryWriter (file, Encoding.UTF8, leaveOpen: true); + file.Position = 40; + long sectionHeaders = reader.ReadInt64 (); + file.Position = sectionHeaders + 3 * 64 + 24; + writer.Write ((ulong) file.Length + 1); + } + break; + case "wrong-endianness": + case "linked-library": + path = WriteNativeObject (["com/contoso/Peer"]); + using (var file = File.Open (path, FileMode.Open, FileAccess.Write)) { + file.Position = kind == "wrong-endianness" ? 5 : 16; + file.WriteByte (kind == "wrong-endianness" ? (byte) 2 : (byte) 3); + } + break; + case "graph": + File.WriteAllText (path, """"""); + break; + } + var task = new GenerateR8JniRemapping { + BuildEngine = engine, + MappingFile = WriteMapping ("com.contoso.Peer -> a.b:\n"), + OutputFile = Path.Combine (TestDirectory, "r8-jni-remap.xml"), + NativeAot = true, + NativeAotObjectFile = path, + }; + Assert.IsFalse (task.Execute ()); + Assert.AreEqual (1, Errors.Count); + Assert.AreEqual ("XA4327", Errors [0].Code); + FileAssert.DoesNotExist (task.OutputFile); + } + + [Test] + public void NativeAotObjectWithoutNativeAotModeFails () + { + var task = new GenerateR8JniRemapping { + BuildEngine = engine, + MappingFile = WriteMapping ("com.contoso.Peer -> a.b:\n"), + OutputFile = Path.Combine (TestDirectory, "r8-jni-remap.xml"), + NativeAotObjectFile = WriteNativeObject (["com/contoso/Peer"]), + }; + Assert.IsFalse (task.Execute ()); + Assert.AreEqual ("XA4327", Errors.Single ().Code); + FileAssert.DoesNotExist (task.OutputFile); + } + + [Test] + public void LinkedAssembliesFilterUnusedMappings () + { + var fixture = new JniFixtureBuilder (); + int fieldStart = fixture.NextFieldRid; + int methodStart = fixture.NextMethodRid; + MethodDefinitionHandle onClick = fixture.AddVoidMethod ("OnClick", fixture.EmitReturnOnlyBody ()); + fixture.Metadata.AddCustomAttribute (onClick, fixture.RegisterCtor3, + fixture.AttributeBlob ("onClick", "()V", "n_OnClick")); + TypeDefinitionHandle peer = fixture.AddType ("Com.Contoso", "Peer", fieldStart, methodStart, + TypeAttributes.Public | TypeAttributes.Class); + fixture.Metadata.AddCustomAttribute (peer, fixture.RegisterCtor1, fixture.AttributeBlob ("com/contoso/Peer")); + + string assembly = Path.Combine (TestDirectory, "Linked.dll"); + File.WriteAllBytes (assembly, fixture.Serialize ()); + var xml = Run ( + """ + com.contoso.Peer -> a.b: + void onClick() -> c + com.contoso.Unused -> a.d: + void unused() -> e + """, + [ assembly ], + []); + + StringAssert.Contains ("""""", xml); + StringAssert.Contains (Method ("com/contoso/Peer", "onClick", "()V", "a/b", "c", "()V"), xml); + StringAssert.DoesNotContain ("com/contoso/Unused", xml); + StringAssert.DoesNotContain ("unused", xml); + } + + static string Method (string sourceType, string name, string signature, string targetType, string targetName, string targetSignature) => + $""""""; + + static string Field (string sourceType, string name, string signature, string targetType, string targetName, string targetSignature) => + $""""""; + + [Test] + public void RenamedClassesProduceForwardAndReverseTypeEntries () + { + var xml = Run ( + """ + com.contoso.MainActivity -> a.b: + com.contoso.Untouched -> com.contoso.Untouched: + """); + + StringAssert.Contains ("""""", xml); + StringAssert.Contains ("""""", xml); + StringAssert.DoesNotContain ("com/contoso/Untouched", xml, "Unchanged classes must not produce entries."); + } + + [Test] + public void MergedClassesDoNotProduceReverseTypeEntries () + { + // R8 class merging maps two originals onto one residual class: the reverse + // direction is ambiguous and must not be described at all. + var xml = Run ( + """ + com.contoso.One -> a.b: + com.contoso.Two -> a.b: + """); + + StringAssert.Contains ("""""", xml); + StringAssert.Contains ("""""", xml); + StringAssert.DoesNotContain ("reverse-type", xml); + } + + [Test] + public void RemovedClassesAreSkipped () + { + var xml = Run ( + """ + com.contoso.Gone -> R8$$REMOVED$$CLASS$$1: + """); + + StringAssert.DoesNotContain ("com/contoso/Gone", xml); + } + + [Test] + public void MethodOverloadsKeepDistinctSignatures () + { + var xml = Run ( + """ + com.contoso.Peer -> a.b: + void doWork(int) -> c + void doWork(java.lang.String) -> d + void doWork() -> e + """); + + StringAssert.Contains (Method ("com/contoso/Peer", "doWork", "(I)V", "a/b", "c", "(I)V"), xml); + StringAssert.Contains (Method ("com/contoso/Peer", "doWork", "(Ljava/lang/String;)V", "a/b", "d", "(Ljava/lang/String;)V"), xml); + StringAssert.Contains (Method ("com/contoso/Peer", "doWork", "()V", "a/b", "e", "()V"), xml); + } + + [Test] + public void MethodDescriptorsAreRewrittenThroughTheMapping () + { + var xml = Run ( + """ + com.contoso.Peer -> a.b: + com.contoso.Result run(com.contoso.Argument[],int) -> c + com.contoso.Argument -> a.d: + com.contoso.Result -> a.e: + """); + + StringAssert.Contains ( + Method ("com/contoso/Peer", "run", "([Lcom/contoso/Argument;I)Lcom/contoso/Result;", "a/b", "c", "([La/d;I)La/e;"), + xml); + } + + [Test] + public void ConstructorsAreEmittedWhenOnlyTheirDescriptorChanges () + { + var xml = Run ( + """ + com.contoso.Peer -> com.contoso.Peer: + void (com.contoso.Argument) -> + com.contoso.Argument -> a.d: + """); + + StringAssert.Contains ( + Method ("com/contoso/Peer", "<init>", "(Lcom/contoso/Argument;)V", "com/contoso/Peer", "<init>", "(La/d;)V"), + xml); + } + + [Test] + public void UnchangedMembersAreNotEmitted () + { + var xml = Run ( + """ + com.contoso.Peer -> com.contoso.Peer: + void doWork(int) -> doWork + int counter -> counter + """); + + StringAssert.DoesNotContain ("replace-method", xml); + StringAssert.DoesNotContain ("replace-field", xml); + } + + [Test] + public void FieldsAreEmittedWithRewrittenSignatures () + { + var xml = Run ( + """ + com.contoso.Peer -> a.b: + int counter -> c + com.contoso.Argument argument -> d + com.contoso.Argument[] arguments -> e + com.contoso.Argument -> a.d: + """); + + StringAssert.Contains (Field ("com/contoso/Peer", "counter", "I", "a/b", "c", "I"), xml); + StringAssert.Contains (Field ("com/contoso/Peer", "argument", "Lcom/contoso/Argument;", "a/b", "d", "La/d;"), xml); + StringAssert.Contains (Field ("com/contoso/Peer", "arguments", "[Lcom/contoso/Argument;", "a/b", "e", "[La/d;"), xml); + } + + [Test] + public void AmbiguousMethodNamesAreSkipped () + { + // The same method mapped to two different residual names has no single runtime name. + var xml = Run ( + """ + com.contoso.Peer -> a.b: + void doWork(int) -> c + void doWork(int) -> d + """); + + StringAssert.DoesNotContain ("doWork", xml); + } + + [Test] + public void OutputIsDeterministic () + { + // The mapping is written in a different order the second time around. + const string first = + """ + com.contoso.Zebra -> a.b: + void run(int) -> c + int counter -> d + com.contoso.Apple -> a.e: + void run() -> f + """; + const string second = + """ + com.contoso.Apple -> a.e: + void run() -> f + com.contoso.Zebra -> a.b: + int counter -> d + void run(int) -> c + """; + + Assert.AreEqual (Run (first), Run (second), "The output must not depend on the mapping file's order."); + } + + [Test] + public void MalformedMappingIsReportedAsXA4327 () + { + var task = new GenerateR8JniRemapping { + BuildEngine = engine, + MappingFile = WriteMapping (" void doWork(int) -> c\n"), + OutputFile = Path.Combine (TestDirectory, "r8-jni-remap.xml"), + }; + + Assert.IsFalse (task.Execute (), "Task should have failed."); + Assert.AreEqual (1, Errors.Count, "Task should have reported one error."); + Assert.AreEqual ("XA4327", Errors [0].Code); + } + + [Test] + public void MissingMappingIsReportedAsXA4327 () + { + var task = new GenerateR8JniRemapping { + BuildEngine = engine, + MappingFile = Path.Combine (TestDirectory, "does-not-exist.txt"), + OutputFile = Path.Combine (TestDirectory, "r8-jni-remap.xml"), + }; + + Assert.IsFalse (task.Execute (), "Task should have failed."); + Assert.AreEqual (1, Errors.Count, "Task should have reported one error."); + Assert.AreEqual ("XA4327", Errors [0].Code); + } + + [Test] + public void ExistingRemapEntriesAreNotOverridden () + { + var existing = WriteRemapXml ( + """ + + + + """); + + var xml = Run ( + """ + com.contoso.MainActivity -> a.b: + com.contoso.Other -> a.c: + """, + existing); + + StringAssert.DoesNotContain ("com/contoso/MainActivity", xml, + "The pre-existing remapping input must win."); + StringAssert.Contains ("""""", xml); + Assert.AreEqual (1, Warnings.Count, "The conflict should have been reported."); + Assert.AreEqual ("XA4328", Warnings [0].Code); + } + + [Test] + public void IdenticalExistingRemapEntriesDoNotWarn () + { + var existing = WriteRemapXml ( + """ + + + + """); + + var xml = Run ( + """ + com.contoso.MainActivity -> a.b: + """, + existing); + + StringAssert.DoesNotContain ("replace-type", xml, + "A duplicate entry must not be emitted twice."); + Assert.AreEqual (0, Warnings.Count, "An identical entry is not a conflict."); + } + + [Test] + public void ExistingMethodEntriesOnlyConflictForTheSameOverload () + { + var existing = WriteRemapXml ( + """ + + + + """); + + var xml = Run ( + """ + com.contoso.Peer -> a.b: + void doWork(int) -> c + void doWork(java.lang.String) -> d + """, + existing); + + StringAssert.DoesNotContain ("(I)V", xml, + "The overload owned by another input must not be emitted."); + StringAssert.Contains (Method ("com/contoso/Peer", "doWork", "(Ljava/lang/String;)V", "a/b", "d", "(Ljava/lang/String;)V"), xml, + "A different overload is not a conflict."); + Assert.AreEqual (1, Warnings.Count); + Assert.AreEqual ("XA4328", Warnings [0].Code); + } + + [Test] + public void GeneratedDocumentParsesWithTheExistingRemapSchema () + { + var mappingFile = WriteMapping ( + """ + com.contoso.Peer -> a.b: + void doWork(int) -> c + int counter -> d + """); + var outputFile = Path.Combine (TestDirectory, "r8-jni-remap.xml"); + var task = new GenerateR8JniRemapping { + BuildEngine = engine, + MappingFile = mappingFile, + OutputFile = outputFile, + }; + Assert.IsTrue (task.Execute (), "Task should have succeeded."); + + var mergedFile = Path.Combine (TestDirectory, "xa-remap-members.xml"); + var mamFile = WriteRemapXml ( + """ + + + + """, + "mam.xml"); + var merge = new MergeRemapXml { + BuildEngine = engine, + InputRemapXmlFiles = new ITaskItem [] { + new TaskItem (mamFile), + new TaskItem (outputFile), + }, + OutputFile = new TaskItem (mergedFile), + }; + Assert.IsTrue (merge.Execute (), "MergeRemapXml should have succeeded."); + Assert.AreEqual (0, Errors.Count, "The merge should have no errors."); + + var merged = File.ReadAllText (mergedFile); + StringAssert.Contains ("""""", merged, + "Existing inputs must survive the merge."); + StringAssert.Contains ("""""", merged); + StringAssert.Contains ("replace-field", merged, "New elements must survive the merge."); + + // The pre-existing consumer must still be able to read the merged document. + var generate = new GenerateJniRemappingNativeCode { + BuildEngine = engine, + RemappingXmlFilePath = new TaskItem (mergedFile), + OutputDirectory = TestDirectory, + SupportedAbis = new [] { "arm64-v8a" }, + }; + Assert.IsTrue (generate.Execute (), "GenerateJniRemappingNativeCode should have succeeded."); + Assert.AreEqual (0, Errors.Count, "The generated document must parse with the existing schema."); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs index 197e6dd0c7e..96206e0c5c8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs @@ -341,8 +341,9 @@ public void Execute_ManifestPlaceholdersAreResolvedForRooting () Assert.IsFalse (warnings.Any (w => w.Code == "XA4250"), "Resolved placeholder-based manifest references should not log XA4250."); } - [Test] - public void Execute_GenerateNativeAotProguardConfiguration_UsesDgmlTypeMetadata () + [TestCase (false)] + [TestCase (true)] + public void Execute_GenerateNativeAotProguardConfiguration_UsesDgmlTypeMetadata (bool enableObfuscation) { var path = Path.Combine (Root, "temp", TestName); var dgmlFile = Path.Combine (path, "app.scan.dgml.xml"); @@ -379,14 +380,16 @@ public void Execute_GenerateNativeAotProguardConfiguration_UsesDgmlTypeMetadata AcwMapFile = acwMapFile, OutputFile = outputFile, TrimJavaCallableWrappers = true, + EnableObfuscation = enableObfuscation, }; Assert.IsTrue (task.Execute (), "Task should succeed."); var proguard = File.ReadAllText (outputFile); - StringAssert.Contains ("-keep class crc64a1.MainActivity { *; }", proguard); - StringAssert.Contains ("-keep class android.app.Activity { *; }", proguard); - StringAssert.Contains ("-keep class my.app.Duplicate { *; }", proguard); - StringAssert.Contains ("-keep class androidx.activity.result.contract.ActivityResultContracts$TakePicture { *; }", proguard); + var keepOption = enableObfuscation ? "-keep,allowobfuscation" : "-keep"; + StringAssert.Contains ($"{keepOption} class crc64a1.MainActivity {{ *; }}", proguard); + StringAssert.Contains ($"{keepOption} class android.app.Activity {{ *; }}", proguard); + StringAssert.Contains ($"{keepOption} class my.app.Duplicate {{ *; }}", proguard); + StringAssert.Contains ($"{keepOption} class androidx.activity.result.contract.ActivityResultContracts$TakePicture {{ *; }}", proguard); StringAssert.DoesNotContain ("wrong.Duplicate", proguard); StringAssert.DoesNotContain ("other.Type", proguard); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs index d6f5f8b1bec..41565a5e62f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Linq; using NUnit.Framework; using Xamarin.Android.Tasks; @@ -32,8 +33,7 @@ public void TryGetDisallowedOption (string line, bool expected, string expectedO Assert.AreEqual (expectedOption, option); } - [TestCase ("package com.example.app;\npublic class Foo {}", "com.example.app")] - [TestCase ("package com.example.app ;\npublic class Foo {}", "com.example.app")] // space before ';' + [TestCase ("package com.example.app;\npublic class Foo {}", "com.example.app")] [TestCase ("package com.example.app ;\npublic class Foo {}", "com.example.app")] // space before ';' [TestCase ("// header\n/* license */\npackage com.example.app;\nclass Foo {}", "com.example.app")] // skip comments [TestCase ("public class Foo {}", null)] // no package [TestCase ("import java.util.List;\npackage com.late;\nclass Foo {}", null)] // package after import is ignored @@ -48,6 +48,45 @@ public void ReadJavaPackage (string content, string? expected) File.Delete (path); } } + + [TestCase (false, "-keep")] + [TestCase (true, "-keep,allowobfuscation")] + public void KeepOption (bool enableObfuscation, string expected) + { + var task = new R8 { EnableObfuscation = enableObfuscation }; + Assert.AreEqual (expected, task.KeepOption); + } + + [TestCase (false, true, false)] + [TestCase (true, false, false)] + [TestCase (false, true, true)] + [TestCase (true, false, true)] + public void GenerateCommonXamarinConfiguration_OnlyDropsDontObfuscate (bool enableObfuscation, bool expectDontObfuscate, bool nativeAot) + { + var path = Path.GetTempFileName (); + try { + var task = new R8 { + BuildEngine = new MockBuildEngine (TestContext.Out), + EnableObfuscation = enableObfuscation, + UseTrimmableNativeAotProguardConfiguration = nativeAot, + ProguardCommonXamarinConfiguration = path, + }; + task.GenerateCommonXamarinConfiguration (); + + var lines = File.ReadAllLines (path); + Assert.AreEqual (expectDontObfuscate, lines.Any (l => l.Trim () == "-dontobfuscate"), + "-dontobfuscate is the only option that may be dropped."); + Assert.IsTrue (lines.Any (l => l.Contains ("-keep class net.dot.jni.")), + "Every other rule must survive."); + if (nativeAot) { + CollectionAssert.Contains (lines, "-keep class net.dot.android.ApplicationRegistration { *; (...); }"); + CollectionAssert.Contains (lines, "-keep class mono.android.Runtime { *; }"); + CollectionAssert.Contains (lines, "-keep class mono.android.GCUserPeer { (); }"); + CollectionAssert.Contains (lines, "-keep class mono.android.IGCUserPeer { *; }"); + } + } finally { + File.Delete (path); + } + } } } - diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniAssemblyRewriter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniAssemblyRewriter.cs index 0253b5aed5e..6b8c4bcbff9 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniAssemblyRewriter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniAssemblyRewriter.cs @@ -65,5 +65,8 @@ public static void ScanRewrittenAssembly (byte [] sourceImage, R8Mapping mapping public static void ScanRewrittenAssembly (PEReader peReader, MetadataReader reader, R8Mapping mapping, TaskLoggingHelper log) => new JniRewritePlanner (peReader, reader, mapping.CreateReverseMapping (), log).CreatePlan (); + + public static void ScanAssembly (PEReader peReader, MetadataReader reader, R8Mapping mapping, TaskLoggingHelper log) + => new JniRewritePlanner (peReader, reader, mapping, log).CreatePlan (); } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniDescriptorText.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniDescriptorText.cs index b60d63a6d65..b464030d6aa 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniDescriptorText.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniDescriptorText.cs @@ -271,5 +271,58 @@ public static void MethodDescriptorToJavaTypes (string descriptor, out List + /// Converts a Java *source* form type as used in mapping.txt member lines ("int", + /// "java.lang.String[]") to its JNI type token ("I", "[Ljava/lang/String;"). + /// + public static string JavaSourceTypeToJniTypeToken (string javaSourceType) + { + string trimmed = javaSourceType.Trim (); + int arrayDepth = 0; + int elementEnd = trimmed.Length; + while (elementEnd >= 2 && + trimmed [elementEnd - 1] == ']' && + trimmed [elementEnd - 2] == '[') { + arrayDepth++; + elementEnd -= 2; + } + + string elementType = trimmed.Substring (0, elementEnd).Trim (); + if (elementType.Length == 0) { + throw new ArgumentException ($"Malformed Java source type '{javaSourceType}'.", nameof (javaSourceType)); + } + + string elementToken = elementType switch { + "void" => "V", + "boolean" => "Z", + "byte" => "B", + "char" => "C", + "short" => "S", + "int" => "I", + "long" => "J", + "float" => "F", + "double" => "D", + _ => "L" + elementType.Replace ('.', '/') + ";", + }; + + return arrayDepth == 0 ? elementToken : new string ('[', arrayDepth) + elementToken; + } + + /// + /// Builds a JNI method descriptor from Java *source* form parameter and return types, + /// e.g. (["android.os.Bundle", "int"], "void") -> "(Landroid/os/Bundle;I)V". + /// + public static string JavaSourceTypesToMethodDescriptor (IReadOnlyList javaParameterTypes, string javaReturnType) + { + var descriptor = new StringBuilder (); + descriptor.Append ('('); + foreach (string javaParameterType in javaParameterTypes) { + descriptor.Append (JavaSourceTypeToJniTypeToken (javaParameterType)); + } + descriptor.Append (')'); + descriptor.Append (JavaSourceTypeToJniTypeToken (javaReturnType)); + return descriptor.ToString (); + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/NativeAotJniRetention.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/NativeAotJniRetention.cs new file mode 100644 index 00000000000..7c60d21254a --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/NativeAotJniRetention.cs @@ -0,0 +1,252 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using ELFSharp; +using ELFSharp.ELF; +using ELFSharp.ELF.Sections; + +namespace Xamarin.Android.Tasks.JniRemapping +{ + /// + /// A conservative bound for normal generated bindings with literal JNI identifiers. + /// Unlike compiled method names, literals survive inlining, generic sharing and static initialization. + /// Frozen strings are UTF-16; reflection metadata contains UTF-8 strings. Neither symbol + /// names nor debug information are evidence that an identifier survived compilation. + /// Arbitrary runtime-constructed names require explicit remapping or R8 keep rules. + /// + static class NativeAotJniRetention + { + public static HashSet GetRequiredEntries (string objectFile, R8Mapping mapping) + { + var sections = ReadObjectData (objectFile); + var classes = new List (mapping.EnumerateClassMappings ()); + var classPatterns = new LiteralMatcher (); + foreach (var type in classes) { + classPatterns.Add (type.OriginalJniName); + classPatterns.Add (type.OriginalJniName.Replace ('/', '.')); + } + HashSet retainedClasses = classPatterns.Match (sections); + + var memberPatterns = new LiteralMatcher (); + var candidateClasses = new List (); + foreach (var type in classes) { + if (!retainedClasses.Contains (type.OriginalJniName) && + !retainedClasses.Contains (type.OriginalJniName.Replace ('/', '.'))) { + continue; + } + candidateClasses.Add (type); + foreach (var method in type.Methods) { + memberPatterns.Add (method.OriginalName); + memberPatterns.Add (JniDescriptorText.JavaSourceTypesToMethodDescriptor (method.JavaParameterTypes, method.JavaReturnType)); + } + foreach (var field in type.Fields) { + memberPatterns.Add (field.OriginalName); + memberPatterns.Add (JniDescriptorText.JavaSourceTypeToJniTypeToken (field.JavaFieldType)); + } + } + HashSet retainedMembers = memberPatterns.Match (sections); + var required = new HashSet (StringComparer.Ordinal); + foreach (var type in candidateClasses) { + required.Add (R8Mapping.BuildClassEntry (type.OriginalJniName)); + foreach (var method in type.Methods) { + string descriptor = JniDescriptorText.JavaSourceTypesToMethodDescriptor (method.JavaParameterTypes, method.JavaReturnType); + // Generated constructor calls carry only the descriptor, not "". + bool constructor = method.OriginalName == "" || method.OriginalName == ""; + if (retainedMembers.Contains (descriptor) && (constructor || retainedMembers.Contains (method.OriginalName))) { + required.Add (R8Mapping.BuildMethodEntry (type.OriginalJniName, + R8Mapping.BuildMethodKey (method.OriginalName, method.JavaParameterTypes, method.JavaReturnType))); + } + } + foreach (var field in type.Fields) { + string descriptor = JniDescriptorText.JavaSourceTypeToJniTypeToken (field.JavaFieldType); + if (retainedMembers.Contains (field.OriginalName) && retainedMembers.Contains (descriptor)) { + required.Add (R8Mapping.BuildFieldEntry (type.OriginalJniName, field.OriginalName)); + } + } + } + return required; + } + + static List ReadObjectData (string path) + { + using var stream = File.OpenRead (path); + using IELF elf = ReadElfData (() => ELFReader.Load (stream, shouldOwnStream: false)); + ulong fileSize = (ulong) stream.Length; + if (elf.Type != FileType.Relocatable || elf.Endianess != Endianess.LittleEndian || + (elf.Class != Class.Bit64 && elf.Class != Class.Bit32)) { + throw new InvalidDataException (Properties.Resources.XA4327_NativeAotObjectFormat); + } + var data = new List (); + bool hasManagedCode = false; + bool hasData = false; + foreach (ISection section in elf.Sections) { + ulong offset; + ulong size; + if (section is Section section64) { + offset = section64.Offset; + size = section64.Size; + } else if (section is Section section32) { + offset = section32.Offset; + size = section32.Size; + } else { + throw new InvalidDataException (Properties.Resources.XA4327_NativeAotObjectFormat); + } + if (section.Type != SectionType.NoBits && (offset > fileSize || size > fileSize - offset)) { + throw new InvalidDataException (Properties.Resources.XA4327_NativeAotInvalidSection); + } + if ((section.Flags & SectionFlags.Allocatable) == 0 || section.Type == SectionType.NoBits) { + continue; + } + byte [] contents = ReadElfData (() => section.GetContents ()); + if ((ulong) contents.Length != size) { + throw new InvalidDataException (Properties.Resources.XA4327_NativeAotTruncatedSection); + } + if (contents.Length == 0) { + continue; + } + hasManagedCode |= section.Name == "__managedcode"; + hasData |= (section.Flags & SectionFlags.Executable) == 0; + data.Add (contents); + } + if (!hasManagedCode || !hasData) { + throw new InvalidDataException (Properties.Resources.XA4327_NativeAotMissingSections); + } + return data; + } + + static T ReadElfData (Func read) + { + try { + return read (); + } catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException || + ex is IndexOutOfRangeException || ex is OverflowException) { + // ELFSharp uses these exceptions for malformed headers, string tables and section + // indexes. Normalize only library reads, not failures in the retention matcher. + throw new InvalidDataException (ex.Message, ex); + } + } + + // Match substrings deliberately: member IDs, registration blocks and descriptors contain + // multiple JNI identifiers. Shared or coincidental matches can only retain extra entries. + // A compact Aho-Corasick trie avoids scanning a large object once per mapping entry. + sealed class LiteralMatcher + { + struct Node + { + public byte Value; + public int Child; + public int Sibling; + public int Failure; + public int Output; + public List? Patterns; + } + + Node [] nodes = new Node [256]; + int count = 1; + readonly int [] root = new int [256]; + readonly HashSet patterns = new HashSet (StringComparer.Ordinal); + + public void Add (string pattern) + { + if (pattern.Length == 0 || !patterns.Add (pattern)) { + return; + } + Add (Encoding.UTF8.GetBytes (pattern), pattern); + byte [] utf16 = Encoding.Unicode.GetBytes (pattern); + // ILC dehydration replaces runs of >=4 zero bytes. A legal JNI identifier has + // no NULs, so its interior is intact, but a boundary zero byte can join a run + // in the string header, terminator or alignment padding. Do not require it. + int start = utf16 [0] == 0 ? 1 : 0; + int length = utf16.Length - start - (utf16 [utf16.Length - 1] == 0 ? 1 : 0); + var payload = new byte [length]; + Buffer.BlockCopy (utf16, start, payload, 0, length); + Add (payload, pattern); + } + + void Add (byte [] bytes, string pattern) + { + int current = 0; + foreach (byte value in bytes) { + int next = Find (current, value); + if (next == 0) { + if (count == nodes.Length) { + Array.Resize (ref nodes, checked (nodes.Length * 2)); + } + next = count++; + nodes [next].Value = value; + nodes [next].Sibling = nodes [current].Child; + nodes [current].Child = next; + if (current == 0) { + root [value] = next; + } + } + current = next; + } + var terminalPatterns = nodes [current].Patterns; + if (terminalPatterns == null) { + nodes [current].Patterns = terminalPatterns = new List (); + } + terminalPatterns.Add (pattern); + } + + int Find (int node, byte value) + { + if (node == 0) { + return root [value]; + } + for (int child = nodes [node].Child; child != 0; child = nodes [child].Sibling) { + if (nodes [child].Value == value) { + return child; + } + } + return 0; + } + + public HashSet Match (List sections) + { + var queue = new Queue (); + for (int child = nodes [0].Child; child != 0; child = nodes [child].Sibling) { + queue.Enqueue (child); + } + while (queue.Count > 0) { + int parent = queue.Dequeue (); + for (int child = nodes [parent].Child; child != 0; child = nodes [child].Sibling) { + int failure = nodes [parent].Failure; + int next; + while ((next = Find (failure, nodes [child].Value)) == 0 && failure != 0) { + failure = nodes [failure].Failure; + } + nodes [child].Failure = next; + nodes [child].Output = nodes [next].Patterns != null ? next : nodes [next].Output; + queue.Enqueue (child); + } + } + + var found = new HashSet (StringComparer.Ordinal); + foreach (byte [] section in sections) { + int current = 0; + foreach (byte value in section) { + int next; + while ((next = Find (current, value)) == 0 && current != 0) { + current = nodes [current].Failure; + } + current = next; + for (int output = current; output != 0; output = nodes [output].Output) { + var terminalPatterns = nodes [output].Patterns; + if (terminalPatterns != null) { + foreach (string pattern in terminalPatterns) { + found.Add (pattern); + } + } + } + } + } + return found; + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs index d8506ecdb73..025a009d1e4 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs @@ -30,6 +30,9 @@ sealed class R8Mapping : IJniNameMapping // Original JNI class name -> (original field name -> obfuscated field name). readonly Dictionary> fields = new Dictionary> (StringComparer.Ordinal); + // Original JNI class name -> (original field name -> declared field type, in Java source form). + readonly Dictionary> fieldTypes = new Dictionary> (StringComparer.Ordinal); + // Original JNI class name -> ("name(javaParam,javaParam,...):javaReturn" -> obfuscated method name). readonly Dictionary> methods = new Dictionary> (StringComparer.Ordinal); @@ -136,6 +139,10 @@ static R8Mapping Parse (TextReader reader, string sourceName) mapping.fields [currentOriginalClass] = classFields = new Dictionary (StringComparer.Ordinal); } classFields [memberName] = obfuscatedName; + if (!mapping.fieldTypes.TryGetValue (currentOriginalClass, out var classFieldTypes)) { + mapping.fieldTypes [currentOriginalClass] = classFieldTypes = new Dictionary (StringComparer.Ordinal); + } + classFieldTypes [memberName] = javaReturnType ?? ""; } else { string key = BuildMethodKey (memberName, javaParameterTypes, javaReturnType ?? ""); if (positionRange == null) { @@ -465,8 +472,95 @@ public IEnumerable GetReachabilityConflicts (R8Mapping finalMapping, IEn } } - internal static string BuildClassEntry (string className) => $"C\t{className}"; - internal static string BuildFieldEntry (string className, string fieldName) => $"F\t{className}\t{fieldName}"; + /// + /// Enumerates every surviving class mapping, and the field and method mappings it + /// declares, in a stable order (ordinal by original JNI class name, then by member + /// name and signature). Classes R8 removed and members whose residual name is + /// ambiguous are skipped, so the result only describes names that exist at runtime. + /// Unlike the TryGet* lookups this does not record accessed entries: it is a + /// read-only projection of the parsed mapping. + /// + internal IEnumerable EnumerateClassMappings () + { + var originalClassNames = new List (classes.Keys); + originalClassNames.Sort (StringComparer.Ordinal); + foreach (string originalClassName in originalClassNames) { + string obfuscatedClassName = classes [originalClassName]; + if (IsRemovedClassName (obfuscatedClassName)) { + continue; + } + yield return new R8ClassMapping ( + originalClassName, + obfuscatedClassName, + EnumerateFieldMappings (originalClassName), + EnumerateMethodMappings (originalClassName)); + } + } + + List EnumerateFieldMappings (string originalClassName) + { + var result = new List (); + if (!fields.TryGetValue (originalClassName, out var classFields)) { + return result; + } + + var fieldNames = new List (classFields.Keys); + fieldNames.Sort (StringComparer.Ordinal); + fieldTypes.TryGetValue (originalClassName, out var classFieldTypes); + foreach (string fieldName in fieldNames) { + string javaFieldType = ""; + classFieldTypes?.TryGetValue (fieldName, out javaFieldType); + result.Add (new R8FieldMapping (fieldName, classFields [fieldName], javaFieldType ?? "")); + } + return result; + } + + List EnumerateMethodMappings (string originalClassName) + { + var result = new List (); + if (!methods.TryGetValue (originalClassName, out var classMethods)) { + return result; + } + + var methodKeys = new List (classMethods.Keys); + methodKeys.Sort (StringComparer.Ordinal); + foreach (string methodKey in methodKeys) { + string obfuscatedName = classMethods [methodKey]; + if (obfuscatedName.Length == 0) { + // Inlined into several destinations: no single residual name exists. + continue; + } + if (!TrySplitMethodKey (methodKey, out string name, out string [] javaParameterTypes, out string javaReturnType)) { + continue; + } + result.Add (new R8MethodMapping (name, obfuscatedName, javaParameterTypes, javaReturnType)); + } + return result; + } + + /// + /// Splits a key built by back into its parts. + /// + internal static bool TrySplitMethodKey (string methodKey, out string javaMethodName, out string [] javaParameterTypes, out string javaReturnType) + { + javaMethodName = ""; + javaParameterTypes = Array.Empty (); + javaReturnType = ""; + + int parenOpen = methodKey.IndexOf ('('); + int parenClose = methodKey.LastIndexOf ("):", StringComparison.Ordinal); + if (parenOpen < 0 || parenClose < parenOpen) { + return false; + } + + javaMethodName = methodKey.Substring (0, parenOpen); + string parameterList = methodKey.Substring (parenOpen + 1, parenClose - parenOpen - 1); + javaParameterTypes = parameterList.Length == 0 ? Array.Empty () : parameterList.Split (','); + javaReturnType = methodKey.Substring (parenClose + 2); + return javaMethodName.Length != 0; + } + + internal static string BuildClassEntry (string className) => $"C\t{className}"; internal static string BuildFieldEntry (string className, string fieldName) => $"F\t{className}\t{fieldName}"; internal static string BuildMethodEntry (string className, string methodKey) => $"M\t{className}\t{methodKey}"; internal static string CreateManifestContent (IEnumerable entries) @@ -742,7 +836,7 @@ static bool TryParseMemberLine (string trimmed, out string name, out string []? name = left.Substring (lastSpace + 1); javaParameterTypes = null; - javaReturnType = null; + javaReturnType = left.Substring (0, lastSpace); return name.Length > 0; } } @@ -810,4 +904,65 @@ static string StripTrailingLineRange (string s) return s.Substring (0, lastColon); } } + + /// + /// One class rename described by a mapping.txt file, plus the member renames declared + /// inside it. Produced by . + /// + sealed class R8ClassMapping + { + public string OriginalJniName { get; } + public string ObfuscatedJniName { get; } + public IReadOnlyList Fields { get; } + public IReadOnlyList Methods { get; } + + public bool IsRenamed => !String.Equals (OriginalJniName, ObfuscatedJniName, StringComparison.Ordinal); + + public R8ClassMapping (string originalJniName, string obfuscatedJniName, IReadOnlyList fields, IReadOnlyList methods) + { + OriginalJniName = originalJniName; + ObfuscatedJniName = obfuscatedJniName; + Fields = fields; + Methods = methods; + } + } + + sealed class R8FieldMapping + { + public string OriginalName { get; } + public string ObfuscatedName { get; } + + /// The declared field type in Java source form, e.g. "int" or "java.lang.String[]". + public string JavaFieldType { get; } + + public bool IsRenamed => !String.Equals (OriginalName, ObfuscatedName, StringComparison.Ordinal); + + public R8FieldMapping (string originalName, string obfuscatedName, string javaFieldType) + { + OriginalName = originalName; + ObfuscatedName = obfuscatedName; + JavaFieldType = javaFieldType; + } + } + + sealed class R8MethodMapping + { + public string OriginalName { get; } + public string ObfuscatedName { get; } + + /// Parameter types in Java source form; they identify the specific overload. + public IReadOnlyList JavaParameterTypes { get; } + + public string JavaReturnType { get; } + + public bool IsRenamed => !String.Equals (OriginalName, ObfuscatedName, StringComparison.Ordinal); + + public R8MethodMapping (string originalName, string obfuscatedName, IReadOnlyList javaParameterTypes, string javaReturnType) + { + OriginalName = originalName; + ObfuscatedName = obfuscatedName; + JavaParameterTypes = javaParameterTypes; + JavaReturnType = javaReturnType; + } + } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index c79f4855a58..61e73210c1d 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -31,10 +31,18 @@ sealed class JniRemappingMethodReplacement public string TargetType { get; } public string TargetMethod { get; } + /// + /// The JNI method descriptor to use on the target type, or null when the source + /// signature is used unchanged. Remapping inputs which predate this attribute (for example + /// the Intune/MAM mapping) leave it unset. + /// + public string TargetMethodSignature { get; } + public bool TargetIsStatic { get; } public JniRemappingMethodReplacement (string sourceType, string sourceMethod, string sourceMethodSignature, - string targetType, string targetMethod, bool targetIsStatic) + string targetType, string targetMethod, string targetMethodSignature, + bool targetIsStatic) { SourceType = sourceType; SourceMethod = sourceMethod; @@ -42,14 +50,48 @@ public JniRemappingMethodReplacement (string sourceType, string sourceMethod, st TargetType = targetType; TargetMethod = targetMethod; + TargetMethodSignature = targetMethodSignature; TargetIsStatic = targetIsStatic; } } + sealed class JniRemappingFieldReplacement + { + public string SourceType { get; } + public string SourceField { get; } + public string SourceFieldSignature { get; } + + public string TargetType { get; } + public string TargetField { get; } + public string TargetFieldSignature { get; } + + public JniRemappingFieldReplacement (string sourceType, string sourceField, string sourceFieldSignature, + string targetType, string targetField, string targetFieldSignature) + { + SourceType = sourceType; + SourceField = sourceField; + SourceFieldSignature = sourceFieldSignature; + + TargetType = targetType; + TargetField = targetField; + TargetFieldSignature = targetFieldSignature; + } + } + class JniRemappingAssemblyGenerator : LlvmIrComposer { const string TypeReplacementsVariableName = "jni_remapping_type_replacements"; + const string ReverseTypeReplacementsVariableName = "jni_remapping_reverse_type_replacements"; const string MethodReplacementIndexVariableName = "jni_remapping_method_replacement_index"; + const string FieldReplacementIndexVariableName = "jni_remapping_field_replacement_index"; + + // The runtime reads the table sizes from these symbols instead of `application_config`, so + // that the same lookup implementation works in the NativeAOT build, which has no + // application config at all. + const string TypeReplacementCountVariableName = "jni_remapping_type_replacement_count"; + const string ReverseTypeReplacementCountVariableName = "jni_remapping_reverse_type_replacement_count"; + const string MethodReplacementIndexCountVariableName = "jni_remapping_method_replacement_index_count"; + const string FieldReplacementIndexCountVariableName = "jni_remapping_field_replacement_index_count"; sealed class JniRemappingTypeReplacementEntryContextDataProvider : NativeAssemblerStructContextDataProvider { @@ -130,6 +172,67 @@ public override string GetComment (object data, string fieldName) } } + sealed class JniRemappingIndexFieldTypeEntryContextDataProvider : NativeAssemblerStructContextDataProvider + { + public override string GetComment (object data, string fieldName) + { + var entry = EnsureType (data); + + if (MonoAndroidHelper.StringEquals ("name", fieldName)) { + return $" name: {entry.name.str}"; + } + + return String.Empty; + } + + public override string GetPointedToSymbolName (object data, string fieldName) + { + var entry = EnsureType (data); + + if (MonoAndroidHelper.StringEquals ("fields", fieldName)) { + return entry.FieldsArraySymbolName; + } + + return base.GetPointedToSymbolName (data, fieldName); + } + + public override ulong GetBufferSize (object data, string fieldName) + { + var entry = EnsureType (data); + if (MonoAndroidHelper.StringEquals ("fields", fieldName)) { + return (ulong)entry.TypeFields.Count; + } + + return 0; + } + } + + sealed class JniRemappingIndexFieldEntryContextDataProvider : NativeAssemblerStructContextDataProvider + { + public override string GetComment (object data, string fieldName) + { + var entry = EnsureType (data); + + if (MonoAndroidHelper.StringEquals ("name", fieldName)) { + return $" name: {entry.name.str}"; + } + + if (MonoAndroidHelper.StringEquals ("replacement", fieldName)) { + return $" replacement: {entry.replacement.target_type}.{entry.replacement.target_name}"; + } + + if (MonoAndroidHelper.StringEquals ("signature", fieldName)) { + if (entry.signature.length == 0) { + return String.Empty; + } + + return $"signature: {entry.signature.str}"; + } + + return String.Empty; + } + } + sealed class JniRemappingString { public uint length; @@ -140,9 +243,17 @@ sealed class JniRemappingReplacementMethod { public string target_type; public string target_name; + public string target_signature; public bool is_static; }; + sealed class JniRemappingReplacementField + { + public string target_type; + public string target_name; + public string target_signature; + }; + [NativeAssemblerStructContextDataProvider (typeof(JniRemappingIndexMethodEntryContextDataProvider))] sealed class JniRemappingIndexMethodEntry { @@ -175,6 +286,38 @@ sealed class JniRemappingIndexTypeEntry public List> TypeMethods; }; + [NativeAssemblerStructContextDataProvider (typeof(JniRemappingIndexFieldEntryContextDataProvider))] + sealed class JniRemappingIndexFieldEntry + { + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingString name; + + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingString signature; + + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingReplacementField replacement; + }; + + [NativeAssemblerStructContextDataProvider (typeof(JniRemappingIndexFieldTypeEntryContextDataProvider))] + sealed class JniRemappingIndexFieldTypeEntry + { + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingString name; + public uint field_count; + + [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value - populated during native code generation + public JniRemappingIndexFieldEntry fields; +#pragma warning restore CS0649 + + [NativeAssembler (Ignore = true)] + public string FieldsArraySymbolName; + + [NativeAssembler (Ignore = true)] + public List> TypeFields; + }; + [NativeAssemblerStructContextDataProvider (typeof(JniRemappingTypeReplacementEntryContextDataProvider))] sealed class JniRemappingTypeReplacementEntry { @@ -185,105 +328,232 @@ sealed class JniRemappingTypeReplacementEntry public string replacement; }; + sealed class GeneratedTables + { + public List> TypeReplacements; + public List> ReverseTypeReplacements; + public List> MethodIndexTypes; + public List> FieldIndexTypes; + } + List typeReplacementsInput; + List reverseTypeReplacementsInput; List methodReplacementsInput; + List fieldReplacementsInput; StructureInfo jniRemappingStringStructureInfo; StructureInfo jniRemappingReplacementMethodStructureInfo; + StructureInfo jniRemappingReplacementFieldStructureInfo; StructureInfo jniRemappingIndexMethodEntryStructureInfo; StructureInfo jniRemappingIndexTypeEntryStructureInfo; + StructureInfo jniRemappingIndexFieldEntryStructureInfo; + StructureInfo jniRemappingIndexFieldTypeEntryStructureInfo; StructureInfo jniRemappingTypeReplacementEntryStructureInfo; + public int ReplacementTypeCount { get; private set; } = 0; + public int ReverseTypeCount { get; private set; } = 0; public int ReplacementMethodIndexEntryCount { get; private set; } = 0; + public int ReplacementFieldIndexEntryCount { get; private set; } = 0; public JniRemappingAssemblyGenerator (TaskLoggingHelper log) : base (log) {} - public JniRemappingAssemblyGenerator (TaskLoggingHelper log, List typeReplacements, List methodReplacements) + public JniRemappingAssemblyGenerator (TaskLoggingHelper log, + List typeReplacements, + List reverseTypeReplacements, + List methodReplacements, + List fieldReplacements) : base (log) { this.typeReplacementsInput = typeReplacements ?? throw new ArgumentNullException (nameof (typeReplacements)); + this.reverseTypeReplacementsInput = reverseTypeReplacements ?? throw new ArgumentNullException (nameof (reverseTypeReplacements)); this.methodReplacementsInput = methodReplacements ?? throw new ArgumentNullException (nameof (methodReplacements)); + this.fieldReplacementsInput = fieldReplacements ?? throw new ArgumentNullException (nameof (fieldReplacements)); } - (List>? typeReplacements, List>? methodIndexTypes) Init () + /// + /// Orders UTF-8 encoded names exactly the way the native lookup's memcmp-based + /// comparison does, so the runtime can binary-search the emitted tables. + /// + internal static int CompareUtf8 (byte [] left, byte [] right) + { + int min = Math.Min (left.Length, right.Length); + for (int i = 0; i < min; i++) { + if (left [i] != right [i]) { + return left [i] < right [i] ? -1 : 1; + } + } + + if (left.Length == right.Length) { + return 0; + } + + return left.Length < right.Length ? -1 : 1; + } + + static byte [] Utf8 (string str) => String.IsNullOrEmpty (str) ? Array.Empty () : Encoding.UTF8.GetBytes (str); + + GeneratedTables Init () { if (typeReplacementsInput == null) { - return (null, null); + return null; } - var typeReplacements = new List> (); - foreach (JniRemappingTypeReplacement mtr in typeReplacementsInput) { + var ret = new GeneratedTables { + TypeReplacements = MakeTypeReplacements (typeReplacementsInput), + ReverseTypeReplacements = MakeTypeReplacements (reverseTypeReplacementsInput), + MethodIndexTypes = MakeMethodIndex (), + FieldIndexTypes = MakeFieldIndex (), + }; + + ReplacementTypeCount = ret.TypeReplacements.Count; + ReverseTypeCount = ret.ReverseTypeReplacements.Count; + ReplacementMethodIndexEntryCount = ret.MethodIndexTypes.Count; + ReplacementFieldIndexEntryCount = ret.FieldIndexTypes.Count; + + return ret; + } + + List> MakeTypeReplacements (List input) + { + var sorted = new List<(byte [] key, JniRemappingTypeReplacement replacement)> (input.Count); + foreach (JniRemappingTypeReplacement tr in input) { + sorted.Add ((Utf8 (tr.From), tr)); + } + sorted.Sort ((l, r) => CompareUtf8 (l.key, r.key)); + + var ret = new List> (sorted.Count); + foreach ((byte [] key, JniRemappingTypeReplacement tr) in sorted) { var entry = new JniRemappingTypeReplacementEntry { - name = MakeJniRemappingString (mtr.From), - replacement = mtr.To, + name = MakeJniRemappingString (tr.From, key), + replacement = tr.To, }; - typeReplacements.Add (new StructureInstance (jniRemappingTypeReplacementEntryStructureInfo, entry)); + ret.Add (new StructureInstance (jniRemappingTypeReplacementEntryStructureInfo, entry)); } - typeReplacements.Sort ((StructureInstance l, StructureInstance r) => l.Instance.name.str.CompareTo (r.Instance.name.str)); - var methodIndexTypes = new List> (); - var types = new Dictionary> (StringComparer.Ordinal); + return ret; + } + + List> MakeMethodIndex () + { + var types = new Dictionary methods)> (StringComparer.Ordinal); foreach (JniRemappingMethodReplacement mmr in methodReplacementsInput) { - if (!types.TryGetValue (mmr.SourceType, out StructureInstance typeEntry)) { - var entry = new JniRemappingIndexTypeEntry { - name = MakeJniRemappingString (mmr.SourceType), - MethodsArraySymbolName = MakeMethodsArrayName (mmr.SourceType), - TypeMethods = new List> (), + if (!types.TryGetValue (mmr.SourceType, out var typeEntry)) { + typeEntry = (Utf8 (mmr.SourceType), new List<(byte [], byte [], JniRemappingMethodReplacement)> ()); + types.Add (mmr.SourceType, typeEntry); + } + + typeEntry.methods.Add ((Utf8 (mmr.SourceMethod), Utf8 (mmr.SourceMethodSignature), mmr)); + } + + var sortedTypes = new List methods)>> (types); + sortedTypes.Sort ((l, r) => CompareUtf8 (l.Value.key, r.Value.key)); + + var ret = new List> (sortedTypes.Count); + foreach (var kvp in sortedTypes) { + var methods = kvp.Value.methods; + // Overloads share a name, so the native lookup binary-searches the name and then + // scans the equal-name run for a matching signature. Keep both keys in the sort. + methods.Sort ((l, r) => { + int cmp = CompareUtf8 (l.nameKey, r.nameKey); + return cmp != 0 ? cmp : CompareUtf8 (l.signatureKey, r.signatureKey); + }); + + var typeMethods = new List> (methods.Count); + foreach ((byte [] nameKey, byte [] signatureKey, JniRemappingMethodReplacement mmr) in methods) { + var method = new JniRemappingIndexMethodEntry { + name = MakeJniRemappingString (mmr.SourceMethod, nameKey), + signature = MakeJniRemappingString (mmr.SourceMethodSignature, signatureKey), + replacement = new JniRemappingReplacementMethod { + target_type = mmr.TargetType, + target_name = mmr.TargetMethod, + target_signature = mmr.TargetMethodSignature, + is_static = mmr.TargetIsStatic, + }, }; - typeEntry = new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, entry); - methodIndexTypes.Add (typeEntry); - types.Add (mmr.SourceType, typeEntry); + typeMethods.Add (new StructureInstance (jniRemappingIndexMethodEntryStructureInfo, method)); } - var method = new JniRemappingIndexMethodEntry { - name = MakeJniRemappingString (mmr.SourceMethod), - signature = MakeJniRemappingString (mmr.SourceMethodSignature), - replacement = new JniRemappingReplacementMethod { - target_type = mmr.TargetType, - target_name = mmr.TargetMethod, - is_static = mmr.TargetIsStatic, - }, + var entry = new JniRemappingIndexTypeEntry { + name = MakeJniRemappingString (kvp.Key, kvp.Value.key), + method_count = (uint)typeMethods.Count, + MethodsArraySymbolName = MakeMembersArrayName ("mm", kvp.Key), + TypeMethods = typeMethods, }; - typeEntry.Instance.TypeMethods.Add (new StructureInstance (jniRemappingIndexMethodEntryStructureInfo, method)); + ret.Add (new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, entry)); } - foreach (var kvp in types) { - kvp.Value.Instance.method_count = (uint)kvp.Value.Instance.TypeMethods.Count; - kvp.Value.Instance.TypeMethods.Sort ((StructureInstance l, StructureInstance r) => l.Instance.name.str.CompareTo (r.Instance.name.str)); - } + return ret; + } - methodIndexTypes.Sort ((StructureInstance l, StructureInstance r) => l.Instance.name.str.CompareTo (r.Instance.name.str)); - ReplacementMethodIndexEntryCount = methodIndexTypes.Count; + List> MakeFieldIndex () + { + var types = new Dictionary fields)> (StringComparer.Ordinal); - return (typeReplacements, methodIndexTypes); + foreach (JniRemappingFieldReplacement mfr in fieldReplacementsInput) { + if (!types.TryGetValue (mfr.SourceType, out var typeEntry)) { + typeEntry = (Utf8 (mfr.SourceType), new List<(byte [], byte [], JniRemappingFieldReplacement)> ()); + types.Add (mfr.SourceType, typeEntry); + } - string MakeMethodsArrayName (string typeName) - { - return $"mm_{typeName.Replace ('/', '_')}"; + typeEntry.fields.Add ((Utf8 (mfr.SourceField), Utf8 (mfr.SourceFieldSignature), mfr)); } - JniRemappingString MakeJniRemappingString (string str) - { - return new JniRemappingString { - length = GetLength (str), - str = str, - }; - } + var sortedTypes = new List fields)>> (types); + sortedTypes.Sort ((l, r) => CompareUtf8 (l.Value.key, r.Value.key)); + + var ret = new List> (sortedTypes.Count); + foreach (var kvp in sortedTypes) { + var fields = kvp.Value.fields; + fields.Sort ((l, r) => { + int cmp = CompareUtf8 (l.nameKey, r.nameKey); + return cmp != 0 ? cmp : CompareUtf8 (l.signatureKey, r.signatureKey); + }); + + var typeFields = new List> (fields.Count); + foreach ((byte [] nameKey, byte [] signatureKey, JniRemappingFieldReplacement mfr) in fields) { + var field = new JniRemappingIndexFieldEntry { + name = MakeJniRemappingString (mfr.SourceField, nameKey), + signature = MakeJniRemappingString (mfr.SourceFieldSignature, signatureKey), + replacement = new JniRemappingReplacementField { + target_type = mfr.TargetType, + target_name = mfr.TargetField, + target_signature = mfr.TargetFieldSignature, + }, + }; - uint GetLength (string str) - { - if (String.IsNullOrEmpty (str)) { - return 0; + typeFields.Add (new StructureInstance (jniRemappingIndexFieldEntryStructureInfo, field)); } - return (uint)Encoding.UTF8.GetBytes (str).Length; + var entry = new JniRemappingIndexFieldTypeEntry { + name = MakeJniRemappingString (kvp.Key, kvp.Value.key), + field_count = (uint)typeFields.Count, + FieldsArraySymbolName = MakeMembersArrayName ("mf", kvp.Key), + TypeFields = typeFields, + }; + + ret.Add (new StructureInstance (jniRemappingIndexFieldTypeEntryStructureInfo, entry)); } + + return ret; + } + + static string MakeMembersArrayName (string prefix, string typeName) + { + return $"{prefix}_{typeName.Replace ('/', '_')}"; + } + + static JniRemappingString MakeJniRemappingString (string str, byte [] utf8) + { + return new JniRemappingString { + length = (uint)utf8.Length, + str = str, + }; } protected override void Construct (LlvmIrModule module) @@ -291,12 +561,10 @@ protected override void Construct (LlvmIrModule module) module.DefaultStringGroup = "jremap"; MapStructures (module); - List>? typeReplacements; - List>? methodIndexTypes; - (typeReplacements, methodIndexTypes) = Init (); + GeneratedTables tables = Init (); - if (typeReplacements == null) { + if (tables == null) { module.AddGlobalVariable ( typeof(StructureInstance), TypeReplacementsVariableName, @@ -304,30 +572,66 @@ protected override void Construct (LlvmIrModule module) LlvmIrVariableOptions.GlobalConstant ); + module.AddGlobalVariable ( + typeof(StructureInstance), + ReverseTypeReplacementsVariableName, + new StructureInstance (jniRemappingTypeReplacementEntryStructureInfo, new JniRemappingTypeReplacementEntry ()) { IsZeroInitialized = true }, + LlvmIrVariableOptions.GlobalConstant + ); + module.AddGlobalVariable ( typeof(StructureInstance), MethodReplacementIndexVariableName, new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, new JniRemappingIndexTypeEntry ()) { IsZeroInitialized = true }, LlvmIrVariableOptions.GlobalConstant ); + + module.AddGlobalVariable ( + typeof(StructureInstance), + FieldReplacementIndexVariableName, + new StructureInstance (jniRemappingIndexFieldTypeEntryStructureInfo, new JniRemappingIndexFieldTypeEntry ()) { IsZeroInitialized = true }, + LlvmIrVariableOptions.GlobalConstant + ); + + AddCounts (module); return; } - module.AddGlobalVariable (TypeReplacementsVariableName, typeReplacements, LlvmIrVariableOptions.GlobalConstant); + module.AddGlobalVariable (TypeReplacementsVariableName, tables.TypeReplacements, LlvmIrVariableOptions.GlobalConstant); + module.AddGlobalVariable (ReverseTypeReplacementsVariableName, tables.ReverseTypeReplacements, LlvmIrVariableOptions.GlobalConstant); - foreach (StructureInstance entry in methodIndexTypes) { + foreach (StructureInstance entry in tables.MethodIndexTypes) { module.AddGlobalVariable (entry.Instance.MethodsArraySymbolName, entry.Instance.TypeMethods, LlvmIrVariableOptions.LocalConstant); } - module.AddGlobalVariable (MethodReplacementIndexVariableName, methodIndexTypes, LlvmIrVariableOptions.GlobalConstant); + module.AddGlobalVariable (MethodReplacementIndexVariableName, tables.MethodIndexTypes, LlvmIrVariableOptions.GlobalConstant); + + foreach (StructureInstance entry in tables.FieldIndexTypes) { + module.AddGlobalVariable (entry.Instance.FieldsArraySymbolName, entry.Instance.TypeFields, LlvmIrVariableOptions.LocalConstant); + } + + module.AddGlobalVariable (FieldReplacementIndexVariableName, tables.FieldIndexTypes, LlvmIrVariableOptions.GlobalConstant); + + AddCounts (module); + } + + void AddCounts (LlvmIrModule module) + { + module.AddGlobalVariable (TypeReplacementCountVariableName, (uint)ReplacementTypeCount); + module.AddGlobalVariable (ReverseTypeReplacementCountVariableName, (uint)ReverseTypeCount); + module.AddGlobalVariable (MethodReplacementIndexCountVariableName, (uint)ReplacementMethodIndexEntryCount); + module.AddGlobalVariable (FieldReplacementIndexCountVariableName, (uint)ReplacementFieldIndexEntryCount); } void MapStructures (LlvmIrModule module) { jniRemappingStringStructureInfo = module.MapStructure (); jniRemappingReplacementMethodStructureInfo = module.MapStructure (); + jniRemappingReplacementFieldStructureInfo = module.MapStructure (); jniRemappingIndexMethodEntryStructureInfo = module.MapStructure (); jniRemappingIndexTypeEntryStructureInfo = module.MapStructure (); + jniRemappingIndexFieldEntryStructureInfo = module.MapStructure (); + jniRemappingIndexFieldTypeEntryStructureInfo = module.MapStructure (); jniRemappingTypeReplacementEntryStructureInfo = module.MapStructure (); } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 09cf0e73152..34713cb595c 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -973,6 +973,8 @@ because xbuild doesn't support framework reference assemblies. <_PropertyCacheItems Include="AndroidEnableProfiledAot=$(AndroidEnableProfiledAot)" /> <_PropertyCacheItems Include="AndroidDexTool=$(AndroidDexTool)" /> <_PropertyCacheItems Include="AndroidLinkTool=$(AndroidLinkTool)" /> + <_PropertyCacheItems Include="AndroidEnableR8Obfuscation=$(AndroidEnableR8Obfuscation)" /> + <_PropertyCacheItems Include="AndroidR8ObfuscationMode=$(AndroidR8ObfuscationMode)" /> <_PropertyCacheItems Include="AndroidLinkResources=$(AndroidLinkResources)" /> <_PropertyCacheItems Include="AndroidBundleToolExtraArgs=$(AndroidBundleToolExtraArgs)" /> <_PropertyCacheItems Include="AndroidKeyStore=$(AndroidKeyStore)" /> @@ -1709,8 +1711,11 @@ because xbuild doesn't support framework reference assemblies. + <_AndroidGenerateR8JniRemappingDependsOn + Condition=" '$(AndroidTypeMapImplementation)' == 'trimmable' ">_AndroidGenerateR8JniRemapping <_GenerateAndroidRemapNativeCodeDependsOn> _ConvertAndroidMamMappingFileToXml; + $(_AndroidGenerateR8JniRemappingDependsOn); _CollectAndroidRemapMembers; _PrepareAndroidRemapNativeAssemblySources; _GetGeneratePackageManagerJavaInputs @@ -1719,7 +1724,7 @@ because xbuild doesn't support framework reference assemblies. + <_Aapt2ProguardRules Condition=" '$(AndroidLinkTool)' != '' ">$(IntermediateOutputPath)aapt_rules.txt <_CreateBaseApkInputs> $(_CreateBaseApkInputs); @(_AndroidMSBuildAllProjects); @@ -1881,7 +1887,7 @@ because xbuild doesn't support framework reference assemblies. - + @@ -1945,6 +1951,12 @@ because xbuild doesn't support framework reference assemblies. + + + + + + @@ -1993,6 +2005,7 @@ because xbuild doesn't support framework reference assemblies. _GetLibraryImports; _SetProguardMappingFileProperty; _CalculateProguardConfigurationFiles; + _AndroidPrepareR8JniCompileToDalvikInputs; <_CompileToDalvikInputs> @(_AndroidMSBuildAllProjects) @@ -2014,7 +2027,28 @@ because xbuild doesn't support framework reference assemblies. $(OutputPath)mapping.txt + <_AndroidR8ProguardMappingFileOutput>$(AndroidProguardMappingFile) + + <_AndroidR8ProguardMappingFileOutput Condition=" '$(_AndroidR8RuntimeRemappingEnabled)' == 'true' And '$(_AndroidR8ProguardMappingFileOutput)' == '' ">$(IntermediateOutputPath)r8-jni-final-mapping.txt + + + + + + <_CompileToDalvikInputs> + $(_CompileToDalvikInputs) + ;$(_AndroidR8JniSeedMapping) + ;$(_AndroidR8JniManifestProguardConfiguration) + ;$(_ProguardProjectConfiguration) + ;@(ProguardConfiguration) + ;$(AndroidR8JarPath) + + + + @@ -2027,6 +2061,8 @@ because xbuild doesn't support framework reference assemblies. <_ProguardConfiguration Include="$(IntermediateOutputPath)proguard\proguard_xamarin.cfg" Condition=" '$(AndroidLinkTool)' != '' " /> <_ProguardConfiguration Include="$(_ProguardProjectConfiguration)" Condition=" '$(AndroidLinkTool)' != '' " /> <_ProguardConfiguration Include="$(IntermediateOutputPath)proguard\proguard_project_primary.cfg" Condition=" '$(AndroidLinkTool)' != '' " /> + <_ProguardConfiguration Include="$(_AndroidR8JniManifestProguardConfiguration)" + Condition=" '$(_AndroidR8RuntimeRemappingEnabled)' == 'true' and Exists('$(_AndroidR8JniManifestProguardConfiguration)') " /> <_ProguardConfiguration Include="@(ProguardConfiguration)" /> @@ -3088,6 +3124,43 @@ because xbuild doesn't support framework reference assemblies. Text="Invalid value for AndroidTypeMapImplementation: '$(AndroidTypeMapImplementation)'. Valid values are: llvm-ir, trimmable." /> + + false + runtime-remapping + <_AndroidR8RuntimeRemappingEnabled>false + <_AndroidR8RuntimeRemappingEnabled + Condition=" '$(AndroidApplication)' == 'true' and '$(AndroidEnableR8Obfuscation)' == 'true' and '$(AndroidR8ObfuscationMode)' == 'runtime-remapping' ">true + + + + + + + + + + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets index bbf73eb8e77..3001b728242 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets @@ -77,7 +77,9 @@ Copyright (C) 2018 Xamarin. All rights reserved. ProguardCommonXamarinConfiguration="$(IntermediateOutputPath)proguard\proguard_xamarin.cfg" ProguardGeneratedReferenceConfiguration="$(_ProguardProjectConfiguration)" ProguardGeneratedApplicationConfiguration="$(IntermediateOutputPath)proguard\proguard_project_primary.cfg" - ProguardMappingFileOutput="$(AndroidProguardMappingFile)" + ProguardMappingFileOutput="$(_AndroidR8ProguardMappingFileOutput)" + ProguardMappingFileInput="$(_AndroidR8JniSeedMapping)" + EnableObfuscation="$(_AndroidR8RuntimeRemappingEnabled)" BuildMetadataFileOutput="$(_AndroidR8BuildMetadataFile)" ProguardConfigurationFiles="@(_ProguardConfiguration)" UseTrimmableNativeAotProguardConfiguration="$(_UseTrimmableNativeAotProguardConfiguration)" @@ -115,7 +117,7 @@ Copyright (C) 2018 Xamarin. All rights reserved. - + diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index a7d12ed8ab8..fe3b7883c64 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -460,7 +461,7 @@ void Host::Java_mono_android_Runtime_initInternal ( init.packageNamingPolicy = static_cast(application_config.package_naming_policy); init.boundExceptionType = 0; // System init.jniAddNativeMethodRegistrationAttributePresent = application_config.jni_add_native_method_registration_attribute_present ? 1 : 0; - init.jniRemappingInUse = application_config.jni_remapping_replacement_type_count > 0 || application_config.jni_remapping_replacement_method_index_entry_count > 0; + init.jniRemappingInUse = JniRemapping::is_in_use (); init.marshalMethodsEnabled = application_config.marshal_methods_enabled; // GC threshold is 90% of the max GREF count diff --git a/src/native/clr/host/internal-pinvokes-clr.cc b/src/native/clr/host/internal-pinvokes-clr.cc index 844f9b748f0..c6c1f27b09d 100644 --- a/src/native/clr/host/internal-pinvokes-clr.cc +++ b/src/native/clr/host/internal-pinvokes-clr.cc @@ -5,7 +5,6 @@ #include #include #include -#include using namespace xamarin::android; @@ -27,18 +26,6 @@ bool clr_typemap_java_to_managed (const char *java_type_name, char const** assem return TypeMapper::java_to_managed (java_type_name, assembly_name, managed_type_token_id); } -const char* -_monodroid_lookup_replacement_type (const char *jniSimpleReference) -{ - return JniRemapping::lookup_replacement_type (jniSimpleReference); -} - -const JniRemappingReplacementMethod* -_monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) -{ - return JniRemapping::lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature); -} - managed_timing_sequence* monodroid_timing_start (const char *message) { // Technically a reference here is against the idea of shared pointers, but diff --git a/src/native/clr/host/internal-pinvokes-shared.cc b/src/native/clr/host/internal-pinvokes-shared.cc index 18bffb5812e..0203e7fbb53 100644 --- a/src/native/clr/host/internal-pinvokes-shared.cc +++ b/src/native/clr/host/internal-pinvokes-shared.cc @@ -9,6 +9,30 @@ using namespace xamarin::android; +const char* +_monodroid_lookup_replacement_type (const char *jniSimpleReference) +{ + return JniRemapping::lookup_replacement_type (jniSimpleReference); +} + +const char* +_monodroid_lookup_reverse_type (const char *jniSimpleReference) +{ + return JniRemapping::lookup_reverse_type (jniSimpleReference); +} + +const JniRemappingReplacementMethod* +_monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) +{ + return JniRemapping::lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature); +} + +const JniRemappingReplacementField* +_monodroid_lookup_replacement_field_info (const char *jniSourceType, const char *jniFieldName, const char *jniFieldSignature) +{ + return JniRemapping::lookup_replacement_field_info (jniSourceType, jniFieldName, jniFieldSignature); +} + int _monodroid_gref_get () noexcept { return OSBridge::get_gc_gref_count (); diff --git a/src/native/clr/include/runtime-base/internal-pinvokes.hh b/src/native/clr/include/runtime-base/internal-pinvokes.hh index a5408b45046..bb4a2cd73f7 100644 --- a/src/native/clr/include/runtime-base/internal-pinvokes.hh +++ b/src/native/clr/include/runtime-base/internal-pinvokes.hh @@ -24,7 +24,9 @@ extern "C" { char* monodroid_TypeManager_get_java_class_name (jclass klass) noexcept; void monodroid_free (void *ptr) noexcept; const char* _monodroid_lookup_replacement_type (const char *jniSimpleReference); + const char* _monodroid_lookup_reverse_type (const char *jniSimpleReference); const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature); + const JniRemappingReplacementField* _monodroid_lookup_replacement_field_info (const char *jniSourceType, const char *jniFieldName, const char *jniFieldSignature); xamarin::android::managed_timing_sequence* monodroid_timing_start (const char *message); void monodroid_timing_stop (xamarin::android::managed_timing_sequence *sequence, const char *message); diff --git a/src/native/clr/include/runtime-base/jni-remapping.hh b/src/native/clr/include/runtime-base/jni-remapping.hh index f7b421b43cb..e6683282443 100644 --- a/src/native/clr/include/runtime-base/jni-remapping.hh +++ b/src/native/clr/include/runtime-base/jni-remapping.hh @@ -1,17 +1,30 @@ #pragma once -#include "xamarin-app.hh" +struct JniRemappingReplacementMethod; +struct JniRemappingReplacementField; namespace xamarin::android { + // + // Lookups over the JNI remapping tables emitted by + // `src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs`. + // + // The tables are sorted by their UTF-8 name so every lookup can binary-search them: R8 produces + // one entry per renamed type and member, so the tables are far too large for linear scans. + // class JniRemapping final { public: + // `true` when the application ships any remapping data at all. + static auto is_in_use () noexcept -> bool; + + // Original (managed) JNI type name -> the name the type has in the packaged application. static auto lookup_replacement_type (const char *jniSimpleReference) noexcept -> const char*; - static auto lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept -> const JniRemappingReplacementMethod*; - private: - [[gnu::nonnull (2)]] - static auto equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept -> bool; + // The name a type has in the packaged application -> original (managed) JNI type name. + static auto lookup_reverse_type (const char *jniSimpleReference) noexcept -> const char*; + + static auto lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept -> const JniRemappingReplacementMethod*; + static auto lookup_replacement_field_info (const char *jniSourceType, const char *jniFieldName, const char *jniFieldSignature) noexcept -> const JniRemappingReplacementField*; }; } diff --git a/src/native/clr/include/xamarin-app.hh b/src/native/clr/include/xamarin-app.hh index 1fdb13b78a4..6eccfe9f877 100644 --- a/src/native/clr/include/xamarin-app.hh +++ b/src/native/clr/include/xamarin-app.hh @@ -220,6 +220,8 @@ struct ApplicationConfig uint32_t android_runtime_jnienv_class_token; uint32_t jnienv_initialize_method_token; uint32_t jnienv_registerjninatives_method_token; + // Unused by the CoreCLR runtime, which reads the table sizes from the `jni_remapping_*_count` + // symbols instead so that the lookup code is shared with NativeAOT. Kept for layout stability. uint32_t jni_remapping_replacement_type_count; uint32_t jni_remapping_replacement_method_index_entry_count; const char *android_package_name; @@ -246,8 +248,9 @@ struct JniRemappingReplacementMethod { const char *target_type; const char *target_name; - // const char *target_signature; - // const int32_t param_count; + // JNI descriptor to use on the target type, or `nullptr` when the source signature is used + // unchanged (remapping inputs which predate `target-method-signature`). + const char *target_signature; const bool is_static; }; @@ -265,6 +268,27 @@ struct JniRemappingIndexTypeEntry const JniRemappingIndexMethodEntry *methods; }; +struct JniRemappingReplacementField +{ + const char *target_type; + const char *target_name; + const char *target_signature; +}; + +struct JniRemappingIndexFieldEntry +{ + const JniRemappingString name; + const JniRemappingString signature; + const JniRemappingReplacementField replacement; +}; + +struct JniRemappingIndexFieldTypeEntry +{ + const JniRemappingString name; + const uint32_t field_count; + const JniRemappingIndexFieldEntry *fields; +}; + struct JniRemappingTypeReplacementEntry { const JniRemappingString name; @@ -278,8 +302,19 @@ struct AppEnvironmentVariable }; extern "C" { + // MUST match src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs + // + // The table sizes live in dedicated symbols rather than in `ApplicationConfig` so that the + // NativeAOT build, which has no application config, can share the same lookup implementation. [[gnu::visibility("default")]] extern const JniRemappingIndexTypeEntry jni_remapping_method_replacement_index[]; + [[gnu::visibility("default")]] extern const JniRemappingIndexFieldTypeEntry jni_remapping_field_replacement_index[]; [[gnu::visibility("default")]] extern const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[]; + [[gnu::visibility("default")]] extern const JniRemappingTypeReplacementEntry jni_remapping_reverse_type_replacements[]; + + [[gnu::visibility("default")]] extern const uint32_t jni_remapping_type_replacement_count; + [[gnu::visibility("default")]] extern const uint32_t jni_remapping_reverse_type_replacement_count; + [[gnu::visibility("default")]] extern const uint32_t jni_remapping_method_replacement_index_count; + [[gnu::visibility("default")]] extern const uint32_t jni_remapping_field_replacement_index_count; [[gnu::visibility("default")]] extern const uint64_t format_tag; diff --git a/src/native/clr/pinvoke-override/precompiled.cc b/src/native/clr/pinvoke-override/precompiled.cc index ec6ae2cb522..97062f29c6c 100644 --- a/src/native/clr/pinvoke-override/precompiled.cc +++ b/src/native/clr/pinvoke-override/precompiled.cc @@ -61,6 +61,12 @@ namespace { if (entrypoint_name == "_monodroid_lookup_replacement_method_info"sv) { return reinterpret_cast (&_monodroid_lookup_replacement_method_info); } + if (entrypoint_name == "_monodroid_lookup_reverse_type"sv) { + return reinterpret_cast (&_monodroid_lookup_reverse_type); + } + if (entrypoint_name == "_monodroid_lookup_replacement_field_info"sv) { + return reinterpret_cast (&_monodroid_lookup_replacement_field_info); + } if (entrypoint_name == "_monodroid_lref_log_delete"sv) { return reinterpret_cast (&_monodroid_lref_log_delete); } diff --git a/src/native/clr/runtime-base/jni-remapping.cc b/src/native/clr/runtime-base/jni-remapping.cc index 715e5cb662e..bf5d4a98dfc 100644 --- a/src/native/clr/runtime-base/jni-remapping.cc +++ b/src/native/clr/runtime-base/jni-remapping.cc @@ -1,95 +1,227 @@ +#include #include -#include #include #include "xamarin-app.hh" using namespace xamarin::android; -[[gnu::always_inline]] -auto JniRemapping::equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept -> bool -{ - if (left.length != static_cast(right_len) || left.str[0] != *right) { - return false; +namespace { + // + // `memcmp` ordering over the UTF-8 bytes of the name. `JniRemappingAssemblyGenerator` sorts the + // tables with exactly the same ordering, which is what makes the binary searches below valid. + // + [[gnu::always_inline]] + auto compare (JniRemappingString const& left, const char *right, size_t right_len) noexcept -> int + { + size_t left_len = static_cast(left.length); + size_t min_len = std::min (left_len, right_len); + + if (min_len > 0uz) { + int ret = memcmp (left.str, right, min_len); + if (ret != 0) { + return ret; + } + } + + if (left_len == right_len) { + return 0; + } + + return left_len < right_len ? -1 : 1; } - if (memcmp (left.str, right, right_len) == 0) { - return true; + template + [[gnu::always_inline]] + auto lower_bound_by_name (const TEntry *entries, size_t count, const char *name, size_t name_len) noexcept -> size_t + { + size_t lo = 0uz; + size_t hi = count; + + while (lo < hi) { + size_t mid = lo + ((hi - lo) / 2uz); + if (compare (entries[mid].name, name, name_len) < 0) { + lo = mid + 1uz; + } else { + hi = mid; + } + } + + return lo; } - return false; -} + // Returns the half-open range of entries whose name equals `name`. Overloads share a name, so + // callers scan the (short) returned range instead of searching the whole table. + template + auto equal_name_range (const TEntry *entries, size_t count, const char *name, size_t name_len, size_t &first, size_t &last) noexcept -> bool + { + first = lower_bound_by_name (entries, count, name, name_len); + last = first; -auto JniRemapping::lookup_replacement_type (const char *jniSimpleReference) noexcept -> const char* -{ - if (application_config.jni_remapping_replacement_type_count == 0 || jniSimpleReference == nullptr || *jniSimpleReference == '\0') { - return nullptr; + while (last < count && compare (entries[last].name, name, name_len) == 0) { + last++; + } + + return first != last; } - size_t ref_len = strlen (jniSimpleReference); - for (size_t i = 0uz; i < application_config.jni_remapping_replacement_type_count; i++) { - JniRemappingTypeReplacementEntry const& entry = jni_remapping_type_replacements[i]; + auto lookup_type (const JniRemappingTypeReplacementEntry *entries, uint32_t count, const char *jniSimpleReference) noexcept -> const char* + { + if (count == 0 || jniSimpleReference == nullptr || *jniSimpleReference == '\0') { + return nullptr; + } + + size_t ref_len = strlen (jniSimpleReference); + size_t idx = lower_bound_by_name (entries, static_cast(count), jniSimpleReference, ref_len); - if (equal (entry.name, jniSimpleReference, ref_len)) { - return entry.replacement; + if (idx >= static_cast(count) || compare (entries[idx].name, jniSimpleReference, ref_len) != 0) { + return nullptr; } + + return entries[idx].replacement; } +} - return nullptr; +auto JniRemapping::is_in_use () noexcept -> bool +{ + return jni_remapping_type_replacement_count > 0 || + jni_remapping_reverse_type_replacement_count > 0 || + jni_remapping_method_replacement_index_count > 0 || + jni_remapping_field_replacement_index_count > 0; +} + +auto JniRemapping::lookup_replacement_type (const char *jniSimpleReference) noexcept -> const char* +{ + return lookup_type (jni_remapping_type_replacements, jni_remapping_type_replacement_count, jniSimpleReference); +} + +auto JniRemapping::lookup_reverse_type (const char *jniSimpleReference) noexcept -> const char* +{ + return lookup_type (jni_remapping_reverse_type_replacements, jni_remapping_reverse_type_replacement_count, jniSimpleReference); } auto JniRemapping::lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept -> const JniRemappingReplacementMethod* { - if (application_config.jni_remapping_replacement_method_index_entry_count == 0 || + if (jni_remapping_method_replacement_index_count == 0 || jniSourceType == nullptr || *jniSourceType == '\0' || jniMethodName == nullptr || *jniMethodName == '\0') { return nullptr; } size_t source_type_len = strlen (jniSourceType); - - const JniRemappingIndexTypeEntry *type = nullptr; - for (size_t i = 0uz; i < application_config.jni_remapping_replacement_method_index_entry_count; i++) { - JniRemappingIndexTypeEntry const& entry = jni_remapping_method_replacement_index[i]; - - if (!equal (entry.name, jniSourceType, source_type_len)) { - continue; - } - - type = &jni_remapping_method_replacement_index[i]; - break; + size_t type_idx = lower_bound_by_name ( + jni_remapping_method_replacement_index, + static_cast(jni_remapping_method_replacement_index_count), + jniSourceType, + source_type_len + ); + + if (type_idx >= static_cast(jni_remapping_method_replacement_index_count) || + compare (jni_remapping_method_replacement_index[type_idx].name, jniSourceType, source_type_len) != 0) { + return nullptr; } - if (type == nullptr || type->method_count == 0 || type->methods == nullptr) { + JniRemappingIndexTypeEntry const& type = jni_remapping_method_replacement_index[type_idx]; + if (type.method_count == 0 || type.methods == nullptr) { return nullptr; } size_t method_name_len = strlen (jniMethodName); - size_t signature_len = jniMethodSignature == nullptr ? 0uz : strlen (jniMethodSignature); + size_t first, last; + if (!equal_name_range (type.methods, static_cast(type.method_count), jniMethodName, method_name_len, first, last)) { + return nullptr; + } - for (size_t i = 0uz; i < type->method_count; i++) { - JniRemappingIndexMethodEntry const& entry = type->methods[i]; + size_t signature_len = jniMethodSignature == nullptr ? 0uz : strlen (jniMethodSignature); - if (!equal (entry.name, jniMethodName, method_name_len)) { - continue; + // Most specific first: the full descriptor... + if (signature_len > 0uz) { + for (size_t i = first; i < last; i++) { + JniRemappingIndexMethodEntry const& entry = type.methods[i]; + if (entry.signature.length != 0 && compare (entry.signature, jniMethodSignature, signature_len) == 0) { + return &entry.replacement; + } } - if (entry.signature.length == 0 || equal (entry.signature, jniMethodSignature, signature_len)) { - return &type->methods[i].replacement; + // ...then the parameter list only, e.g. an entry of `(I)` matching a call of `(I)V`. This + // is how the Intune/MAM mapping describes methods whose return type it does not pin. + const char *sig_end = jniMethodSignature + signature_len; + while (sig_end != jniMethodSignature && *sig_end != ')') { + sig_end--; } - const char *sig_end = jniMethodSignature + signature_len; if (*sig_end == ')') { - continue; + size_t prefix_len = static_cast(sig_end - jniMethodSignature) + 1uz; + if (prefix_len != signature_len) { + for (size_t i = first; i < last; i++) { + JniRemappingIndexMethodEntry const& entry = type.methods[i]; + if (entry.signature.length != 0 && compare (entry.signature, jniMethodSignature, prefix_len) == 0) { + return &entry.replacement; + } + } + } } + } - while (sig_end != jniMethodSignature && *sig_end != ')') { - sig_end--; + // ...and finally an entry with no signature at all, which matches every overload. + for (size_t i = first; i < last; i++) { + JniRemappingIndexMethodEntry const& entry = type.methods[i]; + if (entry.signature.length == 0) { + return &entry.replacement; + } + } + + return nullptr; +} + +auto JniRemapping::lookup_replacement_field_info (const char *jniSourceType, const char *jniFieldName, const char *jniFieldSignature) noexcept -> const JniRemappingReplacementField* +{ + if (jni_remapping_field_replacement_index_count == 0 || + jniSourceType == nullptr || *jniSourceType == '\0' || + jniFieldName == nullptr || *jniFieldName == '\0') { + return nullptr; + } + + size_t source_type_len = strlen (jniSourceType); + size_t type_idx = lower_bound_by_name ( + jni_remapping_field_replacement_index, + static_cast(jni_remapping_field_replacement_index_count), + jniSourceType, + source_type_len + ); + + if (type_idx >= static_cast(jni_remapping_field_replacement_index_count) || + compare (jni_remapping_field_replacement_index[type_idx].name, jniSourceType, source_type_len) != 0) { + return nullptr; + } + + JniRemappingIndexFieldTypeEntry const& type = jni_remapping_field_replacement_index[type_idx]; + if (type.field_count == 0 || type.fields == nullptr) { + return nullptr; + } + + size_t field_name_len = strlen (jniFieldName); + size_t first, last; + if (!equal_name_range (type.fields, static_cast(type.field_count), jniFieldName, field_name_len, first, last)) { + return nullptr; + } + + size_t signature_len = jniFieldSignature == nullptr ? 0uz : strlen (jniFieldSignature); + + if (signature_len > 0uz) { + for (size_t i = first; i < last; i++) { + JniRemappingIndexFieldEntry const& entry = type.fields[i]; + if (entry.signature.length != 0 && compare (entry.signature, jniFieldSignature, signature_len) == 0) { + return &entry.replacement; + } } + } - if (equal (entry.signature, jniMethodSignature, static_cast(sig_end - jniMethodSignature) + 1uz)) { - return &type->methods[i].replacement; + for (size_t i = first; i < last; i++) { + JniRemappingIndexFieldEntry const& entry = type.fields[i]; + if (entry.signature.length == 0) { + return &entry.replacement; } } diff --git a/src/native/clr/xamarin-app-stub/application_dso_stub.cc b/src/native/clr/xamarin-app-stub/application_dso_stub.cc index df7bbad9ffb..7c3800b9050 100644 --- a/src/native/clr/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/clr/xamarin-app-stub/application_dso_stub.cc @@ -153,6 +153,7 @@ static const JniRemappingIndexMethodEntry some_java_type_one_methods[] = { .replacement = { .target_type = "some/java/target_type_one", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = false, } }, @@ -173,6 +174,7 @@ static const JniRemappingIndexMethodEntry some_java_type_two_methods[] = { .replacement = { .target_type = "some/java/target_type_two", .target_name = "new_method_name", + .target_signature = "(IILanother/content/Intent;)V", .is_static = true, } }, @@ -216,6 +218,52 @@ const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[] = { }, }; +static const JniRemappingIndexFieldEntry some_java_type_one_fields[] = { + { + .name = { + .length = 14, + .str = "old_field_name", + }, + + .signature = { + .length = 16, + .str = "Lsome/java/type;", + }, + + .replacement = { + .target_type = "some/java/target_type_one", + .target_name = "new_field_name", + .target_signature = "Lanother/java/type;", + } + }, +}; + +const JniRemappingIndexFieldTypeEntry jni_remapping_field_replacement_index[] = { + { + .name = { + .length = 18, + .str = "some/java/type_one", + }, + .field_count = 1, + .fields = some_java_type_one_fields, + }, +}; + +const JniRemappingTypeReplacementEntry jni_remapping_reverse_type_replacements[] = { + { + .name = { + .length = 17, + .str = "another/java/type", + }, + .replacement = "some/java/type", + }, +}; + +const uint32_t jni_remapping_type_replacement_count = 2; +const uint32_t jni_remapping_reverse_type_replacement_count = 1; +const uint32_t jni_remapping_method_replacement_index_count = 2; +const uint32_t jni_remapping_field_replacement_index_count = 1; + const char *init_runtime_property_names[] = { "HOST_RUNTIME_CONTRACT", "RUNTIME_IDENTIFIER", diff --git a/src/native/mono/monodroid/internal-pinvokes.cc b/src/native/mono/monodroid/internal-pinvokes.cc index e7f580e8e41..38cc884cf7d 100644 --- a/src/native/mono/monodroid/internal-pinvokes.cc +++ b/src/native/mono/monodroid/internal-pinvokes.cc @@ -289,3 +289,22 @@ _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char return JniRemapping::lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature); } +// +// Reverse type and field remapping are only produced for the CoreCLR and NativeAOT runtimes; the +// entry points exist so that managed code shared with them resolves on MonoVM as well. +// +const char* +_monodroid_lookup_reverse_type ([[maybe_unused]] const char *jniSimpleReference) +{ + return nullptr; +} + +const JniRemappingReplacementField* +_monodroid_lookup_replacement_field_info ( + [[maybe_unused]] const char *jniSourceType, + [[maybe_unused]] const char *jniFieldName, + [[maybe_unused]] const char *jniFieldSignature) +{ + return nullptr; +} + diff --git a/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc b/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc index dc6570c2a23..e0866b30166 100644 --- a/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc +++ b/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc @@ -53,6 +53,8 @@ const std::vector internal_pinvoke_names = { "monodroid_log", "_monodroid_lookup_replacement_type", "_monodroid_lookup_replacement_method_info", + "_monodroid_lookup_reverse_type", + "_monodroid_lookup_replacement_field_info", "_monodroid_lref_log_delete", "_monodroid_lref_log_new", "_monodroid_max_gref_get", diff --git a/src/native/mono/pinvoke-override/pinvoke-tables.include b/src/native/mono/pinvoke-override/pinvoke-tables.include index e26bde46029..c450320a56f 100644 --- a/src/native/mono/pinvoke-override/pinvoke-tables.include +++ b/src/native/mono/pinvoke-override/pinvoke-tables.include @@ -11,7 +11,7 @@ namespace { #if INTPTR_MAX == INT64_MAX //64-bit internal p/invoke table - std::array internal_pinvokes {{ + std::array internal_pinvokes {{ {0x2b3b0ca1d14076da, "monodroid_get_dylib", reinterpret_cast(&monodroid_get_dylib)}, {0x37307e5fddf709dc, "_monodroid_weak_gref_dec", reinterpret_cast(&_monodroid_weak_gref_dec)}, {0x3b2467e7eadd4a6a, "_monodroid_lref_log_new", reinterpret_cast(&_monodroid_lref_log_new)}, @@ -25,6 +25,7 @@ namespace { {0x70fc9bab8d56666d, "create_public_directory", reinterpret_cast(&create_public_directory)}, {0x9099a4b95e3c3a89, "_monodroid_lref_log_delete", reinterpret_cast(&_monodroid_lref_log_delete)}, {0x958cdb6fd9d1b67b, "monodroid_dylib_mono_new", reinterpret_cast(&monodroid_dylib_mono_new)}, + {0x9b2cb47e6be7df2c, "_monodroid_lookup_replacement_field_info", reinterpret_cast(&_monodroid_lookup_replacement_field_info)}, {0x9d2b3233c41789df, "_monodroid_weak_gref_inc", reinterpret_cast(&_monodroid_weak_gref_inc)}, {0xa6ec846592d99536, "_monodroid_weak_gref_delete", reinterpret_cast(&_monodroid_weak_gref_delete)}, {0xa7f58f3ee428cc6b, "_monodroid_gref_log_delete", reinterpret_cast(&_monodroid_gref_log_delete)}, @@ -45,6 +46,7 @@ namespace { {0xe27b9849b7e982cb, "_monodroid_max_gref_get", reinterpret_cast(&_monodroid_max_gref_get)}, {0xe78f1161604ae672, "send_uninterrupted", reinterpret_cast(&send_uninterrupted)}, {0xe86307aac9a2631a, "_monodroid_weak_gref_new", reinterpret_cast(&_monodroid_weak_gref_new)}, + {0xeb225667f99934ef, "_monodroid_lookup_reverse_type", reinterpret_cast(&_monodroid_lookup_reverse_type)}, {0xebc2c68e10075cc9, "monodroid_fopen", reinterpret_cast(&monodroid_fopen)}, {0xf3048baf83034541, "_monodroid_gc_wait_for_bridge_processing", reinterpret_cast(&_monodroid_gc_wait_for_bridge_processing)}, {0xf41c48df6f9be476, "monodroid_free", reinterpret_cast(&monodroid_free)}, @@ -576,7 +578,7 @@ constexpr hash_t system_security_cryptography_native_android_library_hash = 0x18 constexpr hash_t system_globalization_native_library_hash = 0x28b5c8fca080abd5; #else //32-bit internal p/invoke table - std::array internal_pinvokes {{ + std::array internal_pinvokes {{ {0xb7a486a, "monodroid_TypeManager_get_java_class_name", reinterpret_cast(&monodroid_TypeManager_get_java_class_name)}, {0xf562bd9, "monodroid_embedded_assemblies_set_assemblies_prefix", reinterpret_cast(&monodroid_embedded_assemblies_set_assemblies_prefix)}, {0x1bef8dce, "_monodroid_gref_inc", reinterpret_cast(&_monodroid_gref_inc)}, @@ -585,9 +587,11 @@ constexpr hash_t system_globalization_native_library_hash = 0x28b5c8fca080abd5; {0x3227d81a, "monodroid_timing_start", reinterpret_cast(&monodroid_timing_start)}, {0x333d4835, "_monodroid_lookup_replacement_method_info", reinterpret_cast(&_monodroid_lookup_replacement_method_info)}, {0x395808e5, "monodroid_dylib_mono_free", reinterpret_cast(&monodroid_dylib_mono_free)}, + {0x4249d3e9, "_monodroid_lookup_replacement_field_info", reinterpret_cast(&_monodroid_lookup_replacement_field_info)}, {0x42b41fe4, "send_uninterrupted", reinterpret_cast(&send_uninterrupted)}, {0x4b58e0da, "monodroid_get_dylib", reinterpret_cast(&monodroid_get_dylib)}, {0x501ebdc2, "monodroid_dylib_mono_init", reinterpret_cast(&monodroid_dylib_mono_init)}, + {0x576399b2, "_monodroid_lookup_reverse_type", reinterpret_cast(&_monodroid_lookup_reverse_type)}, {0x7c94dbf5, "monodroid_fopen", reinterpret_cast(&monodroid_fopen)}, {0x8f6837ec, "monodroid_strdup_printf", reinterpret_cast(&monodroid_strdup_printf)}, {0x9070e02c, "_monodroid_lref_log_delete", reinterpret_cast(&_monodroid_lref_log_delete)}, @@ -1141,6 +1145,6 @@ constexpr hash_t system_security_cryptography_native_android_library_hash = 0x93 constexpr hash_t system_globalization_native_library_hash = 0xa66f1e5a; #endif -constexpr size_t internal_pinvokes_count = 39; +constexpr size_t internal_pinvokes_count = 41; constexpr size_t dotnet_pinvokes_count = 510; } // end of anonymous namespace diff --git a/src/native/mono/runtime-base/internal-pinvokes.hh b/src/native/mono/runtime-base/internal-pinvokes.hh index bff70b0e9fa..171f003a2f6 100644 --- a/src/native/mono/runtime-base/internal-pinvokes.hh +++ b/src/native/mono/runtime-base/internal-pinvokes.hh @@ -46,4 +46,6 @@ int monodroid_dylib_mono_init (void *mono_imports, [[maybe_unused]] const char * void* monodroid_get_dylib (); const char* _monodroid_lookup_replacement_type (const char *jniSimpleReference); const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature); +const char* _monodroid_lookup_reverse_type (const char *jniSimpleReference); +const JniRemappingReplacementField* _monodroid_lookup_replacement_field_info (const char *jniSourceType, const char *jniFieldName, const char *jniFieldSignature); void _monodroid_detect_cpu_and_architecture (unsigned short *built_for_cpu, unsigned short *running_on_cpu, unsigned char *is64bit); diff --git a/src/native/mono/xamarin-app-stub/application_dso_stub.cc b/src/native/mono/xamarin-app-stub/application_dso_stub.cc index 6ed48fac62c..f86d53ab622 100644 --- a/src/native/mono/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/mono/xamarin-app-stub/application_dso_stub.cc @@ -248,6 +248,7 @@ static const JniRemappingIndexMethodEntry some_java_type_one_methods[] = { .replacement = { .target_type = "some/java/target_type_one", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = false, } }, @@ -268,6 +269,7 @@ static const JniRemappingIndexMethodEntry some_java_type_two_methods[] = { .replacement = { .target_type = "some/java/target_type_two", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = true, } }, diff --git a/src/native/mono/xamarin-app-stub/xamarin-app.hh b/src/native/mono/xamarin-app-stub/xamarin-app.hh index 6504ff88667..7edc2878797 100644 --- a/src/native/mono/xamarin-app-stub/xamarin-app.hh +++ b/src/native/mono/xamarin-app-stub/xamarin-app.hh @@ -285,8 +285,10 @@ struct JniRemappingReplacementMethod { const char *target_type; const char *target_name; - // const char *target_signature; - // const int32_t param_count; + // JNI descriptor to use on the target type, or `nullptr` when the source signature is used + // unchanged. MonoVM does not consume it, but the field must be present because the remapping + // tables are generated once and shared by every runtime. + const char *target_signature; const bool is_static; }; @@ -304,6 +306,13 @@ struct JniRemappingIndexTypeEntry const JniRemappingIndexMethodEntry *methods; }; +struct JniRemappingReplacementField +{ + const char *target_type; + const char *target_name; + const char *target_signature; +}; + struct JniRemappingTypeReplacementEntry { const JniRemappingString name; diff --git a/src/native/native.targets b/src/native/native.targets index 58d133d595e..552935eee8a 100644 --- a/src/native/native.targets +++ b/src/native/native.targets @@ -238,8 +238,12 @@ <_RuntimeSources Include="clr\include\constants.hh" /> + <_RuntimeSources Include="clr\include\xamarin-app.hh" /> <_RuntimeSources Include="clr\include\runtime-base\android-system.hh" /> + <_RuntimeSources Include="clr\include\runtime-base\jni-remapping.hh" /> + <_RuntimeSources Include="clr\host\internal-pinvokes-shared.cc" /> <_RuntimeSources Include="clr\runtime-base\android-system-shared.cc" /> + <_RuntimeSources Include="clr\runtime-base\jni-remapping.cc" /> <_RuntimeSources Include="clr\runtime-base\logger.cc" /> <_RuntimeSources Include="nativeaot\include\**\*.hh" /> <_RuntimeSources Include="nativeaot\host\*.cc" /> diff --git a/src/native/nativeaot/host/CMakeLists.txt b/src/native/nativeaot/host/CMakeLists.txt index 570a97a8fe9..9b4abe32bb6 100644 --- a/src/native/nativeaot/host/CMakeLists.txt +++ b/src/native/nativeaot/host/CMakeLists.txt @@ -31,6 +31,7 @@ set(XAMARIN_MONODROID_SOURCES host-environment.cc host-jni.cc internal-pinvoke-stubs.cc + jni-remapping-tables-stub.cc ../runtime-base/android-system.cc @@ -43,6 +44,7 @@ set(XAMARIN_MONODROID_SOURCES ${CLR_SOURCES_PATH}/host/runtime-util.cc ${CLR_SOURCES_PATH}/runtime-base/android-system-shared.cc ${CLR_SOURCES_PATH}/runtime-base/cpu-arch-detect.cc + ${CLR_SOURCES_PATH}/runtime-base/jni-remapping.cc ${CLR_SOURCES_PATH}/runtime-base/logger.cc ${CLR_SOURCES_PATH}/runtime-base/util.cc ${CLR_SOURCES_PATH}/shared/helpers.cc diff --git a/src/native/nativeaot/host/host.cc b/src/native/nativeaot/host/host.cc index 28830d7cef5..7a5577a30ff 100644 --- a/src/native/nativeaot/host/host.cc +++ b/src/native/nativeaot/host/host.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include using namespace xamarin::android; @@ -85,6 +86,7 @@ void Host::OnInit (jstring language, jstring filesDir, jstring cacheDir, JnienvI initArgs->logCategories = log_categories; initArgs->grefGcThreshold = static_cast(AndroidSystem::get_gref_gc_threshold ()); + initArgs->jniRemappingInUse = JniRemapping::is_in_use (); initArgs->grefIGCUserPeer = env->NewGlobalRef (lrefIGCUserPeer); initArgs->grefGCUserPeerable = env->NewGlobalRef (lrefGCUserPeerable); diff --git a/src/native/nativeaot/host/internal-pinvoke-stubs.cc b/src/native/nativeaot/host/internal-pinvoke-stubs.cc index 1e7dd83833b..f46f8f8944f 100644 --- a/src/native/nativeaot/host/internal-pinvoke-stubs.cc +++ b/src/native/nativeaot/host/internal-pinvoke-stubs.cc @@ -32,19 +32,6 @@ bool clr_typemap_java_to_managed ( pinvoke_unreachable (); } -const char* _monodroid_lookup_replacement_type ([[maybe_unused]] const char *jniSimpleReference) -{ - pinvoke_unreachable (); -} - -const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info ( - [[maybe_unused]] const char *jniSourceType, - [[maybe_unused]] const char *jniMethodName, - [[maybe_unused]] const char *jniMethodSignature) -{ - pinvoke_unreachable (); -} - managed_timing_sequence* monodroid_timing_start ([[maybe_unused]] const char *message) { pinvoke_unreachable (); diff --git a/src/native/nativeaot/host/jni-remapping-tables-stub.cc b/src/native/nativeaot/host/jni-remapping-tables-stub.cc new file mode 100644 index 00000000000..5113c3d3b4c --- /dev/null +++ b/src/native/nativeaot/host/jni-remapping-tables-stub.cc @@ -0,0 +1,16 @@ +#include + +// Apps without remapping data use these empty tables. The post-ILC remapping object supplies +// strong definitions when needed. Keep the defaults separate from the lookup code so that its +// references are resolved by the final application link rather than folded to these empty tables. +extern "C" { + [[gnu::weak]] extern const JniRemappingIndexTypeEntry jni_remapping_method_replacement_index[1] {}; + [[gnu::weak]] extern const JniRemappingIndexFieldTypeEntry jni_remapping_field_replacement_index[1] {}; + [[gnu::weak]] extern const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[1] {}; + [[gnu::weak]] extern const JniRemappingTypeReplacementEntry jni_remapping_reverse_type_replacements[1] {}; + + [[gnu::weak]] extern const uint32_t jni_remapping_type_replacement_count = 0; + [[gnu::weak]] extern const uint32_t jni_remapping_reverse_type_replacement_count = 0; + [[gnu::weak]] extern const uint32_t jni_remapping_method_replacement_index_count = 0; + [[gnu::weak]] extern const uint32_t jni_remapping_field_replacement_index_count = 0; +} diff --git a/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh b/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh index 60ff24596fc..4724121de7e 100644 --- a/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh +++ b/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh @@ -27,7 +27,9 @@ extern "C" { char* monodroid_TypeManager_get_java_class_name (jclass klass) noexcept; void monodroid_free (void *ptr) noexcept; const char* _monodroid_lookup_replacement_type (const char *jniSimpleReference); + const char* _monodroid_lookup_reverse_type (const char *jniSimpleReference); const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature); + const JniRemappingReplacementField* _monodroid_lookup_replacement_field_info (const char *jniSourceType, const char *jniFieldName, const char *jniFieldSignature); xamarin::android::managed_timing_sequence* monodroid_timing_start (const char *message); void monodroid_timing_stop (xamarin::android::managed_timing_sequence *sequence, const char *message); diff --git a/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs new file mode 100644 index 00000000000..fab852f9db5 --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs @@ -0,0 +1,161 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using NUnit.Framework; +using Xamarin.Android.Tasks; +using Xamarin.ProjectTools; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + [Category ("UsesDevice")] + public class R8RuntimeRemappingTests : DeviceTest + { + [TestCase (AndroidRuntime.CoreCLR)] + [TestCase (AndroidRuntime.NativeAOT)] + public void ObfuscatedMembersRun (AndroidRuntime runtime) + { + if (IgnoreUnsupportedConfiguration (runtime, release: true)) { + return; + } + + var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime, "r8remapping")) { + IsRelease = true, + EnableDefaultItems = true, + OtherBuildItems = { + new AndroidItem.AndroidJavaSource ("RuntimePeer.java") { + Encoding = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false), + Metadata = { + { "Bind", "True" }, + }, + TextContent = () => """ + package example; + + public class RuntimePeer { + public int value = 7; + public static int staticValue = 11; + public RuntimePeer () {} + public RuntimePeer echo (RuntimePeer other) { return other; } + public static RuntimePeer create () { return new RuntimePeer (); } + public static Object createHidden () { return new HiddenPeer (); } + public int add (int amount) { return value + amount; } + public int add (String text) { return value + text.length (); } + public int unusedMethod () { return -1; } + } + + class HiddenPeer extends RuntimePeer {} + """, + }, + }, + }; + proj.SetRuntime (runtime); + proj.SetRuntimeIdentifiers (new [] { DeviceAbi }); + proj.SetDefaultTargetDevice (); + proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + proj.SetProperty ("AndroidLinkTool", "r8"); + proj.SetProperty ("TrimMode", "full"); + proj.SetProperty ("AndroidEnableR8Obfuscation", "true"); + if (runtime == AndroidRuntime.NativeAOT) { + proj.SetProperty ("AndroidR8ObfuscationMode", "runtime-remapping"); + } + proj.Sources.Add (new BuildItem.Source ("HiddenPeerBinding.cs") { + TextContent = () => """ + using System; + using System.Diagnostics.CodeAnalysis; + using Android.Runtime; + + [Register ("example/HiddenPeer", DoNotGenerateAcw = true)] + public class HiddenPeerBinding : Example.RuntimePeer + { + public HiddenPeerBinding (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) {} + + [DynamicDependency (DynamicallyAccessedMemberTypes.PublicConstructors, typeof (HiddenPeerBinding))] + public static Type GetBindingType () => typeof (HiddenPeerBinding); + } + """, + }); + proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", """ + using var peer = new Example.RuntimePeer (); + peer.Value = 13; + Example.RuntimePeer.StaticValue = 17; + using var created = Example.RuntimePeer.Create (); + var echoed = peer.Echo (created); + using var hidden = Example.RuntimePeer.CreateHidden (); + if (peer.Add (2) != 15 || peer.Add ("abc") != 16 || + Example.RuntimePeer.StaticValue != 17 || echoed.Value != 7 || + echoed.GetType () != typeof (Example.RuntimePeer) || + hidden.GetType () != HiddenPeerBinding.GetBindingType ()) + throw new InvalidOperationException ("Obfuscated JNI lookup returned an incorrect value or managed type."); + Console.WriteLine ("R8_RUNTIME_REMAP_SUCCESS"); + """); + + using var builder = CreateApkBuilder (); + void AssertAppRuns (string logFile) + { + ClearAdbLogcat (); + RunProjectAndAssert (proj, builder, doNotCleanupOnUpdate: true); + Assert.IsTrue (MonitorAdbLogcat ( + line => line.Contains ("R8_RUNTIME_REMAP_SUCCESS", StringComparison.Ordinal), + Path.Combine (Root, builder.ProjectDirectory, logFile), + timeout: 30), "Constructors, overloads, fields, and peer return values should work."); + } + Assert.IsTrue (builder.Install (proj), "Obfuscated app should build and install."); + try { + var intermediate = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath); + var remapFiles = Directory.GetFiles (intermediate, "r8-jni-remap.xml", SearchOption.AllDirectories); + Assert.IsNotEmpty (remapFiles, "A compact runtime remapping file should be generated."); + var elements = remapFiles.SelectMany (file => XDocument.Load (file).Root.Elements ()).ToList (); + Assert.IsTrue (elements.Any (e => e.Name == "replace-method" && + (string) e.Attribute ("source-method-name") == "add" && + (string) e.Attribute ("target-method-name") != "add"), "The exercised methods must really be obfuscated."); + Assert.IsTrue (elements.Any (e => e.Name == "replace-field" && + (string) e.Attribute ("source-field-name") == "value" && + (string) e.Attribute ("target-field-name") != "value"), "The exercised fields must really be obfuscated."); + Assert.IsTrue (elements.Any (e => e.Name == "replace-type" && + (string) e.Attribute ("from") == "example/HiddenPeer" && + (string) e.Attribute ("to") != "example/HiddenPeer"), "Java-to-managed activation must exercise a genuinely renamed class."); + Assert.IsFalse (elements.Any (e => (string) e.Attribute ("source-method-name") == "unusedMethod"), + "An unused method on a retained type must not occupy the runtime table."); + + AssertAppRuns ("r8-runtime-remap.log"); + + if (runtime == AndroidRuntime.NativeAOT) { + var aaptRules = Path.Combine (intermediate, "aapt_rules.txt"); + FileAssert.Exists (aaptRules); + var originalAaptRules = File.ReadAllText (aaptRules); + Assert.IsTrue (builder.Build (proj), "A no-op build should succeed."); + Assert.IsTrue (builder.Output.IsTargetSkipped ("_AndroidGenerateNativeAotR8Remapping")); + Assert.IsTrue (builder.Output.IsTargetSkipped ("_AndroidCompileNativeAotR8Remapping")); + Assert.IsTrue (builder.Output.IsTargetSkipped ("_AndroidLinkNativeAotSharedLibrary")); + FileAssert.Exists (aaptRules, "IncrementalClean must retain AAPT keep rules."); + Assert.AreEqual (originalAaptRules, File.ReadAllText (aaptRules)); + + var ilcObject = Directory.GetFiles (intermediate, $"{proj.ProjectName}.o", SearchOption.AllDirectories).Single (); + var ilcTimestamp = File.GetLastWriteTimeUtc (ilcObject); + var remapObject = Directory.GetFiles (intermediate, $"jni_remap.{DeviceAbi}.o", SearchOption.AllDirectories).Single (); + File.Delete (remapObject); + Assert.IsTrue (builder.Build (proj), "A missing remapping object should be regenerated."); + FileAssert.Exists (remapObject); + Assert.AreEqual (ilcTimestamp, File.GetLastWriteTimeUtc (ilcObject), "Recovering the late-linked table must not recompile IL."); + Assert.IsFalse (builder.Output.IsTargetSkipped ("_AndroidCompileNativeAotR8Remapping")); + Assert.IsFalse (builder.Output.IsTargetSkipped ("_AndroidLinkNativeAotSharedLibrary")); + + File.Delete (aaptRules); + Assert.IsTrue (builder.Build (proj), "Missing resource keep rules should be regenerated."); + FileAssert.Exists (aaptRules); + Assert.AreEqual (originalAaptRules, File.ReadAllText (aaptRules)); + Assert.IsFalse (builder.Output.IsTargetSkipped ("_CreateBaseApk")); + } + + proj.SetProperty ("AndroidEnableR8Obfuscation", "false"); + Assert.IsTrue (builder.Install (proj), "Disabling obfuscation should rebuild and install the baseline."); + StringAssert.Contains ("-dontobfuscate", File.ReadAllText (Path.Combine (intermediate, "proguard", "proguard_xamarin.cfg"))); + AssertAppRuns ("r8-disabled.log"); + } finally { + Assert.IsTrue (builder.Uninstall (proj), "Obfuscated app should uninstall."); + } + } + } +} From 0fa1381312386d9c5f74b0afe753b423620ba705 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 6 Sep 2026 14:01:43 +0200 Subject: [PATCH 02/14] [Java.Interop] Key R8 member remaps by replaced type Context: https://github.com/dotnet/android/pull/12692 Follow the existing Intune contract: member lookup keys contain the replaced owner type and the original managed member name and descriptor. Generate R8 member entries and collision keys in that same form rather than teaching every consumer to try both original and replaced owners. Remove JniPeerOriginalTypeName, the duplicate constructor identity, and the now-unused Java.Interop reverse-type hook. Keep reverse lookup for peer activation and derive desugared companion names in the Android remapping helper where the original name is actually needed. Cover residual-owner keys, descriptors, MAM conflicts and duplicates, and real renamed-class construction and member access on both runtimes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../JniPeerMembers.JniInstanceFields.cs | 2 +- .../JniPeerMembers.JniInstanceMethods.cs | 17 +-- .../JniPeerMembers.JniStaticFields.cs | 2 +- .../JniPeerMembers.JniStaticMethods.cs | 4 +- .../Java.Interop/JniPeerMembers.cs | 53 +++------- .../Java.Interop/JniRuntime.JniTypeManager.cs | 9 -- .../src/Java.Interop/PublicAPI.Unshipped.txt | 1 - .../Android.Runtime/AndroidRuntime.cs | 5 - .../JniRemappingLookup.cs | 4 + .../TrimmableTypeMapTypeManager.cs | 3 - .../Tasks/GenerateR8JniRemapping.cs | 10 +- .../Tasks/GenerateR8JniRemappingTests.cs | 100 +++++++++++++++--- .../Tests/R8RuntimeRemappingTests.cs | 38 ++++++- 13 files changed, 150 insertions(+), 98 deletions(-) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs index 0346b47c2f7..d81e15f71b8 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs @@ -33,7 +33,7 @@ public JniFieldInfo GetFieldInfo (string encodedMember) JniFieldInfo GetFieldInfo (string field, string signature) { - var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerOriginalTypeName, Members.JniPeerTypeName, Members.ManagedPeerType, field, signature); + var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, Members.ManagedPeerType, field, signature); if (newField.HasValue) { var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName; var fieldName = newField.Value.TargetJniFieldName ?? field; diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs index 5bdad4129b7..8a9bd475f0a 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs @@ -24,24 +24,15 @@ internal JniInstanceMethods (JniPeerMembers members) declaringType.FullName)); DeclaringType = declaringType; - // The managed type declares its original JNI name; the peer must be looked up under the - // name it has in the packaged application, but member replacements stay keyed by the - // original one. - originalJniTypeName = jvm.TypeManager.GetOriginalType (info.SimpleReference) ?? info.SimpleReference; - targetJniTypeName = jvm.TypeManager.GetReplacementType (originalJniTypeName) ?? info.Name; + targetJniTypeName = info.Name; jniPeerType = new JniType (targetJniTypeName); jniPeerType.RegisterWithRuntime (); } JniPeerMembers? members; JniType? jniPeerType; - readonly string? originalJniTypeName; readonly string? targetJniTypeName; - // The JNI type name member replacements are keyed by... - string SourceJniTypeName => originalJniTypeName ?? Members.JniPeerOriginalTypeName; - - // ...and the one members are actually looked up on. string TargetJniTypeName => targetJniTypeName ?? Members.JniPeerTypeName; internal JniPeerMembers Members => members ?? throw new InvalidOperationException (); @@ -79,7 +70,7 @@ JniMethodInfo GetConstructorCore (string signature) { // Constructors are never renamed, but their parameter types can be, so the descriptor // still has to be translated. - var newMethod = JniPeerMembers.GetReplacementMethodInfo (SourceJniTypeName, TargetJniTypeName, DeclaringType, "", signature, searchBaseTypes: false); + var newMethod = JniPeerMembers.GetReplacementMethodInfo (TargetJniTypeName, DeclaringType, "", signature, searchBaseTypes: false); var targetSignature = newMethod?.TargetJniMethodSignature; if (targetSignature != null && !string.Equals (targetSignature, signature, StringComparison.Ordinal)) { var typeName = newMethod?.TargetJniType ?? TargetJniTypeName; @@ -133,7 +124,7 @@ public JniMethodInfo GetMethodInfo (string encodedMember) JniMethodInfo GetMethodInfo (string method, string signature) { var m = (JniMethodInfo?) null; - var newMethod = JniPeerMembers.GetReplacementMethodInfo (SourceJniTypeName, TargetJniTypeName, DeclaringType, method, signature); + var newMethod = JniPeerMembers.GetReplacementMethodInfo (TargetJniTypeName, DeclaringType, method, signature); if (newMethod.HasValue) { var typeName = newMethod.Value.TargetJniType ?? TargetJniTypeName; var methodName = newMethod.Value.TargetJniMethodName ?? method; @@ -149,7 +140,7 @@ JniMethodInfo GetMethodInfo (string method, string signature) if (t.TryGetInstanceMethod (methodName, methodSig, out m)) { return m; } - Console.Error.WriteLine ($"warning: For declared method `{SourceJniTypeName}.{method}.{signature}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!"); + Console.Error.WriteLine ($"warning: For declared method `{TargetJniTypeName}.{method}.{signature}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!"); } return JniPeerType.GetInstanceMethod (method, signature); } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs index 4d4fb7f0457..6dfba46ea79 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs @@ -28,7 +28,7 @@ public JniFieldInfo GetFieldInfo (string encodedMember) JniFieldInfo GetFieldInfo (string field, string signature) { - var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerOriginalTypeName, Members.JniPeerTypeName, Members.ManagedPeerType, field, signature); + var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, Members.ManagedPeerType, field, signature); if (newField.HasValue) { var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName; var fieldName = newField.Value.TargetJniFieldName ?? field; diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs index 6e3a3388c49..4dc953e0201 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs @@ -34,7 +34,7 @@ public JniMethodInfo GetMethodInfo (string encodedMember) JniMethodInfo GetMethodInfo (string method, string signature) { var m = (JniMethodInfo?) null; - var newMethod = JniPeerMembers.GetReplacementMethodInfo (Members.JniPeerOriginalTypeName, Members.JniPeerTypeName, Members.ManagedPeerType, method, signature); + var newMethod = JniPeerMembers.GetReplacementMethodInfo (Members.JniPeerTypeName, Members.ManagedPeerType, method, signature); if (newMethod.HasValue) { using var t = new JniType (newMethod.Value.TargetJniType ?? Members.JniPeerTypeName); if (t.TryGetStaticMethod ( @@ -66,7 +66,7 @@ JniType GetMethodDeclaringType (JniMethodInfo method) JniMethodInfo? FindInFallbackTypes (string method, string signature) { - var fallbackTypes = JniEnvironment.Runtime.TypeManager.GetStaticMethodFallbackTypes (Members.JniPeerOriginalTypeName); + var fallbackTypes = JniEnvironment.Runtime.TypeManager.GetStaticMethodFallbackTypes (Members.JniPeerTypeName); if (fallbackTypes == null) { return null; } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs index d5e976a9a23..6c8e75021a4 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs @@ -12,12 +12,12 @@ public partial class JniPeerMembers { private bool isInterface; public JniPeerMembers (string jniPeerTypeName, Type managedPeerType, bool isInterface) - : this (jniPeerTypeName, GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: true, isInterface: isInterface) + : this (GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: true, isInterface: isInterface) { } public JniPeerMembers (string jniPeerTypeName, Type managedPeerType) - : this (jniPeerTypeName, GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: true, isInterface: false) + : this (GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: true, isInterface: false) { } @@ -31,12 +31,10 @@ static string GetReplacementType (string jniPeerTypeName) return jniPeerTypeName; } - JniPeerMembers (string originalJniPeerTypeName, string jniPeerTypeName, Type managedPeerType, bool checkManagedPeerType, bool isInterface = false) + JniPeerMembers (string jniPeerTypeName, Type managedPeerType, bool checkManagedPeerType, bool isInterface = false) { if (jniPeerTypeName == null) throw new ArgumentNullException (nameof (jniPeerTypeName)); - if (originalJniPeerTypeName == null) - throw new ArgumentNullException (nameof (originalJniPeerTypeName)); if (checkManagedPeerType) { if (managedPeerType == null) @@ -45,10 +43,8 @@ static string GetReplacementType (string jniPeerTypeName) throw new ArgumentException ("'managedPeerType' must implement the IJavaPeerable interface.", nameof (managedPeerType)); #if DEBUG - // The managed type still declares its *original* JNI name, so compare against that - // and not against the (possibly remapped) name used to look the type up. var signatureFromType = JniEnvironment.Runtime.TypeManager.GetTypeSignature (managedPeerType); - if (signatureFromType.SimpleReference != originalJniPeerTypeName) { + if (signatureFromType.SimpleReference != jniPeerTypeName) { Debug.WriteLine ("WARNING-Java.Interop: ManagedPeerType <=> JniTypeName Mismatch! javaVM.GetJniTypeInfoForType(typeof({0})).JniTypeName=\"{1}\" != \"{2}\"", managedPeerType.FullName, signatureFromType.SimpleReference, @@ -59,7 +55,6 @@ static string GetReplacementType (string jniPeerTypeName) } JniPeerTypeName = jniPeerTypeName; - JniPeerOriginalTypeName = originalJniPeerTypeName; ManagedPeerType = managedPeerType; this.isInterface = isInterface; @@ -72,7 +67,7 @@ static string GetReplacementType (string jniPeerTypeName) static JniPeerMembers CreatePeerMembers (string jniPeerTypeName, Type managedPeerType) { - return new JniPeerMembers (jniPeerTypeName, GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: false); + return new JniPeerMembers (GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: false); } JniType? jniPeerType; @@ -87,9 +82,6 @@ static JniPeerMembers CreatePeerMembers (string jniPeerTypeName, Type managedPee /// remapped name when the type was renamed in the packaged application. public string JniPeerTypeName {get; private set;} - /// The JNI type name the managed peer type declares. Member replacements are keyed - /// by it, because the mapping describes the original names. - internal string JniPeerOriginalTypeName {get; private set;} public JniType JniPeerType { get { var t = JniType.GetCachedJniType (ref jniPeerType, JniPeerTypeName); @@ -155,25 +147,16 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value) return isInterface ? this : value.JniPeerMembers; } - // - // Member replacements are described in terms of the JNI names the managed code declares, so - // `sourceJniTypeName` is the natural key. Remapping inputs which predate type renaming being - // applied to member entries - the Intune/MAM mapping - instead key them by the replaced - // name, so that is tried as well. - // + // Member keys use the replaced type name but retain the managed member name and signature. internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo ( - string sourceJniTypeName, - string effectiveJniTypeName, + string jniTypeName, Type managedPeerType, string method, string signature, bool searchBaseTypes = true) { var typeManager = JniEnvironment.Runtime.TypeManager; - var info = typeManager.GetReplacementMethodInfo (sourceJniTypeName, method, signature); - if (info == null && !string.Equals (sourceJniTypeName, effectiveJniTypeName, StringComparison.Ordinal)) { - info = typeManager.GetReplacementMethodInfo (effectiveJniTypeName, method, signature); - } + var info = typeManager.GetReplacementMethodInfo (jniTypeName, method, signature); if (info == null && searchBaseTypes) { for (Type? baseType = managedPeerType.BaseType; baseType != null; baseType = baseType.BaseType) { var baseSignature = typeManager.GetTypeSignature (baseType); @@ -181,11 +164,7 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value) if (effectiveBaseType == null) { continue; } - string sourceBaseType = typeManager.GetOriginalType (effectiveBaseType) ?? effectiveBaseType; - info = typeManager.GetReplacementMethodInfo (sourceBaseType, method, signature); - if (info == null && !string.Equals (sourceBaseType, effectiveBaseType, StringComparison.Ordinal)) { - info = typeManager.GetReplacementMethodInfo (effectiveBaseType, method, signature); - } + info = typeManager.GetReplacementMethodInfo (effectiveBaseType, method, signature); if (info != null) { break; } @@ -195,17 +174,13 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value) } internal static JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo ( - string sourceJniTypeName, - string effectiveJniTypeName, + string jniTypeName, Type managedPeerType, string field, string signature) { var typeManager = JniEnvironment.Runtime.TypeManager; - var info = typeManager.GetReplacementFieldInfo (sourceJniTypeName, field, signature); - if (info == null && !string.Equals (sourceJniTypeName, effectiveJniTypeName, StringComparison.Ordinal)) { - info = typeManager.GetReplacementFieldInfo (effectiveJniTypeName, field, signature); - } + var info = typeManager.GetReplacementFieldInfo (jniTypeName, field, signature); if (info == null) { for (Type? baseType = managedPeerType.BaseType; baseType != null; baseType = baseType.BaseType) { var baseSignature = typeManager.GetTypeSignature (baseType); @@ -213,11 +188,7 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value) if (effectiveBaseType == null) { continue; } - string sourceBaseType = typeManager.GetOriginalType (effectiveBaseType) ?? effectiveBaseType; - info = typeManager.GetReplacementFieldInfo (sourceBaseType, field, signature); - if (info == null && !string.Equals (sourceBaseType, effectiveBaseType, StringComparison.Ordinal)) { - info = typeManager.GetReplacementFieldInfo (effectiveBaseType, field, signature); - } + info = typeManager.GetReplacementFieldInfo (effectiveBaseType, field, signature); if (info != null) { break; } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs index dc9f22c0f21..81a5fba7bc2 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs @@ -305,15 +305,6 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type) protected virtual string? GetReplacementTypeCore (string jniSimpleReference) => null; - internal string? GetOriginalType (string jniSimpleReference) - { - AssertValid (); - AssertSimpleReference (jniSimpleReference, nameof (jniSimpleReference)); - return GetOriginalTypeCore (jniSimpleReference); - } - - protected virtual string? GetOriginalTypeCore (string jniSimpleReference) => null; - public IReadOnlyList? GetStaticMethodFallbackTypes (string jniSimpleReference) { AssertValid (); diff --git a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt index 5f4294251ac..90ee630a34a 100644 --- a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt @@ -141,4 +141,3 @@ override Java.Interop.JniRuntime.ReplacementFieldInfo.ToString() -> string! static Java.Interop.JniRuntime.ReplacementFieldInfo.operator !=(Java.Interop.JniRuntime.ReplacementFieldInfo a, Java.Interop.JniRuntime.ReplacementFieldInfo b) -> bool static Java.Interop.JniRuntime.ReplacementFieldInfo.operator ==(Java.Interop.JniRuntime.ReplacementFieldInfo a, Java.Interop.JniRuntime.ReplacementFieldInfo b) -> bool virtual Java.Interop.JniRuntime.JniTypeManager.GetReplacementFieldInfoCore(string! jniSimpleReference, string! jniFieldName, string! jniFieldSignature) -> Java.Interop.JniRuntime.ReplacementFieldInfo? -virtual Java.Interop.JniRuntime.JniTypeManager.GetOriginalTypeCore(string! jniSimpleReference) -> string? diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index e976e300bbe..ee2772e654d 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -381,11 +381,6 @@ protected override IEnumerable GetSimpleReferences (Type type) return JniRemappingLookup.GetReplacementType (jniSimpleReference); } - protected override string? GetOriginalTypeCore (string jniSimpleReference) - { - return JniRemappingLookup.GetReverseType (jniSimpleReference); - } - protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) { return JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs index 7fe7172f701..f9bec1a5efd 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs @@ -32,6 +32,10 @@ struct JniRemappingReplacementField internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSimpleReference, bool useReplacementTypes) { + // Desugared companion names are derived before R8 renames the interface and companions. + if (useReplacementTypes) { + jniSimpleReference = GetReverseType (jniSimpleReference) ?? jniSimpleReference; + } int slash = jniSimpleReference.LastIndexOf ('/'); var desugarType = slash > 0 ? $"{jniSimpleReference.Substring (0, slash + 1)}Desugar{jniSimpleReference.Substring (slash + 1)}" diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index a53a02ee4dd..0b7d7a2bcae 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -402,9 +402,6 @@ protected override IEnumerable GetTypeSignaturesCore (Type typ protected override string? GetReplacementTypeCore (string jniSimpleReference) => JniRemappingLookup.GetReplacementType (jniSimpleReference); - protected override string? GetOriginalTypeCore (string jniSimpleReference) - => JniRemappingLookup.GetReverseType (jniSimpleReference); - protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) => JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs index 9b9016b0bc9..d5c636c2be2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs @@ -24,6 +24,8 @@ namespace Xamarin.Android.Tasks /// The generated document is what teaches the runtime how those original names map onto the /// obfuscated names R8 produced, and how the obfuscated names map back for Java-to-managed /// lookups. + /// Member lookups use the remapped owner type, but retain the original member names and + /// descriptors from managed code, matching the existing Intune/MAM remapping contract. /// /// The document extends the existing schema in a backward-compatible way: /// @@ -294,13 +296,13 @@ void WriteField (XmlWriter writer, R8ClassMapping classMapping, R8FieldMapping f if (!TryClaimEntry ( "replace-field", - BuildFieldKey (classMapping.OriginalJniName, field.OriginalName), + BuildFieldKey (classMapping.ObfuscatedJniName, field.OriginalName), $"{classMapping.ObfuscatedJniName}\t{field.ObfuscatedName}\t{targetSignature}")) { return; } writer.WriteStartElement ("replace-field"); - writer.WriteAttributeString ("source-type", classMapping.OriginalJniName); + writer.WriteAttributeString ("source-type", classMapping.ObfuscatedJniName); writer.WriteAttributeString ("source-field-name", field.OriginalName); writer.WriteAttributeString ("source-field-signature", sourceSignature); writer.WriteAttributeString ("target-type", classMapping.ObfuscatedJniName); @@ -331,13 +333,13 @@ void WriteMethod (XmlWriter writer, R8ClassMapping classMapping, R8MethodMapping // The source signature is part of the key, so overloads stay distinct entries. if (!TryClaimEntry ( "replace-method", - BuildMethodKey (classMapping.OriginalJniName, method.OriginalName, sourceSignature), + BuildMethodKey (classMapping.ObfuscatedJniName, method.OriginalName, sourceSignature), $"{classMapping.ObfuscatedJniName}\t{method.ObfuscatedName}\t{targetSignature}")) { return; } writer.WriteStartElement ("replace-method"); - writer.WriteAttributeString ("source-type", classMapping.OriginalJniName); + writer.WriteAttributeString ("source-type", classMapping.ObfuscatedJniName); writer.WriteAttributeString ("source-method-name", method.OriginalName); writer.WriteAttributeString ("source-method-signature", sourceSignature); writer.WriteAttributeString ("target-type", classMapping.ObfuscatedJniName); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs index 2d3b2ab8f9b..5e3725c59af 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs @@ -197,9 +197,9 @@ int unused -> h void run(int) -> j """, null, [], nativeObject); - StringAssert.Contains (Method ("com/contoso/Peer", "run", "(I)V", "a/b", "c", "(I)V"), xml); - StringAssert.Contains (Method ("com/contoso/Peer", "callback", "()V", "a/b", "f", "()V"), xml); - StringAssert.Contains (Field ("com/contoso/Peer", "value", "I", "a/b", "g", "I"), xml); + StringAssert.Contains (Method ("a/b", "run", "(I)V", "a/b", "c", "(I)V"), xml); + StringAssert.Contains (Method ("a/b", "callback", "()V", "a/b", "f", "()V"), xml); + StringAssert.Contains (Field ("a/b", "value", "I", "a/b", "g", "I"), xml); StringAssert.DoesNotContain ("removed", xml); StringAssert.DoesNotContain ("unused", xml); StringAssert.DoesNotContain ("Unused", xml); @@ -221,9 +221,9 @@ com.contoso.Result run(com.contoso.Argument[]) -> c com.contoso.Result -> a.e: """, null, [], nativeObject); - StringAssert.Contains (Method ("com/contoso/Peer", "<init>", "([Lcom/contoso/Argument;)V", + StringAssert.Contains (Method ("a/b", "<init>", "([Lcom/contoso/Argument;)V", "a/b", "<init>", "([La/d;)V"), xml); - StringAssert.Contains (Method ("com/contoso/Peer", "run", "([Lcom/contoso/Argument;)Lcom/contoso/Result;", + StringAssert.Contains (Method ("a/b", "run", "([Lcom/contoso/Argument;)Lcom/contoso/Result;", "a/b", "c", "([La/d;)La/e;"), xml); StringAssert.Contains ("""""", xml); StringAssert.Contains ("""""", xml); @@ -246,9 +246,9 @@ int get(int) -> d java.lang.Object get(java.lang.Object) -> f """, null, [], nativeObject); - StringAssert.Contains (Method ("com/contoso/Generic", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", + StringAssert.Contains (Method ("a/b", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", "a/b", "c", "(Ljava/lang/Object;)Ljava/lang/Object;"), xml); - StringAssert.Contains (Method ("com/contoso/Generic$Nested", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", + StringAssert.Contains (Method ("a/e", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", "a/e", "f", "(Ljava/lang/Object;)Ljava/lang/Object;"), xml); StringAssert.DoesNotContain ("(I)I", xml); } @@ -391,7 +391,7 @@ void unused() -> e []); StringAssert.Contains ("""""", xml); - StringAssert.Contains (Method ("com/contoso/Peer", "onClick", "()V", "a/b", "c", "()V"), xml); + StringAssert.Contains (Method ("a/b", "onClick", "()V", "a/b", "c", "()V"), xml); StringAssert.DoesNotContain ("com/contoso/Unused", xml); StringAssert.DoesNotContain ("unused", xml); } @@ -454,9 +454,9 @@ void doWork(java.lang.String) -> d void doWork() -> e """); - StringAssert.Contains (Method ("com/contoso/Peer", "doWork", "(I)V", "a/b", "c", "(I)V"), xml); - StringAssert.Contains (Method ("com/contoso/Peer", "doWork", "(Ljava/lang/String;)V", "a/b", "d", "(Ljava/lang/String;)V"), xml); - StringAssert.Contains (Method ("com/contoso/Peer", "doWork", "()V", "a/b", "e", "()V"), xml); + StringAssert.Contains (Method ("a/b", "doWork", "(I)V", "a/b", "c", "(I)V"), xml); + StringAssert.Contains (Method ("a/b", "doWork", "(Ljava/lang/String;)V", "a/b", "d", "(Ljava/lang/String;)V"), xml); + StringAssert.Contains (Method ("a/b", "doWork", "()V", "a/b", "e", "()V"), xml); } [Test] @@ -471,10 +471,29 @@ com.contoso.Result run(com.contoso.Argument[],int) -> c """); StringAssert.Contains ( - Method ("com/contoso/Peer", "run", "([Lcom/contoso/Argument;I)Lcom/contoso/Result;", "a/b", "c", "([La/d;I)La/e;"), + Method ("a/b", "run", "([Lcom/contoso/Argument;I)Lcom/contoso/Result;", "a/b", "c", "([La/d;I)La/e;"), xml); } + [Test] + public void RenamedMembersUseResidualOwnersAndOriginalSignatures () + { + var xml = Run ( + """ + com.contoso.Peer -> a.b: + com.contoso.Peer run(com.contoso.Peer[]) -> c + com.contoso.Peer[] peers -> d + """); + + StringAssert.Contains ("""""", xml); + StringAssert.Contains ("""""", xml); + StringAssert.Contains (Method ("a/b", "run", "([Lcom/contoso/Peer;)Lcom/contoso/Peer;", + "a/b", "c", "([La/b;)La/b;"), xml); + StringAssert.Contains (Field ("a/b", "peers", "[Lcom/contoso/Peer;", "a/b", "d", "[La/b;"), xml); + StringAssert.DoesNotContain ("source-type=\"com/contoso/Peer\"", xml); + Assert.AreEqual (0, Warnings.Count); + } + [Test] public void ConstructorsAreEmittedWhenOnlyTheirDescriptorChanges () { @@ -516,9 +535,9 @@ com.contoso.Argument[] arguments -> e com.contoso.Argument -> a.d: """); - StringAssert.Contains (Field ("com/contoso/Peer", "counter", "I", "a/b", "c", "I"), xml); - StringAssert.Contains (Field ("com/contoso/Peer", "argument", "Lcom/contoso/Argument;", "a/b", "d", "La/d;"), xml); - StringAssert.Contains (Field ("com/contoso/Peer", "arguments", "[Lcom/contoso/Argument;", "a/b", "e", "[La/d;"), xml); + StringAssert.Contains (Field ("a/b", "counter", "I", "a/b", "c", "I"), xml); + StringAssert.Contains (Field ("a/b", "argument", "Lcom/contoso/Argument;", "a/b", "d", "La/d;"), xml); + StringAssert.Contains (Field ("a/b", "arguments", "[Lcom/contoso/Argument;", "a/b", "e", "[La/d;"), xml); } [Test] @@ -600,12 +619,16 @@ public void ExistingRemapEntriesAreNotOverridden () var xml = Run ( """ com.contoso.MainActivity -> a.b: + void onCreate() -> c + int counter -> d com.contoso.Other -> a.c: """, existing); StringAssert.DoesNotContain ("com/contoso/MainActivity", xml, "The pre-existing remapping input must win."); + StringAssert.DoesNotContain ("source-type=\"a/b\"", xml, + "Members of an externally owned type must not be emitted using the residual owner."); StringAssert.Contains ("""""", xml); Assert.AreEqual (1, Warnings.Count, "The conflict should have been reported."); Assert.AreEqual ("XA4328", Warnings [0].Code); @@ -624,11 +647,16 @@ public void IdenticalExistingRemapEntriesDoNotWarn () var xml = Run ( """ com.contoso.MainActivity -> a.b: + void onCreate() -> c + int counter -> d """, existing); StringAssert.DoesNotContain ("replace-type", xml, "A duplicate entry must not be emitted twice."); + StringAssert.DoesNotContain ("reverse-type", xml); + StringAssert.DoesNotContain ("replace-method", xml); + StringAssert.DoesNotContain ("replace-field", xml); Assert.AreEqual (0, Warnings.Count, "An identical entry is not a conflict."); } @@ -638,7 +666,7 @@ public void ExistingMethodEntriesOnlyConflictForTheSameOverload () var existing = WriteRemapXml ( """ - + """); @@ -652,12 +680,50 @@ void doWork(java.lang.String) -> d StringAssert.DoesNotContain ("(I)V", xml, "The overload owned by another input must not be emitted."); - StringAssert.Contains (Method ("com/contoso/Peer", "doWork", "(Ljava/lang/String;)V", "a/b", "d", "(Ljava/lang/String;)V"), xml, + StringAssert.Contains (Method ("a/b", "doWork", "(Ljava/lang/String;)V", "a/b", "d", "(Ljava/lang/String;)V"), xml, "A different overload is not a conflict."); Assert.AreEqual (1, Warnings.Count); Assert.AreEqual ("XA4328", Warnings [0].Code); } + [TestCase ("a/b", false)] + [TestCase ("a/b", true)] + [TestCase ("com/contoso/Peer", false)] + [TestCase ("com/contoso/Peer", true)] + public void ExistingMemberEntriesAreMatchedOnResidualOwner (string sourceType, bool identicalTarget) + { + string targetType = identicalTarget ? "a/b" : "com/contoso/Mam"; + var existing = WriteRemapXml ( + $""" + + {Method (sourceType, "run", "([Lcom/contoso/Peer;)Lcom/contoso/Peer;", targetType, "c", "([La/b;)La/b;")} + {Field (sourceType, "peers", "[Lcom/contoso/Peer;", targetType, "d", "[La/b;")} + + """); + var xml = Run ( + """ + com.contoso.Peer -> a.b: + com.contoso.Peer run(com.contoso.Peer[]) -> c + com.contoso.Peer[] peers -> d + """, existing); + + StringAssert.Contains ("""""", xml); + StringAssert.Contains ("""""", xml); + if (sourceType == "a/b") { + StringAssert.DoesNotContain ("replace-method", xml); + StringAssert.DoesNotContain ("replace-field", xml); + Assert.AreEqual (identicalTarget ? 0 : 2, Warnings.Count); + } else { + StringAssert.Contains (Method ("a/b", "run", "([Lcom/contoso/Peer;)Lcom/contoso/Peer;", + "a/b", "c", "([La/b;)La/b;"), xml); + StringAssert.Contains (Field ("a/b", "peers", "[Lcom/contoso/Peer;", "a/b", "d", "[La/b;"), xml); + Assert.AreEqual (0, Warnings.Count, "An original owner is a different member lookup key."); + } + foreach (var warning in Warnings) { + Assert.AreEqual ("XA4328", warning.Code); + } + } + [Test] public void GeneratedDocumentParsesWithTheExistingRemapSchema () { diff --git a/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs index fab852f9db5..7b5080e08e1 100644 --- a/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs @@ -45,7 +45,11 @@ public RuntimePeer () {} public int unusedMethod () { return -1; } } - class HiddenPeer extends RuntimePeer {} + class HiddenPeer extends RuntimePeer { + public int hiddenValue = 23; + public HiddenPeer () {} + public int hiddenAdd () { return hiddenValue + 2; } + } """, }, }, @@ -55,6 +59,7 @@ class HiddenPeer extends RuntimePeer {} proj.SetDefaultTargetDevice (); proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); proj.SetProperty ("AndroidLinkTool", "r8"); + proj.SetProperty ("AllowUnsafeBlocks", "true"); proj.SetProperty ("TrimMode", "full"); proj.SetProperty ("AndroidEnableR8Obfuscation", "true"); if (runtime == AndroidRuntime.NativeAOT) { @@ -65,12 +70,28 @@ class HiddenPeer extends RuntimePeer {} using System; using System.Diagnostics.CodeAnalysis; using Android.Runtime; + using Java.Interop; [Register ("example/HiddenPeer", DoNotGenerateAcw = true)] public class HiddenPeerBinding : Example.RuntimePeer { + static readonly JniPeerMembers _members = new XAPeerMembers ("example/HiddenPeer", typeof (HiddenPeerBinding)); + public override JniPeerMembers JniPeerMembers => _members; + protected override IntPtr ThresholdClass => _members.JniPeerType.PeerReference.Handle; + protected override Type ThresholdType => _members.ManagedPeerType; + + public HiddenPeerBinding () {} public HiddenPeerBinding (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) {} + [Register ("hiddenValue")] + public int HiddenValue { + get => _members.InstanceFields.GetInt32Value ("hiddenValue.I", this); + set => _members.InstanceFields.SetValue ("hiddenValue.I", this, value); + } + + [Register ("hiddenAdd", "()I", "")] + public unsafe int HiddenAdd () => _members.InstanceMethods.InvokeVirtualInt32Method ("hiddenAdd.()I", this, null); + [DynamicDependency (DynamicallyAccessedMemberTypes.PublicConstructors, typeof (HiddenPeerBinding))] public static Type GetBindingType () => typeof (HiddenPeerBinding); } @@ -83,8 +104,13 @@ public HiddenPeerBinding (IntPtr handle, JniHandleOwnership transfer) : base (ha using var created = Example.RuntimePeer.Create (); var echoed = peer.Echo (created); using var hidden = Example.RuntimePeer.CreateHidden (); + using var constructedHidden = new HiddenPeerBinding (); + var boundHidden = (HiddenPeerBinding) hidden; + boundHidden.HiddenValue = 29; if (peer.Add (2) != 15 || peer.Add ("abc") != 16 || Example.RuntimePeer.StaticValue != 17 || echoed.Value != 7 || + boundHidden.HiddenAdd () != 31 || constructedHidden.HiddenValue != 23 || + boundHidden.Add (1) != 8 || echoed.GetType () != typeof (Example.RuntimePeer) || hidden.GetType () != HiddenPeerBinding.GetBindingType ()) throw new InvalidOperationException ("Obfuscated JNI lookup returned an incorrect value or managed type."); @@ -116,6 +142,16 @@ void AssertAppRuns (string logFile) Assert.IsTrue (elements.Any (e => e.Name == "replace-type" && (string) e.Attribute ("from") == "example/HiddenPeer" && (string) e.Attribute ("to") != "example/HiddenPeer"), "Java-to-managed activation must exercise a genuinely renamed class."); + var hiddenType = (string) elements.First (e => e.Name == "replace-type" && + (string) e.Attribute ("from") == "example/HiddenPeer").Attribute ("to"); + Assert.IsTrue (elements.Any (e => e.Name == "replace-method" && + (string) e.Attribute ("source-type") == hiddenType && + (string) e.Attribute ("source-method-name") == "hiddenAdd" && + (string) e.Attribute ("target-method-name") != "hiddenAdd"), "Method lookups must use the renamed owner."); + Assert.IsTrue (elements.Any (e => e.Name == "replace-field" && + (string) e.Attribute ("source-type") == hiddenType && + (string) e.Attribute ("source-field-name") == "hiddenValue" && + (string) e.Attribute ("target-field-name") != "hiddenValue"), "Field lookups must use the renamed owner."); Assert.IsFalse (elements.Any (e => (string) e.Attribute ("source-method-name") == "unusedMethod"), "An unused method on a retained type must not occupy the runtime table."); From 48d20606a0ccce9a80cabbdf19958cccb10a41fc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 7 Sep 2026 09:40:51 +0200 Subject: [PATCH 03/14] [Xamarin.Android.Build.Tasks] Run R8 once after managed trimming Runtime remapping keeps original JNI names in managed code, so it does not need an early naming pass. Remove the seed compilation, seed R8 invocation, applymapping support, and early manifest-rule generation. Generate remapping tables from the one final R8 mapping. Defer CoreCLR native linking until those tables exist. For NativeAOT, compile IL early and link each RID after the shared R8 pass without running ILC again. Keep post-ILC table filtering, ordinary MAM assets, and opt-out behavior. Add invocation-count and ordering coverage, including multi-RID builds, no-op builds, missing mapping/table recovery, and changed R8 rules. Context: https://github.com/dotnet/android/issues/12535 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../building-apps/build-properties.md | 5 +- Documentation/docs-mobile/messages/xa4327.md | 22 +- Documentation/docs-mobile/messages/xa4328.md | 6 +- Documentation/docs-mobile/messages/xa4329.md | 2 + .../Microsoft.Android.Sdk.BuildOrder.targets | 2 - ...rosoft.Android.Sdk.NativeAOT.After.targets | 2 + ...Microsoft.Android.Sdk.NativeAOT.R8.targets | 48 ++++ .../Microsoft.Android.Sdk.NativeAOT.targets | 43 ++- ...crosoft.Android.Sdk.R8JniRemapping.targets | 252 +++--------------- .../Properties/Resources.Designer.cs | 35 +-- .../Properties/Resources.resx | 25 +- ...erateR8JniManifestProguardConfiguration.cs | 105 -------- .../Tasks/GenerateR8JniRemapping.cs | 9 +- src/Xamarin.Android.Build.Tasks/Tasks/R8.cs | 61 +---- .../Xamarin.Android.Common.targets | 33 ++- .../Xamarin.Android.D8.targets | 3 +- .../Tests/R8RuntimeRemappingBuildTests.cs | 70 +++++ .../Tests/R8RuntimeRemappingTests.cs | 46 ++++ 18 files changed, 291 insertions(+), 478 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.R8.targets delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniManifestProguardConfiguration.cs create mode 100644 tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs diff --git a/Documentation/docs-mobile/building-apps/build-properties.md b/Documentation/docs-mobile/building-apps/build-properties.md index 695a1531b61..c2fd3f389bf 100644 --- a/Documentation/docs-mobile/building-apps/build-properties.md +++ b/Documentation/docs-mobile/building-apps/build-properties.md @@ -1134,8 +1134,9 @@ The default is `runtime-remapping`. | `runtime-remapping` | Keeps managed assemblies unchanged and translates JNI type/member lookups using generated native remapping tables. Available for trimmed CoreCLR and NativeAOT applications. | | `experimental-rewriting` | Reserved for the separate managed-assembly rewriting implementation. This SDK does not yet include its build pipeline; selecting it reports [XA4329](../messages/xa4329.md). | -The runtime-remapping mode runs a naming-only R8 pass before ILLink or ILC and -applies that mapping in the final R8 pass. CoreCLR selects remaps from linked +The runtime-remapping mode leaves managed assemblies unchanged. It runs R8 once, +after managed trimming or ILC, then uses the resulting R8 mapping to +generate native runtime remapping tables. CoreCLR selects remaps from linked assemblies. NativeAOT selects remaps from retained JNI literals in ILC's native object and statically links the table afterward. diff --git a/Documentation/docs-mobile/messages/xa4327.md b/Documentation/docs-mobile/messages/xa4327.md index 19dc8698184..f4d523ba911 100644 --- a/Documentation/docs-mobile/messages/xa4327.md +++ b/Documentation/docs-mobile/messages/xa4327.md @@ -11,11 +11,7 @@ f1_keywords: ## Example messages ``` -error XA4327: Failed to generate the R8 JNI remapping data. The R8 seed mapping file 'obj/Release/net11.0/android-arm64/r8-jni-seed/mapping.txt' was not found. -``` - -``` -error XA4327: Failed to generate the R8 JNI remapping data. The Android manifest 'obj/Release/net11.0/android/AndroidManifest.xml' does not have a element with a 'package' attribute. +error XA4327: Failed to generate the R8 JNI remapping data. The R8 mapping file 'obj/Release/net11.0/android-arm64/r8-jni-final-mapping.txt' was not found. ``` ## Issue @@ -25,12 +21,11 @@ original JNI names in the managed assemblies into the names R8 chose. This only happens when R8 obfuscation is enabled with `$(AndroidEnableR8Obfuscation)=true` and -`$(AndroidR8ObfuscationMode)=runtime-remapping`. The remapping is built from a -naming-only "seed" R8 pass that runs before trimming; the error means either -that pass did not produce a usable mapping file, or the merged -`AndroidManifest.xml` it depends on could not be read. On NativeAOT, this also -reports a missing or invalid ILC native object: remapping data is selected from -the surviving JNI literals in that object before the final native link. +`$(AndroidR8ObfuscationMode)=runtime-remapping`. The remapping is built from the +mapping file produced by the final R8 pass after managed trimming or ILC. On +NativeAOT, this also reports a missing or invalid ILC native object: remapping +data is selected from the surviving JNI literals in that object before the final +native link. NativeAOT filtering supports normal generated JNI bindings whose class names, member names, and descriptors are literal strings. It inspects the initialized @@ -44,9 +39,8 @@ affected Java types and members. The message names the specific file that is missing or unreadable. -* Build with `-v:diag` (or check the binary log) for the output of the seed R8 - pass that should have produced the mapping file, and address any failure it - reports. +* Build with `-v:diag` (or check the binary log) for the output of the final R8 + pass that should have produced the mapping file, and address any failure it reports. * Delete the `obj` directory and rebuild if the intermediate output is in an inconsistent state. * For NativeAOT, ensure ILC completed and its `NativeObject` output exists before diff --git a/Documentation/docs-mobile/messages/xa4328.md b/Documentation/docs-mobile/messages/xa4328.md index 1cbf713afd2..5b52b542b99 100644 --- a/Documentation/docs-mobile/messages/xa4328.md +++ b/Documentation/docs-mobile/messages/xa4328.md @@ -16,7 +16,7 @@ warning XA4328: The R8 JNI remapping data is incomplete. The 'replace-type' entr ## Issue -The R8 JNI runtime remapping is generated from the R8 seed mapping file and is +The R8 JNI runtime remapping is generated from the final R8 mapping file and is merged with every other JNI remapping input in the build, such as the Intune (MAM) mapping. @@ -37,8 +37,8 @@ Only one remapping input can own a given type or member. * If the app uses the Intune (MAM) mapping, exclude the affected types from the R8 renaming with a `-keep` rule in a `@(ProguardConfiguration)` file so the - seed R8 pass does not rename them. + final R8 pass does not rename them. * If the conflict is unexpected, [report an issue][report-issue] and include the - full warning, the R8 seed mapping file, and the other remapping input. + full warning, the final R8 mapping file, and the other remapping input. [report-issue]: https://github.com/dotnet/android/issues/new/choose diff --git a/Documentation/docs-mobile/messages/xa4329.md b/Documentation/docs-mobile/messages/xa4329.md index 05d587c7a14..a6ba5febb29 100644 --- a/Documentation/docs-mobile/messages/xa4329.md +++ b/Documentation/docs-mobile/messages/xa4329.md @@ -33,6 +33,8 @@ or NativeAOT. The `experimental-rewriting` value is reserved for a separate implementation whose build pipeline is not included in this SDK. It does not fall back to runtime remapping. Setting a mode alone does not enable obfuscation. +Runtime remapping does not rewrite managed assemblies; it uses the final R8 +mapping to generate runtime lookup tables after trimming or ILC. See [AndroidEnableR8Obfuscation](../building-apps/build-properties.md#androidenabler8obfuscation) and [AndroidR8ObfuscationMode](../building-apps/build-properties.md#androidr8obfuscationmode). diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets index bb9999a24f1..ef66d4f8a46 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets @@ -81,8 +81,6 @@ properties that determine build ordering. $(AfterGenerateAndroidManifest); _ReadAndroidManifest; _CompileJava; - _CreateApplicationSharedLibraries; - $(_NativeRuntimeLinking); _CompileDex; $(_AfterCompileDex); _CreateBaseApk; diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.After.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.After.targets index 3413a3e713f..957cad3120b 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.After.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.After.targets @@ -21,4 +21,6 @@ Imported from Microsoft.Android.Sdk.After.targets. Condition=" '$(_AndroidRuntime)' == 'NativeAOT' " DependsOnTargets="IlcCompile" /> + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.R8.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.R8.targets new file mode 100644 index 00000000000..fb081ab2363 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.R8.targets @@ -0,0 +1,48 @@ + + + + + + <_ComputeFilesToPublishDependsOn>$([MSBuild]::Unescape($(_ComputeFilesToPublishDependsOn.Replace('NativeCompile;', '')))) + <_AndroidRunNativeCompileDependsOn>_ComputeAssembliesToCompileToNative;IlcCompile + + <_AndroidRunNativeCompileDependsOn Condition=" '$(_AndroidNativeAotLinkAfterR8)' == 'true' ">_ComputeAssembliesToCompileToNative;SetupOSSpecificProps + + + + + <_AndroidNativeAotLinkedFileToPublish Include="@(ResolvedFileToPublish)" + Condition=" '%(ResolvedFileToPublish.Identity)' == '$(_AndroidNativeAotSharedLibrary)' "> + $(_AndroidNativeAotR8RemappingDirectory) + + + + + + + + + + + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets index 7424ec4db2a..f09ccca6a0c 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets @@ -262,15 +262,23 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. libs into a .so. LinkNative is overridden as a no-op in Microsoft.Android.Sdk.After.targets (which is imported after the ILC NuGet targets). --> - + <_AndroidNativeAotSharedLibrary>$(NativeOutputPath)$(NativeBinaryPrefix)$(TargetName).so + <_AndroidNativeAotSharedLibrarySymbols Condition=" '$(AndroidIncludeDebugSymbols)' != 'true' ">$(NativeOutputPath)$(NativeBinaryPrefix)$(TargetName).dbg.so + + <_NdkLibs Include="@(RuntimePackAsset->WithMetadataValue('Filename', 'libnaot-android.release-static-release'))" /> + + + @@ -359,7 +367,7 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. - + @@ -388,13 +396,34 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. - + + $(_AndroidNativeAotSharedLibraryName) PreserveNewest + $([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(NativeObject)')) + $(NativeIntermediateOutputPath) + $([System.IO.Path]::ChangeExtension('$(_AndroidNativeAotSharedLibrary)', '.dbg.so')) + + + + <_AndroidNativeAotFileToPublish Include="@(ResolvedFileToPublish)" + Condition=" '%(ResolvedFileToPublish.AndroidNativeAotObjectFile)' != '' " /> + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.R8JniRemapping.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.R8JniRemapping.targets index 335b874a164..aaa2756cd94 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.R8JniRemapping.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.R8JniRemapping.targets @@ -1,238 +1,62 @@ - - - - - - <_AndroidR8JniSeedDirectory>$(_TypeMapBaseOutputDir)r8-jni-seed/ - <_AndroidR8JniSeedMapping>$(_AndroidR8JniSeedDirectory)mapping.txt - <_AndroidR8JniManifestProguardConfiguration>$(_AndroidR8JniSeedDirectory)manifest_rules.txt - <_AndroidR8JniSeedXamarinConfiguration>$(_AndroidR8JniSeedDirectory)xamarin.cfg - <_AndroidR8JniSeedJavaClassDirectory>$(_AndroidR8JniSeedDirectory)classes/ - <_AndroidR8JniSeedJavaStamp>$(_AndroidR8JniSeedDirectory)compile-java.stamp - <_AndroidR8JniRemappingXml>$(_TypeMapBaseOutputDir)r8-jni-remap.xml - - <_PreTrimTypeMapAcwMapOutputFile Condition=" '$(_AndroidRuntime)' == 'CoreCLR' and '$(_PreTrimTypeMapAcwMapOutputFile)' == '' ">$(_AndroidR8JniSeedDirectory)acw-map.txt - <_PreTrimTypeMapApplicationRegistrationOutputFile Condition=" '$(_AndroidRuntime)' == 'CoreCLR' and '$(_PreTrimTypeMapApplicationRegistrationOutputFile)' == '' ">$(_AndroidR8JniSeedDirectory)java/net/dot/android/ApplicationRegistration.java - <_AndroidR8JniSeedAcwMap>$(_PreTrimTypeMapAcwMapOutputFile) - <_AndroidR8JniSeedApplicationRegistration>$(_PreTrimTypeMapApplicationRegistrationOutputFile) - <_AndroidR8JniSeedAcwMap Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">$(_TypeMapBaseOutputDir)acw-map.txt - <_AndroidR8JniSeedApplicationRegistration Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">$(_TypeMapBaseOutputDir)android/src/net/dot/android/ApplicationRegistration.java - - - - - <_AndroidR8JniSeedJavaSource Include="$(_TypeMapJavaOutputDirectory)/**/*.java" /> - <_AndroidR8JniSeedJavaSource Include="$(_AndroidR8JniSeedApplicationRegistration)" - Condition=" '$(_AndroidR8JniSeedApplicationRegistration)' != '' " /> - - - - - - - - - - - - - - - - - - - - - <_AndroidR8JniMergedManifest>$(_TypeMapBaseOutputDir)AndroidManifest.xml - <_AndroidR8JniMergedManifest Condition=" '$(AndroidManifestMerger)' == 'manifestmerger.jar' ">$(IntermediateOutputPath)android/AndroidManifest.xml - - - - - - - - - - - - - + <_AndroidR8JniTaskAssembly>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', '$(_XamarinAndroidBuildTasksAssembly)')) - - <_AndroidR8JniSeedClassFile Include="$(_AndroidR8JniSeedJavaClassDirectory)**\*.class" /> - <_AndroidR8JniSeedManifestProguardConfiguration - Include="$(_AndroidR8JniManifestProguardConfiguration)" - Condition=" '$(ProguardConfigFiles)' == '' and Exists('$(_AndroidR8JniManifestProguardConfiguration)') " /> - - <_AndroidR8JniSeedProguardConfiguration Include="$(ProguardConfigFiles)" Condition=" '$(ProguardConfigFiles)' != '' " /> - - <_AndroidR8JniSeedProguardConfiguration - Include="$(MSBuildThisFileDirectory)..\tools\proguard-android.txt" - Condition=" '$(ProguardConfigFiles)' == '' " /> - <_AndroidR8JniSeedProguardConfiguration - Include="@(ProguardConfiguration)" - Condition=" '$(ProguardConfigFiles)' == '' and '%(ProguardConfiguration.AndroidGeneratedProguardConfiguration)' != 'true' " /> - <_AndroidR8JniSeedProguardConfiguration - Include="@(_AndroidR8JniSeedManifestProguardConfiguration)" - Condition=" '$(ProguardConfigFiles)' == '' " /> - <_AndroidR8JniSeedMapDiagnostics Condition=" '$(AndroidR8IgnoreWarnings)' == 'true' " Include="warning" To="info" /> - - - - - - - - - - - - - - - - - - - - <_AndroidR8JniExistingRemapMembers Include="@(_AndroidRemapMembers)" - Condition=" '%(Identity)' != '$(_AndroidR8JniRemappingXml)' " /> - + DependsOnTargets="_AndroidR8JniRemappingInputs;_ConvertAndroidMamMappingFileToXml;_AndroidPrepareR8JniRemappingAssemblies"> + + <_AndroidR8JniGeneratedRemappingXml>$(IntermediateOutputPath)r8-jni-generated-remap.xml + <_AndroidR8JniRemappingXml>$(IntermediateOutputPath)r8-jni-remap.xml + - + Inputs="$(_AndroidR8JniMappingFile);@(_AndroidRemapMembers);@(_AndroidR8JniRemappingAssembly);$(_AndroidBuildPropertiesCache);$(_AndroidR8JniTaskAssembly)" + Outputs="$(_AndroidR8JniGeneratedRemappingXml);$(_AndroidR8JniRemappingXml)"> + - - - <_AndroidRemapMembers Include="$(_AndroidR8JniRemappingXml)" /> - + + + + + + + + + + + - + + DependsOnTargets="_AndroidR8JniRemappingInputs;_ConvertAndroidMamMappingFileToXml;_PrepareNativeAotAndroidAppInputs"> <_AndroidNativeAotR8RemappingDirectory>$(NativeIntermediateOutputPath)jni-remap/ <_AndroidNativeAotR8GeneratedRemappingXml>$(_AndroidNativeAotR8RemappingDirectory)r8-jni-generated-remap.xml @@ -255,10 +79,10 @@ - + diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 5e8cbb2a2c9..c2b89a3482a 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1987,25 +1987,16 @@ public static string XA4327 { } /// - /// Looks up a localized string similar to The seed R8 pass requires a mapping file output.. + /// Looks up a localized string similar to The R8 mapping file '{0}' was not found.. /// - public static string XA4327_SeedMappingOutputRequired { + public static string XA4327_MappingNotFound { get { - return ResourceManager.GetString("XA4327_SeedMappingOutputRequired", resourceCulture); + return ResourceManager.GetString("XA4327_MappingNotFound", resourceCulture); } } /// - /// Looks up a localized string similar to The R8 seed mapping file '{0}' was not found.. - /// - public static string XA4327_SeedMappingNotFound { - get { - return ResourceManager.GetString("XA4327_SeedMappingNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The R8 seed mapping file '{0}' could not be read: {1}. + /// Looks up a localized string similar to The R8 mapping file '{0}' could not be read: {1}. /// public static string XA4327_MappingDataFailure { get { @@ -2013,15 +2004,6 @@ public static string XA4327_MappingDataFailure { } } - /// - /// Looks up a localized string similar to The Android manifest '{0}' could not be read: {1}. - /// - public static string XA4327_ManifestReadFailure { - get { - return ResourceManager.GetString("XA4327_ManifestReadFailure", resourceCulture); - } - } - /// /// Looks up a localized string similar to NativeAOT JNI retention requires an existing post-ILC NativeAotObjectFile; '{0}' was not found.. /// @@ -2085,15 +2067,6 @@ public static string XA4327_NativeAotMissingSections { } } - /// - /// Looks up a localized string similar to The Android manifest '{0}' does not have a <manifest> element with a 'package' attribute.. - /// - public static string XA4327_ManifestPackageMissing { - get { - return ResourceManager.GetString("XA4327_ManifestPackageMissing", resourceCulture); - } - } - /// /// Looks up a localized string similar to The R8 JNI remapping data is incomplete. {0}. /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index f33261845c0..2b86883268c 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -906,26 +906,16 @@ Remove the '{0}' reference from your project and add the '{1}' NuGet package ins The following are literal names and should not be translated: R8, JNI. {0} - A sentence describing the specific failure. It is supplied by one of the XA4327_* resources. - - The seed R8 pass requires a mapping file output. - The following are literal names and should not be translated: R8. - - - The R8 seed mapping file '{0}' was not found. + + The R8 mapping file '{0}' was not found. The following are literal names and should not be translated: R8. -{0} - The path of the missing seed mapping file. +{0} - The path of the missing mapping file. - The R8 seed mapping file '{0}' could not be read: {1} + The R8 mapping file '{0}' could not be read: {1} The following are literal names and should not be translated: R8. -{0} - The path of the seed mapping file. +{0} - The path of the mapping file. {1} - The underlying message describing why the file could not be read. It is not localized. - - - The Android manifest '{0}' could not be read: {1} - The following are literal names and should not be translated: Android. -{0} - The path of the Android manifest. -{1} - The underlying message describing why the manifest could not be read. It is not localized. NativeAOT JNI retention requires an existing post-ILC NativeAotObjectFile; '{0}' was not found. @@ -958,11 +948,6 @@ Remove the '{0}' reference from your project and add the '{1}' NuGet package ins The NativeAOT object must contain allocated __managedcode and initialized data sections. The following are literal names and should not be translated: NativeAOT, __managedcode. - - The Android manifest '{0}' does not have a <manifest> element with a 'package' attribute. - The following are literal names and should not be translated: Android, <manifest>, 'package'. -{0} - The path of the Android manifest. - The R8 JNI remapping data is incomplete. {0} The following are literal names and should not be translated: R8, JNI. diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniManifestProguardConfiguration.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniManifestProguardConfiguration.cs deleted file mode 100644 index ea2ca22295c..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniManifestProguardConfiguration.cs +++ /dev/null @@ -1,105 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Xml; -using System.Xml.Linq; - -using Microsoft.Android.Build.Tasks; -using Microsoft.Build.Framework; - -namespace Xamarin.Android.Tasks; - -/// -/// Emits keep rules for the types the merged AndroidManifest.xml names, so the naming-only -/// seed R8 pass cannot rename them. The final R8 pass keeps the same names via the AAPT-generated -/// rules, so pinning them up front is what keeps the seed mapping applicable with -/// -applymapping. -/// -public sealed class GenerateR8JniManifestProguardConfiguration : AndroidTask -{ - static readonly XNamespace AndroidNamespace = "http://schemas.android.com/apk/res/android"; - - public override string TaskPrefix => "GRJMPC"; - - [Required] - public string AndroidManifestFile { get; set; } = ""; - - [Required] - public string OutputFile { get; set; } = ""; - - public override bool RunTask () - { - XDocument manifest; - try { - manifest = XDocument.Load (AndroidManifestFile, LoadOptions.None); - } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is XmlException) { - LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_ManifestReadFailure, AndroidManifestFile, ex.Message)); - return false; - } - - XElement? root = manifest.Root; - string? packageName = root?.Attribute ("package")?.Value; - if (root?.Name.LocalName != "manifest" || packageName.IsNullOrWhiteSpace ()) { - LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_ManifestPackageMissing, AndroidManifestFile)); - return false; - } - - var classes = new SortedSet (StringComparer.Ordinal); - foreach (XElement element in root.DescendantsAndSelf ()) { - switch (element.Name.LocalName) { - case "application": - AddClass (classes, packageName, element, "name"); - AddClass (classes, packageName, element, "backupAgent"); - AddClass (classes, packageName, element, "appComponentFactory"); - AddClass (classes, packageName, element, "zygotePreloadName"); - break; - case "activity": - case "service": - case "receiver": - case "provider": - case "instrumentation": - case "process": - AddClass (classes, packageName, element, "name"); - break; - case "activity-alias": - // android:name on an is an alias, not a real type; only the - // targetActivity names a class that must survive with its name intact. - AddClass (classes, packageName, element, "targetActivity"); - break; - } - } - - string content = string.Join ("\n", classes.Select (name => $"-keep class {name} {{ (); }}")); - if (content.Length > 0) { - content += "\n"; - } - - string? directory = Path.GetDirectoryName (OutputFile); - if (!directory.IsNullOrEmpty ()) { - Directory.CreateDirectory (directory); - } - File.WriteAllText (OutputFile, content, Files.UTF8withoutBOM); - return !Log.HasLoggedErrors; - } - - static void AddClass (ISet classes, string packageName, XElement element, string attributeName) - { - string? value = element.Attribute (AndroidNamespace + attributeName)?.Value; - if (value.IsNullOrWhiteSpace () || value [0] == '@' || value [0] == '?') { - return; - } - if (value [0] == '.') { - classes.Add (packageName + value); - } else if (value.IndexOf ('.') < 0) { - classes.Add (packageName + "." + value); - } else { - classes.Add (value); - } - } - - void LogR8JniRemappingError (string detail) => - Log.LogCodedError ("XA4327", Properties.Resources.XA4327, detail); -} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs index d5c636c2be2..527d0c8a794 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs @@ -16,9 +16,8 @@ namespace Xamarin.Android.Tasks { /// - /// Converts the naming-only R8 seed mapping.txt into a JNI remapping XML document that - /// the existing @(_AndroidRemapMembers) -> MergeRemapXml -> - /// GenerateJniRemappingNativeCode pipeline consumes. + /// Converts the final R8 mapping.txt into a JNI remapping XML document that + /// the existing MergeRemapXml and GenerateJniRemappingNativeCode tasks consume. /// /// Managed assemblies are *not* rewritten on this path, so they keep the original JNI names. /// The generated document is what teaches the runtime how those original names map onto the @@ -48,7 +47,7 @@ public class GenerateR8JniRemapping : AndroidTask { public override string TaskPrefix => "GR8JR"; - /// The naming-only R8 seed mapping file. + /// The final R8 mapping file. [Required] public string MappingFile { get; set; } = ""; @@ -81,7 +80,7 @@ public class GenerateR8JniRemapping : AndroidTask public override bool RunTask () { if (!File.Exists (MappingFile)) { - LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_SeedMappingNotFound, MappingFile)); + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_MappingNotFound, MappingFile)); return false; } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs index a66a3c70e98..34f0c55565f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -35,17 +35,6 @@ public class R8 : D8 public string? ProguardCommonXamarinConfiguration { get; set; } public string? ProguardMappingFileOutput { get; set; } - /// - /// A mapping file applied with -applymapping, so this R8 run reproduces the names an - /// earlier (seed) run chose. - /// - public string? ProguardMappingFileInput { get; set; } - - /// - /// Runs R8 as a naming-only seed pass: no tree shaking, no optimization, mapping output only. - /// - public bool GenerateSeedMapping { get; set; } - /// /// Allows R8 to rename types and members by omitting the SDK-generated /// -dontobfuscate, and by letting the generated Java Callable Wrapper keep rules @@ -68,10 +57,6 @@ public class R8 : D8 public override bool RunTask () { try { - if (GenerateSeedMapping && ProguardMappingFileOutput.IsNullOrEmpty ()) { - Log.LogCodedError ("XA4327", Properties.Resources.XA4327, Properties.Resources.XA4327_SeedMappingOutputRequired); - return false; - } return base.RunTask (); } finally { foreach (var temp in tempFiles) { @@ -182,26 +167,7 @@ protected override string CreateResponseFile () } } - if (GenerateSeedMapping) { - // Naming-only seed pass: choose the names, keep everything else intact. The mapping - // this produces is applied to the final R8 run with -applymapping. - WriteArg (response, "--no-tree-shaking"); - var seedConfiguration = new List { - "-dontoptimize", - "-dontpreverify", - "-keepattributes **", - $"-printmapping \"{Path.GetFullPath (GetRequiredSeedMappingOutput ())}\"", - }; - if (IgnoreWarnings) { - seedConfiguration.Add ("-ignorewarnings"); - } - WriteConfiguration (response, seedConfiguration); - GenerateCommonXamarinConfiguration (); - if (!ProguardCommonXamarinConfiguration.IsNullOrEmpty ()) { - WriteArg (response, "--pg-conf"); - WriteArg (response, ProguardCommonXamarinConfiguration); - } - } else if (EnableShrinking) { + if (EnableShrinking) { if (UseTrimmableNativeAotProguardConfiguration && !ProguardGeneratedApplicationConfiguration.IsNullOrEmpty ()) { // ACW keep rules come from the DGML/acw-map-driven proguard_project_references.cfg on // the trimmable path. User-authored AndroidJavaSource (Bind != true) has no managed peer @@ -256,11 +222,6 @@ protected override string CreateResponseFile () WriteArg (response, "--pg-conf"); WriteArg (response, temp); } - if (!ProguardMappingFileInput.IsNullOrEmpty ()) { - WriteConfiguration (response, new [] { - $"-applymapping \"{Path.GetFullPath (ProguardMappingFileInput)}\"", - }); - } if (ProguardConfigurationFiles != null) { foreach (var item in ProguardConfigurationFiles) { var file = item.ItemSpec; @@ -284,19 +245,10 @@ protected override string CreateResponseFile () /// /// The keep option used for the generated Java Callable Wrapper keep rules. When the JNI /// names are remapped at runtime the wrappers must survive shrinking but stay renameable, - /// otherwise a plain -keep pins their names and -applymapping has no effect. + /// otherwise a plain -keep pins their names and prevents obfuscation. /// internal string KeepOption => EnableObfuscation ? "-keep,allowobfuscation" : "-keep"; - string GetRequiredSeedMappingOutput () - { - string? output = ProguardMappingFileOutput; - if (output.IsNullOrEmpty ()) { - throw new InvalidOperationException (Properties.Resources.XA4327_SeedMappingOutputRequired); - } - return output; - } - internal void GenerateCommonXamarinConfiguration () { if (ProguardCommonXamarinConfiguration.IsNullOrWhiteSpace ()) { @@ -326,15 +278,6 @@ internal void GenerateCommonXamarinConfiguration () } } - void WriteConfiguration (StreamWriter response, IEnumerable lines) - { - var temp = Path.GetTempFileName (); - File.WriteAllLines (temp, lines); - tempFiles.Add (temp); - WriteArg (response, "--pg-conf"); - WriteArg (response, temp); - } - // ProGuard "global" options that affect the whole build and are not allowed inside // a library's proguard.txt (the file packaged inside an .aar's root). AGP 9.0 // introduced the same restriction — see "Behavior changes" in the AGP 9.0 release diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 34713cb595c..b16f12170e7 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1699,7 +1699,7 @@ because xbuild doesn't support framework reference assemblies. + + + - <_AndroidGenerateR8JniRemappingDependsOn - Condition=" '$(AndroidTypeMapImplementation)' == 'trimmable' ">_AndroidGenerateR8JniRemapping <_GenerateAndroidRemapNativeCodeDependsOn> _ConvertAndroidMamMappingFileToXml; - $(_AndroidGenerateR8JniRemappingDependsOn); _CollectAndroidRemapMembers; _PrepareAndroidRemapNativeAssemblySources; _GetGeneratePackageManagerJavaInputs @@ -1724,7 +1726,7 @@ because xbuild doesn't support framework reference assemblies. <_CompileToDalvikDependsOnTargets> _CompileJava; - _CreateApplicationSharedLibraries; - $(_NativeRuntimeLinking); + _AndroidLinkBeforeR8; $(_BeforeCompileToDalvik); _GetLibraryImports; _SetProguardMappingFileProperty; @@ -2021,9 +2023,18 @@ because xbuild doesn't support framework reference assemblies. <_CompileDexDependsOn> _CompileToDalvik; + _AndroidLinkAfterR8; + + + + $(OutputPath)mapping.txt @@ -2031,6 +2042,7 @@ because xbuild doesn't support framework reference assemblies. <_AndroidR8ProguardMappingFileOutput Condition=" '$(_AndroidR8RuntimeRemappingEnabled)' == 'true' And '$(_AndroidR8ProguardMappingFileOutput)' == '' ">$(IntermediateOutputPath)r8-jni-final-mapping.txt + <_AndroidR8JniMappingFile Condition=" '$(_AndroidR8RuntimeRemappingEnabled)' == 'true' ">$([System.IO.Path]::GetFullPath('$(_AndroidR8ProguardMappingFileOutput)')) @@ -2039,16 +2051,11 @@ because xbuild doesn't support framework reference assemblies. <_CompileToDalvikInputs> $(_CompileToDalvikInputs) - ;$(_AndroidR8JniSeedMapping) - ;$(_AndroidR8JniManifestProguardConfiguration) ;$(_ProguardProjectConfiguration) ;@(ProguardConfiguration) ;$(AndroidR8JarPath) - - - @@ -2061,8 +2068,6 @@ because xbuild doesn't support framework reference assemblies. <_ProguardConfiguration Include="$(IntermediateOutputPath)proguard\proguard_xamarin.cfg" Condition=" '$(AndroidLinkTool)' != '' " /> <_ProguardConfiguration Include="$(_ProguardProjectConfiguration)" Condition=" '$(AndroidLinkTool)' != '' " /> <_ProguardConfiguration Include="$(IntermediateOutputPath)proguard\proguard_project_primary.cfg" Condition=" '$(AndroidLinkTool)' != '' " /> - <_ProguardConfiguration Include="$(_AndroidR8JniManifestProguardConfiguration)" - Condition=" '$(_AndroidR8RuntimeRemappingEnabled)' == 'true' and Exists('$(_AndroidR8JniManifestProguardConfiguration)') " /> <_ProguardConfiguration Include="@(ProguardConfiguration)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets index 3001b728242..76789268f8f 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets @@ -20,7 +20,7 @@ Copyright (C) 2018 Xamarin. All rights reserved. + Outputs="$(_AndroidStampDirectory)_CompileToDalvik.stamp;$(_AndroidR8JniMappingFile)"> @@ -78,7 +78,6 @@ Copyright (C) 2018 Xamarin. All rights reserved. ProguardGeneratedReferenceConfiguration="$(_ProguardProjectConfiguration)" ProguardGeneratedApplicationConfiguration="$(IntermediateOutputPath)proguard\proguard_project_primary.cfg" ProguardMappingFileOutput="$(_AndroidR8ProguardMappingFileOutput)" - ProguardMappingFileInput="$(_AndroidR8JniSeedMapping)" EnableObfuscation="$(_AndroidR8RuntimeRemappingEnabled)" BuildMetadataFileOutput="$(_AndroidR8BuildMetadataFile)" ProguardConfigurationFiles="@(_ProguardConfiguration)" diff --git a/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs new file mode 100644 index 00000000000..19d56f41b9b --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs @@ -0,0 +1,70 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using Microsoft.Build.Logging.StructuredLogger; +using NUnit.Framework; +using Xamarin.Android.Tasks; +using Xamarin.ProjectTools; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + public class R8RuntimeRemappingBuildTests : BaseTest + { + [TestCase (AndroidRuntime.CoreCLR, false)] + [TestCase (AndroidRuntime.NativeAOT, false)] + [TestCase (AndroidRuntime.NativeAOT, true)] + public void MultiRidUsesOneR8Mapping (AndroidRuntime runtime, bool explicitPrimaryRid) + { + if (IgnoreUnsupportedConfiguration (runtime, release: true)) { + return; + } + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + }; + proj.SetRuntime (runtime); + proj.SetRuntimeIdentifiers (new [] { "arm64-v8a", "x86_64" }); + if (explicitPrimaryRid) { + proj.SetProperty ("RuntimeIdentifier", "android-arm64"); + } + proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + proj.SetProperty ("AndroidLinkTool", "r8"); + proj.SetProperty ("AndroidEnableR8Obfuscation", "true"); + proj.SetProperty ("AndroidCreateProguardMappingFile", "false"); + proj.SetProperty ("AndroidPackageFormats", "apk"); + + using var builder = CreateApkBuilder (); + (int R8, int NativeLinks) ReadInvocationCounts () + { + var build = BinaryLog.ReadBuild (Path.Combine (Root, builder.ProjectDirectory, + $"{Path.GetFileNameWithoutExtension (builder.BuildLogFile)}.binlog")); + var tasks = build.FindChildrenRecursive ().ToList (); + return (tasks.Count (t => t.Name == "R8"), tasks.Count (t => t.Name == "LinkNativeAotSharedLibrary")); + } + + Assert.IsTrue (builder.Build (proj), "Both RIDs should build from the same final R8 mapping."); + var first = ReadInvocationCounts (); + Assert.AreEqual (1, first.R8); + var intermediate = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath); + var maps = Directory.GetFiles (intermediate, "r8-jni-final-mapping.txt", SearchOption.AllDirectories); + Assert.AreEqual (1, maps.Length, "R8 output must be shared, not regenerated for each RID."); + if (runtime == AndroidRuntime.NativeAOT) { + Assert.AreEqual (2, first.NativeLinks, "Each RID should link once, after R8."); + Assert.AreEqual (2, Directory.GetFiles (intermediate, "r8-jni-remap.xml", SearchOption.AllDirectories).Length); + using var apk = ZipFile.OpenRead (Path.Combine (Root, builder.ProjectDirectory, + proj.OutputPath, $"{proj.PackageName}-Signed.apk")); + foreach (var abi in new [] { "arm64-v8a", "x86_64" }) { + Assert.IsNotNull (apk.GetEntry ($"lib/{abi}/lib{proj.ProjectName}.so"), $"Missing final {abi} native library."); + } + } + var objects = Directory.GetFiles (intermediate, $"{proj.ProjectName}.o", SearchOption.AllDirectories) + .ToDictionary (path => path, File.GetLastWriteTimeUtc); + Assert.IsTrue (builder.Build (proj), "A multi-RID no-op build should succeed."); + Assert.AreEqual ((0, 0), ReadInvocationCounts (), "No-op builds must not run R8 or native linking."); + foreach (var entry in objects) { + Assert.AreEqual (entry.Value, File.GetLastWriteTimeUtc (entry.Key), "No-op builds must not recompile ILC."); + } + } + } +} diff --git a/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs index 7b5080e08e1..234c7cd6d7f 100644 --- a/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Xml.Linq; +using Microsoft.Build.Logging.StructuredLogger; using NUnit.Framework; using Xamarin.Android.Tasks; using Xamarin.ProjectTools; @@ -13,6 +14,24 @@ namespace Xamarin.Android.Build.Tests [Category ("UsesDevice")] public class R8RuntimeRemappingTests : DeviceTest { + void AssertR8Invocations (ProjectBuilder builder, int expected, bool obfuscationEnabled = true) + { + var binlog = Path.Combine (Root, builder.ProjectDirectory, $"{Path.GetFileNameWithoutExtension (builder.BuildLogFile)}.binlog"); + var build = BinaryLog.ReadBuild (binlog); + var tasks = build.FindChildrenRecursive ().ToList (); + var r8 = tasks.Where (t => t.Name == "R8").ToList (); + Assert.AreEqual (expected, r8.Count, $"Unexpected R8 invocation count in {binlog}."); + if (expected != 1 || !obfuscationEnabled) { + return; + } + foreach (var trimming in build.FindChildrenRecursive (t => t.Name == "_RunILLink" || t.Name == "IlcCompile")) { + Assert.LessOrEqual (trimming.EndTime, r8 [0].StartTime, "R8 must run after managed trimming/ILC."); + } + foreach (var link in tasks.Where (t => t.Name == "LinkNativeRuntime" || t.Name == "LinkApplicationSharedLibraries" || t.Name == "LinkNativeAotSharedLibrary")) { + Assert.GreaterOrEqual (link.StartTime, r8 [0].EndTime, "Native linking must consume the final R8 mapping."); + } + } + [TestCase (AndroidRuntime.CoreCLR)] [TestCase (AndroidRuntime.NativeAOT)] public void ObfuscatedMembersRun (AndroidRuntime runtime) @@ -62,6 +81,11 @@ public HiddenPeer () {} proj.SetProperty ("AllowUnsafeBlocks", "true"); proj.SetProperty ("TrimMode", "full"); proj.SetProperty ("AndroidEnableR8Obfuscation", "true"); + proj.SetProperty ("AndroidCreateProguardMappingFile", "false"); + string extraRules = ""; + proj.OtherBuildItems.Add (new AndroidItem.ProguardConfiguration ("r8-custom.pro") { + TextContent = () => extraRules, + }); if (runtime == AndroidRuntime.NativeAOT) { proj.SetProperty ("AndroidR8ObfuscationMode", "runtime-remapping"); } @@ -128,6 +152,7 @@ void AssertAppRuns (string logFile) timeout: 30), "Constructors, overloads, fields, and peer return values should work."); } Assert.IsTrue (builder.Install (proj), "Obfuscated app should build and install."); + AssertR8Invocations (builder, 1); try { var intermediate = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath); var remapFiles = Directory.GetFiles (intermediate, "r8-jni-remap.xml", SearchOption.AllDirectories); @@ -157,11 +182,16 @@ void AssertAppRuns (string logFile) AssertAppRuns ("r8-runtime-remap.log"); + Assert.IsTrue (builder.Build (proj), "A no-op build should succeed."); + AssertR8Invocations (builder, 0); + Assert.IsTrue (builder.Output.IsTargetSkipped ("_CompileToDalvik")); + if (runtime == AndroidRuntime.NativeAOT) { var aaptRules = Path.Combine (intermediate, "aapt_rules.txt"); FileAssert.Exists (aaptRules); var originalAaptRules = File.ReadAllText (aaptRules); Assert.IsTrue (builder.Build (proj), "A no-op build should succeed."); + AssertR8Invocations (builder, 0); Assert.IsTrue (builder.Output.IsTargetSkipped ("_AndroidGenerateNativeAotR8Remapping")); Assert.IsTrue (builder.Output.IsTargetSkipped ("_AndroidCompileNativeAotR8Remapping")); Assert.IsTrue (builder.Output.IsTargetSkipped ("_AndroidLinkNativeAotSharedLibrary")); @@ -173,6 +203,7 @@ void AssertAppRuns (string logFile) var remapObject = Directory.GetFiles (intermediate, $"jni_remap.{DeviceAbi}.o", SearchOption.AllDirectories).Single (); File.Delete (remapObject); Assert.IsTrue (builder.Build (proj), "A missing remapping object should be regenerated."); + AssertR8Invocations (builder, 0); FileAssert.Exists (remapObject); Assert.AreEqual (ilcTimestamp, File.GetLastWriteTimeUtc (ilcObject), "Recovering the late-linked table must not recompile IL."); Assert.IsFalse (builder.Output.IsTargetSkipped ("_AndroidCompileNativeAotR8Remapping")); @@ -180,13 +211,28 @@ void AssertAppRuns (string logFile) File.Delete (aaptRules); Assert.IsTrue (builder.Build (proj), "Missing resource keep rules should be regenerated."); + AssertR8Invocations (builder, 1); FileAssert.Exists (aaptRules); Assert.AreEqual (originalAaptRules, File.ReadAllText (aaptRules)); Assert.IsFalse (builder.Output.IsTargetSkipped ("_CreateBaseApk")); } + var finalMapping = Path.Combine (intermediate, "r8-jni-final-mapping.txt"); + FileAssert.Exists (finalMapping); + File.Delete (finalMapping); + Assert.IsTrue (builder.Build (proj), "A missing final mapping must rerun R8, not reuse stale tables."); + AssertR8Invocations (builder, 1); + FileAssert.Exists (finalMapping); + + extraRules = "-keepclassmembernames class example.RuntimePeer { public int value; }"; + proj.Touch ("r8-custom.pro"); + Assert.IsTrue (builder.Install (proj), "Changed R8 rules must update the late-linked tables."); + AssertR8Invocations (builder, 1); + AssertAppRuns ("r8-changed-rules.log"); + proj.SetProperty ("AndroidEnableR8Obfuscation", "false"); Assert.IsTrue (builder.Install (proj), "Disabling obfuscation should rebuild and install the baseline."); + AssertR8Invocations (builder, 1, obfuscationEnabled: false); StringAssert.Contains ("-dontobfuscate", File.ReadAllText (Path.Combine (intermediate, "proguard", "proguard_xamarin.cfg"))); AssertAppRuns ("r8-disabled.log"); } finally { From 640a3dc8395d428dc2137586c6e6f9cb13d1f032 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 7 Sep 2026 17:10:55 +0200 Subject: [PATCH 04/14] [Xamarin.Android.Build.Tasks] Preserve unchanged ProGuard rules Use content-based writes and generation stamps so managed-only rebuilds do not unnecessarily rerun R8. Preserve missing-output recovery, no-op incrementality, and cleanup across per-RID builds, with regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 7 +- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 8 +- .../Tasks/GenerateProguardConfiguration.cs | 4 +- .../Xamarin.Android.Common.targets | 11 +++ .../Tests/R8RuntimeRemappingBuildTests.cs | 94 +++++++++++++++++++ 5 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets index 07fabaf1631..f9bf8c7b4ab 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets @@ -445,13 +445,18 @@ + Outputs="$(_ProguardProjectConfiguration).stamp"> + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 455bc9967ba..8a5c3767221 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -40,14 +40,18 @@ + Outputs="$(_ProguardProjectConfiguration).stamp"> + + + + + + + + + + + + + <_ProguardConfiguration Include="$(ProguardConfigFiles)" /> diff --git a/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs index 19d56f41b9b..10cb7d031d2 100644 --- a/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs @@ -2,6 +2,7 @@ using System.IO; using System.IO.Compression; using System.Linq; +using System.Text; using Microsoft.Build.Logging.StructuredLogger; using NUnit.Framework; using Xamarin.Android.Tasks; @@ -12,6 +13,99 @@ namespace Xamarin.Android.Build.Tests [TestFixture] public class R8RuntimeRemappingBuildTests : BaseTest { + [TestCase (true, "trimmable")] + [TestCase (false, "trimmable")] + [TestCase (false, "llvm-ir")] + public void UnchangedProguardRulesDoNotRerunR8 (bool obfuscation, string typeMap) + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { + return; + } + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + EnableDefaultItems = true, + OtherBuildItems = { + new AndroidItem.AndroidJavaSource ("Peer.java") { + Encoding = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false), + Metadata = { { "Bind", "True" } }, + TextContent = () => """ + package example; + public class Peer { + public int first () { return 1; } + public int second () { return 2; } + } + """, + }, + }, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetRuntimeIdentifiers (new [] { "arm64-v8a" }); + proj.SetProperty ("AndroidTypeMapImplementation", typeMap); + proj.SetProperty ("AndroidLinkTool", "r8"); + proj.SetProperty ("AndroidEnableR8Obfuscation", obfuscation.ToString ()); + proj.SetProperty ("AndroidPackageFormats", "apk"); + proj.SetProperty ("TrimMode", "full"); + proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", """ + using var peer = new Example.Peer (); + System.Console.WriteLine (peer.First ()); + """); + + using var builder = CreateApkBuilder (); + void AssertTaskCount (string task, int expected) + { + var build = BinaryLog.ReadBuild (Path.Combine (Root, builder.ProjectDirectory, + $"{Path.GetFileNameWithoutExtension (builder.BuildLogFile)}.binlog")); + Assert.AreEqual (expected, build.FindChildrenRecursive () + .Count (t => t.Name == task), $"Unexpected {task} invocation count."); + } + + Assert.IsTrue (builder.Build (proj)); + AssertTaskCount ("R8", 1); + var intermediate = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath); + var rules = Directory.GetFiles (intermediate, "proguard_project_references.cfg", SearchOption.AllDirectories).Single (); + var originalRules = File.ReadAllText (rules); + var originalTime = File.GetLastWriteTimeUtc (rules); + StringAssert.Contains ("first(...)", originalRules); + FileAssert.Exists (rules + ".stamp"); + + proj.MainActivity = proj.MainActivity.Replace ("peer.First ()", "peer.First () + 1"); + proj.Touch ("MainActivity.cs"); + Assert.IsTrue (builder.Build (proj), "A managed-only change should rebuild without running R8."); + AssertTaskCount ("Csc", 1); + AssertTaskCount ("GenerateProguardConfiguration", 1); + AssertTaskCount ("R8", 0); + Assert.AreEqual (originalRules, File.ReadAllText (rules)); + Assert.AreEqual (originalTime, File.GetLastWriteTimeUtc (rules)); + + Assert.IsTrue (builder.Build (proj)); + AssertTaskCount ("GenerateProguardConfiguration", 0); + AssertTaskCount ("R8", 0); + + File.Delete (rules); + Assert.IsTrue (builder.Build (proj), "A missing rule file must be restored even when the stamp exists."); + AssertTaskCount ("GenerateProguardConfiguration", 1); + AssertTaskCount ("R8", obfuscation ? 1 : 0); + Assert.AreEqual (originalRules, File.ReadAllText (rules)); + + proj.MainActivity = proj.MainActivity.Replace ("peer.First () + 1", "peer.Second ()"); + proj.Touch ("MainActivity.cs"); + Assert.IsTrue (builder.Build (proj), "Newly retained bindings must update the keep rules."); + AssertTaskCount ("GenerateProguardConfiguration", 1); + StringAssert.Contains ("second(...)", File.ReadAllText (rules)); + if (typeMap == "trimmable") { + Assert.AreNotEqual (originalRules, File.ReadAllText (rules)); + } else { + Assert.AreEqual (originalRules, File.ReadAllText (rules), "LLVM typemaps already retain both bound methods."); + } + if (obfuscation) { + AssertTaskCount ("R8", 1); + } + + Assert.IsTrue (builder.Clean (proj)); + Assert.IsFalse (File.Exists (rules), "Clean should remove the rules."); + Assert.IsFalse (File.Exists (rules + ".stamp"), "Clean should remove the generation stamp."); + } + [TestCase (AndroidRuntime.CoreCLR, false)] [TestCase (AndroidRuntime.NativeAOT, false)] [TestCase (AndroidRuntime.NativeAOT, true)] From 95c138bb0fbcb4f2ac225780a3e68dd1562f34ac Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 7 Sep 2026 17:10:56 +0200 Subject: [PATCH 05/14] [Xamarin.Android.Build.Tasks] Address R8 review feedback Replace null-forgiving operators in the native remapping tests with explicit assertion guards, and separate adjacent test attributes and mapping helper declarations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateJniRemappingNativeCodeTests.cs | 14 +++++--------- .../Xamarin.Android.Build.Tests/Tasks/R8Tests.cs | 3 ++- .../Utilities/JniRemapping/R8Mapping.cs | 3 ++- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs index 0518f753320..4f79ae9ae7f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs @@ -35,15 +35,13 @@ public void Setup () string TestDirectory { get { - Assert.IsNotNull (directory); - return directory!; + return directory ?? throw new AssertionException ("The test directory must be initialized."); } } List Errors { get { - Assert.IsNotNull (errors); - return errors!; + return errors ?? throw new AssertionException ("The build error collection must be initialized."); } } @@ -69,8 +67,7 @@ string RunTask (string remappingXml) GenerateJniRemappingNativeCode.JniRemappingNativeCodeInfo Info { get { - Assert.IsNotNull (LastNativeCodeInfo); - return LastNativeCodeInfo!; + return LastNativeCodeInfo ?? throw new AssertionException ("The task must provide native code information."); } } @@ -105,9 +102,8 @@ public void EmptyCodeEmitsAllTablesAndZeroCounts () StringAssert.Contains ($"@{counter} = dso_local local_unnamed_addr constant i32 0", ll, $"`{counter}` must be zero."); } - var info = task.NativeCodeInfo; - Assert.IsNotNull (info); - Assert.AreEqual (0, info!.ReplacementTypeCount); + var info = task.NativeCodeInfo ?? throw new AssertionException ("The task must provide native code information."); + Assert.AreEqual (0, info.ReplacementTypeCount); Assert.AreEqual (0, info.ReverseTypeCount); Assert.AreEqual (0, info.ReplacementMethodIndexEntryCount); Assert.AreEqual (0, info.ReplacementFieldIndexEntryCount); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs index 41565a5e62f..3cfc7b3e35d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs @@ -33,7 +33,8 @@ public void TryGetDisallowedOption (string line, bool expected, string expectedO Assert.AreEqual (expectedOption, option); } - [TestCase ("package com.example.app;\npublic class Foo {}", "com.example.app")] [TestCase ("package com.example.app ;\npublic class Foo {}", "com.example.app")] // space before ';' + [TestCase ("package com.example.app;\npublic class Foo {}", "com.example.app")] + [TestCase ("package com.example.app ;\npublic class Foo {}", "com.example.app")] // space before ';' [TestCase ("// header\n/* license */\npackage com.example.app;\nclass Foo {}", "com.example.app")] // skip comments [TestCase ("public class Foo {}", null)] // no package [TestCase ("import java.util.List;\npackage com.late;\nclass Foo {}", null)] // package after import is ignored diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs index 025a009d1e4..3fac8e207a2 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs @@ -560,7 +560,8 @@ internal static bool TrySplitMethodKey (string methodKey, out string javaMethodN return javaMethodName.Length != 0; } - internal static string BuildClassEntry (string className) => $"C\t{className}"; internal static string BuildFieldEntry (string className, string fieldName) => $"F\t{className}\t{fieldName}"; + internal static string BuildClassEntry (string className) => $"C\t{className}"; + internal static string BuildFieldEntry (string className, string fieldName) => $"F\t{className}\t{fieldName}"; internal static string BuildMethodEntry (string className, string methodKey) => $"M\t{className}\t{methodKey}"; internal static string CreateManifestContent (IEnumerable entries) From 15da1e43b24e535398f3e2aa3fef08ce7506d6f5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 7 Sep 2026 17:38:40 +0200 Subject: [PATCH 06/14] [Xamarin.Android.Build.Tasks] Fix AAPT keep-rule incrementality Resolve merged and per-manifest keep-rule paths against the captured project working directory during asynchronous AAPT execution. Preserve unchanged rule timestamps and use the existing packaged-resource output to recover missing rules without adding another stamp. Cover resource-only changes, no-op builds, and missing-rule recovery. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tasks/Aapt2Link.cs | 4 ++-- .../IncrementalBuildTest.cs | 18 ++++++++++++++++++ .../Xamarin.Android.Common.targets | 10 +++++++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs index 7bdaf4c8589..d23a0f06a6c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs @@ -131,7 +131,7 @@ public async override System.Threading.Tasks.Task RunTaskAsync () sb.AppendLine (line); } } - Files.CopyIfStringChanged (sb.ToString (), ProguardRuleOutput); + Files.CopyIfStringChanged (sb.ToString (), GetFullPath (ProguardRuleOutput)); } if (!ResourceSymbolsTextFile.IsNullOrEmpty ()) Files.CopyIfChanged (resourceSymbolsTextFileTemp, GetFullPath (ResourceSymbolsTextFile)); @@ -392,7 +392,7 @@ void ProcessManifest (ITaskItem manifestFile) string GetManifestRulesFile (string manifestDir) { - string rulesFile = Path.Combine (manifestDir, "aapt_rules.txt"); + string rulesFile = GetFullPath (Path.Combine (manifestDir, "aapt_rules.txt")); lock (rulesFiles) rulesFiles.Add (rulesFile); return rulesFile; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs index ed99e7cf577..160a94c1635 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs @@ -1766,6 +1766,8 @@ public void AndroidResourceChange ([Values (AndroidRuntime.CoreCLR, AndroidRunti proj.SetRuntime (runtime); using (var builder = CreateApkBuilder ()) { Assert.IsTrue (builder.Build (proj), "first build should succeed"); + var rules = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath, "aapt_rules.txt"); + var rulesTimestamp = File.GetLastWriteTimeUtc (rules); // AndroidResource change proj.LayoutMain += $"{Environment.NewLine}"; @@ -1781,6 +1783,22 @@ public void AndroidResourceChange ([Values (AndroidRuntime.CoreCLR, AndroidRunti } builder.Output.AssertTargetIsSkipped ("_CompileJava"); builder.Output.AssertTargetIsSkipped ("_CompileToDalvik"); + if (runtime == AndroidRuntime.NativeAOT) { + Assert.AreEqual (rulesTimestamp, File.GetLastWriteTimeUtc (rules), "Unchanged AAPT rules should retain their timestamp."); + } + + builder.BuildLogFile = "build3.log"; + Assert.IsTrue (builder.Build (proj), "no-op build should succeed"); + builder.Output.AssertTargetIsSkipped ("_CreateBaseApk"); + builder.Output.AssertTargetIsSkipped ("_CompileToDalvik"); + + if (runtime == AndroidRuntime.NativeAOT) { + File.Delete (rules); + builder.BuildLogFile = "build4.log"; + Assert.IsTrue (builder.Build (proj), "missing AAPT rules should be regenerated"); + Assert.IsTrue (File.Exists (rules), "AAPT rules should exist after recovery."); + builder.Output.AssertTargetIsNotSkipped ("_CreateBaseApk"); + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index d65aa3c7bf1..2c01ff10612 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1885,12 +1885,16 @@ because xbuild doesn't support framework reference assemblies. $(_AndroidBuildPropertiesCache); + + - + From db78ab55a769c39136451e561732ad0b9f4092e3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 7 Sep 2026 17:38:40 +0200 Subject: [PATCH 07/14] [Mono.Android] Complete Android JNI remapping test support Delegate field remapping through AndroidTypeManager and supply the Android fixture with the field and target-descriptor mappings already used by desktop JVM tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniPeerMembersTests.cs | 2 +- .../Android.Runtime/AndroidRuntime.cs | 5 ++++ .../Mono.Android-Tests/Remaps.xml | 28 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs index 97cd8caff55..77d0d5bac03 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs @@ -102,7 +102,7 @@ public void ReplaceInstanceFieldName () [Category ("TrimmableTypeMapUnsupported")] public void ReplacementConstructorUsesTargetSignature () { - // The declared parameter type does not exist; the replacement pins `([C)V` instead. + // The declared parameter type does not exist; the replacement pins `(I)V` instead. var ctor = JavaLangRemappingTestStringBuilder._members.InstanceMethods.GetConstructor ("(Lnet/dot/jni/test/RenamedInt;)V"); Assert.IsNotNull (ctor); } diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index ee2772e654d..e6950d9f778 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -386,6 +386,11 @@ protected override IEnumerable GetSimpleReferences (Type type) return JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); } + protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, string jniFieldName, string jniFieldSignature) + { + return JniRemappingLookup.GetReplacementFieldInfo (jniSourceType, jniFieldName, jniFieldSignature); + } + protected override Type? GetInvokerTypeCore (Type type) { if (type.IsInterface || type.IsAbstract) { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml index 53a299d9d49..58836c1b08b 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml @@ -27,4 +27,32 @@ source-method-signature="()" target-type="net/dot/jni/test/RenameClassBase2" target-method-name="myNewHashCode" target-method-instance-to-static="false" /> + + + + From 31e3638e3383ec24000d9b52d01a6d9c6dbbd1ef Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 8 Sep 2026 16:52:55 +0200 Subject: [PATCH 08/14] [Xamarin.Android.Build.Tasks] Preserve distinct field remap signatures Include the original JNI descriptor in generated and existing-XML field claim keys. Keep fields from merged owners distinct even when their residual descriptors match, while preserving duplicate suppression and same-signature conflict warnings. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateR8JniRemapping.cs | 12 +++-- .../Tasks/GenerateR8JniRemappingTests.cs | 46 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs index 527d0c8a794..e0173c378f2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs @@ -295,7 +295,7 @@ void WriteField (XmlWriter writer, R8ClassMapping classMapping, R8FieldMapping f if (!TryClaimEntry ( "replace-field", - BuildFieldKey (classMapping.ObfuscatedJniName, field.OriginalName), + BuildFieldKey (classMapping.ObfuscatedJniName, field.OriginalName, sourceSignature), $"{classMapping.ObfuscatedJniName}\t{field.ObfuscatedName}\t{targetSignature}")) { return; } @@ -425,7 +425,10 @@ void ReadExistingEntries (XmlReader reader) break; case "replace-field": AddExistingEntry ( - BuildFieldKey (reader.GetAttribute ("source-type"), reader.GetAttribute ("source-field-name")), + BuildFieldKey ( + reader.GetAttribute ("source-type"), + reader.GetAttribute ("source-field-name"), + reader.GetAttribute ("source-field-signature")), $"{reader.GetAttribute ("target-type")}\t{reader.GetAttribute ("target-field-name")}\t{reader.GetAttribute ("target-field-signature")}"); break; case "replace-method": @@ -455,8 +458,9 @@ void AddExistingEntry (string key, string? target, bool externallyOwnedType = fa static string BuildReverseTypeKey (string? from) => from.IsNullOrEmpty () ? "" : $"R\t{from}"; - static string BuildFieldKey (string? sourceType, string? fieldName) - => sourceType.IsNullOrEmpty () || fieldName.IsNullOrEmpty () ? "" : $"F\t{sourceType}\t{fieldName}"; + // Merged classes can have same-named fields with distinct source signatures. + static string BuildFieldKey (string? sourceType, string? fieldName, string? signature) + => sourceType.IsNullOrEmpty () || fieldName.IsNullOrEmpty () ? "" : $"F\t{sourceType}\t{fieldName}\t{signature}"; // A method's source signature is part of its identity: overloads must not collapse. static string BuildMethodKey (string? sourceType, string? methodName, string? signature) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs index 5e3725c59af..ea1752f2ceb 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs @@ -540,6 +540,52 @@ com.contoso.Argument[] arguments -> e StringAssert.Contains (Field ("a/b", "arguments", "[Lcom/contoso/Argument;", "a/b", "e", "[La/d;"), xml); } + [TestCase ("int", "I", "java.lang.String", "Ljava/lang/String;")] + [TestCase ("com.contoso.One", "Lcom/contoso/One;", "com.contoso.Two", "Lcom/contoso/Two;")] + public void MergedFieldsKeepDistinctSourceSignatures (string firstType, string firstSignature, string secondType, string secondSignature) + { + var xml = Run ( + $""" + com.contoso.One -> a.b: + {firstType} value -> c + com.contoso.Two -> a.b: + {secondType} value -> d + """); + + string firstTargetSignature = firstType == "com.contoso.One" ? "La/b;" : firstSignature; + string secondTargetSignature = secondType == "com.contoso.Two" ? "La/b;" : secondSignature; + StringAssert.Contains (Field ("a/b", "value", firstSignature, "a/b", "c", firstTargetSignature), xml); + StringAssert.Contains (Field ("a/b", "value", secondSignature, "a/b", "d", secondTargetSignature), xml); + Assert.AreEqual (0, Warnings.Count, "Different source descriptors must not conflict, even if the target descriptors match."); + } + + [TestCase (false)] + [TestCase (true)] + public void ExistingFieldEntriesOnlyConflictForTheSameSignature (bool identicalTarget) + { + var existing = WriteRemapXml ( + $""" + + {Field ("a/b", "value", "I", identicalTarget ? "a/b" : "com/contoso/Mam", "c", "I")} + + """); + var xml = Run ( + """ + com.contoso.One -> a.b: + int value -> c + com.contoso.Two -> a.b: + java.lang.String value -> d + """, + existing); + + StringAssert.DoesNotContain ("""source-field-signature="I" """, xml, "The existing mapping must win for the same signature."); + StringAssert.Contains (Field ("a/b", "value", "Ljava/lang/String;", "a/b", "d", "Ljava/lang/String;"), xml); + Assert.AreEqual (identicalTarget ? 0 : 1, Warnings.Count); + if (!identicalTarget) { + Assert.AreEqual ("XA4328", Warnings [0].Code); + } + } + [Test] public void AmbiguousMethodNamesAreSkipped () { From d48e096c6ecaff40f275e70625957afd7b3131dd Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 9 Sep 2026 11:58:52 +0200 Subject: [PATCH 09/14] [tests] Trim redundant R8 remapping coverage Consolidate duplicate class and member mapping checks into retained regression cases. Drop standalone count/symbol and property checks, and reduce malformed-input permutations while keeping representative failures, Intune compatibility, retention, ordering, and end-to-end coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../GenerateJniRemappingNativeCodeTests.cs | 48 +---- .../Tasks/GenerateR8JniRemappingTests.cs | 176 ++---------------- .../Tasks/R8Tests.cs | 8 - 3 files changed, 16 insertions(+), 216 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs index 4f79ae9ae7f..652db5f4380 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs @@ -109,51 +109,6 @@ public void EmptyCodeEmitsAllTablesAndZeroCounts () Assert.AreEqual (0, info.ReplacementFieldIndexEntryCount); } - [Test] - public void CountsMatchGeneratedTables () - { - RunTask ( - """ - - - - - - - - - """); - - Assert.AreEqual (2, Info.ReplacementTypeCount, "replace-type count"); - Assert.AreEqual (1, Info.ReverseTypeCount, "reverse-type count"); - Assert.AreEqual (2, Info.ReplacementMethodIndexEntryCount, "replace-method type count"); - Assert.AreEqual (1, Info.ReplacementFieldIndexEntryCount, "replace-field type count"); - } - - [Test] - public void ReverseTypesAreEmittedSeparatelyFromForwardTypes () - { - string ll = RunTask ( - """ - - - - - """); - - int forward = ll.IndexOf ("@jni_remapping_type_replacements"); - int reverse = ll.IndexOf ("@jni_remapping_reverse_type_replacements"); - Assert.Greater (forward, -1, "Forward table must be emitted."); - Assert.Greater (reverse, -1, "Reverse table must be emitted."); - Assert.AreEqual (1, Info.ReplacementTypeCount); - Assert.AreEqual (1, Info.ReverseTypeCount); - } - [Test] public void MissingTargetMethodSignatureIsBackwardCompatible () { @@ -189,13 +144,12 @@ public void TypeTablesAreSortedForBinarySearch () - """); AssertOrdered (ll, "aa/First", "mm/Middle", "zz/Last"); Assert.AreEqual (3, Info.ReplacementTypeCount); - Assert.AreEqual (3, Info.ReverseTypeCount); + Assert.AreEqual (2, Info.ReverseTypeCount); } [Test] diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs index ea1752f2ceb..385a8fa7efa 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs @@ -291,24 +291,13 @@ public void NativeAotEmptySelectionDoesNotFallBackToFullMapping () } [TestCase ("missing")] - [TestCase ("empty-path")] - [TestCase ("empty-file")] [TestCase ("truncated")] [TestCase ("unrelated-object")] [TestCase ("invalid-section")] - [TestCase ("wrong-endianness")] - [TestCase ("linked-library")] - [TestCase ("graph")] public void InvalidNativeAotRetentionIsReportedAsXA4327 (string kind) { string path = Path.Combine (TestDirectory, "missing.o"); switch (kind) { - case "empty-path": - path = ""; - break; - case "empty-file": - File.WriteAllBytes (path, []); - break; case "truncated": File.WriteAllBytes (path, [0x7F, (byte) 'E', (byte) 'L', (byte) 'F', 2, 1, 1]); break; @@ -326,17 +315,6 @@ public void InvalidNativeAotRetentionIsReportedAsXA4327 (string kind) writer.Write ((ulong) file.Length + 1); } break; - case "wrong-endianness": - case "linked-library": - path = WriteNativeObject (["com/contoso/Peer"]); - using (var file = File.Open (path, FileMode.Open, FileAccess.Write)) { - file.Position = kind == "wrong-endianness" ? 5 : 16; - file.WriteByte (kind == "wrong-endianness" ? (byte) 2 : (byte) 3); - } - break; - case "graph": - File.WriteAllText (path, """"""); - break; } var task = new GenerateR8JniRemapping { BuildEngine = engine, @@ -351,20 +329,6 @@ public void InvalidNativeAotRetentionIsReportedAsXA4327 (string kind) FileAssert.DoesNotExist (task.OutputFile); } - [Test] - public void NativeAotObjectWithoutNativeAotModeFails () - { - var task = new GenerateR8JniRemapping { - BuildEngine = engine, - MappingFile = WriteMapping ("com.contoso.Peer -> a.b:\n"), - OutputFile = Path.Combine (TestDirectory, "r8-jni-remap.xml"), - NativeAotObjectFile = WriteNativeObject (["com/contoso/Peer"]), - }; - Assert.IsFalse (task.Execute ()); - Assert.AreEqual ("XA4327", Errors.Single ().Code); - FileAssert.DoesNotExist (task.OutputFile); - } - [Test] public void LinkedAssembliesFilterUnusedMappings () { @@ -402,36 +366,6 @@ static string Method (string sourceType, string name, string signature, string t static string Field (string sourceType, string name, string signature, string targetType, string targetName, string targetSignature) => $""""""; - [Test] - public void RenamedClassesProduceForwardAndReverseTypeEntries () - { - var xml = Run ( - """ - com.contoso.MainActivity -> a.b: - com.contoso.Untouched -> com.contoso.Untouched: - """); - - StringAssert.Contains ("""""", xml); - StringAssert.Contains ("""""", xml); - StringAssert.DoesNotContain ("com/contoso/Untouched", xml, "Unchanged classes must not produce entries."); - } - - [Test] - public void MergedClassesDoNotProduceReverseTypeEntries () - { - // R8 class merging maps two originals onto one residual class: the reverse - // direction is ambiguous and must not be described at all. - var xml = Run ( - """ - com.contoso.One -> a.b: - com.contoso.Two -> a.b: - """); - - StringAssert.Contains ("""""", xml); - StringAssert.Contains ("""""", xml); - StringAssert.DoesNotContain ("reverse-type", xml); - } - [Test] public void RemovedClassesAreSkipped () { @@ -443,38 +377,6 @@ public void RemovedClassesAreSkipped () StringAssert.DoesNotContain ("com/contoso/Gone", xml); } - [Test] - public void MethodOverloadsKeepDistinctSignatures () - { - var xml = Run ( - """ - com.contoso.Peer -> a.b: - void doWork(int) -> c - void doWork(java.lang.String) -> d - void doWork() -> e - """); - - StringAssert.Contains (Method ("a/b", "doWork", "(I)V", "a/b", "c", "(I)V"), xml); - StringAssert.Contains (Method ("a/b", "doWork", "(Ljava/lang/String;)V", "a/b", "d", "(Ljava/lang/String;)V"), xml); - StringAssert.Contains (Method ("a/b", "doWork", "()V", "a/b", "e", "()V"), xml); - } - - [Test] - public void MethodDescriptorsAreRewrittenThroughTheMapping () - { - var xml = Run ( - """ - com.contoso.Peer -> a.b: - com.contoso.Result run(com.contoso.Argument[],int) -> c - com.contoso.Argument -> a.d: - com.contoso.Result -> a.e: - """); - - StringAssert.Contains ( - Method ("a/b", "run", "([Lcom/contoso/Argument;I)Lcom/contoso/Result;", "a/b", "c", "([La/d;I)La/e;"), - xml); - } - [Test] public void RenamedMembersUseResidualOwnersAndOriginalSignatures () { @@ -521,23 +423,8 @@ int counter -> counter StringAssert.DoesNotContain ("replace-method", xml); StringAssert.DoesNotContain ("replace-field", xml); - } - - [Test] - public void FieldsAreEmittedWithRewrittenSignatures () - { - var xml = Run ( - """ - com.contoso.Peer -> a.b: - int counter -> c - com.contoso.Argument argument -> d - com.contoso.Argument[] arguments -> e - com.contoso.Argument -> a.d: - """); - - StringAssert.Contains (Field ("a/b", "counter", "I", "a/b", "c", "I"), xml); - StringAssert.Contains (Field ("a/b", "argument", "Lcom/contoso/Argument;", "a/b", "d", "La/d;"), xml); - StringAssert.Contains (Field ("a/b", "arguments", "[Lcom/contoso/Argument;", "a/b", "e", "[La/d;"), xml); + StringAssert.DoesNotContain ("replace-type", xml); + StringAssert.DoesNotContain ("reverse-type", xml); } [TestCase ("int", "I", "java.lang.String", "Ljava/lang/String;")] @@ -556,6 +443,9 @@ public void MergedFieldsKeepDistinctSourceSignatures (string firstType, string f string secondTargetSignature = secondType == "com.contoso.Two" ? "La/b;" : secondSignature; StringAssert.Contains (Field ("a/b", "value", firstSignature, "a/b", "c", firstTargetSignature), xml); StringAssert.Contains (Field ("a/b", "value", secondSignature, "a/b", "d", secondTargetSignature), xml); + StringAssert.Contains ("""""", xml); + StringAssert.Contains ("""""", xml); + StringAssert.DoesNotContain ("reverse-type", xml, "Merged classes have no unambiguous reverse mapping."); Assert.AreEqual (0, Warnings.Count, "Different source descriptors must not conflict, even if the target descriptors match."); } @@ -638,27 +528,15 @@ public void MalformedMappingIsReportedAsXA4327 () Assert.AreEqual ("XA4327", Errors [0].Code); } - [Test] - public void MissingMappingIsReportedAsXA4327 () - { - var task = new GenerateR8JniRemapping { - BuildEngine = engine, - MappingFile = Path.Combine (TestDirectory, "does-not-exist.txt"), - OutputFile = Path.Combine (TestDirectory, "r8-jni-remap.xml"), - }; - - Assert.IsFalse (task.Execute (), "Task should have failed."); - Assert.AreEqual (1, Errors.Count, "Task should have reported one error."); - Assert.AreEqual ("XA4327", Errors [0].Code); - } - - [Test] - public void ExistingRemapEntriesAreNotOverridden () + [TestCase (false)] + [TestCase (true)] + public void ExistingRemapEntriesAreNotOverridden (bool identicalTarget) { + string targetType = identicalTarget ? "a/b" : "com/microsoft/intune/MainActivity"; var existing = WriteRemapXml ( - """ + $""" - + """); @@ -676,34 +554,10 @@ int counter -> d StringAssert.DoesNotContain ("source-type=\"a/b\"", xml, "Members of an externally owned type must not be emitted using the residual owner."); StringAssert.Contains ("""""", xml); - Assert.AreEqual (1, Warnings.Count, "The conflict should have been reported."); - Assert.AreEqual ("XA4328", Warnings [0].Code); - } - - [Test] - public void IdenticalExistingRemapEntriesDoNotWarn () - { - var existing = WriteRemapXml ( - """ - - - - """); - - var xml = Run ( - """ - com.contoso.MainActivity -> a.b: - void onCreate() -> c - int counter -> d - """, - existing); - - StringAssert.DoesNotContain ("replace-type", xml, - "A duplicate entry must not be emitted twice."); - StringAssert.DoesNotContain ("reverse-type", xml); - StringAssert.DoesNotContain ("replace-method", xml); - StringAssert.DoesNotContain ("replace-field", xml); - Assert.AreEqual (0, Warnings.Count, "An identical entry is not a conflict."); + Assert.AreEqual (identicalTarget ? 0 : 1, Warnings.Count); + if (!identicalTarget) { + Assert.AreEqual ("XA4328", Warnings [0].Code); + } } [Test] diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs index 3cfc7b3e35d..0c393414ff4 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs @@ -50,14 +50,6 @@ public void ReadJavaPackage (string content, string? expected) } } - [TestCase (false, "-keep")] - [TestCase (true, "-keep,allowobfuscation")] - public void KeepOption (bool enableObfuscation, string expected) - { - var task = new R8 { EnableObfuscation = enableObfuscation }; - Assert.AreEqual (expected, task.KeepOption); - } - [TestCase (false, true, false)] [TestCase (true, false, false)] [TestCase (false, true, true)] From 461ac77e31591503a821117615d064cd33440e47 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 9 Sep 2026 17:33:57 +0200 Subject: [PATCH 10/14] [runtime] Fix inherited field remapping Preserve Java field-hiding semantics and enable reverse and field remapping on MonoVM, including field-only configurations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../JniPeerMembers.JniInstanceFields.cs | 17 +++- .../JniPeerMembers.JniStaticFields.cs | 17 +++- .../Java.Interop/JniPeerMembers.cs | 30 ++++--- .../Java.Interop-Tests.csproj | 2 + .../Java.Interop/JavaVMFixture.cs | 6 ++ .../Java.Interop/JniPeerMembersTests.cs | 85 +++++++++++++++++++ .../java/net/dot/jni/test/FieldRemapBase.java | 11 +++ .../net/dot/jni/test/FieldRemapDerived.java | 7 ++ .../mono/monodroid/internal-pinvokes.cc | 17 ++-- src/native/mono/monodroid/jni-remapping.cc | 72 ++++++++++++++++ src/native/mono/monodroid/jni-remapping.hh | 2 + src/native/mono/monodroid/monodroid-glue.cc | 6 +- .../xamarin-app-stub/application_dso_stub.cc | 44 ++++++++++ .../mono/xamarin-app-stub/xamarin-app.hh | 18 ++++ 14 files changed, 307 insertions(+), 27 deletions(-) create mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java create mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs index 59e4c0944d0..aafb1156963 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs @@ -35,7 +35,22 @@ public JniFieldInfo GetFieldInfo (string encodedMember) JniFieldInfo GetFieldInfo (ReadOnlySpan field, ReadOnlySpan signature) { - var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, Members.ManagedPeerType, field, signature); + var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, field, signature); + if (newField.HasValue) { + var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName; + var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field; + var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature; + + using var t = new JniType (typeName); + if (t.TryGetInstanceField (fieldName, fieldSig, out var f)) { + return f; + } + } + if (Members.JniPeerType.TryGetInstanceField (field, signature, out var originalField)) { + return originalField; + } + + newField = JniPeerMembers.GetBaseReplacementFieldInfo (Members.ManagedPeerType, field, signature); if (newField.HasValue) { var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName; var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field; diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs index 350e9d51924..80bf3b57e5e 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs @@ -30,7 +30,22 @@ public JniFieldInfo GetFieldInfo (string encodedMember) JniFieldInfo GetFieldInfo (ReadOnlySpan field, ReadOnlySpan signature) { - var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, Members.ManagedPeerType, field, signature); + var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, field, signature); + if (newField.HasValue) { + var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName; + var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field; + var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature; + + using var t = new JniType (typeName); + if (t.TryGetStaticField (fieldName, fieldSig, out var f)) { + return f; + } + } + if (Members.JniPeerType.TryGetStaticField (field, signature, out var originalField)) { + return originalField; + } + + newField = JniPeerMembers.GetBaseReplacementFieldInfo (Members.ManagedPeerType, field, signature); if (newField.HasValue) { var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName; var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field; diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs index b4ef9c33c0c..7026dadf23a 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs @@ -201,26 +201,30 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value) internal static JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo ( string jniTypeName, + ReadOnlySpan field, + ReadOnlySpan signature) + { + return JniEnvironment.Runtime.TypeManager.GetReplacementFieldInfo (jniTypeName, field, signature); + } + + internal static JniRuntime.ReplacementFieldInfo? GetBaseReplacementFieldInfo ( Type managedPeerType, ReadOnlySpan field, ReadOnlySpan signature) { var typeManager = JniEnvironment.Runtime.TypeManager; - var info = typeManager.GetReplacementFieldInfo (jniTypeName, field, signature); - if (info == null) { - for (Type? baseType = managedPeerType.BaseType; baseType != null; baseType = baseType.BaseType) { - var baseSignature = typeManager.GetTypeSignature (baseType); - string? effectiveBaseType = baseSignature.SimpleReference; - if (effectiveBaseType == null) { - continue; - } - info = typeManager.GetReplacementFieldInfo (effectiveBaseType, field, signature); - if (info != null) { - break; - } + for (Type? baseType = managedPeerType.BaseType; baseType != null; baseType = baseType.BaseType) { + var baseSignature = typeManager.GetTypeSignature (baseType); + string? effectiveBaseType = baseSignature.SimpleReference; + if (effectiveBaseType == null) { + continue; + } + var info = typeManager.GetReplacementFieldInfo (effectiveBaseType, field, signature); + if (info != null) { + return info; } } - return info; + return null; } internal static void AssertSelf (IJavaPeerable self) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj index 1d43a2ca427..4e95f6f8b3c 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj @@ -36,6 +36,8 @@ + + diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs index 66090d7ad1a..bf66bcb72f8 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs @@ -140,6 +140,12 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type) Dictionary<(string SourceType, string SourceName, string? SourceSignature), (string? TargetType, string? TargetName, string? TargetSignature)> ReplacementFields = new() { [("java/lang/Math", "remappedToPi", "D")] = (null, "PI", null), [("java/io/ByteArrayInputStream", "remappedToPos", "I")] = (null, "pos", null), + [(FieldRemapBase.JniTypeName, "hiddenInstanceField", "Z")] = (null, "remappedInstanceField", null), + [(FieldRemapBase.JniTypeName, "hiddenStaticField", "Ljava/lang/String;")] = (null, "remappedStaticField", null), + [(FieldRemapBase.JniTypeName, "inheritedInstanceField", "Z")] = (null, "remappedInheritedInstanceField", null), + [(FieldRemapBase.JniTypeName, "inheritedStaticField", "Ljava/lang/String;")] = (null, "remappedInheritedStaticField", null), + [(FieldRemapDerived.JniTypeName, "inheritedInstanceField", "Z")] = (null, "missingInstanceField", null), + [(FieldRemapDerived.JniTypeName, "inheritedStaticField", "Ljava/lang/String;")] = (null, "missingStaticField", null), }; protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, string jniFieldName, string jniFieldSignature) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs index c9bcf931df7..d2e57b735fc 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs @@ -227,6 +227,78 @@ public void ReplaceInstanceFieldName () Assert.IsFalse (info.IsStatic); } + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void DeclaredInstanceFieldHidesBaseFieldRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapDerived.JniTypeName); + var expected = type.GetInstanceField ("hiddenInstanceField", "Z"); + var remapped = type.GetInstanceField ("remappedInstanceField", "Z"); + var actual = members.InstanceFields.GetFieldInfo ("hiddenInstanceField.Z"); + + Assert.AreEqual (expected.ID, actual.ID); + Assert.AreNotEqual (remapped.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void DeclaredStaticFieldHidesBaseFieldRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapDerived.JniTypeName); + var expected = type.GetStaticField ("hiddenStaticField", "Ljava/lang/String;"); + var remapped = type.GetStaticField ("remappedStaticField", "Ljava/lang/String;"); + var actual = members.StaticFields.GetFieldInfo ("hiddenStaticField.Ljava/lang/String;"); + + Assert.AreEqual (expected.ID, actual.ID); + Assert.AreNotEqual (remapped.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void FailedCurrentInstanceFieldRemapFallsBackToBaseRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapBase.JniTypeName); + var expected = type.GetInstanceField ("remappedInheritedInstanceField", "Z"); + var actual = members.InstanceFields.GetFieldInfo ("inheritedInstanceField.Z"); + + Assert.AreEqual (expected.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void FailedCurrentStaticFieldRemapFallsBackToBaseRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapBase.JniTypeName); + var expected = type.GetStaticField ("remappedInheritedStaticField", "Ljava/lang/String;"); + var actual = members.StaticFields.GetFieldInfo ("inheritedStaticField.Ljava/lang/String;"); + + Assert.AreEqual (expected.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + [Test] [Category ("NativeAOTIgnore")] [Category ("TrimmableTypeMapUnsupported")] @@ -412,6 +484,19 @@ class JavaLangRemappingTestStringBuilder : JavaObject { internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaLangRemappingTestStringBuilder)); } + [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] + class FieldRemapBase : JavaObject { + internal const string JniTypeName = "net/dot/jni/test/FieldRemapBase"; + static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (FieldRemapBase)); + + public override JniPeerMembers JniPeerMembers => _members; + } + + [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] + class FieldRemapDerived : FieldRemapBase { + internal new const string JniTypeName = "net/dot/jni/test/FieldRemapDerived"; + } + [JniTypeSignature (JavaLangRemappingTestRuntime.JniTypeName, GenerateJavaPeer=false)] internal class JavaLangRemappingTestRuntime : JavaObject { internal const string JniTypeName = "java/lang/Runtime"; diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java new file mode 100644 index 00000000000..3bf23955f25 --- /dev/null +++ b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java @@ -0,0 +1,11 @@ +package net.dot.jni.test; + +public class FieldRemapBase +{ + public boolean hiddenInstanceField; + public boolean remappedInstanceField; + public boolean remappedInheritedInstanceField; + public static String hiddenStaticField = "base"; + public static String remappedStaticField = "remapped"; + public static String remappedInheritedStaticField = "inherited"; +} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java new file mode 100644 index 00000000000..c0ea018eed0 --- /dev/null +++ b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java @@ -0,0 +1,7 @@ +package net.dot.jni.test; + +public class FieldRemapDerived extends FieldRemapBase +{ + public boolean hiddenInstanceField; + public static String hiddenStaticField = "derived"; +} diff --git a/src/native/mono/monodroid/internal-pinvokes.cc b/src/native/mono/monodroid/internal-pinvokes.cc index 38cc884cf7d..4fa85a00200 100644 --- a/src/native/mono/monodroid/internal-pinvokes.cc +++ b/src/native/mono/monodroid/internal-pinvokes.cc @@ -289,22 +289,17 @@ _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char return JniRemapping::lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature); } -// -// Reverse type and field remapping are only produced for the CoreCLR and NativeAOT runtimes; the -// entry points exist so that managed code shared with them resolves on MonoVM as well. -// const char* -_monodroid_lookup_reverse_type ([[maybe_unused]] const char *jniSimpleReference) +_monodroid_lookup_reverse_type (const char *jniSimpleReference) { - return nullptr; + return JniRemapping::lookup_reverse_type (jniSimpleReference); } const JniRemappingReplacementField* _monodroid_lookup_replacement_field_info ( - [[maybe_unused]] const char *jniSourceType, - [[maybe_unused]] const char *jniFieldName, - [[maybe_unused]] const char *jniFieldSignature) + const char *jniSourceType, + const char *jniFieldName, + const char *jniFieldSignature) { - return nullptr; + return JniRemapping::lookup_replacement_field_info (jniSourceType, jniFieldName, jniFieldSignature); } - diff --git a/src/native/mono/monodroid/jni-remapping.cc b/src/native/mono/monodroid/jni-remapping.cc index 4122c8fe68a..9fb95806818 100644 --- a/src/native/mono/monodroid/jni-remapping.cc +++ b/src/native/mono/monodroid/jni-remapping.cc @@ -39,6 +39,25 @@ JniRemapping::lookup_replacement_type (const char *jniSimpleReference) noexcept return nullptr; } +const char* +JniRemapping::lookup_reverse_type (const char *jniSimpleReference) noexcept +{ + if (jni_remapping_reverse_type_replacement_count == 0 || jniSimpleReference == nullptr || *jniSimpleReference == '\0') { + return nullptr; + } + + size_t ref_len = strlen (jniSimpleReference); + for (size_t i = 0uz; i < jni_remapping_reverse_type_replacement_count; i++) { + JniRemappingTypeReplacementEntry const& entry = jni_remapping_reverse_type_replacements[i]; + + if (equal (entry.name, jniSimpleReference, ref_len)) { + return entry.replacement; + } + } + + return nullptr; +} + const JniRemappingReplacementMethod* JniRemapping::lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept { @@ -96,3 +115,56 @@ JniRemapping::lookup_replacement_method_info (const char *jniSourceType, const c return nullptr; } + +const JniRemappingReplacementField* +JniRemapping::lookup_replacement_field_info (const char *jniSourceType, const char *jniFieldName, const char *jniFieldSignature) noexcept +{ + if (jni_remapping_field_replacement_index_count == 0 || + jniSourceType == nullptr || *jniSourceType == '\0' || + jniFieldName == nullptr || *jniFieldName == '\0') { + return nullptr; + } + + size_t source_type_len = strlen (jniSourceType); + + const JniRemappingIndexFieldTypeEntry *type = nullptr; + for (size_t i = 0uz; i < jni_remapping_field_replacement_index_count; i++) { + JniRemappingIndexFieldTypeEntry const& entry = jni_remapping_field_replacement_index[i]; + + if (!equal (entry.name, jniSourceType, source_type_len)) { + continue; + } + + type = &jni_remapping_field_replacement_index[i]; + break; + } + + if (type == nullptr || type->field_count == 0 || type->fields == nullptr) { + return nullptr; + } + + size_t field_name_len = strlen (jniFieldName); + size_t signature_len = jniFieldSignature == nullptr ? 0uz : strlen (jniFieldSignature); + + if (signature_len > 0uz) { + for (size_t i = 0uz; i < type->field_count; i++) { + JniRemappingIndexFieldEntry const& entry = type->fields[i]; + + if (equal (entry.name, jniFieldName, field_name_len) && + entry.signature.length != 0 && + equal (entry.signature, jniFieldSignature, signature_len)) { + return &entry.replacement; + } + } + } + + for (size_t i = 0uz; i < type->field_count; i++) { + JniRemappingIndexFieldEntry const& entry = type->fields[i]; + + if (equal (entry.name, jniFieldName, field_name_len) && entry.signature.length == 0) { + return &entry.replacement; + } + } + + return nullptr; +} diff --git a/src/native/mono/monodroid/jni-remapping.hh b/src/native/mono/monodroid/jni-remapping.hh index e76f89e78ff..ca234aafa7e 100644 --- a/src/native/mono/monodroid/jni-remapping.hh +++ b/src/native/mono/monodroid/jni-remapping.hh @@ -11,7 +11,9 @@ namespace xamarin::android::internal { public: static const char* lookup_replacement_type (const char *jniSimpleReference) noexcept; + static const char* lookup_reverse_type (const char *jniSimpleReference) noexcept; static const JniRemappingReplacementMethod* lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept; + static const JniRemappingReplacementField* lookup_replacement_field_info (const char *jniSourceType, const char *jniFieldName, const char *jniFieldSignature) noexcept; private: [[gnu::nonnull (2)]] diff --git a/src/native/mono/monodroid/monodroid-glue.cc b/src/native/mono/monodroid/monodroid-glue.cc index 4a5f9e38c40..ffb95570592 100644 --- a/src/native/mono/monodroid/monodroid-glue.cc +++ b/src/native/mono/monodroid/monodroid-glue.cc @@ -826,7 +826,11 @@ MonodroidRuntime::init_android_runtime (JNIEnv *env, jclass runtimeClass, jobjec init.packageNamingPolicy = static_cast(application_config.package_naming_policy); init.boundExceptionType = application_config.bound_exception_type; init.jniAddNativeMethodRegistrationAttributePresent = application_config.jni_add_native_method_registration_attribute_present ? 1 : 0; - init.jniRemappingInUse = application_config.jni_remapping_replacement_type_count > 0 || application_config.jni_remapping_replacement_method_index_entry_count > 0; + init.jniRemappingInUse = + application_config.jni_remapping_replacement_type_count > 0 || + application_config.jni_remapping_replacement_method_index_entry_count > 0 || + jni_remapping_reverse_type_replacement_count > 0 || + jni_remapping_field_replacement_index_count > 0; init.marshalMethodsEnabled = application_config.marshal_methods_enabled; java_System_identityHashCode = env->GetStaticMethodID (java_System, "identityHashCode", "(Ljava/lang/Object;)I"); diff --git a/src/native/mono/xamarin-app-stub/application_dso_stub.cc b/src/native/mono/xamarin-app-stub/application_dso_stub.cc index f86d53ab622..65c552a14ed 100644 --- a/src/native/mono/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/mono/xamarin-app-stub/application_dso_stub.cc @@ -312,3 +312,47 @@ const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[] = { .replacement = "another/replacement/java/type", }, }; + +static const JniRemappingIndexFieldEntry some_java_type_one_fields[] = { + { + .name = { + .length = 14, + .str = "old_field_name", + }, + + .signature = { + .length = 16, + .str = "Lsome/java/type;", + }, + + .replacement = { + .target_type = "some/java/target_type_one", + .target_name = "new_field_name", + .target_signature = "Lanother/java/type;", + } + }, +}; + +const JniRemappingIndexFieldTypeEntry jni_remapping_field_replacement_index[] = { + { + .name = { + .length = 18, + .str = "some/java/type_one", + }, + .field_count = 1, + .fields = some_java_type_one_fields, + }, +}; + +const JniRemappingTypeReplacementEntry jni_remapping_reverse_type_replacements[] = { + { + .name = { + .length = 17, + .str = "another/java/type", + }, + .replacement = "some/java/type", + }, +}; + +const uint32_t jni_remapping_reverse_type_replacement_count = 1; +const uint32_t jni_remapping_field_replacement_index_count = 1; diff --git a/src/native/mono/xamarin-app-stub/xamarin-app.hh b/src/native/mono/xamarin-app-stub/xamarin-app.hh index 7edc2878797..5ab6598c0bd 100644 --- a/src/native/mono/xamarin-app-stub/xamarin-app.hh +++ b/src/native/mono/xamarin-app-stub/xamarin-app.hh @@ -313,6 +313,20 @@ struct JniRemappingReplacementField const char *target_signature; }; +struct JniRemappingIndexFieldEntry +{ + const JniRemappingString name; + const JniRemappingString signature; + const JniRemappingReplacementField replacement; +}; + +struct JniRemappingIndexFieldTypeEntry +{ + const JniRemappingString name; + const uint32_t field_count; + const JniRemappingIndexFieldEntry *fields; +}; + struct JniRemappingTypeReplacementEntry { const JniRemappingString name; @@ -320,7 +334,11 @@ struct JniRemappingTypeReplacementEntry }; MONO_API MONO_API_EXPORT const JniRemappingIndexTypeEntry jni_remapping_method_replacement_index[]; +MONO_API MONO_API_EXPORT const JniRemappingIndexFieldTypeEntry jni_remapping_field_replacement_index[]; MONO_API MONO_API_EXPORT const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[]; +MONO_API MONO_API_EXPORT const JniRemappingTypeReplacementEntry jni_remapping_reverse_type_replacements[]; +MONO_API MONO_API_EXPORT const uint32_t jni_remapping_reverse_type_replacement_count; +MONO_API MONO_API_EXPORT const uint32_t jni_remapping_field_replacement_index_count; MONO_API MONO_API_EXPORT const uint64_t format_tag; From bdc5441f08cb155b90af882d35331f4474f1c866 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 9 Sep 2026 23:49:18 +0200 Subject: [PATCH 11/14] [tests] Add Android field fallback remaps Mirror the dedicated field-remapping fixtures in the on-device remap table so the hiding and inherited-fallback tests exercise the intended mappings. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Mono.Android-Tests/Remaps.xml | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml index 58836c1b08b..e667834b9d9 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml @@ -55,4 +55,46 @@ target-type="java/io/ByteArrayInputStream" target-field-name="pos" target-field-signature="I" /> + + + + + + From 447e355a2b09a4a1832010769fee3c5d13b1db24 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 10 Sep 2026 11:19:42 +0200 Subject: [PATCH 12/14] [Xamarin.Android.Build.Tasks] Dispose remapping input streams Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateR8JniRemapping.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs index e0173c378f2..ef991f39e69 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs @@ -395,7 +395,8 @@ void ReadExistingEntries () } try { - using var reader = XmlReader.Create (File.OpenRead (file), readerSettings); + using var stream = File.OpenRead (file); + using var reader = XmlReader.Create (stream, readerSettings); ReadExistingEntries (reader); } catch (Exception ex) when (ex is XmlException || ex is IOException || ex is UnauthorizedAccessException) { // MergeRemapXml reports unreadable inputs (XA4318) later in the build. From 6e548092646e1ac00d908412780daae8770b8476 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 10 Sep 2026 13:09:26 +0200 Subject: [PATCH 13/14] [r8] Align runtime remapping mode contract Use AndroidR8ObfuscationMode as the sole opt-in, place its behavior-preserving default with the shared R8 defaults, and keep runtime-specific derivation and validation late. This prepares the branch to add runtime-remapping to the mode contract from PR #12668 after it merges. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../building-apps/build-properties.md | 56 ++++++++----------- Documentation/docs-mobile/messages/xa4327.md | 3 +- Documentation/docs-mobile/messages/xa4329.md | 14 ++--- ...id.Sdk.TypeMap.Trimmable.NativeAOT.targets | 2 +- .../Properties/Resources.Designer.cs | 6 +- .../Properties/Resources.resx | 12 ++-- src/Xamarin.Android.Build.Tasks/Tasks/R8.cs | 22 ++++++-- .../InvalidConfigTests.cs | 28 ++++------ .../Tasks/R8Tests.cs | 29 ++++++++-- .../Xamarin.Android.Common.targets | 28 ++++------ .../Xamarin.Android.D8.targets | 2 +- .../Tests/R8RuntimeRemappingBuildTests.cs | 4 +- .../Tests/R8RuntimeRemappingTests.cs | 7 +-- 13 files changed, 107 insertions(+), 106 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-properties.md b/Documentation/docs-mobile/building-apps/build-properties.md index d99a93dc64f..dec95fa92b8 100644 --- a/Documentation/docs-mobile/building-apps/build-properties.md +++ b/Documentation/docs-mobile/building-apps/build-properties.md @@ -489,33 +489,6 @@ removing the existing one(s) and adding your own AOT profiles. This property is `False` by default. -## AndroidEnableR8Obfuscation - -A boolean property that opts an Android application into R8 name obfuscation. -The default is `false`; setting -[`$(AndroidR8ObfuscationMode)`](#androidr8obfuscationmode) alone does not enable it. -This feature is experimental. - -The current implementation requires `AndroidLinkTool=r8`, -`AndroidTypeMapImplementation=trimmable`, `PublishTrimmed=true`, and either the -CoreCLR or NativeAOT runtime. Explicit incompatible settings produce -[XA4329](../messages/xa4329.md) rather than being silently changed. -This property has no effect on library projects. - -For example: - -```xml - - r8 - trimmable - true - true - runtime-remapping - -``` - -Added in .NET 11. - ## AndroidEnableRestrictToAttributes An enum-style property with valid values of `obsolete` and `disable`. @@ -1144,16 +1117,33 @@ documentation on [D8 and R8][d8-r8]. ## AndroidR8ObfuscationMode -Selects how managed JNI references are reconciled with R8's obfuscated Java -names. It is only used when -[`$(AndroidEnableR8Obfuscation)`](#androidenabler8obfuscation) is `true`. -The default is `runtime-remapping`. +An enum-style property that selects how R8 obfuscates Java names. The default is +`disabled`; selecting `runtime-remapping` explicitly opts the application into +the experimental runtime-remapping implementation. | Value | Behavior | |---|---| +| `disabled` | Disables obfuscation and preserves Java names. | | `runtime-remapping` | Keeps managed assemblies unchanged and translates JNI type/member lookups using generated native remapping tables. Available for trimmed CoreCLR and NativeAOT applications. | | `experimental-rewriting` | Reserved for the separate managed-assembly rewriting implementation. This SDK does not yet include its build pipeline; selecting it reports [XA4329](../messages/xa4329.md). | +The `runtime-remapping` value requires `AndroidLinkTool=r8`, +`AndroidTypeMapImplementation=trimmable`, `PublishTrimmed=true`, and either the +CoreCLR or NativeAOT runtime. Explicit incompatible settings produce +[XA4329](../messages/xa4329.md) rather than being silently changed. This +property has no effect on library projects. + +For example: + +```xml + + r8 + trimmable + true + runtime-remapping + +``` + The runtime-remapping mode leaves managed assemblies unchanged. It runs R8 once, after managed trimming or ILC, then uses the resulting R8 mapping to generate native runtime remapping tables. CoreCLR selects remaps from linked @@ -1162,8 +1152,8 @@ object and statically links the table afterward. Runtime-generated JNI names may require explicit remapping or keep rules. Conservative keep rules still protect native callbacks, bootstrap code, and -resource-referenced names. Neither mode is selected as a fallback for another -mode; unrecognized values report XA4329 when obfuscation is enabled. +resource-referenced names. No mode falls back to another mode; unrecognized +values report XA4329. Added in .NET 11. diff --git a/Documentation/docs-mobile/messages/xa4327.md b/Documentation/docs-mobile/messages/xa4327.md index f4d523ba911..c0e390f66cc 100644 --- a/Documentation/docs-mobile/messages/xa4327.md +++ b/Documentation/docs-mobile/messages/xa4327.md @@ -19,8 +19,7 @@ error XA4327: Failed to generate the R8 JNI remapping data. The R8 mapping file The build could not produce the data that lets the runtime translate the original JNI names in the managed assemblies into the names R8 chose. -This only happens when R8 obfuscation is enabled with -`$(AndroidEnableR8Obfuscation)=true` and +This only happens when `$(AndroidR8ObfuscationMode)=runtime-remapping`. The remapping is built from the mapping file produced by the final R8 pass after managed trimming or ILC. On NativeAOT, this also reports a missing or invalid ILC native object: remapping diff --git a/Documentation/docs-mobile/messages/xa4329.md b/Documentation/docs-mobile/messages/xa4329.md index a6ba5febb29..00aae62725d 100644 --- a/Documentation/docs-mobile/messages/xa4329.md +++ b/Documentation/docs-mobile/messages/xa4329.md @@ -11,11 +11,11 @@ f1_keywords: ## Example messages ``` -Invalid value for AndroidEnableR8Obfuscation: 'yes'. Valid values are: true, false. +Invalid value for AndroidR8ObfuscationMode: 'unknown'. Valid values are: disabled, runtime-remapping, experimental-rewriting. ``` ``` -AndroidR8ObfuscationMode='experimental-rewriting' is not available in this SDK. Use 'runtime-remapping' or set AndroidEnableR8Obfuscation=false. +AndroidR8ObfuscationMode='experimental-rewriting' is not available in this SDK. Use 'runtime-remapping' or 'disabled'. ``` ## Issue @@ -25,16 +25,16 @@ or the application's build configuration is incompatible with obfuscation. ## Solution -Set `AndroidEnableR8Obfuscation` to `true` or `false`. When enabled, use -`AndroidR8ObfuscationMode=runtime-remapping` (the default), `AndroidLinkTool=r8`, +Use `AndroidR8ObfuscationMode=disabled` (the default) to preserve Java names. +To enable runtime remapping, use `AndroidR8ObfuscationMode=runtime-remapping`, +`AndroidLinkTool=r8`, `AndroidTypeMapImplementation=trimmable`, and `PublishTrimmed=true` with CoreCLR or NativeAOT. The `experimental-rewriting` value is reserved for a separate implementation whose build pipeline is not included in this SDK. It does not fall back to -runtime remapping. Setting a mode alone does not enable obfuscation. +runtime remapping. Runtime remapping does not rewrite managed assemblies; it uses the final R8 mapping to generate runtime lookup tables after trimming or ILC. -See [AndroidEnableR8Obfuscation](../building-apps/build-properties.md#androidenabler8obfuscation) -and [AndroidR8ObfuscationMode](../building-apps/build-properties.md#androidr8obfuscationmode). +See [AndroidR8ObfuscationMode](../building-apps/build-properties.md#androidr8obfuscationmode). diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index f91d4526340..6964e61a52d 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -220,7 +220,7 @@ diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 907dfffd6ee..97b10a83adf 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -2140,7 +2140,7 @@ public static string XA4329 { } /// - /// Looks up a localized string similar to AndroidR8ObfuscationMode='experimental-rewriting' is not available in this SDK. Use 'runtime-remapping' or set AndroidEnableR8Obfuscation=false.. + /// Looks up a localized string similar to AndroidR8ObfuscationMode='experimental-rewriting' is not available in this SDK. Use 'runtime-remapping' or 'disabled'.. /// public static string XA4329_RewritingUnavailable { get { @@ -2149,7 +2149,7 @@ public static string XA4329_RewritingUnavailable { } /// - /// Looks up a localized string similar to AndroidEnableR8Obfuscation=true requires $({0}) to be '{1}', but it is {2}.. + /// Looks up a localized string similar to AndroidR8ObfuscationMode=runtime-remapping requires $({0}) to be '{1}', but it is {2}.. /// public static string XA4329_RequiredProperty { get { @@ -2158,7 +2158,7 @@ public static string XA4329_RequiredProperty { } /// - /// Looks up a localized string similar to AndroidEnableR8Obfuscation=true is not supported for runtime '{0}'. Supported runtimes are CoreCLR and NativeAOT.. + /// Looks up a localized string similar to AndroidR8ObfuscationMode=runtime-remapping is not supported for runtime '{0}'. Supported runtimes are CoreCLR and NativeAOT.. /// public static string XA4329_UnsupportedRuntime { get { diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index b964a11784b..dffab9e7fa5 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -974,19 +974,19 @@ Remove the '{0}' reference from your project and add the '{1}' NuGet package ins {2} - A comma-separated list of valid literal values. Do not translate these values. - AndroidR8ObfuscationMode='experimental-rewriting' is not available in this SDK. Use 'runtime-remapping' or set AndroidEnableR8Obfuscation=false. - The following are literal names and should not be translated: AndroidR8ObfuscationMode, experimental-rewriting, runtime-remapping, AndroidEnableR8Obfuscation, false, SDK. + AndroidR8ObfuscationMode='experimental-rewriting' is not available in this SDK. Use 'runtime-remapping' or 'disabled'. + The following are literal names and should not be translated: AndroidR8ObfuscationMode, experimental-rewriting, runtime-remapping, disabled, SDK. - AndroidEnableR8Obfuscation=true requires $({0}) to be '{1}', but it is {2}. - The following are literal names and should not be translated: AndroidEnableR8Obfuscation, true. + AndroidR8ObfuscationMode=runtime-remapping requires $({0}) to be '{1}', but it is {2}. + The following are literal names and should not be translated: AndroidR8ObfuscationMode, runtime-remapping. {0} - The required MSBuild property name. {1} - The required literal value. {2} - The actual value, including quotes. - AndroidEnableR8Obfuscation=true is not supported for runtime '{0}'. Supported runtimes are CoreCLR and NativeAOT. - The following are literal names and should not be translated: AndroidEnableR8Obfuscation, true, CoreCLR, NativeAOT. + AndroidR8ObfuscationMode=runtime-remapping is not supported for runtime '{0}'. Supported runtimes are CoreCLR and NativeAOT. + The following are literal names and should not be translated: AndroidR8ObfuscationMode, runtime-remapping, CoreCLR, NativeAOT. {0} - The runtime name. diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs index 34f0c55565f..80a4876aa3f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -36,11 +36,9 @@ public class R8 : D8 public string? ProguardMappingFileOutput { get; set; } /// - /// Allows R8 to rename types and members by omitting the SDK-generated - /// -dontobfuscate, and by letting the generated Java Callable Wrapper keep rules - /// retain their types without pinning their names. + /// Selects how R8 obfuscation is reconciled with managed JNI names. /// - public bool EnableObfuscation { get; set; } + public string ObfuscationMode { get; set; } = "disabled"; public string? BuildMetadataFileOutput { get; set; } public ITaskItem []? ProguardConfigurationFiles { get; set; } @@ -247,7 +245,19 @@ protected override string CreateResponseFile () /// names are remapped at runtime the wrappers must survive shrinking but stay renameable, /// otherwise a plain -keep pins their names and prevents obfuscation. /// - internal string KeepOption => EnableObfuscation ? "-keep,allowobfuscation" : "-keep"; + internal string KeepOption => IsRuntimeRemappingEnabled ? "-keep,allowobfuscation" : "-keep"; + + internal bool IsRuntimeRemappingEnabled { + get { + if (string.Equals (ObfuscationMode, "disabled", StringComparison.OrdinalIgnoreCase)) { + return false; + } + if (string.Equals (ObfuscationMode, "runtime-remapping", StringComparison.OrdinalIgnoreCase)) { + return true; + } + throw new InvalidOperationException ($"Unsupported R8 obfuscation mode '{ObfuscationMode}'."); + } + } internal void GenerateCommonXamarinConfiguration () { @@ -262,7 +272,7 @@ internal void GenerateCommonXamarinConfiguration () while (reader.ReadLine () is string line) { // The only SDK-generated option dropped when obfuscation is enabled. Every // other rule in the configuration still applies. - if (EnableObfuscation && string.Equals (line.Trim (), "-dontobfuscate", StringComparison.OrdinalIgnoreCase)) { + if (IsRuntimeRemappingEnabled && string.Equals (line.Trim (), "-dontobfuscate", StringComparison.OrdinalIgnoreCase)) { continue; } xamcfg.WriteLine (line); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs index 5b70f8192b9..2b941879385 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs @@ -67,29 +67,23 @@ public void UnsupportedJcwCodegenTargetIsRejected ( } } - [TestCase (null, null, "false", "runtime-remapping", "false")] - [TestCase (null, "runtime-remapping", "false", "runtime-remapping", "false")] - [TestCase (null, "experimental-rewriting", "false", "experimental-rewriting", "false")] - [TestCase ("false", "unknown", "false", "unknown", "false")] - [TestCase ("true", null, "true", "runtime-remapping", "true")] - [TestCase ("true", "runtime-remapping", "true", "runtime-remapping", "true")] - public void R8ObfuscationDefaults (string? enabled, string? mode, string expectedEnabled, string expectedMode, string expectedRemapping) + [TestCase (null, "disabled", "false")] + [TestCase ("disabled", "disabled", "false")] + [TestCase ("runtime-remapping", "runtime-remapping", "true")] + public void R8ObfuscationDefaults (string? mode, string expectedMode, string expectedRemapping) { var project = new XamarinAndroidApplicationProject { IsRelease = true }; project.SetRuntime (AndroidRuntime.CoreCLR); project.SetProperty ("AndroidLinkTool", "r8"); project.SetProperty ("AndroidTypeMapImplementation", "trimmable"); - if (enabled != null) { - project.SetProperty ("AndroidEnableR8Obfuscation", enabled); - } if (mode != null) { project.SetProperty ("AndroidR8ObfuscationMode", mode); } project.Imports.Add (new Import ("R8Options.targets") { TextContent = () => """ - - + + """, @@ -97,10 +91,9 @@ public void R8ObfuscationDefaults (string? enabled, string? mode, string expecte using var builder = CreateApkBuilder (); builder.Target = "ReportR8Options"; Assert.IsTrue (builder.Build (project)); - StringAssertEx.Contains ($"R8_OPTIONS={expectedEnabled}|{expectedMode}|{expectedRemapping}", builder.LastBuildOutput); + StringAssertEx.Contains ($"R8_OPTIONS={expectedMode}|{expectedRemapping}", builder.LastBuildOutput); } - [TestCase ("AndroidEnableR8Obfuscation", "yes", "AndroidEnableR8Obfuscation")] [TestCase ("AndroidR8ObfuscationMode", "unknown", "AndroidR8ObfuscationMode")] [TestCase ("AndroidR8ObfuscationMode", "experimental-rewriting", "not available in this SDK")] [TestCase ("AndroidLinkTool", "d8", "AndroidLinkTool")] @@ -112,13 +105,13 @@ public void R8ObfuscationInvalidConfiguration (string property, string value, st { var project = new XamarinAndroidApplicationProject { IsRelease = true }; project.SetRuntime (AndroidRuntime.CoreCLR); - project.SetProperty ("AndroidEnableR8Obfuscation", "true"); project.SetProperty ("RunAOTCompilation", "false"); project.SetProperty ("AndroidLinkTool", "r8"); project.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + project.SetProperty ("AndroidR8ObfuscationMode", "runtime-remapping"); project.SetProperty (property, value); using var builder = CreateApkBuilder (); - builder.Target = "_ValidateAndroidR8Obfuscation"; + builder.Target = "_ValidateAndroidR8ObfuscationMode"; builder.ThrowOnBuildFailure = false; Assert.IsFalse (builder.Build (project)); StringAssertEx.Contains ("error XA4329:", builder.LastBuildOutput); @@ -129,10 +122,9 @@ public void R8ObfuscationInvalidConfiguration (string property, string value, st public void R8ObfuscationDoesNotEnableLibraries () { var project = new XamarinAndroidLibraryProject (); - project.SetProperty ("AndroidEnableR8Obfuscation", "true"); project.SetProperty ("AndroidR8ObfuscationMode", "experimental-rewriting"); using var builder = CreateDllBuilder (); - builder.Target = "_ValidateAndroidR8Obfuscation"; + builder.Target = "_ValidateAndroidR8ObfuscationMode"; Assert.IsTrue (builder.Build (project), "Application obfuscation settings must not affect referenced libraries."); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs index 0c393414ff4..33d26f043a9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using NUnit.Framework; @@ -50,17 +51,17 @@ public void ReadJavaPackage (string content, string? expected) } } - [TestCase (false, true, false)] - [TestCase (true, false, false)] - [TestCase (false, true, true)] - [TestCase (true, false, true)] - public void GenerateCommonXamarinConfiguration_OnlyDropsDontObfuscate (bool enableObfuscation, bool expectDontObfuscate, bool nativeAot) + [TestCase ("disabled", true, false)] + [TestCase ("runtime-remapping", false, false)] + [TestCase ("disabled", true, true)] + [TestCase ("runtime-remapping", false, true)] + public void GenerateCommonXamarinConfiguration_OnlyDropsDontObfuscate (string obfuscationMode, bool expectDontObfuscate, bool nativeAot) { var path = Path.GetTempFileName (); try { var task = new R8 { BuildEngine = new MockBuildEngine (TestContext.Out), - EnableObfuscation = enableObfuscation, + ObfuscationMode = obfuscationMode, UseTrimmableNativeAotProguardConfiguration = nativeAot, ProguardCommonXamarinConfiguration = path, }; @@ -81,5 +82,21 @@ public void GenerateCommonXamarinConfiguration_OnlyDropsDontObfuscate (bool enab File.Delete (path); } } + + [Test] + public void GenerateCommonXamarinConfiguration_RejectsUnknownObfuscationMode () + { + var path = Path.GetTempFileName (); + var task = new R8 { + BuildEngine = new MockBuildEngine (TestContext.Out), + ObfuscationMode = "unknown", + ProguardCommonXamarinConfiguration = path, + }; + try { + Assert.Throws (() => task.GenerateCommonXamarinConfiguration ()); + } finally { + File.Delete (path); + } + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index b194776beb0..0ba80d65e7b 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -253,6 +253,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. False True True + disabled True @@ -975,7 +976,6 @@ because xbuild doesn't support framework reference assemblies. <_PropertyCacheItems Include="AndroidEnableProfiledAot=$(AndroidEnableProfiledAot)" /> <_PropertyCacheItems Include="AndroidDexTool=$(AndroidDexTool)" /> <_PropertyCacheItems Include="AndroidLinkTool=$(AndroidLinkTool)" /> - <_PropertyCacheItems Include="AndroidEnableR8Obfuscation=$(AndroidEnableR8Obfuscation)" /> <_PropertyCacheItems Include="AndroidR8ObfuscationMode=$(AndroidR8ObfuscationMode)" /> <_PropertyCacheItems Include="AndroidLinkResources=$(AndroidLinkResources)" /> <_PropertyCacheItems Include="AndroidBundleToolExtraArgs=$(AndroidBundleToolExtraArgs)" /> @@ -3148,28 +3148,24 @@ because xbuild doesn't support framework reference assemblies. - false - runtime-remapping <_AndroidR8RuntimeRemappingEnabled>false <_AndroidR8RuntimeRemappingEnabled - Condition=" '$(AndroidApplication)' == 'true' and '$(AndroidEnableR8Obfuscation)' == 'true' and '$(AndroidR8ObfuscationMode)' == 'runtime-remapping' ">true + Condition=" '$(AndroidApplication)' == 'true' and '$(AndroidR8ObfuscationMode)' == 'runtime-remapping' ">true - - - - - - + + FormatArguments="AndroidR8ObfuscationMode;$(AndroidR8ObfuscationMode);disabled, runtime-remapping, experimental-rewriting" + Condition=" '$(AndroidR8ObfuscationMode)' != 'disabled' and '$(AndroidR8ObfuscationMode)' != 'runtime-remapping' and '$(AndroidR8ObfuscationMode)' != 'experimental-rewriting' " /> + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets index 76789268f8f..943d7b85262 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets @@ -78,7 +78,7 @@ Copyright (C) 2018 Xamarin. All rights reserved. ProguardGeneratedReferenceConfiguration="$(_ProguardProjectConfiguration)" ProguardGeneratedApplicationConfiguration="$(IntermediateOutputPath)proguard\proguard_project_primary.cfg" ProguardMappingFileOutput="$(_AndroidR8ProguardMappingFileOutput)" - EnableObfuscation="$(_AndroidR8RuntimeRemappingEnabled)" + ObfuscationMode="$(AndroidR8ObfuscationMode)" BuildMetadataFileOutput="$(_AndroidR8BuildMetadataFile)" ProguardConfigurationFiles="@(_ProguardConfiguration)" UseTrimmableNativeAotProguardConfiguration="$(_UseTrimmableNativeAotProguardConfiguration)" diff --git a/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs index 10cb7d031d2..560fc5eb735 100644 --- a/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs @@ -42,7 +42,7 @@ public class Peer { proj.SetRuntimeIdentifiers (new [] { "arm64-v8a" }); proj.SetProperty ("AndroidTypeMapImplementation", typeMap); proj.SetProperty ("AndroidLinkTool", "r8"); - proj.SetProperty ("AndroidEnableR8Obfuscation", obfuscation.ToString ()); + proj.SetProperty ("AndroidR8ObfuscationMode", obfuscation ? "runtime-remapping" : "disabled"); proj.SetProperty ("AndroidPackageFormats", "apk"); proj.SetProperty ("TrimMode", "full"); proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", """ @@ -124,7 +124,7 @@ public void MultiRidUsesOneR8Mapping (AndroidRuntime runtime, bool explicitPrima } proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); proj.SetProperty ("AndroidLinkTool", "r8"); - proj.SetProperty ("AndroidEnableR8Obfuscation", "true"); + proj.SetProperty ("AndroidR8ObfuscationMode", "runtime-remapping"); proj.SetProperty ("AndroidCreateProguardMappingFile", "false"); proj.SetProperty ("AndroidPackageFormats", "apk"); diff --git a/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs index 234c7cd6d7f..98721ebe124 100644 --- a/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs @@ -80,15 +80,12 @@ public HiddenPeer () {} proj.SetProperty ("AndroidLinkTool", "r8"); proj.SetProperty ("AllowUnsafeBlocks", "true"); proj.SetProperty ("TrimMode", "full"); - proj.SetProperty ("AndroidEnableR8Obfuscation", "true"); + proj.SetProperty ("AndroidR8ObfuscationMode", "runtime-remapping"); proj.SetProperty ("AndroidCreateProguardMappingFile", "false"); string extraRules = ""; proj.OtherBuildItems.Add (new AndroidItem.ProguardConfiguration ("r8-custom.pro") { TextContent = () => extraRules, }); - if (runtime == AndroidRuntime.NativeAOT) { - proj.SetProperty ("AndroidR8ObfuscationMode", "runtime-remapping"); - } proj.Sources.Add (new BuildItem.Source ("HiddenPeerBinding.cs") { TextContent = () => """ using System; @@ -230,7 +227,7 @@ void AssertAppRuns (string logFile) AssertR8Invocations (builder, 1); AssertAppRuns ("r8-changed-rules.log"); - proj.SetProperty ("AndroidEnableR8Obfuscation", "false"); + proj.SetProperty ("AndroidR8ObfuscationMode", "disabled"); Assert.IsTrue (builder.Install (proj), "Disabling obfuscation should rebuild and install the baseline."); AssertR8Invocations (builder, 1, obfuscationEnabled: false); StringAssert.Contains ("-dontobfuscate", File.ReadAllText (Path.Combine (intermediate, "proguard", "proguard_xamarin.cfg"))); From 66ad97c488942924b986e7cdd43cf9c1cff46401 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 14 Sep 2026 12:56:57 +0200 Subject: [PATCH 14/14] [runtime] Preserve JNI remapping specificity Resolve method remaps in exact, parameter-only, then wildcard order; preserve hidden methods before inherited remaps; emit collision-proof native symbols; and make reverse type mappings authoritative. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../JniPeerMembers.JniInstanceMethods.cs | 25 +++++++- .../JniPeerMembers.JniStaticMethods.cs | 12 +++- .../Java.Interop/JniPeerMembers.cs | 33 +++++----- .../Java.Interop/JavaVMFixture.cs | 5 ++ .../Java.Interop/JniPeerMembersTests.cs | 61 +++++++++++++++++++ .../java/net/dot/jni/test/FieldRemapBase.java | 10 +++ .../net/dot/jni/test/FieldRemapDerived.java | 2 + .../TrimmableTypeMapTypeManager.cs | 20 +++--- .../GenerateJniRemappingNativeCodeTests.cs | 59 ++++++++++++++++-- .../JniRemappingAssemblyGenerator.cs | 31 +++++++--- src/native/mono/monodroid/jni-remapping.cc | 42 ++++++++----- .../TrimmableTypeMapTypeManagerTests.cs | 37 +++++++++++ .../Mono.Android-Tests/Remaps.xml | 42 +++++++++++++ 13 files changed, 322 insertions(+), 57 deletions(-) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs index ae8619e139e..36a61d34dd0 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs @@ -71,7 +71,7 @@ JniMethodInfo GetConstructorCore (string signature) { // Constructors are never renamed, but their parameter types can be, so the descriptor // still has to be translated. - var newMethod = JniPeerMembers.GetReplacementMethodInfo (TargetJniTypeName, DeclaringType, "", signature, searchBaseTypes: false); + var newMethod = JniPeerMembers.GetReplacementMethodInfo (TargetJniTypeName, "", signature); var targetSignature = newMethod?.TargetJniMethodSignature; if (targetSignature != null && !string.Equals (targetSignature, signature, StringComparison.Ordinal)) { var typeName = newMethod?.TargetJniType ?? TargetJniTypeName; @@ -125,7 +125,7 @@ public JniMethodInfo GetMethodInfo (string encodedMember) JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signature) { var m = (JniMethodInfo?) null; - var newMethod = JniPeerMembers.GetReplacementMethodInfo (TargetJniTypeName, DeclaringType, method, signature); + var newMethod = JniPeerMembers.GetReplacementMethodInfo (TargetJniTypeName, method, signature); if (newMethod.HasValue) { var typeName = newMethod.Value.TargetJniType ?? TargetJniTypeName; var methodName = newMethod.Value.TargetJniMethodName is string name ? name.AsSpan () : method; @@ -143,6 +143,27 @@ JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signa } Console.Error.WriteLine ($"warning: For declared method `{TargetJniTypeName}.{method}.{signature}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!"); } + if (JniPeerType.TryGetInstanceMethod (method, signature, out m)) { + return m; + } + + newMethod = JniPeerMembers.GetBaseReplacementMethodInfo (DeclaringType, method, signature); + if (newMethod.HasValue) { + var typeName = newMethod.Value.TargetJniType ?? TargetJniTypeName; + var methodName = newMethod.Value.TargetJniMethodName is string name ? name.AsSpan () : method; + var methodSig = newMethod.Value.TargetJniMethodSignature is string sig ? sig.AsSpan () : signature; + + using var t = new JniType (typeName); + if (newMethod.Value.TargetJniMethodInstanceToStatic && + t.TryGetStaticMethod (methodName, methodSig, out m)) { + m.ParameterCount = newMethod.Value.TargetJniMethodParameterCount; + m.StaticRedirect = new JniType (typeName); + return m; + } + if (t.TryGetInstanceMethod (methodName, methodSig, out m)) { + return m; + } + } return JniPeerType.GetInstanceMethod (method, signature); } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs index 8886640d4cb..d53a7318388 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs @@ -36,7 +36,7 @@ public JniMethodInfo GetMethodInfo (string encodedMember) JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signature) { var m = (JniMethodInfo?) null; - var newMethod = JniPeerMembers.GetReplacementMethodInfo (Members.JniPeerTypeName, Members.ManagedPeerType, method, signature); + var newMethod = JniPeerMembers.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature); if (newMethod.HasValue) { using var t = new JniType (newMethod.Value.TargetJniType ?? Members.JniPeerTypeName); if (t.TryGetStaticMethod ( @@ -49,6 +49,16 @@ JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signa if (Members.JniPeerType.TryGetStaticMethod (method, signature, out m)) { return m; } + newMethod = JniPeerMembers.GetBaseReplacementMethodInfo (Members.ManagedPeerType, method, signature); + if (newMethod.HasValue) { + using var t = new JniType (newMethod.Value.TargetJniType ?? Members.JniPeerTypeName); + if (t.TryGetStaticMethod ( + newMethod.Value.TargetJniMethodName is string name ? name.AsSpan () : method, + newMethod.Value.TargetJniMethodSignature is string sig ? sig.AsSpan () : signature, + out m)) { + return m; + } + } m = FindInFallbackTypes (method, signature); if (m != null) { return m; diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs index 7026dadf23a..7654428b776 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs @@ -176,27 +176,30 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value) // Member keys use the replaced type name but retain the managed member name and signature. internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo ( string jniTypeName, + ReadOnlySpan method, + ReadOnlySpan signature) + { + return JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (jniTypeName, method, signature); + } + + internal static JniRuntime.ReplacementMethodInfo? GetBaseReplacementMethodInfo ( Type managedPeerType, ReadOnlySpan method, - ReadOnlySpan signature, - bool searchBaseTypes = true) + ReadOnlySpan signature) { var typeManager = JniEnvironment.Runtime.TypeManager; - var info = typeManager.GetReplacementMethodInfo (jniTypeName, method, signature); - if (info == null && searchBaseTypes) { - for (Type? baseType = managedPeerType.BaseType; baseType != null; baseType = baseType.BaseType) { - var baseSignature = typeManager.GetTypeSignature (baseType); - string? effectiveBaseType = baseSignature.SimpleReference; - if (effectiveBaseType == null) { - continue; - } - info = typeManager.GetReplacementMethodInfo (effectiveBaseType, method, signature); - if (info != null) { - break; - } + for (Type? baseType = managedPeerType.BaseType; baseType != null; baseType = baseType.BaseType) { + var baseSignature = typeManager.GetTypeSignature (baseType); + string? effectiveBaseType = baseSignature.SimpleReference; + if (effectiveBaseType == null) { + continue; + } + var info = typeManager.GetReplacementMethodInfo (effectiveBaseType, method, signature); + if (info != null) { + return info; } } - return info; + return null; } internal static JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo ( diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs index bf66bcb72f8..b68ca1f5807 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs @@ -135,6 +135,11 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type) // `target-method-signature` carries. [("java/lang/StringBuilder", "", "(Lnet/dot/jni/test/RenamedInt;)V")] = (null, "", "(I)V", null, false), [("java/lang/StringBuilder", "indexOf", "(Lnet/dot/jni/test/RenamedString;)I")] = (null, "indexOf", "(Ljava/lang/String;)I", null, false), + [(FieldRemapBase.JniTypeName, "hiddenInstanceMethod", "()I")] = (null, "remappedInstanceMethod", null, null, false), + [(FieldRemapBase.JniTypeName, "hiddenStaticMethod", "()I")] = (null, "remappedStaticMethod", null, null, false), + [(FieldRemapBase.JniTypeName, "remappedSpecificity", "(I)I")] = (null, "specificityExact", "(I)I", null, false), + [(FieldRemapBase.JniTypeName, "remappedSpecificity", "(I)")] = (null, "specificityParameters", "(I)V", null, false), + [(FieldRemapBase.JniTypeName, "remappedSpecificity", null)] = (null, "specificityWildcard", null, null, false), }; Dictionary<(string SourceType, string SourceName, string? SourceSignature), (string? TargetType, string? TargetName, string? TargetSignature)> ReplacementFields = new() { diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs index d2e57b735fc..e16dd41a298 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs @@ -299,6 +299,67 @@ public void FailedCurrentStaticFieldRemapFallsBackToBaseRemap () } } + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void DeclaredInstanceMethodHidesBaseMethodRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapDerived.JniTypeName); + var expected = type.GetInstanceMethod ("hiddenInstanceMethod", "()I"); + var remapped = type.GetInstanceMethod ("remappedInstanceMethod", "()I"); + var actual = members.InstanceMethods.GetMethodInfo ("hiddenInstanceMethod.()I"); + + Assert.AreEqual (expected.ID, actual.ID); + Assert.AreNotEqual (remapped.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void DeclaredStaticMethodHidesBaseMethodRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapDerived.JniTypeName); + var expected = type.GetStaticMethod ("hiddenStaticMethod", "()I"); + var remapped = type.GetStaticMethod ("remappedStaticMethod", "()I"); + var actual = members.StaticMethods.GetMethodInfo ("hiddenStaticMethod.()I"); + + Assert.AreEqual (expected.ID, actual.ID); + Assert.AreNotEqual (remapped.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public unsafe void MethodRemappingPrefersSpecificSignatures () + { + var members = new JniPeerMembers (FieldRemapBase.JniTypeName, typeof (FieldRemapBase)); + try { + var intArgument = new JniArgumentValue (1); + Assert.AreEqual (101, members.StaticMethods.InvokeInt32Method ("remappedSpecificity.(I)I", &intArgument)); + + intArgument = new JniArgumentValue (2); + members.StaticMethods.InvokeVoidMethod ("remappedSpecificity.(I)V", &intArgument); + using var type = new JniType (FieldRemapBase.JniTypeName); + var valueField = type.GetStaticField ("specificityValue", "I"); + Assert.AreEqual (202, JniEnvironment.StaticFields.GetStaticIntField (type.PeerReference, valueField)); + + var longArgument = new JniArgumentValue (3L); + Assert.AreEqual (303, members.StaticMethods.InvokeInt32Method ("remappedSpecificity.(J)I", &longArgument)); + } finally { + JniPeerMembers.Dispose (members); + } + } + [Test] [Category ("NativeAOTIgnore")] [Category ("TrimmableTypeMapUnsupported")] diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java index 3bf23955f25..e437b236f87 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java +++ b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java @@ -8,4 +8,14 @@ public class FieldRemapBase public static String hiddenStaticField = "base"; public static String remappedStaticField = "remapped"; public static String remappedInheritedStaticField = "inherited"; + + public int hiddenInstanceMethod () { return 10; } + public int remappedInstanceMethod () { return 11; } + public static int hiddenStaticMethod () { return 20; } + public static int remappedStaticMethod () { return 21; } + + public static int specificityExact (int value) { return value + 100; } + public static int specificityValue; + public static void specificityParameters (int value) { specificityValue = value + 200; } + public static int specificityWildcard (long value) { return (int)value + 300; } } diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java index c0ea018eed0..4116fb21f2b 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java +++ b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java @@ -4,4 +4,6 @@ public class FieldRemapDerived extends FieldRemapBase { public boolean hiddenInstanceField; public static String hiddenStaticField = "derived"; + public int hiddenInstanceMethod () { return 12; } + public static int hiddenStaticMethod () { return 22; } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 3061d64a7a3..0125663e23c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -196,16 +196,17 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl yield return builtInType; } - foreach (var type in TrimmableTypeMap.Instance.GetTargetTypes (jniSimpleReference)) { - yield return type; - } - // The type map is keyed by the JNI names the managed code declares, so a name that was // renamed in the packaged application has to be translated back first. if (GetOriginalSimpleReference (jniSimpleReference) is string originalReference) { foreach (var type in TrimmableTypeMap.Instance.GetTargetTypes (originalReference)) { yield return type; } + yield break; + } + + foreach (var type in TrimmableTypeMap.Instance.GetTargetTypes (jniSimpleReference)) { + yield return type; } } @@ -218,16 +219,11 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl return builtInType; } - if (TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var type)) { - return type; - } - - if (GetOriginalSimpleReference (jniSimpleReference) is string originalReference && - TrimmableTypeMap.Instance.TryGetTargetType (originalReference, out type)) { - return type; + if (GetOriginalSimpleReference (jniSimpleReference) is string originalReference) { + return TrimmableTypeMap.Instance.TryGetTargetType (originalReference, out var type) ? type : null; } - return null; + return TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var directType) ? directType : null; } static string? GetOriginalSimpleReference (string jniSimpleReference) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs index 652db5f4380..65bd9b2627e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs @@ -7,6 +7,7 @@ using Microsoft.Build.Framework; using NUnit.Framework; using Xamarin.Android.Tasks; +using Xamarin.ProjectTools; namespace Xamarin.Android.Build.Tests.Tasks { @@ -153,11 +154,17 @@ public void TypeTablesAreSortedForBinarySearch () } [Test] - public void MethodsAndFieldsAreSortedByNameThenSignature () + public void MethodsAndFieldsUseStableLookupOrder () { string ll = RunTask ( """ + + @@ -174,14 +181,58 @@ public void MethodsAndFieldsAreSortedByNameThenSignature () """); - // Overloads keep a stable (name, signature) order so the runtime can binary-search the - // name and scan the equal-name run. - AssertOrdered (ll, "c\"alpha", "c\"(I)V", "c\"(J)V", "c\"zeta"); + // Exact descriptors precede parameter-only descriptors and wildcards so MonoVM's + // single scan cannot let a general remap shadow a specific one. + int methodsStart = ll.IndexOf ("@mm_0 =", System.StringComparison.Ordinal); + int methodsEnd = ll.IndexOf ("@jni_remapping_method_replacement_index", methodsStart, System.StringComparison.Ordinal); + Assert.Greater (methodsStart, -1); + Assert.Greater (methodsEnd, methodsStart); + string methodArray = ll.Substring (methodsStart, methodsEnd - methodsStart); + AssertOrdered ( + methodArray, + "ptr @.JniRemappingString.1_str", + "ptr @.JniRemappingString.2_str", + "ptr @.JniRemappingString.3_str", + "ptr null", + "ptr @.JniRemappingString.4_str"); AssertOrdered (ll, "c\"af", "c\"zf"); Assert.AreEqual (1, Info.ReplacementMethodIndexEntryCount); Assert.AreEqual (1, Info.ReplacementFieldIndexEntryCount); } + [Test] + public void MemberArraySymbolsAreCollisionProofAndValidLlvm () + { + string ll = RunTask ( + """ + + + + + + """); + + StringAssert.Contains ("@mm_0", ll); + StringAssert.Contains ("@mm_1", ll); + StringAssert.Contains ("@mf_0", ll); + + string binUtils = Path.Combine (TestEnvironment.OSBinDirectory, "binutils", "bin"); + var compile = new CompileNativeAssembly { + BuildEngine = engine, + Sources = [new Microsoft.Build.Utilities.TaskItem (Path.Combine (TestDirectory, $"jni_remap.{Abi}.ll"))], + DebugBuild = false, + WorkingDirectory = TestDirectory, + AndroidBinUtilsDirectory = binUtils, + }; + Assert.IsTrue (compile.Execute (), $"Generated LLVM IR should compile. Errors: {string.Join ("; ", Errors.Select (e => e.Message))}"); + FileAssert.Exists (Path.Combine (TestDirectory, $"jni_remap.{Abi}.o")); + } + [Test] public void Utf8OrderingMatchesNativeMemcmp () { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index 61e73210c1d..d852d1f4a4d 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -453,12 +453,18 @@ List> MakeMethodIndex () sortedTypes.Sort ((l, r) => CompareUtf8 (l.Value.key, r.Value.key)); var ret = new List> (sortedTypes.Count); - foreach (var kvp in sortedTypes) { + for (int typeIndex = 0; typeIndex < sortedTypes.Count; typeIndex++) { + var kvp = sortedTypes [typeIndex]; var methods = kvp.Value.methods; - // Overloads share a name, so the native lookup binary-searches the name and then - // scans the equal-name run for a matching signature. Keep both keys in the sort. + // Keep exact descriptors before parameter-only descriptors and wildcards, matching + // the specificity passes used by both native runtimes. methods.Sort ((l, r) => { int cmp = CompareUtf8 (l.nameKey, r.nameKey); + if (cmp != 0) { + return cmp; + } + cmp = GetMethodSignatureSpecificity (l.method.SourceMethodSignature).CompareTo ( + GetMethodSignatureSpecificity (r.method.SourceMethodSignature)); return cmp != 0 ? cmp : CompareUtf8 (l.signatureKey, r.signatureKey); }); @@ -481,7 +487,7 @@ List> MakeMethodIndex () var entry = new JniRemappingIndexTypeEntry { name = MakeJniRemappingString (kvp.Key, kvp.Value.key), method_count = (uint)typeMethods.Count, - MethodsArraySymbolName = MakeMembersArrayName ("mm", kvp.Key), + MethodsArraySymbolName = MakeMembersArrayName ("mm", typeIndex), TypeMethods = typeMethods, }; @@ -508,7 +514,8 @@ List> MakeFieldIndex () sortedTypes.Sort ((l, r) => CompareUtf8 (l.Value.key, r.Value.key)); var ret = new List> (sortedTypes.Count); - foreach (var kvp in sortedTypes) { + for (int typeIndex = 0; typeIndex < sortedTypes.Count; typeIndex++) { + var kvp = sortedTypes [typeIndex]; var fields = kvp.Value.fields; fields.Sort ((l, r) => { int cmp = CompareUtf8 (l.nameKey, r.nameKey); @@ -533,7 +540,7 @@ List> MakeFieldIndex () var entry = new JniRemappingIndexFieldTypeEntry { name = MakeJniRemappingString (kvp.Key, kvp.Value.key), field_count = (uint)typeFields.Count, - FieldsArraySymbolName = MakeMembersArrayName ("mf", kvp.Key), + FieldsArraySymbolName = MakeMembersArrayName ("mf", typeIndex), TypeFields = typeFields, }; @@ -543,9 +550,17 @@ List> MakeFieldIndex () return ret; } - static string MakeMembersArrayName (string prefix, string typeName) + static string MakeMembersArrayName (string prefix, int typeIndex) { - return $"{prefix}_{typeName.Replace ('/', '_')}"; + return $"{prefix}_{typeIndex}"; + } + + static int GetMethodSignatureSpecificity (string signature) + { + if (String.IsNullOrEmpty (signature)) { + return 2; + } + return signature [signature.Length - 1] == ')' ? 1 : 0; } static JniRemappingString MakeJniRemappingString (string str, byte [] utf8) diff --git a/src/native/mono/monodroid/jni-remapping.cc b/src/native/mono/monodroid/jni-remapping.cc index 9fb95806818..e78411cc640 100644 --- a/src/native/mono/monodroid/jni-remapping.cc +++ b/src/native/mono/monodroid/jni-remapping.cc @@ -88,28 +88,40 @@ JniRemapping::lookup_replacement_method_info (const char *jniSourceType, const c size_t method_name_len = strlen (jniMethodName); size_t signature_len = jniMethodSignature == nullptr ? 0uz : strlen (jniMethodSignature); - for (size_t i = 0uz; i < type->method_count; i++) { - JniRemappingIndexMethodEntry const& entry = type->methods[i]; - - if (!equal (entry.name, jniMethodName, method_name_len)) { - continue; - } - - if (entry.signature.length == 0 || equal (entry.signature, jniMethodSignature, signature_len)) { - return &type->methods[i].replacement; + if (signature_len > 0uz) { + for (size_t i = 0uz; i < type->method_count; i++) { + JniRemappingIndexMethodEntry const& entry = type->methods[i]; + if (equal (entry.name, jniMethodName, method_name_len) && + entry.signature.length != 0 && + equal (entry.signature, jniMethodSignature, signature_len)) { + return &entry.replacement; + } } const char *sig_end = jniMethodSignature + signature_len; - if (*sig_end == ')') { - continue; - } - while (sig_end != jniMethodSignature && *sig_end != ')') { sig_end--; } - if (equal (entry.signature, jniMethodSignature, static_cast(sig_end - jniMethodSignature) + 1uz)) { - return &type->methods[i].replacement; + if (*sig_end == ')') { + size_t prefix_len = static_cast(sig_end - jniMethodSignature) + 1uz; + if (prefix_len != signature_len) { + for (size_t i = 0uz; i < type->method_count; i++) { + JniRemappingIndexMethodEntry const& entry = type->methods[i]; + if (equal (entry.name, jniMethodName, method_name_len) && + entry.signature.length != 0 && + equal (entry.signature, jniMethodSignature, prefix_len)) { + return &entry.replacement; + } + } + } + } + } + + for (size_t i = 0uz; i < type->method_count; i++) { + JniRemappingIndexMethodEntry const& entry = type->methods[i]; + if (equal (entry.name, jniMethodName, method_name_len) && entry.signature.length == 0) { + return &entry.replacement; } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index ca1053be459..9bb717abd9d 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs @@ -76,6 +76,28 @@ public void GetType_RepeatedJavaToManagedLookup_DoesNotAllocate () Assert.AreEqual (0L, allocatedBytes, $"Expected {iterationCount} cached lookups to allocate no managed memory."); } + [Test] + public void GetType_ReverseMappingPrecedesDirectResidualName () + { + AssumeTrimmableTypeMapEnabled (); + + var signature = new JniTypeSignature ("net/dot/android/test/ResidualCollisionExisting"); + var result = JniEnvironment.Runtime.TypeManager.GetType (signature); + + Assert.AreEqual (typeof (ResidualCollisionRenamed), result); + } + + [Test] + public void GetType_ReverseMappingDoesNotFallBackWhenOriginalIsMissing () + { + AssumeTrimmableTypeMapEnabled (); + + var signature = new JniTypeSignature ("net/dot/android/test/ResidualMissingOriginalExisting"); + var result = JniEnvironment.Runtime.TypeManager.GetType (signature); + + Assert.IsNull (result); + } + [Test] public void TryGetTargetType_MissingEntry_ReturnsFalse () { @@ -700,4 +722,19 @@ class TrimmableRegisteredGenericHolder : Java.Lang.Object { public T Value { get; set; } } + + [Register ("net/dot/android/test/ResidualCollisionRenamed")] + class ResidualCollisionRenamed : Java.Lang.Object + { + } + + [Register ("net/dot/android/test/ResidualCollisionExisting")] + class ResidualCollisionExisting : Java.Lang.Object + { + } + + [Register ("net/dot/android/test/ResidualMissingOriginalExisting")] + class ResidualMissingOriginalExisting : Java.Lang.Object + { + } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml index e667834b9d9..9e81d285144 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml @@ -41,6 +41,48 @@ target-type="java/lang/StringBuilder" target-method-name="indexOf" target-method-signature="(Ljava/lang/String;)I" target-method-instance-to-static="false" /> + + + + + + + +