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" />
+
+
+
+