Skip to content
This repository was archived by the owner on Aug 27, 2026. It is now read-only.

Add ReadOnlySpan<byte> overloads for JNI member lookups - #1393

Merged
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/utf8-overloads
Mar 20, 2026
Merged

Add ReadOnlySpan<byte> overloads for JNI member lookups#1393
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/utf8-overloads

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Summary

Add ReadOnlySpan<byte> overloads for JNI class, method, and field lookups. Callers can pass UTF-8 string literals (e.g., "java/lang/Object"u8) to skip Marshal.StringToCoTaskMemUTF8 / ZeroFreeCoTaskMemUTF8 entirely, eliminating the per-call UTF-16→UTF-8 transcoding and native heap alloc/free.

Contributes to dotnet/android#11885

New API surface

All overloads are additive (existing string APIs unchanged) and only compiled under FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS (the .NET Android runtime path).

JniEnvironment (low-level):

  • Types.FindClass(ReadOnlySpan<byte>) / TryFindClass(ReadOnlySpan<byte>, out JniObjectReference)
  • InstanceMethods.GetMethodID(JniObjectReference, ReadOnlySpan<byte>, ReadOnlySpan<byte>)
  • StaticMethods.GetStaticMethodID(JniObjectReference, ReadOnlySpan<byte>, ReadOnlySpan<byte>)
  • InstanceFields.GetFieldID(JniObjectReference, ReadOnlySpan<byte>, ReadOnlySpan<byte>)
  • StaticFields.GetStaticFieldID(JniObjectReference, ReadOnlySpan<byte>, ReadOnlySpan<byte>)

JniType (high-level):

  • Constructor: JniType(ReadOnlySpan<byte>)
  • GetConstructor, GetInstanceMethod, GetStaticMethod, GetInstanceField, GetStaticField
  • GetCached* variants for all of the above
  • GetCachedJniType(ref JniType?, ReadOnlySpan<byte>)

Performance

The span overloads use fixed pinning and pass the pointer directly to the JNI function table — zero marshalling. Benchmarked on macOS ARM64 (Release, 1M iterations per operation, NoOp JniObjectReferenceManager):

Operation                       String(ns/op)    UTF8(ns/op)   Speedup
----------------------------------------------------------------------
FindClass                               467            430      1.09x
GetMethodID                             354            297      1.19x
GetStaticMethodID                       341            249      1.37x
GetConstructor                          379            238      1.59x
GetStaticFieldID                        356            257      1.38x
GetInstanceField                        450            364      1.24x

The improvement comes entirely from eliminating the Marshal.StringToCoTaskMemUTF8 + ZeroFreeCoTaskMemUTF8 calls on the native heap — both paths produce identical managed objects (JniMethodInfo / JniFieldInfo).

Test coverage

13 new tests covering:

  • Equivalence: string and u8 overloads return identical JNI IDs
  • End-to-end: constructor lookup + method invocation via u8
  • Cached lookups: GetCached* returns same instance on repeated calls
  • Error handling: disposed type, class not found, invalid signature

Copilot AI review requested due to automatic review settings March 19, 2026 12:12
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/utf8-overloads branch 4 times, most recently from 211d339 to 0f746ea Compare March 19, 2026 14:39
Add UTF-8 span overloads for JNI class, method, and field lookups
that accept ReadOnlySpan<byte> parameters. Callers can pass u8 string
literals (e.g., "java/lang/Object"u8) to avoid string-to-UTF-8
marshalling overhead entirely.

New overloads (function pointers backend only):
- JniEnvironment.Types.FindClass/TryFindClass(ReadOnlySpan<byte>)
- JniEnvironment.InstanceMethods.GetMethodID(…, ReadOnlySpan<byte>, ReadOnlySpan<byte>)
- JniEnvironment.StaticMethods.GetStaticMethodID(…, ReadOnlySpan<byte>, ReadOnlySpan<byte>)
- JniEnvironment.InstanceFields.GetFieldID(…, ReadOnlySpan<byte>, ReadOnlySpan<byte>)
- JniEnvironment.StaticFields.GetStaticFieldID(…, ReadOnlySpan<byte>, ReadOnlySpan<byte>)
- JniType constructor, Get*/GetCached* for methods, fields, constructors

Contributes to #950

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/utf8-overloads branch from 0f746ea to f72e652 Compare March 19, 2026 14:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/utf8-overloads branch from f72e652 to 6bf1fce Compare March 19, 2026 15:01
Tests cover:
- Equivalence: string vs u8 overloads return same JNI IDs
- End-to-end: constructor + method invocation via u8 path
- Cached lookups: GetCached* returns same instance
- Error handling: disposed type, class not found, invalid signature

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/utf8-overloads branch from 6bf1fce to 8521bb7 Compare March 19, 2026 15:09
@simonrozsival simonrozsival added the copilot `copilot-cli` or other AIs were used to author this label Mar 19, 2026
The Class.forName fallback requires converting the UTF-8 span back to
a managed string and then to a Java string, which negates the
zero-alloc benefit of the UTF-8 path. Callers who need the classloader
fallback can use the existing string-based FindClass overload.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers
jonathanpeppers merged commit 9a9ffae into main Mar 20, 2026
2 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/utf8-overloads branch March 20, 2026 16:59
simonrozsival added a commit to dotnet/android that referenced this pull request Mar 23, 2026
Avoids string-to-UTF8 conversion at runtime. The ReadOnlySpan<byte>
overload was added in dotnet/java-interop#1393.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Mar 24, 2026
Avoids string-to-UTF8 conversion at runtime. The ReadOnlySpan<byte>
overload was added in dotnet/java-interop#1393.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Mar 25, 2026
Avoids string-to-UTF8 conversion at runtime. The ReadOnlySpan<byte>
overload was added in dotnet/java-interop#1393.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Mar 26, 2026
Avoids string-to-UTF8 conversion at runtime. The ReadOnlySpan<byte>
overload was added in dotnet/java-interop#1393.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Mar 27, 2026
…11002)

* Use JniNativeMethod for zero-allocation RegisterNatives bootstrap

Replace Marshal.GetDelegateForFunctionPointer wrapper with
JniNativeMethod stackalloc + ReadOnlySpan<JniNativeMethod> for the
mono.android.Runtime.registerNatives bootstrap registration.

No delegate allocation, no GC handle — same pattern as the generated
proxy RegisterNatives methods.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Use JniType UTF-8 constructor ("mono/android/Runtime"u8)

Avoids string-to-UTF8 conversion at runtime. The ReadOnlySpan<byte>
overload was added in dotnet/java-interop#1393.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove RegisterMethod - already removed in a separate PR

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify RegisterNatives: collection expression, less nesting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot `copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants