Skip to content

Commit 041f066

Browse files
committed
[Java.Interop] Add JniMemberInfoLookup
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
1 parent e1c7832 commit 041f066

16 files changed

+489
-9
lines changed

src/Java.Interop/GlobalSuppressions.cs

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

2020
// See: 045b8af7, 6a42bb89, f60906cf, e10f7cb0, etc.
2121
[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")]
22+
[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")]
23+
[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")]
24+
[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")]
25+
[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")]
2226
[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")]
2327
[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")]
2428
[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")]

src/Java.Interop/Java.Interop.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
<OutputPath>$(ToolOutputFullPath)</OutputPath>
3030
<DocumentationFile>$(ToolOutputFullPath)Java.Interop.xml</DocumentationFile>
3131
<JNIEnvGenPath>$(BuildToolOutputFullPath)</JNIEnvGenPath>
32-
<LangVersion Condition=" '$(JIBuildingForNetCoreApp)' == 'True' ">9.0</LangVersion>
32+
<LangVersion Condition=" '$(JIBuildingForNetCoreApp)' == 'True' ">12.0</LangVersion>
3333
<LangVersion Condition=" '$(LangVersion)' == '' ">8.0</LangVersion>
3434
<Version>$(JICoreLibVersion)</Version>
3535
<Standalone Condition=" '$(Standalone)' == '' ">true</Standalone>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using System.Runtime.ExceptionServices;
3+
using System.Runtime.InteropServices;
4+
5+
namespace Java.Interop;
6+
7+
partial class JniEnvironment {
8+
partial class InstanceFields {
9+
public static unsafe JniFieldInfo GetFieldID (JniObjectReference type, ReadOnlySpan<byte> name, ReadOnlySpan<byte> signature)
10+
{
11+
if (!type.IsValid)
12+
throw new ArgumentException ("Handle must be valid.", "type");
13+
14+
IntPtr env = JniEnvironment.EnvironmentPointer;
15+
IntPtr field;
16+
IntPtr thrown;
17+
fixed (void* name_ptr = &MemoryMarshal.GetReference (name))
18+
fixed (void* signature_ptr = &MemoryMarshal.GetReference (signature)) {
19+
field = JniNativeMethods.GetFieldID (env, type.Handle, (IntPtr) name_ptr, (IntPtr) signature_ptr);
20+
thrown = JniNativeMethods.ExceptionOccurred (env);
21+
}
22+
23+
Exception? __e = JniEnvironment.GetExceptionForLastThrowable (thrown);
24+
if (__e != null)
25+
ExceptionDispatchInfo.Capture (__e).Throw ();
26+
27+
if (field == IntPtr.Zero)
28+
throw new InvalidOperationException ("Should not be reached; `GetFieldID` should have thrown!");
29+
30+
#if DEBUG
31+
return new JniFieldInfo (name.ToString (), signature.ToString (), field, isStatic: false);
32+
#else // DEBUG
33+
return new JniFieldInfo (null!, null!, field, isStatic: false);
34+
#endif // DEBUG
35+
}
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using System.Runtime.ExceptionServices;
3+
using System.Runtime.InteropServices;
4+
5+
namespace Java.Interop;
6+
7+
partial class JniEnvironment {
8+
partial class InstanceMethods {
9+
public static unsafe JniMethodInfo GetMethodID (JniObjectReference type, ReadOnlySpan<byte> name, ReadOnlySpan<byte> signature)
10+
{
11+
if (!type.IsValid)
12+
throw new ArgumentException ("Handle must be valid.", "type");
13+
14+
IntPtr env = JniEnvironment.EnvironmentPointer;
15+
IntPtr method;
16+
IntPtr thrown;
17+
fixed (void* name_ptr = &MemoryMarshal.GetReference (name))
18+
fixed (void* signature_ptr = &MemoryMarshal.GetReference (signature)) {
19+
method = JniNativeMethods.GetMethodID (env, type.Handle, (IntPtr) name_ptr, (IntPtr) signature_ptr);
20+
thrown = JniNativeMethods.ExceptionOccurred (env);
21+
}
22+
23+
Exception? __e = JniEnvironment.GetExceptionForLastThrowable (thrown);
24+
if (__e != null)
25+
ExceptionDispatchInfo.Capture (__e).Throw ();
26+
27+
if (method == IntPtr.Zero)
28+
throw new InvalidOperationException ("Should not be reached; `GetMethodID` should have thrown!");
29+
30+
#if DEBUG
31+
return new JniMethodInfo (name.ToString (), signature.ToString (), method, isStatic: false);
32+
#else // DEBUG
33+
return new JniMethodInfo (null!, null!, method, isStatic: false);
34+
#endif // DEBUG
35+
}
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using System.Runtime.ExceptionServices;
3+
using System.Runtime.InteropServices;
4+
5+
namespace Java.Interop;
6+
7+
partial class JniEnvironment {
8+
partial class StaticFields {
9+
10+
public static unsafe JniFieldInfo GetStaticFieldID (JniObjectReference type, ReadOnlySpan<byte> name, ReadOnlySpan<byte> signature)
11+
{
12+
if (!type.IsValid)
13+
throw new ArgumentException ("Handle must be valid.", "type");
14+
15+
IntPtr env = JniEnvironment.EnvironmentPointer;
16+
IntPtr field;
17+
IntPtr thrown;
18+
fixed (void* name_ptr = &MemoryMarshal.GetReference (name))
19+
fixed (void* signature_ptr = &MemoryMarshal.GetReference (signature)) {
20+
field = JniNativeMethods.GetStaticFieldID (env, type.Handle, (IntPtr) name_ptr, (IntPtr) signature_ptr);
21+
thrown = JniNativeMethods.ExceptionOccurred (env);
22+
}
23+
24+
Exception? __e = JniEnvironment.GetExceptionForLastThrowable (thrown);
25+
if (__e != null)
26+
ExceptionDispatchInfo.Capture (__e).Throw ();
27+
28+
if (field == IntPtr.Zero)
29+
throw new InvalidOperationException ("Should not be reached; `GetFieldID` should have thrown!");
30+
31+
#if DEBUG
32+
return new JniFieldInfo (name.ToString (), signature.ToString (), field, isStatic: false);
33+
#else // DEBUG
34+
return new JniFieldInfo (null!, null!, field, isStatic: false);
35+
#endif // DEBUG
36+
}
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Runtime.ExceptionServices;
5+
using System.Runtime.InteropServices;
6+
7+
namespace Java.Interop;
8+
9+
partial class JniEnvironment {
10+
partial class StaticMethods {
11+
12+
public static unsafe JniMethodInfo GetStaticMethodID (JniObjectReference type, ReadOnlySpan<byte> name, ReadOnlySpan<byte> signature)
13+
{
14+
if (!type.IsValid)
15+
throw new ArgumentException ("Handle must be valid.", "type");
16+
17+
IntPtr env = JniEnvironment.EnvironmentPointer;
18+
IntPtr method;
19+
IntPtr thrown;
20+
fixed (void* name_ptr = &MemoryMarshal.GetReference (name))
21+
fixed (void* signature_ptr = &MemoryMarshal.GetReference (signature)) {
22+
method = JniNativeMethods.GetStaticMethodID (env, type.Handle, (IntPtr) name_ptr, (IntPtr) signature_ptr);
23+
thrown = JniNativeMethods.ExceptionOccurred (env);
24+
}
25+
26+
Exception? __e = JniEnvironment.GetExceptionForLastThrowable (thrown);
27+
if (__e != null)
28+
ExceptionDispatchInfo.Capture (__e).Throw ();
29+
30+
if (method == IntPtr.Zero)
31+
throw new InvalidOperationException ("Should not be reached; `GetStaticMethodID` should have thrown!");
32+
33+
#if DEBUG
34+
return new JniMethodInfo (name.ToString (), signature.ToString (), method, isStatic: true);
35+
#else // DEBUG
36+
return new JniMethodInfo (null!, null!, method, isStatic: true);
37+
#endif // DEBUG
38+
}
39+
40+
internal static unsafe bool TryGetStaticMethod (
41+
JniObjectReference type,
42+
ReadOnlySpan<byte> name,
43+
ReadOnlySpan<byte> signature,
44+
[NotNullWhen(true)]
45+
out JniMethodInfo? method)
46+
{
47+
method = null;
48+
49+
if (!type.IsValid)
50+
throw new ArgumentException ("Handle must be valid.", "type");
51+
52+
IntPtr env = JniEnvironment.EnvironmentPointer;
53+
IntPtr id;
54+
IntPtr thrown;
55+
fixed (void* name_ptr = &MemoryMarshal.GetReference (name))
56+
fixed (void* signature_ptr = &MemoryMarshal.GetReference (signature)) {
57+
id = JniNativeMethods.GetStaticMethodID (env, type.Handle, (IntPtr) name_ptr, (IntPtr) signature_ptr);
58+
thrown = JniNativeMethods.ExceptionOccurred (env);
59+
}
60+
61+
if (thrown != IntPtr.Zero) {
62+
JniNativeMethods.ExceptionClear (env);
63+
JniEnvironment.References.RawDeleteLocalRef (env, thrown);
64+
thrown = IntPtr.Zero;
65+
return false;
66+
}
67+
68+
Debug.Assert (id != IntPtr.Zero);
69+
if (id == IntPtr.Zero) {
70+
return false;
71+
}
72+
73+
#if DEBUG
74+
method = new JniMethodInfo (name.ToString (), signature.ToString (), id, isStatic: true);
75+
#else // DEBUG
76+
method = new JniMethodInfo (null!, null!, id, isStatic: true);
77+
#endif // DEBUG
78+
79+
return true;
80+
}
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
3+
namespace Java.Interop;
4+
5+
public ref struct JniMemberInfoLookup {
6+
public string EncodedMember {get; private set;}
7+
public ReadOnlySpan<byte> MemberName {get; private set;}
8+
public ReadOnlySpan<byte> MemberSignature {get; private set;}
9+
10+
public JniMemberInfoLookup (string encodedMember, ReadOnlySpan<byte> memberName, ReadOnlySpan<byte> memberSignature)
11+
{
12+
EncodedMember = encodedMember;
13+
MemberName = memberName;
14+
MemberSignature = memberSignature;
15+
}
16+
}

src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs

+11
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ public JniFieldInfo GetFieldInfo (string encodedMember)
3434
return f;
3535
}
3636
}
37+
38+
public JniFieldInfo GetFieldInfo (JniMemberInfoLookup member)
39+
{
40+
lock (InstanceFields) {
41+
if (!InstanceFields.TryGetValue (member.EncodedMember, out var f)) {
42+
f = Members.JniPeerType.GetInstanceField (member.MemberName, member.MemberSignature);
43+
InstanceFields.Add (member.EncodedMember, f);
44+
}
45+
return f;
46+
}
47+
}
3748
}}
3849
}
3950

src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs

+42
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,48 @@ JniMethodInfo GetMethodInfo (string method, string signature)
157157
return JniPeerType.GetInstanceMethod (method, signature);
158158
}
159159

160+
161+
public JniMethodInfo GetMethodInfo (JniMemberInfoLookup member)
162+
{
163+
lock (InstanceMethods) {
164+
if (InstanceMethods.TryGetValue (member.EncodedMember, out var m)) {
165+
return m;
166+
}
167+
}
168+
var info = GetMethodInfo (member.MemberName, member.MemberSignature);
169+
lock (InstanceMethods) {
170+
if (InstanceMethods.TryGetValue (member.EncodedMember, out var m)) {
171+
return m;
172+
}
173+
InstanceMethods.Add (member.EncodedMember, info);
174+
}
175+
return info;
176+
}
177+
178+
JniMethodInfo GetMethodInfo (ReadOnlySpan<byte> method, ReadOnlySpan<byte> signature)
179+
{
180+
var m = (JniMethodInfo?) null;
181+
var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature);
182+
if (newMethod.HasValue) {
183+
var typeName = newMethod.Value.TargetJniType ?? Members.JniPeerTypeName;
184+
var methodName = newMethod.Value.TargetJniMethodName ?? method.ToString ();
185+
var methodSig = newMethod.Value.TargetJniMethodSignature ?? signature.ToString ();
186+
187+
using var t = new JniType (typeName);
188+
if (newMethod.Value.TargetJniMethodInstanceToStatic &&
189+
t.TryGetStaticMethod (methodName, methodSig, out m)) {
190+
m.ParameterCount = newMethod.Value.TargetJniMethodParameterCount;
191+
m.StaticRedirect = new JniType (typeName);
192+
return m;
193+
}
194+
if (t.TryGetInstanceMethod (methodName, methodSig, out m)) {
195+
return m;
196+
}
197+
Console.Error.WriteLine ($"warning: For declared method `{Members.JniPeerTypeName}.{method.ToString ()}.{signature.ToString ()}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!");
198+
}
199+
return JniPeerType.GetInstanceMethod (method, signature);
200+
}
201+
160202
public unsafe JniObjectReference StartCreateInstance (string constructorSignature, Type declaringType, JniArgumentValue* parameters)
161203
{
162204
if (constructorSignature == null)

src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs

+11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ public JniFieldInfo GetFieldInfo (string encodedMember)
3030
}
3131
}
3232

33+
public JniFieldInfo GetFieldInfo (JniMemberInfoLookup member)
34+
{
35+
lock (StaticFields) {
36+
if (!StaticFields.TryGetValue (member.EncodedMember, out var f)) {
37+
f = Members.JniPeerType.GetInstanceField (member.MemberName, member.MemberSignature);
38+
StaticFields.Add (member.EncodedMember, f);
39+
}
40+
return f;
41+
}
42+
}
43+
3344
internal void Dispose ()
3445
{
3546
StaticFields.Clear ();

src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs

+65
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,46 @@ JniMethodInfo GetMethodInfo (string method, string signature)
6666
return Members.JniPeerType.GetStaticMethod (method, signature);
6767
}
6868

69+
public JniMethodInfo GetMethodInfo (JniMemberInfoLookup member)
70+
{
71+
lock (StaticMethods) {
72+
if (StaticMethods.TryGetValue (member.EncodedMember, out var m)) {
73+
return m;
74+
}
75+
}
76+
var info = GetMethodInfo (member.MemberName, member.MemberSignature);
77+
lock (StaticMethods) {
78+
if (StaticMethods.TryGetValue (member.EncodedMember, out var m)) {
79+
return m;
80+
}
81+
StaticMethods.Add (member.EncodedMember, info);
82+
}
83+
return info;
84+
}
85+
86+
JniMethodInfo GetMethodInfo (ReadOnlySpan<byte> method, ReadOnlySpan<byte> signature)
87+
{
88+
var m = (JniMethodInfo?) null;
89+
var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature);
90+
if (newMethod.HasValue) {
91+
using var t = new JniType (newMethod.Value.TargetJniType ?? Members.JniPeerTypeName);
92+
if (t.TryGetStaticMethod (
93+
newMethod.Value.TargetJniMethodName ?? method.ToString (),
94+
newMethod.Value.TargetJniMethodSignature ?? signature.ToString (),
95+
out m)) {
96+
return m;
97+
}
98+
}
99+
if (Members.JniPeerType.TryGetStaticMethod (method, signature, out m)) {
100+
return m;
101+
}
102+
m = FindInFallbackTypes (method, signature);
103+
if (m != null) {
104+
return m;
105+
}
106+
return Members.JniPeerType.GetStaticMethod (method, signature);
107+
}
108+
69109
#pragma warning disable CA1801
70110
JniType GetMethodDeclaringType (JniMethodInfo method)
71111
{
@@ -105,6 +145,31 @@ JniType GetMethodDeclaringType (JniMethodInfo method)
105145
}
106146
#endif // NET
107147

148+
JniMethodInfo? FindInFallbackTypes (ReadOnlySpan<byte> method, ReadOnlySpan<byte> signature)
149+
{
150+
var fallbackTypes = JniEnvironment.Runtime.TypeManager.GetStaticMethodFallbackTypes (Members.JniPeerTypeName);
151+
if (fallbackTypes == null) {
152+
return null;
153+
}
154+
foreach (var ft in fallbackTypes) {
155+
JniType? t = null;
156+
try {
157+
if (!JniType.TryParse (ft, out t)) {
158+
continue;
159+
}
160+
if (t.TryGetStaticMethod (method, signature, out var m)) {
161+
m.StaticRedirect = t;
162+
t = null;
163+
return m;
164+
}
165+
}
166+
finally {
167+
t?.Dispose ();
168+
}
169+
}
170+
return null;
171+
}
172+
108173
public unsafe void InvokeVoidMethod (string encodedMember, JniArgumentValue* parameters)
109174
{
110175
var m = GetMethodInfo (encodedMember);

0 commit comments

Comments
 (0)