diff --git a/Documentation/docs-mobile/TOC.yml b/Documentation/docs-mobile/TOC.yml index 6116d95ea22..12e5254caf6 100644 --- a/Documentation/docs-mobile/TOC.yml +++ b/Documentation/docs-mobile/TOC.yml @@ -262,6 +262,8 @@ href: messages/xa1035.md - name: XA1036 href: messages/xa1036.md + - name: XA1037 + href: messages/xa1037.md - name: XA1038 href: messages/xa1038.md - name: XA1042 diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index abe2d010109..833e4ecf280 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -147,6 +147,7 @@ or 'Help->Report a Problem' in Visual Studio for Mac. + [XA1035](xa1035.md): The 'BundleAssemblies' property is deprecated and it has no effect on the application build. Equivalent functionality is implemented by the 'AndroidUseAssemblyStore' and 'AndroidEnableAssemblyCompression' properties. + [XA1036](xa1036.md): AndroidManifest.xml //uses-sdk/@android:minSdkVersion '29' does not match the $(SupportedOSPlatformVersion) value '21' in the project file (if there is no $(SupportedOSPlatformVersion) value in the project file, then a default value has been assumed). Either change the value in the AndroidManifest.xml to match the $(SupportedOSPlatformVersion) value, or remove the value in the AndroidManifest.xml (and add a $(SupportedOSPlatformVersion) value to the project file if it doesn't already exist). ++ [XA1037](xa1037.md): Unsupported @(Reference) item: {item} + [XA1038](xa1038.md): The '{0}' MSBuild property has an invalid value. Value values are {1}. + [XA1039](xa1039.md): The Android Support libraries are not supported in .NET 9 and later, please migrate to AndroidX. See https://aka.ms/xamarin/androidx for more details. + [XA1040](xa1040.md): The NativeAOT runtime on Android is an experimental feature and not yet suitable for production use. File issues at: https://github.com/dotnet/android/issues diff --git a/Documentation/docs-mobile/messages/xa1037.md b/Documentation/docs-mobile/messages/xa1037.md new file mode 100644 index 00000000000..785a76607d4 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa1037.md @@ -0,0 +1,25 @@ +--- +title: .NET for Android error XA1037 +description: XA1037 error code +ms.date: 09/16/2026 +f1_keywords: + - "XA1037" +--- + +# .NET for Android error XA1037 + +## Example messages + +``` +error XA1037: Unsupported @(Reference) item: {item} +``` + +## Issue + +The specified `@(Reference)` item is not a supported Java archive or source +directory. + +## Solution + +Update the `@(Reference)` item to reference a `.jar` file, an `.aar` file, or +a Java source directory. diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniFieldInfo.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniFieldInfo.cs index a02ec4c8c4f..80de1a3e1aa 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniFieldInfo.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniFieldInfo.cs @@ -10,6 +10,8 @@ public sealed class JniFieldInfo public bool IsStatic {get; private set;} + internal JniType? StaticRedirect; + internal bool IsValid { get {return ID != IntPtr.Zero;} } @@ -70,4 +72,3 @@ public override string ToString () } } } - diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniMethodInfo.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniMethodInfo.cs index 928a53a902c..bd17a8052a3 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniMethodInfo.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniMethodInfo.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Runtime.InteropServices; namespace Java.Interop { @@ -19,11 +20,12 @@ internal bool IsValid { #if DEBUG string? name, signature; + IntPtr nameUtf8, signatureUtf8; #endif // !DEBUG public string Name { #if DEBUG - get => name ?? throw new NotSupportedException (); + get => name ??= GetUtf8String (nameUtf8); #else // !DEBUG get => throw new NotSupportedException (); #endif // !DEBUG @@ -31,7 +33,7 @@ public string Name { public string Signature { #if DEBUG - get => signature ?? throw new NotSupportedException (); + get => signature ??= GetUtf8String (signatureUtf8); #else // !DEBUG get => throw new NotSupportedException (); #endif // !DEBUG @@ -55,11 +57,54 @@ public JniMethodInfo (string name, string signature, IntPtr methodID, bool isSta #endif // DEBUG } + internal JniMethodInfo (IntPtr nameUtf8, string signature, IntPtr methodID, bool isStatic) + { + ID = methodID; + IsStatic = isStatic; + +#if DEBUG + this.nameUtf8 = nameUtf8; + this.signature = signature; +#endif // DEBUG + } + + internal JniMethodInfo (IntPtr nameUtf8, IntPtr signatureUtf8, IntPtr methodID, bool isStatic) + { + ID = methodID; + IsStatic = isStatic; + +#if DEBUG + this.nameUtf8 = nameUtf8; + this.signatureUtf8 = signatureUtf8; +#endif // DEBUG + } + + internal JniMethodInfo (string name, IntPtr signatureUtf8, IntPtr methodID, bool isStatic) + { + ID = methodID; + IsStatic = isStatic; + +#if DEBUG + this.name = name; + this.signatureUtf8 = signatureUtf8; +#endif // DEBUG + } + +#if DEBUG + static unsafe string GetUtf8String (IntPtr value) + { + if (value == IntPtr.Zero) + throw new NotSupportedException (); + + return System.Text.Encoding.UTF8.GetString (MemoryMarshal.CreateReadOnlySpanFromNullTerminated ((byte*)value)); + } +#endif // DEBUG + public override string ToString () { #if DEBUG - bool haveName = !string.IsNullOrEmpty (name); - bool haveSig = !string.IsNullOrEmpty (signature); + bool haveName = !string.IsNullOrEmpty (name) || nameUtf8 != IntPtr.Zero; + bool haveSig = !string.IsNullOrEmpty (signature) || signatureUtf8 != IntPtr.Zero; #else // DEBUG bool haveName = false; bool haveSig = false; @@ -73,4 +118,3 @@ public override string ToString () } } } - diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniFields.cs index ea9d0cb5155..e087e110f1d 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniFields.cs @@ -202,109 +202,109 @@ partial class JniStaticFields { public bool GetBooleanValue (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticBooleanField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticBooleanField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, bool value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticBooleanField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticBooleanField (GetFieldDeclaringType (f).PeerReference, f, value); } public sbyte GetSByteValue (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticByteField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticByteField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, sbyte value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticByteField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticByteField (GetFieldDeclaringType (f).PeerReference, f, value); } public char GetCharValue (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticCharField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticCharField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, char value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticCharField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticCharField (GetFieldDeclaringType (f).PeerReference, f, value); } public short GetInt16Value (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticShortField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticShortField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, short value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticShortField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticShortField (GetFieldDeclaringType (f).PeerReference, f, value); } public int GetInt32Value (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticIntField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticIntField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, int value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticIntField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticIntField (GetFieldDeclaringType (f).PeerReference, f, value); } public long GetInt64Value (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticLongField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticLongField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, long value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticLongField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticLongField (GetFieldDeclaringType (f).PeerReference, f, value); } public float GetSingleValue (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticFloatField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticFloatField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, float value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticFloatField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticFloatField (GetFieldDeclaringType (f).PeerReference, f, value); } public double GetDoubleValue (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticDoubleField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticDoubleField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, double value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticDoubleField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticDoubleField (GetFieldDeclaringType (f).PeerReference, f, value); } public JniObjectReference GetObjectValue (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticObjectField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticObjectField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, JniObjectReference value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticObjectField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticObjectField (GetFieldDeclaringType (f).PeerReference, f, value); } }} } 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..d40b91c6d3d 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,22 @@ 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, "", signature); + if (newMethod.HasValue) { + var info = newMethod.Value; + using var t = CreateTargetType (info, TargetJniTypeName); + if (TryGetInstanceMethod (t, info, "", signature, out var m)) { + return m; + } + } + return JniPeerType.GetConstructor (signature.AsSpan ()); } internal JniInstanceMethods GetConstructorsForType (Type declaringType) @@ -105,24 +124,40 @@ 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 = Members.GetReplacementMethodInfo (method, signature); if (newMethod.HasValue) { - var typeName = newMethod.Value.TargetJniType ?? Members.JniPeerTypeName; - var methodName = newMethod.Value.TargetJniMethodName is string name ? name.AsSpan () : method; - var methodSig = newMethod.Value.TargetJniMethodSignature is string sig ? sig.AsSpan () : signature; - - using var t = new JniType (typeName); - if (newMethod.Value.TargetJniMethodInstanceToStatic && - t.TryGetStaticMethod (methodName, methodSig, out m)) { - m.ParameterCount = newMethod.Value.TargetJniMethodParameterCount; - m.StaticRedirect = new JniType (typeName); + var info = newMethod.Value; + using var t = CreateTargetType (info, Members); + if (info.TargetJniMethodInstanceToStatic && + TryGetStaticMethod (t, info, method, signature, out m)) { + m.ParameterCount = info.TargetJniMethodParameterCount; + m.StaticRedirect = CreateTargetType (info, Members); return m; } - if (t.TryGetInstanceMethod (methodName, methodSig, out m)) { + if (TryGetInstanceMethod (t, info, method, signature, out m)) + return m; + var targetType = GetTargetTypeNameForDiagnostics (info, Members); + var targetName = GetTargetMethodNameForDiagnostics (info, method); + var targetSignature = GetTargetMethodSignatureForDiagnostics (info, signature); + Console.Error.WriteLine ($"warning: For declared method `{Members.JniPeerTypeName}.{method}.{signature}`, could not find requested method `{targetType}.{targetName}.{targetSignature}`!"); + } + if (JniPeerType.TryGetInstanceMethod (method, signature, out m)) + return m; + + newMethod = JniPeerMembers.GetBaseReplacementMethodInfo (DeclaringType, method, signature); + if (newMethod.HasValue) { + var info = newMethod.Value; + using var t = CreateTargetType (info, TargetJniTypeName); + if (info.TargetJniMethodInstanceToStatic && + TryGetStaticMethod (t, info, method, signature, out m)) { + m.ParameterCount = info.TargetJniMethodParameterCount; + m.StaticRedirect = CreateTargetType (info, TargetJniTypeName); return m; } - Console.Error.WriteLine ($"warning: For declared method `{Members.JniPeerTypeName}.{method}.{signature}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!"); + if (TryGetInstanceMethod (t, info, method, signature, out m)) + return m; } + 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..01a393b2441 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,13 +24,61 @@ 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; + + JniType? t = new JniType (typeName); + try { + if (t.TryGetStaticField (fieldName, fieldSig, out var f)) { + f.StaticRedirect = t; + t = null; + return f; + } + } finally { + t?.Dispose (); + } + } + 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; + + JniType? t = new JniType (typeName); + try { + if (t.TryGetStaticField (fieldName, fieldSig, out var f)) { + f.StaticRedirect = t; + t = null; + return f; + } + } finally { + t?.Dispose (); + } + } + return Members.JniPeerType.GetStaticField (field, signature); + } + + JniType GetFieldDeclaringType (JniFieldInfo field) + { + return field.StaticRedirect ?? Members.JniPeerType; + } + internal void Dispose () { - Clear (ref staticFields); + Clear (ref staticFields, static field => field.StaticRedirect?.Dispose ()); } }} } 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..cd655bcad31 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 @@ -21,7 +21,7 @@ internal JniStaticMethods (JniPeerMembers members) internal void Dispose () { - Clear (ref staticMethods); + Clear (ref staticMethods, static method => method.StaticRedirect?.Dispose ()); } public JniMethodInfo GetMethodInfo (string encodedMember) @@ -36,19 +36,37 @@ 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 = Members.GetReplacementMethodInfo (method, signature); if (newMethod.HasValue) { - using var t = new JniType (newMethod.Value.TargetJniType ?? Members.JniPeerTypeName); - if (t.TryGetStaticMethod ( - newMethod.Value.TargetJniMethodName is string name ? name.AsSpan () : method, - newMethod.Value.TargetJniMethodSignature is string sig ? sig.AsSpan () : signature, - out m)) { - return m; + var info = newMethod.Value; + JniType? t = CreateTargetType (info, Members); + try { + if (TryGetStaticMethod (t, info, method, signature, out m)) { + m.StaticRedirect = t; + t = null; + return m; + } + } finally { + t?.Dispose (); } } if (Members.JniPeerType.TryGetStaticMethod (method, signature, out m)) { return m; } + newMethod = JniPeerMembers.GetBaseReplacementMethodInfo (Members.ManagedPeerType, method, signature); + if (newMethod.HasValue) { + var info = newMethod.Value; + JniType? t = CreateTargetType (info, Members); + try { + if (TryGetStaticMethod (t, info, method, signature, out m)) { + m.StaticRedirect = t; + t = null; + return m; + } + } finally { + t?.Dispose (); + } + } m = FindInFallbackTypes (method, signature); if (m != null) { return m; diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs index 1b2242181d2..5e5007f5275 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Reflection; +using System.Runtime.InteropServices; using System.Threading; namespace Java.Interop { @@ -14,27 +15,42 @@ 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) + readonly struct JniPeerTypeNameInfo { - var replacement = JniEnvironment.Runtime.TypeManager.GetReplacementType (jniPeerTypeName); - if (replacement != null) - return replacement; - return jniPeerTypeName; + public JniPeerTypeNameInfo (string sourceName, string? targetName, IntPtr targetNameUtf8) + { + SourceName = sourceName; + TargetName = targetName; + TargetNameUtf8 = targetNameUtf8; + } + + public string SourceName { get; } + public string? TargetName { get; } + public IntPtr TargetNameUtf8 { get; } } - JniPeerMembers (string jniPeerTypeName, Type managedPeerType, bool checkManagedPeerType, bool isInterface = false) + static JniPeerTypeNameInfo GetReplacementType (string jniPeerTypeName) { if (jniPeerTypeName == null) throw new ArgumentNullException (nameof (jniPeerTypeName)); + var typeManager = JniEnvironment.Runtime.TypeManager; + typeManager.GetReplacementTypeInfo (jniPeerTypeName, out var replacement, out var replacementUtf8); + return new JniPeerTypeNameInfo (jniPeerTypeName, replacement, replacementUtf8); + } + + JniPeerMembers (JniPeerTypeNameInfo jniPeerTypeName, Type managedPeerType, bool checkManagedPeerType, bool isInterface = false) + { + if (jniPeerTypeName.SourceName == null) + throw new ArgumentNullException (nameof (jniPeerTypeName)); if (checkManagedPeerType) { if (managedPeerType == null) @@ -44,17 +60,19 @@ static string GetReplacementType (string jniPeerTypeName) #if DEBUG var signatureFromType = JniEnvironment.Runtime.TypeManager.GetTypeSignature (managedPeerType); - if (signatureFromType.SimpleReference != jniPeerTypeName) { + if (signatureFromType.SimpleReference != jniPeerTypeName.SourceName) { Debug.WriteLine ("WARNING-Java.Interop: ManagedPeerType <=> JniTypeName Mismatch! javaVM.GetJniTypeInfoForType(typeof({0})).JniTypeName=\"{1}\" != \"{2}\"", managedPeerType.FullName, signatureFromType.SimpleReference, - jniPeerTypeName); + jniPeerTypeName.SourceName); Debug.WriteLine (new System.Diagnostics.StackTrace (true)); } #endif // DEBUG } - JniPeerTypeName = jniPeerTypeName; + sourceJniPeerTypeName = jniPeerTypeName.SourceName; + this.jniPeerTypeName = jniPeerTypeName.TargetName; + jniPeerTypeNameUtf8 = jniPeerTypeName.TargetNameUtf8; ManagedPeerType = managedPeerType; this.isInterface = isInterface; @@ -67,20 +85,27 @@ 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; + string sourceJniPeerTypeName; + string? jniPeerTypeName; + IntPtr jniPeerTypeNameUtf8; JniInstanceMethods instanceMethods; JniInstanceFields instanceFields; JniStaticMethods staticMethods; JniStaticFields staticFields; public Type ManagedPeerType {get; private set;} - public string JniPeerTypeName {get; private set;} + public string JniPeerTypeName => jniPeerTypeNameUtf8 == IntPtr.Zero + ? jniPeerTypeName ?? sourceJniPeerTypeName + : jniPeerTypeName ??= GetUtf8String (jniPeerTypeNameUtf8); public JniType JniPeerType { get { - var t = JniType.GetCachedJniType (ref jniPeerType, JniPeerTypeName); + var t = jniPeerTypeNameUtf8 == IntPtr.Zero + ? JniType.GetCachedJniType (ref jniPeerType, jniPeerTypeName ?? sourceJniPeerTypeName) + : JniType.GetCachedJniType (ref jniPeerType, jniPeerTypeNameUtf8); t.RegisterWithRuntime (); return t; } @@ -167,6 +192,160 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value) return isInterface ? this : value.JniPeerMembers; } + JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (ReadOnlySpan method, ReadOnlySpan signature) + { + return jniPeerTypeNameUtf8 == IntPtr.Zero + ? JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (jniPeerTypeName ?? sourceJniPeerTypeName, method, signature) + : JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (jniPeerTypeNameUtf8, method, signature); + } + + static JniType CreateTargetType (JniRuntime.ReplacementMethodInfo info, JniPeerMembers fallback) + { + return CreateTargetType (info, fallback.JniPeerTypeName); + } + + static JniType CreateTargetType (JniRuntime.ReplacementMethodInfo info, string fallbackTypeName) + { + if (info.TargetJniTypeUtf8 != IntPtr.Zero) + return new JniType (info.TargetJniTypeUtf8); + if (info.TargetJniType != null) + return new JniType (info.TargetJniType); + return new JniType (fallbackTypeName); + } + + static bool TryGetInstanceMethod ( + JniType type, + JniRuntime.ReplacementMethodInfo info, + ReadOnlySpan fallbackName, + ReadOnlySpan fallbackSignature, + [System.Diagnostics.CodeAnalysis.NotNullWhen (true)] out JniMethodInfo? method) + { + if (info.TargetJniMethodNameUtf8 != IntPtr.Zero) { + if (info.TargetJniMethodSignatureUtf8 != IntPtr.Zero) + return type.TryGetInstanceMethod (info.TargetJniMethodNameUtf8, info.TargetJniMethodSignatureUtf8, out method); + var signature = info.TargetJniMethodSignature is string targetSignature ? targetSignature.AsSpan () : fallbackSignature; + return type.TryGetInstanceMethod (info.TargetJniMethodNameUtf8, signature, out method); + } + + var name = info.TargetJniMethodName is string targetName ? targetName.AsSpan () : fallbackName; + if (info.TargetJniMethodSignatureUtf8 != IntPtr.Zero) + return type.TryGetInstanceMethod (name, info.TargetJniMethodSignatureUtf8, out method); + var fallback = info.TargetJniMethodSignature is string targetSignatureValue ? targetSignatureValue.AsSpan () : fallbackSignature; + return type.TryGetInstanceMethod (name, fallback, out method); + } + + static bool TryGetStaticMethod ( + JniType type, + JniRuntime.ReplacementMethodInfo info, + ReadOnlySpan fallbackName, + ReadOnlySpan fallbackSignature, + [System.Diagnostics.CodeAnalysis.NotNullWhen (true)] out JniMethodInfo? method) + { + if (info.TargetJniMethodNameUtf8 != IntPtr.Zero) { + if (info.TargetJniMethodSignatureUtf8 != IntPtr.Zero) + return type.TryGetStaticMethod (info.TargetJniMethodNameUtf8, info.TargetJniMethodSignatureUtf8, out method); + var signature = info.TargetJniMethodSignature is string targetSignature ? targetSignature.AsSpan () : fallbackSignature; + return type.TryGetStaticMethod (info.TargetJniMethodNameUtf8, signature, out method); + } + + var name = info.TargetJniMethodName is string targetName ? targetName.AsSpan () : fallbackName; + if (info.TargetJniMethodSignatureUtf8 != IntPtr.Zero) + return type.TryGetStaticMethod (name, info.TargetJniMethodSignatureUtf8, out method); + var fallback = info.TargetJniMethodSignature is string targetSignatureValue ? targetSignatureValue.AsSpan () : fallbackSignature; + return type.TryGetStaticMethod (name, fallback, out method); + } + + static unsafe string GetUtf8String (IntPtr value) + { + return System.Text.Encoding.UTF8.GetString (MemoryMarshal.CreateReadOnlySpanFromNullTerminated ((byte*)value)); + } + + static string GetTargetTypeNameForDiagnostics (JniRuntime.ReplacementMethodInfo info, JniPeerMembers fallback) + { + if (info.TargetJniTypeUtf8 != IntPtr.Zero) + return GetUtf8String (info.TargetJniTypeUtf8); + if (info.TargetJniType != null) + return info.TargetJniType; + return fallback.JniPeerTypeName; + } + + static string GetTargetMethodNameForDiagnostics (JniRuntime.ReplacementMethodInfo info, ReadOnlySpan fallback) + { + if (info.TargetJniMethodNameUtf8 != IntPtr.Zero) + return GetUtf8String (info.TargetJniMethodNameUtf8); + if (info.TargetJniMethodName != null) + return info.TargetJniMethodName; + return fallback.ToString (); + } + + static string GetTargetMethodSignatureForDiagnostics (JniRuntime.ReplacementMethodInfo info, ReadOnlySpan fallback) + { + if (info.TargetJniMethodSignatureUtf8 != IntPtr.Zero) + return GetUtf8String (info.TargetJniMethodSignatureUtf8); + if (info.TargetJniMethodSignature != null) + return info.TargetJniMethodSignature; + return fallback.ToString (); + } + + // Member keys use the replaced type name but retain the managed member name and signature. + internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo ( + string jniTypeName, + ReadOnlySpan method, + ReadOnlySpan signature) + { + return JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (jniTypeName, method, signature); + } + + internal static JniRuntime.ReplacementMethodInfo? GetBaseReplacementMethodInfo ( + Type managedPeerType, + ReadOnlySpan method, + ReadOnlySpan signature) + { + 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; + } + effectiveBaseType = typeManager.GetReplacementType (effectiveBaseType) ?? effectiveBaseType; + var info = typeManager.GetReplacementMethodInfo (effectiveBaseType, method, signature); + if (info != null) { + return info; + } + } + return null; + } + + 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; + } + effectiveBaseType = typeManager.GetReplacementType (effectiveBaseType) ?? effectiveBaseType; + 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..93f5cad6b51 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 @@ -7,13 +7,23 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; namespace Java.Interop { public partial class JniRuntime { + static unsafe string GetUtf8String (IntPtr value) + { + if (value == IntPtr.Zero) + return ""; + + return Encoding.UTF8.GetString (MemoryMarshal.CreateReadOnlySpanFromNullTerminated ((byte*)value)); + } + [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 ReplacementMethodInfo : IEquatable @@ -24,6 +34,33 @@ public struct ReplacementMethodInfo : IEquatable public string? TargetJniType {get; set;} public string? TargetJniMethodName {get; set;} public string? TargetJniMethodSignature {get; set;} + /// + /// Gets or sets a pointer to a NUL-terminated UTF-8 JNI type name. + /// + /// + /// Java.Interop does not own or free this memory. A non-zero pointer must remain valid + /// and unchanged for the lifetime of the associated , because + /// cached JNI type and method metadata may retain and dereference it. + /// + public IntPtr TargetJniTypeUtf8 {get; set;} + /// + /// Gets or sets a pointer to a NUL-terminated UTF-8 JNI method name. + /// + /// + /// Java.Interop does not own or free this memory. A non-zero pointer must remain valid + /// and unchanged for the lifetime of the associated , because + /// cached JNI type and method metadata may retain and dereference it. + /// + public IntPtr TargetJniMethodNameUtf8 {get; set;} + /// + /// Gets or sets a pointer to a NUL-terminated UTF-8 JNI method signature. + /// + /// + /// Java.Interop does not own or free this memory. A non-zero pointer must remain valid + /// and unchanged for the lifetime of the associated , because + /// cached JNI type and method metadata may retain and dereference it. + /// + public IntPtr TargetJniMethodSignatureUtf8 {get; set;} public int? TargetJniMethodParameterCount {get; set;} public bool TargetJniMethodInstanceToStatic {get; set;} @@ -43,20 +80,30 @@ public bool Equals (ReplacementMethodInfo other) string.Equals (TargetJniType, other.TargetJniType) && string.Equals (TargetJniMethodName, other.TargetJniMethodName) && string.Equals (TargetJniMethodSignature, other.TargetJniMethodSignature) && + TargetJniTypeUtf8 == other.TargetJniTypeUtf8 && + TargetJniMethodNameUtf8 == other.TargetJniMethodNameUtf8 && + TargetJniMethodSignatureUtf8 == other.TargetJniMethodSignatureUtf8 && TargetJniMethodParameterCount == other.TargetJniMethodParameterCount && TargetJniMethodInstanceToStatic == other.TargetJniMethodInstanceToStatic; } public override int GetHashCode () { - return (SourceJniType?.GetHashCode () ?? 0) ^ - (SourceJniMethodName?.GetHashCode () ?? 0) ^ - (SourceJniMethodSignature?.GetHashCode () ?? 0) ^ - (TargetJniType?.GetHashCode () ?? 0) ^ - (TargetJniMethodName?.GetHashCode () ?? 0) ^ - (TargetJniMethodSignature?.GetHashCode () ?? 0) ^ - (TargetJniMethodParameterCount?.GetHashCode () ?? 0) ^ - TargetJniMethodInstanceToStatic.GetHashCode (); + return HashCode.Combine ( + SourceJniType, + SourceJniMethodName, + SourceJniMethodSignature, + TargetJniType, + TargetJniMethodName, + TargetJniMethodSignature, + HashCode.Combine ( + TargetJniTypeUtf8, + TargetJniMethodNameUtf8, + TargetJniMethodSignatureUtf8, + TargetJniMethodParameterCount, + TargetJniMethodInstanceToStatic + ) + ); } public override string ToString () @@ -68,6 +115,9 @@ public override string ToString () $", {nameof (TargetJniType)} = \"{TargetJniType}\"" + $", {nameof (TargetJniMethodName)} = \"{TargetJniMethodName}\"" + $", {nameof (TargetJniMethodSignature)} = \"{TargetJniMethodSignature}\"" + + $", {nameof (TargetJniTypeUtf8)} = \"{GetUtf8String (TargetJniTypeUtf8)}\"" + + $", {nameof (TargetJniMethodNameUtf8)} = \"{GetUtf8String (TargetJniMethodNameUtf8)}\"" + + $", {nameof (TargetJniMethodSignatureUtf8)} = \"{GetUtf8String (TargetJniMethodSignatureUtf8)}\"" + $", {nameof (TargetJniMethodParameterCount)} = {TargetJniMethodParameterCount?.ToString () ?? "null"}" + $", {nameof (TargetJniMethodInstanceToStatic)} = {TargetJniMethodInstanceToStatic}" + $"}}"; @@ -77,6 +127,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 { @@ -241,14 +346,41 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type) protected virtual IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimple) => null; public string? GetReplacementType (string jniSimpleReference) + { + GetReplacementTypeInfo (jniSimpleReference, out var replacement, out var replacementUtf8); + return replacementUtf8 != IntPtr.Zero ? GetUtf8String (replacementUtf8) : replacement; + } + + protected virtual string? GetReplacementTypeCore (string jniSimpleReference) => null; + + internal void GetReplacementTypeInfo (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8) { AssertValid (); AssertSimpleReference (jniSimpleReference, nameof (jniSimpleReference)); - return GetReplacementTypeCore (jniSimpleReference); + GetReplacementTypeInfoCore (jniSimpleReference, out replacement, out replacementUtf8); + if (replacementUtf8 != IntPtr.Zero) + replacement = null; } - protected virtual string? GetReplacementTypeCore (string jniSimpleReference) => null; + /// + /// Resolves a replacement JNI type as either a managed string or stable NUL-terminated UTF-8 memory. + /// + /// + /// The default implementation preserves compatibility with string-based type managers by + /// calling once and setting + /// to zero. Overrides are authoritative and must return + /// results equivalent to for every reference. + /// A non-zero takes precedence over + /// . + /// Java.Interop does not own or free non-zero UTF-8 memory, which must remain valid and + /// unchanged for the lifetime of the associated . + /// + protected virtual void GetReplacementTypeInfoCore (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8) + { + replacement = GetReplacementTypeCore (jniSimpleReference); + replacementUtf8 = IntPtr.Zero; + } public IReadOnlyList? GetStaticMethodFallbackTypes (string jniSimpleReference) { @@ -286,6 +418,19 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type) return GetReplacementMethodInfoCore (jniSimpleReference, jniMethodName, jniMethodSignature); } + internal ReplacementMethodInfo? GetReplacementMethodInfo (IntPtr jniSimpleReferenceUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) + { + AssertValid (); + if (jniSimpleReferenceUtf8 == IntPtr.Zero) + throw new ArgumentNullException (nameof (jniSimpleReferenceUtf8)); + if (jniMethodName.IsEmpty) + throw new ArgumentNullException (nameof (jniMethodName)); + if (jniMethodSignature.IsEmpty) + throw new ArgumentNullException (nameof (jniMethodSignature)); + + return GetReplacementMethodInfoCore (jniSimpleReferenceUtf8, jniMethodName, jniMethodSignature); + } + /// /// Resolves member remapping without requiring name and signature strings. /// The default implementation preserves dispatch to the string overload. @@ -293,6 +438,47 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type) protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) => GetReplacementMethodInfoCore (jniSimpleReference, jniMethodName.ToString (), jniMethodSignature.ToString ()); + /// + /// Resolves member remapping with a source JNI type in stable NUL-terminated UTF-8 memory. + /// + protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (IntPtr jniSimpleReferenceUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) + => GetReplacementMethodInfoCore (GetUtf8String (jniSimpleReferenceUtf8), jniMethodName, jniMethodSignature); + + 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..be0f27a7884 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs @@ -53,6 +53,16 @@ public JniType (string classname) Initialize (ref peer, JniObjectReferenceOptions.CopyAndDispose); } + internal unsafe JniType (IntPtr classname) + { + if (classname == IntPtr.Zero) + throw new ArgumentNullException (nameof (classname)); + + var name = MemoryMarshal.CreateReadOnlySpanFromNullTerminated ((byte*)classname); + var peer = JniEnvironment.Types.FindClass (name); + Initialize (ref peer, JniObjectReferenceOptions.CopyAndDispose); + } + public JniType (ref JniObjectReference peerReference, JniObjectReferenceOptions transfer) { Initialize (ref peerReference, transfer); @@ -111,6 +121,17 @@ public static JniType GetCachedJniType ([NotNull] ref JniType? cachedType, strin return cachedType; } + internal static JniType GetCachedJniType ([NotNull] ref JniType? cachedType, IntPtr classname) + { + if (cachedType != null && cachedType.PeerReference.IsValid) + return cachedType; + var t = new JniType (classname); + if (Interlocked.CompareExchange (ref cachedType, t, null) != null) + t.Dispose (); + cachedType.RegisterWithRuntime (); + return cachedType; + } + public void Dispose () { if (!PeerReference.IsValid) @@ -496,6 +517,27 @@ internal bool TryGetInstanceMethod (ReadOnlySpan name, ReadOnlySpan return method != null; } + internal bool TryGetInstanceMethod (IntPtr name, ReadOnlySpan signature, [NotNullWhen (true)] out JniMethodInfo? method) + { + var id = GetMemberID (name, signature, MemberKind.InstanceMethod, throwOnError: false); + method = id == IntPtr.Zero ? null : CreateMethodInfo (name, signature, id, isStatic: false); + return method != null; + } + + internal bool TryGetInstanceMethod (IntPtr name, IntPtr signature, [NotNullWhen (true)] out JniMethodInfo? method) + { + var id = GetMemberID (name, signature, MemberKind.InstanceMethod, throwOnError: false); + method = id == IntPtr.Zero ? null : new JniMethodInfo (name, signature, id, isStatic: false); + return method != null; + } + + internal bool TryGetInstanceMethod (ReadOnlySpan name, IntPtr signature, [NotNullWhen (true)] out JniMethodInfo? method) + { + var id = GetMemberID (name, signature, MemberKind.InstanceMethod, throwOnError: false); + method = id == IntPtr.Zero ? null : CreateMethodInfo (name, signature, id, isStatic: false); + return method != null; + } + internal bool TryGetStaticMethod (ReadOnlySpan name, ReadOnlySpan signature, [NotNullWhen (true)] out JniMethodInfo? method) { var id = GetMemberID (name, signature, MemberKind.StaticMethod, throwOnError: false); @@ -503,6 +545,41 @@ internal bool TryGetStaticMethod (ReadOnlySpan name, ReadOnlySpan si return method != null; } + internal bool TryGetStaticMethod (IntPtr name, ReadOnlySpan signature, [NotNullWhen (true)] out JniMethodInfo? method) + { + var id = GetMemberID (name, signature, MemberKind.StaticMethod, throwOnError: false); + method = id == IntPtr.Zero ? null : CreateMethodInfo (name, signature, id, isStatic: true); + return method != null; + } + + internal bool TryGetStaticMethod (IntPtr name, IntPtr signature, [NotNullWhen (true)] out JniMethodInfo? method) + { + var id = GetMemberID (name, signature, MemberKind.StaticMethod, throwOnError: false); + method = id == IntPtr.Zero ? null : new JniMethodInfo (name, signature, id, isStatic: true); + return method != null; + } + + internal bool TryGetStaticMethod (ReadOnlySpan name, IntPtr signature, [NotNullWhen (true)] out JniMethodInfo? method) + { + var id = GetMemberID (name, signature, MemberKind.StaticMethod, throwOnError: false); + method = id == IntPtr.Zero ? null : CreateMethodInfo (name, signature, id, isStatic: true); + 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 @@ -512,6 +589,24 @@ static JniMethodInfo CreateMethodInfo (ReadOnlySpan name, ReadOnlySpan signature, IntPtr id, bool isStatic) + { +#if DEBUG + return new JniMethodInfo (name, signature.ToString (), id, isStatic); +#else + return new JniMethodInfo (id, isStatic); +#endif + } + + static JniMethodInfo CreateMethodInfo (ReadOnlySpan name, IntPtr signature, IntPtr id, bool isStatic) + { +#if DEBUG + return new JniMethodInfo (name.ToString (), signature, id, isStatic); +#else + return new JniMethodInfo (id, isStatic); +#endif + } + static JniFieldInfo CreateFieldInfo (ReadOnlySpan name, ReadOnlySpan signature, IntPtr id, bool isStatic) { #if DEBUG @@ -530,66 +625,100 @@ enum MemberKind { unsafe IntPtr GetMemberID (ReadOnlySpan name, ReadOnlySpan signature, MemberKind kind, bool throwOnError = true) { - AssertValid (); - // Match StringToCoTaskMemUTF8, including unpaired-surrogate replacement // and embedded-NUL termination, rather than changing to JNI modified UTF-8. int nameLength = checked (Encoding.UTF8.GetByteCount (name) + 1); - int signatureLength = checked (Encoding.UTF8.GetByteCount (signature) + 1); byte[]? rentedName = null; - byte[]? rentedSignature = null; 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 (name, nameBuffer); nameBuffer [nameLength - 1] = 0; - Encoding.UTF8.GetBytes (signature, signatureBuffer); - signatureBuffer [signatureLength - 1] = 0; - var env = JniEnvironment.EnvironmentPointer; - IntPtr id; fixed (byte* nameStart = nameBuffer) - fixed (byte* signatureStart = signatureBuffer) { - var namePtr = (IntPtr) nameStart; - var signaturePtr = (IntPtr) signatureStart; - id = kind switch { - MemberKind.InstanceMethod => JniNativeMethods.GetMethodID (env, PeerReference.Handle, namePtr, signaturePtr), - MemberKind.StaticMethod => JniNativeMethods.GetStaticMethodID (env, PeerReference.Handle, namePtr, signaturePtr), - MemberKind.InstanceField => JniNativeMethods.GetFieldID (env, PeerReference.Handle, namePtr, signaturePtr), - MemberKind.StaticField => JniNativeMethods.GetStaticFieldID (env, PeerReference.Handle, namePtr, signaturePtr), - _ => throw new ArgumentOutOfRangeException (nameof (kind)), - }; - } - var thrown = JniNativeMethods.ExceptionOccurred (env); - if (!throwOnError) { - if (thrown != IntPtr.Zero) { - JniEnvironment.Exceptions.ExceptionClear (); - JniEnvironment.References.RawDeleteLocalRef (env, thrown); - return IntPtr.Zero; - } - Debug.Assert (id != IntPtr.Zero); - return id; - } - var exception = JniEnvironment.GetExceptionForLastThrowable (thrown); - if (exception != null) - ExceptionDispatchInfo.Capture (exception).Throw (); - if (id == IntPtr.Zero) - throw new InvalidOperationException ("Should not be reached; JNI member lookup should have thrown!"); - return id; + return GetMemberID ((IntPtr)nameStart, signature, kind, throwOnError); } finally { if (rentedName != null) ArrayPool.Shared.Return (rentedName); - if (rentedSignature != null) - ArrayPool.Shared.Return (rentedSignature); + } + } + + unsafe IntPtr GetMemberID (IntPtr name, ReadOnlySpan signature, MemberKind kind, bool throwOnError = true) + { + if (name == IntPtr.Zero) + throw new ArgumentNullException (nameof (name)); + return GetMemberID (signature, name, false, kind, throwOnError); + } + + unsafe IntPtr GetMemberID (IntPtr name, IntPtr signature, MemberKind kind, bool throwOnError = true) + { + AssertValid (); + if (name == IntPtr.Zero) + throw new ArgumentNullException (nameof (name)); + if (signature == IntPtr.Zero) + throw new ArgumentNullException (nameof (signature)); + + var env = JniEnvironment.EnvironmentPointer; + IntPtr id = kind switch { + MemberKind.InstanceMethod => JniNativeMethods.GetMethodID (env, PeerReference.Handle, name, signature), + MemberKind.StaticMethod => JniNativeMethods.GetStaticMethodID (env, PeerReference.Handle, name, signature), + MemberKind.InstanceField => JniNativeMethods.GetFieldID (env, PeerReference.Handle, name, signature), + MemberKind.StaticField => JniNativeMethods.GetStaticFieldID (env, PeerReference.Handle, name, signature), + _ => throw new ArgumentOutOfRangeException (nameof (kind)), + }; + var thrown = JniNativeMethods.ExceptionOccurred (env); + if (!throwOnError) { + if (thrown != IntPtr.Zero) { + JniEnvironment.Exceptions.ExceptionClear (); + JniEnvironment.References.RawDeleteLocalRef (env, thrown); + return IntPtr.Zero; + } + Debug.Assert (id != IntPtr.Zero); + return id; + } + + var exception = JniEnvironment.GetExceptionForLastThrowable (thrown); + if (exception != null) + ExceptionDispatchInfo.Capture (exception).Throw (); + if (id == IntPtr.Zero) + throw new InvalidOperationException ("Should not be reached; JNI member lookup should have thrown!"); + return id; + } + + unsafe IntPtr GetMemberID (ReadOnlySpan name, IntPtr signature, MemberKind kind, bool throwOnError = true) + { + if (signature == IntPtr.Zero) + throw new ArgumentNullException (nameof (signature)); + return GetMemberID (name, signature, true, kind, throwOnError); + } + + unsafe IntPtr GetMemberID (ReadOnlySpan value, IntPtr otherValue, bool valueIsName, MemberKind kind, bool throwOnError) + { + int valueLength = checked (Encoding.UTF8.GetByteCount (value) + 1); + byte[]? rentedValue = null; + try { + if (valueLength > 512) + rentedValue = ArrayPool.Shared.Rent (valueLength); + + Span valueBuffer = rentedValue == null + ? stackalloc byte [valueLength] + : rentedValue.AsSpan (0, valueLength); + Encoding.UTF8.GetBytes (value, valueBuffer); + valueBuffer [valueLength - 1] = 0; + + fixed (byte* valueStart = valueBuffer) { + var valuePointer = (IntPtr)valueStart; + return valueIsName + ? GetMemberID (valuePointer, otherValue, kind, throwOnError) + : GetMemberID (otherValue, valuePointer, kind, throwOnError); + } + } finally { + if (rentedValue != null) + ArrayPool.Shared.Return (rentedValue); } } } diff --git a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt index 2a9e8e7d8d2..154b67e611e 100644 --- a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt @@ -1,5 +1,14 @@ #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.GetReplacementMethodInfoCore(nint jniSimpleReferenceUtf8, System.ReadOnlySpan jniMethodName, System.ReadOnlySpan jniMethodSignature) -> Java.Interop.JniRuntime.ReplacementMethodInfo? +virtual Java.Interop.JniRuntime.JniTypeManager.GetReplacementTypeInfoCore(string! jniSimpleReference, out string? replacement, out nint replacementUtf8) -> void +Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniMethodNameUtf8.get -> nint +Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniMethodNameUtf8.set -> void +Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniMethodSignatureUtf8.get -> nint +Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniMethodSignatureUtf8.set -> void +Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniTypeUtf8.get -> nint +Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniTypeUtf8.set -> void +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 +128,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..7df4b7ff57c 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,9 @@ + + + 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..7cbfef3a1f3 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 @@ -5,6 +5,8 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; using Java.Interop; @@ -33,6 +35,14 @@ static partial void CreateJavaVM () [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "JavaVMFixtureTypeManager intentionally uses reflection-backed type manager behavior for tests.")] class JavaVMFixtureTypeManager : JniRuntime.ReflectionJniTypeManager { + [Flags] + enum ReplacementMethodStorage { + Strings = 0, + TypeUtf8 = 1, + MethodUtf8 = 2, + SignatureUtf8 = 4, + } + Dictionary TypeMappings = new() { #if !NO_MARSHAL_MEMBER_BUILDER_SUPPORT [TestType.JniTypeName] = typeof (TestType), @@ -52,11 +62,23 @@ class JavaVMFixtureTypeManager : JniRuntime.ReflectionJniTypeManager { [MyDisposableObject.JniTypeName] = typeof (JavaDisposedObject), [MyJavaInterfaceImpl.JniTypeName] = typeof (MyJavaInterfaceImpl), }; + readonly Dictionary utf8Values = new (StringComparer.Ordinal); + readonly object utf8ValuesLock = new (); public JavaVMFixtureTypeManager () { } + protected override void Dispose (bool disposing) + { + lock (utf8ValuesLock) { + foreach (var value in utf8Values.Values) + Marshal.ZeroFreeCoTaskMemUTF8 (value); + utf8Values.Clear (); + } + base.Dispose (disposing); + } + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference)) @@ -115,23 +137,96 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type) Dictionary ReplacmentTypes = new() { ["net/dot/jni/test/RenameClassBase1"] = "net/dot/jni/test/RenameClassBase2", + [FieldRemapBase.JniTypeName] = FieldRemapBase.RuntimeJniTypeName, }; - protected override string? GetReplacementTypeCore (string jniSimpleReference) => - ReplacmentTypes.TryGetValue (jniSimpleReference, out var v) - ? v - : null; + string? trackedReplacementType; + int replacementTypeStringLookupCount; + int replacementTypeUtf8LookupCount; + + public void TrackReplacementTypeLookups (string jniSimpleReference) + { + trackedReplacementType = jniSimpleReference; + replacementTypeStringLookupCount = 0; + replacementTypeUtf8LookupCount = 0; + } + + public (int String, int Utf8) GetReplacementTypeLookupCounts () + => (replacementTypeStringLookupCount, replacementTypeUtf8LookupCount); + + protected override string? GetReplacementTypeCore (string jniSimpleReference) + { + if (jniSimpleReference == trackedReplacementType) + Interlocked.Increment (ref replacementTypeStringLookupCount); + return ReplacmentTypes.TryGetValue (jniSimpleReference, out var value) + ? value + : null; + } + + protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8) + { + if (jniSimpleReference == trackedReplacementType) + Interlocked.Increment (ref replacementTypeUtf8LookupCount); + replacement = null; + replacementUtf8 = ReplacmentTypes.TryGetValue (jniSimpleReference, out var value) + ? GetUtf8Value (value) + : IntPtr.Zero; + } - Dictionary<(string SourceType, string SourceName, string? SourceSignature), (string? TargetType, string? TargetName, string? TargetSignature, int? ParamCount, bool TurnStatic)> ReplacementMethods = new() { - [("java/lang/Object", "remappedToToString", "()Ljava/lang/String;")] = (null, "toString", null, null, false), - [("java/lang/Object", "remappedToStaticHashCode", null)] = ("net/dot/jni/test/ObjectHelper", "getHashCodeHelper", null, null, true), - [("java/lang/Runtime", "remappedToGetRuntime", null)] = (null, "getRuntime", null, null, false), + Dictionary<(string SourceType, string SourceName, string? SourceSignature), (string? TargetType, string? TargetName, string? TargetSignature, int? ParamCount, bool TurnStatic, ReplacementMethodStorage Storage)> ReplacementMethods = new() { + [("java/lang/Object", "remappedToToString", "()Ljava/lang/String;")] = (null, "toString", null, null, false, ReplacementMethodStorage.TypeUtf8 | ReplacementMethodStorage.MethodUtf8), + [("java/lang/Object", "remappedToStringWithUtf8Signature", "()Ljava/lang/String;")] = (null, "toString", "()Ljava/lang/String;", null, false, ReplacementMethodStorage.SignatureUtf8), + [("java/lang/Object", "remappedToStaticHashCode", null)] = ("net/dot/jni/test/ObjectHelper", "getHashCodeHelper", null, null, true, ReplacementMethodStorage.TypeUtf8 | ReplacementMethodStorage.MethodUtf8 | ReplacementMethodStorage.SignatureUtf8), + [("java/lang/Object", "remappedStaticAbs", "(I)I")] = ("java/lang/Math", "abs", null, null, false, ReplacementMethodStorage.Strings), + [("java/lang/Runtime", "remappedToGetRuntime", null)] = (null, "getRuntime", null, null, false, ReplacementMethodStorage.Strings), // 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), + [("net/dot/jni/test/RenameClassBase2", "hashCode", "()")] = ("net/dot/jni/test/RenameClassBase2", "myNewHashCode", null, null, false, ReplacementMethodStorage.TypeUtf8 | ReplacementMethodStorage.MethodUtf8), + + // 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, ReplacementMethodStorage.Strings), + [("java/lang/StringBuilder", "indexOf", "(Lnet/dot/jni/test/RenamedString;)I")] = (null, "indexOf", "(Ljava/lang/String;)I", null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "hiddenInstanceMethod", "()I")] = (null, "remappedInstanceMethod", null, null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "hiddenStaticMethod", "()I")] = (null, "remappedStaticMethod", null, null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "inheritedInstanceMethod", "()I")] = (null, "remappedInheritedInstanceMethod", null, null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "inheritedStaticMethod", "()I")] = (null, "remappedInheritedStaticMethod", null, null, false, ReplacementMethodStorage.Strings), + [(FieldRemapDerived.JniTypeName, "inheritedInstanceMethod", "()I")] = (null, "missingInstanceMethod", null, null, false, ReplacementMethodStorage.Strings), + [(FieldRemapDerived.JniTypeName, "inheritedStaticMethod", "()I")] = (null, "missingStaticMethod", null, null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "remappedSpecificity", "(I)I")] = (null, "specificityExact", "(I)I", null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "remappedSpecificity", "(I)")] = (null, "specificityParameters", "(I)V", null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "remappedSpecificity", null)] = (null, "specificityWildcard", null, null, false, ReplacementMethodStorage.Strings), + }; + + Dictionary<(string SourceType, string SourceName, string? SourceSignature), (string? TargetType, string? TargetName, string? TargetSignature)> ReplacementFields = new() { + [("java/lang/Math", "remappedToPi", "D")] = (null, "PI", null), + [("java/lang/Object", "remappedStaticPi", "D")] = ("java/lang/Math", "PI", null), + [("java/io/ByteArrayInputStream", "remappedToPos", "I")] = (null, "pos", null), + [(FieldRemapBase.RuntimeJniTypeName, "hiddenInstanceField", "Z")] = (null, "remappedInstanceField", null), + [(FieldRemapBase.RuntimeJniTypeName, "hiddenStaticField", "Ljava/lang/String;")] = (null, "remappedStaticField", null), + [(FieldRemapBase.RuntimeJniTypeName, "inheritedInstanceField", "Z")] = (null, "remappedInheritedInstanceField", null), + [(FieldRemapBase.RuntimeJniTypeName, "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}\")"); @@ -148,13 +243,16 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type) paramCount++; } // Console.Error.WriteLine ($"# jonp: found replacement: ({GetValue (r.TargetType)}, {GetValue (r.TargetName)}, {GetValue (r.TargetSignature)}, {r.ParamCount?.ToString () ?? "null"}, {r.IsStatic})"); + var targetType = r.TargetType ?? jniSourceType; + var targetName = r.TargetName ?? jniMethodName; + var targetSignature = r.Storage == ReplacementMethodStorage.Strings ? targetSig ?? jniMethodSignature : targetSig; return new JniRuntime.ReplacementMethodInfo { - SourceJniType = jniSourceType, - SourceJniMethodName = jniMethodName, - SourceJniMethodSignature = jniMethodSignature, - TargetJniType = r.TargetType ?? jniSourceType, - TargetJniMethodName = r.TargetName ?? jniMethodName, - TargetJniMethodSignature = targetSig ?? jniMethodSignature, + TargetJniType = r.Storage.HasFlag (ReplacementMethodStorage.TypeUtf8) ? null : targetType, + TargetJniMethodName = r.Storage.HasFlag (ReplacementMethodStorage.MethodUtf8) ? null : targetName, + TargetJniMethodSignature = r.Storage.HasFlag (ReplacementMethodStorage.SignatureUtf8) ? null : targetSignature, + TargetJniTypeUtf8 = r.Storage.HasFlag (ReplacementMethodStorage.TypeUtf8) ? GetUtf8Value (targetType) : IntPtr.Zero, + TargetJniMethodNameUtf8 = r.Storage.HasFlag (ReplacementMethodStorage.MethodUtf8) ? GetUtf8Value (targetName) : IntPtr.Zero, + TargetJniMethodSignatureUtf8 = r.Storage.HasFlag (ReplacementMethodStorage.SignatureUtf8) && targetSig != null ? GetUtf8Value (targetSig) : IntPtr.Zero, TargetJniMethodParameterCount = paramCount, TargetJniMethodInstanceToStatic = r.TurnStatic, }; @@ -170,5 +268,24 @@ string GetAlternateMethodSignature () // return value == null ? "null" : $"\"{value}\""; // } } + + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (IntPtr jniSourceTypeUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) + { + var jniSourceType = Marshal.PtrToStringUTF8 (jniSourceTypeUtf8); + if (jniSourceType == null) + throw new InvalidOperationException ("The test remapping source type is null."); + return GetReplacementMethodInfoCore (jniSourceType, jniMethodName.ToString (), jniMethodSignature.ToString ()); + } + + IntPtr GetUtf8Value (string value) + { + lock (utf8ValuesLock) { + if (utf8Values.TryGetValue (value, out var pointer)) + return pointer; + pointer = Marshal.StringToCoTaskMemUTF8 (value); + utf8Values.Add (value, pointer); + return pointer; + } + } } } 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..e61132ff1fa 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 @@ -18,6 +18,27 @@ public void Ctor_CanReferenceNonexistentType () JniPeerMembers.Dispose (members); } +#if !ANDROID // Android doesn't allow providing a custom TypeManager + [Test] + [NonParallelizable] + public void HandledReplacementTypeMissDoesNotUseStringFallback () + { + var typeManager = JavaVMFixture.TypeManager; + Assert.IsNotNull (typeManager); + typeManager.TrackReplacementTypeLookups ("java/lang/Double"); + try { + var members = new JniPeerMembers ("java/lang/Double", typeof (MyString)); + JniPeerMembers.Dispose (members); + + var counts = typeManager.GetReplacementTypeLookupCounts (); + Assert.AreEqual (1, counts.Utf8); + Assert.AreEqual (0, counts.String); + } finally { + typeManager.TrackReplacementTypeLookups (""); + } + } +#endif // !ANDROID + [Test] [Category ("TrimmableTypeMapUnsupported")] public void VirtualInvokeOnBaseInvokesMostDerivedJavaMethod () @@ -205,6 +226,234 @@ public void MethodLookupForNonexistentStaticMethodWillTryFallbacks () } } +#if !__ANDROID__ + // These tests use JavaVMFixture's custom JniTypeManager, which Android does not support. + [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 ReplacedStaticFieldRetainsTargetOwner () + { + Assert.AreEqual (global::System.Math.PI, JavaLangRemappingTestObject.remappedStaticPi ()); + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void ReplacedStaticMethodRetainsTargetOwner () + { + Assert.AreEqual (5, JavaLangRemappingTestObject.remappedStaticAbs (-5)); + } + + [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.RuntimeJniTypeName); + 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.RuntimeJniTypeName); + 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 DeclaredInstanceMethodHidesBaseMethodRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapDerived.JniTypeName); + var expected = type.GetInstanceMethod ("hiddenInstanceMethod", "()I"); + var remapped = type.GetInstanceMethod ("remappedInstanceMethod", "()I"); + var actual = members.InstanceMethods.GetMethodInfo ("hiddenInstanceMethod.()I"); + + Assert.AreEqual (expected.ID, actual.ID); + Assert.AreNotEqual (remapped.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void DeclaredStaticMethodHidesBaseMethodRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapDerived.JniTypeName); + var expected = type.GetStaticMethod ("hiddenStaticMethod", "()I"); + var remapped = type.GetStaticMethod ("remappedStaticMethod", "()I"); + var actual = members.StaticMethods.GetMethodInfo ("hiddenStaticMethod.()I"); + + Assert.AreEqual (expected.ID, actual.ID); + Assert.AreNotEqual (remapped.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void FailedCurrentInstanceMethodRemapFallsBackToRenamedBaseRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapBase.RuntimeJniTypeName); + var expected = type.GetInstanceMethod ("remappedInheritedInstanceMethod", "()I"); + var actual = members.InstanceMethods.GetMethodInfo ("inheritedInstanceMethod.()I"); + + Assert.AreEqual (expected.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void FailedCurrentStaticMethodRemapFallsBackToRenamedBaseRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapBase.RuntimeJniTypeName); + var expected = type.GetStaticMethod ("remappedInheritedStaticMethod", "()I"); + var actual = members.StaticMethods.GetMethodInfo ("inheritedStaticMethod.()I"); + + Assert.AreEqual (expected.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public unsafe void MethodRemappingPrefersSpecificSignatures () + { + var members = new JniPeerMembers (FieldRemapBase.JniTypeName, typeof (FieldRemapBase)); + try { + var intArgument = new JniArgumentValue (1); + Assert.AreEqual (101, members.StaticMethods.InvokeInt32Method ("remappedSpecificity.(I)I", &intArgument)); + + intArgument = new JniArgumentValue (2); + members.StaticMethods.InvokeVoidMethod ("remappedSpecificity.(I)V", &intArgument); + using var type = new JniType (FieldRemapBase.RuntimeJniTypeName); + var valueField = type.GetStaticField ("specificityValue", "I"); + Assert.AreEqual (202, JniEnvironment.StaticFields.GetStaticIntField (type.PeerReference, valueField)); + + var longArgument = new JniArgumentValue (3L); + Assert.AreEqual (303, members.StaticMethods.InvokeInt32Method ("remappedSpecificity.(J)I", &longArgument)); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + 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); + } +#endif // !__ANDROID__ + [Test] [Category ("NativeAOTIgnore")] [Category ("TrimmableTypeMapUnsupported")] @@ -225,6 +474,16 @@ public void ReplaceInstanceMethodName () JniObjectReference.Dispose (ref r); } + [Test] + [Category ("NativeAOTIgnore")] + public void ReplaceInstanceMethodWithUtf8Signature () + { + using var o = new JavaLangRemappingTestObject (); + // Shouldn't throw; should instead invoke Object.toString() + var r = o.remappedToStringWithUtf8Signature (); + JniObjectReference.Dispose (ref r); + } + [Test] [Category ("NativeAOTIgnore")] public void ReplaceStaticMethodName () @@ -345,11 +604,60 @@ public unsafe JniObjectReference remappedToToString () return _members.InstanceMethods.InvokeNonvirtualObjectMethod (id, this, null); } + public unsafe JniObjectReference remappedToStringWithUtf8Signature () + { + const string id = "remappedToStringWithUtf8Signature.()Ljava/lang/String;"; + return _members.InstanceMethods.InvokeNonvirtualObjectMethod (id, this, null); + } + public unsafe int remappedToStaticHashCode () { const string id = "remappedToStaticHashCode.()I"; return _members.InstanceMethods.InvokeVirtualInt32Method (id, this, null); } + + public static unsafe double remappedStaticPi () + { + return _members.StaticFields.GetDoubleValue ("remappedStaticPi.D"); + } + + public static unsafe int remappedStaticAbs (int value) + { + var argument = new JniArgumentValue (value); + return _members.StaticMethods.InvokeInt32Method ("remappedStaticAbs.(I)I", &argument); + } + } + + [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"; + internal const string RuntimeJniTypeName = "net/dot/jni/test/FieldRemapRenamedBase"; + 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)] diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs index feb00c0e8e4..f2dd684a8dd 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs @@ -1,4 +1,6 @@ +using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using Java.Interop; @@ -9,6 +11,55 @@ namespace Java.InteropTests { [TestFixture] public class JniRuntimeJniTypeManagerTests : JavaVMFixture { + [Test] + public void ReplacementMethodInfoToStringConvertsUtf8Pointers () + { + var type = Marshal.StringToCoTaskMemUTF8 ("java/lang/Object"); + var name = Marshal.StringToCoTaskMemUTF8 ("toString"); + var signature = Marshal.StringToCoTaskMemUTF8 ("()Ljava/lang/String;"); + try { + var info = new JniRuntime.ReplacementMethodInfo { + TargetJniTypeUtf8 = type, + TargetJniMethodNameUtf8 = name, + TargetJniMethodSignatureUtf8 = signature, + }; + + var value = info.ToString (); + + Assert.That (value, Does.Contain ("TargetJniTypeUtf8 = \"java/lang/Object\"")); + Assert.That (value, Does.Contain ("TargetJniMethodNameUtf8 = \"toString\"")); + Assert.That (value, Does.Contain ("TargetJniMethodSignatureUtf8 = \"()Ljava/lang/String;\"")); + } finally { + Marshal.ZeroFreeCoTaskMemUTF8 (type); + Marshal.ZeroFreeCoTaskMemUTF8 (name); + Marshal.ZeroFreeCoTaskMemUTF8 (signature); + } + } + + [Test] + public void ReplacementTypeInfoSupportsStringAndUtf8Results () + { + using var stringManager = new StringReplacementTypeManager (); + stringManager.GetReplacementTypeInfo ("java/lang/String", out var stringReplacement, out var stringReplacementUtf8); + Assert.AreEqual ("java/lang/Object", stringReplacement); + Assert.AreEqual (IntPtr.Zero, stringReplacementUtf8); + Assert.AreEqual (1, stringManager.LookupCount); + + var utf8Value = Marshal.StringToCoTaskMemUTF8 ("java/lang/Object"); + try { + using var utf8Manager = new Utf8ReplacementTypeManager (utf8Value); + utf8Manager.GetReplacementTypeInfo ("java/lang/Double", out var missingReplacement, out var missingReplacementUtf8); + Assert.IsNull (missingReplacement); + Assert.AreEqual (IntPtr.Zero, missingReplacementUtf8); + utf8Manager.GetReplacementTypeInfo ("java/lang/String", out var replacement, out var replacementUtf8); + Assert.IsNull (replacement); + Assert.AreEqual (utf8Value, replacementUtf8); + Assert.AreEqual ("java/lang/Object", utf8Manager.GetReplacementType ("java/lang/String")); + } finally { + Marshal.ZeroFreeCoTaskMemUTF8 (utf8Value); + } + } + [Test] [Category ("TrimmableTypeMapUnsupported")] [RequiresDynamicCode ("This test uses ReflectionJniTypeManager, which is reflection-based and not NativeAOT-compatible.")] @@ -34,5 +85,32 @@ public MyTypeManager () { } } + + class StringReplacementTypeManager : JniRuntime.JniTypeManager { + + public int LookupCount { get; private set; } + + protected override string GetReplacementTypeCore (string jniSimpleReference) + { + LookupCount++; + return jniSimpleReference == "java/lang/String" ? "java/lang/Object" : null; + } + } + + class Utf8ReplacementTypeManager : JniRuntime.JniTypeManager { + + readonly IntPtr replacement; + + public Utf8ReplacementTypeManager (IntPtr replacement) + { + this.replacement = replacement; + } + + protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string replacement, out IntPtr replacementUtf8) + { + replacement = jniSimpleReference == "java/lang/String" ? "ignored/string/value" : null; + replacementUtf8 = jniSimpleReference == "java/lang/String" ? this.replacement : IntPtr.Zero; + } + } } } 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..c1ea18bf978 --- /dev/null +++ b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java @@ -0,0 +1,23 @@ +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"; + + public int hiddenInstanceMethod () { return 10; } + public int remappedInstanceMethod () { return 11; } + public int remappedInheritedInstanceMethod () { return 12; } + public static int hiddenStaticMethod () { return 20; } + public static int remappedStaticMethod () { return 21; } + public static int remappedInheritedStaticMethod () { return 22; } + + public static int specificityExact (int value) { return value + 100; } + public static int specificityValue; + public static void specificityParameters (int value) { specificityValue = value + 200; } + public static int specificityWildcard (long value) { return (int)value + 300; } +} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java new file mode 100644 index 00000000000..4116fb21f2b --- /dev/null +++ b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java @@ -0,0 +1,9 @@ +package net.dot.jni.test; + +public class FieldRemapDerived extends FieldRemapBase +{ + public boolean hiddenInstanceField; + public static String hiddenStaticField = "derived"; + public int hiddenInstanceMethod () { return 12; } + public static int hiddenStaticMethod () { return 22; } +} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapRenamedBase.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapRenamedBase.java new file mode 100644 index 00000000000..d1a06f46ca5 --- /dev/null +++ b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapRenamedBase.java @@ -0,0 +1,23 @@ +package net.dot.jni.test; + +public class FieldRemapRenamedBase +{ + public boolean hiddenInstanceField; + public boolean remappedInstanceField; + public boolean remappedInheritedInstanceField; + public static String hiddenStaticField = "base"; + public static String remappedStaticField = "remapped"; + public static String remappedInheritedStaticField = "inherited"; + + public int hiddenInstanceMethod () { return 10; } + public int remappedInstanceMethod () { return 11; } + public int remappedInheritedInstanceMethod () { return 12; } + public static int hiddenStaticMethod () { return 20; } + public static int remappedStaticMethod () { return 21; } + public static int remappedInheritedStaticMethod () { return 22; } + + public static int specificityExact (int value) { return value + 100; } + public static int specificityValue; + public static void specificityParameters (int value) { specificityValue = value + 200; } + public static int specificityWildcard (long value) { return (int)value + 300; } +} diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 41c8d513a9f..327ce0d55e8 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -382,6 +382,12 @@ protected override IEnumerable GetSimpleReferences (Type type) return JniRemappingLookup.GetReplacementType (jniSimpleReference); } + protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8) + { + replacement = null; + replacementUtf8 = JniRemappingLookup.GetReplacementTypeUtf8 (jniSimpleReference); + } + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) { return JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); @@ -392,6 +398,11 @@ protected override IEnumerable GetSimpleReferences (Type type) return JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); } + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (IntPtr jniSourceTypeUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) + { + return JniRemappingLookup.GetReplacementMethodInfo (jniSourceTypeUtf8, jniMethodName, jniMethodSignature); + } + protected override Type? GetInvokerTypeCore (Type type) { if (type.IsInterface || type.IsAbstract) { diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 9e6c6fa599d..889565a3d89 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -31,7 +31,7 @@ internal struct JnienvInitializeArgs { public int packageNamingPolicy; public byte ioExceptionType; public int jniAddNativeMethodRegistrationAttributePresent; - public bool jniRemappingInUse; + public IntPtr jniRemappingData; public bool marshalMethodsEnabled; public IntPtr grefGCUserPeerable; public IntPtr propagateUncaughtExceptionFn; @@ -39,7 +39,6 @@ internal struct JnienvInitializeArgs { } #pragma warning restore 0649 - internal static bool jniRemappingInUse; internal static bool MarshalMethodsEnabled; internal static bool PropagateExceptions; internal static BoundExceptionType BoundExceptionType; @@ -205,7 +204,7 @@ static void InitializeCommonState (JnienvInitializeArgs args) Logger.SetLogCategories ((LogCategories)args.logCategories); gref_gc_threshold = args.grefGcThreshold; - jniRemappingInUse = args.jniRemappingInUse; + JniRemappingLookup.Initialize (args.jniRemappingData); MarshalMethodsEnabled = args.marshalMethodsEnabled; java_class_loader = args.grefLoader; diff --git a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs index eaad49ce09f..cb00f9c86c3 100644 --- a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs +++ b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs @@ -60,18 +60,6 @@ internal unsafe static partial class RuntimeNativeMethods [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] internal static partial int _monodroid_weak_gref_dec (); - [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)] - [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] - internal static partial IntPtr _monodroid_lookup_replacement_type (string jniSimpleReference); - - [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)] - [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_replacement_method_info (string jniSourceType, byte* jniMethodName, byte* jniMethodSignature); - [LibraryImport (RuntimeConstants.InternalDllName)] [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] internal static partial void _monodroid_detect_cpu_and_architecture (ref ushort built_for_cpu, ref ushort running_on_cpu, ref byte is64bit); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 1c026b85e9d..210f29675ff 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -15,6 +16,29 @@ sealed class JavaMarshalValueManager : JniRuntime.ReflectionJniValueManager const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; + static readonly Type[] JIConstructorSignature = new Type [] { typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions) }; + static readonly ConcurrentDictionary ActivationConstructorCache = new ConcurrentDictionary (1, 3); + + enum ActivationConstructorKind + { + Missing, + XA, + JI, + } + + readonly record struct ActivationConstructor (ConstructorInfo? Constructor, ActivationConstructorKind Kind); + + // The GetOrAdd factory's key parameter cannot carry constructor-preservation annotations. + readonly struct AnnotatedType + { + public AnnotatedType ([DynamicallyAccessedMembers (Constructors)] Type type) + { + Type = type; + } + + [DynamicallyAccessedMembers (Constructors)] + public Type Type { get; } + } public JavaMarshalValueManager () { @@ -76,8 +100,12 @@ protected override bool TryConstructPeer ( [DynamicallyAccessedMembers (Constructors)] Type type) { - var c = type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); - if (c != null) { + var activation = GetActivationConstructor (type); + var c = activation.Constructor; + if (c == null) + return false; + + if (activation.Kind == ActivationConstructorKind.XA) { var args = new object[] { reference.Handle, JniHandleOwnership.DoNotTransfer, @@ -86,7 +114,28 @@ protected override bool TryConstructPeer ( JniObjectReference.Dispose (ref reference, options); return true; } - return base.TryConstructPeer (self, ref reference, options, type); + + // Preserve ReflectionJniValueManager's JI fallback, including ref argument copy-back. + var jiArgs = new object[] { reference, options }; + c.Invoke (self, jiArgs); + reference = (JniObjectReference) jiArgs [0]; + JniObjectReference.Dispose (ref reference, options); + return true; + } + + static ActivationConstructor GetActivationConstructor ([DynamicallyAccessedMembers (Constructors)] Type type) + { + return ActivationConstructorCache.GetOrAdd (type, + static (_, state) => { + var constructor = state.Type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); + if (constructor != null) + return new ActivationConstructor (constructor, ActivationConstructorKind.XA); + + constructor = state.Type.GetConstructor (ActivationConstructorBindingFlags, null, JIConstructorSignature, null); + return new ActivationConstructor ( + constructor, + constructor == null ? ActivationConstructorKind.Missing : ActivationConstructorKind.JI); + }, new AnnotatedType (type)); } protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (true)] out object? result) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs index e03745ff054..67859211720 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs @@ -2,6 +2,7 @@ using System; using System.Buffers; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; @@ -12,17 +13,101 @@ 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 - struct JniRemappingReplacementMethod + const int AsciiComparisonChunkSize = 16; + + unsafe struct NativeJniRemappingString + { + public uint length; + public byte* str; + } + + unsafe struct NativeJniRemappingReplacementMethod { - public string? target_type; - public string? target_name; - public bool is_static; + public byte* target_type; + public byte* target_name; + public byte* target_signature; + public byte is_static; + } + + unsafe struct NativeJniRemappingReplacementField + { + public byte* target_type; + public byte* target_name; + public byte* target_signature; + } + + unsafe struct NativeJniRemappingIndexMethodEntry + { + public NativeJniRemappingString name; + public NativeJniRemappingString signature; + public NativeJniRemappingReplacementMethod replacement; + } + + unsafe struct NativeJniRemappingIndexTypeEntry + { + public NativeJniRemappingString name; + public uint method_count; + public NativeJniRemappingIndexMethodEntry* methods; + } + + unsafe struct NativeJniRemappingTypeReplacementEntry + { + public NativeJniRemappingString name; + public byte* replacement; + } + + unsafe struct NativeJniRemappingIndexFieldEntry + { + public NativeJniRemappingString name; + public NativeJniRemappingString signature; + public NativeJniRemappingReplacementField replacement; + } + + unsafe struct NativeJniRemappingIndexFieldTypeEntry + { + public NativeJniRemappingString name; + public uint field_count; + public NativeJniRemappingIndexFieldEntry* fields; + } + + unsafe struct NativeJniRemappingData + { + public NativeJniRemappingTypeReplacementEntry* type_replacements; + public NativeJniRemappingTypeReplacementEntry* reverse_type_replacements; + public NativeJniRemappingIndexTypeEntry* method_replacement_index; + public NativeJniRemappingIndexFieldTypeEntry* field_replacement_index; + public uint type_replacement_count; + public uint reverse_type_replacement_count; + public uint method_replacement_index_count; + public uint field_replacement_index_count; + } + + static unsafe NativeJniRemappingData* nativeData; + static bool isInUse; + static readonly ConcurrentDictionary reverseTypes = new (StringComparer.Ordinal); + + internal static unsafe void Initialize (IntPtr data) + { + reverseTypes.Clear (); + if (data == IntPtr.Zero) { + isInUse = false; + return; + } + + nativeData = (NativeJniRemappingData*)data; + isInUse = + nativeData->type_replacement_count > 0 || + nativeData->reverse_type_replacement_count > 0 || + nativeData->method_replacement_index_count > 0 || + nativeData->field_replacement_index_count > 0; } -#pragma warning restore CS0649 internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSimpleReference, bool useReplacementTypes) { + if (useReplacementTypes) { + jniSimpleReference = GetReverseType (jniSimpleReference) ?? jniSimpleReference; + } + int slash = jniSimpleReference.LastIndexOf ('/'); var desugarType = slash > 0 ? $"{jniSimpleReference.Substring (0, slash + 1)}Desugar{jniSimpleReference.Substring (slash + 1)}" @@ -30,7 +115,6 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi var typeWithPrefix = $"{desugarType}$_CC"; var typeWithSuffix = $"{jniSimpleReference}$-CC"; - var replacements = new[] { useReplacementTypes ? GetReplacementType (typeWithPrefix) ?? typeWithPrefix : typeWithPrefix, useReplacementTypes ? GetReplacementType (typeWithSuffix) ?? typeWithSuffix : typeWithSuffix, @@ -44,95 +128,474 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi return replacements; } - internal static string? GetReplacementType (string? jniSimpleReference) + internal static unsafe string? GetReplacementType (string? jniSimpleReference) { - if (jniSimpleReference is null || !JNIEnvInit.jniRemappingInUse) { - return null; - } + IntPtr replacement = GetReplacementTypeUtf8 (jniSimpleReference); + return replacement == IntPtr.Zero ? null : Marshal.PtrToStringUTF8 (replacement); + } + + internal static unsafe IntPtr GetReplacementTypeUtf8 (string? jniSimpleReference) + { + if (jniSimpleReference is null || !isInUse || jniSimpleReference.Length == 0) + return IntPtr.Zero; + + NativeJniRemappingData* data = nativeData; + if (data == null) + throw new InvalidOperationException ("JNI remapping data has not been initialized."); + + return (IntPtr)LookupType (data->type_replacements, data->type_replacement_count, jniSimpleReference); + } - IntPtr ret = RuntimeNativeMethods._monodroid_lookup_replacement_type (jniSimpleReference); - if (ret == IntPtr.Zero) { + internal static unsafe string? GetReverseType (string? jniSimpleReference) + { + if (jniSimpleReference is null || !isInUse || jniSimpleReference.Length == 0) return null; - } - return Marshal.PtrToStringAnsi (ret); + string replacement = reverseTypes.GetOrAdd (jniSimpleReference, static source => LookupReverseType (source)); + return string.Equals (replacement, jniSimpleReference, StringComparison.Ordinal) ? null : replacement; + } + + static unsafe string LookupReverseType (string jniSimpleReference) + { + NativeJniRemappingData* data = nativeData; + if (data == null) + throw new InvalidOperationException ("JNI remapping data has not been initialized."); + + byte* replacement = LookupType ( + data->reverse_type_replacements, + data->reverse_type_replacement_count, + jniSimpleReference); + return replacement == null + ? jniSimpleReference + : Marshal.PtrToStringUTF8 ((IntPtr)replacement) ?? jniSimpleReference; } internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (string jniSourceType, string jniMethodName, string jniMethodSignature) => GetReplacementMethodInfo (jniSourceType, jniMethodName.AsSpan (), jniMethodSignature.AsSpan ()); internal static unsafe JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (string jniSourceType, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) + => GetReplacementMethodInfo (jniSourceType.AsSpan (), IntPtr.Zero, jniMethodName, jniMethodSignature); + + internal static unsafe JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (IntPtr jniSourceTypeUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) + { + if (jniSourceTypeUtf8 == IntPtr.Zero) + throw new ArgumentNullException (nameof (jniSourceTypeUtf8)); + return GetReplacementMethodInfo (default, jniSourceTypeUtf8, jniMethodName, jniMethodSignature); + } + + static unsafe JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo ( + ReadOnlySpan jniSourceType, + IntPtr jniSourceTypeUtf8, + ReadOnlySpan jniMethodName, + ReadOnlySpan jniMethodSignature) { - if (!JNIEnvInit.jniRemappingInUse) { + if (!isInUse) return null; - } - int nameLength = checked (Encoding.UTF8.GetByteCount (jniMethodName) + 1); - int signatureLength = checked (Encoding.UTF8.GetByteCount (jniMethodSignature) + 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 (jniMethodName, nameBuffer); - nameBuffer [nameLength - 1] = 0; - Encoding.UTF8.GetBytes (jniMethodSignature, signatureBuffer); - signatureBuffer [signatureLength - 1] = 0; - - fixed (byte* name = nameBuffer) - fixed (byte* signature = signatureBuffer) { - retInfo = RuntimeNativeMethods._monodroid_lookup_replacement_method_info (jniSourceType, name, signature); - } - } finally { - if (rentedName != null) - ArrayPool.Shared.Return (rentedName); - if (rentedSignature != null) - ArrayPool.Shared.Return (rentedSignature); - } - if (retInfo == IntPtr.Zero) { + NativeJniRemappingData* data = nativeData; + if (data == null) + throw new InvalidOperationException ("JNI remapping data has not been initialized."); + + byte* matchedSignature; + NativeJniRemappingReplacementMethod* method = jniSourceTypeUtf8 == IntPtr.Zero + ? LookupMethod (data, jniSourceType, jniMethodName, jniMethodSignature, out matchedSignature) + : LookupMethod (data, GetNullTerminatedUtf8Span (jniSourceTypeUtf8), jniMethodName, jniMethodSignature, out matchedSignature); + + if (method == null) return null; + if (method->target_type == null || method->target_name == null) { + string sourceType = GetSourceTypeForDiagnostics (jniSourceType, jniSourceTypeUtf8); + throw new InvalidOperationException ( + $"JNI remapping entry for `{sourceType}.{jniMethodName}{jniMethodSignature}` is missing target information."); } - var method = Marshal.PtrToStructure (retInfo); - var targetType = method.target_type ?? throw new InvalidOperationException ( - $"JNI remapping entry for `{jniSourceType}.{jniMethodName}{jniMethodSignature}` is missing a target type."); - var targetName = method.target_name ?? throw new InvalidOperationException ( - $"JNI remapping entry for `{jniSourceType}.{jniMethodName}{jniMethodSignature}` is missing a target method name."); - var sourceSignature = jniMethodSignature.ToString (); - var newSignature = sourceSignature; - int? paramCount = null; - if (method.is_static) { + bool isStatic = method->is_static != 0; + string? targetSignature = null; + if (isStatic) { + string sourceType = GetSourceTypeForDiagnostics (jniSourceType, jniSourceTypeUtf8); + string sourceSignature = jniMethodSignature.ToString (); paramCount = JniMemberSignature.GetParameterCountFromMethodSignature (sourceSignature) + 1; - newSignature = $"(L{jniSourceType};" + sourceSignature.Substring ("(".Length); + targetSignature = method->target_signature == null + ? $"(L{sourceType};" + sourceSignature.Substring ("(".Length) + : Marshal.PtrToStringUTF8 ((IntPtr)method->target_signature); } + var ret = new JniRuntime.ReplacementMethodInfo { + TargetJniTypeUtf8 = (IntPtr)method->target_type, + TargetJniMethodNameUtf8 = (IntPtr)method->target_name, + TargetJniMethodSignature = targetSignature, + TargetJniMethodSignatureUtf8 = isStatic + ? IntPtr.Zero + : (IntPtr)(method->target_signature == null ? matchedSignature : method->target_signature), + TargetJniMethodParameterCount = paramCount, + TargetJniMethodInstanceToStatic = isStatic, + }; + if (Logger.LogAssembly) { - var message = $"Remapping method `{jniSourceType}.{jniMethodName}{jniMethodSignature}` to " + - $"`{targetType}.{targetName}{newSignature}`; " + - $"param-count: {paramCount}; instance-to-static? {method.is_static}"; + string sourceType = GetSourceTypeForDiagnostics (jniSourceType, jniSourceTypeUtf8); + string targetType = Marshal.PtrToStringUTF8 ((IntPtr)method->target_type) ?? ""; + string targetName = Marshal.PtrToStringUTF8 ((IntPtr)method->target_name) ?? ""; + string effectiveTargetSignature = targetSignature ?? + (matchedSignature == null ? jniMethodSignature.ToString () : Marshal.PtrToStringUTF8 ((IntPtr)matchedSignature) ?? ""); + var message = $"Remapping method `{sourceType}.{jniMethodName}{jniMethodSignature}` to " + + $"`{targetType}.{targetName}{effectiveTargetSignature}`; " + + $"param-count: {paramCount}; instance-to-static? {isStatic}"; Logger.Log (LogLevel.Debug, "monodroid-assembly", message); } - return new JniRuntime.ReplacementMethodInfo { - SourceJniType = jniSourceType, - SourceJniMethodName = jniMethodName.ToString (), - SourceJniMethodSignature = sourceSignature, - TargetJniType = targetType, - TargetJniMethodName = targetName, - TargetJniMethodSignature = newSignature, - TargetJniMethodParameterCount = paramCount, - TargetJniMethodInstanceToStatic = method.is_static, + return ret; + } + + 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 (!isInUse) + return null; + + NativeJniRemappingData* data = nativeData; + if (data == null) + throw new InvalidOperationException ("JNI remapping data has not been initialized."); + + NativeJniRemappingReplacementField* field = LookupField ( + data, + jniSourceType, + jniFieldName, + jniFieldSignature); + if (field == null) + return null; + if (field->target_type == null || field->target_name == null) { + throw new InvalidOperationException ( + $"JNI remapping entry for `{jniSourceType}.{jniFieldName}:{jniFieldSignature}` is missing target information."); + } + + string targetType = Marshal.PtrToStringUTF8 ((IntPtr)field->target_type) ?? ""; + string targetName = Marshal.PtrToStringUTF8 ((IntPtr)field->target_name) ?? ""; + string targetSignature = field->target_signature == null + ? jniFieldSignature.ToString () + : Marshal.PtrToStringUTF8 ((IntPtr)field->target_signature) ?? ""; + + if (Logger.LogAssembly) { + var message = $"Remapping field `{jniSourceType}.{jniFieldName}:{jniFieldSignature}` to " + + $"`{targetType}.{targetName}:{targetSignature}`"; + Logger.Log (LogLevel.Debug, "monodroid-assembly", message); + } + + return new JniRuntime.ReplacementFieldInfo { + SourceJniType = jniSourceType, + SourceJniFieldName = jniFieldName.ToString (), + SourceJniFieldSignature = jniFieldSignature.ToString (), + TargetJniType = targetType, + TargetJniFieldName = targetName, + TargetJniFieldSignature = targetSignature, }; } + + static string GetSourceTypeForDiagnostics (ReadOnlySpan jniSourceType, IntPtr jniSourceTypeUtf8) + { + if (jniSourceTypeUtf8 == IntPtr.Zero) + return jniSourceType.ToString (); + return Marshal.PtrToStringUTF8 (jniSourceTypeUtf8) ?? ""; + } + + static unsafe bool Equal (NativeJniRemappingString value, ReadOnlySpan key) + { + return value.length == (uint)key.Length && + new ReadOnlySpan (value.str, key.Length).SequenceEqual (key); + } + + static unsafe bool Equal (NativeJniRemappingString value, ReadOnlySpan key, bool keyIsAscii) + { + ReadOnlySpan utf8 = new ReadOnlySpan (value.str, checked ((int)value.length)); + return keyIsAscii + ? Ascii.Equals (utf8, key) + : CompareUtf8ToUtf16 (utf8, key) == 0; + } + + static unsafe int Compare (NativeJniRemappingString value, ReadOnlySpan key) + { + return new ReadOnlySpan (value.str, checked ((int)value.length)).SequenceCompareTo (key); + } + + static unsafe int Compare (NativeJniRemappingString value, ReadOnlySpan key, bool keyIsAscii) + { + ReadOnlySpan utf8 = new ReadOnlySpan (value.str, checked ((int)value.length)); + return keyIsAscii ? CompareUtf8ToAscii (utf8, key) : CompareUtf8ToUtf16 (utf8, key); + } + + static int CompareUtf8ToAscii (ReadOnlySpan utf8, ReadOnlySpan ascii) + { + int commonLength = Math.Min (utf8.Length, ascii.Length); + int offset = 0; + while (commonLength - offset >= AsciiComparisonChunkSize) { + ReadOnlySpan utf8Chunk = utf8.Slice (offset, AsciiComparisonChunkSize); + ReadOnlySpan asciiChunk = ascii.Slice (offset, AsciiComparisonChunkSize); + if (!Ascii.Equals (utf8Chunk, asciiChunk)) { + for (int i = 0; i < AsciiComparisonChunkSize; i++) { + int result = utf8Chunk [i].CompareTo ((byte)asciiChunk [i]); + if (result != 0) + return result; + } + } + offset += AsciiComparisonChunkSize; + } + + ReadOnlySpan utf8Tail = utf8.Slice (offset, commonLength - offset); + ReadOnlySpan asciiTail = ascii.Slice (offset, commonLength - offset); + if (!Ascii.Equals (utf8Tail, asciiTail)) { + for (int i = 0; i < utf8Tail.Length; i++) { + int result = utf8Tail [i].CompareTo ((byte)asciiTail [i]); + if (result != 0) + return result; + } + } + return utf8.Length.CompareTo (ascii.Length); + } + + static int CompareUtf8ToUtf16 (ReadOnlySpan utf8, ReadOnlySpan utf16) + { + // Generated table strings and runtime JNI names are well-formed Unicode. Replacement behavior + // below only keeps the comparator deterministic if malformed input reaches this internal API. + while (!utf8.IsEmpty && !utf16.IsEmpty) { + while (!utf8.IsEmpty && !utf16.IsEmpty && utf8 [0] < 0x80 && utf16 [0] < 0x80) { + int result = utf8 [0].CompareTo ((byte)utf16 [0]); + if (result != 0) + return result; + utf8 = utf8.Slice (1); + utf16 = utf16.Slice (1); + } + if (utf8.IsEmpty || utf16.IsEmpty) + break; + + OperationStatus utf8Status = Rune.DecodeFromUtf8 (utf8, out Rune utf8Rune, out int utf8Consumed); + if (utf8Status != OperationStatus.Done) { + utf8Rune = Rune.ReplacementChar; + utf8Consumed = 1; + } + + OperationStatus utf16Status = Rune.DecodeFromUtf16 (utf16, out Rune utf16Rune, out int utf16Consumed); + if (utf16Status != OperationStatus.Done) { + utf16Rune = Rune.ReplacementChar; + utf16Consumed = 1; + } + + int runeComparison = utf8Rune.Value.CompareTo (utf16Rune.Value); + if (runeComparison != 0) + return runeComparison; + + utf8 = utf8.Slice (utf8Consumed); + utf16 = utf16.Slice (utf16Consumed); + } + + if (utf8.IsEmpty) + return utf16.IsEmpty ? 0 : -1; + return 1; + } + + static unsafe int LowerBoundByName (void* entries, uint count, int entrySize, ReadOnlySpan key) + { + int left = 0; + int right = checked ((int)count); + while (left < right) { + int middle = left + ((right - left) / 2); + var name = *(NativeJniRemappingString*)((byte*)entries + (middle * entrySize)); + if (Compare (name, key) < 0) { + left = middle + 1; + } else { + right = middle; + } + } + return left; + } + + static unsafe int LowerBoundByName (void* entries, uint count, int entrySize, ReadOnlySpan key, bool keyIsAscii) + { + int left = 0; + int right = checked ((int)count); + while (left < right) { + int middle = left + ((right - left) / 2); + var name = *(NativeJniRemappingString*)((byte*)entries + (middle * entrySize)); + if (Compare (name, key, keyIsAscii) < 0) { + left = middle + 1; + } else { + right = middle; + } + } + return left; + } + + static unsafe byte* LookupType (NativeJniRemappingTypeReplacementEntry* entries, uint count, ReadOnlySpan key) + { + bool keyIsAscii = Ascii.IsValid (key); + int index = LowerBoundByName (entries, count, sizeof (NativeJniRemappingTypeReplacementEntry), key, keyIsAscii); + if (index >= checked ((int)count) || !Equal (entries [index].name, key, keyIsAscii)) + return null; + return entries [index].replacement; + } + + static unsafe byte* LookupType (NativeJniRemappingTypeReplacementEntry* entries, uint count, ReadOnlySpan key) + { + int index = LowerBoundByName (entries, count, sizeof (NativeJniRemappingTypeReplacementEntry), key); + if (index >= checked ((int)count) || !Equal (entries [index].name, key)) + return null; + return entries [index].replacement; + } + + static unsafe NativeJniRemappingReplacementMethod* LookupMethod ( + NativeJniRemappingData* data, + ReadOnlySpan sourceType, + ReadOnlySpan name, + ReadOnlySpan signature, + out byte* matchedSignature) + { + matchedSignature = null; + int typeIndex = LowerBoundByName ( + data->method_replacement_index, + data->method_replacement_index_count, + sizeof (NativeJniRemappingIndexTypeEntry), + sourceType + ); + if (typeIndex >= checked ((int)data->method_replacement_index_count) || + !Equal (data->method_replacement_index [typeIndex].name, sourceType)) + return null; + + NativeJniRemappingIndexTypeEntry* type = &data->method_replacement_index [typeIndex]; + return LookupMethod (type, name, signature, out matchedSignature); + } + + static unsafe NativeJniRemappingReplacementMethod* LookupMethod ( + NativeJniRemappingData* data, + ReadOnlySpan sourceType, + ReadOnlySpan name, + ReadOnlySpan signature, + out byte* matchedSignature) + { + matchedSignature = null; + bool sourceTypeIsAscii = Ascii.IsValid (sourceType); + int typeIndex = LowerBoundByName ( + data->method_replacement_index, + data->method_replacement_index_count, + sizeof (NativeJniRemappingIndexTypeEntry), + sourceType, + sourceTypeIsAscii + ); + if (typeIndex >= checked ((int)data->method_replacement_index_count) || + !Equal (data->method_replacement_index [typeIndex].name, sourceType, sourceTypeIsAscii)) + return null; + + NativeJniRemappingIndexTypeEntry* type = &data->method_replacement_index [typeIndex]; + return LookupMethod (type, name, signature, out matchedSignature); + } + + static unsafe NativeJniRemappingReplacementMethod* LookupMethod ( + NativeJniRemappingIndexTypeEntry* type, + ReadOnlySpan name, + ReadOnlySpan signature, + out byte* matchedSignature) + { + matchedSignature = null; + bool nameIsAscii = Ascii.IsValid (name); + int first = LowerBoundByName (type->methods, type->method_count, sizeof (NativeJniRemappingIndexMethodEntry), name, nameIsAscii); + int count = checked ((int)type->method_count); + if (first >= count || !Equal (type->methods [first].name, name, nameIsAscii)) + return null; + + int last = first + 1; + while (last < count && Equal (type->methods [last].name, name, nameIsAscii)) + last++; + + if (signature.Length > 0) { + bool signatureIsAscii = Ascii.IsValid (signature); + for (int i = first; i < last; i++) { + NativeJniRemappingIndexMethodEntry* entry = &type->methods [i]; + if (entry->signature.length != 0 && Equal (entry->signature, signature, signatureIsAscii)) { + matchedSignature = entry->signature.str; + return &entry->replacement; + } + } + + int closeParenthesis = signature.Length - 1; + while (closeParenthesis >= 0 && signature [closeParenthesis] != ')') + closeParenthesis--; + int prefixLength = closeParenthesis + 1; + if (prefixLength > 0 && prefixLength != signature.Length) { + ReadOnlySpan signaturePrefix = signature.Slice (0, prefixLength); + bool signaturePrefixIsAscii = signatureIsAscii || Ascii.IsValid (signaturePrefix); + for (int i = first; i < last; i++) { + NativeJniRemappingIndexMethodEntry* entry = &type->methods [i]; + if (entry->signature.length != 0 && Equal (entry->signature, signaturePrefix, signaturePrefixIsAscii)) + return &entry->replacement; + } + } + } + + for (int i = first; i < last; i++) { + NativeJniRemappingIndexMethodEntry* entry = &type->methods [i]; + if (entry->signature.length == 0) { + return &entry->replacement; + } + } + return null; + } + + static unsafe NativeJniRemappingReplacementField* LookupField ( + NativeJniRemappingData* data, + ReadOnlySpan sourceType, + ReadOnlySpan name, + ReadOnlySpan signature) + { + bool sourceTypeIsAscii = Ascii.IsValid (sourceType); + int typeIndex = LowerBoundByName ( + data->field_replacement_index, + data->field_replacement_index_count, + sizeof (NativeJniRemappingIndexFieldTypeEntry), + sourceType, + sourceTypeIsAscii); + if (typeIndex >= checked ((int)data->field_replacement_index_count) || + !Equal (data->field_replacement_index [typeIndex].name, sourceType, sourceTypeIsAscii)) + return null; + + NativeJniRemappingIndexFieldTypeEntry* type = &data->field_replacement_index [typeIndex]; + bool nameIsAscii = Ascii.IsValid (name); + int first = LowerBoundByName ( + type->fields, + type->field_count, + sizeof (NativeJniRemappingIndexFieldEntry), + name, + nameIsAscii); + int count = checked ((int)type->field_count); + if (first >= count || !Equal (type->fields [first].name, name, nameIsAscii)) + return null; + + int last = first + 1; + while (last < count && Equal (type->fields [last].name, name, nameIsAscii)) + last++; + + bool signatureIsAscii = Ascii.IsValid (signature); + for (int i = first; i < last; i++) { + NativeJniRemappingIndexFieldEntry* entry = &type->fields [i]; + if (entry->signature.length != 0 && Equal (entry->signature, signature, signatureIsAscii)) + return &entry->replacement; + } + for (int i = first; i < last; i++) { + NativeJniRemappingIndexFieldEntry* entry = &type->fields [i]; + if (entry->signature.length == 0) + return &entry->replacement; + } + return null; + } + + static unsafe ReadOnlySpan GetNullTerminatedUtf8Span (IntPtr value) + { + byte* start = (byte*)value; + int length = 0; + while (start [length] != 0) + length++; + return new ReadOnlySpan (start, length); + } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index ca6be17f1c5..2ce9320463d 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 @@ -555,6 +558,7 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa return; } + className = JniRemappingLookup.GetReverseType (className) ?? className; var cacheEntry = s_instance.GetProxyCacheEntryForJniName (className); if (cacheEntry is JavaPeerProxy[] proxies && proxies.Length == 0) { return; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 59856a9db82..6b4ff814d22 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -196,6 +196,15 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl yield return builtInType; } + // The type map is keyed by the JNI names the managed code declares, so a name that was + // renamed in the packaged application has to be translated back first. + if (GetOriginalSimpleReference (jniSimpleReference) is string originalReference) { + foreach (var type in TrimmableTypeMap.Instance.GetTargetTypes (originalReference)) { + yield return type; + } + yield break; + } + foreach (var type in TrimmableTypeMap.Instance.GetTargetTypes (jniSimpleReference)) { yield return type; } @@ -210,11 +219,21 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl return builtInType; } - if (TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var type)) { - return type; + if (GetOriginalSimpleReference (jniSimpleReference) is string originalReference) { + return TrimmableTypeMap.Instance.TryGetTargetType (originalReference, out var type) ? type : null; + } + + return TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var directType) ? directType : null; + } + + static string? GetOriginalSimpleReference (string jniSimpleReference) + { + var original = JniRemappingLookup.GetReverseType (jniSimpleReference); + if (original is null || string.Equals (original, jniSimpleReference, StringComparison.Ordinal)) { + return null; } - return null; + return original; } // Lookup of the built-in managed type for a JNI simple reference, e.g., string, bool?, int?, etc. @@ -271,7 +290,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 +390,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 generated JNI runtime remapping protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) => JniRemappingLookup.GetStaticMethodFallbackTypes (jniSimpleReference, useReplacementTypes: true); @@ -378,12 +398,27 @@ protected override IEnumerable GetTypeSignaturesCore (Type typ protected override string? GetReplacementTypeCore (string jniSimpleReference) => JniRemappingLookup.GetReplacementType (jniSimpleReference); + protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8) + { + replacement = null; + replacementUtf8 = JniRemappingLookup.GetReplacementTypeUtf8 (jniSimpleReference); + } + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) => JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) => JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (IntPtr jniSourceTypeUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) + => JniRemappingLookup.GetReplacementMethodInfo (jniSourceTypeUtf8, 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/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 13986a46813..ade085c7ac3 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -917,6 +917,16 @@ public static string XA1036 { return ResourceManager.GetString("XA1036", resourceCulture); } } + + /// + /// Looks up a localized string similar to Unsupported @(Reference) item: {0}. + /// + public static string XA1037 { + get { + return ResourceManager.GetString("XA1037", resourceCulture); + } + } + /// /// Looks up a localized string similar to The '{0}' MSBuild property has an invalid value of '{1}'. A valid value is one of: {2}.. /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index c85742d1345..476ac8ae805 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -504,6 +504,11 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla The following are literal names and should not be translated: AndroidManifest.xml, //uses-sdk/@android:minSdkVersion, $(SupportedOSPlatformVersion) {0} - The minimum SDK version number {1} - The SupportedOSPlatformVersion property value + + + Unsupported @(Reference) item: {0} + The following are literal names and should not be translated: @(Reference) +{0} - The unsupported reference item Use of AppDomain.CreateDomain() detected in assembly: {0}. .NET 6 and higher will only support a single AppDomain, so this API will no longer be available in .NET for Android once .NET 6 is released. diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs index 16cf42c3533..3ca615ccdb4 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,9 @@ public JniRemappingNativeCodeInfo (int replacementTypeCount, int replacementMeth public bool GenerateEmptyCode { get; set; } + /// Table sizes produced by the last run, exposed for focused validation. + internal JniRemappingNativeCodeInfo? NativeCodeInfo { get; private set; } + public override bool RunTask () { if (!GenerateEmptyCode) { @@ -56,13 +64,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 +82,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 +104,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 +140,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 +165,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/JavaSourceUtils.cs b/src/Xamarin.Android.Build.Tasks/Tasks/JavaSourceUtils.cs index 55be2657710..e578a96a9cc 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/JavaSourceUtils.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/JavaSourceUtils.cs @@ -130,7 +130,7 @@ string CreateResponseFile () AppendArg (response, Path.GetFullPath (r.ItemSpec)); continue; } - Log.LogError ($"Unsupported @(Reference) item: {r.ItemSpec}"); + Log.LogCodedError ("XA1037", Properties.Resources.XA1037, r.ItemSpec); } } AppendArg (response, "--output-javadoc"); 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..4102257d129 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs @@ -0,0 +1,258 @@ +#nullable enable + +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Microsoft.Build.Framework; +using NUnit.Framework; +using Xamarin.Android.Tasks; +using Xamarin.ProjectTools; + +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."); + } + + StringAssert.Contains ("@jni_remapping_data = dso_local local_unnamed_addr constant %struct.JniRemappingData", ll); + StringAssert.IsMatch ( + @"i32 0,\s+i32 0,\s+i32 0,\s+i32 0", + ll, + "The generated remapping data counts must all 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 MethodsAndFieldsUseStableLookupOrder () + { + string ll = RunTask ( + """ + + + + + + + + + + """); + + // Exact descriptors precede parameter-only descriptors and wildcards so MonoVM's + // single scan cannot let a general remap shadow a specific one. + int methodsStart = ll.IndexOf ("@mm_0 =", System.StringComparison.Ordinal); + int methodsEnd = ll.IndexOf ("@jni_remapping_method_replacement_index", methodsStart, System.StringComparison.Ordinal); + Assert.Greater (methodsStart, -1); + Assert.Greater (methodsEnd, methodsStart); + string methodArray = ll.Substring (methodsStart, methodsEnd - methodsStart); + AssertOrdered ( + methodArray, + "ptr @.JniRemappingString.1_str", + "ptr @.JniRemappingString.2_str", + "ptr @.JniRemappingString.3_str", + "ptr null", + "ptr @.JniRemappingString.4_str"); + AssertOrdered (ll, "c\"af", "c\"zf"); + Assert.AreEqual (1, Info.ReplacementMethodIndexEntryCount); + Assert.AreEqual (1, Info.ReplacementFieldIndexEntryCount); + } + + [Test] + public void MemberArraySymbolsAreCollisionProofAndValidLlvm () + { + string ll = RunTask ( + """ + + + + + + """); + + StringAssert.Contains ("@mm_0", ll); + StringAssert.Contains ("@mm_1", ll); + StringAssert.Contains ("@mf_0", ll); + + string binUtils = Path.Combine (TestEnvironment.OSBinDirectory, "binutils", "bin"); + var compile = new CompileNativeAssembly { + BuildEngine = engine, + Sources = [new Microsoft.Build.Utilities.TaskItem (Path.Combine (TestDirectory, $"jni_remap.{Abi}.ll"))], + DebugBuild = false, + WorkingDirectory = TestDirectory, + AndroidBinUtilsDirectory = binUtils, + }; + Assert.IsTrue (compile.Execute (), $"Generated LLVM IR should compile. Errors: {string.Join ("; ", Errors.Select (e => e.Message))}"); + FileAssert.Exists (Path.Combine (TestDirectory, $"jni_remap.{Abi}.o")); + } + + [Test] + public void Utf8OrderingMatchesNativeMemcmp () + { + // '_' (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.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc index de61cb7ae8c..cf43b2803ff 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc @@ -32,7 +32,7 @@ "Size": 9457592 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2818048 + "Size": 2819424 }, "lib/arm64-v8a/libcoreclr.so": { "Size": 4843824 diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index c79f4855a58..f3942e0a629 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,55 @@ 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"; + const string RemappingDataVariableName = "jni_remapping_data"; + + sealed class JniRemappingDataContextDataProvider : NativeAssemblerStructContextDataProvider + { + public override string GetPointedToSymbolName (object data, string fieldName) + { + return fieldName switch { + nameof (JniRemappingData.type_replacements) => TypeReplacementsVariableName, + nameof (JniRemappingData.reverse_type_replacements) => ReverseTypeReplacementsVariableName, + nameof (JniRemappingData.method_replacement_index) => MethodReplacementIndexVariableName, + nameof (JniRemappingData.field_replacement_index) => FieldReplacementIndexVariableName, + _ => base.GetPointedToSymbolName (data, fieldName), + }; + } + } sealed class JniRemappingTypeReplacementEntryContextDataProvider : NativeAssemblerStructContextDataProvider { @@ -130,6 +179,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 +250,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 +293,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 +335,269 @@ sealed class JniRemappingTypeReplacementEntry public string replacement; }; + [NativeAssemblerStructContextDataProvider (typeof(JniRemappingDataContextDataProvider))] + sealed class JniRemappingData + { + [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] +#pragma warning disable CS0649 // Field is populated during native code generation + public JniRemappingTypeReplacementEntry type_replacements; + + [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] + public JniRemappingTypeReplacementEntry reverse_type_replacements; + + [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] + public JniRemappingIndexTypeEntry method_replacement_index; + + [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] + public JniRemappingIndexFieldTypeEntry field_replacement_index; +#pragma warning restore CS0649 + + public uint type_replacement_count; + public uint reverse_type_replacement_count; + public uint method_replacement_index_count; + public uint field_replacement_index_count; + } + + 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; - + StructureInfo jniRemappingDataStructureInfo; + 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) => str.IsNullOrEmpty () ? [] : Encoding.UTF8.GetBytes (str); + + GeneratedTables Init () { if (typeReplacementsInput == null) { - return (null, null); + return null; + } + + 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 typeReplacements = new List> (); - foreach (JniRemappingTypeReplacement mtr in typeReplacementsInput) { + 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); + for (int typeIndex = 0; typeIndex < sortedTypes.Count; typeIndex++) { + var kvp = sortedTypes [typeIndex]; + var methods = kvp.Value.methods; + // Keep exact descriptors before parameter-only descriptors and wildcards, matching + // the specificity passes used by both native runtimes. + methods.Sort ((l, r) => { + int cmp = CompareUtf8 (l.nameKey, r.nameKey); + if (cmp != 0) { + return cmp; + } + cmp = GetMethodSignatureSpecificity (l.method.SourceMethodSignature).CompareTo ( + GetMethodSignatureSpecificity (r.method.SourceMethodSignature)); + return cmp != 0 ? cmp : CompareUtf8 (l.signatureKey, r.signatureKey); + }); + + 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", typeIndex), + 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); + for (int typeIndex = 0; typeIndex < sortedTypes.Count; typeIndex++) { + var kvp = sortedTypes [typeIndex]; + var fields = kvp.Value.fields; + fields.Sort ((l, r) => { + int cmp = CompareUtf8 (l.nameKey, r.nameKey); + 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, + }, + }; + + typeFields.Add (new StructureInstance (jniRemappingIndexFieldEntryStructureInfo, field)); + } + + var entry = new JniRemappingIndexFieldTypeEntry { + name = MakeJniRemappingString (kvp.Key, kvp.Value.key), + field_count = (uint)typeFields.Count, + FieldsArraySymbolName = MakeMembersArrayName ("mf", typeIndex), + TypeFields = typeFields, }; + + ret.Add (new StructureInstance (jniRemappingIndexFieldTypeEntryStructureInfo, entry)); } - uint GetLength (string str) - { - if (String.IsNullOrEmpty (str)) { - return 0; - } + return ret; + } + + static string MakeMembersArrayName (string prefix, int typeIndex) + { + return $"{prefix}_{typeIndex}"; + } - return (uint)Encoding.UTF8.GetBytes (str).Length; + static int GetMethodSignatureSpecificity (string signature) + { + if (signature.IsNullOrEmpty ()) { + return 2; } + return signature [signature.Length - 1] == ')' ? 1 : 0; + } + + static JniRemappingString MakeJniRemappingString (string str, byte [] utf8) + { + return new JniRemappingString { + length = (uint)utf8.Length, + str = str, + }; } protected override void Construct (LlvmIrModule module) @@ -291,12 +605,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,31 +616,75 @@ 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 + ); + + AddData (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); + + AddData (module); + } + + void AddData (LlvmIrModule module) + { + var data = new JniRemappingData { + type_replacement_count = (uint)ReplacementTypeCount, + reverse_type_replacement_count = (uint)ReverseTypeCount, + method_replacement_index_count = (uint)ReplacementMethodIndexEntryCount, + field_replacement_index_count = (uint)ReplacementFieldIndexEntryCount, + }; + module.AddGlobalVariable ( + RemappingDataVariableName, + new StructureInstance (jniRemappingDataStructureInfo, data), + LlvmIrVariableOptions.GlobalConstant + ); } void MapStructures (LlvmIrModule module) { jniRemappingStringStructureInfo = module.MapStructure (); jniRemappingReplacementMethodStructureInfo = module.MapStructure (); + jniRemappingReplacementFieldStructureInfo = module.MapStructure (); jniRemappingIndexMethodEntryStructureInfo = module.MapStructure (); jniRemappingIndexTypeEntryStructureInfo = module.MapStructure (); + jniRemappingIndexFieldEntryStructureInfo = module.MapStructure (); + jniRemappingIndexFieldTypeEntryStructureInfo = module.MapStructure (); jniRemappingTypeReplacementEntryStructureInfo = module.MapStructure (); + jniRemappingDataStructureInfo = module.MapStructure (); } } } diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index 1e93c54871c..6f74c4d63b9 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -486,7 +486,7 @@ void Host::Java_mono_android_Runtime_initInternal ( init.packageNamingPolicy = static_cast(application_config.package_naming_policy); init.boundExceptionType = 0; // System init.jniAddNativeMethodRegistrationAttributePresent = application_config.jni_add_native_method_registration_attribute_present ? 1 : 0; - init.jniRemappingInUse = application_config.jni_remapping_replacement_type_count > 0 || application_config.jni_remapping_replacement_method_index_entry_count > 0; + init.jniRemappingData = &jni_remapping_data; init.marshalMethodsEnabled = application_config.marshal_methods_enabled; // GC threshold is 90% of the max GREF count diff --git a/src/native/clr/host/internal-pinvokes-clr.cc b/src/native/clr/host/internal-pinvokes-clr.cc index 7c978f9ee08..18971d298fa 100644 --- a/src/native/clr/host/internal-pinvokes-clr.cc +++ b/src/native/clr/host/internal-pinvokes-clr.cc @@ -5,7 +5,6 @@ #include #include #include -#include using namespace xamarin::android; @@ -27,18 +26,6 @@ bool clr_typemap_java_to_managed (const char *java_type_name, char const** assem return TypeMapper::java_to_managed (java_type_name, assembly_name, managed_type_token_id); } -const char* -_monodroid_lookup_replacement_type (const char *jniSimpleReference) -{ - return JniRemapping::lookup_replacement_type (jniSimpleReference); -} - -const JniRemappingReplacementMethod* -_monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) -{ - return JniRemapping::lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature); -} - managed_timing_sequence* monodroid_timing_start (const char *message) { if (!FastTiming::enabled ()) [[likely]] { diff --git a/src/native/clr/host/internal-pinvokes-shared.cc b/src/native/clr/host/internal-pinvokes-shared.cc index 18bffb5812e..08378e85d49 100644 --- a/src/native/clr/host/internal-pinvokes-shared.cc +++ b/src/native/clr/host/internal-pinvokes-shared.cc @@ -5,7 +5,6 @@ #include #include #include -#include using namespace xamarin::android; diff --git a/src/native/clr/include/runtime-base/internal-pinvokes.hh b/src/native/clr/include/runtime-base/internal-pinvokes.hh index a5408b45046..492054bf93e 100644 --- a/src/native/clr/include/runtime-base/internal-pinvokes.hh +++ b/src/native/clr/include/runtime-base/internal-pinvokes.hh @@ -23,8 +23,6 @@ extern "C" { void monodroid_log (xamarin::android::LogLevel level, LogCategories category, const char *message) noexcept; char* monodroid_TypeManager_get_java_class_name (jclass klass) noexcept; void monodroid_free (void *ptr) noexcept; - const char* _monodroid_lookup_replacement_type (const char *jniSimpleReference); - const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature); xamarin::android::managed_timing_sequence* monodroid_timing_start (const char *message); void monodroid_timing_stop (xamarin::android::managed_timing_sequence *sequence, const char *message); diff --git a/src/native/clr/include/runtime-base/jni-remapping.hh b/src/native/clr/include/runtime-base/jni-remapping.hh deleted file mode 100644 index f7b421b43cb..00000000000 --- a/src/native/clr/include/runtime-base/jni-remapping.hh +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "xamarin-app.hh" - -namespace xamarin::android -{ - class JniRemapping final - { - public: - static auto lookup_replacement_type (const char *jniSimpleReference) noexcept -> const char*; - static auto lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept -> const JniRemappingReplacementMethod*; - - private: - [[gnu::nonnull (2)]] - static auto equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept -> bool; - }; -} diff --git a/src/native/clr/include/xamarin-app.hh b/src/native/clr/include/xamarin-app.hh index c15776e2f27..32ce747fd2d 100644 --- a/src/native/clr/include/xamarin-app.hh +++ b/src/native/clr/include/xamarin-app.hh @@ -243,8 +243,7 @@ struct JniRemappingReplacementMethod { const char *target_type; const char *target_name; - // const char *target_signature; - // const int32_t param_count; + const char *target_signature; const bool is_static; }; diff --git a/src/native/clr/pinvoke-override/precompiled.cc b/src/native/clr/pinvoke-override/precompiled.cc index ec6ae2cb522..2ac13cd1c44 100644 --- a/src/native/clr/pinvoke-override/precompiled.cc +++ b/src/native/clr/pinvoke-override/precompiled.cc @@ -55,12 +55,6 @@ namespace { if (entrypoint_name == "monodroid_log"sv) { return reinterpret_cast (&monodroid_log); } - if (entrypoint_name == "_monodroid_lookup_replacement_type"sv) { - return reinterpret_cast (&_monodroid_lookup_replacement_type); - } - if (entrypoint_name == "_monodroid_lookup_replacement_method_info"sv) { - return reinterpret_cast (&_monodroid_lookup_replacement_method_info); - } if (entrypoint_name == "_monodroid_lref_log_delete"sv) { return reinterpret_cast (&_monodroid_lref_log_delete); } diff --git a/src/native/clr/runtime-base/CMakeLists.txt b/src/native/clr/runtime-base/CMakeLists.txt index 29a9e94e43e..99c9710d291 100644 --- a/src/native/clr/runtime-base/CMakeLists.txt +++ b/src/native/clr/runtime-base/CMakeLists.txt @@ -53,7 +53,6 @@ set(XA_RUNTIME_BASE_SOURCES android-system.cc android-system-shared.cc cpu-arch-detect.cc - jni-remapping.cc logger.cc util.cc ) diff --git a/src/native/clr/runtime-base/jni-remapping.cc b/src/native/clr/runtime-base/jni-remapping.cc deleted file mode 100644 index 715e5cb662e..00000000000 --- a/src/native/clr/runtime-base/jni-remapping.cc +++ /dev/null @@ -1,97 +0,0 @@ -#include - -#include -#include - -#include "xamarin-app.hh" - -using namespace xamarin::android; - -[[gnu::always_inline]] -auto JniRemapping::equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept -> bool -{ - if (left.length != static_cast(right_len) || left.str[0] != *right) { - return false; - } - - if (memcmp (left.str, right, right_len) == 0) { - return true; - } - - return false; -} - -auto JniRemapping::lookup_replacement_type (const char *jniSimpleReference) noexcept -> const char* -{ - if (application_config.jni_remapping_replacement_type_count == 0 || jniSimpleReference == nullptr || *jniSimpleReference == '\0') { - return nullptr; - } - - size_t ref_len = strlen (jniSimpleReference); - for (size_t i = 0uz; i < application_config.jni_remapping_replacement_type_count; i++) { - JniRemappingTypeReplacementEntry const& entry = jni_remapping_type_replacements[i]; - - if (equal (entry.name, jniSimpleReference, ref_len)) { - return entry.replacement; - } - } - - return nullptr; -} - -auto JniRemapping::lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept -> const JniRemappingReplacementMethod* -{ - if (application_config.jni_remapping_replacement_method_index_entry_count == 0 || - jniSourceType == nullptr || *jniSourceType == '\0' || - jniMethodName == nullptr || *jniMethodName == '\0') { - return nullptr; - } - - size_t source_type_len = strlen (jniSourceType); - - const JniRemappingIndexTypeEntry *type = nullptr; - for (size_t i = 0uz; i < application_config.jni_remapping_replacement_method_index_entry_count; i++) { - JniRemappingIndexTypeEntry const& entry = jni_remapping_method_replacement_index[i]; - - if (!equal (entry.name, jniSourceType, source_type_len)) { - continue; - } - - type = &jni_remapping_method_replacement_index[i]; - break; - } - - if (type == nullptr || type->method_count == 0 || type->methods == nullptr) { - return nullptr; - } - - size_t method_name_len = strlen (jniMethodName); - size_t signature_len = jniMethodSignature == nullptr ? 0uz : strlen (jniMethodSignature); - - for (size_t i = 0uz; i < type->method_count; i++) { - JniRemappingIndexMethodEntry const& entry = type->methods[i]; - - if (!equal (entry.name, jniMethodName, method_name_len)) { - continue; - } - - if (entry.signature.length == 0 || equal (entry.signature, jniMethodSignature, signature_len)) { - return &type->methods[i].replacement; - } - - const char *sig_end = jniMethodSignature + signature_len; - if (*sig_end == ')') { - continue; - } - - while (sig_end != jniMethodSignature && *sig_end != ')') { - sig_end--; - } - - if (equal (entry.signature, jniMethodSignature, static_cast(sig_end - jniMethodSignature) + 1uz)) { - return &type->methods[i].replacement; - } - } - - return nullptr; -} diff --git a/src/native/clr/xamarin-app-stub/application_dso_stub.cc b/src/native/clr/xamarin-app-stub/application_dso_stub.cc index 3890aef2639..ac0a2721fdf 100644 --- a/src/native/clr/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/clr/xamarin-app-stub/application_dso_stub.cc @@ -1,6 +1,7 @@ #include #include +#include #include // This file MUST have "valid" values everywhere - the DSO it is compiled into is loaded by the @@ -152,6 +153,7 @@ static const JniRemappingIndexMethodEntry some_java_type_one_methods[] = { .replacement = { .target_type = "some/java/target_type_one", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = false, } }, @@ -172,6 +174,7 @@ static const JniRemappingIndexMethodEntry some_java_type_two_methods[] = { .replacement = { .target_type = "some/java/target_type_two", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = true, } }, @@ -215,6 +218,17 @@ const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[] = { }, }; +extern "C" const xamarin::android::JniRemappingData jni_remapping_data { + .type_replacements = jni_remapping_type_replacements, + .reverse_type_replacements = nullptr, + .method_replacement_index = jni_remapping_method_replacement_index, + .field_replacement_index = nullptr, + .type_replacement_count = 2, + .reverse_type_replacement_count = 0, + .method_replacement_index_count = 2, + .field_replacement_index_count = 0, +}; + const char *init_runtime_property_names[] = { "HOST_RUNTIME_CONTRACT", "RUNTIME_IDENTIFIER", diff --git a/src/native/common/include/managed-interface.hh b/src/native/common/include/managed-interface.hh index ca29461dc05..5bf794af12a 100644 --- a/src/native/common/include/managed-interface.hh +++ b/src/native/common/include/managed-interface.hh @@ -17,6 +17,21 @@ namespace xamarin::android { using jnienv_propagate_uncaught_exception_fn = void (*)(JNIEnv *env, jobject javaThread, jthrowable javaException); using jnienv_register_jni_natives_fn = void (*)(const jchar *typeName_ptr, int32_t typeName_len, jclass jniClass, const jchar *methods_ptr, int32_t methods_len); + struct JniRemappingData { + const void *type_replacements; + const void *reverse_type_replacements; + const void *method_replacement_index; + const void *field_replacement_index; + uint32_t type_replacement_count; + uint32_t reverse_type_replacement_count; + uint32_t method_replacement_index_count; + uint32_t field_replacement_index_count; + }; + + extern "C" { + [[gnu::visibility("default")]] extern const JniRemappingData jni_remapping_data; + } + // NOTE: Keep this in sync with managed side in src/Mono.Android/Android.Runtime/JNIEnvInit.cs struct JnienvInitializeArgs { JavaVM *javaVm; @@ -32,7 +47,7 @@ namespace xamarin::android { int packageNamingPolicy; uint8_t boundExceptionType; int jniAddNativeMethodRegistrationAttributePresent; - bool jniRemappingInUse; + const JniRemappingData *jniRemappingData; bool marshalMethodsEnabled; jobject grefGCUserPeerable; jnienv_propagate_uncaught_exception_fn propagateUncaughtExceptionFn; diff --git a/src/native/mono/monodroid/CMakeLists.txt b/src/native/mono/monodroid/CMakeLists.txt index ca48f41f06f..3b07e5d7080 100644 --- a/src/native/mono/monodroid/CMakeLists.txt +++ b/src/native/mono/monodroid/CMakeLists.txt @@ -97,7 +97,6 @@ set(XAMARIN_MONODROID_SOURCES embedded-assemblies.cc globals.cc internal-pinvokes.cc - jni-remapping.cc mono-log-adapter.cc monodroid-glue.cc monodroid-tracing.cc diff --git a/src/native/mono/monodroid/internal-pinvokes.cc b/src/native/mono/monodroid/internal-pinvokes.cc index e7f580e8e41..397a2902b20 100644 --- a/src/native/mono/monodroid/internal-pinvokes.cc +++ b/src/native/mono/monodroid/internal-pinvokes.cc @@ -3,7 +3,6 @@ #include "android-system.hh" #include "globals.hh" #include "internal-pinvokes.hh" -#include "jni-remapping.hh" using namespace xamarin::android; using namespace xamarin::android::internal; @@ -276,16 +275,3 @@ monodroid_get_dylib () { return nullptr; } - -const char* -_monodroid_lookup_replacement_type (const char *jniSimpleReference) -{ - return JniRemapping::lookup_replacement_type (jniSimpleReference); -} - -const JniRemappingReplacementMethod* -_monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) -{ - return JniRemapping::lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature); -} - diff --git a/src/native/mono/monodroid/jni-remapping.cc b/src/native/mono/monodroid/jni-remapping.cc deleted file mode 100644 index 4122c8fe68a..00000000000 --- a/src/native/mono/monodroid/jni-remapping.cc +++ /dev/null @@ -1,98 +0,0 @@ -#include - -#include "logger.hh" -#include "jni-remapping.hh" -#include "xamarin-app.hh" - -using namespace xamarin::android::internal; - -[[gnu::always_inline]] bool -JniRemapping::equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept -{ - if (left.length != static_cast(right_len) || left.str[0] != *right) { - return false; - } - - if (memcmp (left.str, right, right_len) == 0) { - return true; - } - - return false; -} - -const char* -JniRemapping::lookup_replacement_type (const char *jniSimpleReference) noexcept -{ - if (application_config.jni_remapping_replacement_type_count == 0 || jniSimpleReference == nullptr || *jniSimpleReference == '\0') { - return nullptr; - } - - size_t ref_len = strlen (jniSimpleReference); - for (size_t i = 0uz; i < application_config.jni_remapping_replacement_type_count; i++) { - JniRemappingTypeReplacementEntry const& entry = jni_remapping_type_replacements[i]; - - if (equal (entry.name, jniSimpleReference, ref_len)) { - return entry.replacement; - } - } - - return nullptr; -} - -const JniRemappingReplacementMethod* -JniRemapping::lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept -{ - if (application_config.jni_remapping_replacement_method_index_entry_count == 0 || - jniSourceType == nullptr || *jniSourceType == '\0' || - jniMethodName == nullptr || *jniMethodName == '\0') { - return nullptr; - } - - size_t source_type_len = strlen (jniSourceType); - - const JniRemappingIndexTypeEntry *type = nullptr; - for (size_t i = 0uz; i < application_config.jni_remapping_replacement_method_index_entry_count; i++) { - JniRemappingIndexTypeEntry const& entry = jni_remapping_method_replacement_index[i]; - - if (!equal (entry.name, jniSourceType, source_type_len)) { - continue; - } - - type = &jni_remapping_method_replacement_index[i]; - break; - } - - if (type == nullptr || type->method_count == 0 || type->methods == nullptr) { - return nullptr; - } - - size_t method_name_len = strlen (jniMethodName); - size_t signature_len = jniMethodSignature == nullptr ? 0uz : strlen (jniMethodSignature); - - for (size_t i = 0uz; i < type->method_count; i++) { - JniRemappingIndexMethodEntry const& entry = type->methods[i]; - - if (!equal (entry.name, jniMethodName, method_name_len)) { - continue; - } - - if (entry.signature.length == 0 || equal (entry.signature, jniMethodSignature, signature_len)) { - return &type->methods[i].replacement; - } - - const char *sig_end = jniMethodSignature + signature_len; - if (*sig_end == ')') { - continue; - } - - while (sig_end != jniMethodSignature && *sig_end != ')') { - sig_end--; - } - - if (equal (entry.signature, jniMethodSignature, static_cast(sig_end - jniMethodSignature) + 1uz)) { - return &type->methods[i].replacement; - } - } - - return nullptr; -} diff --git a/src/native/mono/monodroid/jni-remapping.hh b/src/native/mono/monodroid/jni-remapping.hh deleted file mode 100644 index e76f89e78ff..00000000000 --- a/src/native/mono/monodroid/jni-remapping.hh +++ /dev/null @@ -1,21 +0,0 @@ -#if !defined (__JNI_REMAPPING_HH) -#define __JNI_REMAPPING_HH - -#include - -#include "xamarin-app.hh" - -namespace xamarin::android::internal -{ - class JniRemapping final - { - public: - static const char* lookup_replacement_type (const char *jniSimpleReference) noexcept; - static const JniRemappingReplacementMethod* lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept; - - private: - [[gnu::nonnull (2)]] - static bool equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept; - }; -} -#endif diff --git a/src/native/mono/monodroid/monodroid-glue.cc b/src/native/mono/monodroid/monodroid-glue.cc index 4a5f9e38c40..20c9a24b188 100644 --- a/src/native/mono/monodroid/monodroid-glue.cc +++ b/src/native/mono/monodroid/monodroid-glue.cc @@ -826,7 +826,7 @@ MonodroidRuntime::init_android_runtime (JNIEnv *env, jclass runtimeClass, jobjec init.packageNamingPolicy = static_cast(application_config.package_naming_policy); init.boundExceptionType = application_config.bound_exception_type; init.jniAddNativeMethodRegistrationAttributePresent = application_config.jni_add_native_method_registration_attribute_present ? 1 : 0; - init.jniRemappingInUse = application_config.jni_remapping_replacement_type_count > 0 || application_config.jni_remapping_replacement_method_index_entry_count > 0; + init.jniRemappingData = &jni_remapping_data; init.marshalMethodsEnabled = application_config.marshal_methods_enabled; java_System_identityHashCode = env->GetStaticMethodID (java_System, "identityHashCode", "(Ljava/lang/Object;)I"); diff --git a/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc b/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc index dc6570c2a23..69d6a196cc3 100644 --- a/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc +++ b/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc @@ -51,8 +51,6 @@ const std::vector internal_pinvoke_names = { "_monodroid_gref_log_delete", "_monodroid_gref_log_new", "monodroid_log", - "_monodroid_lookup_replacement_type", - "_monodroid_lookup_replacement_method_info", "_monodroid_lref_log_delete", "_monodroid_lref_log_new", "_monodroid_max_gref_get", diff --git a/src/native/mono/pinvoke-override/pinvoke-tables.include b/src/native/mono/pinvoke-override/pinvoke-tables.include index e26bde46029..173b4d4f347 100644 --- a/src/native/mono/pinvoke-override/pinvoke-tables.include +++ b/src/native/mono/pinvoke-override/pinvoke-tables.include @@ -11,12 +11,11 @@ namespace { #if INTPTR_MAX == INT64_MAX //64-bit internal p/invoke table - std::array internal_pinvokes {{ + std::array internal_pinvokes {{ {0x2b3b0ca1d14076da, "monodroid_get_dylib", reinterpret_cast(&monodroid_get_dylib)}, {0x37307e5fddf709dc, "_monodroid_weak_gref_dec", reinterpret_cast(&_monodroid_weak_gref_dec)}, {0x3b2467e7eadd4a6a, "_monodroid_lref_log_new", reinterpret_cast(&_monodroid_lref_log_new)}, {0x3c5532ecdab53f89, "set_world_accessable", reinterpret_cast(&set_world_accessable)}, - {0x423c8f539a2c56d2, "_monodroid_lookup_replacement_type", reinterpret_cast(&_monodroid_lookup_replacement_type)}, {0x4b1956138764939a, "_monodroid_gref_log_new", reinterpret_cast(&_monodroid_gref_log_new)}, {0x4d5b5b488f736058, "path_combine", reinterpret_cast(&path_combine)}, {0x5a2614d15e2fdc2e, "monodroid_strdup_printf", reinterpret_cast(&monodroid_strdup_printf)}, @@ -35,7 +34,6 @@ namespace { {0xb9bae9c43fb05089, "xamarin_app_init", reinterpret_cast(&xamarin_app_init)}, {0xbe5a300beec69c35, "monodroid_get_system_property", reinterpret_cast(&monodroid_get_system_property)}, {0xbfbb924fbe190616, "monodroid_dylib_mono_free", reinterpret_cast(&monodroid_dylib_mono_free)}, - {0xc2a21d3f6c8ccc24, "_monodroid_lookup_replacement_method_info", reinterpret_cast(&_monodroid_lookup_replacement_method_info)}, {0xc5b4690e13898fa3, "monodroid_timing_start", reinterpret_cast(&monodroid_timing_start)}, {0xcaab0a3ab6057bbd, "_monodroid_gref_inc", reinterpret_cast(&_monodroid_gref_inc)}, {0xcc873ea8493d1dd5, "monodroid_embedded_assemblies_set_assemblies_prefix", reinterpret_cast(&monodroid_embedded_assemblies_set_assemblies_prefix)}, @@ -576,14 +574,13 @@ constexpr hash_t system_security_cryptography_native_android_library_hash = 0x18 constexpr hash_t system_globalization_native_library_hash = 0x28b5c8fca080abd5; #else //32-bit internal p/invoke table - std::array internal_pinvokes {{ + std::array internal_pinvokes {{ {0xb7a486a, "monodroid_TypeManager_get_java_class_name", reinterpret_cast(&monodroid_TypeManager_get_java_class_name)}, {0xf562bd9, "monodroid_embedded_assemblies_set_assemblies_prefix", reinterpret_cast(&monodroid_embedded_assemblies_set_assemblies_prefix)}, {0x1bef8dce, "_monodroid_gref_inc", reinterpret_cast(&_monodroid_gref_inc)}, {0x1f1e0ee9, "_monodroid_gref_dec", reinterpret_cast(&_monodroid_gref_dec)}, {0x2aea7c33, "_monodroid_max_gref_get", reinterpret_cast(&_monodroid_max_gref_get)}, {0x3227d81a, "monodroid_timing_start", reinterpret_cast(&monodroid_timing_start)}, - {0x333d4835, "_monodroid_lookup_replacement_method_info", reinterpret_cast(&_monodroid_lookup_replacement_method_info)}, {0x395808e5, "monodroid_dylib_mono_free", reinterpret_cast(&monodroid_dylib_mono_free)}, {0x42b41fe4, "send_uninterrupted", reinterpret_cast(&send_uninterrupted)}, {0x4b58e0da, "monodroid_get_dylib", reinterpret_cast(&monodroid_get_dylib)}, @@ -601,7 +598,6 @@ constexpr hash_t system_globalization_native_library_hash = 0x28b5c8fca080abd5; {0xb02468aa, "_monodroid_gref_get", reinterpret_cast(&_monodroid_gref_get)}, {0xbe8d7701, "_monodroid_gref_log_new", reinterpret_cast(&_monodroid_gref_log_new)}, {0xc0d097a7, "_monodroid_lref_log_new", reinterpret_cast(&_monodroid_lref_log_new)}, - {0xc439b5d7, "_monodroid_lookup_replacement_type", reinterpret_cast(&_monodroid_lookup_replacement_type)}, {0xc5146c54, "_monodroid_gref_log_delete", reinterpret_cast(&_monodroid_gref_log_delete)}, {0xc58eafa5, "java_interop_free", reinterpret_cast(&java_interop_free)}, {0xd91f3619, "create_public_directory", reinterpret_cast(&create_public_directory)}, @@ -1141,6 +1137,6 @@ constexpr hash_t system_security_cryptography_native_android_library_hash = 0x93 constexpr hash_t system_globalization_native_library_hash = 0xa66f1e5a; #endif -constexpr size_t internal_pinvokes_count = 39; +constexpr size_t internal_pinvokes_count = 37; constexpr size_t dotnet_pinvokes_count = 510; } // end of anonymous namespace diff --git a/src/native/mono/runtime-base/internal-pinvokes.hh b/src/native/mono/runtime-base/internal-pinvokes.hh index bff70b0e9fa..005a9c53afe 100644 --- a/src/native/mono/runtime-base/internal-pinvokes.hh +++ b/src/native/mono/runtime-base/internal-pinvokes.hh @@ -44,6 +44,4 @@ void* monodroid_dylib_mono_new ([[maybe_unused]] const char *libmono_path); void monodroid_dylib_mono_free ([[maybe_unused]] void *mono_imports); int monodroid_dylib_mono_init (void *mono_imports, [[maybe_unused]] const char *libmono_path); void* monodroid_get_dylib (); -const char* _monodroid_lookup_replacement_type (const char *jniSimpleReference); -const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature); void _monodroid_detect_cpu_and_architecture (unsigned short *built_for_cpu, unsigned short *running_on_cpu, unsigned char *is64bit); diff --git a/src/native/mono/xamarin-app-stub/application_dso_stub.cc b/src/native/mono/xamarin-app-stub/application_dso_stub.cc index 6ed48fac62c..7a8d7940432 100644 --- a/src/native/mono/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/mono/xamarin-app-stub/application_dso_stub.cc @@ -1,6 +1,7 @@ #include #include +#include #include "xamarin-app.hh" #include @@ -248,6 +249,7 @@ static const JniRemappingIndexMethodEntry some_java_type_one_methods[] = { .replacement = { .target_type = "some/java/target_type_one", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = false, } }, @@ -268,6 +270,7 @@ static const JniRemappingIndexMethodEntry some_java_type_two_methods[] = { .replacement = { .target_type = "some/java/target_type_two", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = true, } }, @@ -310,3 +313,14 @@ const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[] = { .replacement = "another/replacement/java/type", }, }; + +extern "C" const xamarin::android::JniRemappingData jni_remapping_data { + .type_replacements = jni_remapping_type_replacements, + .reverse_type_replacements = nullptr, + .method_replacement_index = jni_remapping_method_replacement_index, + .field_replacement_index = nullptr, + .type_replacement_count = 2, + .reverse_type_replacement_count = 0, + .method_replacement_index_count = 2, + .field_replacement_index_count = 0, +}; diff --git a/src/native/mono/xamarin-app-stub/xamarin-app.hh b/src/native/mono/xamarin-app-stub/xamarin-app.hh index d2dacdd9263..fb8c94c68c3 100644 --- a/src/native/mono/xamarin-app-stub/xamarin-app.hh +++ b/src/native/mono/xamarin-app-stub/xamarin-app.hh @@ -283,8 +283,7 @@ struct JniRemappingReplacementMethod { const char *target_type; const char *target_name; - // const char *target_signature; - // const int32_t param_count; + const char *target_signature; const bool is_static; }; diff --git a/src/native/nativeaot/host/internal-pinvoke-stubs.cc b/src/native/nativeaot/host/internal-pinvoke-stubs.cc index 1e7dd83833b..f46f8f8944f 100644 --- a/src/native/nativeaot/host/internal-pinvoke-stubs.cc +++ b/src/native/nativeaot/host/internal-pinvoke-stubs.cc @@ -32,19 +32,6 @@ bool clr_typemap_java_to_managed ( pinvoke_unreachable (); } -const char* _monodroid_lookup_replacement_type ([[maybe_unused]] const char *jniSimpleReference) -{ - pinvoke_unreachable (); -} - -const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info ( - [[maybe_unused]] const char *jniSourceType, - [[maybe_unused]] const char *jniMethodName, - [[maybe_unused]] const char *jniMethodSignature) -{ - pinvoke_unreachable (); -} - managed_timing_sequence* monodroid_timing_start ([[maybe_unused]] const char *message) { pinvoke_unreachable (); diff --git a/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh b/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh index 60ff24596fc..14d25414941 100644 --- a/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh +++ b/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh @@ -26,8 +26,6 @@ extern "C" { void monodroid_log (xamarin::android::LogLevel level, LogCategories category, const char *message) noexcept; char* monodroid_TypeManager_get_java_class_name (jclass klass) noexcept; void monodroid_free (void *ptr) noexcept; - const char* _monodroid_lookup_replacement_type (const char *jniSimpleReference); - const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature); xamarin::android::managed_timing_sequence* monodroid_timing_start (const char *message); void monodroid_timing_stop (xamarin::android::managed_timing_sequence *sequence, const char *message); diff --git a/src/r8/build.gradle b/src/r8/build.gradle index 7d295115ba9..fa4ee88b372 100644 --- a/src/r8/build.gradle +++ b/src/r8/build.gradle @@ -10,7 +10,7 @@ java { } dependencies { - implementation 'com.android.tools:r8:9.4.17' + implementation 'com.android.tools:r8:9.4.24' } jar { diff --git a/tests/MSBuildDeviceIntegration/Resources/RemapActivity.java b/tests/MSBuildDeviceIntegration/Resources/RemapActivity.java index 2be2dc9b667..6e5d919fbd5 100644 --- a/tests/MSBuildDeviceIntegration/Resources/RemapActivity.java +++ b/tests/MSBuildDeviceIntegration/Resources/RemapActivity.java @@ -7,6 +7,10 @@ public void onMyCreate (android.os.Bundle bundle) { Log.d ("*REMAP-TEST*", "RemapActivity.onMyCreate() invoked!"); super.onCreate(bundle); } + + public void méthodeCible () { + Log.d ("*REMAP-TEST*", "RemapActivity.méthodeCible() invoked!"); + } } class ViewHelper { diff --git a/tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml b/tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml index 283a3557045..67db7a9d2b1 100644 --- a/tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml +++ b/tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml @@ -1,5 +1,6 @@ + + diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index a50837a873b..cbc8fb8285a 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -2553,7 +2553,18 @@ public void TypeAndMemberRemapping ([Values] bool isRelease, [Values (AndroidRun }, }; proj.SetRuntime (runtime); - proj.MainActivity = proj.DefaultMainActivity.Replace (": Activity", ": global::Example.RemapActivity"); + proj.MainActivity = proj.DefaultMainActivity + .Replace (": Activity", ": global::Example.RemapActivity") + .Replace ("//${AFTER_ONCREATE}", """ + unsafe { + var members = new Java.Interop.JniPeerMembers ("example/ActivitéSource", typeof (global::Example.RemapActivity)); + try { + members.InstanceMethods.InvokeNonvirtualVoidMethod ("méthodeSource.()V", this, null); + } finally { + Java.Interop.JniPeerMembers.Dispose (members); + } + } +"""); var builder = CreateApkBuilder (); Assert.IsTrue (builder.Build (proj), "`dotnet build` should succeed"); RunProjectAndAssert (proj, builder); @@ -2572,6 +2583,11 @@ public void TypeAndMemberRemapping ([Values] bool isRelease, [Values (AndroidRun logcatOutput, "View.setOnClickListener() wasn't remapped to ViewHelper.mySetOnClickListener()!" ); + StringAssert.Contains ( + "RemapActivity.méthodeCible() invoked!", + logcatOutput, + "The non-ASCII method name wasn't remapped!" + ); } [Test] diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ActivationConstructorCacheTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ActivationConstructorCacheTests.cs new file mode 100644 index 00000000000..066a0219af6 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ActivationConstructorCacheTests.cs @@ -0,0 +1,251 @@ +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using Android.Runtime; +using Java.Interop; +using NUnit.Framework; +using JavaObject = Java.Interop.JavaObject; + +namespace Java.InteropTests; + +[TestFixture] +[Category ("ReflectionActivationCache")] +public class ActivationConstructorCacheTests +{ + const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + + delegate bool TryConstructPeerDelegate (IJavaPeerable self, ref JniObjectReference reference, JniObjectReferenceOptions options, Type type); + + [TestCase (typeof (XAPeer), 1)] + [TestCase (typeof (JIPeer), 2)] + [TestCase (typeof (MissingPeer), 2)] + public void ResolutionIsCached (Type peerType, int expectedLookups) + { + AssumeReflectionActivation (); + var type = new CountingType (peerType); + var resolve = GetResolver (); + var activation = resolve.Invoke (null, new object [] { type }); + Assert.AreEqual (expectedLookups, type.Lookups); + for (int i = 0; i < 10; i++) + Assert.AreEqual (activation, resolve.Invoke (null, new object [] { type })); + Assert.AreEqual (expectedLookups, type.Lookups, "Warm resolution must not enter the reflection binder."); + } + + [TestCase (typeof (XAPeer))] + [TestCase (typeof (JIPeer))] + [TestCase (typeof (MissingPeer))] + public void ConcurrentResolutionPublishesConsistentResult (Type peerType) + { + AssumeReflectionActivation (); + var type = new CountingType (peerType); + var resolve = GetResolver (); + var results = new object? [32]; + Parallel.For (0, results.Length, i => results [i] = resolve.Invoke (null, new object [] { type })); + foreach (var result in results) + Assert.AreEqual (results [0], result); + var lookups = type.Lookups; + Assert.Greater (lookups, 0); + Parallel.For (0, results.Length, i => resolve.Invoke (null, new object [] { type })); + Assert.AreEqual (lookups, type.Lookups); + } + + [TestCase (false, JniObjectReferenceOptions.Copy)] + [TestCase (false, JniObjectReferenceOptions.CopyAndDispose)] + [TestCase (true, JniObjectReferenceOptions.Copy)] + [TestCase (true, JniObjectReferenceOptions.CopyAndDispose)] + public void CoreClrConstructsExistingPeerAndPreservesOwnership (bool ji, JniObjectReferenceOptions options) + { + var construct = GetCoreClrConstructor (); + var peerType = ji ? typeof (JIPeer) : typeof (XAPeer); + var type = new CountingType (peerType); + for (int i = 0; i < 2; i++) { + using var source = new JavaObject (); + var reference = source.PeerReference.NewLocalRef (); + var self = (Java.Lang.Object) RuntimeHelpers.GetUninitializedObject (peerType); + ((IJavaPeerable) self).SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); + try { + Assert.IsTrue (construct (self, ref reference, options, type)); + Assert.IsTrue (JniEnvironment.Types.IsSameObject (source.PeerReference, self.PeerReference)); + if (self is XAPeer xa) { + Assert.AreSame (self, xa.ConstructedSelf); + Assert.AreEqual (JniHandleOwnership.DoNotTransfer, xa.Transfer); + Assert.IsFalse (xa.UsedJI, "XA must win when both constructors exist."); + } else if (self is JIPeer jp) { + Assert.AreSame (self, jp.ConstructedSelf); + Assert.AreEqual (options, jp.Options); + } + Assert.AreEqual (options == JniObjectReferenceOptions.Copy, reference.IsValid); + } finally { + self.Dispose (); + JniObjectReference.Dispose (ref reference); + } + } + Assert.AreEqual (ji ? 2 : 1, type.Lookups, "Repeated construction must reuse the value manager's cache."); + } + + [Test] + public void CoreClrMissingConstructorLeavesReferenceAndPeerUntouched () + { + var construct = GetCoreClrConstructor (); + var type = new CountingType (typeof (MissingPeer)); + using var source = new JavaObject (); + var reference = source.PeerReference.NewLocalRef (); + var original = reference; + var self = (MissingPeer) RuntimeHelpers.GetUninitializedObject (typeof (MissingPeer)); + GC.SuppressFinalize (self); + try { + for (int i = 0; i < 2; i++) { + Assert.IsFalse (construct (self, ref reference, JniObjectReferenceOptions.CopyAndDispose, type)); + Assert.AreEqual (original, reference); + Assert.IsFalse (self.PeerReference.IsValid); + } + Assert.AreEqual (2, type.Lookups); + } finally { + JniObjectReference.Dispose (ref reference); + } + } + + [TestCase (typeof (ThrowingXAPeer))] + [TestCase (typeof (ThrowingJIPeer))] + public void CoreClrThrowingConstructorDoesNotDisposeOrCopyBackReference ([DynamicallyAccessedMembers (Constructors)] Type peerType) + { + var construct = GetCoreClrConstructor (); + var type = new CountingType (peerType); + using var source = new JavaObject (); + var reference = source.PeerReference.NewLocalRef (); + var original = reference; + try { + for (int i = 0; i < 2; i++) { + var self = (Java.Lang.Object) RuntimeHelpers.GetUninitializedObject (peerType); + GC.SuppressFinalize (self); + var exception = Assert.Throws (() => + construct (self, ref reference, JniObjectReferenceOptions.CopyAndDispose, type)); + Assert.IsInstanceOf (exception?.InnerException); + Assert.AreEqual ("activation failed", exception?.InnerException?.Message); + Assert.AreEqual (original, reference); + Assert.IsTrue (JniEnvironment.Types.IsSameObject (source.PeerReference, reference)); + } + Assert.AreEqual (peerType == typeof (ThrowingXAPeer) ? 1 : 2, type.Lookups); + } finally { + JniObjectReference.Dispose (ref reference); + } + } + + [DynamicDependency (Constructors, typeof (XAPeer))] + [DynamicDependency (Constructors, typeof (JIPeer))] + [DynamicDependency (Constructors, typeof (MissingPeer))] + [DynamicDependency (Constructors, typeof (ThrowingXAPeer))] + [DynamicDependency (Constructors, typeof (ThrowingJIPeer))] + static void AssumeReflectionActivation () + { + if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) + Assert.Ignore ("This test exercises reflection activation, not the generated trimmable typemap."); + } + + [UnconditionalSuppressMessage ("Trimming", "IL2111", Justification = "Only the explicitly preserved test peer constructors are resolved, through CountingType.")] + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This test explicitly exercises the reflection value manager with preserved peer constructors.")] + static MethodInfo GetResolver () + { + var managerType = Type.GetType ("Microsoft.Android.Runtime.JavaMarshalValueManager, Mono.Android", throwOnError: true) + ?? throw new InvalidOperationException ("Could not find the CoreCLR reflection value manager."); + return managerType.GetMethod ("GetActivationConstructor", BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException ("Could not find the value manager's activation constructor resolver."); + } + + [UnconditionalSuppressMessage ("Trimming", "IL2111", Justification = "Only the explicitly preserved test peer constructors are invoked, through CountingType.")] + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This test explicitly exercises the reflection value manager with preserved peer constructors.")] + static TryConstructPeerDelegate GetCoreClrConstructor () + { + AssumeReflectionActivation (); + if (!AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.IsCoreClrRuntime", out bool isCoreClr) || !isCoreClr) + Assert.Ignore ("This test exercises the CoreCLR reflection value manager."); + var manager = JniEnvironment.Runtime.ValueManager; + Assert.AreEqual ("Microsoft.Android.Runtime.JavaMarshalValueManager", manager.GetType ().FullName); + var managerType = Type.GetType ("Microsoft.Android.Runtime.JavaMarshalValueManager, Mono.Android", throwOnError: true) + ?? throw new InvalidOperationException ("Could not find the CoreCLR reflection value manager."); + var method = managerType.GetMethod ("TryConstructPeer", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly) + ?? throw new InvalidOperationException ("Could not find CoreCLR TryConstructPeer."); + return method.CreateDelegate (manager); + } + + sealed class CountingType : TypeDelegator + { + int lookups; + public int Lookups => Volatile.Read (ref lookups); + + public CountingType (Type type) : base (type) {} + + public override bool Equals (object? other) => ReferenceEquals (this, other); + public override bool Equals (Type? other) => ReferenceEquals (this, other); + public override int GetHashCode () => RuntimeHelpers.GetHashCode (this); + + [DynamicallyAccessedMembers (Constructors)] + protected override ConstructorInfo? GetConstructorImpl (BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type [] types, ParameterModifier []? modifiers) + { + Interlocked.Increment (ref lookups); + Assert.AreEqual (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, bindingAttr); + Assert.IsNull (binder); + return base.GetConstructorImpl (bindingAttr, binder, callConvention, types, modifiers); + } + } + + sealed class XAPeer : Java.Lang.Object + { + public object? ConstructedSelf; + public JniHandleOwnership Transfer; + public bool UsedJI; + + internal XAPeer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) + { + ConstructedSelf = this; + Transfer = transfer; + } + + public XAPeer (ref JniObjectReference reference, JniObjectReferenceOptions options) : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer) + { + UsedJI = true; + Construct (ref reference, options); + } + } + + sealed class JIPeer : Java.Lang.Object + { + public object? ConstructedSelf; + public JniObjectReferenceOptions Options; + + internal JIPeer (ref JniObjectReference reference, JniObjectReferenceOptions options) : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer) + { + ConstructedSelf = this; + Options = options; + Construct (ref reference, options); + } + } + + sealed class MissingPeer : Java.Lang.Object + { + public MissingPeer () {} + } + + sealed class ThrowingXAPeer : Java.Lang.Object + { + public ThrowingXAPeer (IntPtr handle, JniHandleOwnership transfer) : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer) + { + throw new InvalidOperationException ("activation failed"); + } + } + + sealed class ThrowingJIPeer : Java.Lang.Object + { + public ThrowingJIPeer (ref JniObjectReference reference, JniObjectReferenceOptions options) : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer) + { + reference = default; + throw new InvalidOperationException ("activation failed"); + } + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index 9c44a461bea..698976ce66c 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -138,6 +138,7 @@ + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml index 53a299d9d49..643106c7d20 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml @@ -11,6 +11,14 @@ source-method-signature="()Ljava/lang/String;" target-type="java/lang/Object" target-method-name="toString" target-method-instance-to-static="false" /> +