From fd27cca486fecf3a9a40390b2b3b1cb487406164 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Apr 2026 14:43:43 +0200 Subject: [PATCH 1/9] Fix UTF-8 JniType class lookup fallback Make the ReadOnlySpan JniType/FindClass path fall back through Class.forName using the runtime class loader, matching the existing string overload semantics. Add regression coverage for Java-style class names so both overload families stay aligned. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniEnvironment.Types.cs | 102 ++++++++++-------- .../Java.Interop/JniTypeUtf8Test.cs | 23 ++++ 2 files changed, 78 insertions(+), 47 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index 8c6d5916d..2c7cbbda0 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -56,43 +56,7 @@ static unsafe JniObjectReference TryFindClass (string classname, bool throwOnErr return r; } RawExceptionClear (info.EnvironmentPointer); - - var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); - LogCreateLocalRef (findClassThrown); - var pendingException = info.Runtime.GetExceptionForThrowable (ref findClassThrown, JniObjectReferenceOptions.CopyAndDispose); - - if (Class_forName.IsValid) { - var java = info.ToJavaName (classname); - var __args = stackalloc JniArgumentValue [3]; - __args [0] = new JniArgumentValue (java); - __args [1] = new JniArgumentValue (true); // initialize the class - __args [2] = new JniArgumentValue (info.Runtime.ClassLoader); - - c = RawCallStaticObjectMethodA (info.EnvironmentPointer, out thrown, Class_reference.Handle, Class_forName.ID, (IntPtr) __args); - JniObjectReference.Dispose (ref java); - if (thrown == IntPtr.Zero) { - (pendingException as IJavaPeerable)?.Dispose (); - var r = new JniObjectReference (c, JniObjectReferenceType.Local); - JniEnvironment.LogCreateLocalRef (r); - return r; - } - RawExceptionClear (info.EnvironmentPointer); - - if (pendingException != null) { - JniEnvironment.References.RawDeleteLocalRef (info.EnvironmentPointer, thrown); - } - else { - var loadClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); - LogCreateLocalRef (loadClassThrown); - pendingException = info.Runtime.GetExceptionForThrowable (ref loadClassThrown, JniObjectReferenceOptions.CopyAndDispose); - } - } - - if (!throwOnError) { - (pendingException as IJavaPeerable)?.Dispose (); - return default; - } - throw pendingException!; + return TryLoadClassWithFallback (info, thrown, classname, throwOnError); #endif // !(FEATURE_JNIENVIRONMENT_JI_PINVOKES || FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS) #if FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES var c = info.Invoker.FindClass (info.EnvironmentPointer, classname); @@ -135,6 +99,48 @@ static unsafe JniObjectReference TryFindClass (string classname, bool throwOnErr #endif // !FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES } + static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo info, IntPtr thrown, string classname, bool throwOnError) + { + var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); + LogCreateLocalRef (findClassThrown); + Exception? pendingException = info.Runtime.GetExceptionForThrowable (ref findClassThrown, JniObjectReferenceOptions.CopyAndDispose); + + if (Class_forName.IsValid) { + var java = info.ToJavaName (classname); + var __args = stackalloc JniArgumentValue [3]; + __args [0] = new JniArgumentValue (java); + __args [1] = new JniArgumentValue (true); // initialize the class + __args [2] = new JniArgumentValue (info.Runtime.ClassLoader); + + var c = RawCallStaticObjectMethodA (info.EnvironmentPointer, out thrown, Class_reference.Handle, Class_forName.ID, (IntPtr) __args); + JniObjectReference.Dispose (ref java); + if (thrown == IntPtr.Zero) { + (pendingException as IJavaPeerable)?.Dispose (); + var r = new JniObjectReference (c, JniObjectReferenceType.Local); + JniEnvironment.LogCreateLocalRef (r); + return r; + } + RawExceptionClear (info.EnvironmentPointer); + + if (pendingException != null) { + JniEnvironment.References.RawDeleteLocalRef (info.EnvironmentPointer, thrown); + } else { + var loadClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); + LogCreateLocalRef (loadClassThrown); + pendingException = info.Runtime.GetExceptionForThrowable (ref loadClassThrown, JniObjectReferenceOptions.CopyAndDispose); + } + } + + if (!throwOnError) { + (pendingException as IJavaPeerable)?.Dispose (); + return default; + } + if (pendingException != null) + throw pendingException; + + throw new InvalidOperationException ($"Could not find Java class '{classname}'."); + } + static bool TryRawFindClass (IntPtr env, string classname, out IntPtr klass, out IntPtr thrown) { #if FEATURE_JNIENVIRONMENT_JI_PINVOKES @@ -331,6 +337,9 @@ public static unsafe bool TryFindClass (ReadOnlySpan classname, out JniObj static unsafe JniObjectReference TryFindClass (ReadOnlySpan classname, bool throwOnError) { + if (classname.Length == 0) + throw new ArgumentException ("'classname' cannot be a zero-length string.", nameof (classname)); + var info = JniEnvironment.CurrentInfo; fixed (byte* _classname_ptr = classname) { var c = JniNativeMethods.FindClass (info.EnvironmentPointer, (IntPtr) _classname_ptr); @@ -342,20 +351,19 @@ static unsafe JniObjectReference TryFindClass (ReadOnlySpan classname, boo } RawExceptionClear (info.EnvironmentPointer); + return TryLoadClassWithFallback (info, thrown, GetStringClassName (classname), throwOnError); + } + } - var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); - LogCreateLocalRef (findClassThrown); - var pendingException = info.Runtime.GetExceptionForThrowable (ref findClassThrown, JniObjectReferenceOptions.CopyAndDispose); + static string GetStringClassName (ReadOnlySpan classname) + { + var terminator = classname.IndexOf ((byte)0); + if (terminator >= 0) + classname = classname.Slice (0, terminator); - if (!throwOnError) { - (pendingException as IJavaPeerable)?.Dispose (); - return default; - } - throw pendingException!; - } + return Encoding.UTF8.GetString (classname); } #endif // FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS } } } - diff --git a/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs b/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs index d104cd2ee..ca461f7b0 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs @@ -43,6 +43,29 @@ public void FindClass_Utf8_ReturnsValidReference () } } + [Test] + public void FindClass_Utf8_UsesSameFallbackAsStringOverload () + { + var fromString = JniEnvironment.Types.FindClass ("java.lang.Object"); + var fromUtf8 = JniEnvironment.Types.FindClass ("java.lang.Object"u8); + try { + Assert.IsTrue (JniEnvironment.Types.IsSameObject (fromString, fromUtf8)); + } finally { + JniObjectReference.Dispose (ref fromString); + JniObjectReference.Dispose (ref fromUtf8); + } + } + + [Test] + public void Ctor_Utf8_UsesSameFallbackAsStringOverload () + { + using (var fromString = new JniType ("java.lang.Object")) + using (var fromUtf8 = new JniType ("java.lang.Object"u8)) { + Assert.IsTrue (JniEnvironment.Types.IsSameObject (fromString.PeerReference, fromUtf8.PeerReference)); + Assert.AreEqual ("java/lang/Object", fromUtf8.Name); + } + } + [Test] public void FindClass_Utf8_ThrowsOnNotFound () { From ae5f5f86feb140e43ad809887885676d0909f933 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Apr 2026 15:54:17 +0200 Subject: [PATCH 2/9] Use NewStringUTF in UTF-8 class fallback Avoid allocating a managed UTF-16 string in the ReadOnlySpan JniType/Class.forName fallback path by creating the Java name with NewStringUTF instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniEnvironment.Types.cs | 95 ++++++++++++++++--- 1 file changed, 84 insertions(+), 11 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index 2c7cbbda0..01f426ed5 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -3,7 +3,7 @@ using System; using System.Diagnostics; using System.Collections.Generic; -using System.Text; +using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; namespace Java.Interop @@ -141,6 +141,79 @@ static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo in throw new InvalidOperationException ($"Could not find Java class '{classname}'."); } + static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo info, IntPtr thrown, ReadOnlySpan classname, bool throwOnError) + { + var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); + LogCreateLocalRef (findClassThrown); + Exception? pendingException = info.Runtime.GetExceptionForThrowable (ref findClassThrown, JniObjectReferenceOptions.CopyAndDispose); + + if (Class_forName.IsValid) { + var java = NewJavaNameFromUtf8 (info.EnvironmentPointer, classname); + try { + var __args = stackalloc JniArgumentValue [3]; + __args [0] = new JniArgumentValue (java); + __args [1] = new JniArgumentValue (true); // initialize the class + __args [2] = new JniArgumentValue (info.Runtime.ClassLoader); + + var c = RawCallStaticObjectMethodA (info.EnvironmentPointer, out thrown, Class_reference.Handle, Class_forName.ID, (IntPtr) __args); + if (thrown == IntPtr.Zero) { + (pendingException as IJavaPeerable)?.Dispose (); + var r = new JniObjectReference (c, JniObjectReferenceType.Local); + JniEnvironment.LogCreateLocalRef (r); + return r; + } + RawExceptionClear (info.EnvironmentPointer); + } finally { + JniObjectReference.Dispose (ref java); + } + + if (pendingException != null) { + JniEnvironment.References.RawDeleteLocalRef (info.EnvironmentPointer, thrown); + } else { + var loadClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); + LogCreateLocalRef (loadClassThrown); + pendingException = info.Runtime.GetExceptionForThrowable (ref loadClassThrown, JniObjectReferenceOptions.CopyAndDispose); + } + } + + if (!throwOnError) { + (pendingException as IJavaPeerable)?.Dispose (); + return default; + } + if (pendingException != null) + throw pendingException; + + throw new InvalidOperationException ("Could not find Java class from the supplied UTF-8 name."); + } + + static unsafe JniObjectReference NewJavaNameFromUtf8 (IntPtr env, ReadOnlySpan classname) + { + var terminator = classname.IndexOf ((byte) 0); + if (terminator >= 0) + classname = classname.Slice (0, terminator); + + // Class names here are binary/JNI names, so `NewStringUTF()` lets the fallback + // avoid a managed UTF-16 allocation while still calling `Class.forName()`. + Span javaName = classname.Length + 1 <= 256 + ? stackalloc byte [classname.Length + 1] + : new byte [classname.Length + 1]; + + for (int i = 0; i < classname.Length; ++i) + javaName [i] = classname [i] == (byte) '/' ? (byte) '.' : classname [i]; + javaName [classname.Length] = 0; + + fixed (byte* pJavaName = javaName) { + var s = RawNewStringUTF (env, (IntPtr) pJavaName); + var e = JniEnvironment.GetExceptionForLastThrowable (); + if (e != null) + ExceptionDispatchInfo.Capture (e).Throw (); + + var r = new JniObjectReference (s, JniObjectReferenceType.Local); + JniEnvironment.LogCreateLocalRef (r); + return r; + } + } + static bool TryRawFindClass (IntPtr env, string classname, out IntPtr klass, out IntPtr thrown) { #if FEATURE_JNIENVIRONMENT_JI_PINVOKES @@ -161,6 +234,15 @@ static bool TryRawFindClass (IntPtr env, string classname, out IntPtr klass, out return false; } + static unsafe IntPtr RawNewStringUTF (IntPtr env, IntPtr bytes) + { +#if FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS + return (*((JNIEnv**) env))->NewStringUTF (env, bytes); +#else +#error Unsupported backend +#endif + } + static void RawExceptionClear (IntPtr env) { #if FEATURE_JNIENVIRONMENT_JI_PINVOKES @@ -351,18 +433,9 @@ static unsafe JniObjectReference TryFindClass (ReadOnlySpan classname, boo } RawExceptionClear (info.EnvironmentPointer); - return TryLoadClassWithFallback (info, thrown, GetStringClassName (classname), throwOnError); + return TryLoadClassWithFallback (info, thrown, classname, throwOnError); } } - - static string GetStringClassName (ReadOnlySpan classname) - { - var terminator = classname.IndexOf ((byte)0); - if (terminator >= 0) - classname = classname.Slice (0, terminator); - - return Encoding.UTF8.GetString (classname); - } #endif // FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS } } From 9fe9c9fe591d3293ea04556bbd142a995460d43f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Apr 2026 16:06:07 +0200 Subject: [PATCH 3/9] Address Copilot review feedback Limit the NewStringUTF helper to function-pointer builds so other backends continue to compile, and include the UTF-8 class name in the fallback exception message for better diagnostics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniEnvironment.Types.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index 01f426ed5..1f48cf737 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; +using System.Text; namespace Java.Interop { @@ -141,6 +142,7 @@ static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo in throw new InvalidOperationException ($"Could not find Java class '{classname}'."); } +#if FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo info, IntPtr thrown, ReadOnlySpan classname, bool throwOnError) { var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); @@ -183,7 +185,7 @@ static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo in if (pendingException != null) throw pendingException; - throw new InvalidOperationException ("Could not find Java class from the supplied UTF-8 name."); + throw new InvalidOperationException ($"Could not find Java class '{GetStringClassName (classname)}'."); } static unsafe JniObjectReference NewJavaNameFromUtf8 (IntPtr env, ReadOnlySpan classname) @@ -214,6 +216,16 @@ static unsafe JniObjectReference NewJavaNameFromUtf8 (IntPtr env, ReadOnlySpan classname) + { + var terminator = classname.IndexOf ((byte) 0); + if (terminator >= 0) + classname = classname.Slice (0, terminator); + + return Encoding.UTF8.GetString (classname); + } +#endif + static bool TryRawFindClass (IntPtr env, string classname, out IntPtr klass, out IntPtr thrown) { #if FEATURE_JNIENVIRONMENT_JI_PINVOKES @@ -234,14 +246,12 @@ static bool TryRawFindClass (IntPtr env, string classname, out IntPtr klass, out return false; } +#if FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS static unsafe IntPtr RawNewStringUTF (IntPtr env, IntPtr bytes) { -#if FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS return (*((JNIEnv**) env))->NewStringUTF (env, bytes); -#else -#error Unsupported backend -#endif } +#endif static void RawExceptionClear (IntPtr env) { From f3bbd31730c2a1a7907b83d7b17b0b8de57b9a93 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Apr 2026 16:14:46 +0200 Subject: [PATCH 4/9] Unify JniType fallback helper Refactor the string and UTF-8 class lookup fallback paths to share a single implementation built around a Java string reference. This preserves the NewStringUTF optimization while removing duplicated fallback logic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniEnvironment.Types.cs | 58 +++++-------------- 1 file changed, 13 insertions(+), 45 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index 1f48cf737..88fb56c4c 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -57,7 +57,12 @@ static unsafe JniObjectReference TryFindClass (string classname, bool throwOnErr return r; } RawExceptionClear (info.EnvironmentPointer); - return TryLoadClassWithFallback (info, thrown, classname, throwOnError); + var java = info.ToJavaName (classname); + try { + return TryLoadClassWithFallback (info, thrown, java, classname, throwOnError); + } finally { + JniObjectReference.Dispose (ref java); + } #endif // !(FEATURE_JNIENVIRONMENT_JI_PINVOKES || FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS) #if FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES var c = info.Invoker.FindClass (info.EnvironmentPointer, classname); @@ -100,21 +105,19 @@ static unsafe JniObjectReference TryFindClass (string classname, bool throwOnErr #endif // !FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES } - static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo info, IntPtr thrown, string classname, bool throwOnError) + static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo info, IntPtr thrown, JniObjectReference classNameJavaString, string classname, bool throwOnError) { var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); LogCreateLocalRef (findClassThrown); Exception? pendingException = info.Runtime.GetExceptionForThrowable (ref findClassThrown, JniObjectReferenceOptions.CopyAndDispose); if (Class_forName.IsValid) { - var java = info.ToJavaName (classname); var __args = stackalloc JniArgumentValue [3]; - __args [0] = new JniArgumentValue (java); + __args [0] = new JniArgumentValue (classNameJavaString); __args [1] = new JniArgumentValue (true); // initialize the class __args [2] = new JniArgumentValue (info.Runtime.ClassLoader); var c = RawCallStaticObjectMethodA (info.EnvironmentPointer, out thrown, Class_reference.Handle, Class_forName.ID, (IntPtr) __args); - JniObjectReference.Dispose (ref java); if (thrown == IntPtr.Zero) { (pendingException as IJavaPeerable)?.Dispose (); var r = new JniObjectReference (c, JniObjectReferenceType.Local); @@ -145,47 +148,12 @@ static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo in #if FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo info, IntPtr thrown, ReadOnlySpan classname, bool throwOnError) { - var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); - LogCreateLocalRef (findClassThrown); - Exception? pendingException = info.Runtime.GetExceptionForThrowable (ref findClassThrown, JniObjectReferenceOptions.CopyAndDispose); - - if (Class_forName.IsValid) { - var java = NewJavaNameFromUtf8 (info.EnvironmentPointer, classname); - try { - var __args = stackalloc JniArgumentValue [3]; - __args [0] = new JniArgumentValue (java); - __args [1] = new JniArgumentValue (true); // initialize the class - __args [2] = new JniArgumentValue (info.Runtime.ClassLoader); - - var c = RawCallStaticObjectMethodA (info.EnvironmentPointer, out thrown, Class_reference.Handle, Class_forName.ID, (IntPtr) __args); - if (thrown == IntPtr.Zero) { - (pendingException as IJavaPeerable)?.Dispose (); - var r = new JniObjectReference (c, JniObjectReferenceType.Local); - JniEnvironment.LogCreateLocalRef (r); - return r; - } - RawExceptionClear (info.EnvironmentPointer); - } finally { - JniObjectReference.Dispose (ref java); - } - - if (pendingException != null) { - JniEnvironment.References.RawDeleteLocalRef (info.EnvironmentPointer, thrown); - } else { - var loadClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); - LogCreateLocalRef (loadClassThrown); - pendingException = info.Runtime.GetExceptionForThrowable (ref loadClassThrown, JniObjectReferenceOptions.CopyAndDispose); - } - } - - if (!throwOnError) { - (pendingException as IJavaPeerable)?.Dispose (); - return default; + var javaName = NewJavaNameFromUtf8 (info.EnvironmentPointer, classname); + try { + return TryLoadClassWithFallback (info, thrown, javaName, GetStringClassName (classname), throwOnError); + } finally { + JniObjectReference.Dispose (ref javaName); } - if (pendingException != null) - throw pendingException; - - throw new InvalidOperationException ($"Could not find Java class '{GetStringClassName (classname)}'."); } static unsafe JniObjectReference NewJavaNameFromUtf8 (IntPtr env, ReadOnlySpan classname) From fb21aca6621a8848756323ed2a8ef0f30e360d66 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Apr 2026 16:18:36 +0200 Subject: [PATCH 5/9] Inline UTF-8 fallback callsite Inline the UTF-8 fallback Java string creation at the TryFindClass(ReadOnlySpan) callsite so it matches the string overload pattern and avoids an unnecessary wrapper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Java.Interop/Java.Interop/JniEnvironment.Types.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index 88fb56c4c..ef2d45b7f 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -411,7 +411,12 @@ static unsafe JniObjectReference TryFindClass (ReadOnlySpan classname, boo } RawExceptionClear (info.EnvironmentPointer); - return TryLoadClassWithFallback (info, thrown, classname, throwOnError); + var javaName = NewJavaNameFromUtf8 (info.EnvironmentPointer, classname); + try { + return TryLoadClassWithFallback (info, thrown, javaName, GetStringClassName (classname), throwOnError); + } finally { + JniObjectReference.Dispose (ref javaName); + } } } #endif // FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS From 189ac79b5377d5c0183520f937a278e42ad285eb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Apr 2026 16:21:13 +0200 Subject: [PATCH 6/9] Remove leftover UTF-8 wrapper Delete the obsolete TryLoadClassWithFallback(ReadOnlySpan) overload now that the UTF-8 callsite creates and disposes the Java string inline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Java.Interop/Java.Interop/JniEnvironment.Types.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index ef2d45b7f..5343f9cbe 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -146,16 +146,6 @@ static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo in } #if FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS - static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo info, IntPtr thrown, ReadOnlySpan classname, bool throwOnError) - { - var javaName = NewJavaNameFromUtf8 (info.EnvironmentPointer, classname); - try { - return TryLoadClassWithFallback (info, thrown, javaName, GetStringClassName (classname), throwOnError); - } finally { - JniObjectReference.Dispose (ref javaName); - } - } - static unsafe JniObjectReference NewJavaNameFromUtf8 (IntPtr env, ReadOnlySpan classname) { var terminator = classname.IndexOf ((byte) 0); From 7e7dc88ef47f8c9ea00f701166484e57fd3bb7e0 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Apr 2026 16:25:33 +0200 Subject: [PATCH 7/9] Inline NewStringUTF call Inline the single-use RawNewStringUTF helper and keep managed class-name conversion on the final error path only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniEnvironment.Types.cs | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index 5343f9cbe..9533b192f 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; -using System.Text; namespace Java.Interop { @@ -105,7 +104,7 @@ static unsafe JniObjectReference TryFindClass (string classname, bool throwOnErr #endif // !FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES } - static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo info, IntPtr thrown, JniObjectReference classNameJavaString, string classname, bool throwOnError) + static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo info, IntPtr thrown, JniObjectReference classNameJavaString, string? classname, bool throwOnError) { var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); LogCreateLocalRef (findClassThrown); @@ -142,6 +141,7 @@ static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo in if (pendingException != null) throw pendingException; + classname ??= JniEnvironment.Strings.ToString (classNameJavaString) ?? ""; throw new InvalidOperationException ($"Could not find Java class '{classname}'."); } @@ -163,7 +163,7 @@ static unsafe JniObjectReference NewJavaNameFromUtf8 (IntPtr env, ReadOnlySpanNewStringUTF (env, (IntPtr) pJavaName); var e = JniEnvironment.GetExceptionForLastThrowable (); if (e != null) ExceptionDispatchInfo.Capture (e).Throw (); @@ -173,15 +173,6 @@ static unsafe JniObjectReference NewJavaNameFromUtf8 (IntPtr env, ReadOnlySpan classname) - { - var terminator = classname.IndexOf ((byte) 0); - if (terminator >= 0) - classname = classname.Slice (0, terminator); - - return Encoding.UTF8.GetString (classname); - } #endif static bool TryRawFindClass (IntPtr env, string classname, out IntPtr klass, out IntPtr thrown) @@ -204,13 +195,6 @@ static bool TryRawFindClass (IntPtr env, string classname, out IntPtr klass, out return false; } -#if FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS - static unsafe IntPtr RawNewStringUTF (IntPtr env, IntPtr bytes) - { - return (*((JNIEnv**) env))->NewStringUTF (env, bytes); - } -#endif - static void RawExceptionClear (IntPtr env) { #if FEATURE_JNIENVIRONMENT_JI_PINVOKES @@ -403,7 +387,7 @@ static unsafe JniObjectReference TryFindClass (ReadOnlySpan classname, boo RawExceptionClear (info.EnvironmentPointer); var javaName = NewJavaNameFromUtf8 (info.EnvironmentPointer, classname); try { - return TryLoadClassWithFallback (info, thrown, javaName, GetStringClassName (classname), throwOnError); + return TryLoadClassWithFallback (info, thrown, javaName, null, throwOnError); } finally { JniObjectReference.Dispose (ref javaName); } From e305f12bfc39b9a18b49c661b1cb2d30c52e1b65 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Apr 2026 16:50:21 +0200 Subject: [PATCH 8/9] Move not-found throw to callsites Refactor TryLoadClassWithFallback to return success via an out JniObjectReference so callers can defer class-name allocation until the final not-found branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniEnvironment.Types.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index 9533b192f..23ae25232 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; +using System.Text; namespace Java.Interop { @@ -58,10 +59,15 @@ static unsafe JniObjectReference TryFindClass (string classname, bool throwOnErr RawExceptionClear (info.EnvironmentPointer); var java = info.ToJavaName (classname); try { - return TryLoadClassWithFallback (info, thrown, java, classname, throwOnError); + if (TryLoadClassWithFallback (info, thrown, java, throwOnError, out var result)) + return result; } finally { JniObjectReference.Dispose (ref java); } + if (!throwOnError) + return default; + + throw new InvalidOperationException ($"Could not find Java class '{classname}'."); #endif // !(FEATURE_JNIENVIRONMENT_JI_PINVOKES || FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS) #if FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES var c = info.Invoker.FindClass (info.EnvironmentPointer, classname); @@ -104,8 +110,10 @@ static unsafe JniObjectReference TryFindClass (string classname, bool throwOnErr #endif // !FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES } - static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo info, IntPtr thrown, JniObjectReference classNameJavaString, string? classname, bool throwOnError) + static unsafe bool TryLoadClassWithFallback (JniEnvironmentInfo info, IntPtr thrown, JniObjectReference classNameJavaString, bool throwOnError, out JniObjectReference result) { + result = default; + var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); LogCreateLocalRef (findClassThrown); Exception? pendingException = info.Runtime.GetExceptionForThrowable (ref findClassThrown, JniObjectReferenceOptions.CopyAndDispose); @@ -119,9 +127,9 @@ static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo in var c = RawCallStaticObjectMethodA (info.EnvironmentPointer, out thrown, Class_reference.Handle, Class_forName.ID, (IntPtr) __args); if (thrown == IntPtr.Zero) { (pendingException as IJavaPeerable)?.Dispose (); - var r = new JniObjectReference (c, JniObjectReferenceType.Local); - JniEnvironment.LogCreateLocalRef (r); - return r; + result = new JniObjectReference (c, JniObjectReferenceType.Local); + JniEnvironment.LogCreateLocalRef (result); + return true; } RawExceptionClear (info.EnvironmentPointer); @@ -136,13 +144,12 @@ static unsafe JniObjectReference TryLoadClassWithFallback (JniEnvironmentInfo in if (!throwOnError) { (pendingException as IJavaPeerable)?.Dispose (); - return default; + return false; } if (pendingException != null) throw pendingException; - classname ??= JniEnvironment.Strings.ToString (classNameJavaString) ?? ""; - throw new InvalidOperationException ($"Could not find Java class '{classname}'."); + return false; } #if FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS @@ -387,12 +394,26 @@ static unsafe JniObjectReference TryFindClass (ReadOnlySpan classname, boo RawExceptionClear (info.EnvironmentPointer); var javaName = NewJavaNameFromUtf8 (info.EnvironmentPointer, classname); try { - return TryLoadClassWithFallback (info, thrown, javaName, null, throwOnError); + if (TryLoadClassWithFallback (info, thrown, javaName, throwOnError, out var result)) + return result; } finally { JniObjectReference.Dispose (ref javaName); } + if (!throwOnError) + return default; + + throw new InvalidOperationException ($"Could not find Java class '{GetStringClassName (classname)}'."); } } + + static string GetStringClassName (ReadOnlySpan classname) + { + var terminator = classname.IndexOf ((byte) 0); + if (terminator >= 0) + classname = classname.Slice (0, terminator); + + return Encoding.UTF8.GetString (classname); + } #endif // FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS } } From 498fbf4534cc16dbf945f33fe5b1a42c1a7d8c56 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Apr 2026 16:53:48 +0200 Subject: [PATCH 9/9] Inline UTF-8 classname decode Drop the single-use GetStringClassName helper and decode the UTF-8 class name directly at the final not-found throw site. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniEnvironment.Types.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index 23ae25232..1f29d21cd 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -402,18 +402,13 @@ static unsafe JniObjectReference TryFindClass (ReadOnlySpan classname, boo if (!throwOnError) return default; - throw new InvalidOperationException ($"Could not find Java class '{GetStringClassName (classname)}'."); + var terminator = classname.IndexOf ((byte) 0); + var errorClassName = terminator >= 0 + ? Encoding.UTF8.GetString (classname.Slice (0, terminator)) + : Encoding.UTF8.GetString (classname); + throw new InvalidOperationException ($"Could not find Java class '{errorClassName}'."); } } - - static string GetStringClassName (ReadOnlySpan classname) - { - var terminator = classname.IndexOf ((byte) 0); - if (terminator >= 0) - classname = classname.Slice (0, terminator); - - return Encoding.UTF8.GetString (classname); - } #endif // FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS } }