From 041f0667bcfffeb5fb3949df2add7c49156d3240 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Fri, 29 Mar 2024 13:49:58 -0400 Subject: [PATCH] [Java.Interop] Add JniMemberInfoLookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: c6c487b62dab4ffec45e61b09dd43afc89898caf Context: 312fbf439ed874bb5f4f25ee6d2c9a2b3c2f5a8b Context: 2197579478152fbc815eb15195977f808cd6bde4 Context: https://github.com/xamarin/xamarin-android/issues/7276 There is a desire to remove the "marshal-ilgen" component from .NET Android, which is responsible for all non-blittable type marshaling within P/Invoke (and related) invocations. The largest source of such non-blittable parameter marshaling was with string marshaling: `JNIEnv::GetFieldID()` was "wrapped" by `java_interop_jnienv_get_field_id`: JI_API jfieldID java_interop_jnienv_get_field_id (JNIEnv *env, jthrowable *_thrown, jclass type, const char* name, const char* signature); which was P/Invoked within `JniEnvironment.g.cs`: partial class NativeMethods { internal static extern unsafe IntPtr java_interop_jnienv_get_field_id (IntPtr jnienv, out IntPtr thrown, jobject type, string name, string signature); } and `string` parameter marshaling is *not* blittable. Turns outâ„¢ that this particular usage of non-blittable parameter marshaling was fixed and rendered moot by: * 312fbf43: C#9 function pointer backend for `JNIEnv` invocations * c6c487b6: "Standalone" build config to use C#9 function pointers * 21975794: Standalone build config is now the default That said, this code path felt slightly less than ideal: the "top-level abstraction" for member lookups is an "encoded member", a string containing the name of the member, a `.`, and the JNI signature of the member, e.g.: _members.InstanceFields.GetBooleanValue("propogateFinallyBlockExecuted.Z", this) The "encoded member" would need to be split on `.`, and with c6c487b6 the name and signature would be separately passed to `Marshal.StringToCoTaskMemUTF8()`, which performs a memory allocation and converts the UTF-16 string to UTF-8. Meanwhile, [C# 11 introduced UTF-8 string literals][0], which allows the compiler to deal with UTF-8 conversion and memory allocation. Enter `JniMemberInfoLookup``: public ref struct JniMemberInfoLookup { public string EncodedMember {get;} public ReadOnlySpan MemberName {get;} public ReadOnlySpan MemberSignature {get;} public JniMemberInfoLookup (string encodedMember, ReadOnlySpan memberName, ReadOnlySpan memberSignature); } `JniMemberInfoLookup` removes the need to call `Marshal.StringToCoTaskMemUTF8()` entirely, at the cost of a more complicated member invocation: // Old and busted: bool value = _members.InstanceFields.GetBooleanValue("propogateFinallyBlockExecuted.Z", this); // Eventual new hawtness: var lookup = new JniMemberInfoLookup ( "propogateFinallyBlockExecuted.Z", "propogateFinallyBlockExecuted"u8, "Z"u8); bool value = _members.InstanceFields.GetBooleanValue(lookup, this); Is It Worth Itâ„¢? *Maybe*; see the new `JniFieldLookupTiming.FieldLookupTiming()` test, which allocates a new `JniPeerMembers` instance and invoke `members.InstanceFields.GetFieldInfo(string)` and `members.InstanceFields.GetFieldInfo(JniMemberInfoLookup)`. (A new `JniPeerMembers` instance is required because `GetFieldInfo()` caches the field lookup.) Using `JniMemberInfoLookup` is about 4% faster. # FieldLookupTiming Timing: looking up JavaTiming.instanceIntField 10000 times # .InstanceMethods.GetFieldInfo(string): 00:00:02.2780667 # .InstanceMethods.GetFieldInfo(JniMemberInfoLookup): 00:00:02.2016146 I'm not sure if this is *actually* worth it, especially as this will imply an increase in code size. TODO: * Update `JniPeerMembers.*.cs` to use `JniMemberInfoLookup`, so that e.g. the above `_members.InstanceFields.GetBooleanValue()` overload exists. * `generator` changes to use `JniMemberInfoLookup` [0]: https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-11#utf-8-string-literals --- src/Java.Interop/GlobalSuppressions.cs | 4 + src/Java.Interop/Java.Interop.csproj | 2 +- .../JniEnvironment.InstanceFields.cs | 37 ++++++++ .../JniEnvironment.InstanceMethods.cs | 37 ++++++++ .../JniEnvironment.StaticFields.cs | 38 ++++++++ .../JniEnvironment.StaticMethods.cs | 82 +++++++++++++++++ .../Java.Interop/JniMemberInfoLookup.cs | 16 ++++ .../JniPeerMembers.JniInstanceFields.cs | 11 +++ .../JniPeerMembers.JniInstanceMethods.cs | 42 +++++++++ .../JniPeerMembers.JniStaticFields.cs | 11 +++ .../JniPeerMembers.JniStaticMethods.cs | 65 ++++++++++++++ .../Java.Interop/JniRuntime.JniTypeManager.cs | 16 ++++ src/Java.Interop/Java.Interop/JniType.cs | 88 +++++++++++++++++++ .../Java.Interop/TimingTests.cs | 31 +++++++ .../interop/performance/JavaTiming.java | 2 + .../Java.Interop/JniTypeTest.cs | 16 ++-- 16 files changed, 489 insertions(+), 9 deletions(-) create mode 100644 src/Java.Interop/Java.Interop/JniEnvironment.InstanceFields.cs create mode 100644 src/Java.Interop/Java.Interop/JniEnvironment.InstanceMethods.cs create mode 100644 src/Java.Interop/Java.Interop/JniEnvironment.StaticFields.cs create mode 100644 src/Java.Interop/Java.Interop/JniEnvironment.StaticMethods.cs create mode 100644 src/Java.Interop/Java.Interop/JniMemberInfoLookup.cs diff --git a/src/Java.Interop/GlobalSuppressions.cs b/src/Java.Interop/GlobalSuppressions.cs index e5cc3b533..e1efbc7da 100644 --- a/src/Java.Interop/GlobalSuppressions.cs +++ b/src/Java.Interop/GlobalSuppressions.cs @@ -19,6 +19,10 @@ // See: 045b8af7, 6a42bb89, f60906cf, e10f7cb0, etc. [assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniEnvironment.Exceptions")] +[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniEnvironment.InstanceFields")] +[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniEnvironment.InstanceMethods")] +[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniEnvironment.StaticFields")] +[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniEnvironment.StaticMethods")] [assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniPeerMembers.JniStaticMethods")] [assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniRuntime.JniMarshalMemberBuilder")] [assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`.", Scope = "type", Target = "~T:Java.Interop.JniPeerMembers.JniStaticFields")] diff --git a/src/Java.Interop/Java.Interop.csproj b/src/Java.Interop/Java.Interop.csproj index 1c4f0c681..0b90eb370 100644 --- a/src/Java.Interop/Java.Interop.csproj +++ b/src/Java.Interop/Java.Interop.csproj @@ -29,7 +29,7 @@ $(ToolOutputFullPath) $(ToolOutputFullPath)Java.Interop.xml $(BuildToolOutputFullPath) - 9.0 + 12.0 8.0 $(JICoreLibVersion) true diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.InstanceFields.cs b/src/Java.Interop/Java.Interop/JniEnvironment.InstanceFields.cs new file mode 100644 index 000000000..2245069c3 --- /dev/null +++ b/src/Java.Interop/Java.Interop/JniEnvironment.InstanceFields.cs @@ -0,0 +1,37 @@ +using System; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; + +namespace Java.Interop; + +partial class JniEnvironment { + partial class InstanceFields { + public static unsafe JniFieldInfo GetFieldID (JniObjectReference type, ReadOnlySpan name, ReadOnlySpan signature) + { + if (!type.IsValid) + throw new ArgumentException ("Handle must be valid.", "type"); + + IntPtr env = JniEnvironment.EnvironmentPointer; + IntPtr field; + IntPtr thrown; + fixed (void* name_ptr = &MemoryMarshal.GetReference (name)) + fixed (void* signature_ptr = &MemoryMarshal.GetReference (signature)) { + field = JniNativeMethods.GetFieldID (env, type.Handle, (IntPtr) name_ptr, (IntPtr) signature_ptr); + thrown = JniNativeMethods.ExceptionOccurred (env); + } + + Exception? __e = JniEnvironment.GetExceptionForLastThrowable (thrown); + if (__e != null) + ExceptionDispatchInfo.Capture (__e).Throw (); + + if (field == IntPtr.Zero) + throw new InvalidOperationException ("Should not be reached; `GetFieldID` should have thrown!"); + +#if DEBUG + return new JniFieldInfo (name.ToString (), signature.ToString (), field, isStatic: false); +#else // DEBUG + return new JniFieldInfo (null!, null!, field, isStatic: false); +#endif // DEBUG + } + } +} diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.InstanceMethods.cs b/src/Java.Interop/Java.Interop/JniEnvironment.InstanceMethods.cs new file mode 100644 index 000000000..e84ce5837 --- /dev/null +++ b/src/Java.Interop/Java.Interop/JniEnvironment.InstanceMethods.cs @@ -0,0 +1,37 @@ +using System; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; + +namespace Java.Interop; + +partial class JniEnvironment { + partial class InstanceMethods { + public static unsafe JniMethodInfo GetMethodID (JniObjectReference type, ReadOnlySpan name, ReadOnlySpan signature) + { + if (!type.IsValid) + throw new ArgumentException ("Handle must be valid.", "type"); + + IntPtr env = JniEnvironment.EnvironmentPointer; + IntPtr method; + IntPtr thrown; + fixed (void* name_ptr = &MemoryMarshal.GetReference (name)) + fixed (void* signature_ptr = &MemoryMarshal.GetReference (signature)) { + method = JniNativeMethods.GetMethodID (env, type.Handle, (IntPtr) name_ptr, (IntPtr) signature_ptr); + thrown = JniNativeMethods.ExceptionOccurred (env); + } + + Exception? __e = JniEnvironment.GetExceptionForLastThrowable (thrown); + if (__e != null) + ExceptionDispatchInfo.Capture (__e).Throw (); + + if (method == IntPtr.Zero) + throw new InvalidOperationException ("Should not be reached; `GetMethodID` should have thrown!"); + +#if DEBUG + return new JniMethodInfo (name.ToString (), signature.ToString (), method, isStatic: false); +#else // DEBUG + return new JniMethodInfo (null!, null!, method, isStatic: false); +#endif // DEBUG + } + } +} diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.StaticFields.cs b/src/Java.Interop/Java.Interop/JniEnvironment.StaticFields.cs new file mode 100644 index 000000000..0433dec28 --- /dev/null +++ b/src/Java.Interop/Java.Interop/JniEnvironment.StaticFields.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; + +namespace Java.Interop; + +partial class JniEnvironment { + partial class StaticFields { + + public static unsafe JniFieldInfo GetStaticFieldID (JniObjectReference type, ReadOnlySpan name, ReadOnlySpan signature) + { + if (!type.IsValid) + throw new ArgumentException ("Handle must be valid.", "type"); + + IntPtr env = JniEnvironment.EnvironmentPointer; + IntPtr field; + IntPtr thrown; + fixed (void* name_ptr = &MemoryMarshal.GetReference (name)) + fixed (void* signature_ptr = &MemoryMarshal.GetReference (signature)) { + field = JniNativeMethods.GetStaticFieldID (env, type.Handle, (IntPtr) name_ptr, (IntPtr) signature_ptr); + thrown = JniNativeMethods.ExceptionOccurred (env); + } + + Exception? __e = JniEnvironment.GetExceptionForLastThrowable (thrown); + if (__e != null) + ExceptionDispatchInfo.Capture (__e).Throw (); + + if (field == IntPtr.Zero) + throw new InvalidOperationException ("Should not be reached; `GetFieldID` should have thrown!"); + +#if DEBUG + return new JniFieldInfo (name.ToString (), signature.ToString (), field, isStatic: false); +#else // DEBUG + return new JniFieldInfo (null!, null!, field, isStatic: false); +#endif // DEBUG + } + } +} diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.StaticMethods.cs b/src/Java.Interop/Java.Interop/JniEnvironment.StaticMethods.cs new file mode 100644 index 000000000..a4e62d48b --- /dev/null +++ b/src/Java.Interop/Java.Interop/JniEnvironment.StaticMethods.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; + +namespace Java.Interop; + +partial class JniEnvironment { + partial class StaticMethods { + + public static unsafe JniMethodInfo GetStaticMethodID (JniObjectReference type, ReadOnlySpan name, ReadOnlySpan signature) + { + if (!type.IsValid) + throw new ArgumentException ("Handle must be valid.", "type"); + + IntPtr env = JniEnvironment.EnvironmentPointer; + IntPtr method; + IntPtr thrown; + fixed (void* name_ptr = &MemoryMarshal.GetReference (name)) + fixed (void* signature_ptr = &MemoryMarshal.GetReference (signature)) { + method = JniNativeMethods.GetStaticMethodID (env, type.Handle, (IntPtr) name_ptr, (IntPtr) signature_ptr); + thrown = JniNativeMethods.ExceptionOccurred (env); + } + + Exception? __e = JniEnvironment.GetExceptionForLastThrowable (thrown); + if (__e != null) + ExceptionDispatchInfo.Capture (__e).Throw (); + + if (method == IntPtr.Zero) + throw new InvalidOperationException ("Should not be reached; `GetStaticMethodID` should have thrown!"); + +#if DEBUG + return new JniMethodInfo (name.ToString (), signature.ToString (), method, isStatic: true); +#else // DEBUG + return new JniMethodInfo (null!, null!, method, isStatic: true); +#endif // DEBUG + } + + internal static unsafe bool TryGetStaticMethod ( + JniObjectReference type, + ReadOnlySpan name, + ReadOnlySpan signature, + [NotNullWhen(true)] + out JniMethodInfo? method) + { + method = null; + + if (!type.IsValid) + throw new ArgumentException ("Handle must be valid.", "type"); + + IntPtr env = JniEnvironment.EnvironmentPointer; + IntPtr id; + IntPtr thrown; + fixed (void* name_ptr = &MemoryMarshal.GetReference (name)) + fixed (void* signature_ptr = &MemoryMarshal.GetReference (signature)) { + id = JniNativeMethods.GetStaticMethodID (env, type.Handle, (IntPtr) name_ptr, (IntPtr) signature_ptr); + thrown = JniNativeMethods.ExceptionOccurred (env); + } + + if (thrown != IntPtr.Zero) { + JniNativeMethods.ExceptionClear (env); + JniEnvironment.References.RawDeleteLocalRef (env, thrown); + thrown = IntPtr.Zero; + return false; + } + + Debug.Assert (id != IntPtr.Zero); + if (id == IntPtr.Zero) { + return false; + } + +#if DEBUG + method = new JniMethodInfo (name.ToString (), signature.ToString (), id, isStatic: true); +#else // DEBUG + method = new JniMethodInfo (null!, null!, id, isStatic: true); +#endif // DEBUG + + return true; + } + } +} diff --git a/src/Java.Interop/Java.Interop/JniMemberInfoLookup.cs b/src/Java.Interop/Java.Interop/JniMemberInfoLookup.cs new file mode 100644 index 000000000..c2e019141 --- /dev/null +++ b/src/Java.Interop/Java.Interop/JniMemberInfoLookup.cs @@ -0,0 +1,16 @@ +using System; + +namespace Java.Interop; + +public ref struct JniMemberInfoLookup { + public string EncodedMember {get; private set;} + public ReadOnlySpan MemberName {get; private set;} + public ReadOnlySpan MemberSignature {get; private set;} + + public JniMemberInfoLookup (string encodedMember, ReadOnlySpan memberName, ReadOnlySpan memberSignature) + { + EncodedMember = encodedMember; + MemberName = memberName; + MemberSignature = memberSignature; + } +} diff --git a/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs b/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs index e0a5ae40d..48f6f9e16 100644 --- a/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs +++ b/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs @@ -34,6 +34,17 @@ public JniFieldInfo GetFieldInfo (string encodedMember) return f; } } + + public JniFieldInfo GetFieldInfo (JniMemberInfoLookup member) + { + lock (InstanceFields) { + if (!InstanceFields.TryGetValue (member.EncodedMember, out var f)) { + f = Members.JniPeerType.GetInstanceField (member.MemberName, member.MemberSignature); + InstanceFields.Add (member.EncodedMember, f); + } + return f; + } + } }} } diff --git a/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs b/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs index 04d96870e..dbf55b000 100644 --- a/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs +++ b/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs @@ -157,6 +157,48 @@ JniMethodInfo GetMethodInfo (string method, string signature) return JniPeerType.GetInstanceMethod (method, signature); } + + public JniMethodInfo GetMethodInfo (JniMemberInfoLookup member) + { + lock (InstanceMethods) { + if (InstanceMethods.TryGetValue (member.EncodedMember, out var m)) { + return m; + } + } + var info = GetMethodInfo (member.MemberName, member.MemberSignature); + lock (InstanceMethods) { + if (InstanceMethods.TryGetValue (member.EncodedMember, out var m)) { + return m; + } + InstanceMethods.Add (member.EncodedMember, info); + } + return info; + } + + JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signature) + { + var m = (JniMethodInfo?) null; + var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature); + if (newMethod.HasValue) { + var typeName = newMethod.Value.TargetJniType ?? Members.JniPeerTypeName; + var methodName = newMethod.Value.TargetJniMethodName ?? method.ToString (); + var methodSig = newMethod.Value.TargetJniMethodSignature ?? signature.ToString (); + + using var t = new JniType (typeName); + if (newMethod.Value.TargetJniMethodInstanceToStatic && + t.TryGetStaticMethod (methodName, methodSig, out m)) { + m.ParameterCount = newMethod.Value.TargetJniMethodParameterCount; + m.StaticRedirect = new JniType (typeName); + return m; + } + if (t.TryGetInstanceMethod (methodName, methodSig, out m)) { + return m; + } + Console.Error.WriteLine ($"warning: For declared method `{Members.JniPeerTypeName}.{method.ToString ()}.{signature.ToString ()}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!"); + } + return JniPeerType.GetInstanceMethod (method, signature); + } + public unsafe JniObjectReference StartCreateInstance (string constructorSignature, Type declaringType, JniArgumentValue* parameters) { if (constructorSignature == null) diff --git a/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs b/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs index 7fe131b62..5a2626cb4 100644 --- a/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs +++ b/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs @@ -30,6 +30,17 @@ public JniFieldInfo GetFieldInfo (string encodedMember) } } + public JniFieldInfo GetFieldInfo (JniMemberInfoLookup member) + { + lock (StaticFields) { + if (!StaticFields.TryGetValue (member.EncodedMember, out var f)) { + f = Members.JniPeerType.GetInstanceField (member.MemberName, member.MemberSignature); + StaticFields.Add (member.EncodedMember, f); + } + return f; + } + } + internal void Dispose () { StaticFields.Clear (); diff --git a/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs b/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs index 9ce77fbe3..84123874f 100644 --- a/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs +++ b/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs @@ -66,6 +66,46 @@ JniMethodInfo GetMethodInfo (string method, string signature) return Members.JniPeerType.GetStaticMethod (method, signature); } + public JniMethodInfo GetMethodInfo (JniMemberInfoLookup member) + { + lock (StaticMethods) { + if (StaticMethods.TryGetValue (member.EncodedMember, out var m)) { + return m; + } + } + var info = GetMethodInfo (member.MemberName, member.MemberSignature); + lock (StaticMethods) { + if (StaticMethods.TryGetValue (member.EncodedMember, out var m)) { + return m; + } + StaticMethods.Add (member.EncodedMember, info); + } + return info; + } + + JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signature) + { + var m = (JniMethodInfo?) null; + var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature); + if (newMethod.HasValue) { + using var t = new JniType (newMethod.Value.TargetJniType ?? Members.JniPeerTypeName); + if (t.TryGetStaticMethod ( + newMethod.Value.TargetJniMethodName ?? method.ToString (), + newMethod.Value.TargetJniMethodSignature ?? signature.ToString (), + out m)) { + return m; + } + } + if (Members.JniPeerType.TryGetStaticMethod (method, signature, out m)) { + return m; + } + m = FindInFallbackTypes (method, signature); + if (m != null) { + return m; + } + return Members.JniPeerType.GetStaticMethod (method, signature); + } + #pragma warning disable CA1801 JniType GetMethodDeclaringType (JniMethodInfo method) { @@ -105,6 +145,31 @@ JniType GetMethodDeclaringType (JniMethodInfo method) } #endif // NET + JniMethodInfo? FindInFallbackTypes (ReadOnlySpan method, ReadOnlySpan signature) + { + var fallbackTypes = JniEnvironment.Runtime.TypeManager.GetStaticMethodFallbackTypes (Members.JniPeerTypeName); + if (fallbackTypes == null) { + return null; + } + foreach (var ft in fallbackTypes) { + JniType? t = null; + try { + if (!JniType.TryParse (ft, out t)) { + continue; + } + if (t.TryGetStaticMethod (method, signature, out var m)) { + m.StaticRedirect = t; + t = null; + return m; + } + } + finally { + t?.Dispose (); + } + } + return null; + } + public unsafe void InvokeVoidMethod (string encodedMember, JniArgumentValue* parameters) { var m = GetMethodInfo (encodedMember); diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs index 7d5584f9f..a1c30e02c 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs @@ -423,6 +423,22 @@ IEnumerable CreateGetTypesForSimpleReferenceEnumerator (string jniSimpleRe protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, string jniMethodName, string jniMethodSignature) => null; + public ReplacementMethodInfo? GetReplacementMethodInfo (string jniSimpleReference, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) + { + AssertValid (); + AssertSimpleReference (jniSimpleReference, nameof (jniSimpleReference)); + if (jniMethodName.IsEmpty) { + throw new ArgumentNullException (nameof (jniMethodName)); + } + if (jniMethodSignature.IsEmpty) { + throw new ArgumentNullException (nameof (jniMethodSignature)); + } + + return GetReplacementMethodInfoCore (jniSimpleReference, jniMethodName, jniMethodSignature); + } + + protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) => null; + public virtual void RegisterNativeMembers ( JniType nativeClass, [DynamicallyAccessedMembers (MethodsAndPrivateNested)] diff --git a/src/Java.Interop/Java.Interop/JniType.cs b/src/Java.Interop/Java.Interop/JniType.cs index 396f5067b..f02d32936 100644 --- a/src/Java.Interop/Java.Interop/JniType.cs +++ b/src/Java.Interop/Java.Interop/JniType.cs @@ -210,6 +210,13 @@ public JniFieldInfo GetInstanceField (string name, string signature) return JniEnvironment.InstanceFields.GetFieldID (PeerReference, name, signature); } + public JniFieldInfo GetInstanceField (ReadOnlySpan name, ReadOnlySpan signature) + { + AssertValid (); + + return JniEnvironment.InstanceFields.GetFieldID (PeerReference, name, signature); + } + public JniFieldInfo GetCachedInstanceField ([NotNull] ref JniFieldInfo? cachedField, string name, string signature) { AssertValid (); @@ -223,6 +230,19 @@ public JniFieldInfo GetCachedInstanceField ([NotNull] ref JniFieldInfo? cachedFi return cachedField; } + public JniFieldInfo GetCachedInstanceField ([NotNull] ref JniFieldInfo? cachedField, ReadOnlySpan name, ReadOnlySpan signature) + { + AssertValid (); + + if (cachedField != null && cachedField.IsValid) + return cachedField; + var m = GetInstanceField (name, signature); + if (Interlocked.CompareExchange (ref cachedField, m, null) != null) { + // No cleanup required; let the GC collect the unused instance + } + return cachedField; + } + public JniFieldInfo GetStaticField (string name, string signature) { AssertValid (); @@ -230,6 +250,14 @@ public JniFieldInfo GetStaticField (string name, string signature) return JniEnvironment.StaticFields.GetStaticFieldID (PeerReference, name, signature); } + + public JniFieldInfo GetStaticField (ReadOnlySpan name, ReadOnlySpan signature) + { + AssertValid (); + + return JniEnvironment.StaticFields.GetStaticFieldID (PeerReference, name, signature); + } + public JniFieldInfo GetCachedStaticField ([NotNull] ref JniFieldInfo? cachedField, string name, string signature) { AssertValid (); @@ -243,6 +271,19 @@ public JniFieldInfo GetCachedStaticField ([NotNull] ref JniFieldInfo? cachedFiel return cachedField; } + public JniFieldInfo GetCachedStaticField ([NotNull] ref JniFieldInfo? cachedField, ReadOnlySpan name, ReadOnlySpan signature) + { + AssertValid (); + + if (cachedField != null && cachedField.IsValid) + return cachedField; + var m = GetStaticField (name, signature); + if (Interlocked.CompareExchange (ref cachedField, m, null) != null) { + // No cleanup required; let the GC collect the unused instance + } + return cachedField; + } + public JniMethodInfo GetInstanceMethod (string name, string signature) { AssertValid (); @@ -250,6 +291,13 @@ public JniMethodInfo GetInstanceMethod (string name, string signature) return JniEnvironment.InstanceMethods.GetMethodID (PeerReference, name, signature); } + public JniMethodInfo GetInstanceMethod (ReadOnlySpan name, ReadOnlySpan signature) + { + AssertValid (); + + return JniEnvironment.InstanceMethods.GetMethodID (PeerReference, name, signature); + } + #if NET internal bool TryGetInstanceMethod (string name, string signature, [NotNullWhen(true)] out JniMethodInfo? method) { @@ -308,6 +356,19 @@ public JniMethodInfo GetCachedInstanceMethod ([NotNull] ref JniMethodInfo? cache return cachedMethod; } + public JniMethodInfo GetCachedInstanceMethod ([NotNull] ref JniMethodInfo? cachedMethod, ReadOnlySpan name, ReadOnlySpan signature) + { + AssertValid (); + + if (cachedMethod != null && cachedMethod.IsValid) + return cachedMethod; + var m = GetInstanceMethod (name, signature); + if (Interlocked.CompareExchange (ref cachedMethod, m, null) != null) { + // No cleanup required; let the GC collect the unused instance + } + return cachedMethod; + } + public JniMethodInfo GetStaticMethod (string name, string signature) { AssertValid (); @@ -315,6 +376,13 @@ public JniMethodInfo GetStaticMethod (string name, string signature) return JniEnvironment.StaticMethods.GetStaticMethodID (PeerReference, name, signature); } + + public JniMethodInfo GetStaticMethod (ReadOnlySpan name, ReadOnlySpan signature) + { + AssertValid (); + + return JniEnvironment.StaticMethods.GetStaticMethodID (PeerReference, name, signature); + } #if NET internal bool TryGetStaticMethod (string name, string signature, [NotNullWhen(true)] out JniMethodInfo? method) { @@ -360,6 +428,13 @@ IntPtr RawGetStaticMethodID (IntPtr env, string name, string signature, out IntP } #endif // NET + internal bool TryGetStaticMethod (ReadOnlySpan name, ReadOnlySpan signature, [NotNullWhen(true)] out JniMethodInfo? method) + { + AssertValid (); + + return JniEnvironment.StaticMethods.TryGetStaticMethod (PeerReference, name, signature, out method); + } + public JniMethodInfo GetCachedStaticMethod ([NotNull] ref JniMethodInfo? cachedMethod, string name, string signature) { AssertValid (); @@ -372,5 +447,18 @@ public JniMethodInfo GetCachedStaticMethod ([NotNull] ref JniMethodInfo? cachedM } return cachedMethod; } + + public JniMethodInfo GetCachedStaticMethod ([NotNull] ref JniMethodInfo? cachedMethod, ReadOnlySpan name, ReadOnlySpan signature) + { + AssertValid (); + + if (cachedMethod != null && cachedMethod.IsValid) + return cachedMethod; + var m = GetStaticMethod (name, signature); + if (Interlocked.CompareExchange (ref cachedMethod, m, null) != null) { + // No cleanup required; let the GC collect the unused instance + } + return cachedMethod; + } } } diff --git a/tests/Java.Interop-PerformanceTests/Java.Interop/TimingTests.cs b/tests/Java.Interop-PerformanceTests/Java.Interop/TimingTests.cs index 67e4c1255..e7f275580 100644 --- a/tests/Java.Interop-PerformanceTests/Java.Interop/TimingTests.cs +++ b/tests/Java.Interop-PerformanceTests/Java.Interop/TimingTests.cs @@ -402,6 +402,37 @@ public void MethodLookupTiming () } } + [TestFixture] + class JniFieldLookupTiming : Java.InteropTests.JavaVMFixture { + + [Test] + public void FieldLookupTiming () + { + const string JniType = "com/xamarin/interop/performance/JavaTiming"; + const string EncodedMember = "instanceIntField.I"; + const int count = 10000; + + var strLookupTime = Stopwatch.StartNew (); + for (int i = 0; i < count; ++i) { + var p = new JniPeerMembers (JniType, typeof (JavaTiming)); + var f = p.InstanceFields.GetFieldInfo (EncodedMember); + } + strLookupTime.Stop (); + + var encLookupTime = Stopwatch.StartNew (); + for (int i = 0; i < count; ++i) { + var p = new JniPeerMembers (JniType, typeof (JavaTiming)); + var lookup = new JniMemberInfoLookup (EncodedMember, "instanceIntField"u8, "I"u8); + var f = p.InstanceFields.GetFieldInfo (lookup); + } + encLookupTime.Stop (); + + Console.WriteLine ($"# {nameof (FieldLookupTiming)} Timing: looking up JavaTiming.instanceIntField {count} times"); + Console.WriteLine ($"# .InstanceMethods.GetFieldInfo(string): {strLookupTime.Elapsed}"); + Console.WriteLine ($"# .InstanceMethods.GetFieldInfo(JniMemberInfoLookup): {encLookupTime.Elapsed}"); + } + } + [TestFixture] class JavaArrayTiming : Java.InteropTests.JavaVMFixture { diff --git a/tests/Java.Interop-PerformanceTests/java/com/xamarin/interop/performance/JavaTiming.java b/tests/Java.Interop-PerformanceTests/java/com/xamarin/interop/performance/JavaTiming.java index ca45b787d..099e16de8 100644 --- a/tests/Java.Interop-PerformanceTests/java/com/xamarin/interop/performance/JavaTiming.java +++ b/tests/Java.Interop-PerformanceTests/java/com/xamarin/interop/performance/JavaTiming.java @@ -2,6 +2,8 @@ public class JavaTiming { + public int instanceIntField; + public static void StaticVoidMethod () { } diff --git a/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs b/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs index f56002836..2ccb60474 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs @@ -48,10 +48,10 @@ public unsafe void Dispose_Exceptions () Assert.Throws (() => t.AllocObject ()); Assert.Throws (() => t.NewObject (null, null)); Assert.Throws (() => t.GetConstructor (null)); - Assert.Throws (() => t.GetInstanceField (null, null)); - Assert.Throws (() => t.GetInstanceMethod (null, null)); - Assert.Throws (() => t.GetStaticField (null, null)); - Assert.Throws (() => t.GetStaticMethod (null, null)); + Assert.Throws (() => t.GetInstanceField (null, (string) null)); + Assert.Throws (() => t.GetInstanceMethod (null, (string) null)); + Assert.Throws (() => t.GetStaticField (null, (string) null)); + Assert.Throws (() => t.GetStaticMethod (null, (string) null)); Assert.Throws (() => t.GetSuperclass ()); Assert.Throws (() => t.IsAssignableFrom (null)); Assert.Throws (() => t.IsInstanceOfType (new JniObjectReference ())); @@ -60,14 +60,14 @@ public unsafe void Dispose_Exceptions () Assert.Throws (() => t.UnregisterNativeMethods ()); JniFieldInfo jif = null; - Assert.Throws (() => t.GetCachedInstanceField (ref jif, null, null)); + Assert.Throws (() => t.GetCachedInstanceField (ref jif, (string) null, (string) null)); JniMethodInfo jim = null; Assert.Throws (() => t.GetCachedConstructor (ref jim, null)); - Assert.Throws (() => t.GetCachedInstanceMethod (ref jim, null, null)); + Assert.Throws (() => t.GetCachedInstanceMethod (ref jim, (string) null, (string) null)); JniFieldInfo jsf = null; - Assert.Throws (() => t.GetCachedStaticField (ref jsf, null, null)); + Assert.Throws (() => t.GetCachedStaticField (ref jsf, (string) null, (string) null)); JniMethodInfo jsm = null; - Assert.Throws (() => t.GetCachedStaticMethod (ref jsm, null, null)); + Assert.Throws (() => t.GetCachedStaticMethod (ref jsm, (string) null, (string) null)); } [Test]