-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
Context: c6c487b Context: 312fbf4 Context: 2197579 Context: dotnet/android#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: * 312fbf4: C#9 function pointer backend for `JNIEnv` invocations * c6c487b: "Standalone" build config to use C#9 function pointers * 2197579: 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 c6c487b 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<byte> MemberName {get;} public ReadOnlySpan<byte> MemberSignature {get;} public JniMemberInfoLookup (string encodedMember, ReadOnlySpan<byte> memberName, ReadOnlySpan<byte> 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
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<byte> name, ReadOnlySpan<byte> signature) | ||
Check failure on line 9 in src/Java.Interop/Java.Interop/JniEnvironment.InstanceFields.cs Azure Pipelines / Java.Interop (Mac - .NET)src/Java.Interop/Java.Interop/JniEnvironment.InstanceFields.cs#L9
Check failure on line 9 in src/Java.Interop/Java.Interop/JniEnvironment.InstanceFields.cs Azure Pipelines / Java.Interopsrc/Java.Interop/Java.Interop/JniEnvironment.InstanceFields.cs#L9
|
||
{ | ||
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 | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<byte> name, ReadOnlySpan<byte> signature) | ||
Check failure on line 9 in src/Java.Interop/Java.Interop/JniEnvironment.InstanceMethods.cs Azure Pipelines / Java.Interop (Mac - .NET)src/Java.Interop/Java.Interop/JniEnvironment.InstanceMethods.cs#L9
Check failure on line 9 in src/Java.Interop/Java.Interop/JniEnvironment.InstanceMethods.cs Azure Pipelines / Java.Interopsrc/Java.Interop/Java.Interop/JniEnvironment.InstanceMethods.cs#L9
|
||
{ | ||
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 | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<byte> name, ReadOnlySpan<byte> 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 | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<byte> name, ReadOnlySpan<byte> signature) | ||
Check failure on line 12 in src/Java.Interop/Java.Interop/JniEnvironment.StaticMethods.cs Azure Pipelines / Java.Interop (Mac - .NET)src/Java.Interop/Java.Interop/JniEnvironment.StaticMethods.cs#L12
|
||
{ | ||
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<byte> name, | ||
ReadOnlySpan<byte> 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; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using System; | ||
|
||
namespace Java.Interop; | ||
|
||
public ref struct JniMemberInfoLookup { | ||
Check failure on line 5 in src/Java.Interop/Java.Interop/JniMemberInfoLookup.cs Azure Pipelines / Java.Interop (Mac - .NET)src/Java.Interop/Java.Interop/JniMemberInfoLookup.cs#L5
Check failure on line 5 in src/Java.Interop/Java.Interop/JniMemberInfoLookup.cs Azure Pipelines / Java.Interop (Mac - .NET)src/Java.Interop/Java.Interop/JniMemberInfoLookup.cs#L5
Check failure on line 5 in src/Java.Interop/Java.Interop/JniMemberInfoLookup.cs Azure Pipelines / Java.Interopsrc/Java.Interop/Java.Interop/JniMemberInfoLookup.cs#L5
Check failure on line 5 in src/Java.Interop/Java.Interop/JniMemberInfoLookup.cs Azure Pipelines / Java.Interopsrc/Java.Interop/Java.Interop/JniMemberInfoLookup.cs#L5
|
||
public string EncodedMember {get; private set;} | ||
public ReadOnlySpan<byte> MemberName {get; private set;} | ||
public ReadOnlySpan<byte> MemberSignature {get; private set;} | ||
|
||
public JniMemberInfoLookup (string encodedMember, ReadOnlySpan<byte> memberName, ReadOnlySpan<byte> memberSignature) | ||
{ | ||
EncodedMember = encodedMember; | ||
MemberName = memberName; | ||
MemberSignature = memberSignature; | ||
} | ||
} |