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 5bbed8b7f11..e54e3e700cb 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -382,6 +382,12 @@ protected override IEnumerable GetSimpleReferences (Type type) return JniRemappingLookup.GetReplacementType (jniSimpleReference); } + protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8) + { + replacement = null; + replacementUtf8 = JniRemappingLookup.GetReplacementTypeUtf8 (jniSimpleReference); + } + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) { return JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); @@ -392,6 +398,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 +510,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 +520,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 9e6c6fa599d..889565a3d89 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -31,7 +31,7 @@ internal struct JnienvInitializeArgs { public int packageNamingPolicy; public byte ioExceptionType; public int jniAddNativeMethodRegistrationAttributePresent; - public bool jniRemappingInUse; + public IntPtr jniRemappingData; public bool marshalMethodsEnabled; public IntPtr grefGCUserPeerable; public IntPtr propagateUncaughtExceptionFn; @@ -39,7 +39,6 @@ internal struct JnienvInitializeArgs { } #pragma warning restore 0649 - internal static bool jniRemappingInUse; internal static bool MarshalMethodsEnabled; internal static bool PropagateExceptions; internal static BoundExceptionType BoundExceptionType; @@ -205,7 +204,7 @@ static void InitializeCommonState (JnienvInitializeArgs args) Logger.SetLogCategories ((LogCategories)args.logCategories); gref_gc_threshold = args.grefGcThreshold; - jniRemappingInUse = args.jniRemappingInUse; + JniRemappingLookup.Initialize (args.jniRemappingData); MarshalMethodsEnabled = args.marshalMethodsEnabled; java_class_loader = args.grefLoader; diff --git a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs index eaad49ce09f..cb00f9c86c3 100644 --- a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs +++ b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs @@ -60,18 +60,6 @@ internal unsafe static partial class RuntimeNativeMethods [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] internal static partial int _monodroid_weak_gref_dec (); - [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)] - [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] - internal static partial IntPtr _monodroid_lookup_replacement_type (string jniSimpleReference); - - [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)] - [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] - internal static partial IntPtr _monodroid_lookup_replacement_method_info (string jniSourceType, string jniMethodName, string jniMethodSignature); - - [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)] - [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] - internal static partial IntPtr _monodroid_lookup_replacement_method_info (string jniSourceType, byte* jniMethodName, byte* jniMethodSignature); - [LibraryImport (RuntimeConstants.InternalDllName)] [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] internal static partial void _monodroid_detect_cpu_and_architecture (ref ushort built_for_cpu, ref ushort running_on_cpu, ref byte is64bit); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs index e03745ff054..67859211720 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs @@ -2,6 +2,7 @@ using System; using System.Buffers; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; @@ -12,17 +13,101 @@ namespace Microsoft.Android.Runtime; static class JniRemappingLookup { -#pragma warning disable CS0649 // Field 'JniRemappingLookup.JniRemappingReplacementMethod.target_type' is never assigned to, and will always have its default value null - struct JniRemappingReplacementMethod + const int AsciiComparisonChunkSize = 16; + + unsafe struct NativeJniRemappingString + { + public uint length; + public byte* str; + } + + unsafe struct NativeJniRemappingReplacementMethod { - public string? target_type; - public string? target_name; - public bool is_static; + public byte* target_type; + public byte* target_name; + public byte* target_signature; + public byte is_static; + } + + unsafe struct NativeJniRemappingReplacementField + { + public byte* target_type; + public byte* target_name; + public byte* target_signature; + } + + unsafe struct NativeJniRemappingIndexMethodEntry + { + public NativeJniRemappingString name; + public NativeJniRemappingString signature; + public NativeJniRemappingReplacementMethod replacement; + } + + unsafe struct NativeJniRemappingIndexTypeEntry + { + public NativeJniRemappingString name; + public uint method_count; + public NativeJniRemappingIndexMethodEntry* methods; + } + + unsafe struct NativeJniRemappingTypeReplacementEntry + { + public NativeJniRemappingString name; + public byte* replacement; + } + + unsafe struct NativeJniRemappingIndexFieldEntry + { + public NativeJniRemappingString name; + public NativeJniRemappingString signature; + public NativeJniRemappingReplacementField replacement; + } + + unsafe struct NativeJniRemappingIndexFieldTypeEntry + { + public NativeJniRemappingString name; + public uint field_count; + public NativeJniRemappingIndexFieldEntry* fields; + } + + unsafe struct NativeJniRemappingData + { + public NativeJniRemappingTypeReplacementEntry* type_replacements; + public NativeJniRemappingTypeReplacementEntry* reverse_type_replacements; + public NativeJniRemappingIndexTypeEntry* method_replacement_index; + public NativeJniRemappingIndexFieldTypeEntry* field_replacement_index; + public uint type_replacement_count; + public uint reverse_type_replacement_count; + public uint method_replacement_index_count; + public uint field_replacement_index_count; + } + + static unsafe NativeJniRemappingData* nativeData; + static bool isInUse; + static readonly ConcurrentDictionary reverseTypes = new (StringComparer.Ordinal); + + internal static unsafe void Initialize (IntPtr data) + { + reverseTypes.Clear (); + if (data == IntPtr.Zero) { + isInUse = false; + return; + } + + nativeData = (NativeJniRemappingData*)data; + isInUse = + nativeData->type_replacement_count > 0 || + nativeData->reverse_type_replacement_count > 0 || + nativeData->method_replacement_index_count > 0 || + nativeData->field_replacement_index_count > 0; } -#pragma warning restore CS0649 internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSimpleReference, bool useReplacementTypes) { + if (useReplacementTypes) { + jniSimpleReference = GetReverseType (jniSimpleReference) ?? jniSimpleReference; + } + int slash = jniSimpleReference.LastIndexOf ('/'); var desugarType = slash > 0 ? $"{jniSimpleReference.Substring (0, slash + 1)}Desugar{jniSimpleReference.Substring (slash + 1)}" @@ -30,7 +115,6 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi var typeWithPrefix = $"{desugarType}$_CC"; var typeWithSuffix = $"{jniSimpleReference}$-CC"; - var replacements = new[] { useReplacementTypes ? GetReplacementType (typeWithPrefix) ?? typeWithPrefix : typeWithPrefix, useReplacementTypes ? GetReplacementType (typeWithSuffix) ?? typeWithSuffix : typeWithSuffix, @@ -44,95 +128,474 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi return replacements; } - internal static string? GetReplacementType (string? jniSimpleReference) + internal static unsafe string? GetReplacementType (string? jniSimpleReference) { - if (jniSimpleReference is null || !JNIEnvInit.jniRemappingInUse) { - return null; - } + IntPtr replacement = GetReplacementTypeUtf8 (jniSimpleReference); + return replacement == IntPtr.Zero ? null : Marshal.PtrToStringUTF8 (replacement); + } + + internal static unsafe IntPtr GetReplacementTypeUtf8 (string? jniSimpleReference) + { + if (jniSimpleReference is null || !isInUse || jniSimpleReference.Length == 0) + return IntPtr.Zero; + + NativeJniRemappingData* data = nativeData; + if (data == null) + throw new InvalidOperationException ("JNI remapping data has not been initialized."); + + return (IntPtr)LookupType (data->type_replacements, data->type_replacement_count, jniSimpleReference); + } - IntPtr ret = RuntimeNativeMethods._monodroid_lookup_replacement_type (jniSimpleReference); - if (ret == IntPtr.Zero) { + internal static unsafe string? GetReverseType (string? jniSimpleReference) + { + if (jniSimpleReference is null || !isInUse || jniSimpleReference.Length == 0) return null; - } - return Marshal.PtrToStringAnsi (ret); + string replacement = reverseTypes.GetOrAdd (jniSimpleReference, static source => LookupReverseType (source)); + return string.Equals (replacement, jniSimpleReference, StringComparison.Ordinal) ? null : replacement; + } + + static unsafe string LookupReverseType (string jniSimpleReference) + { + NativeJniRemappingData* data = nativeData; + if (data == null) + throw new InvalidOperationException ("JNI remapping data has not been initialized."); + + byte* replacement = LookupType ( + data->reverse_type_replacements, + data->reverse_type_replacement_count, + jniSimpleReference); + return replacement == null + ? jniSimpleReference + : Marshal.PtrToStringUTF8 ((IntPtr)replacement) ?? jniSimpleReference; } internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (string jniSourceType, string jniMethodName, string jniMethodSignature) => GetReplacementMethodInfo (jniSourceType, jniMethodName.AsSpan (), jniMethodSignature.AsSpan ()); internal static unsafe JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (string jniSourceType, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) + => GetReplacementMethodInfo (jniSourceType.AsSpan (), IntPtr.Zero, jniMethodName, jniMethodSignature); + + internal static unsafe JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (IntPtr jniSourceTypeUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) + { + if (jniSourceTypeUtf8 == IntPtr.Zero) + throw new ArgumentNullException (nameof (jniSourceTypeUtf8)); + return GetReplacementMethodInfo (default, jniSourceTypeUtf8, jniMethodName, jniMethodSignature); + } + + static unsafe JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo ( + ReadOnlySpan jniSourceType, + IntPtr jniSourceTypeUtf8, + ReadOnlySpan jniMethodName, + ReadOnlySpan jniMethodSignature) { - if (!JNIEnvInit.jniRemappingInUse) { + if (!isInUse) return null; - } - int nameLength = checked (Encoding.UTF8.GetByteCount (jniMethodName) + 1); - int signatureLength = checked (Encoding.UTF8.GetByteCount (jniMethodSignature) + 1); - byte[]? rentedName = null; - byte[]? rentedSignature = null; - IntPtr retInfo; - try { - if (nameLength > 512) - rentedName = ArrayPool.Shared.Rent (nameLength); - if (signatureLength > 512) - rentedSignature = ArrayPool.Shared.Rent (signatureLength); - - Span nameBuffer = rentedName == null - ? stackalloc byte [nameLength] - : rentedName.AsSpan (0, nameLength); - Span signatureBuffer = rentedSignature == null - ? stackalloc byte [signatureLength] - : rentedSignature.AsSpan (0, signatureLength); - Encoding.UTF8.GetBytes (jniMethodName, nameBuffer); - nameBuffer [nameLength - 1] = 0; - Encoding.UTF8.GetBytes (jniMethodSignature, signatureBuffer); - signatureBuffer [signatureLength - 1] = 0; - - fixed (byte* name = nameBuffer) - fixed (byte* signature = signatureBuffer) { - retInfo = RuntimeNativeMethods._monodroid_lookup_replacement_method_info (jniSourceType, name, signature); - } - } finally { - if (rentedName != null) - ArrayPool.Shared.Return (rentedName); - if (rentedSignature != null) - ArrayPool.Shared.Return (rentedSignature); - } - if (retInfo == IntPtr.Zero) { + NativeJniRemappingData* data = nativeData; + if (data == null) + throw new InvalidOperationException ("JNI remapping data has not been initialized."); + + byte* matchedSignature; + NativeJniRemappingReplacementMethod* method = jniSourceTypeUtf8 == IntPtr.Zero + ? LookupMethod (data, jniSourceType, jniMethodName, jniMethodSignature, out matchedSignature) + : LookupMethod (data, GetNullTerminatedUtf8Span (jniSourceTypeUtf8), jniMethodName, jniMethodSignature, out matchedSignature); + + if (method == null) return null; + if (method->target_type == null || method->target_name == null) { + string sourceType = GetSourceTypeForDiagnostics (jniSourceType, jniSourceTypeUtf8); + throw new InvalidOperationException ( + $"JNI remapping entry for `{sourceType}.{jniMethodName}{jniMethodSignature}` is missing target information."); } - var method = Marshal.PtrToStructure (retInfo); - var targetType = method.target_type ?? throw new InvalidOperationException ( - $"JNI remapping entry for `{jniSourceType}.{jniMethodName}{jniMethodSignature}` is missing a target type."); - var targetName = method.target_name ?? throw new InvalidOperationException ( - $"JNI remapping entry for `{jniSourceType}.{jniMethodName}{jniMethodSignature}` is missing a target method name."); - var sourceSignature = jniMethodSignature.ToString (); - var newSignature = sourceSignature; - int? paramCount = null; - if (method.is_static) { + bool isStatic = method->is_static != 0; + string? targetSignature = null; + if (isStatic) { + string sourceType = GetSourceTypeForDiagnostics (jniSourceType, jniSourceTypeUtf8); + string sourceSignature = jniMethodSignature.ToString (); paramCount = JniMemberSignature.GetParameterCountFromMethodSignature (sourceSignature) + 1; - newSignature = $"(L{jniSourceType};" + sourceSignature.Substring ("(".Length); + targetSignature = method->target_signature == null + ? $"(L{sourceType};" + sourceSignature.Substring ("(".Length) + : Marshal.PtrToStringUTF8 ((IntPtr)method->target_signature); } + var ret = new JniRuntime.ReplacementMethodInfo { + TargetJniTypeUtf8 = (IntPtr)method->target_type, + TargetJniMethodNameUtf8 = (IntPtr)method->target_name, + TargetJniMethodSignature = targetSignature, + TargetJniMethodSignatureUtf8 = isStatic + ? IntPtr.Zero + : (IntPtr)(method->target_signature == null ? matchedSignature : method->target_signature), + TargetJniMethodParameterCount = paramCount, + TargetJniMethodInstanceToStatic = isStatic, + }; + if (Logger.LogAssembly) { - var message = $"Remapping method `{jniSourceType}.{jniMethodName}{jniMethodSignature}` to " + - $"`{targetType}.{targetName}{newSignature}`; " + - $"param-count: {paramCount}; instance-to-static? {method.is_static}"; + string sourceType = GetSourceTypeForDiagnostics (jniSourceType, jniSourceTypeUtf8); + string targetType = Marshal.PtrToStringUTF8 ((IntPtr)method->target_type) ?? ""; + string targetName = Marshal.PtrToStringUTF8 ((IntPtr)method->target_name) ?? ""; + string effectiveTargetSignature = targetSignature ?? + (matchedSignature == null ? jniMethodSignature.ToString () : Marshal.PtrToStringUTF8 ((IntPtr)matchedSignature) ?? ""); + var message = $"Remapping method `{sourceType}.{jniMethodName}{jniMethodSignature}` to " + + $"`{targetType}.{targetName}{effectiveTargetSignature}`; " + + $"param-count: {paramCount}; instance-to-static? {isStatic}"; Logger.Log (LogLevel.Debug, "monodroid-assembly", message); } - return new JniRuntime.ReplacementMethodInfo { - SourceJniType = jniSourceType, - SourceJniMethodName = jniMethodName.ToString (), - SourceJniMethodSignature = sourceSignature, - TargetJniType = targetType, - TargetJniMethodName = targetName, - TargetJniMethodSignature = newSignature, - TargetJniMethodParameterCount = paramCount, - TargetJniMethodInstanceToStatic = method.is_static, + return ret; + } + + internal static JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo (string jniSourceType, string jniFieldName, string jniFieldSignature) + => GetReplacementFieldInfo (jniSourceType, jniFieldName.AsSpan (), jniFieldSignature.AsSpan ()); + + internal static unsafe JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo ( + string jniSourceType, + ReadOnlySpan jniFieldName, + ReadOnlySpan jniFieldSignature) + { + if (!isInUse) + return null; + + NativeJniRemappingData* data = nativeData; + if (data == null) + throw new InvalidOperationException ("JNI remapping data has not been initialized."); + + NativeJniRemappingReplacementField* field = LookupField ( + data, + jniSourceType, + jniFieldName, + jniFieldSignature); + if (field == null) + return null; + if (field->target_type == null || field->target_name == null) { + throw new InvalidOperationException ( + $"JNI remapping entry for `{jniSourceType}.{jniFieldName}:{jniFieldSignature}` is missing target information."); + } + + string targetType = Marshal.PtrToStringUTF8 ((IntPtr)field->target_type) ?? ""; + string targetName = Marshal.PtrToStringUTF8 ((IntPtr)field->target_name) ?? ""; + string targetSignature = field->target_signature == null + ? jniFieldSignature.ToString () + : Marshal.PtrToStringUTF8 ((IntPtr)field->target_signature) ?? ""; + + if (Logger.LogAssembly) { + var message = $"Remapping field `{jniSourceType}.{jniFieldName}:{jniFieldSignature}` to " + + $"`{targetType}.{targetName}:{targetSignature}`"; + Logger.Log (LogLevel.Debug, "monodroid-assembly", message); + } + + return new JniRuntime.ReplacementFieldInfo { + SourceJniType = jniSourceType, + SourceJniFieldName = jniFieldName.ToString (), + SourceJniFieldSignature = jniFieldSignature.ToString (), + TargetJniType = targetType, + TargetJniFieldName = targetName, + TargetJniFieldSignature = targetSignature, }; } + + static string GetSourceTypeForDiagnostics (ReadOnlySpan jniSourceType, IntPtr jniSourceTypeUtf8) + { + if (jniSourceTypeUtf8 == IntPtr.Zero) + return jniSourceType.ToString (); + return Marshal.PtrToStringUTF8 (jniSourceTypeUtf8) ?? ""; + } + + static unsafe bool Equal (NativeJniRemappingString value, ReadOnlySpan key) + { + return value.length == (uint)key.Length && + new ReadOnlySpan (value.str, key.Length).SequenceEqual (key); + } + + static unsafe bool Equal (NativeJniRemappingString value, ReadOnlySpan key, bool keyIsAscii) + { + ReadOnlySpan utf8 = new ReadOnlySpan (value.str, checked ((int)value.length)); + return keyIsAscii + ? Ascii.Equals (utf8, key) + : CompareUtf8ToUtf16 (utf8, key) == 0; + } + + static unsafe int Compare (NativeJniRemappingString value, ReadOnlySpan key) + { + return new ReadOnlySpan (value.str, checked ((int)value.length)).SequenceCompareTo (key); + } + + static unsafe int Compare (NativeJniRemappingString value, ReadOnlySpan key, bool keyIsAscii) + { + ReadOnlySpan utf8 = new ReadOnlySpan (value.str, checked ((int)value.length)); + return keyIsAscii ? CompareUtf8ToAscii (utf8, key) : CompareUtf8ToUtf16 (utf8, key); + } + + static int CompareUtf8ToAscii (ReadOnlySpan utf8, ReadOnlySpan ascii) + { + int commonLength = Math.Min (utf8.Length, ascii.Length); + int offset = 0; + while (commonLength - offset >= AsciiComparisonChunkSize) { + ReadOnlySpan utf8Chunk = utf8.Slice (offset, AsciiComparisonChunkSize); + ReadOnlySpan asciiChunk = ascii.Slice (offset, AsciiComparisonChunkSize); + if (!Ascii.Equals (utf8Chunk, asciiChunk)) { + for (int i = 0; i < AsciiComparisonChunkSize; i++) { + int result = utf8Chunk [i].CompareTo ((byte)asciiChunk [i]); + if (result != 0) + return result; + } + } + offset += AsciiComparisonChunkSize; + } + + ReadOnlySpan utf8Tail = utf8.Slice (offset, commonLength - offset); + ReadOnlySpan asciiTail = ascii.Slice (offset, commonLength - offset); + if (!Ascii.Equals (utf8Tail, asciiTail)) { + for (int i = 0; i < utf8Tail.Length; i++) { + int result = utf8Tail [i].CompareTo ((byte)asciiTail [i]); + if (result != 0) + return result; + } + } + return utf8.Length.CompareTo (ascii.Length); + } + + static int CompareUtf8ToUtf16 (ReadOnlySpan utf8, ReadOnlySpan utf16) + { + // Generated table strings and runtime JNI names are well-formed Unicode. Replacement behavior + // below only keeps the comparator deterministic if malformed input reaches this internal API. + while (!utf8.IsEmpty && !utf16.IsEmpty) { + while (!utf8.IsEmpty && !utf16.IsEmpty && utf8 [0] < 0x80 && utf16 [0] < 0x80) { + int result = utf8 [0].CompareTo ((byte)utf16 [0]); + if (result != 0) + return result; + utf8 = utf8.Slice (1); + utf16 = utf16.Slice (1); + } + if (utf8.IsEmpty || utf16.IsEmpty) + break; + + OperationStatus utf8Status = Rune.DecodeFromUtf8 (utf8, out Rune utf8Rune, out int utf8Consumed); + if (utf8Status != OperationStatus.Done) { + utf8Rune = Rune.ReplacementChar; + utf8Consumed = 1; + } + + OperationStatus utf16Status = Rune.DecodeFromUtf16 (utf16, out Rune utf16Rune, out int utf16Consumed); + if (utf16Status != OperationStatus.Done) { + utf16Rune = Rune.ReplacementChar; + utf16Consumed = 1; + } + + int runeComparison = utf8Rune.Value.CompareTo (utf16Rune.Value); + if (runeComparison != 0) + return runeComparison; + + utf8 = utf8.Slice (utf8Consumed); + utf16 = utf16.Slice (utf16Consumed); + } + + if (utf8.IsEmpty) + return utf16.IsEmpty ? 0 : -1; + return 1; + } + + static unsafe int LowerBoundByName (void* entries, uint count, int entrySize, ReadOnlySpan key) + { + int left = 0; + int right = checked ((int)count); + while (left < right) { + int middle = left + ((right - left) / 2); + var name = *(NativeJniRemappingString*)((byte*)entries + (middle * entrySize)); + if (Compare (name, key) < 0) { + left = middle + 1; + } else { + right = middle; + } + } + return left; + } + + static unsafe int LowerBoundByName (void* entries, uint count, int entrySize, ReadOnlySpan key, bool keyIsAscii) + { + int left = 0; + int right = checked ((int)count); + while (left < right) { + int middle = left + ((right - left) / 2); + var name = *(NativeJniRemappingString*)((byte*)entries + (middle * entrySize)); + if (Compare (name, key, keyIsAscii) < 0) { + left = middle + 1; + } else { + right = middle; + } + } + return left; + } + + static unsafe byte* LookupType (NativeJniRemappingTypeReplacementEntry* entries, uint count, ReadOnlySpan key) + { + bool keyIsAscii = Ascii.IsValid (key); + int index = LowerBoundByName (entries, count, sizeof (NativeJniRemappingTypeReplacementEntry), key, keyIsAscii); + if (index >= checked ((int)count) || !Equal (entries [index].name, key, keyIsAscii)) + return null; + return entries [index].replacement; + } + + static unsafe byte* LookupType (NativeJniRemappingTypeReplacementEntry* entries, uint count, ReadOnlySpan key) + { + int index = LowerBoundByName (entries, count, sizeof (NativeJniRemappingTypeReplacementEntry), key); + if (index >= checked ((int)count) || !Equal (entries [index].name, key)) + return null; + return entries [index].replacement; + } + + static unsafe NativeJniRemappingReplacementMethod* LookupMethod ( + NativeJniRemappingData* data, + ReadOnlySpan sourceType, + ReadOnlySpan name, + ReadOnlySpan signature, + out byte* matchedSignature) + { + matchedSignature = null; + int typeIndex = LowerBoundByName ( + data->method_replacement_index, + data->method_replacement_index_count, + sizeof (NativeJniRemappingIndexTypeEntry), + sourceType + ); + if (typeIndex >= checked ((int)data->method_replacement_index_count) || + !Equal (data->method_replacement_index [typeIndex].name, sourceType)) + return null; + + NativeJniRemappingIndexTypeEntry* type = &data->method_replacement_index [typeIndex]; + return LookupMethod (type, name, signature, out matchedSignature); + } + + static unsafe NativeJniRemappingReplacementMethod* LookupMethod ( + NativeJniRemappingData* data, + ReadOnlySpan sourceType, + ReadOnlySpan name, + ReadOnlySpan signature, + out byte* matchedSignature) + { + matchedSignature = null; + bool sourceTypeIsAscii = Ascii.IsValid (sourceType); + int typeIndex = LowerBoundByName ( + data->method_replacement_index, + data->method_replacement_index_count, + sizeof (NativeJniRemappingIndexTypeEntry), + sourceType, + sourceTypeIsAscii + ); + if (typeIndex >= checked ((int)data->method_replacement_index_count) || + !Equal (data->method_replacement_index [typeIndex].name, sourceType, sourceTypeIsAscii)) + return null; + + NativeJniRemappingIndexTypeEntry* type = &data->method_replacement_index [typeIndex]; + return LookupMethod (type, name, signature, out matchedSignature); + } + + static unsafe NativeJniRemappingReplacementMethod* LookupMethod ( + NativeJniRemappingIndexTypeEntry* type, + ReadOnlySpan name, + ReadOnlySpan signature, + out byte* matchedSignature) + { + matchedSignature = null; + bool nameIsAscii = Ascii.IsValid (name); + int first = LowerBoundByName (type->methods, type->method_count, sizeof (NativeJniRemappingIndexMethodEntry), name, nameIsAscii); + int count = checked ((int)type->method_count); + if (first >= count || !Equal (type->methods [first].name, name, nameIsAscii)) + return null; + + int last = first + 1; + while (last < count && Equal (type->methods [last].name, name, nameIsAscii)) + last++; + + if (signature.Length > 0) { + bool signatureIsAscii = Ascii.IsValid (signature); + for (int i = first; i < last; i++) { + NativeJniRemappingIndexMethodEntry* entry = &type->methods [i]; + if (entry->signature.length != 0 && Equal (entry->signature, signature, signatureIsAscii)) { + matchedSignature = entry->signature.str; + return &entry->replacement; + } + } + + int closeParenthesis = signature.Length - 1; + while (closeParenthesis >= 0 && signature [closeParenthesis] != ')') + closeParenthesis--; + int prefixLength = closeParenthesis + 1; + if (prefixLength > 0 && prefixLength != signature.Length) { + ReadOnlySpan signaturePrefix = signature.Slice (0, prefixLength); + bool signaturePrefixIsAscii = signatureIsAscii || Ascii.IsValid (signaturePrefix); + for (int i = first; i < last; i++) { + NativeJniRemappingIndexMethodEntry* entry = &type->methods [i]; + if (entry->signature.length != 0 && Equal (entry->signature, signaturePrefix, signaturePrefixIsAscii)) + return &entry->replacement; + } + } + } + + for (int i = first; i < last; i++) { + NativeJniRemappingIndexMethodEntry* entry = &type->methods [i]; + if (entry->signature.length == 0) { + return &entry->replacement; + } + } + return null; + } + + static unsafe NativeJniRemappingReplacementField* LookupField ( + NativeJniRemappingData* data, + ReadOnlySpan sourceType, + ReadOnlySpan name, + ReadOnlySpan signature) + { + bool sourceTypeIsAscii = Ascii.IsValid (sourceType); + int typeIndex = LowerBoundByName ( + data->field_replacement_index, + data->field_replacement_index_count, + sizeof (NativeJniRemappingIndexFieldTypeEntry), + sourceType, + sourceTypeIsAscii); + if (typeIndex >= checked ((int)data->field_replacement_index_count) || + !Equal (data->field_replacement_index [typeIndex].name, sourceType, sourceTypeIsAscii)) + return null; + + NativeJniRemappingIndexFieldTypeEntry* type = &data->field_replacement_index [typeIndex]; + bool nameIsAscii = Ascii.IsValid (name); + int first = LowerBoundByName ( + type->fields, + type->field_count, + sizeof (NativeJniRemappingIndexFieldEntry), + name, + nameIsAscii); + int count = checked ((int)type->field_count); + if (first >= count || !Equal (type->fields [first].name, name, nameIsAscii)) + return null; + + int last = first + 1; + while (last < count && Equal (type->fields [last].name, name, nameIsAscii)) + last++; + + bool signatureIsAscii = Ascii.IsValid (signature); + for (int i = first; i < last; i++) { + NativeJniRemappingIndexFieldEntry* entry = &type->fields [i]; + if (entry->signature.length != 0 && Equal (entry->signature, signature, signatureIsAscii)) + return &entry->replacement; + } + for (int i = first; i < last; i++) { + NativeJniRemappingIndexFieldEntry* entry = &type->fields [i]; + if (entry->signature.length == 0) + return &entry->replacement; + } + return null; + } + + static unsafe ReadOnlySpan GetNullTerminatedUtf8Span (IntPtr value) + { + byte* start = (byte*)value; + int length = 0; + while (start [length] != 0) + length++; + return new ReadOnlySpan (start, length); + } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index ca6be17f1c5..2ce9320463d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -173,6 +173,7 @@ internal static JavaPeerProxy[] GetProxyArrayCacheEntry (object cacheEntry) /// JavaPeerProxy? GetProxyForJniClass (string className, Type? targetType) { + className = JniRemappingLookup.GetReverseType (className) ?? className; var cacheEntry = GetProxyCacheEntryForJniName (className); if (cacheEntry is JavaPeerProxy singleProxy) { return targetType is null || TargetTypeMatches (targetType, singleProxy.TargetType) @@ -267,7 +268,8 @@ bool TryResolveProxyFromSealedTargetType ( var targetClass = default (JniObjectReference); try { - targetClass = JniEnvironment.Types.FindClass (targetProxy.JniName); + string runtimeJniName = JniRemappingLookup.GetReplacementType (targetProxy.JniName) ?? targetProxy.JniName; + targetClass = JniEnvironment.Types.FindClass (runtimeJniName); var reference = new JniObjectReference (handle); if (JniEnvironment.Types.IsInstanceOf (reference, targetClass)) { proxy = targetProxy; @@ -403,7 +405,8 @@ static JniMethodInfo GetClassGetInterfacesMethod () try { objClass = JniEnvironment.Types.GetObjectClass (selfRef); try { - targetClass = JniEnvironment.Types.FindClass (targetJniName); + string runtimeJniName = JniRemappingLookup.GetReplacementType (targetJniName) ?? targetJniName; + targetClass = JniEnvironment.Types.FindClass (runtimeJniName); } catch (Java.Lang.ClassNotFoundException) { // FindClass throws for managed types whose Java peer class is // not present in the APK (e.g. test types annotated with @@ -555,6 +558,7 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa return; } + className = JniRemappingLookup.GetReverseType (className) ?? className; var cacheEntry = s_instance.GetProxyCacheEntryForJniName (className); if (cacheEntry is JavaPeerProxy[] proxies && proxies.Length == 0) { return; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 59856a9db82..6b4ff814d22 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -196,6 +196,15 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl yield return builtInType; } + // The type map is keyed by the JNI names the managed code declares, so a name that was + // renamed in the packaged application has to be translated back first. + if (GetOriginalSimpleReference (jniSimpleReference) is string originalReference) { + foreach (var type in TrimmableTypeMap.Instance.GetTargetTypes (originalReference)) { + yield return type; + } + yield break; + } + foreach (var type in TrimmableTypeMap.Instance.GetTargetTypes (jniSimpleReference)) { yield return type; } @@ -210,11 +219,21 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl return builtInType; } - if (TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var type)) { - return type; + if (GetOriginalSimpleReference (jniSimpleReference) is string originalReference) { + return TrimmableTypeMap.Instance.TryGetTargetType (originalReference, out var type) ? type : null; + } + + return TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var directType) ? directType : null; + } + + static string? GetOriginalSimpleReference (string jniSimpleReference) + { + var original = JniRemappingLookup.GetReverseType (jniSimpleReference); + if (original is null || string.Equals (original, jniSimpleReference, StringComparison.Ordinal)) { + return null; } - return null; + return original; } // Lookup of the built-in managed type for a JNI simple reference, e.g., string, bool?, int?, etc. @@ -271,7 +290,8 @@ static JniTypeSignature GetTypeSignatureUncached (Type type) while (currentType is not null) { if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (currentType, out var jniName)) { - return new (jniName, rank, keyword: false); + string runtimeJniName = JniRemappingLookup.GetReplacementType (jniName) ?? jniName; + return new (runtimeJniName, rank, keyword: false); } currentType = currentType.BaseType; @@ -370,7 +390,7 @@ protected override IEnumerable GetTypeSignaturesCore (Type typ return signature.IsValid ? [signature] : []; } - // Remapping APIs for InTune support + // Remapping APIs, used by the Intune/MAM mapping and generated JNI runtime remapping protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) => JniRemappingLookup.GetStaticMethodFallbackTypes (jniSimpleReference, useReplacementTypes: true); @@ -378,12 +398,27 @@ protected override IEnumerable GetTypeSignaturesCore (Type typ protected override string? GetReplacementTypeCore (string jniSimpleReference) => JniRemappingLookup.GetReplacementType (jniSimpleReference); + protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8) + { + replacement = null; + replacementUtf8 = JniRemappingLookup.GetReplacementTypeUtf8 (jniSimpleReference); + } + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) => JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) => JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (IntPtr jniSourceTypeUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) + => JniRemappingLookup.GetReplacementMethodInfo (jniSourceTypeUtf8, jniMethodName, jniMethodSignature); + + protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, string jniFieldName, string jniFieldSignature) + => JniRemappingLookup.GetReplacementFieldInfo (jniSourceType, jniFieldName, jniFieldSignature); + + protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, ReadOnlySpan jniFieldName, ReadOnlySpan jniFieldSignature) + => JniRemappingLookup.GetReplacementFieldInfo (jniSourceType, jniFieldName, jniFieldSignature); + // The rest of the APIs are unsupported - they are not needed internally anywhere anyway protected override Type? GetInvokerTypeCore (Type type) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs index b2344f8fd2a..483ee7b3d22 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs @@ -192,7 +192,8 @@ static bool IsIncompatibleCast ( var instanceClass = JniEnvironment.Types.GetObjectClass (reference); JniObjectReference targetClass = default; try { - targetClass = JniEnvironment.Types.FindClass (targetJniName); + string runtimeJniName = JniRemappingLookup.GetReplacementType (targetJniName) ?? targetJniName; + targetClass = JniEnvironment.Types.FindClass (runtimeJniName); if (!JniEnvironment.Types.IsAssignableFrom (instanceClass, targetClass)) { // Match the legacy cast diagnostic when assembly logging is enabled. diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 04363884dcd..b2bb71438f6 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -2004,6 +2004,96 @@ public static string XA4324 { return ResourceManager.GetString("XA4324", resourceCulture); } } + + public static string XA4327 { + get { + return ResourceManager.GetString("XA4327", resourceCulture); + } + } + + public static string XA4327_MappingNotFound { + get { + return ResourceManager.GetString("XA4327_MappingNotFound", resourceCulture); + } + } + + public static string XA4327_MappingDataFailure { + get { + return ResourceManager.GetString("XA4327_MappingDataFailure", resourceCulture); + } + } + + public static string XA4327_AssemblyReadFailure { + get { + return ResourceManager.GetString("XA4327_AssemblyReadFailure", resourceCulture); + } + } + + public static string XA4327_AmbiguousEntry { + get { + return ResourceManager.GetString("XA4327_AmbiguousEntry", resourceCulture); + } + } + + public static string XA4327_NativeAotObjectRequired { + get { + return ResourceManager.GetString("XA4327_NativeAotObjectRequired", resourceCulture); + } + } + + public static string XA4327_NativeAotObjectReadFailure { + get { + return ResourceManager.GetString("XA4327_NativeAotObjectReadFailure", resourceCulture); + } + } + + public static string XA4327_NativeAotModeRequired { + get { + return ResourceManager.GetString("XA4327_NativeAotModeRequired", resourceCulture); + } + } + + public static string XA4327_NativeAotObjectFormat { + get { + return ResourceManager.GetString("XA4327_NativeAotObjectFormat", resourceCulture); + } + } + + public static string XA4327_NativeAotInvalidSection { + get { + return ResourceManager.GetString("XA4327_NativeAotInvalidSection", resourceCulture); + } + } + + public static string XA4327_NativeAotTruncatedSection { + get { + return ResourceManager.GetString("XA4327_NativeAotTruncatedSection", resourceCulture); + } + } + + public static string XA4327_NativeAotMissingSections { + get { + return ResourceManager.GetString("XA4327_NativeAotMissingSections", resourceCulture); + } + } + + public static string XA4328 { + get { + return ResourceManager.GetString("XA4328", resourceCulture); + } + } + + public static string XA4328_ConflictingEntry { + get { + return ResourceManager.GetString("XA4328_ConflictingEntry", resourceCulture); + } + } + + public static string XA4328_UnsupportedSignature { + get { + return ResourceManager.GetString("XA4328_UnsupportedSignature", resourceCulture); + } + } /// /// Looks up a localized string similar to Missing Android NDK toolchains directory '{0}'. Please install the Android NDK.. diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index ef71d9feb30..cf9d3109c0d 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -888,6 +888,85 @@ Remove the '{0}' reference from your project and add the '{1}' NuGet package ins [{0}] Unable to delete source file '{1}' {0} - The target architecture, such as Arm, Arm64, or X86_64 {1} - The path to the source file which could not be deleted. + + + Failed to generate the R8 JNI remapping data. {0} + The following are literal names and should not be translated: R8, JNI. +{0} - A sentence describing the specific failure. + + + The R8 mapping file '{0}' was not found. + The following is a literal name and should not be translated: R8. +{0} - The path of the missing mapping file. + + + The R8 mapping file '{0}' could not be read: {1} + The following is a literal name and should not be translated: R8. +{0} - The path of the mapping file. +{1} - The exception message. + + + The linked managed assembly '{0}' could not be read: {1} + {0} - The path of the managed assembly. +{1} - The exception message. + + + The generated '{0}' entries for '{1}' have conflicting runtime targets '{2}' and '{3}', which cannot be represented by the JNI remapping tables. + The following is a literal name and should not be translated: JNI. +{0} - The XML element name. +{1} - The runtime lookup key. +{2} - The first target. +{3} - The conflicting target. + + + NativeAOT JNI retention requires an existing post-ILC NativeAotObjectFile; '{0}' was not found. + The following are literal names and should not be translated: NativeAOT, JNI, ILC, NativeAotObjectFile. +{0} - The path of the missing ILC native object, or an empty string if none was supplied. + + + The NativeAOT retention object '{0}' could not be read: {1} + The following is a literal name and should not be translated: NativeAOT. +{0} - The path of the ILC native object. +{1} - The exception message. + + + NativeAotObjectFile requires NativeAot=true. + The following are literal names and should not be translated: NativeAotObjectFile, NativeAot=true. + + + Expected a 32-bit or 64-bit little-endian relocatable NativeAOT ELF object. + The following are literal names and should not be translated: NativeAOT, ELF. + + + The NativeAOT ELF object contains an invalid section extent. + The following are literal names and should not be translated: NativeAOT, ELF. + + + The NativeAOT ELF object contains truncated section data. + The following are literal names and should not be translated: NativeAOT, ELF. + + + The NativeAOT object must contain allocated __managedcode and initialized data sections. + The following are literal names and should not be translated: NativeAOT, __managedcode. + + + The R8 JNI remapping data is incomplete. {0} + The following are literal names and should not be translated: R8, JNI. +{0} - A sentence describing the specific omission. + + + The '{0}' entry for '{1}' was not emitted: another JNI remapping input already maps it to '{2}', which conflicts with '{3}'. + The following is a literal name and should not be translated: JNI. +{0} - The XML element name. +{1} - The source lookup key. +{2} - The existing target. +{3} - The generated target. + + + The entry for '{0}' was not emitted: its signature '{1}' could not be converted to a JNI descriptor. + The following is a literal name and should not be translated: JNI. +{0} - The member the entry describes. +{1} - The Java signature which could not be converted. Missing Android NDK toolchains directory '{0}'. Please install the Android NDK. diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs index 16cf42c3533..532af78968f 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 JniRemappingNativeCodeGenerator (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 JniRemappingNativeCodeGenerator (Log, typeReplacements, reverseTypeReplacements, methodReplacements, fieldReplacements)); } - void Generate (JniRemappingAssemblyGenerator jniRemappingComposer, int typeReplacementsCount) + void Generate (JniRemappingNativeCodeGenerator 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,13 @@ 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); @@ -135,11 +163,31 @@ void ReadXml (XmlReader reader, List typeReplacemen continue; } - string sourceMethodSignature = reader.GetAttribute ("source-method-signature"); + string sourceMethodSignature = reader.GetAttribute ("source-method-signature") ?? ""; + // Optional: inputs which predate it (for example the Intune/MAM mapping) keep + // the source signature on the target method. + string? targetMethodSignature = reader.GetAttribute ("target-method-signature"); methodReplacements.Add ( new JniRemappingMethodReplacement ( sourceType, sourceMethodName, sourceMethodSignature, - targetType, targetMethodName, isStatic + targetType, targetMethodName, targetMethodSignature, isStatic + ) + ); + } else if (MonoAndroidHelper.StringEquals ("replace-field", reader.LocalName)) { + haveAllAttributes &= GetRequiredAttribute ("source-type", out string sourceType); + haveAllAttributes &= GetRequiredAttribute ("source-field-name", out string sourceFieldName); + haveAllAttributes &= GetRequiredAttribute ("target-type", out string targetType); + haveAllAttributes &= GetRequiredAttribute ("target-field-name", out string targetFieldName); + if (!haveAllAttributes) { + continue; + } + + string sourceFieldSignature = reader.GetAttribute ("source-field-signature") ?? ""; + string? targetFieldSignature = reader.GetAttribute ("target-field-signature"); + fieldReplacements.Add ( + new JniRemappingFieldReplacement ( + sourceType, sourceFieldName, sourceFieldSignature, + targetType, targetFieldName, targetFieldSignature ) ); } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs new file mode 100644 index 00000000000..5688f0090a1 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs @@ -0,0 +1,418 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Xml; + +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; + +using Xamarin.Android.Tasks.JniRemapping; + +namespace Xamarin.Android.Tasks +{ + /// + /// Converts an R8 mapping file into runtime JNI remapping XML without modifying managed + /// assemblies. + /// + public class GenerateR8JniRemapping : AndroidTask + { + public override string TaskPrefix => "GR8JR"; + + [Required] + public string MappingFile { get; set; } = ""; + + [Required] + public string OutputFile { get; set; } = ""; + + public ITaskItem []? ExistingRemapXmlFiles { get; set; } + + public ITaskItem []? LinkedAssemblies { get; set; } + + public bool NativeAot { get; set; } + + public string? NativeAotObjectFile { get; set; } + + readonly Dictionary existingEntries = new Dictionary (StringComparer.Ordinal); + readonly HashSet preexistingEntryKeys = new HashSet (StringComparer.Ordinal); + readonly HashSet externallyOwnedTypes = new HashSet (StringComparer.Ordinal); + + public override bool RunTask () + { + if (!File.Exists (MappingFile)) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_MappingNotFound, MappingFile)); + return false; + } + + R8Mapping mapping; + try { + mapping = R8Mapping.Load (MappingFile); + } catch (Exception ex) when (ex is FormatException || ex is IOException || ex is UnauthorizedAccessException) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_MappingDataFailure, MappingFile, ex.Message)); + return false; + } + + ReadExistingEntries (); + + HashSet? requiredEntries; + if (NativeAot) { + if (NativeAotObjectFile.IsNullOrEmpty () || !File.Exists (NativeAotObjectFile)) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_NativeAotObjectRequired, NativeAotObjectFile ?? "")); + return false; + } + try { + requiredEntries = NativeAotJniRetention.GetRequiredEntries (NativeAotObjectFile, mapping); + } catch (Exception ex) when (ex is IOException || ex is InvalidDataException || ex is UnauthorizedAccessException) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_NativeAotObjectReadFailure, NativeAotObjectFile, ex.Message)); + return false; + } + Log.LogDebugMessage ($"Post-ILC NativeAOT JNI retention selected {requiredEntries.Count} mapping entries."); + } else { + if (!NativeAotObjectFile.IsNullOrEmpty ()) { + LogR8JniRemappingError (Properties.Resources.XA4327_NativeAotModeRequired); + return false; + } + ScanLinkedAssemblies (mapping); + requiredEntries = LinkedAssemblies?.Length > 0 + ? new HashSet (mapping.AccessedEntries, StringComparer.Ordinal) + : null; + } + if (Log.HasLoggedErrors) { + return false; + } + + string content = GenerateContent (mapping, requiredEntries); + if (Log.HasLoggedErrors) { + return false; + } + string? directory = Path.GetDirectoryName (OutputFile); + if (!directory.IsNullOrEmpty ()) { + Directory.CreateDirectory (directory); + } + File.WriteAllText (OutputFile, content, Files.UTF8withoutBOM); + return !Log.HasLoggedErrors; + } + + void ScanLinkedAssemblies (R8Mapping mapping) + { + if (LinkedAssemblies == null) { + return; + } + + var seen = new HashSet (StringComparer.OrdinalIgnoreCase); + foreach (ITaskItem assembly in LinkedAssemblies) { + string path = assembly.ItemSpec; + if (!seen.Add (path) || !File.Exists (path)) { + continue; + } + + try { + using var stream = File.OpenRead (path); + using var peReader = new PEReader (stream); + if (!peReader.HasMetadata) { + continue; + } + JniRemappingAssemblyScanner.Scan (peReader, peReader.GetMetadataReader (), mapping, Log); + } catch (BadImageFormatException ex) { + Log.LogDebugMessage ($"Could not read assembly '{path}': {ex.Message}"); + } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { + LogR8JniRemappingError (string.Format (Properties.Resources.XA4327_AssemblyReadFailure, path, ex.Message)); + } + } + } + + string GenerateContent (R8Mapping mapping, HashSet? requiredEntries) + { + var allClassMappings = new List (mapping.EnumerateClassMappings ()); + var classMappings = new List (); + foreach (R8ClassMapping classMapping in allClassMappings) { + if (requiredEntries == null || requiredEntries.Contains (R8Mapping.BuildClassEntry (classMapping.OriginalJniName))) { + classMappings.Add (classMapping); + } + } + var classRenames = new Dictionary (StringComparer.Ordinal); + var requiredOriginalClasses = new Dictionary (StringComparer.Ordinal); + foreach (R8ClassMapping classMapping in allClassMappings) { + classRenames [classMapping.OriginalJniName] = classMapping.ObfuscatedJniName; + } + foreach (R8ClassMapping classMapping in classMappings) { + if (requiredOriginalClasses.TryGetValue (classMapping.ObfuscatedJniName, out string? existing) && + !string.Equals (existing, classMapping.OriginalJniName, StringComparison.Ordinal)) { + requiredOriginalClasses [classMapping.ObfuscatedJniName] = null; + } else if (!requiredOriginalClasses.ContainsKey (classMapping.ObfuscatedJniName)) { + requiredOriginalClasses [classMapping.ObfuscatedJniName] = classMapping.OriginalJniName; + } + } + string? RenameClass (string className) + => classRenames.TryGetValue (className, out string? renamed) ? renamed : null; + + var settings = new XmlWriterSettings { + Encoding = Files.UTF8withoutBOM, + Indent = true, + IndentChars = " ", + NewLineChars = "\n", + OmitXmlDeclaration = true, + }; + + var output = new StringBuilder (); + using (var writer = XmlWriter.Create (output, settings)) { + writer.WriteStartElement ("replacements"); + var skippedClasses = new HashSet (StringComparer.Ordinal); + foreach (R8ClassMapping classMapping in classMappings) { + if (!WriteClass (writer, classMapping, requiredOriginalClasses)) { + skippedClasses.Add (classMapping.OriginalJniName); + } + } + foreach (R8ClassMapping classMapping in classMappings) { + if (skippedClasses.Contains (classMapping.OriginalJniName)) { + continue; + } + foreach (R8FieldMapping field in classMapping.Fields) { + if (requiredEntries != null && + !requiredEntries.Contains (R8Mapping.BuildFieldEntry (classMapping.OriginalJniName, field.OriginalName))) { + continue; + } + WriteField (writer, classMapping, field, RenameClass); + } + foreach (R8MethodMapping method in classMapping.Methods) { + string methodKey = R8Mapping.BuildMethodKey (method.OriginalName, method.JavaParameterTypes, method.JavaReturnType); + if (requiredEntries != null && + !requiredEntries.Contains (R8Mapping.BuildMethodEntry (classMapping.OriginalJniName, methodKey))) { + continue; + } + WriteMethod (writer, classMapping, method, RenameClass); + } + } + writer.WriteEndElement (); + } + output.Append ('\n'); + return output.ToString (); + } + + bool WriteClass (XmlWriter writer, R8ClassMapping classMapping, Dictionary requiredOriginalClasses) + { + bool ownedExternally = externallyOwnedTypes.Contains (BuildTypeKey (classMapping.OriginalJniName)); + if (classMapping.IsRenamed) { + if (TryClaimEntry ("replace-type", BuildTypeKey (classMapping.OriginalJniName), classMapping.ObfuscatedJniName)) { + writer.WriteStartElement ("replace-type"); + writer.WriteAttributeString ("from", classMapping.OriginalJniName); + writer.WriteAttributeString ("to", classMapping.ObfuscatedJniName); + writer.WriteEndElement (); + } else { + ownedExternally = true; + } + } + + if (ownedExternally) { + return false; + } + if (!classMapping.IsRenamed || + !requiredOriginalClasses.TryGetValue (classMapping.ObfuscatedJniName, out string? originalJniName) || + !string.Equals (originalJniName, classMapping.OriginalJniName, StringComparison.Ordinal)) { + return true; + } + + if (TryClaimEntry ("reverse-type", BuildReverseTypeKey (classMapping.ObfuscatedJniName), classMapping.OriginalJniName)) { + writer.WriteStartElement ("reverse-type"); + writer.WriteAttributeString ("from", classMapping.ObfuscatedJniName); + writer.WriteAttributeString ("to", classMapping.OriginalJniName); + writer.WriteEndElement (); + } + return true; + } + + void WriteField (XmlWriter writer, R8ClassMapping classMapping, R8FieldMapping field, Func renameClass) + { + if (field.JavaFieldType.Length == 0) { + return; + } + + string sourceSignature; + try { + sourceSignature = JniDescriptorText.JavaSourceTypeToJniTypeToken (field.JavaFieldType); + } catch (ArgumentException) { + LogR8JniRemappingWarning (string.Format ( + Properties.Resources.XA4328_UnsupportedSignature, + $"{classMapping.OriginalJniName}.{field.OriginalName}", + field.JavaFieldType)); + return; + } + + JniDescriptorText.TryRewriteDescriptor (sourceSignature, renameClass, out string targetSignature); + if (!classMapping.IsRenamed && !field.IsRenamed && + string.Equals (sourceSignature, targetSignature, StringComparison.Ordinal)) { + return; + } + + if (!TryClaimEntry ( + "replace-field", + BuildFieldKey (classMapping.ObfuscatedJniName, field.OriginalName, sourceSignature), + $"{classMapping.ObfuscatedJniName}\t{field.ObfuscatedName}\t{targetSignature}")) { + return; + } + + writer.WriteStartElement ("replace-field"); + writer.WriteAttributeString ("source-type", classMapping.ObfuscatedJniName); + writer.WriteAttributeString ("source-field-name", field.OriginalName); + writer.WriteAttributeString ("source-field-signature", sourceSignature); + writer.WriteAttributeString ("target-type", classMapping.ObfuscatedJniName); + writer.WriteAttributeString ("target-field-name", field.ObfuscatedName); + writer.WriteAttributeString ("target-field-signature", targetSignature); + writer.WriteEndElement (); + } + + void WriteMethod (XmlWriter writer, R8ClassMapping classMapping, R8MethodMapping method, Func renameClass) + { + string sourceSignature; + try { + sourceSignature = JniDescriptorText.JavaSourceTypesToMethodDescriptor (method.JavaParameterTypes, method.JavaReturnType); + } catch (ArgumentException) { + LogR8JniRemappingWarning (string.Format ( + Properties.Resources.XA4328_UnsupportedSignature, + $"{classMapping.OriginalJniName}.{method.OriginalName}", + R8Mapping.BuildMethodKey (method.OriginalName, method.JavaParameterTypes, method.JavaReturnType))); + return; + } + + JniDescriptorText.TryRewriteDescriptor (sourceSignature, renameClass, out string targetSignature); + if (!classMapping.IsRenamed && !method.IsRenamed && + string.Equals (sourceSignature, targetSignature, StringComparison.Ordinal)) { + return; + } + + if (!TryClaimEntry ( + "replace-method", + BuildMethodKey (classMapping.ObfuscatedJniName, method.OriginalName, sourceSignature), + $"{classMapping.ObfuscatedJniName}\t{method.ObfuscatedName}\t{targetSignature}")) { + return; + } + + writer.WriteStartElement ("replace-method"); + writer.WriteAttributeString ("source-type", classMapping.ObfuscatedJniName); + writer.WriteAttributeString ("source-method-name", method.OriginalName); + writer.WriteAttributeString ("source-method-signature", sourceSignature); + writer.WriteAttributeString ("target-type", classMapping.ObfuscatedJniName); + writer.WriteAttributeString ("target-method-name", method.ObfuscatedName); + writer.WriteAttributeString ("target-method-signature", targetSignature); + writer.WriteAttributeString ("target-method-instance-to-static", "false"); + writer.WriteEndElement (); + } + + bool TryClaimEntry (string elementName, string key, string target) + { + if (!existingEntries.TryGetValue (key, out string? existingTarget)) { + existingEntries [key] = target; + return true; + } + + if (string.Equals (existingTarget, target, StringComparison.Ordinal)) { + Log.LogDebugMessage ($"Skipping duplicate `{elementName}` entry for `{key.Replace ('\t', ' ')}`."); + return false; + } + + if (!preexistingEntryKeys.Contains (key)) { + LogR8JniRemappingError (string.Format ( + Properties.Resources.XA4327_AmbiguousEntry, + elementName, + key.Replace ('\t', ' '), + existingTarget.Replace ('\t', ' '), + target.Replace ('\t', ' '))); + return false; + } + + LogR8JniRemappingWarning (string.Format ( + Properties.Resources.XA4328_ConflictingEntry, + elementName, + key.Replace ('\t', ' '), + existingTarget.Replace ('\t', ' '), + target.Replace ('\t', ' '))); + return false; + } + + void ReadExistingEntries () + { + if (ExistingRemapXmlFiles == null) { + return; + } + + var readerSettings = new XmlReaderSettings { + XmlResolver = null, + }; + foreach (ITaskItem item in ExistingRemapXmlFiles) { + string file = item.ItemSpec; + if (string.Equals (Path.GetFullPath (file), Path.GetFullPath (OutputFile), StringComparison.OrdinalIgnoreCase)) { + continue; + } + if (!File.Exists (file)) { + Log.LogDebugMessage ($"Existing remapping input `{file}` does not exist yet."); + continue; + } + + try { + using var stream = File.OpenRead (file); + using var reader = XmlReader.Create (stream, readerSettings); + ReadExistingEntries (reader); + } catch (Exception ex) when (ex is XmlException || ex is IOException || ex is UnauthorizedAccessException) { + Log.LogDebugMessage ($"Existing remapping input `{file}` could not be read: {ex.Message}"); + } + } + } + + void ReadExistingEntries (XmlReader reader) + { + while (reader.Read ()) { + if (reader.NodeType != XmlNodeType.Element) { + continue; + } + + switch (reader.LocalName) { + case "replace-type": + AddExistingEntry (BuildTypeKey (reader.GetAttribute ("from")), reader.GetAttribute ("to"), externallyOwnedType: true); + break; + case "reverse-type": + AddExistingEntry (BuildReverseTypeKey (reader.GetAttribute ("from")), reader.GetAttribute ("to")); + break; + case "replace-field": + AddExistingEntry ( + BuildFieldKey (reader.GetAttribute ("source-type"), reader.GetAttribute ("source-field-name"), reader.GetAttribute ("source-field-signature")), + $"{reader.GetAttribute ("target-type")}\t{reader.GetAttribute ("target-field-name")}\t{reader.GetAttribute ("target-field-signature")}"); + break; + case "replace-method": + AddExistingEntry ( + BuildMethodKey (reader.GetAttribute ("source-type"), reader.GetAttribute ("source-method-name"), reader.GetAttribute ("source-method-signature")), + $"{reader.GetAttribute ("target-type")}\t{reader.GetAttribute ("target-method-name")}\t{reader.GetAttribute ("target-method-signature")}"); + break; + } + } + } + + void AddExistingEntry (string key, string? target, bool externallyOwnedType = false) + { + if (key.Length == 0) { + return; + } + existingEntries [key] = target ?? ""; + preexistingEntryKeys.Add (key); + if (externallyOwnedType) { + externallyOwnedTypes.Add (key); + } + } + + static string BuildTypeKey (string? from) => from.IsNullOrEmpty () ? "" : $"T\t{from}"; + static string BuildReverseTypeKey (string? from) => from.IsNullOrEmpty () ? "" : $"R\t{from}"; + static string BuildFieldKey (string? sourceType, string? fieldName, string? signature) + => sourceType.IsNullOrEmpty () || fieldName.IsNullOrEmpty () ? "" : $"F\t{sourceType}\t{fieldName}\t{signature}"; + static string BuildMethodKey (string? sourceType, string? methodName, string? signature) + => sourceType.IsNullOrEmpty () || methodName.IsNullOrEmpty () ? "" : $"M\t{sourceType}\t{methodName}\t{signature}"; + + void LogR8JniRemappingError (string detail) + => Log.LogCodedError ("XA4327", Properties.Resources.XA4327, detail); + + void LogR8JniRemappingWarning (string detail) + => Log.LogCodedWarning ("XA4328", Properties.Resources.XA4328, detail); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/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..a5d50f25052 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs @@ -0,0 +1,282 @@ +#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 @.JniRemappingString.4_str", + "ptr null", + "ptr @.JniRemappingString.5_str"); + AssertOrdered (ll, "c\"af", "c\"zf"); + Assert.AreEqual (1, Info.ReplacementMethodIndexEntryCount); + Assert.AreEqual (1, Info.ReplacementFieldIndexEntryCount); + } + + [Test] + public void MissingFieldSignaturesAreBackwardCompatible () + { + string ll = RunTask ( + """ + + + + """); + + Assert.AreEqual (1, Info.ReplacementFieldIndexEntryCount); + int fieldsStart = ll.IndexOf ("@mf_0 =", System.StringComparison.Ordinal); + int fieldsEnd = ll.IndexOf ("@jni_remapping_field_replacement_index", fieldsStart, System.StringComparison.Ordinal); + Assert.Greater (fieldsStart, -1); + Assert.Greater (fieldsEnd, fieldsStart); + string fieldArray = ll.Substring (fieldsStart, fieldsEnd - fieldsStart); + StringAssert.IsMatch (@"i32 0,\s+ptr @\.JniRemappingString\.\d+_str", fieldArray, + "An absent source-field-signature must be emitted as a zero-length lookup string."); + StringAssert.Contains ("ptr null", fieldArray, + "An absent target-field-signature must remain null so the runtime uses the source signature."); + } + + [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 (JniRemappingNativeCodeGenerator.CompareUtf8 (Utf8 ("Z"), Utf8 ("_")), 0); + Assert.Less (JniRemappingNativeCodeGenerator.CompareUtf8 (Utf8 ("_"), Utf8 ("a")), 0); + Assert.Less (JniRemappingNativeCodeGenerator.CompareUtf8 (Utf8 ("a"), Utf8 ("ab")), 0); + Assert.AreEqual (0, JniRemappingNativeCodeGenerator.CompareUtf8 (Utf8 ("a/B"), Utf8 ("a/B"))); + + static byte [] Utf8 (string s) => System.Text.Encoding.UTF8.GetBytes (s); + } + + static void AssertOrdered (string haystack, params string [] needles) + { + int previous = -1; + string previousNeedle = ""; + foreach (string needle in needles) { + int index = haystack.IndexOf (needle, previous + 1, System.StringComparison.Ordinal); + Assert.Greater (index, previous, $"`{needle}` must appear after `{previousNeedle}`."); + previous = index; + previousNeedle = needle; + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs new file mode 100644 index 00000000000..3d90758147c --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateR8JniRemappingTests.cs @@ -0,0 +1,232 @@ +#nullable enable + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using NUnit.Framework; + +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests.Tasks +{ + [TestFixture] + public class GenerateR8JniRemappingTests : BaseTest + { + List? errors; + List? warnings; + MockBuildEngine? engine; + string? directory; + + [SetUp] + public void Setup () + { + errors = new List (); + warnings = new List (); + engine = new MockBuildEngine (TestContext.Out, errors, warnings); + directory = Path.Combine (Root, "temp", TestName); + if (Directory.Exists (directory)) { + Directory.Delete (directory, recursive: true); + } + Directory.CreateDirectory (directory); + } + + string TestDirectory => directory ?? throw new AssertionException ("Test directory was not initialized."); + List Errors => errors ?? throw new AssertionException ("Error list was not initialized."); + List Warnings => warnings ?? throw new AssertionException ("Warning list was not initialized."); + + string Run (string mapping, string? nativeObject = null) + { + string mappingFile = Path.Combine (TestDirectory, "mapping.txt"); + string outputFile = Path.Combine (TestDirectory, "r8-remap.xml"); + File.WriteAllText (mappingFile, mapping); + var task = new GenerateR8JniRemapping { + BuildEngine = engine, + MappingFile = mappingFile, + OutputFile = outputFile, + NativeAot = nativeObject != null, + NativeAotObjectFile = nativeObject, + }; + + Assert.IsTrue (task.Execute (), string.Join ("; ", Errors.Select (error => error.Message))); + return File.ReadAllText (outputFile); + } + + [Test] + public void GeneratesRuntimeLookupEntries () + { + string xml = Run (""" + com.contoso.Peer -> a.b: + com.contoso.Peer run(com.contoso.Peer[]) -> c + com.contoso.Peer[] peers -> d + + """); + + StringAssert.Contains ("""""", xml); + StringAssert.Contains ("""""", xml); + StringAssert.Contains ("source-method-signature=\"([Lcom/contoso/Peer;)Lcom/contoso/Peer;\"", xml); + StringAssert.Contains ("target-method-signature=\"([La/b;)La/b;\"", xml); + StringAssert.Contains ("source-field-signature=\"[Lcom/contoso/Peer;\"", xml); + StringAssert.Contains ("target-field-signature=\"[La/b;\"", xml); + } + + [Test] + public void SkipsAmbiguousReverseTypeForMergedClasses () + { + string xml = Run (""" + com.contoso.First -> a.b: + com.contoso.Second -> a.b: + + """); + + StringAssert.DoesNotContain ("reverse-type", xml); + } + + [TestCase ("method")] + [TestCase ("field")] + public void ConflictingMergedMembersFailInsteadOfChoosingOne (string memberKind) + { + string memberMappings = memberKind == "method" + ? " void run(int) -> c\ncom.contoso.Second -> a.b:\n void run(int) -> d\n" + : " int value -> c\ncom.contoso.Second -> a.b:\n int value -> d\n"; + string mappingFile = Path.Combine (TestDirectory, "mapping.txt"); + string outputFile = Path.Combine (TestDirectory, "output.xml"); + File.WriteAllText (mappingFile, "com.contoso.First -> a.b:\n" + memberMappings); + var task = new GenerateR8JniRemapping { + BuildEngine = engine, + MappingFile = mappingFile, + OutputFile = outputFile, + }; + + Assert.IsFalse (task.Execute ()); + Assert.That (Errors, Has.Some.Property ("Code").EqualTo ("XA4327")); + FileAssert.DoesNotExist (outputFile); + } + + [Test] + public void NativeAotRetentionFiltersUnusedEntries () + { + string objectFile = WriteNativeObject (["com/contoso/Peer", "run.(I)V"]); + string xml = Run (""" + com.contoso.Peer -> a.b: + void run(int) -> c + void removed() -> d + com.contoso.Unused -> a.e: + + """, objectFile); + + StringAssert.Contains ("source-method-name=\"run\"", xml); + StringAssert.DoesNotContain ("removed", xml); + StringAssert.DoesNotContain ("Unused", xml); + } + + [Test] + public void RetentionMakesMergedReverseTypeUnambiguous () + { + string objectFile = WriteNativeObject (["com/contoso/First"]); + string xml = Run (""" + com.contoso.First -> a.b: + com.contoso.Second -> a.b: + + """, objectFile); + + StringAssert.Contains ("""""", xml); + StringAssert.DoesNotContain ("com/contoso/Second", xml); + } + + [Test] + public void MalformedMappingReportsXA4327 () + { + string mappingFile = Path.Combine (TestDirectory, "mapping.txt"); + File.WriteAllText (mappingFile, " void run() -> a\n"); + var task = new GenerateR8JniRemapping { + BuildEngine = engine, + MappingFile = mappingFile, + OutputFile = Path.Combine (TestDirectory, "output.xml"), + }; + + Assert.IsFalse (task.Execute ()); + Assert.AreEqual ("XA4327", Errors.Single ().Code); + } + + [Test] + public void UnsupportedMethodSignatureWarningIncludesReturnType () + { + Run (""" + com.contoso.Peer -> a.b: + void run( ) -> c + + """); + + Assert.AreEqual ("XA4328", Warnings.Single ().Code); + StringAssert.Contains ("run( ):void", Warnings [0].Message); + } + + string WriteNativeObject (string [] literals) + { + byte [] Encode () + { + using var data = new MemoryStream (); + foreach (string value in literals) { + data.Write (Encoding.Unicode.GetBytes (value)); + data.WriteByte (0xFF); + data.WriteByte (0xFF); + } + return data.ToArray (); + } + + var sections = new [] { + (Name: "", Flags: 0UL, Type: 0U, Bytes: new byte [0]), + (Name: ".shstrtab", Flags: 0UL, Type: 3U, Bytes: new byte [0]), + (Name: "__managedcode", Flags: 6UL, Type: 1U, Bytes: new byte [] { 0xC0, 0x03, 0x5F, 0xD6 }), + (Name: ".rodata", Flags: 2UL, Type: 1U, Bytes: Encode ()), + }; + sections [1].Bytes = Encoding.UTF8.GetBytes (string.Join ("\0", sections.Select (section => section.Name)) + "\0"); + var offsets = new long [sections.Length]; + using var image = new MemoryStream (); + using var writer = new BinaryWriter (image); + writer.Write (new byte [] { 0x7F, (byte) 'E', (byte) 'L', (byte) 'F', 2, 1, 1, 0 }); + writer.Write (0UL); + writer.Write ((ushort) 1); + writer.Write ((ushort) 183); + writer.Write (1U); + writer.Write (0UL); + writer.Write (0UL); + writer.Write (0UL); + writer.Write (0U); + writer.Write ((ushort) 64); + writer.Write ((ushort) 0); + writer.Write ((ushort) 0); + writer.Write ((ushort) 64); + writer.Write ((ushort) sections.Length); + writer.Write ((ushort) 1); + for (int i = 1; i < sections.Length; i++) { + offsets [i] = image.Position; + writer.Write (sections [i].Bytes); + } + long sectionHeaders = image.Position; + int nameIndex = 0; + for (int i = 0; i < sections.Length; i++) { + writer.Write (nameIndex); + writer.Write (sections [i].Type); + writer.Write (sections [i].Flags); + writer.Write (0UL); + writer.Write ((ulong) offsets [i]); + writer.Write ((ulong) sections [i].Bytes.Length); + writer.Write (0U); + writer.Write (0U); + writer.Write (i == 0 ? 0UL : 1UL); + writer.Write (0UL); + nameIndex += Encoding.UTF8.GetByteCount (sections [i].Name) + 1; + } + image.Position = 40; + writer.Write ((ulong) sectionHeaders); + string path = Path.Combine (TestDirectory, "app.o"); + File.WriteAllBytes (path, image.ToArray ()); + return path; + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniDescriptorTextTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniDescriptorTextTests.cs new file mode 100644 index 00000000000..867ea4449a9 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniDescriptorTextTests.cs @@ -0,0 +1,100 @@ +using System; +using NUnit.Framework; +using Xamarin.Android.Tasks.JniRemapping; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + [Parallelizable (ParallelScope.Children)] + public class JniDescriptorTextTests : BaseTest + { + static string? Rename (string cls) => cls == "acme/orig/MyView" ? "a/b/C" : null; + + [Test] + public void RewritesObjectParameterAndReturnTypes () + { + bool changed = JniDescriptorText.TryRewriteDescriptor ("(Lacme/orig/MyView;I)Lacme/orig/MyView;", Rename, out string rewritten); + + Assert.IsTrue (changed); + Assert.AreEqual ("(La/b/C;I)La/b/C;", rewritten); + } + + [Test] + public void RewritesArrayOfObjectType () + { + bool changed = JniDescriptorText.TryRewriteDescriptor ("[Lacme/orig/MyView;", Rename, out string rewritten); + + Assert.IsTrue (changed); + Assert.AreEqual ("[La/b/C;", rewritten); + } + + [Test] + public void LeavesUnrelatedTypesAndPrimitivesAlone () + { + bool changed = JniDescriptorText.TryRewriteDescriptor ("(Landroid/view/View;[I)V", Rename, out string rewritten); + + Assert.IsFalse (changed); + Assert.AreEqual ("(Landroid/view/View;[I)V", rewritten); + } + + [TestCase ("()V", true)] + [TestCase ("(Ljava/lang/Object;)Z", true)] + [TestCase ("(I)I", true)] + [TestCase ("(V)V", false)] + [TestCase ("()[V", false)] + [TestCase ("(L;)V", false)] + [TestCase ("(Ljava.lang.Object;)V", false)] + [TestCase ("(Lfoo[Bar;)V", false)] + [TestCase ("(Lfoo//Bar;)V", false)] + [TestCase ("I", false)] + [TestCase ("Ljava/lang/Object;", false)] + [TestCase ("not a descriptor", false)] + public void ValidatesMethodDescriptors (string descriptor, bool expected) + { + Assert.AreEqual (expected, JniDescriptorText.IsValidMethodDescriptor (descriptor)); + } + + [TestCase ("I", true)] + [TestCase ("[I", true)] + [TestCase ("Ljava/lang/Object;", true)] + [TestCase ("V", false)] + [TestCase ("[V", false)] + [TestCase ("L;", false)] + [TestCase ("[L;", false)] + [TestCase ("Ljava.lang.Object;", false)] + [TestCase ("Lfoo[Bar;", false)] + [TestCase ("Lfoo//Bar;", false)] + [TestCase ("()V", false)] + [TestCase ("", false)] + public void ValidatesFieldDescriptors (string descriptor, bool expected) + { + Assert.AreEqual (expected, JniDescriptorText.IsValidFieldDescriptor (descriptor)); + } + + [Test] + public void ConvertsMethodDescriptorToJavaParameterTypes () + { + var parameters = JniDescriptorText.MethodDescriptorToJavaParameterTypes ("(Landroid/os/Bundle;I[Ljava/lang/String;)V"); + + CollectionAssert.AreEqual (new [] { "android.os.Bundle", "int", "java.lang.String[]" }, parameters); + } + + [Test] + public void ConvertsSingleTypeTokenToJavaSource () + { + Assert.AreEqual ("boolean", JniDescriptorText.JniTypeTokenToJavaSource ("Z")); + Assert.AreEqual ("int[]", JniDescriptorText.JniTypeTokenToJavaSource ("[I")); + Assert.AreEqual ("java.lang.Object", JniDescriptorText.JniTypeTokenToJavaSource ("Ljava/lang/Object;")); + } + + [TestCase ("")] + [TestCase ("[")] + [TestCase ("L;")] + [TestCase ("Ljava.lang.Object;")] + [TestCase ("Lfoo[Bar;")] + public void RejectsMalformedSingleTypeToken (string token) + { + Assert.Throws (() => JniDescriptorText.JniTypeTokenToJavaSource (token)); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniRemappingAssemblyScannerTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniRemappingAssemblyScannerTests.cs new file mode 100644 index 00000000000..7009d3e9651 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniRemappingAssemblyScannerTests.cs @@ -0,0 +1,167 @@ +#nullable enable + +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +using Microsoft.Build.Utilities; +using Cecil = Mono.Cecil; +using Mono.Cecil.Cil; +using NUnit.Framework; + +using Xamarin.Android.Tasks; +using Xamarin.Android.Tasks.JniRemapping; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + public class JniRemappingAssemblyScannerTests : BaseTest + { + [Test] + public void RecordsOnlyMappingsReferencedBySurvivingMetadata () + { + string path = Path.Combine (Root, "temp", TestName, "Linked.dll"); + Directory.CreateDirectory (Path.Combine (Root, "temp", TestName)); + CreateFixture (path); + + R8Mapping mapping = R8Mapping.Parse (new StringReader (""" + com.contoso.Peer -> a.b: + void onClick() -> c + int value -> d + com.contoso.Unused -> a.e: + void unused() -> f + + """)); + var engine = new MockBuildEngine (TestContext.Out); + var task = new GenerateR8JniRemapping { + BuildEngine = engine, + }; + + using var stream = File.OpenRead (path); + using var peReader = new PEReader (stream); + var reader = peReader.GetMetadataReader (); + JniRemappingAssemblyScanner.Scan (peReader, reader, mapping, new TaskLoggingHelper (task)); + + CollectionAssert.AreEquivalent (new [] { + "C\tcom/contoso/Peer", + "M\tcom/contoso/Peer\tonClick():void", + "F\tcom/contoso/Peer\tvalue", + }, mapping.AccessedEntries); + } + + [Test] + public void JavaPeerProxyRetainsItsGeneratedMappings () + { + string directory = Path.Combine (Root, "temp", TestName); + string path = Path.Combine (directory, "TypeMap.dll"); + Directory.CreateDirectory (directory); + CreateProxyFixture (path); + + R8Mapping mapping = R8Mapping.Parse (new StringReader (""" + com.contoso.ProxyPeer -> a.b: + void callback() -> c + int value -> d + + """)); + var task = new GenerateR8JniRemapping { + BuildEngine = new MockBuildEngine (TestContext.Out), + }; + + using var stream = File.OpenRead (path); + using var peReader = new PEReader (stream); + JniRemappingAssemblyScanner.Scan (peReader, peReader.GetMetadataReader (), mapping, new TaskLoggingHelper (task)); + + CollectionAssert.AreEquivalent (new [] { + "C\tcom/contoso/ProxyPeer", + "M\tcom/contoso/ProxyPeer\tcallback():void", + "F\tcom/contoso/ProxyPeer\tvalue", + }, mapping.AccessedEntries); + } + + static void CreateFixture (string path) + { + using var assembly = Cecil.AssemblyDefinition.CreateAssembly ( + new Cecil.AssemblyNameDefinition ("Linked", new System.Version (1, 0)), + "Linked", + Cecil.ModuleKind.Dll); + Cecil.ModuleDefinition module = assembly.MainModule; + Cecil.TypeReference attributeType = module.ImportReference (typeof (System.Attribute)); + + Cecil.TypeDefinition registerAttribute = AddAttribute (module, attributeType, "Android.Runtime", "RegisterAttribute", 3); + Cecil.MethodReference registerCtor1 = registerAttribute.Methods [0]; + Cecil.MethodReference registerCtor3 = registerAttribute.Methods [1]; + + var peer = new Cecil.TypeDefinition ("Com.Contoso", "Peer", Cecil.TypeAttributes.Public | Cecil.TypeAttributes.Class, module.TypeSystem.Object); + peer.CustomAttributes.Add (Attribute (registerCtor1, "com/contoso/Peer")); + module.Types.Add (peer); + + var method = new Cecil.MethodDefinition ("OnClick", Cecil.MethodAttributes.Public, module.TypeSystem.Void); + method.Body.Instructions.Add (Instruction.Create (OpCodes.Ret)); + method.CustomAttributes.Add (Attribute (registerCtor3, "onClick", "()V", "n_OnClick")); + peer.Methods.Add (method); + + var field = new Cecil.FieldDefinition ("Value", Cecil.FieldAttributes.Public, module.TypeSystem.Int32); + field.CustomAttributes.Add (Attribute (registerCtor1, "value")); + peer.Fields.Add (field); + + assembly.Write (path); + } + + static void CreateProxyFixture (string path) + { + using var assembly = Cecil.AssemblyDefinition.CreateAssembly ( + new Cecil.AssemblyNameDefinition ("TypeMap", new System.Version (1, 0)), + "TypeMap", + Cecil.ModuleKind.Dll); + Cecil.ModuleDefinition module = assembly.MainModule; + var proxyBase = new Cecil.TypeReference ("Java.Interop", "JavaPeerProxy", module, module.TypeSystem.CoreLibrary); + var proxy = new Cecil.TypeDefinition ("Generated", "ProxyPeer", + Cecil.TypeAttributes.Public | Cecil.TypeAttributes.Sealed | Cecil.TypeAttributes.Class, + proxyBase); + module.Types.Add (proxy); + + var constructor = new Cecil.MethodDefinition (".ctor", + Cecil.MethodAttributes.Public | Cecil.MethodAttributes.SpecialName | Cecil.MethodAttributes.RTSpecialName, + module.TypeSystem.Void); + var il = constructor.Body.GetILProcessor (); + il.Append (Instruction.Create (OpCodes.Ldstr, "com/contoso/ProxyPeer")); + il.Append (Instruction.Create (OpCodes.Pop)); + il.Append (Instruction.Create (OpCodes.Ret)); + proxy.Methods.Add (constructor); + assembly.Write (path); + } + + static Cecil.TypeDefinition AddAttribute (Cecil.ModuleDefinition module, Cecil.TypeReference attributeType, string ns, string name, int maxArguments) + { + var type = new Cecil.TypeDefinition (ns, name, Cecil.TypeAttributes.Public | Cecil.TypeAttributes.Class, attributeType); + module.Types.Add (type); + for (int argumentCount = 1; argumentCount <= maxArguments; argumentCount += 2) { + var constructor = new Cecil.MethodDefinition (".ctor", + Cecil.MethodAttributes.Public | Cecil.MethodAttributes.SpecialName | Cecil.MethodAttributes.RTSpecialName, + module.TypeSystem.Void); + for (int i = 0; i < argumentCount; i++) { + constructor.Parameters.Add (new Cecil.ParameterDefinition (module.TypeSystem.String)); + } + var il = constructor.Body.GetILProcessor (); + il.Append (Instruction.Create (OpCodes.Ldarg_0)); + il.Append (Instruction.Create (OpCodes.Call, module.ImportReference (typeof (System.Attribute).GetConstructor ( + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic, + null, + System.Type.EmptyTypes, + null)))); + il.Append (Instruction.Create (OpCodes.Ret)); + type.Methods.Add (constructor); + } + return type; + } + + static Cecil.CustomAttribute Attribute (Cecil.MethodReference constructor, params string [] values) + { + var attribute = new Cecil.CustomAttribute (constructor); + foreach (string value in values) { + attribute.ConstructorArguments.Add (new Cecil.CustomAttributeArgument (constructor.Module.TypeSystem.String, value)); + } + return attribute; + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/R8MappingTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/R8MappingTests.cs index 08407ee1026..ca6ae47fc00 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/R8MappingTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/R8MappingTests.cs @@ -617,5 +617,43 @@ java.lang.String value() -> b Assert.IsTrue (mapping.TryGetRenamedMethod ("acme/orig/MyView", "value", [], "java.lang.String", out string stringMethod)); Assert.AreEqual ("b", stringMethod); } + + [Test] + public void EnumeratesFieldTypesAndMembersDeterministically () + { + R8Mapping mapping = R8Mapping.Parse (new StringReader (""" + com.contoso.Zebra -> a.z: + java.lang.String[] values -> b + void run(int) -> c + com.contoso.Apple -> a.a: + int count -> d + + """)); + + var classes = new System.Collections.Generic.List (mapping.EnumerateClassMappings ()); + + Assert.AreEqual (2, classes.Count); + Assert.AreEqual ("com/contoso/Apple", classes [0].OriginalJniName); + Assert.AreEqual ("int", classes [0].Fields [0].JavaFieldType); + Assert.AreEqual ("com/contoso/Zebra", classes [1].OriginalJniName); + Assert.AreEqual ("java.lang.String[]", classes [1].Fields [0].JavaFieldType); + Assert.AreEqual ("run", classes [1].Methods [0].OriginalName); + CollectionAssert.AreEqual (new [] { "int" }, classes [1].Methods [0].JavaParameterTypes); + } + + [TestCase ("run(int):void", true)] + [TestCase ("run():java.lang.String", true)] + [TestCase ("missing", false)] + public void SplitsMethodKeys (string key, bool expected) + { + bool result = R8Mapping.TrySplitMethodKey (key, out string name, out string [] parameters, out string returnType); + + Assert.AreEqual (expected, result); + if (expected) { + Assert.AreEqual ("run", name); + Assert.That (returnType, Is.Not.Empty); + Assert.That (parameters, Is.Not.Null); + } + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniDescriptorText.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniDescriptorText.cs new file mode 100644 index 00000000000..2fe31f009f1 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniDescriptorText.cs @@ -0,0 +1,282 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Xamarin.Android.Tasks.JniRemapping +{ + static class JniDescriptorText + { + public static bool TryRewriteDescriptor (string descriptor, Func renameClass, out string rewritten) + { + var sb = new StringBuilder (descriptor.Length); + bool changed = false; + int i = 0; + while (i < descriptor.Length) { + int start = i; + if (!TryScanSingleToken (descriptor, ref i, allowVoid: true)) { + sb.Append (descriptor [start]); + i = start + 1; + continue; + } + + string token = descriptor.Substring (start, i - start); + if (TryRewriteSingleTypeToken (token, renameClass, out string newToken)) { + changed = true; + sb.Append (newToken); + } else { + sb.Append (token); + } + } + + rewritten = changed ? sb.ToString () : descriptor; + return changed; + } + + static bool TryRewriteSingleTypeToken (string token, Func renameClass, out string rewritten) + { + rewritten = token; + int arrayDepth = 0; + while (arrayDepth < token.Length && token [arrayDepth] == '[') { + arrayDepth++; + } + + if (arrayDepth >= token.Length || token [arrayDepth] != 'L') { + return false; + } + + string className = token.Substring (arrayDepth + 1, token.Length - arrayDepth - 2); + string? renamed = renameClass (className); + if (renamed == null || renamed == className) { + return false; + } + + rewritten = token.Substring (0, arrayDepth) + "L" + renamed + ";"; + return true; + } + + static bool TryScanSingleToken (string s, ref int i, bool allowVoid) + { + int start = i; + int j = i; + while (j < s.Length && s [j] == '[') { + j++; + } + + if (j >= s.Length) { + return false; + } + + switch (s [j]) { + case 'V': + if (!allowVoid || j != start) { + return false; + } + i = j + 1; + return true; + case 'Z': + case 'B': + case 'C': + case 'S': + case 'I': + case 'J': + case 'F': + case 'D': + i = j + 1; + return true; + case 'L': + int end = s.IndexOf (';', j + 1); + if (end < 0 || !IsValidJniClassName (s, j + 1, end)) { + return false; + } + i = end + 1; + return true; + default: + return false; + } + } + + static bool IsValidJniClassName (string value, int start, int end) + { + if (start == end) { + return false; + } + + bool segmentHasCharacters = false; + for (int i = start; i < end; i++) { + switch (value [i]) { + case '/': + if (!segmentHasCharacters) { + return false; + } + segmentHasCharacters = false; + break; + case '.': + case '[': + return false; + default: + segmentHasCharacters = true; + break; + } + } + return segmentHasCharacters; + } + + public static bool TryParseMethodDescriptor (string descriptor, out List parameterTypes, out string returnType) + { + parameterTypes = new List (); + returnType = ""; + + if (descriptor.Length == 0 || descriptor [0] != '(') { + return false; + } + + int i = 1; + while (i < descriptor.Length && descriptor [i] != ')') { + int start = i; + if (!TryScanSingleToken (descriptor, ref i, allowVoid: false)) { + return false; + } + parameterTypes.Add (descriptor.Substring (start, i - start)); + } + + if (i >= descriptor.Length || descriptor [i] != ')') { + return false; + } + i++; + + int retStart = i; + if (!TryScanSingleToken (descriptor, ref i, allowVoid: true) || i != descriptor.Length) { + return false; + } + + returnType = descriptor.Substring (retStart); + return true; + } + + public static bool IsValidMethodDescriptor (string descriptor) + => TryParseMethodDescriptor (descriptor, out _, out _); + + public static bool IsValidFieldDescriptor (string descriptor) + { + int i = 0; + return descriptor.Length > 0 && TryScanSingleToken (descriptor, ref i, allowVoid: false) && i == descriptor.Length; + } + + public static string JniTypeTokenToJavaSource (string token) + { + int tokenEnd = 0; + if (!TryScanSingleToken (token, ref tokenEnd, allowVoid: true) || tokenEnd != token.Length) { + throw new ArgumentException ($"Malformed JNI type token '{token}'.", nameof (token)); + } + + int arrayDepth = 0; + while (arrayDepth < token.Length && token [arrayDepth] == '[') { + arrayDepth++; + } + + return JniTypeTokenToJavaSource (token, arrayDepth); + } + + static string JniTypeTokenToJavaSource (string token, int arrayDepth) + { + string elementJavaName = token [arrayDepth] switch { + 'V' => "void", + 'Z' => "boolean", + 'B' => "byte", + 'C' => "char", + 'S' => "short", + 'I' => "int", + 'J' => "long", + 'F' => "float", + 'D' => "double", + 'L' => token.Substring (arrayDepth + 1, token.Length - arrayDepth - 2).Replace ('/', '.'), + _ => throw new ArgumentException ($"Malformed JNI type token '{token}'.", nameof (token)), + }; + + if (arrayDepth == 0) { + return elementJavaName; + } + + var result = new StringBuilder (elementJavaName.Length + arrayDepth * 2); + result.Append (elementJavaName); + for (int i = 0; i < arrayDepth; i++) { + result.Append ("[]"); + } + return result.ToString (); + } + + public static List MethodDescriptorToJavaParameterTypes (string descriptor) + { + MethodDescriptorToJavaTypes (descriptor, out var parameterTypes, out _); + return parameterTypes; + } + + public static void MethodDescriptorToJavaTypes (string descriptor, out List parameterTypes, out string returnType) + { + if (!TryParseMethodDescriptor (descriptor, out var jniParameterTypes, out string jniReturnType)) { + throw new ArgumentException ($"Malformed JNI method descriptor '{descriptor}'.", nameof (descriptor)); + } + + parameterTypes = new List (jniParameterTypes.Count); + foreach (string parameterType in jniParameterTypes) { + int arrayDepth = 0; + while (parameterType [arrayDepth] == '[') { + arrayDepth++; + } + parameterTypes.Add (JniTypeTokenToJavaSource (parameterType, arrayDepth)); + } + int returnArrayDepth = 0; + while (jniReturnType [returnArrayDepth] == '[') { + returnArrayDepth++; + } + returnType = JniTypeTokenToJavaSource (jniReturnType, returnArrayDepth); + } + + public static string JavaSourceTypeToJniTypeToken (string javaSourceType) + { + string trimmed = javaSourceType.Trim (); + int arrayDepth = 0; + int elementEnd = trimmed.Length; + while (elementEnd >= 2 && + trimmed [elementEnd - 1] == ']' && + trimmed [elementEnd - 2] == '[') { + arrayDepth++; + elementEnd -= 2; + } + + string elementType = trimmed.Substring (0, elementEnd).Trim (); + if (elementType.Length == 0) { + throw new ArgumentException ($"Malformed Java source type '{javaSourceType}'.", nameof (javaSourceType)); + } + + string elementToken = elementType switch { + "void" => "V", + "boolean" => "Z", + "byte" => "B", + "char" => "C", + "short" => "S", + "int" => "I", + "long" => "J", + "float" => "F", + "double" => "D", + _ => "L" + elementType.Replace ('.', '/') + ";", + }; + + return arrayDepth == 0 ? elementToken : new string ('[', arrayDepth) + elementToken; + } + + public static string JavaSourceTypesToMethodDescriptor (IReadOnlyList javaParameterTypes, string javaReturnType) + { + var descriptor = new StringBuilder (); + descriptor.Append ('('); + foreach (string javaParameterType in javaParameterTypes) { + descriptor.Append (JavaSourceTypeToJniTypeToken (javaParameterType)); + } + descriptor.Append (')'); + descriptor.Append (JavaSourceTypeToJniTypeToken (javaReturnType)); + return descriptor.ToString (); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniRemappingAssemblyScanner.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniRemappingAssemblyScanner.cs new file mode 100644 index 00000000000..18972fc8488 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/JniRemappingAssemblyScanner.cs @@ -0,0 +1,229 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +using Microsoft.Build.Utilities; + +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Tasks.JniRemapping +{ + /// + /// Reads linked managed metadata to identify JNI mappings that still have managed consumers. + /// This scanner never modifies or reconstructs the input assembly. + /// + static class JniRemappingAssemblyScanner + { + const string RegisterAttributeFullName = "Android.Runtime.RegisterAttribute"; + const string JniTypeSignatureAttributeFullName = "Java.Interop.JniTypeSignatureAttribute"; + const string JniMethodSignatureAttributeFullName = "Java.Interop.JniMethodSignatureAttribute"; + const string JniConstructorSignatureAttributeFullName = "Java.Interop.JniConstructorSignatureAttribute"; + const string JavaPeerProxyNamespace = "Java.Interop"; + const string JavaPeerProxyName = "JavaPeerProxy"; + + public static void Scan (PEReader peReader, MetadataReader reader, R8Mapping mapping, TaskLoggingHelper log) + { + var ownerJniNames = new Dictionary (); + foreach (TypeDefinitionHandle typeHandle in reader.TypeDefinitions) { + ScanType (peReader, reader, mapping, log, ownerJniNames, typeHandle); + } + } + + static void ScanType (PEReader peReader, MetadataReader reader, R8Mapping mapping, TaskLoggingHelper log, + Dictionary ownerJniNames, TypeDefinitionHandle typeHandle) + { + TypeDefinition type = reader.GetTypeDefinition (typeHandle); + string? ownerJniName = ResolveOwnerJniName (peReader, reader, log, ownerJniNames, typeHandle); + if (ownerJniName != null) { + mapping.TryGetRenamedClass (ownerJniName, out _); + if (IsJavaPeerProxy (reader, type.BaseType)) { + RecordAllMappings (mapping, ownerJniName); + } + } + + foreach (MethodDefinitionHandle methodHandle in type.GetMethods ()) { + ScanMethod (reader, mapping, log, ownerJniName, methodHandle); + } + + foreach (FieldDefinitionHandle fieldHandle in type.GetFields ()) { + ScanFieldLikeMember (reader, mapping, log, ownerJniName, reader.GetFieldDefinition (fieldHandle).GetCustomAttributes ()); + } + foreach (PropertyDefinitionHandle propertyHandle in type.GetProperties ()) { + ScanFieldLikeMember (reader, mapping, log, ownerJniName, reader.GetPropertyDefinition (propertyHandle).GetCustomAttributes ()); + } + foreach (EventDefinitionHandle eventHandle in type.GetEvents ()) { + ScanFieldLikeMember (reader, mapping, log, ownerJniName, reader.GetEventDefinition (eventHandle).GetCustomAttributes ()); + } + } + + static string? ResolveOwnerJniName (PEReader peReader, MetadataReader reader, TaskLoggingHelper log, + Dictionary ownerJniNames, TypeDefinitionHandle typeHandle) + { + if (ownerJniNames.TryGetValue (typeHandle, out string? cached)) { + return cached; + } + + ownerJniNames [typeHandle] = null; + TypeDefinition type = reader.GetTypeDefinition (typeHandle); + string? result = GetTypeJniName (reader, log, type.GetCustomAttributes ()) ?? GetJavaPeerProxyJniName (peReader, reader, type); + if (result == null) { + TypeDefinitionHandle declaringType = type.GetDeclaringType (); + if (!declaringType.IsNil) { + result = ResolveOwnerJniName (peReader, reader, log, ownerJniNames, declaringType); + } + } + ownerJniNames [typeHandle] = result; + return result; + } + + static string? GetTypeJniName (MetadataReader reader, TaskLoggingHelper log, CustomAttributeHandleCollection attributes) + { + foreach (CustomAttributeHandle attributeHandle in attributes) { + CustomAttribute attribute = reader.GetCustomAttribute (attributeHandle); + string? name = reader.GetCustomAttributeFullName (attribute, log); + if (name != RegisterAttributeFullName && name != JniTypeSignatureAttributeFullName) { + continue; + } + var arguments = attribute.GetCustomAttributeArguments ().FixedArguments; + if (arguments.Length > 0 && arguments [0].Value is string jniName && jniName.Length > 0) { + return jniName; + } + } + return null; + } + + static string? GetJavaPeerProxyJniName (PEReader peReader, MetadataReader reader, TypeDefinition type) + { + if (!IsJavaPeerProxy (reader, type.BaseType)) { + return null; + } + + foreach (MethodDefinitionHandle methodHandle in type.GetMethods ()) { + MethodDefinition method = reader.GetMethodDefinition (methodHandle); + if ((method.Attributes & System.Reflection.MethodAttributes.RTSpecialName) == 0 || + reader.GetString (method.Name) != ".ctor" || + method.RelativeVirtualAddress == 0) { + continue; + } + + byte [] il = peReader.GetMethodBody (method.RelativeVirtualAddress).GetILBytes () ?? []; + for (int i = 0; i + 4 < il.Length; i++) { + if (il [i] != (byte) ILOpCode.Ldstr) { + continue; + } + int token = il [i + 1] | il [i + 2] << 8 | il [i + 3] << 16 | il [i + 4] << 24; + if ((token & unchecked ((int) 0xFF000000)) != 0x70000000) { + continue; + } + string value = reader.GetUserString (MetadataTokens.UserStringHandle (token & 0x00FFFFFF)); + if (value.Length > 0) { + return value; + } + } + } + return null; + } + + static bool IsJavaPeerProxy (MetadataReader reader, EntityHandle baseType) + { + if (baseType.IsNil) { + return false; + } + if (baseType.Kind == HandleKind.TypeReference) { + TypeReference type = reader.GetTypeReference ((TypeReferenceHandle) baseType); + return reader.GetString (type.Namespace) == JavaPeerProxyNamespace && + reader.GetString (type.Name) == JavaPeerProxyName; + } + if (baseType.Kind == HandleKind.TypeDefinition) { + TypeDefinition type = reader.GetTypeDefinition ((TypeDefinitionHandle) baseType); + return reader.GetString (type.Namespace) == JavaPeerProxyNamespace && + reader.GetString (type.Name) == JavaPeerProxyName; + } + return false; + } + + static void RecordAllMappings (R8Mapping mapping, string ownerJniName) + { + foreach (R8ClassMapping type in mapping.EnumerateClassMappings ()) { + if (type.OriginalJniName != ownerJniName) { + continue; + } + foreach (R8FieldMapping field in type.Fields) { + mapping.TryGetRenamedField (ownerJniName, field.OriginalName, out _); + } + foreach (R8MethodMapping method in type.Methods) { + mapping.TryGetRenamedMethod ( + ownerJniName, + method.OriginalName, + method.JavaParameterTypes, + method.JavaReturnType, + out _); + } + return; + } + } + + static void ScanMethod (MetadataReader reader, R8Mapping mapping, TaskLoggingHelper log, + string? ownerJniName, MethodDefinitionHandle methodHandle) + { + if (ownerJniName == null) { + return; + } + + MethodDefinition method = reader.GetMethodDefinition (methodHandle); + foreach (CustomAttributeHandle attributeHandle in method.GetCustomAttributes ()) { + CustomAttribute attribute = reader.GetCustomAttribute (attributeHandle); + string? name = reader.GetCustomAttributeFullName (attribute, log); + var arguments = attribute.GetCustomAttributeArguments ().FixedArguments; + string? methodName; + string? descriptor; + switch (name) { + case RegisterAttributeFullName: + case JniMethodSignatureAttributeFullName: + methodName = arguments.Length > 0 ? arguments [0].Value as string : null; + descriptor = arguments.Length > 1 ? arguments [1].Value as string : null; + break; + case JniConstructorSignatureAttributeFullName: + methodName = ""; + descriptor = arguments.Length > 0 ? arguments [0].Value as string : null; + break; + default: + continue; + } + + if (methodName.IsNullOrEmpty ()) { + continue; + } + if (descriptor != null && JniDescriptorText.IsValidMethodDescriptor (descriptor)) { + JniDescriptorText.MethodDescriptorToJavaTypes (descriptor, out var parameterTypes, out string returnType); + mapping.TryGetRenamedMethod (ownerJniName, R8Mapping.JniMemberNameToMappingName (methodName), parameterTypes, returnType, out _); + } else { + mapping.TryGetRenamedMethodByNameOnly (ownerJniName, R8Mapping.JniMemberNameToMappingName (methodName), out _); + } + } + } + + static void ScanFieldLikeMember (MetadataReader reader, R8Mapping mapping, TaskLoggingHelper log, + string? ownerJniName, CustomAttributeHandleCollection attributes) + { + if (ownerJniName == null) { + return; + } + + foreach (CustomAttributeHandle attributeHandle in attributes) { + CustomAttribute attribute = reader.GetCustomAttribute (attributeHandle); + if (reader.GetCustomAttributeFullName (attribute, log) != RegisterAttributeFullName) { + continue; + } + var arguments = attribute.GetCustomAttributeArguments ().FixedArguments; + if (arguments.Length > 0 && arguments [0].Value is string fieldName && fieldName.Length > 0) { + mapping.TryGetRenamedField (ownerJniName, fieldName, out _); + } + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/NativeAotJniRetention.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/NativeAotJniRetention.cs new file mode 100644 index 00000000000..312aa83f2ff --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/NativeAotJniRetention.cs @@ -0,0 +1,236 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using ELFSharp; +using ELFSharp.ELF; +using ELFSharp.ELF.Sections; + +namespace Xamarin.Android.Tasks.JniRemapping +{ + static class NativeAotJniRetention + { + public static HashSet GetRequiredEntries (string objectFile, R8Mapping mapping) + { + var sections = ReadObjectData (objectFile); + var classes = new List (mapping.EnumerateClassMappings ()); + var classPatterns = new LiteralMatcher (); + foreach (var type in classes) { + classPatterns.Add (type.OriginalJniName); + classPatterns.Add (type.OriginalJniName.Replace ('/', '.')); + } + HashSet retainedClasses = classPatterns.Match (sections); + + var memberPatterns = new LiteralMatcher (); + var candidateClasses = new List (); + foreach (var type in classes) { + if (!retainedClasses.Contains (type.OriginalJniName) && + !retainedClasses.Contains (type.OriginalJniName.Replace ('/', '.'))) { + continue; + } + candidateClasses.Add (type); + foreach (var method in type.Methods) { + memberPatterns.Add (method.OriginalName); + memberPatterns.Add (JniDescriptorText.JavaSourceTypesToMethodDescriptor (method.JavaParameterTypes, method.JavaReturnType)); + } + foreach (var field in type.Fields) { + memberPatterns.Add (field.OriginalName); + memberPatterns.Add (JniDescriptorText.JavaSourceTypeToJniTypeToken (field.JavaFieldType)); + } + } + HashSet retainedMembers = memberPatterns.Match (sections); + var required = new HashSet (StringComparer.Ordinal); + foreach (var type in candidateClasses) { + required.Add (R8Mapping.BuildClassEntry (type.OriginalJniName)); + foreach (var method in type.Methods) { + string descriptor = JniDescriptorText.JavaSourceTypesToMethodDescriptor (method.JavaParameterTypes, method.JavaReturnType); + bool constructor = method.OriginalName == "" || method.OriginalName == ""; + if (retainedMembers.Contains (descriptor) && (constructor || retainedMembers.Contains (method.OriginalName))) { + required.Add (R8Mapping.BuildMethodEntry (type.OriginalJniName, + R8Mapping.BuildMethodKey (method.OriginalName, method.JavaParameterTypes, method.JavaReturnType))); + } + } + foreach (var field in type.Fields) { + string descriptor = JniDescriptorText.JavaSourceTypeToJniTypeToken (field.JavaFieldType); + if (retainedMembers.Contains (field.OriginalName) && retainedMembers.Contains (descriptor)) { + required.Add (R8Mapping.BuildFieldEntry (type.OriginalJniName, field.OriginalName)); + } + } + } + return required; + } + + static List ReadObjectData (string path) + { + using var stream = File.OpenRead (path); + using IELF elf = ReadElfData (() => ELFReader.Load (stream, shouldOwnStream: false)); + ulong fileSize = (ulong) stream.Length; + if (elf.Type != FileType.Relocatable || elf.Endianess != Endianess.LittleEndian || + (elf.Class != Class.Bit64 && elf.Class != Class.Bit32)) { + throw new InvalidDataException (Properties.Resources.XA4327_NativeAotObjectFormat); + } + var data = new List (); + bool hasManagedCode = false; + bool hasData = false; + foreach (ISection section in elf.Sections) { + ulong offset; + ulong size; + if (section is Section section64) { + offset = section64.Offset; + size = section64.Size; + } else if (section is Section section32) { + offset = section32.Offset; + size = section32.Size; + } else { + throw new InvalidDataException (Properties.Resources.XA4327_NativeAotObjectFormat); + } + if (section.Type != SectionType.NoBits && (offset > fileSize || size > fileSize - offset)) { + throw new InvalidDataException (Properties.Resources.XA4327_NativeAotInvalidSection); + } + if ((section.Flags & SectionFlags.Allocatable) == 0 || section.Type == SectionType.NoBits) { + continue; + } + byte [] contents = ReadElfData (() => section.GetContents ()); + if ((ulong) contents.Length != size) { + throw new InvalidDataException (Properties.Resources.XA4327_NativeAotTruncatedSection); + } + if (contents.Length == 0) { + continue; + } + hasManagedCode |= section.Name == "__managedcode"; + hasData |= (section.Flags & SectionFlags.Executable) == 0; + data.Add (contents); + } + if (!hasManagedCode || !hasData) { + throw new InvalidDataException (Properties.Resources.XA4327_NativeAotMissingSections); + } + return data; + } + + static T ReadElfData (Func read) + { + try { + return read (); + } catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException || + ex is IndexOutOfRangeException || ex is OverflowException) { + throw new InvalidDataException (ex.Message, ex); + } + } + + sealed class LiteralMatcher + { + struct Node + { + public byte Value; + public int Child; + public int Sibling; + public int Failure; + public int Output; + public List? Patterns; + } + + Node [] nodes = new Node [256]; + int count = 1; + readonly int [] root = new int [256]; + readonly HashSet patterns = new HashSet (StringComparer.Ordinal); + + public void Add (string pattern) + { + if (pattern.Length == 0 || !patterns.Add (pattern)) { + return; + } + Add (Encoding.UTF8.GetBytes (pattern), pattern); + byte [] utf16 = Encoding.Unicode.GetBytes (pattern); + int start = utf16 [0] == 0 ? 1 : 0; + int length = utf16.Length - start - (utf16 [utf16.Length - 1] == 0 ? 1 : 0); + var payload = new byte [length]; + Buffer.BlockCopy (utf16, start, payload, 0, length); + Add (payload, pattern); + } + + void Add (byte [] bytes, string pattern) + { + int current = 0; + foreach (byte value in bytes) { + int next = Find (current, value); + if (next == 0) { + if (count == nodes.Length) { + Array.Resize (ref nodes, checked (nodes.Length * 2)); + } + next = count++; + nodes [next].Value = value; + nodes [next].Sibling = nodes [current].Child; + nodes [current].Child = next; + if (current == 0) { + root [value] = next; + } + } + current = next; + } + var terminalPatterns = nodes [current].Patterns; + if (terminalPatterns == null) { + nodes [current].Patterns = terminalPatterns = new List (); + } + terminalPatterns.Add (pattern); + } + + int Find (int node, byte value) + { + if (node == 0) { + return root [value]; + } + for (int child = nodes [node].Child; child != 0; child = nodes [child].Sibling) { + if (nodes [child].Value == value) { + return child; + } + } + return 0; + } + + public HashSet Match (List sections) + { + var queue = new Queue (); + for (int child = nodes [0].Child; child != 0; child = nodes [child].Sibling) { + queue.Enqueue (child); + } + while (queue.Count > 0) { + int parent = queue.Dequeue (); + for (int child = nodes [parent].Child; child != 0; child = nodes [child].Sibling) { + int failure = nodes [parent].Failure; + int next; + while ((next = Find (failure, nodes [child].Value)) == 0 && failure != 0) { + failure = nodes [failure].Failure; + } + nodes [child].Failure = next; + nodes [child].Output = nodes [next].Patterns != null ? next : nodes [next].Output; + queue.Enqueue (child); + } + } + + var found = new HashSet (StringComparer.Ordinal); + foreach (byte [] section in sections) { + int current = 0; + foreach (byte value in section) { + int next; + while ((next = Find (current, value)) == 0 && current != 0) { + current = nodes [current].Failure; + } + current = next; + for (int output = current; output != 0; output = nodes [output].Output) { + var terminalPatterns = nodes [output].Patterns; + if (terminalPatterns != null) { + foreach (string pattern in terminalPatterns) { + found.Add (pattern); + } + } + } + } + } + return found; + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs index d8506ecdb73..00349c3c39b 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs @@ -30,6 +30,9 @@ sealed class R8Mapping : IJniNameMapping // Original JNI class name -> (original field name -> obfuscated field name). readonly Dictionary> fields = new Dictionary> (StringComparer.Ordinal); + // Original JNI class name -> (original field name -> declared field type, in Java source form). + readonly Dictionary> fieldTypes = new Dictionary> (StringComparer.Ordinal); + // Original JNI class name -> ("name(javaParam,javaParam,...):javaReturn" -> obfuscated method name). readonly Dictionary> methods = new Dictionary> (StringComparer.Ordinal); @@ -136,6 +139,10 @@ static R8Mapping Parse (TextReader reader, string sourceName) mapping.fields [currentOriginalClass] = classFields = new Dictionary (StringComparer.Ordinal); } classFields [memberName] = obfuscatedName; + if (!mapping.fieldTypes.TryGetValue (currentOriginalClass, out var classFieldTypes)) { + mapping.fieldTypes [currentOriginalClass] = classFieldTypes = new Dictionary (StringComparer.Ordinal); + } + classFieldTypes [memberName] = javaReturnType ?? ""; } else { string key = BuildMethodKey (memberName, javaParameterTypes, javaReturnType ?? ""); if (positionRange == null) { @@ -465,6 +472,86 @@ public IEnumerable GetReachabilityConflicts (R8Mapping finalMapping, IEn } } + /// + /// Enumerates surviving class mappings and their unambiguous member mappings in stable + /// ordinal order without recording them as accessed. + /// + internal IEnumerable EnumerateClassMappings () + { + var originalClassNames = new List (classes.Keys); + originalClassNames.Sort (StringComparer.Ordinal); + foreach (string originalClassName in originalClassNames) { + string obfuscatedClassName = classes [originalClassName]; + if (IsRemovedClassName (obfuscatedClassName)) { + continue; + } + yield return new R8ClassMapping ( + originalClassName, + obfuscatedClassName, + EnumerateFieldMappings (originalClassName), + EnumerateMethodMappings (originalClassName)); + } + } + + List EnumerateFieldMappings (string originalClassName) + { + var result = new List (); + if (!fields.TryGetValue (originalClassName, out var classFields)) { + return result; + } + + var fieldNames = new List (classFields.Keys); + fieldNames.Sort (StringComparer.Ordinal); + fieldTypes.TryGetValue (originalClassName, out var classFieldTypes); + foreach (string fieldName in fieldNames) { + string javaFieldType = ""; + classFieldTypes?.TryGetValue (fieldName, out javaFieldType); + result.Add (new R8FieldMapping (fieldName, classFields [fieldName], javaFieldType ?? "")); + } + return result; + } + + List EnumerateMethodMappings (string originalClassName) + { + var result = new List (); + if (!methods.TryGetValue (originalClassName, out var classMethods)) { + return result; + } + + var methodKeys = new List (classMethods.Keys); + methodKeys.Sort (StringComparer.Ordinal); + foreach (string methodKey in methodKeys) { + string obfuscatedName = classMethods [methodKey]; + if (obfuscatedName.Length == 0) { + continue; + } + if (!TrySplitMethodKey (methodKey, out string name, out string [] javaParameterTypes, out string javaReturnType)) { + continue; + } + result.Add (new R8MethodMapping (name, obfuscatedName, javaParameterTypes, javaReturnType)); + } + return result; + } + + internal static bool TrySplitMethodKey (string methodKey, out string javaMethodName, out string [] javaParameterTypes, out string javaReturnType) + { + javaMethodName = ""; + javaParameterTypes = []; + javaReturnType = ""; + + int parenOpen = methodKey.IndexOf ('('); + int parenClose = methodKey.LastIndexOf ("):", StringComparison.Ordinal); + if (parenOpen < 0 || parenClose < parenOpen) { + return false; + } + + javaMethodName = methodKey.Substring (0, parenOpen); + string parameterList = methodKey.Substring (parenOpen + 1, parenClose - parenOpen - 1); + javaParameterTypes = parameterList.Length == 0 ? [] : parameterList.Split (','); + javaReturnType = methodKey.Substring (parenClose + 2); + return javaMethodName.Length != 0; + } + internal static string BuildClassEntry (string className) => $"C\t{className}"; internal static string BuildFieldEntry (string className, string fieldName) => $"F\t{className}\t{fieldName}"; internal static string BuildMethodEntry (string className, string methodKey) => $"M\t{className}\t{methodKey}"; @@ -742,7 +829,7 @@ static bool TryParseMemberLine (string trimmed, out string name, out string []? name = left.Substring (lastSpace + 1); javaParameterTypes = null; - javaReturnType = null; + javaReturnType = left.Substring (0, lastSpace); return name.Length > 0; } } @@ -810,4 +897,56 @@ static string StripTrailingLineRange (string s) return s.Substring (0, lastColon); } } + + sealed class R8ClassMapping + { + public string OriginalJniName { get; } + public string ObfuscatedJniName { get; } + public IReadOnlyList Fields { get; } + public IReadOnlyList Methods { get; } + + public bool IsRenamed => !String.Equals (OriginalJniName, ObfuscatedJniName, StringComparison.Ordinal); + + public R8ClassMapping (string originalJniName, string obfuscatedJniName, IReadOnlyList fields, IReadOnlyList methods) + { + OriginalJniName = originalJniName; + ObfuscatedJniName = obfuscatedJniName; + Fields = fields; + Methods = methods; + } + } + + sealed class R8FieldMapping + { + public string OriginalName { get; } + public string ObfuscatedName { get; } + public string JavaFieldType { get; } + + public bool IsRenamed => !String.Equals (OriginalName, ObfuscatedName, StringComparison.Ordinal); + + public R8FieldMapping (string originalName, string obfuscatedName, string javaFieldType) + { + OriginalName = originalName; + ObfuscatedName = obfuscatedName; + JavaFieldType = javaFieldType; + } + } + + sealed class R8MethodMapping + { + public string OriginalName { get; } + public string ObfuscatedName { get; } + public IReadOnlyList JavaParameterTypes { get; } + public string JavaReturnType { get; } + + public bool IsRenamed => !String.Equals (OriginalName, ObfuscatedName, StringComparison.Ordinal); + + public R8MethodMapping (string originalName, string obfuscatedName, IReadOnlyList javaParameterTypes, string javaReturnType) + { + OriginalName = originalName; + ObfuscatedName = obfuscatedName; + JavaParameterTypes = javaParameterTypes; + JavaReturnType = javaReturnType; + } + } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs deleted file mode 100644 index c79f4855a58..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ /dev/null @@ -1,334 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using System.Text; - -using Microsoft.Build.Utilities; - -using Xamarin.Android.Tasks.LLVMIR; - -namespace Xamarin.Android.Tasks -{ - sealed class JniRemappingTypeReplacement - { - public string From { get; } - public string To { get; } - - public JniRemappingTypeReplacement (string from, string to) - { - From = from; - To = to; - } - } - - sealed class JniRemappingMethodReplacement - { - public string SourceType { get; } - public string SourceMethod { get; } - public string SourceMethodSignature { get; } - - public string TargetType { get; } - public string TargetMethod { get; } - - public bool TargetIsStatic { get; } - - public JniRemappingMethodReplacement (string sourceType, string sourceMethod, string sourceMethodSignature, - string targetType, string targetMethod, bool targetIsStatic) - { - SourceType = sourceType; - SourceMethod = sourceMethod; - SourceMethodSignature = sourceMethodSignature; - - TargetType = targetType; - TargetMethod = targetMethod; - TargetIsStatic = targetIsStatic; - } - } - - class JniRemappingAssemblyGenerator : LlvmIrComposer - { - const string TypeReplacementsVariableName = "jni_remapping_type_replacements"; - const string MethodReplacementIndexVariableName = "jni_remapping_method_replacement_index"; - - sealed class JniRemappingTypeReplacementEntryContextDataProvider : 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}"; - } - - return String.Empty; - } - } - - sealed class JniRemappingIndexTypeEntryContextDataProvider : 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 ("methods", fieldName)) { - return entry.MethodsArraySymbolName; - } - - return base.GetPointedToSymbolName (data, fieldName); - } - - public override ulong GetBufferSize (object data, string fieldName) - { - var entry = EnsureType (data); - if (MonoAndroidHelper.StringEquals ("methods", fieldName)) { - return (ulong)entry.TypeMethods.Count; - } - - return 0; - } - } - - sealed class JniRemappingIndexMethodEntryContextDataProvider : 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; - public string str; - }; - - sealed class JniRemappingReplacementMethod - { - public string target_type; - public string target_name; - public bool is_static; - }; - - [NativeAssemblerStructContextDataProvider (typeof(JniRemappingIndexMethodEntryContextDataProvider))] - sealed class JniRemappingIndexMethodEntry - { - [NativeAssembler (UsesDataProvider = true)] - public JniRemappingString name; - - [NativeAssembler (UsesDataProvider = true)] - public JniRemappingString signature; - - [NativeAssembler (UsesDataProvider = true)] - public JniRemappingReplacementMethod replacement; - }; - - [NativeAssemblerStructContextDataProvider (typeof(JniRemappingIndexTypeEntryContextDataProvider))] - sealed class JniRemappingIndexTypeEntry - { - [NativeAssembler (UsesDataProvider = true)] - public JniRemappingString name; - public uint method_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 JniRemappingIndexMethodEntry methods; -#pragma warning restore CS0649 - - [NativeAssembler (Ignore = true)] - public string MethodsArraySymbolName; - - [NativeAssembler (Ignore = true)] - public List> TypeMethods; - }; - - [NativeAssemblerStructContextDataProvider (typeof(JniRemappingTypeReplacementEntryContextDataProvider))] - sealed class JniRemappingTypeReplacementEntry - { - [NativeAssembler (UsesDataProvider = true)] - public JniRemappingString name; - - [NativeAssembler (UsesDataProvider = true)] - public string replacement; - }; - - List typeReplacementsInput; - List methodReplacementsInput; - - StructureInfo jniRemappingStringStructureInfo; - StructureInfo jniRemappingReplacementMethodStructureInfo; - StructureInfo jniRemappingIndexMethodEntryStructureInfo; - StructureInfo jniRemappingIndexTypeEntryStructureInfo; - StructureInfo jniRemappingTypeReplacementEntryStructureInfo; - - public int ReplacementMethodIndexEntryCount { get; private set; } = 0; - - public JniRemappingAssemblyGenerator (TaskLoggingHelper log) - : base (log) - {} - - public JniRemappingAssemblyGenerator (TaskLoggingHelper log, List typeReplacements, List methodReplacements) - : base (log) - { - this.typeReplacementsInput = typeReplacements ?? throw new ArgumentNullException (nameof (typeReplacements)); - this.methodReplacementsInput = methodReplacements ?? throw new ArgumentNullException (nameof (methodReplacements)); - } - - (List>? typeReplacements, List>? methodIndexTypes) Init () - { - if (typeReplacementsInput == null) { - return (null, null); - } - - var typeReplacements = new List> (); - foreach (JniRemappingTypeReplacement mtr in typeReplacementsInput) { - var entry = new JniRemappingTypeReplacementEntry { - name = MakeJniRemappingString (mtr.From), - replacement = mtr.To, - }; - - typeReplacements.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); - - 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> (), - }; - - typeEntry = new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, entry); - methodIndexTypes.Add (typeEntry); - types.Add (mmr.SourceType, typeEntry); - } - - 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, - }, - }; - - typeEntry.Instance.TypeMethods.Add (new StructureInstance (jniRemappingIndexMethodEntryStructureInfo, method)); - } - - 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)); - } - - methodIndexTypes.Sort ((StructureInstance l, StructureInstance r) => l.Instance.name.str.CompareTo (r.Instance.name.str)); - ReplacementMethodIndexEntryCount = methodIndexTypes.Count; - - return (typeReplacements, methodIndexTypes); - - string MakeMethodsArrayName (string typeName) - { - return $"mm_{typeName.Replace ('/', '_')}"; - } - - JniRemappingString MakeJniRemappingString (string str) - { - return new JniRemappingString { - length = GetLength (str), - str = str, - }; - } - - uint GetLength (string str) - { - if (String.IsNullOrEmpty (str)) { - return 0; - } - - return (uint)Encoding.UTF8.GetBytes (str).Length; - } - } - - protected override void Construct (LlvmIrModule module) - { - module.DefaultStringGroup = "jremap"; - - MapStructures (module); - List>? typeReplacements; - List>? methodIndexTypes; - - (typeReplacements, methodIndexTypes) = Init (); - - if (typeReplacements == null) { - module.AddGlobalVariable ( - typeof(StructureInstance), - TypeReplacementsVariableName, - new StructureInstance (jniRemappingTypeReplacementEntryStructureInfo, new JniRemappingTypeReplacementEntry ()) { IsZeroInitialized = true }, - LlvmIrVariableOptions.GlobalConstant - ); - - module.AddGlobalVariable ( - typeof(StructureInstance), - MethodReplacementIndexVariableName, - new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, new JniRemappingIndexTypeEntry ()) { IsZeroInitialized = true }, - LlvmIrVariableOptions.GlobalConstant - ); - return; - } - - module.AddGlobalVariable (TypeReplacementsVariableName, typeReplacements, LlvmIrVariableOptions.GlobalConstant); - - foreach (StructureInstance entry in methodIndexTypes) { - module.AddGlobalVariable (entry.Instance.MethodsArraySymbolName, entry.Instance.TypeMethods, LlvmIrVariableOptions.LocalConstant); - } - - module.AddGlobalVariable (MethodReplacementIndexVariableName, methodIndexTypes, LlvmIrVariableOptions.GlobalConstant); - } - - void MapStructures (LlvmIrModule module) - { - jniRemappingStringStructureInfo = module.MapStructure (); - jniRemappingReplacementMethodStructureInfo = module.MapStructure (); - jniRemappingIndexMethodEntryStructureInfo = module.MapStructure (); - jniRemappingIndexTypeEntryStructureInfo = module.MapStructure (); - jniRemappingTypeReplacementEntryStructureInfo = module.MapStructure (); - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingNativeCodeGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingNativeCodeGenerator.cs new file mode 100644 index 00000000000..042bca373f5 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingNativeCodeGenerator.cs @@ -0,0 +1,690 @@ +#nullable disable + +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.Build.Utilities; + +using Xamarin.Android.Tasks.LLVMIR; + +namespace Xamarin.Android.Tasks +{ + sealed class JniRemappingTypeReplacement + { + public string From { get; } + public string To { get; } + + public JniRemappingTypeReplacement (string from, string to) + { + From = from; + To = to; + } + } + + sealed class JniRemappingMethodReplacement + { + public string SourceType { get; } + public string SourceMethod { get; } + public string SourceMethodSignature { get; } + + 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, string targetMethodSignature, + bool targetIsStatic) + { + SourceType = sourceType; + SourceMethod = sourceMethod; + SourceMethodSignature = sourceMethodSignature; + + 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 JniRemappingNativeCodeGenerator : 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 + { + 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}"; + } + + return String.Empty; + } + } + + sealed class JniRemappingIndexTypeEntryContextDataProvider : 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 ("methods", fieldName)) { + return entry.MethodsArraySymbolName; + } + + return base.GetPointedToSymbolName (data, fieldName); + } + + public override ulong GetBufferSize (object data, string fieldName) + { + var entry = EnsureType (data); + if (MonoAndroidHelper.StringEquals ("methods", fieldName)) { + return (ulong)entry.TypeMethods.Count; + } + + return 0; + } + } + + sealed class JniRemappingIndexMethodEntryContextDataProvider : 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 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; + public string str; + }; + + 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 + { + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingString name; + + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingString signature; + + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingReplacementMethod replacement; + }; + + [NativeAssemblerStructContextDataProvider (typeof(JniRemappingIndexTypeEntryContextDataProvider))] + sealed class JniRemappingIndexTypeEntry + { + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingString name; + public uint method_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 JniRemappingIndexMethodEntry methods; +#pragma warning restore CS0649 + + [NativeAssembler (Ignore = true)] + public string MethodsArraySymbolName; + + [NativeAssembler (Ignore = true)] + 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 + { + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingString name; + + [NativeAssembler (UsesDataProvider = true)] + 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 JniRemappingNativeCodeGenerator (TaskLoggingHelper log) + : base (log) + {} + + public JniRemappingNativeCodeGenerator (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)); + } + + /// + /// 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; + } + + var ret = new GeneratedTables { + TypeReplacements = MakeTypeReplacements (typeReplacementsInput), + ReverseTypeReplacements = MakeTypeReplacements (reverseTypeReplacementsInput), + MethodIndexTypes = MakeMethodIndex (), + FieldIndexTypes = MakeFieldIndex (), + }; + + ReplacementTypeCount = ret.TypeReplacements.Count; + ReverseTypeCount = ret.ReverseTypeReplacements.Count; + ReplacementMethodIndexEntryCount = ret.MethodIndexTypes.Count; + ReplacementFieldIndexEntryCount = ret.FieldIndexTypes.Count; + + return ret; + } + + List> MakeTypeReplacements (List input) + { + var sorted = new List<(byte [] key, JniRemappingTypeReplacement replacement)> (input.Count); + foreach (JniRemappingTypeReplacement tr in input) { + sorted.Add ((Utf8 (tr.From), tr)); + } + sorted.Sort ((l, r) => CompareUtf8 (l.key, r.key)); + + var ret = new List> (sorted.Count); + foreach ((byte [] key, JniRemappingTypeReplacement tr) in sorted) { + var entry = new JniRemappingTypeReplacementEntry { + name = MakeJniRemappingString (tr.From, key), + replacement = tr.To, + }; + + ret.Add (new StructureInstance (jniRemappingTypeReplacementEntryStructureInfo, entry)); + } + + return ret; + } + + List> MakeMethodIndex () + { + var types = new Dictionary methods)> (StringComparer.Ordinal); + + foreach (JniRemappingMethodReplacement mmr in methodReplacementsInput) { + 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, + }, + }; + + typeMethods.Add (new StructureInstance (jniRemappingIndexMethodEntryStructureInfo, method)); + } + + var entry = new JniRemappingIndexTypeEntry { + name = MakeJniRemappingString (kvp.Key, kvp.Value.key), + method_count = (uint)typeMethods.Count, + MethodsArraySymbolName = MakeMembersArrayName ("mm", typeIndex), + TypeMethods = typeMethods, + }; + ret.Add (new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, entry)); + } + + return ret; + } + + List> MakeFieldIndex () + { + var types = new Dictionary fields)> (StringComparer.Ordinal); + + 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); + } + + typeEntry.fields.Add ((Utf8 (mfr.SourceField), Utf8 (mfr.SourceFieldSignature), mfr)); + } + + 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)); + } + + return ret; + } + + static string MakeMembersArrayName (string prefix, int typeIndex) + { + return $"{prefix}_{typeIndex}"; + } + + 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) + { + module.DefaultStringGroup = "jremap"; + + MapStructures (module); + + GeneratedTables tables = Init (); + + if (tables == null) { + module.AddGlobalVariable ( + typeof(StructureInstance), + TypeReplacementsVariableName, + new StructureInstance (jniRemappingTypeReplacementEntryStructureInfo, new JniRemappingTypeReplacementEntry ()) { IsZeroInitialized = true }, + 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, tables.TypeReplacements, LlvmIrVariableOptions.GlobalConstant); + module.AddGlobalVariable (ReverseTypeReplacementsVariableName, tables.ReverseTypeReplacements, LlvmIrVariableOptions.GlobalConstant); + + foreach (StructureInstance entry in tables.MethodIndexTypes) { + module.AddGlobalVariable (entry.Instance.MethodsArraySymbolName, entry.Instance.TypeMethods, LlvmIrVariableOptions.LocalConstant); + } + + module.AddGlobalVariable (MethodReplacementIndexVariableName, tables.MethodIndexTypes, LlvmIrVariableOptions.GlobalConstant); + + foreach (StructureInstance entry in tables.FieldIndexTypes) { + module.AddGlobalVariable (entry.Instance.FieldsArraySymbolName, entry.Instance.TypeFields, LlvmIrVariableOptions.LocalConstant); + } + + module.AddGlobalVariable (FieldReplacementIndexVariableName, tables.FieldIndexTypes, LlvmIrVariableOptions.GlobalConstant); + + AddData (module); + } + + void AddData (LlvmIrModule module) + { + var data = new JniRemappingData { + type_replacement_count = (uint)ReplacementTypeCount, + reverse_type_replacement_count = (uint)ReverseTypeCount, + method_replacement_index_count = (uint)ReplacementMethodIndexEntryCount, + field_replacement_index_count = (uint)ReplacementFieldIndexEntryCount, + }; + module.AddGlobalVariable ( + RemappingDataVariableName, + new StructureInstance (jniRemappingDataStructureInfo, data), + LlvmIrVariableOptions.GlobalConstant + ); + } + + void MapStructures (LlvmIrModule module) + { + jniRemappingStringStructureInfo = module.MapStructure (); + jniRemappingReplacementMethodStructureInfo = module.MapStructure (); + jniRemappingReplacementFieldStructureInfo = module.MapStructure (); + jniRemappingIndexMethodEntryStructureInfo = module.MapStructure (); + jniRemappingIndexTypeEntryStructureInfo = module.MapStructure (); + jniRemappingIndexFieldEntryStructureInfo = module.MapStructure (); + jniRemappingIndexFieldTypeEntryStructureInfo = module.MapStructure (); + jniRemappingTypeReplacementEntryStructureInfo = module.MapStructure (); + jniRemappingDataStructureInfo = module.MapStructure (); + } + } +} diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index 1e93c54871c..6f74c4d63b9 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -486,7 +486,7 @@ void Host::Java_mono_android_Runtime_initInternal ( init.packageNamingPolicy = static_cast(application_config.package_naming_policy); init.boundExceptionType = 0; // System init.jniAddNativeMethodRegistrationAttributePresent = application_config.jni_add_native_method_registration_attribute_present ? 1 : 0; - init.jniRemappingInUse = application_config.jni_remapping_replacement_type_count > 0 || application_config.jni_remapping_replacement_method_index_entry_count > 0; + init.jniRemappingData = &jni_remapping_data; init.marshalMethodsEnabled = application_config.marshal_methods_enabled; // GC threshold is 90% of the max GREF count diff --git a/src/native/clr/host/internal-pinvokes-clr.cc b/src/native/clr/host/internal-pinvokes-clr.cc index 7c978f9ee08..18971d298fa 100644 --- a/src/native/clr/host/internal-pinvokes-clr.cc +++ b/src/native/clr/host/internal-pinvokes-clr.cc @@ -5,7 +5,6 @@ #include #include #include -#include using namespace xamarin::android; @@ -27,18 +26,6 @@ bool clr_typemap_java_to_managed (const char *java_type_name, char const** assem return TypeMapper::java_to_managed (java_type_name, assembly_name, managed_type_token_id); } -const char* -_monodroid_lookup_replacement_type (const char *jniSimpleReference) -{ - return JniRemapping::lookup_replacement_type (jniSimpleReference); -} - -const JniRemappingReplacementMethod* -_monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) -{ - return JniRemapping::lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature); -} - managed_timing_sequence* monodroid_timing_start (const char *message) { if (!FastTiming::enabled ()) [[likely]] { diff --git a/src/native/clr/host/internal-pinvokes-shared.cc b/src/native/clr/host/internal-pinvokes-shared.cc index 18bffb5812e..08378e85d49 100644 --- a/src/native/clr/host/internal-pinvokes-shared.cc +++ b/src/native/clr/host/internal-pinvokes-shared.cc @@ -5,7 +5,6 @@ #include #include #include -#include using namespace xamarin::android; diff --git a/src/native/clr/include/runtime-base/internal-pinvokes.hh b/src/native/clr/include/runtime-base/internal-pinvokes.hh index a5408b45046..492054bf93e 100644 --- a/src/native/clr/include/runtime-base/internal-pinvokes.hh +++ b/src/native/clr/include/runtime-base/internal-pinvokes.hh @@ -23,8 +23,6 @@ extern "C" { void monodroid_log (xamarin::android::LogLevel level, LogCategories category, const char *message) noexcept; char* monodroid_TypeManager_get_java_class_name (jclass klass) noexcept; void monodroid_free (void *ptr) noexcept; - const char* _monodroid_lookup_replacement_type (const char *jniSimpleReference); - const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature); xamarin::android::managed_timing_sequence* monodroid_timing_start (const char *message); void monodroid_timing_stop (xamarin::android::managed_timing_sequence *sequence, const char *message); diff --git a/src/native/clr/include/runtime-base/jni-remapping.hh b/src/native/clr/include/runtime-base/jni-remapping.hh deleted file mode 100644 index f7b421b43cb..00000000000 --- a/src/native/clr/include/runtime-base/jni-remapping.hh +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "xamarin-app.hh" - -namespace xamarin::android -{ - class JniRemapping final - { - public: - static auto lookup_replacement_type (const char *jniSimpleReference) noexcept -> const char*; - static auto lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept -> const JniRemappingReplacementMethod*; - - private: - [[gnu::nonnull (2)]] - static auto equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept -> bool; - }; -} diff --git a/src/native/clr/include/xamarin-app.hh b/src/native/clr/include/xamarin-app.hh index c15776e2f27..32ce747fd2d 100644 --- a/src/native/clr/include/xamarin-app.hh +++ b/src/native/clr/include/xamarin-app.hh @@ -243,8 +243,7 @@ struct JniRemappingReplacementMethod { const char *target_type; const char *target_name; - // const char *target_signature; - // const int32_t param_count; + const char *target_signature; const bool is_static; }; diff --git a/src/native/clr/pinvoke-override/precompiled.cc b/src/native/clr/pinvoke-override/precompiled.cc index ec6ae2cb522..2ac13cd1c44 100644 --- a/src/native/clr/pinvoke-override/precompiled.cc +++ b/src/native/clr/pinvoke-override/precompiled.cc @@ -55,12 +55,6 @@ namespace { if (entrypoint_name == "monodroid_log"sv) { return reinterpret_cast (&monodroid_log); } - if (entrypoint_name == "_monodroid_lookup_replacement_type"sv) { - return reinterpret_cast (&_monodroid_lookup_replacement_type); - } - if (entrypoint_name == "_monodroid_lookup_replacement_method_info"sv) { - return reinterpret_cast (&_monodroid_lookup_replacement_method_info); - } if (entrypoint_name == "_monodroid_lref_log_delete"sv) { return reinterpret_cast (&_monodroid_lref_log_delete); } diff --git a/src/native/clr/runtime-base/CMakeLists.txt b/src/native/clr/runtime-base/CMakeLists.txt index 29a9e94e43e..99c9710d291 100644 --- a/src/native/clr/runtime-base/CMakeLists.txt +++ b/src/native/clr/runtime-base/CMakeLists.txt @@ -53,7 +53,6 @@ set(XA_RUNTIME_BASE_SOURCES android-system.cc android-system-shared.cc cpu-arch-detect.cc - jni-remapping.cc logger.cc util.cc ) diff --git a/src/native/clr/runtime-base/jni-remapping.cc b/src/native/clr/runtime-base/jni-remapping.cc deleted file mode 100644 index 715e5cb662e..00000000000 --- a/src/native/clr/runtime-base/jni-remapping.cc +++ /dev/null @@ -1,97 +0,0 @@ -#include - -#include -#include - -#include "xamarin-app.hh" - -using namespace xamarin::android; - -[[gnu::always_inline]] -auto JniRemapping::equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept -> bool -{ - if (left.length != static_cast(right_len) || left.str[0] != *right) { - return false; - } - - if (memcmp (left.str, right, right_len) == 0) { - return true; - } - - return false; -} - -auto JniRemapping::lookup_replacement_type (const char *jniSimpleReference) noexcept -> const char* -{ - if (application_config.jni_remapping_replacement_type_count == 0 || jniSimpleReference == nullptr || *jniSimpleReference == '\0') { - return nullptr; - } - - size_t ref_len = strlen (jniSimpleReference); - for (size_t i = 0uz; i < application_config.jni_remapping_replacement_type_count; i++) { - JniRemappingTypeReplacementEntry const& entry = jni_remapping_type_replacements[i]; - - if (equal (entry.name, jniSimpleReference, ref_len)) { - return entry.replacement; - } - } - - return nullptr; -} - -auto JniRemapping::lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept -> const JniRemappingReplacementMethod* -{ - if (application_config.jni_remapping_replacement_method_index_entry_count == 0 || - jniSourceType == nullptr || *jniSourceType == '\0' || - jniMethodName == nullptr || *jniMethodName == '\0') { - return nullptr; - } - - size_t source_type_len = strlen (jniSourceType); - - const JniRemappingIndexTypeEntry *type = nullptr; - for (size_t i = 0uz; i < application_config.jni_remapping_replacement_method_index_entry_count; i++) { - JniRemappingIndexTypeEntry const& entry = jni_remapping_method_replacement_index[i]; - - if (!equal (entry.name, jniSourceType, source_type_len)) { - continue; - } - - type = &jni_remapping_method_replacement_index[i]; - break; - } - - if (type == nullptr || type->method_count == 0 || type->methods == nullptr) { - return nullptr; - } - - size_t method_name_len = strlen (jniMethodName); - size_t signature_len = jniMethodSignature == nullptr ? 0uz : strlen (jniMethodSignature); - - for (size_t i = 0uz; i < type->method_count; i++) { - JniRemappingIndexMethodEntry const& entry = type->methods[i]; - - if (!equal (entry.name, jniMethodName, method_name_len)) { - continue; - } - - if (entry.signature.length == 0 || equal (entry.signature, jniMethodSignature, signature_len)) { - return &type->methods[i].replacement; - } - - const char *sig_end = jniMethodSignature + signature_len; - if (*sig_end == ')') { - continue; - } - - while (sig_end != jniMethodSignature && *sig_end != ')') { - sig_end--; - } - - if (equal (entry.signature, jniMethodSignature, static_cast(sig_end - jniMethodSignature) + 1uz)) { - return &type->methods[i].replacement; - } - } - - return nullptr; -} diff --git a/src/native/clr/xamarin-app-stub/application_dso_stub.cc b/src/native/clr/xamarin-app-stub/application_dso_stub.cc index 3890aef2639..ac0a2721fdf 100644 --- a/src/native/clr/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/clr/xamarin-app-stub/application_dso_stub.cc @@ -1,6 +1,7 @@ #include #include +#include #include // This file MUST have "valid" values everywhere - the DSO it is compiled into is loaded by the @@ -152,6 +153,7 @@ static const JniRemappingIndexMethodEntry some_java_type_one_methods[] = { .replacement = { .target_type = "some/java/target_type_one", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = false, } }, @@ -172,6 +174,7 @@ static const JniRemappingIndexMethodEntry some_java_type_two_methods[] = { .replacement = { .target_type = "some/java/target_type_two", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = true, } }, @@ -215,6 +218,17 @@ const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[] = { }, }; +extern "C" const xamarin::android::JniRemappingData jni_remapping_data { + .type_replacements = jni_remapping_type_replacements, + .reverse_type_replacements = nullptr, + .method_replacement_index = jni_remapping_method_replacement_index, + .field_replacement_index = nullptr, + .type_replacement_count = 2, + .reverse_type_replacement_count = 0, + .method_replacement_index_count = 2, + .field_replacement_index_count = 0, +}; + const char *init_runtime_property_names[] = { "HOST_RUNTIME_CONTRACT", "RUNTIME_IDENTIFIER", diff --git a/src/native/common/include/managed-interface.hh b/src/native/common/include/managed-interface.hh index ca29461dc05..5bf794af12a 100644 --- a/src/native/common/include/managed-interface.hh +++ b/src/native/common/include/managed-interface.hh @@ -17,6 +17,21 @@ namespace xamarin::android { using jnienv_propagate_uncaught_exception_fn = void (*)(JNIEnv *env, jobject javaThread, jthrowable javaException); using jnienv_register_jni_natives_fn = void (*)(const jchar *typeName_ptr, int32_t typeName_len, jclass jniClass, const jchar *methods_ptr, int32_t methods_len); + struct JniRemappingData { + const void *type_replacements; + const void *reverse_type_replacements; + const void *method_replacement_index; + const void *field_replacement_index; + uint32_t type_replacement_count; + uint32_t reverse_type_replacement_count; + uint32_t method_replacement_index_count; + uint32_t field_replacement_index_count; + }; + + extern "C" { + [[gnu::visibility("default")]] extern const JniRemappingData jni_remapping_data; + } + // NOTE: Keep this in sync with managed side in src/Mono.Android/Android.Runtime/JNIEnvInit.cs struct JnienvInitializeArgs { JavaVM *javaVm; @@ -32,7 +47,7 @@ namespace xamarin::android { int packageNamingPolicy; uint8_t boundExceptionType; int jniAddNativeMethodRegistrationAttributePresent; - bool jniRemappingInUse; + const JniRemappingData *jniRemappingData; bool marshalMethodsEnabled; jobject grefGCUserPeerable; jnienv_propagate_uncaught_exception_fn propagateUncaughtExceptionFn; diff --git a/src/native/mono/monodroid/CMakeLists.txt b/src/native/mono/monodroid/CMakeLists.txt index ca48f41f06f..3b07e5d7080 100644 --- a/src/native/mono/monodroid/CMakeLists.txt +++ b/src/native/mono/monodroid/CMakeLists.txt @@ -97,7 +97,6 @@ set(XAMARIN_MONODROID_SOURCES embedded-assemblies.cc globals.cc internal-pinvokes.cc - jni-remapping.cc mono-log-adapter.cc monodroid-glue.cc monodroid-tracing.cc diff --git a/src/native/mono/monodroid/internal-pinvokes.cc b/src/native/mono/monodroid/internal-pinvokes.cc index e7f580e8e41..397a2902b20 100644 --- a/src/native/mono/monodroid/internal-pinvokes.cc +++ b/src/native/mono/monodroid/internal-pinvokes.cc @@ -3,7 +3,6 @@ #include "android-system.hh" #include "globals.hh" #include "internal-pinvokes.hh" -#include "jni-remapping.hh" using namespace xamarin::android; using namespace xamarin::android::internal; @@ -276,16 +275,3 @@ monodroid_get_dylib () { return nullptr; } - -const char* -_monodroid_lookup_replacement_type (const char *jniSimpleReference) -{ - return JniRemapping::lookup_replacement_type (jniSimpleReference); -} - -const JniRemappingReplacementMethod* -_monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) -{ - return JniRemapping::lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature); -} - diff --git a/src/native/mono/monodroid/jni-remapping.cc b/src/native/mono/monodroid/jni-remapping.cc deleted file mode 100644 index 4122c8fe68a..00000000000 --- a/src/native/mono/monodroid/jni-remapping.cc +++ /dev/null @@ -1,98 +0,0 @@ -#include - -#include "logger.hh" -#include "jni-remapping.hh" -#include "xamarin-app.hh" - -using namespace xamarin::android::internal; - -[[gnu::always_inline]] bool -JniRemapping::equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept -{ - if (left.length != static_cast(right_len) || left.str[0] != *right) { - return false; - } - - if (memcmp (left.str, right, right_len) == 0) { - return true; - } - - return false; -} - -const char* -JniRemapping::lookup_replacement_type (const char *jniSimpleReference) noexcept -{ - if (application_config.jni_remapping_replacement_type_count == 0 || jniSimpleReference == nullptr || *jniSimpleReference == '\0') { - return nullptr; - } - - size_t ref_len = strlen (jniSimpleReference); - for (size_t i = 0uz; i < application_config.jni_remapping_replacement_type_count; i++) { - JniRemappingTypeReplacementEntry const& entry = jni_remapping_type_replacements[i]; - - if (equal (entry.name, jniSimpleReference, ref_len)) { - return entry.replacement; - } - } - - return nullptr; -} - -const JniRemappingReplacementMethod* -JniRemapping::lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept -{ - if (application_config.jni_remapping_replacement_method_index_entry_count == 0 || - jniSourceType == nullptr || *jniSourceType == '\0' || - jniMethodName == nullptr || *jniMethodName == '\0') { - return nullptr; - } - - size_t source_type_len = strlen (jniSourceType); - - const JniRemappingIndexTypeEntry *type = nullptr; - for (size_t i = 0uz; i < application_config.jni_remapping_replacement_method_index_entry_count; i++) { - JniRemappingIndexTypeEntry const& entry = jni_remapping_method_replacement_index[i]; - - if (!equal (entry.name, jniSourceType, source_type_len)) { - continue; - } - - type = &jni_remapping_method_replacement_index[i]; - break; - } - - if (type == nullptr || type->method_count == 0 || type->methods == nullptr) { - return nullptr; - } - - size_t method_name_len = strlen (jniMethodName); - size_t signature_len = jniMethodSignature == nullptr ? 0uz : strlen (jniMethodSignature); - - for (size_t i = 0uz; i < type->method_count; i++) { - JniRemappingIndexMethodEntry const& entry = type->methods[i]; - - if (!equal (entry.name, jniMethodName, method_name_len)) { - continue; - } - - if (entry.signature.length == 0 || equal (entry.signature, jniMethodSignature, signature_len)) { - return &type->methods[i].replacement; - } - - const char *sig_end = jniMethodSignature + signature_len; - if (*sig_end == ')') { - continue; - } - - while (sig_end != jniMethodSignature && *sig_end != ')') { - sig_end--; - } - - if (equal (entry.signature, jniMethodSignature, static_cast(sig_end - jniMethodSignature) + 1uz)) { - return &type->methods[i].replacement; - } - } - - return nullptr; -} diff --git a/src/native/mono/monodroid/jni-remapping.hh b/src/native/mono/monodroid/jni-remapping.hh deleted file mode 100644 index e76f89e78ff..00000000000 --- a/src/native/mono/monodroid/jni-remapping.hh +++ /dev/null @@ -1,21 +0,0 @@ -#if !defined (__JNI_REMAPPING_HH) -#define __JNI_REMAPPING_HH - -#include - -#include "xamarin-app.hh" - -namespace xamarin::android::internal -{ - class JniRemapping final - { - public: - static const char* lookup_replacement_type (const char *jniSimpleReference) noexcept; - static const JniRemappingReplacementMethod* lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept; - - private: - [[gnu::nonnull (2)]] - static bool equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept; - }; -} -#endif diff --git a/src/native/mono/monodroid/monodroid-glue.cc b/src/native/mono/monodroid/monodroid-glue.cc index 4a5f9e38c40..20c9a24b188 100644 --- a/src/native/mono/monodroid/monodroid-glue.cc +++ b/src/native/mono/monodroid/monodroid-glue.cc @@ -826,7 +826,7 @@ MonodroidRuntime::init_android_runtime (JNIEnv *env, jclass runtimeClass, jobjec init.packageNamingPolicy = static_cast(application_config.package_naming_policy); init.boundExceptionType = application_config.bound_exception_type; init.jniAddNativeMethodRegistrationAttributePresent = application_config.jni_add_native_method_registration_attribute_present ? 1 : 0; - init.jniRemappingInUse = application_config.jni_remapping_replacement_type_count > 0 || application_config.jni_remapping_replacement_method_index_entry_count > 0; + init.jniRemappingData = &jni_remapping_data; init.marshalMethodsEnabled = application_config.marshal_methods_enabled; java_System_identityHashCode = env->GetStaticMethodID (java_System, "identityHashCode", "(Ljava/lang/Object;)I"); diff --git a/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc b/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc index dc6570c2a23..69d6a196cc3 100644 --- a/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc +++ b/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc @@ -51,8 +51,6 @@ const std::vector internal_pinvoke_names = { "_monodroid_gref_log_delete", "_monodroid_gref_log_new", "monodroid_log", - "_monodroid_lookup_replacement_type", - "_monodroid_lookup_replacement_method_info", "_monodroid_lref_log_delete", "_monodroid_lref_log_new", "_monodroid_max_gref_get", diff --git a/src/native/mono/pinvoke-override/pinvoke-tables.include b/src/native/mono/pinvoke-override/pinvoke-tables.include index e26bde46029..173b4d4f347 100644 --- a/src/native/mono/pinvoke-override/pinvoke-tables.include +++ b/src/native/mono/pinvoke-override/pinvoke-tables.include @@ -11,12 +11,11 @@ namespace { #if INTPTR_MAX == INT64_MAX //64-bit internal p/invoke table - std::array internal_pinvokes {{ + std::array internal_pinvokes {{ {0x2b3b0ca1d14076da, "monodroid_get_dylib", reinterpret_cast(&monodroid_get_dylib)}, {0x37307e5fddf709dc, "_monodroid_weak_gref_dec", reinterpret_cast(&_monodroid_weak_gref_dec)}, {0x3b2467e7eadd4a6a, "_monodroid_lref_log_new", reinterpret_cast(&_monodroid_lref_log_new)}, {0x3c5532ecdab53f89, "set_world_accessable", reinterpret_cast(&set_world_accessable)}, - {0x423c8f539a2c56d2, "_monodroid_lookup_replacement_type", reinterpret_cast(&_monodroid_lookup_replacement_type)}, {0x4b1956138764939a, "_monodroid_gref_log_new", reinterpret_cast(&_monodroid_gref_log_new)}, {0x4d5b5b488f736058, "path_combine", reinterpret_cast(&path_combine)}, {0x5a2614d15e2fdc2e, "monodroid_strdup_printf", reinterpret_cast(&monodroid_strdup_printf)}, @@ -35,7 +34,6 @@ namespace { {0xb9bae9c43fb05089, "xamarin_app_init", reinterpret_cast(&xamarin_app_init)}, {0xbe5a300beec69c35, "monodroid_get_system_property", reinterpret_cast(&monodroid_get_system_property)}, {0xbfbb924fbe190616, "monodroid_dylib_mono_free", reinterpret_cast(&monodroid_dylib_mono_free)}, - {0xc2a21d3f6c8ccc24, "_monodroid_lookup_replacement_method_info", reinterpret_cast(&_monodroid_lookup_replacement_method_info)}, {0xc5b4690e13898fa3, "monodroid_timing_start", reinterpret_cast(&monodroid_timing_start)}, {0xcaab0a3ab6057bbd, "_monodroid_gref_inc", reinterpret_cast(&_monodroid_gref_inc)}, {0xcc873ea8493d1dd5, "monodroid_embedded_assemblies_set_assemblies_prefix", reinterpret_cast(&monodroid_embedded_assemblies_set_assemblies_prefix)}, @@ -576,14 +574,13 @@ constexpr hash_t system_security_cryptography_native_android_library_hash = 0x18 constexpr hash_t system_globalization_native_library_hash = 0x28b5c8fca080abd5; #else //32-bit internal p/invoke table - std::array internal_pinvokes {{ + std::array internal_pinvokes {{ {0xb7a486a, "monodroid_TypeManager_get_java_class_name", reinterpret_cast(&monodroid_TypeManager_get_java_class_name)}, {0xf562bd9, "monodroid_embedded_assemblies_set_assemblies_prefix", reinterpret_cast(&monodroid_embedded_assemblies_set_assemblies_prefix)}, {0x1bef8dce, "_monodroid_gref_inc", reinterpret_cast(&_monodroid_gref_inc)}, {0x1f1e0ee9, "_monodroid_gref_dec", reinterpret_cast(&_monodroid_gref_dec)}, {0x2aea7c33, "_monodroid_max_gref_get", reinterpret_cast(&_monodroid_max_gref_get)}, {0x3227d81a, "monodroid_timing_start", reinterpret_cast(&monodroid_timing_start)}, - {0x333d4835, "_monodroid_lookup_replacement_method_info", reinterpret_cast(&_monodroid_lookup_replacement_method_info)}, {0x395808e5, "monodroid_dylib_mono_free", reinterpret_cast(&monodroid_dylib_mono_free)}, {0x42b41fe4, "send_uninterrupted", reinterpret_cast(&send_uninterrupted)}, {0x4b58e0da, "monodroid_get_dylib", reinterpret_cast(&monodroid_get_dylib)}, @@ -601,7 +598,6 @@ constexpr hash_t system_globalization_native_library_hash = 0x28b5c8fca080abd5; {0xb02468aa, "_monodroid_gref_get", reinterpret_cast(&_monodroid_gref_get)}, {0xbe8d7701, "_monodroid_gref_log_new", reinterpret_cast(&_monodroid_gref_log_new)}, {0xc0d097a7, "_monodroid_lref_log_new", reinterpret_cast(&_monodroid_lref_log_new)}, - {0xc439b5d7, "_monodroid_lookup_replacement_type", reinterpret_cast(&_monodroid_lookup_replacement_type)}, {0xc5146c54, "_monodroid_gref_log_delete", reinterpret_cast(&_monodroid_gref_log_delete)}, {0xc58eafa5, "java_interop_free", reinterpret_cast(&java_interop_free)}, {0xd91f3619, "create_public_directory", reinterpret_cast(&create_public_directory)}, @@ -1141,6 +1137,6 @@ constexpr hash_t system_security_cryptography_native_android_library_hash = 0x93 constexpr hash_t system_globalization_native_library_hash = 0xa66f1e5a; #endif -constexpr size_t internal_pinvokes_count = 39; +constexpr size_t internal_pinvokes_count = 37; constexpr size_t dotnet_pinvokes_count = 510; } // end of anonymous namespace diff --git a/src/native/mono/runtime-base/internal-pinvokes.hh b/src/native/mono/runtime-base/internal-pinvokes.hh index bff70b0e9fa..005a9c53afe 100644 --- a/src/native/mono/runtime-base/internal-pinvokes.hh +++ b/src/native/mono/runtime-base/internal-pinvokes.hh @@ -44,6 +44,4 @@ void* monodroid_dylib_mono_new ([[maybe_unused]] const char *libmono_path); void monodroid_dylib_mono_free ([[maybe_unused]] void *mono_imports); int monodroid_dylib_mono_init (void *mono_imports, [[maybe_unused]] const char *libmono_path); void* monodroid_get_dylib (); -const char* _monodroid_lookup_replacement_type (const char *jniSimpleReference); -const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature); void _monodroid_detect_cpu_and_architecture (unsigned short *built_for_cpu, unsigned short *running_on_cpu, unsigned char *is64bit); diff --git a/src/native/mono/xamarin-app-stub/application_dso_stub.cc b/src/native/mono/xamarin-app-stub/application_dso_stub.cc index 6ed48fac62c..7a8d7940432 100644 --- a/src/native/mono/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/mono/xamarin-app-stub/application_dso_stub.cc @@ -1,6 +1,7 @@ #include #include +#include #include "xamarin-app.hh" #include @@ -248,6 +249,7 @@ static const JniRemappingIndexMethodEntry some_java_type_one_methods[] = { .replacement = { .target_type = "some/java/target_type_one", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = false, } }, @@ -268,6 +270,7 @@ static const JniRemappingIndexMethodEntry some_java_type_two_methods[] = { .replacement = { .target_type = "some/java/target_type_two", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = true, } }, @@ -310,3 +313,14 @@ const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[] = { .replacement = "another/replacement/java/type", }, }; + +extern "C" const xamarin::android::JniRemappingData jni_remapping_data { + .type_replacements = jni_remapping_type_replacements, + .reverse_type_replacements = nullptr, + .method_replacement_index = jni_remapping_method_replacement_index, + .field_replacement_index = nullptr, + .type_replacement_count = 2, + .reverse_type_replacement_count = 0, + .method_replacement_index_count = 2, + .field_replacement_index_count = 0, +}; diff --git a/src/native/mono/xamarin-app-stub/xamarin-app.hh b/src/native/mono/xamarin-app-stub/xamarin-app.hh index d2dacdd9263..fb8c94c68c3 100644 --- a/src/native/mono/xamarin-app-stub/xamarin-app.hh +++ b/src/native/mono/xamarin-app-stub/xamarin-app.hh @@ -283,8 +283,7 @@ struct JniRemappingReplacementMethod { const char *target_type; const char *target_name; - // const char *target_signature; - // const int32_t param_count; + const char *target_signature; const bool is_static; }; diff --git a/src/native/nativeaot/host/internal-pinvoke-stubs.cc b/src/native/nativeaot/host/internal-pinvoke-stubs.cc index 1e7dd83833b..f46f8f8944f 100644 --- a/src/native/nativeaot/host/internal-pinvoke-stubs.cc +++ b/src/native/nativeaot/host/internal-pinvoke-stubs.cc @@ -32,19 +32,6 @@ bool clr_typemap_java_to_managed ( pinvoke_unreachable (); } -const char* _monodroid_lookup_replacement_type ([[maybe_unused]] const char *jniSimpleReference) -{ - pinvoke_unreachable (); -} - -const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info ( - [[maybe_unused]] const char *jniSourceType, - [[maybe_unused]] const char *jniMethodName, - [[maybe_unused]] const char *jniMethodSignature) -{ - pinvoke_unreachable (); -} - managed_timing_sequence* monodroid_timing_start ([[maybe_unused]] const char *message) { pinvoke_unreachable (); diff --git a/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh b/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh index 60ff24596fc..14d25414941 100644 --- a/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh +++ b/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh @@ -26,8 +26,6 @@ extern "C" { void monodroid_log (xamarin::android::LogLevel level, LogCategories category, const char *message) noexcept; char* monodroid_TypeManager_get_java_class_name (jclass klass) noexcept; void monodroid_free (void *ptr) noexcept; - const char* _monodroid_lookup_replacement_type (const char *jniSimpleReference); - const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature); xamarin::android::managed_timing_sequence* monodroid_timing_start (const char *message); void monodroid_timing_stop (xamarin::android::managed_timing_sequence *sequence, const char *message); diff --git a/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..3d4f82b0833 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JniRemappingLookupTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Runtime.InteropServices; + +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 ("\ue000/net/dot/android/remap/After")] + public void ReplacementTypeLookupReturnsNullOutsideTable (string source) + { + Assert.IsNull (JniEnvironment.Runtime.TypeManager.GetReplacementType (source)); + } + + [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); + } + + [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); + } +} 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 698976ce66c..5e47d4b2519 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 @@ -146,6 +146,7 @@ + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml index 643106c7d20..d3b04627482 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml @@ -5,6 +5,18 @@ + + + + + + + + +