diff --git a/Documentation/docs-mobile/TOC.yml b/Documentation/docs-mobile/TOC.yml
index 8adb45b8312..05e2c0296e5 100644
--- a/Documentation/docs-mobile/TOC.yml
+++ b/Documentation/docs-mobile/TOC.yml
@@ -378,6 +378,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 634d78840db..dec95fa92b8 100644
--- a/Documentation/docs-mobile/building-apps/build-properties.md
+++ b/Documentation/docs-mobile/building-apps/build-properties.md
@@ -1115,6 +1115,48 @@ 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
+
+An enum-style property that selects how R8 obfuscates Java names. The default is
+`disabled`; selecting `runtime-remapping` explicitly opts the application into
+the experimental runtime-remapping implementation.
+
+| Value | Behavior |
+|---|---|
+| `disabled` | Disables obfuscation and preserves Java names. |
+| `runtime-remapping` | Keeps managed assemblies unchanged and translates JNI type/member lookups using generated native remapping tables. Available for trimmed CoreCLR and NativeAOT applications. |
+| `experimental-rewriting` | Reserved for the separate managed-assembly rewriting implementation. This SDK does not yet include its build pipeline; selecting it reports [XA4329](../messages/xa4329.md). |
+
+The `runtime-remapping` value requires `AndroidLinkTool=r8`,
+`AndroidTypeMapImplementation=trimmable`, `PublishTrimmed=true`, and either the
+CoreCLR or NativeAOT runtime. Explicit incompatible settings produce
+[XA4329](../messages/xa4329.md) rather than being silently changed. This
+property has no effect on library projects.
+
+For example:
+
+```xml
+
+ r8
+ trimmable
+ true
+ runtime-remapping
+
+```
+
+The runtime-remapping mode leaves managed assemblies unchanged. It runs R8 once,
+after managed trimming or ILC, then uses the resulting R8 mapping to
+generate native runtime remapping tables. CoreCLR selects remaps from linked
+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. No mode falls back to another mode; unrecognized
+values report XA4329.
+
+Added in .NET 11.
+
## AndroidResgenExtraArgs
Specifies
diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md
index 29b747ff8ec..10b0474f72f 100644
--- a/Documentation/docs-mobile/messages/index.md
+++ b/Documentation/docs-mobile/messages/index.md
@@ -261,6 +261,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..c0e390f66cc
--- /dev/null
+++ b/Documentation/docs-mobile/messages/xa4327.md
@@ -0,0 +1,52 @@
+---
+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
+`$(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..00aae62725d
--- /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 AndroidR8ObfuscationMode: 'unknown'. Valid values are: disabled, runtime-remapping, experimental-rewriting.
+```
+
+```
+AndroidR8ObfuscationMode='experimental-rewriting' is not available in this SDK. Use 'runtime-remapping' or 'disabled'.
+```
+
+## 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
+
+Use `AndroidR8ObfuscationMode=disabled` (the default) to preserve Java names.
+To enable runtime remapping, use `AndroidR8ObfuscationMode=runtime-remapping`,
+`AndroidLinkTool=r8`,
+`AndroidTypeMapImplementation=trimmable`, and `PublishTrimmed=true` with CoreCLR
+or NativeAOT.
+
+The `experimental-rewriting` value is reserved for a separate implementation
+whose build pipeline is not included in this SDK. It does not fall back to
+runtime remapping.
+Runtime remapping does not rewrite managed assemblies; it uses the final R8
+mapping to generate runtime lookup tables after trimming or ILC.
+
+See [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 18b6fbd6ee6..aafb1156963 100644
--- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs
+++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs
@@ -29,8 +29,39 @@ public JniFieldInfo GetFieldInfo (string encodedMember)
return InstanceFields.GetOrAdd (encodedMember, static (member, fields) => {
ReadOnlySpan field, signature;
JniPeerMembers.GetNameAndSignature (member, out field, out signature);
- return fields.Members.JniPeerType.GetInstanceField (field, signature);
+ return fields.GetFieldInfo (field, signature);
}, this);
}
+
+ JniFieldInfo GetFieldInfo (ReadOnlySpan field, ReadOnlySpan signature)
+ {
+ var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, field, signature);
+ if (newField.HasValue) {
+ var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName;
+ var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field;
+ var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature;
+
+ using var t = new JniType (typeName);
+ if (t.TryGetInstanceField (fieldName, fieldSig, out var f)) {
+ return f;
+ }
+ }
+ if (Members.JniPeerType.TryGetInstanceField (field, signature, out var originalField)) {
+ return originalField;
+ }
+
+ newField = JniPeerMembers.GetBaseReplacementFieldInfo (Members.ManagedPeerType, field, signature);
+ if (newField.HasValue) {
+ var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName;
+ var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field;
+ var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature;
+
+ using var t = new JniType (typeName);
+ if (t.TryGetInstanceField (fieldName, fieldSig, out var f)) {
+ return f;
+ }
+ }
+ 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 f9e3092ddfa..ae8619e139e 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 ();
@@ -60,7 +64,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.AsSpan ()), 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.AsSpan ());
}
internal JniInstanceMethods GetConstructorsForType (Type declaringType)
@@ -105,9 +125,9 @@ public JniMethodInfo GetMethodInfo (string encodedMember)
JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan 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 is string name ? name.AsSpan () : method;
var methodSig = newMethod.Value.TargetJniMethodSignature is string sig ? sig.AsSpan () : signature;
@@ -121,7 +141,7 @@ JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signa
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 e31a7f25f8f..80bf3b57e5e 100644
--- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs
+++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs
@@ -24,10 +24,41 @@ public JniFieldInfo GetFieldInfo (string encodedMember)
return StaticFields.GetOrAdd (encodedMember, static (member, fields) => {
ReadOnlySpan field, signature;
JniPeerMembers.GetNameAndSignature (member, out field, out signature);
- return fields.Members.JniPeerType.GetStaticField (field, signature);
+ return fields.GetFieldInfo (field, signature);
}, this);
}
+ JniFieldInfo GetFieldInfo (ReadOnlySpan field, ReadOnlySpan signature)
+ {
+ var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, field, signature);
+ if (newField.HasValue) {
+ var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName;
+ var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field;
+ var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature;
+
+ using var t = new JniType (typeName);
+ if (t.TryGetStaticField (fieldName, fieldSig, out var f)) {
+ return f;
+ }
+ }
+ if (Members.JniPeerType.TryGetStaticField (field, signature, out var originalField)) {
+ return originalField;
+ }
+
+ newField = JniPeerMembers.GetBaseReplacementFieldInfo (Members.ManagedPeerType, field, signature);
+ if (newField.HasValue) {
+ var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName;
+ var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field;
+ var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature;
+
+ using var t = new JniType (typeName);
+ if (t.TryGetStaticField (fieldName, fieldSig, out var f)) {
+ return f;
+ }
+ }
+ return Members.JniPeerType.GetStaticField (field, signature);
+ }
+
internal void Dispose ()
{
Clear (ref staticFields);
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 a7f8ce9a096..8886640d4cb 100644
--- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs
+++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs
@@ -36,7 +36,7 @@ public JniMethodInfo GetMethodInfo (string encodedMember)
JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signature)
{
var m = (JniMethodInfo?) null;
- var newMethod = 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 1b2242181d2..7026dadf23a 100644
--- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs
+++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs
@@ -14,17 +14,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;
@@ -67,7 +69,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;
@@ -77,7 +79,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);
@@ -167,6 +173,60 @@ 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,
+ ReadOnlySpan method,
+ ReadOnlySpan 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,
+ ReadOnlySpan field,
+ ReadOnlySpan signature)
+ {
+ return JniEnvironment.Runtime.TypeManager.GetReplacementFieldInfo (jniTypeName, field, signature);
+ }
+
+ internal static JniRuntime.ReplacementFieldInfo? GetBaseReplacementFieldInfo (
+ Type managedPeerType,
+ ReadOnlySpan field,
+ ReadOnlySpan signature)
+ {
+ var typeManager = JniEnvironment.Runtime.TypeManager;
+ for (Type? baseType = managedPeerType.BaseType; baseType != null; baseType = baseType.BaseType) {
+ var baseSignature = typeManager.GetTypeSignature (baseType);
+ string? effectiveBaseType = baseSignature.SimpleReference;
+ if (effectiveBaseType == null) {
+ continue;
+ }
+ var info = typeManager.GetReplacementFieldInfo (effectiveBaseType, field, signature);
+ if (info != null) {
+ return info;
+ }
+ }
+ return null;
+ }
+
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 a58c1f92da4..0e405f37747 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 {
@@ -293,6 +348,41 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type)
protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature)
=> GetReplacementMethodInfoCore (jniSimpleReference, jniMethodName.ToString (), jniMethodSignature.ToString ());
+ 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;
+
+ internal ReplacementFieldInfo? GetReplacementFieldInfo (string jniSimpleReference, ReadOnlySpan jniFieldName, ReadOnlySpan jniFieldSignature)
+ {
+ AssertValid ();
+ AssertSimpleReference (jniSimpleReference, nameof (jniSimpleReference));
+ if (jniFieldName.IsEmpty)
+ throw new ArgumentNullException (nameof (jniFieldName));
+ if (jniFieldSignature.IsEmpty)
+ throw new ArgumentNullException (nameof (jniFieldSignature));
+
+ return GetReplacementFieldInfoCore (jniSimpleReference, jniFieldName, jniFieldSignature);
+ }
+
+ ///
+ /// Resolves field remapping without requiring name and signature strings.
+ /// The default implementation preserves dispatch to the string overload.
+ ///
+ protected virtual ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSimpleReference, ReadOnlySpan jniFieldName, ReadOnlySpan jniFieldSignature)
+ => GetReplacementFieldInfoCore (jniSimpleReference, jniFieldName.ToString (), jniFieldSignature.ToString ());
+
// 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 d23487846e3..e5087ae03fb 100644
--- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs
+++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs
@@ -212,6 +212,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 ();
@@ -503,6 +555,20 @@ internal bool TryGetStaticMethod (ReadOnlySpan name, ReadOnlySpan si
return method != null;
}
+ internal bool TryGetInstanceField (ReadOnlySpan name, ReadOnlySpan signature, [NotNullWhen (true)] out JniFieldInfo? field)
+ {
+ var id = GetMemberID (name, signature, MemberKind.InstanceField, throwOnError: false);
+ field = id == IntPtr.Zero ? null : CreateFieldInfo (name, signature, id, isStatic: false);
+ return field != null;
+ }
+
+ internal bool TryGetStaticField (ReadOnlySpan name, ReadOnlySpan signature, [NotNullWhen (true)] out JniFieldInfo? field)
+ {
+ var id = GetMemberID (name, signature, MemberKind.StaticField, throwOnError: false);
+ field = id == IntPtr.Zero ? null : CreateFieldInfo (name, signature, id, isStatic: true);
+ return field != null;
+ }
+
static JniMethodInfo CreateMethodInfo (ReadOnlySpan name, ReadOnlySpan signature, IntPtr id, bool isStatic)
{
#if DEBUG
diff --git a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt
index 2a9e8e7d8d2..1e5a927d1c3 100644
--- a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt
+++ b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt
@@ -1,5 +1,6 @@
#nullable enable
virtual Java.Interop.JniRuntime.JniTypeManager.GetReplacementMethodInfoCore(string! jniSimpleReference, System.ReadOnlySpan jniMethodName, System.ReadOnlySpan jniMethodSignature) -> Java.Interop.JniRuntime.ReplacementMethodInfo?
+virtual Java.Interop.JniRuntime.JniTypeManager.GetReplacementFieldInfoCore(string! jniSimpleReference, System.ReadOnlySpan jniFieldName, System.ReadOnlySpan jniFieldSignature) -> Java.Interop.JniRuntime.ReplacementFieldInfo?
static Java.Interop.JniEnvironment.BeginMarshalMethod(nint jnienv, out Java.Interop.JniTransition transition, out Java.Interop.JniRuntime? runtime) -> bool
static Java.Interop.JniEnvironment.EndMarshalMethod(ref Java.Interop.JniTransition transition) -> void
virtual Java.Interop.JniRuntime.OnEnterMarshalMethod() -> void
@@ -119,3 +120,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-Tests.csproj b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj
index 1d43a2ca427..4e95f6f8b3c 100644
--- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj
+++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj
@@ -36,6 +36,8 @@
+
+
diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs
index 99004f98c2b..bf66bcb72f8 100644
--- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs
+++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs
@@ -130,8 +130,40 @@ 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),
+ [(FieldRemapBase.JniTypeName, "hiddenInstanceField", "Z")] = (null, "remappedInstanceField", null),
+ [(FieldRemapBase.JniTypeName, "hiddenStaticField", "Ljava/lang/String;")] = (null, "remappedStaticField", null),
+ [(FieldRemapBase.JniTypeName, "inheritedInstanceField", "Z")] = (null, "remappedInheritedInstanceField", null),
+ [(FieldRemapBase.JniTypeName, "inheritedStaticField", "Ljava/lang/String;")] = (null, "remappedInheritedStaticField", null),
+ [(FieldRemapDerived.JniTypeName, "inheritedInstanceField", "Z")] = (null, "missingInstanceField", null),
+ [(FieldRemapDerived.JniTypeName, "inheritedStaticField", "Ljava/lang/String;")] = (null, "missingStaticField", null),
};
+ protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, string jniFieldName, string jniFieldSignature)
+ {
+ 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 51a11362b45..d2e57b735fc 100644
--- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs
+++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs
@@ -205,6 +205,120 @@ 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 DeclaredInstanceFieldHidesBaseFieldRemap ()
+ {
+ var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived));
+ try {
+ using var type = new JniType (FieldRemapDerived.JniTypeName);
+ var expected = type.GetInstanceField ("hiddenInstanceField", "Z");
+ var remapped = type.GetInstanceField ("remappedInstanceField", "Z");
+ var actual = members.InstanceFields.GetFieldInfo ("hiddenInstanceField.Z");
+
+ Assert.AreEqual (expected.ID, actual.ID);
+ Assert.AreNotEqual (remapped.ID, actual.ID);
+ } finally {
+ JniPeerMembers.Dispose (members);
+ }
+ }
+
+ [Test]
+ [Category ("NativeAOTIgnore")]
+ [Category ("TrimmableTypeMapUnsupported")]
+ public void DeclaredStaticFieldHidesBaseFieldRemap ()
+ {
+ var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived));
+ try {
+ using var type = new JniType (FieldRemapDerived.JniTypeName);
+ var expected = type.GetStaticField ("hiddenStaticField", "Ljava/lang/String;");
+ var remapped = type.GetStaticField ("remappedStaticField", "Ljava/lang/String;");
+ var actual = members.StaticFields.GetFieldInfo ("hiddenStaticField.Ljava/lang/String;");
+
+ Assert.AreEqual (expected.ID, actual.ID);
+ Assert.AreNotEqual (remapped.ID, actual.ID);
+ } finally {
+ JniPeerMembers.Dispose (members);
+ }
+ }
+
+ [Test]
+ [Category ("NativeAOTIgnore")]
+ [Category ("TrimmableTypeMapUnsupported")]
+ public void FailedCurrentInstanceFieldRemapFallsBackToBaseRemap ()
+ {
+ var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived));
+ try {
+ using var type = new JniType (FieldRemapBase.JniTypeName);
+ var expected = type.GetInstanceField ("remappedInheritedInstanceField", "Z");
+ var actual = members.InstanceFields.GetFieldInfo ("inheritedInstanceField.Z");
+
+ Assert.AreEqual (expected.ID, actual.ID);
+ } finally {
+ JniPeerMembers.Dispose (members);
+ }
+ }
+
+ [Test]
+ [Category ("NativeAOTIgnore")]
+ [Category ("TrimmableTypeMapUnsupported")]
+ public void FailedCurrentStaticFieldRemapFallsBackToBaseRemap ()
+ {
+ var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived));
+ try {
+ using var type = new JniType (FieldRemapBase.JniTypeName);
+ var expected = type.GetStaticField ("remappedInheritedStaticField", "Ljava/lang/String;");
+ var actual = members.StaticFields.GetFieldInfo ("inheritedStaticField.Ljava/lang/String;");
+
+ Assert.AreEqual (expected.ID, actual.ID);
+ } finally {
+ JniPeerMembers.Dispose (members);
+ }
+ }
+
+ [Test]
+ [Category ("NativeAOTIgnore")]
+ [Category ("TrimmableTypeMapUnsupported")]
+ 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")]
@@ -352,6 +466,37 @@ 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 (JniTypeName, GenerateJavaPeer=false)]
+ class FieldRemapBase : JavaObject {
+ internal const string JniTypeName = "net/dot/jni/test/FieldRemapBase";
+ static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (FieldRemapBase));
+
+ public override JniPeerMembers JniPeerMembers => _members;
+ }
+
+ [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)]
+ class FieldRemapDerived : FieldRemapBase {
+ internal new const string JniTypeName = "net/dot/jni/test/FieldRemapDerived";
+ }
+
[JniTypeSignature (JavaLangRemappingTestRuntime.JniTypeName, GenerateJavaPeer=false)]
internal class JavaLangRemappingTestRuntime : JavaObject {
internal const string JniTypeName = "java/lang/Runtime";
diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java
new file mode 100644
index 00000000000..3bf23955f25
--- /dev/null
+++ b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java
@@ -0,0 +1,11 @@
+package net.dot.jni.test;
+
+public class FieldRemapBase
+{
+ public boolean hiddenInstanceField;
+ public boolean remappedInstanceField;
+ public boolean remappedInheritedInstanceField;
+ public static String hiddenStaticField = "base";
+ public static String remappedStaticField = "remapped";
+ public static String remappedInheritedStaticField = "inherited";
+}
diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java
new file mode 100644
index 00000000000..c0ea018eed0
--- /dev/null
+++ b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java
@@ -0,0 +1,7 @@
+package net.dot.jni.test;
+
+public class FieldRemapDerived extends FieldRemapBase
+{
+ public boolean hiddenInstanceField;
+ public static String hiddenStaticField = "derived";
+}
diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs
index 9640b39650a..becb2fb3996 100644
--- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs
+++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs
@@ -391,6 +391,16 @@ 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 JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, ReadOnlySpan jniFieldName, ReadOnlySpan 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 eaad49ce09f..12219ca7775 100644
--- a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs
+++ b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs
@@ -68,6 +68,18 @@ 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, StringMarshalling = StringMarshalling.Utf8)]
+ [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })]
+ internal static partial IntPtr _monodroid_lookup_replacement_field_info (string jniSourceType, byte* jniFieldName, byte* jniFieldSignature);
+
[LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)]
[UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })]
internal static partial IntPtr _monodroid_lookup_replacement_method_info (string jniSourceType, byte* jniMethodName, byte* jniMethodSignature);
diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs
index e03745ff054..3b12fc4b3e7 100644
--- a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs
+++ b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs
@@ -13,16 +13,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)}"
@@ -58,6 +73,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)
=> GetReplacementMethodInfo (jniSourceType, jniMethodName.AsSpan (), jniMethodSignature.AsSpan ());
@@ -109,12 +142,17 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi
var targetName = method.target_name ?? throw new InvalidOperationException (
$"JNI remapping entry for `{jniSourceType}.{jniMethodName}{jniMethodSignature}` is missing a target method name.");
var sourceSignature = jniMethodSignature.ToString ();
- var newSignature = sourceSignature;
+ // 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 ?? sourceSignature;
int? paramCount = null;
if (method.is_static) {
paramCount = JniMemberSignature.GetParameterCountFromMethodSignature (sourceSignature) + 1;
- newSignature = $"(L{jniSourceType};" + sourceSignature.Substring ("(".Length);
+ if (method.target_signature is null) {
+ newSignature = $"(L{jniSourceType};" + sourceSignature.Substring ("(".Length);
+ }
}
if (Logger.LogAssembly) {
@@ -135,4 +173,74 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi
TargetJniMethodInstanceToStatic = method.is_static,
};
}
+
+ internal static JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo (string jniSourceType, string jniFieldName, string jniFieldSignature)
+ => GetReplacementFieldInfo (jniSourceType, jniFieldName.AsSpan (), jniFieldSignature.AsSpan ());
+
+ internal static unsafe JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo (string jniSourceType, ReadOnlySpan jniFieldName, ReadOnlySpan jniFieldSignature)
+ {
+ if (!JNIEnvInit.jniRemappingInUse) {
+ return null;
+ }
+
+ int nameLength = checked (Encoding.UTF8.GetByteCount (jniFieldName) + 1);
+ int signatureLength = checked (Encoding.UTF8.GetByteCount (jniFieldSignature) + 1);
+ byte[]? rentedName = null;
+ byte[]? rentedSignature = null;
+ IntPtr retInfo;
+ try {
+ if (nameLength > 512)
+ rentedName = ArrayPool.Shared.Rent (nameLength);
+ if (signatureLength > 512)
+ rentedSignature = ArrayPool.Shared.Rent (signatureLength);
+
+ Span nameBuffer = rentedName == null
+ ? stackalloc byte [nameLength]
+ : rentedName.AsSpan (0, nameLength);
+ Span signatureBuffer = rentedSignature == null
+ ? stackalloc byte [signatureLength]
+ : rentedSignature.AsSpan (0, signatureLength);
+ Encoding.UTF8.GetBytes (jniFieldName, nameBuffer);
+ nameBuffer [nameLength - 1] = 0;
+ Encoding.UTF8.GetBytes (jniFieldSignature, signatureBuffer);
+ signatureBuffer [signatureLength - 1] = 0;
+
+ fixed (byte* name = nameBuffer)
+ fixed (byte* signature = signatureBuffer) {
+ retInfo = RuntimeNativeMethods._monodroid_lookup_replacement_field_info (jniSourceType, name, signature);
+ }
+ } finally {
+ if (rentedName != null)
+ ArrayPool.Shared.Return (rentedName);
+ if (rentedSignature != null)
+ ArrayPool.Shared.Return (rentedSignature);
+ }
+ 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 sourceName = jniFieldName.ToString ();
+ var sourceSignature = jniFieldSignature.ToString ();
+ var targetSignature = field.target_signature ?? sourceSignature;
+
+ 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 = sourceName,
+ SourceJniFieldSignature = sourceSignature,
+ 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 59856a9db82..3061d64a7a3 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);
@@ -384,6 +408,12 @@ protected override IEnumerable GetTypeSignaturesCore (Type typ
protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature)
=> JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature);
+ protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, string jniFieldName, string jniFieldSignature)
+ => JniRemappingLookup.GetReplacementFieldInfo (jniSourceType, jniFieldName, jniFieldSignature);
+
+ protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, ReadOnlySpan jniFieldName, ReadOnlySpan 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 00f25c4954e..1f5a7d63ace 100644
--- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets
+++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets
@@ -446,12 +446,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 5dd6a30bb97..0deae41d49c 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
@@ -8,10 +8,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 5787482cd32..1380a517894 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
@@ -541,4 +541,8 @@
+
+
+
diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs
index ee769081fea..97b10a83adf 100644
--- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs
+++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs
@@ -2013,6 +2013,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 'disabled'..
+ ///
+ public static string XA4329_RewritingUnavailable {
+ get {
+ return ResourceManager.GetString("XA4329_RewritingUnavailable", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to AndroidR8ObfuscationMode=runtime-remapping requires $({0}) to be '{1}', but it is {2}..
+ ///
+ public static string XA4329_RequiredProperty {
+ get {
+ return ResourceManager.GetString("XA4329_RequiredProperty", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to AndroidR8ObfuscationMode=runtime-remapping is not supported for runtime '{0}'. Supported runtimes are CoreCLR and NativeAOT..
+ ///
+ public static string XA4329_UnsupportedRuntime {
+ get {
+ 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 81e4fa63a37..dffab9e7fa5 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 'disabled'.
+ The following are literal names and should not be translated: AndroidR8ObfuscationMode, experimental-rewriting, runtime-remapping, disabled, SDK.
+
+
+ AndroidR8ObfuscationMode=runtime-remapping requires $({0}) to be '{1}', but it is {2}.
+ The following are literal names and should not be translated: AndroidR8ObfuscationMode, runtime-remapping.
+{0} - The required MSBuild property name.
+{1} - The required literal value.
+{2} - The actual value, including quotes.
+
+
+ AndroidR8ObfuscationMode=runtime-remapping is not supported for runtime '{0}'. Supported runtimes are CoreCLR and NativeAOT.
+ The following are literal names and should not be translated: AndroidR8ObfuscationMode, runtime-remapping, CoreCLR, NativeAOT.
+{0} - The runtime name.
+
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..ef991f39e69
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs
@@ -0,0 +1,476 @@
+#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 stream = File.OpenRead (file);
+ using var reader = XmlReader.Create (stream, readerSettings);
+ ReadExistingEntries (reader);
+ } catch (Exception ex) when (ex is XmlException || ex is IOException || ex is UnauthorizedAccessException) {
+ // MergeRemapXml reports unreadable inputs (XA4318) later in the build.
+ 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..80a4876aa3f 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs
@@ -34,6 +34,12 @@ public class R8 : D8
public string? ProguardGeneratedApplicationConfiguration { get; set; }
public string? ProguardCommonXamarinConfiguration { get; set; }
public string? ProguardMappingFileOutput { get; set; }
+
+ ///
+ /// Selects how R8 obfuscation is reconciled with managed JNI names.
+ ///
+ public string ObfuscationMode { get; set; } = "disabled";
+
public string? BuildMetadataFileOutput { get; set; }
public ITaskItem []? ProguardConfigurationFiles { get; set; }
public bool UseTrimmableNativeAotProguardConfiguration { get; set; }
@@ -168,7 +174,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 +186,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 +240,54 @@ 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 => IsRuntimeRemappingEnabled ? "-keep,allowobfuscation" : "-keep";
+
+ internal bool IsRuntimeRemappingEnabled {
+ get {
+ if (string.Equals (ObfuscationMode, "disabled", StringComparison.OrdinalIgnoreCase)) {
+ return false;
+ }
+ if (string.Equals (ObfuscationMode, "runtime-remapping", StringComparison.OrdinalIgnoreCase)) {
+ return true;
+ }
+ throw new InvalidOperationException ($"Unsupported R8 obfuscation mode '{ObfuscationMode}'.");
+ }
+ }
+
+ internal void GenerateCommonXamarinConfiguration ()
+ {
+ 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 (IsRuntimeRemappingEnabled && 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..2b941879385 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs
@@ -67,5 +67,66 @@ public void UnsupportedJcwCodegenTargetIsRejected (
}
}
+ [TestCase (null, "disabled", "false")]
+ [TestCase ("disabled", "disabled", "false")]
+ [TestCase ("runtime-remapping", "runtime-remapping", "true")]
+ public void R8ObfuscationDefaults (string? mode, string expectedMode, string expectedRemapping)
+ {
+ var project = new XamarinAndroidApplicationProject { IsRelease = true };
+ project.SetRuntime (AndroidRuntime.CoreCLR);
+ project.SetProperty ("AndroidLinkTool", "r8");
+ project.SetProperty ("AndroidTypeMapImplementation", "trimmable");
+ if (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={expectedMode}|{expectedRemapping}", builder.LastBuildOutput);
+ }
+
+ [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 ("RunAOTCompilation", "false");
+ project.SetProperty ("AndroidLinkTool", "r8");
+ project.SetProperty ("AndroidTypeMapImplementation", "trimmable");
+ project.SetProperty ("AndroidR8ObfuscationMode", "runtime-remapping");
+ project.SetProperty (property, value);
+ using var builder = CreateApkBuilder ();
+ builder.Target = "_ValidateAndroidR8ObfuscationMode";
+ 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 ("AndroidR8ObfuscationMode", "experimental-rewriting");
+ using var builder = CreateDllBuilder ();
+ builder.Target = "_ValidateAndroidR8ObfuscationMode";
+ Assert.IsTrue (builder.Build (project), "Application obfuscation settings must not affect referenced libraries.");
+ }
+
}
}
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs
new file mode 100644
index 00000000000..652db5f4380
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs
@@ -0,0 +1,210 @@
+#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 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 (2, 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/GenerateNativeAotProguardConfigurationTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeAotProguardConfigurationTests.cs
index fd9679e8c27..15842c2e73e 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeAotProguardConfigurationTests.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeAotProguardConfigurationTests.cs
@@ -9,8 +9,9 @@ namespace Xamarin.Android.Build.Tests;
[Parallelizable (ParallelScope.Children)]
public class GenerateNativeAotProguardConfigurationTests : BaseTest
{
- [Test]
- public void Execute_UsesDgmlTypeMetadata ()
+ [TestCase (false)]
+ [TestCase (true)]
+ public void Execute_UsesDgmlTypeMetadata (bool enableObfuscation)
{
var path = Path.Combine (Root, "temp", TestName);
var dgmlFile = Path.Combine (path, "app.scan.dgml.xml");
@@ -47,14 +48,16 @@ public void Execute_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/GenerateR8JniRemappingTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs
new file mode 100644
index 00000000000..385a8fa7efa
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs
@@ -0,0 +1,680 @@
+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 ("truncated")]
+ [TestCase ("unrelated-object")]
+ [TestCase ("invalid-section")]
+ public void InvalidNativeAotRetentionIsReportedAsXA4327 (string kind)
+ {
+ string path = Path.Combine (TestDirectory, "missing.o");
+ switch (kind) {
+ 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;
+ }
+ 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 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 RemovedClassesAreSkipped ()
+ {
+ var xml = Run (
+ """
+ com.contoso.Gone -> R8$$REMOVED$$CLASS$$1:
+ """);
+
+ StringAssert.DoesNotContain ("com/contoso/Gone", 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);
+ StringAssert.DoesNotContain ("replace-type", xml);
+ StringAssert.DoesNotContain ("reverse-type", 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);
+ StringAssert.Contains ("""""", xml);
+ StringAssert.Contains ("""""", xml);
+ StringAssert.DoesNotContain ("reverse-type", xml, "Merged classes have no unambiguous reverse mapping.");
+ Assert.AreEqual (0, Warnings.Count, "Different source descriptors must not conflict, even if the target descriptors match.");
+ }
+
+ [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);
+ }
+
+ [TestCase (false)]
+ [TestCase (true)]
+ public void ExistingRemapEntriesAreNotOverridden (bool identicalTarget)
+ {
+ string targetType = identicalTarget ? "a/b" : "com/microsoft/intune/MainActivity";
+ 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 (identicalTarget ? 0 : 1, Warnings.Count);
+ if (!identicalTarget) {
+ Assert.AreEqual ("XA4328", Warnings [0].Code);
+ }
+ }
+
+ [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/R8Tests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs
index d6f5f8b1bec..33d26f043a9 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs
@@ -1,4 +1,6 @@
+using System;
using System.IO;
+using System.Linq;
using NUnit.Framework;
using Xamarin.Android.Tasks;
@@ -48,6 +50,53 @@ public void ReadJavaPackage (string content, string? expected)
File.Delete (path);
}
}
+
+ [TestCase ("disabled", true, false)]
+ [TestCase ("runtime-remapping", false, false)]
+ [TestCase ("disabled", true, true)]
+ [TestCase ("runtime-remapping", false, true)]
+ public void GenerateCommonXamarinConfiguration_OnlyDropsDontObfuscate (string obfuscationMode, bool expectDontObfuscate, bool nativeAot)
+ {
+ var path = Path.GetTempFileName ();
+ try {
+ var task = new R8 {
+ BuildEngine = new MockBuildEngine (TestContext.Out),
+ ObfuscationMode = obfuscationMode,
+ 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);
+ }
+ }
+
+ [Test]
+ public void GenerateCommonXamarinConfiguration_RejectsUnknownObfuscationMode ()
+ {
+ var path = Path.GetTempFileName ();
+ var task = new R8 {
+ BuildEngine = new MockBuildEngine (TestContext.Out),
+ ObfuscationMode = "unknown",
+ ProguardCommonXamarinConfiguration = path,
+ };
+ try {
+ Assert.Throws (() => task.GenerateCommonXamarinConfiguration ());
+ } finally {
+ File.Delete (path);
+ }
+ }
}
}
-
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniAssemblyRewriter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniAssemblyRewriter.cs
index 2cdef5eae57..6e277175ecb 100644
--- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniAssemblyRewriter.cs
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniAssemblyRewriter.cs
@@ -71,5 +71,11 @@ public static void ScanRewrittenAssembly (PEReader peReader, MetadataReader read
FieldRvaTable fieldRvaTable = FieldRvaTable.Read (peReader, reader);
new JniRewritePlanner (peReader, reader, mapping.CreateReverseMapping (), fieldRvaTable, log).CreatePlan ();
}
+
+ public static void ScanAssembly (PEReader peReader, MetadataReader reader, R8Mapping mapping, TaskLoggingHelper log)
+ {
+ FieldRvaTable fieldRvaTable = FieldRvaTable.Read (peReader, reader);
+ new JniRewritePlanner (peReader, reader, mapping, fieldRvaTable, 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