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..c2fd3f389bf 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,31 @@ 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 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. + +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..f4d523ba911 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4327.md @@ -0,0 +1,53 @@ +--- +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 mapping file 'obj/Release/net11.0/android-arm64/r8-jni-final-mapping.txt' was not found. +``` + +## 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 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 +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 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 + 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..5b52b542b99 --- /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 final R8 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 + final R8 pass does not rename them. +* If the conflict is unexpected, [report an issue][report-issue] and include the + 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 new file mode 100644 index 00000000000..a6ba5febb29 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4329.md @@ -0,0 +1,40 @@ +--- +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. +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/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..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 @@ -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.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..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,12 +24,16 @@ internal JniInstanceMethods (JniPeerMembers members) declaringType.FullName)); DeclaringType = declaringType; - jniPeerType = new JniType (info.Name); + targetJniTypeName = info.Name; + jniPeerType = new JniType (targetJniTypeName); jniPeerType.RegisterWithRuntime (); } JniPeerMembers? members; JniType? jniPeerType; + readonly string? targetJniTypeName; + + string TargetJniTypeName => targetJniTypeName ?? Members.JniPeerTypeName; internal JniPeerMembers Members => members ?? throw new InvalidOperationException (); @@ -59,7 +63,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 (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 +124,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 (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 +140,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 `{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 f0c490460ab..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 @@ -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.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..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 = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, 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 ( 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..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,17 +12,19 @@ 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) { } 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; @@ -65,7 +67,7 @@ static string GetReplacementType (string jniPeerTypeName) static JniPeerMembers CreatePeerMembers (string jniPeerTypeName, Type managedPeerType) { - return new JniPeerMembers (jniPeerTypeName, managedPeerType, checkManagedPeerType: false); + return new JniPeerMembers (GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: false); } JniType? jniPeerType; @@ -75,7 +77,11 @@ 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;} + public JniType JniPeerType { get { var t = JniType.GetCachedJniType (ref jniPeerType, JniPeerTypeName); @@ -141,6 +147,56 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value) return isInterface ? this : value.JniPeerMembers; } + // Member keys use the replaced type name but retain the managed member name and signature. + internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo ( + string jniTypeName, + Type managedPeerType, + string method, + string signature, + bool searchBaseTypes = true) + { + 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; + } + } + } + return info; + } + + internal static JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo ( + string jniTypeName, + Type managedPeerType, + string field, + string 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; + } + } + } + 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..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 @@ -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 { @@ -274,6 +329,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..90ee630a34a 100644 --- a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt @@ -118,3 +118,26 @@ 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? 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..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 @@ -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 `(I)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..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/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..f9bec1a5efd 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs @@ -11,16 +11,31 @@ 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) { + // 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)}" @@ -56,6 +71,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 +105,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 +136,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..0b7d7a2bcae 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); @@ -381,6 +405,9 @@ protected override IEnumerable GetTypeSignaturesCore (Type typ 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.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 aa9074cb6ff..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 @@ -241,6 +241,7 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. DebugBuild="$(AndroidIncludeDebugSymbols)" WorkingDirectory="$(_NativeAssemblySourceDir)" AndroidBinUtilsDirectory="$(AndroidBinUtilsDirectory)" /> + @@ -261,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'))" /> + + + @@ -319,6 +328,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" /> @@ -357,7 +367,7 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. - + @@ -386,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 new file mode 100644 index 00000000000..aaa2756cd94 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.R8JniRemapping.targets @@ -0,0 +1,119 @@ + + + + + + + + <_AndroidR8JniTaskAssembly>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', '$(_XamarinAndroidBuildTasksAssembly)')) + + + + + + <_AndroidR8JniGeneratedRemappingXml>$(IntermediateOutputPath)r8-jni-generated-remap.xml + <_AndroidR8JniRemappingXml>$(IntermediateOutputPath)r8-jni-remap.xml + + + + + + + + + + + + + + + + + + + + + + + + <_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..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,12 +445,18 @@ + Inputs="@(_LinkedAssemblyForProguard);$(_AndroidBuildPropertiesCache)" + 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 b12f80fdc8f..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 @@ -7,10 +7,6 @@ <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">mono.MonoRuntimeProvider - - <_GenerateProguardAfterTargets Condition=" '$(_GenerateProguardAfterTargets)' == '' ">ILLink - - - + <_LinkedAssemblyForProguard Remove="@(_LinkedAssemblyForProguard)" /> <_LinkedAssemblyForProguard Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> - + Inputs="@(_LinkedAssemblyForProguard);$(_AndroidBuildPropertiesCache)" + Outputs="$(_ProguardProjectConfiguration).stamp"> + + + + + + + + + <_AndroidR8JniRemappingAssembly Remove="@(_AndroidR8JniRemappingAssembly)" /> + <_AndroidR8JniRemappingAssembly Include="@(_LinkedAssemblyForProguard)" /> + @@ -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..c2b89a3482a 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1977,6 +1977,159 @@ 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 R8 mapping file '{0}' was not found.. + /// + public static string XA4327_MappingNotFound { + get { + return ResourceManager.GetString("XA4327_MappingNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The R8 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 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 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..2b86883268c 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -901,6 +901,94 @@ 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 R8 mapping file '{0}' was not found. + The following are literal names and should not be translated: R8. +{0} - The path of the missing mapping file. + + + 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 mapping file. +{1} - The underlying message describing why the file 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 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/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/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..d3a8b961426 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateProguardConfiguration.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateProguardConfiguration.cs @@ -19,18 +19,22 @@ public class GenerateProguardConfiguration : AndroidTask [Required] public string OutputFile { get; set; } = ""; + public bool EnableObfuscation { get; set; } + public override bool RunTask () { var dir = Path.GetDirectoryName (OutputFile); if (!dir.IsNullOrEmpty () && !Directory.Exists (dir)) { Directory.CreateDirectory (dir); } - using var writer = File.CreateText (OutputFile); + using var writer = new StringWriter (); foreach (var assembly in LinkedAssemblies) { ProcessAssembly (assembly.ItemSpec, writer); } + Files.CopyIfStringChanged (writer.ToString (), OutputFile); + return !Log.HasLoggedErrors; } @@ -100,8 +104,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/GenerateR8JniRemapping.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs new file mode 100644 index 00000000000..e0173c378f2 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs @@ -0,0 +1,475 @@ +#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 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 + /// 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: + /// + /// + /// <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 final R8 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_MappingNotFound, 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.ObfuscatedJniName, field.OriginalName, sourceSignature), + $"{classMapping.ObfuscatedJniName}\t{field.ObfuscatedName}\t{targetSignature}")) { + return; + } + + writer.WriteStartElement ("replace-field"); + 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); + 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.ObfuscatedJniName, method.OriginalName, sourceSignature), + $"{classMapping.ObfuscatedJniName}\t{method.ObfuscatedName}\t{targetSignature}")) { + return; + } + + writer.WriteStartElement ("replace-method"); + 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); + 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 ("source-field-signature")), + $"{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}"; + + // 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) + => 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..34f0c55565f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -34,6 +34,14 @@ public class R8 : D8 public string? ProguardGeneratedApplicationConfiguration { get; set; } public string? ProguardCommonXamarinConfiguration { get; set; } 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. + /// + public bool EnableObfuscation { get; set; } + public string? BuildMetadataFileOutput { get; set; } public ITaskItem []? ProguardConfigurationFiles { get; set; } public bool UseTrimmableNativeAotProguardConfiguration { get; set; } @@ -168,7 +176,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 +188,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"); @@ -252,6 +242,42 @@ 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 prevents obfuscation. + /// + internal string KeepOption => EnableObfuscation ? "-keep,allowobfuscation" : "-keep"; + + 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)}\""); + } + } + // 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/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/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..4f79ae9ae7f --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs @@ -0,0 +1,256 @@ +#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 { + return directory ?? throw new AssertionException ("The test directory must be initialized."); + } + } + + List Errors { + get { + return errors ?? throw new AssertionException ("The build error collection must be initialized."); + } + } + + 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 { + return LastNativeCodeInfo ?? throw new AssertionException ("The task must provide native code information."); + } + } + + [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 ?? 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); + } + + [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..ea1752f2ceb --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs @@ -0,0 +1,826 @@ +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 ("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); + 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 ("a/b", "<init>", "([Lcom/contoso/Argument;)V", + "a/b", "<init>", "([La/d;)V"), xml); + 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); + 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 ("a/b", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", + "a/b", "c", "(Ljava/lang/Object;)Ljava/lang/Object;"), xml); + 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); + } + + [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 ("a/b", "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 ("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 () + { + 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 () + { + 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 ("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); + } + + [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 () + { + // 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: + 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); + } + + [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."); + } + + [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 ("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 () + { + 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..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 @@ -1,4 +1,5 @@ using System.IO; +using System.Linq; using NUnit.Framework; using Xamarin.Android.Tasks; @@ -48,6 +49,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..3fac8e207a2 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,6 +472,94 @@ public IEnumerable GetReachabilityConflicts (R8Mapping finalMapping, IEn } } + /// + /// 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}"; @@ -742,7 +837,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 +905,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..2c01ff10612 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)" /> @@ -1697,7 +1699,7 @@ because xbuild doesn't support framework reference assemblies. + + + <_GenerateAndroidRemapNativeCodeDependsOn> _ConvertAndroidMamMappingFileToXml; @@ -1719,7 +1726,7 @@ because xbuild doesn't support framework reference assemblies. + <_Aapt2ProguardRules Condition=" '$(AndroidLinkTool)' != '' ">$(IntermediateOutputPath)aapt_rules.txt <_CreateBaseApkInputs> $(_CreateBaseApkInputs); @(_AndroidMSBuildAllProjects); @@ -1876,6 +1885,10 @@ because xbuild doesn't support framework reference assemblies. $(_AndroidBuildPropertiesCache); + + + + + + + + @@ -1987,12 +2006,12 @@ because xbuild doesn't support framework reference assemblies. <_CompileToDalvikDependsOnTargets> _CompileJava; - _CreateApplicationSharedLibraries; - $(_NativeRuntimeLinking); + _AndroidLinkBeforeR8; $(_BeforeCompileToDalvik); _GetLibraryImports; _SetProguardMappingFileProperty; _CalculateProguardConfigurationFiles; + _AndroidPrepareR8JniCompileToDalvikInputs; <_CompileToDalvikInputs> @(_AndroidMSBuildAllProjects) @@ -2008,16 +2027,53 @@ because xbuild doesn't support framework reference assemblies. <_CompileDexDependsOn> _CompileToDalvik; + _AndroidLinkAfterR8; + + + + $(OutputPath)mapping.txt + <_AndroidR8ProguardMappingFileOutput>$(AndroidProguardMappingFile) + + <_AndroidR8ProguardMappingFileOutput Condition=" '$(_AndroidR8RuntimeRemappingEnabled)' == 'true' And '$(_AndroidR8ProguardMappingFileOutput)' == '' ">$(IntermediateOutputPath)r8-jni-final-mapping.txt + <_AndroidR8JniMappingFile Condition=" '$(_AndroidR8RuntimeRemappingEnabled)' == 'true' ">$([System.IO.Path]::GetFullPath('$(_AndroidR8ProguardMappingFileOutput)')) + + + + + + <_CompileToDalvikInputs> + $(_CompileToDalvikInputs) + ;$(_ProguardProjectConfiguration) + ;@(ProguardConfiguration) + ;$(AndroidR8JarPath) + + + + + + + + + + <_ProguardConfiguration Include="$(ProguardConfigFiles)" /> @@ -3088,6 +3144,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..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)"> @@ -77,7 +77,8 @@ 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)" + EnableObfuscation="$(_AndroidR8RuntimeRemappingEnabled)" BuildMetadataFileOutput="$(_AndroidR8BuildMetadataFile)" ProguardConfigurationFiles="@(_ProguardConfiguration)" UseTrimmableNativeAotProguardConfiguration="$(_UseTrimmableNativeAotProguardConfiguration)" @@ -115,7 +116,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/R8RuntimeRemappingBuildTests.cs b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs new file mode 100644 index 00000000000..10cb7d031d2 --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingBuildTests.cs @@ -0,0 +1,164 @@ +using System; +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; +using Xamarin.ProjectTools; + +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)] + 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 new file mode 100644 index 00000000000..234c7cd6d7f --- /dev/null +++ b/tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs @@ -0,0 +1,243 @@ +using System; +using System.IO; +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; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + [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) + { + 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 { + public int hiddenValue = 23; + public HiddenPeer () {} + public int hiddenAdd () { return hiddenValue + 2; } + } + """, + }, + }, + }; + proj.SetRuntime (runtime); + proj.SetRuntimeIdentifiers (new [] { DeviceAbi }); + proj.SetDefaultTargetDevice (); + proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + proj.SetProperty ("AndroidLinkTool", "r8"); + 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"); + } + proj.Sources.Add (new BuildItem.Source ("HiddenPeerBinding.cs") { + TextContent = () => """ + 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); + } + """, + }); + 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 (); + 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."); + 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."); + AssertR8Invocations (builder, 1); + 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."); + 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."); + + 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")); + 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."); + 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")); + Assert.IsFalse (builder.Output.IsTargetSkipped ("_AndroidLinkNativeAotSharedLibrary")); + + 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 { + Assert.IsTrue (builder.Uninstall (proj), "Obfuscated app should uninstall."); + } + } + } +} 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" /> + + + +