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/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.JniFields.tt b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniFields.tt index f569b372b01..50644b2e001 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniFields.tt +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniFields.tt @@ -62,13 +62,13 @@ namespace Java.Interop { public <#= info.ReturnType #> Get<#= info.ManagedType #>Value (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStatic<#= info.JniCallType #>Field (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStatic<#= info.JniCallType #>Field (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, <#= info.ParameterType #> value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStatic<#= info.JniCallType #>Field (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStatic<#= info.JniCallType #>Field (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 0f834bba544..8d4f28d3aa8 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 (); @@ -47,7 +51,7 @@ internal JniType JniPeerType { internal void Dispose () { - Clear (ref instanceMethods); + Clear (ref instanceMethods, static method => method.StaticRedirect?.Dispose ()); Clear (ref subclassConstructors, static value => value.Dispose ()); if (jniPeerType != null) @@ -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) @@ -90,16 +109,26 @@ internal JniInstanceMethods GetConstructorsForType (Type declaringType) // at Java.Interop.JniPeerMembers.JniInstanceMethods..ctor(Type declaringType) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 27 // at Java.Interop.JniPeerMembers.JniInstanceMethods.GetConstructorsForType(Type declaringType) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 77 // at Java.Interop.JniPeerMembers.JniInstanceMethods.StartCreateInstance(String constructorSignature, Type declaringType, JniArgumentValue* parameters) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 146 - return SubclassConstructors.GetOrAdd (declaringType, static type => new JniInstanceMethods (type)); + return GetOrAdd ( + SubclassConstructors, + declaringType, + static (type, _) => new JniInstanceMethods (type), + this, + static value => value.Dispose ()); } public JniMethodInfo GetMethodInfo (string encodedMember) { - return InstanceMethods.GetOrAdd (encodedMember, static (member, methods) => { - ReadOnlySpan method, signature; - JniPeerMembers.GetNameAndSignature (member, out method, out signature); - return methods.GetMethodInfo (method, signature); - }, this); + return GetOrAdd ( + InstanceMethods, + encodedMember, + static (member, methods) => { + ReadOnlySpan method, signature; + JniPeerMembers.GetNameAndSignature (member, out method, out signature); + return methods.GetMethodInfo (method, signature); + }, + this, + static method => method.StaticRedirect?.Dispose ()); } JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signature) @@ -108,21 +137,48 @@ JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signa var newMethod = Members.GetReplacementMethodInfo (method, signature); if (newMethod.HasValue) { 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 (TryGetInstanceMethod (t, info, method, signature, out m)) { - return m; + JniType? t = CreateTargetType (info, Members); + try { + if (info.TargetJniMethodInstanceToStatic && + TryGetStaticMethod (t, info, method, signature, out m)) { + m.ParameterCount = info.TargetJniMethodParameterCount; + m.StaticRedirect = t; + t = null; + return m; + } + if (TryGetInstanceMethod (t, info, method, signature, out m)) { + return m; + } + } finally { + t?.Dispose (); } 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; + JniType? t = CreateTargetType (info, TargetJniTypeName); + try { + if (info.TargetJniMethodInstanceToStatic && + TryGetStaticMethod (t, info, method, signature, out m)) { + m.ParameterCount = info.TargetJniMethodParameterCount; + m.StaticRedirect = t; + t = null; + return m; + } + if (TryGetInstanceMethod (t, info, method, signature, out m)) + return m; + } finally { + t?.Dispose (); + } + } + 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..4f89c1addd0 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 @@ -21,16 +21,69 @@ internal JniStaticFields (JniPeerMembers members) 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); - }, this); + return GetOrAdd ( + StaticFields, + encodedMember, + static (member, fields) => { + ReadOnlySpan field, signature; + JniPeerMembers.GetNameAndSignature (member, out field, out signature); + return fields.GetFieldInfo (field, signature); + }, + this, + static field => field.StaticRedirect?.Dispose ()); + } + + 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 107b97d763b..42bee839cb3 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,16 +21,21 @@ internal JniStaticMethods (JniPeerMembers members) internal void Dispose () { - Clear (ref staticMethods); + Clear (ref staticMethods, static method => method.StaticRedirect?.Dispose ()); } public JniMethodInfo GetMethodInfo (string encodedMember) { - return StaticMethods.GetOrAdd (encodedMember, static (member, methods) => { - ReadOnlySpan method, signature; - JniPeerMembers.GetNameAndSignature (member, out method, out signature); - return methods.GetMethodInfo (method, signature); - }, this); + return GetOrAdd ( + StaticMethods, + encodedMember, + static (member, methods) => { + ReadOnlySpan method, signature; + JniPeerMembers.GetNameAndSignature (member, out method, out signature); + return methods.GetMethodInfo (method, signature); + }, + this, + static method => method.StaticRedirect?.Dispose ()); } JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signature) @@ -39,14 +44,34 @@ JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signa var newMethod = Members.GetReplacementMethodInfo (method, signature); if (newMethod.HasValue) { var info = newMethod.Value; - using var t = CreateTargetType (info, Members); - if (TryGetStaticMethod (t, info, method, signature, out m)) { - return m; + 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 60d52d916ad..838c4ebb838 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs @@ -40,6 +40,8 @@ public JniPeerTypeNameInfo (string sourceName, string? targetName, IntPtr target 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); @@ -83,7 +85,7 @@ static JniPeerTypeNameInfo GetReplacementType (string jniPeerTypeName) static JniPeerMembers CreatePeerMembers (string jniPeerTypeName, Type managedPeerType) { - return new JniPeerMembers (new JniPeerTypeNameInfo (jniPeerTypeName, null, IntPtr.Zero), managedPeerType, checkManagedPeerType: false); + return new JniPeerMembers (GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: false); } JniType? jniPeerType; @@ -144,6 +146,25 @@ static ConcurrentDictionary GetOrCreate (ref Concurr return Interlocked.CompareExchange (ref dictionary, candidate, null) ?? candidate; } + static TValue GetOrAdd ( + ConcurrentDictionary dictionary, + TKey key, + Func valueFactory, + TState state, + Action dispose) + where TKey : notnull + where TValue : class + { + if (dictionary.TryGetValue (key, out var value)) + return value; + + var candidate = valueFactory (key, state); + value = dictionary.GetOrAdd (key, candidate); + if (!ReferenceEquals (value, candidate)) + dispose (candidate); + return value; + } + static void Clear (ref ConcurrentDictionary? dictionary, Action? dispose = null) where TKey : notnull { @@ -159,14 +180,14 @@ static void Clear (ref ConcurrentDictionary? diction protected virtual void Dispose (bool disposing) { - if (!disposing || jniPeerType == null) + if (!disposing) return; instanceMethods.Dispose (); instanceFields.Dispose (); staticMethods.Dispose (); staticFields.Dispose (); - jniPeerType.Dispose (); + jniPeerType?.Dispose (); jniPeerType = null; } @@ -198,14 +219,17 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value) } 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 fallback.jniPeerTypeNameUtf8 != IntPtr.Zero - ? new JniType (fallback.jniPeerTypeNameUtf8) - : new JniType (fallback.jniPeerTypeName ?? fallback.sourceJniPeerTypeName); + return new JniType (fallbackTypeName); } static bool TryGetInstanceMethod ( @@ -282,6 +306,71 @@ static string GetTargetMethodSignatureForDiagnostics (JniRuntime.ReplacementMeth 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); + } + + static string? GetEffectiveBaseTypeName (JniRuntime.JniTypeManager typeManager, Type baseType) + { + var baseSignature = typeManager.GetTypeSignature (baseType); + string? effectiveBaseType = baseSignature.SimpleReference; + if (effectiveBaseType == null) + return null; + + // Type managers may return either the declared or runtime JNI name. The extra lookup + // supports declared names; remapping producers must emit single-hop final targets. + return typeManager.GetReplacementType (effectiveBaseType) ?? effectiveBaseType; + } + + 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) { + string? effectiveBaseType = GetEffectiveBaseTypeName (typeManager, baseType); + if (effectiveBaseType == null) + continue; + 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) { + string? effectiveBaseType = GetEffectiveBaseTypeName (typeManager, baseType); + if (effectiveBaseType == null) + continue; + var info = typeManager.GetReplacementFieldInfo (effectiveBaseType, field, signature); + if (info != null) { + return info; + } + } + return null; + } + internal static void AssertSelf (IJavaPeerable self) { if (self == null) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs index ea5f2ab5ba6..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 @@ -127,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 { @@ -389,6 +444,41 @@ protected virtual void GetReplacementTypeInfoCore (string jniSimpleReference, ou 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 25f8e7efd9b..be0f27a7884 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs @@ -566,6 +566,20 @@ internal bool TryGetStaticMethod (ReadOnlySpan name, IntPtr signature, [No return method != null; } + internal bool TryGetInstanceField (ReadOnlySpan name, ReadOnlySpan signature, [NotNullWhen (true)] out JniFieldInfo? field) + { + var id = GetMemberID (name, signature, MemberKind.InstanceField, throwOnError: false); + field = id == IntPtr.Zero ? null : CreateFieldInfo (name, signature, id, isStatic: false); + return field != null; + } + + internal bool TryGetStaticField (ReadOnlySpan name, ReadOnlySpan signature, [NotNullWhen (true)] out JniFieldInfo? field) + { + var id = GetMemberID (name, signature, MemberKind.StaticField, throwOnError: false); + field = id == IntPtr.Zero ? null : CreateFieldInfo (name, signature, id, isStatic: true); + return field != null; + } + static JniMethodInfo CreateMethodInfo (ReadOnlySpan name, ReadOnlySpan signature, IntPtr id, bool isStatic) { #if DEBUG diff --git a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt index 11723423a5b..154b67e611e 100644 --- a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt @@ -8,6 +8,7 @@ Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniMethodSignatureUtf8.get - 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 @@ -127,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 8b5459df5eb..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 @@ -137,6 +137,7 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type) Dictionary ReplacmentTypes = new() { ["net/dot/jni/test/RenameClassBase1"] = "net/dot/jni/test/RenameClassBase2", + [FieldRemapBase.JniTypeName] = FieldRemapBase.RuntimeJniTypeName, }; string? trackedReplacementType; @@ -176,13 +177,56 @@ protected override void GetReplacementTypeInfoCore (string jniSimpleReference, o [("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, 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}\")"); 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 40ed2dc5390..a3deadac1a1 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 @@ -182,6 +182,12 @@ static ConcurrentDictionary GetStaticMethods (JniPeerMemb return GetCache (field, methods); } + static JniType GetStaticRedirect (JniMethodInfo method) + { + var field = typeof (JniMethodInfo).GetField ("StaticRedirect", BindingFlags.NonPublic | BindingFlags.Instance); + return (JniType) field.GetValue (method); + } + static ConcurrentDictionary GetCache (FieldInfo field, object owner) { return (ConcurrentDictionary) field.GetValue (owner); @@ -226,6 +232,279 @@ 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 DisposeReleasesInstanceMethodStaticRedirect () + { + var members = new JniPeerMembers (JavaLangRemappingTestObject.JniTypeName, typeof (JavaLangRemappingTestObject)); + var method = members.InstanceMethods.GetMethodInfo ("remappedToStaticHashCode.()I"); + var redirect = GetStaticRedirect (method); + Assert.IsTrue (redirect.PeerReference.IsValid); + + JniPeerMembers.Dispose (members); + + Assert.IsFalse (redirect.PeerReference.IsValid); + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void ConcurrentRemappedMethodLookupDoesNotLeakGlobalReferences () + { + const int iterationCount = 20; + const int concurrency = 16; + + RunIteration (); + int grefsBefore = JniEnvironment.Runtime.GlobalReferenceCount; + for (int i = 0; i < iterationCount; i++) + RunIteration (); + int grefsAfter = JniEnvironment.Runtime.GlobalReferenceCount; + + Assert.AreEqual (grefsBefore, grefsAfter); + + static void RunIteration () + { + var members = new JniPeerMembers (JavaLangRemappingTestObject.JniTypeName, typeof (JavaLangRemappingTestObject)); + try { + Parallel.For ( + 0, + concurrency, + _ => members.InstanceMethods.GetMethodInfo ("remappedToStaticHashCode.()I")); + } finally { + JniPeerMembers.Dispose (members); + } + } + } + + [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")] @@ -387,6 +666,49 @@ 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/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 6f7231a97ce..e59dabacb4a 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -330,7 +330,9 @@ public AndroidTypeManager (bool jniAddNativeMethodRegistrationAttributePresent) protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { - foreach (var ti in base.GetTypesForSimpleReference (jniSimpleReference)) + string lookupReference = JniRemappingLookup.GetReverseType (jniSimpleReference) ?? jniSimpleReference; + + foreach (var ti in base.GetTypesForSimpleReference (lookupReference)) yield return ti; var t = Java.Interop.TypeManager.GetJavaToManagedType (jniSimpleReference); @@ -340,7 +342,8 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl protected override Type? GetTypeForSimpleReference (string jniSimpleReference) { - var type = base.GetTypeForSimpleReference (jniSimpleReference); + string lookupReference = JniRemappingLookup.GetReverseType (jniSimpleReference) ?? jniSimpleReference; + var type = base.GetTypeForSimpleReference (lookupReference); if (type != null) { return type; } @@ -382,6 +385,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 +401,21 @@ 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 JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, string jniFieldName, string jniFieldSignature) + { + return JniRemappingLookup.GetReplacementFieldInfo (jniSourceType, jniFieldName, jniFieldSignature); + } + + protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, ReadOnlySpan jniFieldName, ReadOnlySpan jniFieldSignature) + { + return JniRemappingLookup.GetReplacementFieldInfo (jniSourceType, jniFieldName, jniFieldSignature); + } + protected override Type? GetInvokerTypeCore (Type type) { if (type.IsInterface || type.IsAbstract) { @@ -489,11 +513,8 @@ public override void RegisterNativeMembers (JniType nativeClass, Type type, Read { try { if (methods.IsEmpty) { - if (jniAddNativeMethodRegistrationAttributePresent) { -#pragma warning disable CS0618 // ReflectionJniTypeManager has not migrated its registration override to spans. + if (jniAddNativeMethodRegistrationAttributePresent) base.RegisterNativeMembers (nativeClass, type, methods.ToString ()); -#pragma warning restore CS0618 - } return; } else if (FastRegisterNativeMembers (nativeClass, type, methods)) { return; @@ -502,9 +523,7 @@ public override void RegisterNativeMembers (JniType nativeClass, Type type, Read int methodCount = CountMethods (methods); if (methodCount < 1) { if (jniAddNativeMethodRegistrationAttributePresent) { -#pragma warning disable CS0618 // ReflectionJniTypeManager has not migrated its registration override to spans. base.RegisterNativeMembers (nativeClass, type, methods.ToString ()); -#pragma warning restore CS0618 } return; } diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 0048287ee5f..43b3b250c18 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; @@ -47,7 +47,6 @@ internal struct JnienvInitializeArgs { } #pragma warning restore 0649 - internal static bool jniRemappingInUse; internal static bool MarshalMethodsEnabled; internal static bool PropagateExceptions; internal static BoundExceptionType BoundExceptionType; @@ -203,7 +202,7 @@ static void InitializeCommonState (JnienvInitializeArgs args) Logger.SetLogCategories ((LogCategories)args.logCategories); InitializeMaxGrefCounts (args); - 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 14819922893..6e30f308427 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/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index 5e6f3ef85e1..cb9cb228dfa 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -267,17 +267,22 @@ static Exception CreateJavaLocationException () return type; } + string lookupName = JniRemappingLookup.GetReverseType (class_name) ?? class_name; + if (TypeManagerMapDictionaries.JniToManaged.TryGetValue (lookupName, out type)) { + return type; + } + if (RuntimeFeature.TrimmableTypeMap) { throw new System.Diagnostics.UnreachableException ( $"{nameof (TypeManager)}.{nameof (GetJavaToManagedTypeCore)} should not be used when " + $"{nameof (RuntimeFeature.TrimmableTypeMap)} is enabled. The trimmable path should resolve " + $"types through {nameof (TrimmableTypeMapTypeManager)}."); } else { - type = clr_typemap_java_to_managed (class_name); + type = clr_typemap_java_to_managed (lookupName); } if (type != null) { - TypeManagerMapDictionaries.JniToManaged.Add (class_name, type); + TypeManagerMapDictionaries.JniToManaged.Add (lookupName, type); return type; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs index e03745ff054..9a6cf15253a 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 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 string? target_type; - public string? target_name; - public bool is_static; + 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,483 @@ 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 ()); + { + var replacement = GetReplacementMethodInfo (jniSourceType, jniMethodName.AsSpan (), jniMethodSignature.AsSpan ()); + if (replacement is not JniRuntime.ReplacementMethodInfo info) + return null; + + info.SourceJniType = jniSourceType; + info.SourceJniMethodName = jniMethodName; + info.SourceJniMethodSignature = jniMethodSignature; + return info; + } 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 (!JNIEnvInit.jniRemappingInUse) { + 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 (!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) { + 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 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 method `{jniSourceType}.{jniMethodName}{jniMethodSignature}` to " + - $"`{targetType}.{targetName}{newSignature}`; " + - $"param-count: {paramCount}; instance-to-static? {method.is_static}"; + var message = $"Remapping field `{jniSourceType}.{jniFieldName}:{jniFieldSignature}` to " + + $"`{targetType}.{targetName}:{targetSignature}`"; 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 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 a5e4e230bfd..a681a8b43a6 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs @@ -193,7 +193,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/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/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..51689b38dd4 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 @@ -29,16 +29,16 @@ "Size": 2396 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 9457592 + "Size": 9544272 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2818048 + "Size": 2819768 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4843824 + "Size": 4844368 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 196760 + "Size": 186248 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72432 @@ -53,7 +53,7 @@ "Size": 171584 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 119784 + "Size": 120080 }, "META-INF/androidx.activity_activity.version": { "Size": 6 @@ -2210,5 +2210,5 @@ "Size": 794696 } }, - "PackageSize": 15821708 + "PackageSize": 15907724 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc index 49dedb83b32..51ef9ca4d50 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc @@ -32,16 +32,16 @@ "Size": 2396 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 9457608 + "Size": 9544336 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2818048 + "Size": 2819768 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4843824 + "Size": 4844368 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 196760 + "Size": 186248 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72432 @@ -56,7 +56,7 @@ "Size": 171584 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 119784 + "Size": 120080 }, "META-INF/androidx.activity_activity.version": { "Size": 6 @@ -2213,5 +2213,5 @@ "Size": 794696 } }, - "PackageSize": 18209734 + "PackageSize": 18295750 } \ No newline at end of file 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 3fc603a446f..226c4596d85 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -485,7 +485,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; init.grefLogPath = Logger::gref_log_path (); init.lrefLogPath = Logger::lref_log_path (); 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 dbb37d6084f..78ffeced1a6 100644 --- a/src/native/clr/host/internal-pinvokes-shared.cc +++ b/src/native/clr/host/internal-pinvokes-shared.cc @@ -4,7 +4,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 c1fc0f1a5bb..88e758b74fb 100644 --- a/src/native/clr/include/runtime-base/internal-pinvokes.hh +++ b/src/native/clr/include/runtime-base/internal-pinvokes.hh @@ -18,8 +18,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 c4d6ce850eb..afc99202ce1 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 5712cb8aa81..7d276359767 100644 --- a/src/native/clr/pinvoke-override/precompiled.cc +++ b/src/native/clr/pinvoke-override/precompiled.cc @@ -37,12 +37,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_register_reference_logging_callbacks"sv) { return reinterpret_cast (&_monodroid_register_reference_logging_callbacks); } 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 d0ff6cf9ac2..57e7e651e63 100644 --- a/src/native/common/include/managed-interface.hh +++ b/src/native/common/include/managed-interface.hh @@ -36,6 +36,21 @@ namespace xamarin::android { const char *stack_trace); using reference_log_message_fn = void (*) (const char *message); + 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; @@ -51,7 +66,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/nativeaot/host/CMakeLists.txt b/src/native/nativeaot/host/CMakeLists.txt index 2f95d1405d8..db1f44849f8 100644 --- a/src/native/nativeaot/host/CMakeLists.txt +++ b/src/native/nativeaot/host/CMakeLists.txt @@ -31,6 +31,7 @@ set(XAMARIN_MONODROID_SOURCES host-environment.cc host-jni.cc internal-pinvoke-stubs.cc + jni-remapping-data-stub.cc ../runtime-base/android-system.cc diff --git a/src/native/nativeaot/host/host.cc b/src/native/nativeaot/host/host.cc index 08af3c9be65..d60c7c440c1 100644 --- a/src/native/nativeaot/host/host.cc +++ b/src/native/nativeaot/host/host.cc @@ -85,6 +85,7 @@ void Host::OnInit (jstring_wrapper &language, jstring_wrapper &files_dir, jstrin initArgs->maxGrefCount = static_cast(AndroidSystem::get_max_gref_count ()); initArgs->grefIGCUserPeer = env->NewGlobalRef (lrefIGCUserPeer); initArgs->grefGCUserPeerable = env->NewGlobalRef (lrefGCUserPeerable); + initArgs->jniRemappingData = &jni_remapping_data; initArgs->grefLogPath = Logger::gref_log_path (); initArgs->lrefLogPath = Logger::lref_log_path (); initArgs->referenceLogDirectory = Logger::reference_log_directory (); diff --git a/src/native/nativeaot/host/internal-pinvoke-stubs.cc b/src/native/nativeaot/host/internal-pinvoke-stubs.cc index 1e7dd83833b..f46f8f8944f 100644 --- a/src/native/nativeaot/host/internal-pinvoke-stubs.cc +++ b/src/native/nativeaot/host/internal-pinvoke-stubs.cc @@ -32,19 +32,6 @@ bool clr_typemap_java_to_managed ( pinvoke_unreachable (); } -const char* _monodroid_lookup_replacement_type ([[maybe_unused]] const char *jniSimpleReference) -{ - pinvoke_unreachable (); -} - -const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info ( - [[maybe_unused]] const char *jniSourceType, - [[maybe_unused]] const char *jniMethodName, - [[maybe_unused]] const char *jniMethodSignature) -{ - pinvoke_unreachable (); -} - managed_timing_sequence* monodroid_timing_start ([[maybe_unused]] const char *message) { pinvoke_unreachable (); diff --git a/src/native/nativeaot/host/jni-remapping-data-stub.cc b/src/native/nativeaot/host/jni-remapping-data-stub.cc new file mode 100644 index 00000000000..06ba6dd3a7a --- /dev/null +++ b/src/native/nativeaot/host/jni-remapping-data-stub.cc @@ -0,0 +1,9 @@ +#include + +using namespace xamarin::android; + +// Apps without remapping data use this empty descriptor. A later application-link step can +// override it with the strong definition generated from the application's remapping tables. +extern "C" { + [[gnu::weak]] extern const JniRemappingData jni_remapping_data {}; +} diff --git a/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh b/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh index 8b57bb79410..ab60e69431a 100644 --- a/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh +++ b/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh @@ -21,8 +21,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/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JniRemappingLookupTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JniRemappingLookupTests.cs new file mode 100644 index 00000000000..c5121c586d1 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JniRemappingLookupTests.cs @@ -0,0 +1,151 @@ +using System; +using System.Runtime.InteropServices; + +using Android.Runtime; +using Java.Interop; +using NUnit.Framework; + +namespace Java.InteropTests +{ + [TestFixture] + [Category ("NativeAOTIgnore")] + public class JniRemappingLookupTests + { + [TestCase ("net/dot/android/remap/AsciiFirst", "net/dot/android/remap/TargetFirst")] + [TestCase ("net/dot/android/remap/Middle", "net/dot/android/remap/TargetMiddle")] + [TestCase ("net/dot/android/remap/Zebra", "net/dot/android/remap/TargetLast")] + [TestCase ("net/dot/android/remap/Źródło", "net/dot/android/remap/UnicodeTarget")] + public void ReplacementTypeLookupUsesGeneratedTable (string source, string target) + { + Assert.AreEqual (target, JniEnvironment.Runtime.TypeManager.GetReplacementType (source)); + } + + [TestCase ("0/net/dot/android/remap/Before")] + [TestCase ("net/dot/android/remap/Between")] + [TestCase ("\ue000/net/dot/android/remap/After")] + public void ReplacementTypeLookupReturnsNullOutsideTable (string source) + { + Assert.IsNull (JniEnvironment.Runtime.TypeManager.GetReplacementType (source)); + } + + [Test] + public void IncomingRenamedPeerUsesReverseTypeWithLlvmIrTypeMap () + { + if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) + Assert.Ignore ("This test validates the nontrimmable LLVM-IR typemap path."); + + Assert.AreEqual ( + typeof (IncomingDeclaredPeer), + JniEnvironment.Runtime.TypeManager.GetType (new JniTypeSignature (IncomingDeclaredPeer.RuntimeJniName))); + + var handle = JNIEnv.CreateInstance (IncomingDeclaredPeer.RuntimeJniName, "()V"); + try { + using var peer = Java.Lang.Object.GetObject (handle, JniHandleOwnership.DoNotTransfer); + Assert.IsInstanceOf (peer); + } finally { + JNIEnv.DeleteLocalRef (handle); + } + } + + [Test] + [NonParallelizable] + public void ExplicitRuntimeRegistrationPrecedesReverseTypeWithLlvmIrTypeMap () + { + if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) + Assert.Ignore ("This test validates the nontrimmable LLVM-IR typemap path."); + + global::Java.Interop.TypeManager.RegisterType ( + ExplicitRegisteredPeer.RuntimeJniName, + typeof (ExplicitRegisteredPeer)); + + Assert.AreEqual ( + typeof (ExplicitRegisteredPeer), + JniEnvironment.Runtime.TypeManager.GetType (new JniTypeSignature (ExplicitRegisteredPeer.RuntimeJniName))); + + var handle = JNIEnv.CreateInstance (ExplicitRegisteredPeer.RuntimeJniName, "()V"); + try { + using var peer = Java.Lang.Object.GetObject (handle, JniHandleOwnership.DoNotTransfer); + Assert.IsInstanceOf (peer); + } finally { + JNIEnv.DeleteLocalRef (handle); + } + } + + [TestCase ("(I)I", "exact", "(I)I")] + [TestCase ("(I)V", "parameters", "(I)V")] + [TestCase ("(J)I", "wildcard", "(J)I")] + public void ReplacementMethodLookupPrefersSpecificSignature (string sourceSignature, string targetName, string targetSignature) + { + var info = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo ( + "net/dot/android/remap/ManagedLookup", + "overload", + sourceSignature); + + Assert.IsTrue (info.HasValue); + var replacement = info.GetValueOrDefault (); + Assert.AreEqual ( + "net/dot/android/remap/ManagedTarget", + GetString (replacement.TargetJniType, replacement.TargetJniTypeUtf8)); + Assert.AreEqual ( + targetName, + GetString (replacement.TargetJniMethodName, replacement.TargetJniMethodNameUtf8)); + Assert.AreEqual ( + targetSignature, + GetString (replacement.TargetJniMethodSignature, replacement.TargetJniMethodSignatureUtf8) ?? sourceSignature); + Assert.AreEqual ("net/dot/android/remap/ManagedLookup", replacement.SourceJniType); + Assert.AreEqual ("overload", replacement.SourceJniMethodName); + Assert.AreEqual (sourceSignature, replacement.SourceJniMethodSignature); + } + + [TestCase ("I", "exactValue", "J")] + [TestCase ("J", "wildcardValue", "J")] + public void ReplacementFieldLookupPrefersSpecificSignature (string sourceSignature, string targetName, string targetSignature) + { + var info = JniEnvironment.Runtime.TypeManager.GetReplacementFieldInfo ( + "net/dot/android/remap/ManagedLookup", + "value", + sourceSignature); + + Assert.IsTrue (info.HasValue); + var replacement = info.GetValueOrDefault (); + Assert.AreEqual ("net/dot/android/remap/ManagedTarget", replacement.TargetJniType); + Assert.AreEqual (targetName, replacement.TargetJniFieldName); + Assert.AreEqual (targetSignature, replacement.TargetJniFieldSignature); + } + + static string GetString (string value, IntPtr utf8) + => utf8 == IntPtr.Zero ? value : Marshal.PtrToStringUTF8 (utf8); + } + + [Register (DeclaredJniName, DoNotGenerateAcw = true)] + sealed class IncomingDeclaredPeer : Java.Lang.Object + { + public const string DeclaredJniName = "net/dot/android/remap/IncomingDeclaredPeer"; + public const string RuntimeJniName = "net/dot/android/remap/IncomingRenamedPeer"; + + public IncomingDeclaredPeer (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + [Register ("net/dot/android/remap/IncomingWrongPeer", DoNotGenerateAcw = true)] + sealed class IncomingWrongPeer : Java.Lang.Object + { + public IncomingWrongPeer (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + [Register ("net/dot/android/remap/ExplicitRegisteredPeer", DoNotGenerateAcw = true)] + sealed class ExplicitRegisteredPeer : Java.Lang.Object + { + public const string RuntimeJniName = "net/dot/android/remap/ExplicitRuntimePeer"; + + public ExplicitRegisteredPeer (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} 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 1aa71c64eda..8e85d99fc13 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 @@ -142,6 +142,7 @@ + @@ -194,6 +195,8 @@ + + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml index 643106c7d20..349faad5fd4 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml @@ -5,6 +5,27 @@ + + + + + + + + + + + + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/remap/ExplicitRuntimePeer.java b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/remap/ExplicitRuntimePeer.java new file mode 100644 index 00000000000..2a72f4be5f5 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/remap/ExplicitRuntimePeer.java @@ -0,0 +1,5 @@ +package net.dot.android.remap; + +public class ExplicitRuntimePeer { + public ExplicitRuntimePeer() {} +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/remap/IncomingRenamedPeer.java b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/remap/IncomingRenamedPeer.java new file mode 100644 index 00000000000..5eba46aa4a8 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/remap/IncomingRenamedPeer.java @@ -0,0 +1,5 @@ +package net.dot.android.remap; + +public class IncomingRenamedPeer { + public IncomingRenamedPeer() {} +}