Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
311c6f0
[Java.Interop] Use native UTF-8 JNI method remaps
simonrozsival Sep 16, 2026
85b1867
[Java.Interop] Document and test UTF-8 remap pointers
simonrozsival Sep 16, 2026
3517dd2
[Java.Interop] Keep JNI remaps in UTF-8
simonrozsival Sep 16, 2026
883989f
[Java.Interop] Clarify cached remap pointer lifetime
simonrozsival Sep 16, 2026
94d5836
[Java.Interop] Address UTF-8 remap review feedback
simonrozsival Sep 17, 2026
baf62ed
[Java.Interop] Limit custom remap test to host JVM
simonrozsival Sep 17, 2026
f12babb
[Java.Interop] Add Android UTF-8 signature remap fixture
simonrozsival Sep 17, 2026
7e537c9
[Java.Interop] Combine replacement type lookup
simonrozsival Sep 18, 2026
73355cf
[Java.Interop] Keep replacement type APIs consistent
simonrozsival Sep 18, 2026
0a25f17
[runtime] Move JNI type and method remapping to managed code
simonrozsival Sep 16, 2026
5b2d34b
[runtime] Compare JNI remaps without transcoding
simonrozsival Sep 16, 2026
eced034
[tests] Cover non-ASCII JNI remap lookup
simonrozsival Sep 16, 2026
cb11194
[runtime] Bound mixed ASCII comparison rescans
simonrozsival Sep 16, 2026
420715b
[build] Suppress generated remapping pointer warnings
simonrozsival Sep 16, 2026
187a093
[tests] Update CoreCLR R8 APK size reference
simonrozsival Sep 16, 2026
b1655eb
[build] Reuse cached UTF-8 remap keys
simonrozsival Sep 18, 2026
1f354ca
[build] Compute UTF-8 remap lengths without allocation
simonrozsival Sep 18, 2026
2ab1f9c
[runtime] Use one replacement type lookup
simonrozsival Sep 18, 2026
d338ba5
[tests] Cover JNI remap ASCII chunk boundaries
simonrozsival Sep 19, 2026
58c5c31
[tests] Assert JNI remap target order
simonrozsival Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable enable

using System;
using System.Runtime.InteropServices;

namespace Java.Interop
{
Expand All @@ -19,19 +20,20 @@ internal bool IsValid {

#if DEBUG
string? name, signature;
IntPtr nameUtf8, signatureUtf8;
#endif // !DEBUG

public string Name {
#if DEBUG
get => name ?? throw new NotSupportedException ();
get => name ??= GetUtf8String (nameUtf8);
#else // !DEBUG
get => throw new NotSupportedException ();
#endif // !DEBUG
}

public string Signature {
#if DEBUG
get => signature ?? throw new NotSupportedException ();
get => signature ??= GetUtf8String (signatureUtf8);
#else // !DEBUG
get => throw new NotSupportedException ();
#endif // !DEBUG
Expand All @@ -55,11 +57,54 @@ public JniMethodInfo (string name, string signature, IntPtr methodID, bool isSta
#endif // DEBUG
}

internal JniMethodInfo (IntPtr nameUtf8, string signature, IntPtr methodID, bool isStatic)
{
ID = methodID;
IsStatic = isStatic;

#if DEBUG
this.nameUtf8 = nameUtf8;
this.signature = signature;
#endif // DEBUG
}

internal JniMethodInfo (IntPtr nameUtf8, IntPtr signatureUtf8, IntPtr methodID, bool isStatic)
{
ID = methodID;
IsStatic = isStatic;

#if DEBUG
this.nameUtf8 = nameUtf8;
this.signatureUtf8 = signatureUtf8;
#endif // DEBUG
}

internal JniMethodInfo (string name, IntPtr signatureUtf8, IntPtr methodID, bool isStatic)
{
ID = methodID;
IsStatic = isStatic;

#if DEBUG
this.name = name;
this.signatureUtf8 = signatureUtf8;
#endif // DEBUG
}

#if DEBUG
static unsafe string GetUtf8String (IntPtr value)
{
if (value == IntPtr.Zero)
throw new NotSupportedException ();

return System.Text.Encoding.UTF8.GetString (MemoryMarshal.CreateReadOnlySpanFromNullTerminated ((byte*)value));
}
#endif // DEBUG

public override string ToString ()
{
#if DEBUG
bool haveName = !string.IsNullOrEmpty (name);
bool haveSig = !string.IsNullOrEmpty (signature);
bool haveName = !string.IsNullOrEmpty (name) || nameUtf8 != IntPtr.Zero;
bool haveSig = !string.IsNullOrEmpty (signature) || signatureUtf8 != IntPtr.Zero;
#else // DEBUG
bool haveName = false;
bool haveSig = false;
Expand All @@ -73,4 +118,3 @@ public override string ToString ()
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -105,23 +105,23 @@ public JniMethodInfo GetMethodInfo (string encodedMember)
JniMethodInfo GetMethodInfo (ReadOnlySpan<char> method, ReadOnlySpan<char> signature)
{
var m = (JniMethodInfo?) null;
var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature);
var newMethod = Members.GetReplacementMethodInfo (method, signature);
if (newMethod.HasValue) {
var typeName = newMethod.Value.TargetJniType ?? Members.JniPeerTypeName;
var methodName = newMethod.Value.TargetJniMethodName is string name ? name.AsSpan () : method;
var methodSig = newMethod.Value.TargetJniMethodSignature is string sig ? sig.AsSpan () : signature;

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);
var info = newMethod.Value;
using var t = CreateTargetType (info, Members);
if (info.TargetJniMethodInstanceToStatic &&
TryGetStaticMethod (t, info, method, signature, out m)) {
m.ParameterCount = info.TargetJniMethodParameterCount;
m.StaticRedirect = CreateTargetType (info, Members);
return m;
}
if (t.TryGetInstanceMethod (methodName, methodSig, out m)) {
if (TryGetInstanceMethod (t, info, method, signature, out m)) {
return m;
}
Console.Error.WriteLine ($"warning: For declared method `{Members.JniPeerTypeName}.{method}.{signature}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!");
var targetType = GetTargetTypeNameForDiagnostics (info, Members);
var targetName = GetTargetMethodNameForDiagnostics (info, method);
var targetSignature = GetTargetMethodSignatureForDiagnostics (info, signature);
Console.Error.WriteLine ($"warning: For declared method `{Members.JniPeerTypeName}.{method}.{signature}`, could not find requested method `{targetType}.{targetName}.{targetSignature}`!");
}
return JniPeerType.GetInstanceMethod (method, signature);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,11 @@ public JniMethodInfo GetMethodInfo (string encodedMember)
JniMethodInfo GetMethodInfo (ReadOnlySpan<char> method, ReadOnlySpan<char> signature)
{
var m = (JniMethodInfo?) null;
var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature);
var newMethod = Members.GetReplacementMethodInfo (method, signature);
if (newMethod.HasValue) {
using var t = new JniType (newMethod.Value.TargetJniType ?? Members.JniPeerTypeName);
if (t.TryGetStaticMethod (
newMethod.Value.TargetJniMethodName is string name ? name.AsSpan () : method,
newMethod.Value.TargetJniMethodSignature is string sig ? sig.AsSpan () : signature,
out m)) {
var info = newMethod.Value;
using var t = CreateTargetType (info, Members);
if (TryGetStaticMethod (t, info, method, signature, out m)) {
return m;
}
}
Expand Down
145 changes: 130 additions & 15 deletions external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;

namespace Java.Interop {
Expand All @@ -14,26 +15,39 @@ public partial class JniPeerMembers {
private bool isInterface;

public JniPeerMembers (string jniPeerTypeName, Type managedPeerType, bool isInterface)
: this (jniPeerTypeName = GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: true, isInterface: isInterface)
: this (GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: true, isInterface: isInterface)
{
}

public JniPeerMembers (string jniPeerTypeName, Type managedPeerType)
: this (jniPeerTypeName = GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: true, isInterface: false)
: this (GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: true, isInterface: false)
{
}

static string GetReplacementType (string jniPeerTypeName)
readonly struct JniPeerTypeNameInfo
{
var replacement = JniEnvironment.Runtime.TypeManager.GetReplacementType (jniPeerTypeName);
if (replacement != null)
return replacement;
return jniPeerTypeName;
public JniPeerTypeNameInfo (string sourceName, string? targetName, IntPtr targetNameUtf8)
{
SourceName = sourceName;
TargetName = targetName;
TargetNameUtf8 = targetNameUtf8;
}

public string SourceName { get; }
public string? TargetName { get; }
public IntPtr TargetNameUtf8 { get; }
}

static JniPeerTypeNameInfo GetReplacementType (string jniPeerTypeName)
{
var typeManager = JniEnvironment.Runtime.TypeManager;
typeManager.GetReplacementTypeInfo (jniPeerTypeName, out var replacement, out var replacementUtf8);
return new JniPeerTypeNameInfo (jniPeerTypeName, replacement, replacementUtf8);
}

JniPeerMembers (string jniPeerTypeName, Type managedPeerType, bool checkManagedPeerType, bool isInterface = false)
JniPeerMembers (JniPeerTypeNameInfo jniPeerTypeName, Type managedPeerType, bool checkManagedPeerType, bool isInterface = false)
{
if (jniPeerTypeName == null)
if (jniPeerTypeName.SourceName == null)
throw new ArgumentNullException (nameof (jniPeerTypeName));

if (checkManagedPeerType) {
Expand All @@ -44,17 +58,19 @@ static string GetReplacementType (string jniPeerTypeName)

#if DEBUG
var signatureFromType = JniEnvironment.Runtime.TypeManager.GetTypeSignature (managedPeerType);
if (signatureFromType.SimpleReference != jniPeerTypeName) {
if (signatureFromType.SimpleReference != jniPeerTypeName.SourceName) {
Debug.WriteLine ("WARNING-Java.Interop: ManagedPeerType <=> JniTypeName Mismatch! javaVM.GetJniTypeInfoForType(typeof({0})).JniTypeName=\"{1}\" != \"{2}\"",
managedPeerType.FullName,
signatureFromType.SimpleReference,
jniPeerTypeName);
jniPeerTypeName.SourceName);
Debug.WriteLine (new System.Diagnostics.StackTrace (true));
}
#endif // DEBUG
}

JniPeerTypeName = jniPeerTypeName;
sourceJniPeerTypeName = jniPeerTypeName.SourceName;
this.jniPeerTypeName = jniPeerTypeName.TargetName;
jniPeerTypeNameUtf8 = jniPeerTypeName.TargetNameUtf8;
ManagedPeerType = managedPeerType;

this.isInterface = isInterface;
Expand All @@ -67,20 +83,27 @@ static string GetReplacementType (string jniPeerTypeName)

static JniPeerMembers CreatePeerMembers (string jniPeerTypeName, Type managedPeerType)
{
return new JniPeerMembers (jniPeerTypeName, managedPeerType, checkManagedPeerType: false);
return new JniPeerMembers (new JniPeerTypeNameInfo (jniPeerTypeName, null, IntPtr.Zero), managedPeerType, checkManagedPeerType: false);
}

JniType? jniPeerType;
string sourceJniPeerTypeName;
string? jniPeerTypeName;
IntPtr jniPeerTypeNameUtf8;
JniInstanceMethods instanceMethods;
JniInstanceFields instanceFields;
JniStaticMethods staticMethods;
JniStaticFields staticFields;

public Type ManagedPeerType {get; private set;}
public string JniPeerTypeName {get; private set;}
public string JniPeerTypeName => jniPeerTypeNameUtf8 == IntPtr.Zero
? jniPeerTypeName ?? sourceJniPeerTypeName
: jniPeerTypeName ??= GetUtf8String (jniPeerTypeNameUtf8);
public JniType JniPeerType {
get {
var t = JniType.GetCachedJniType (ref jniPeerType, JniPeerTypeName);
var t = jniPeerTypeNameUtf8 == IntPtr.Zero
? JniType.GetCachedJniType (ref jniPeerType, jniPeerTypeName ?? sourceJniPeerTypeName)
: JniType.GetCachedJniType (ref jniPeerType, jniPeerTypeNameUtf8);
t.RegisterWithRuntime ();
return t;
}
Expand Down Expand Up @@ -167,6 +190,98 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value)
return isInterface ? this : value.JniPeerMembers;
}

JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (ReadOnlySpan<char> method, ReadOnlySpan<char> signature)
{
return jniPeerTypeNameUtf8 == IntPtr.Zero
? JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (jniPeerTypeName ?? sourceJniPeerTypeName, method, signature)
: JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (jniPeerTypeNameUtf8, method, signature);
}

static JniType CreateTargetType (JniRuntime.ReplacementMethodInfo info, JniPeerMembers fallback)
{
if (info.TargetJniTypeUtf8 != IntPtr.Zero)
return new JniType (info.TargetJniTypeUtf8);
if (info.TargetJniType != null)
return new JniType (info.TargetJniType);
return fallback.jniPeerTypeNameUtf8 != IntPtr.Zero
? new JniType (fallback.jniPeerTypeNameUtf8)
: new JniType (fallback.jniPeerTypeName ?? fallback.sourceJniPeerTypeName);
}

static bool TryGetInstanceMethod (
JniType type,
JniRuntime.ReplacementMethodInfo info,
ReadOnlySpan<char> fallbackName,
ReadOnlySpan<char> fallbackSignature,
[System.Diagnostics.CodeAnalysis.NotNullWhen (true)] out JniMethodInfo? method)
{
if (info.TargetJniMethodNameUtf8 != IntPtr.Zero) {
if (info.TargetJniMethodSignatureUtf8 != IntPtr.Zero)
return type.TryGetInstanceMethod (info.TargetJniMethodNameUtf8, info.TargetJniMethodSignatureUtf8, out method);
var signature = info.TargetJniMethodSignature is string targetSignature ? targetSignature.AsSpan () : fallbackSignature;
return type.TryGetInstanceMethod (info.TargetJniMethodNameUtf8, signature, out method);
}

var name = info.TargetJniMethodName is string targetName ? targetName.AsSpan () : fallbackName;
if (info.TargetJniMethodSignatureUtf8 != IntPtr.Zero)
return type.TryGetInstanceMethod (name, info.TargetJniMethodSignatureUtf8, out method);
var fallback = info.TargetJniMethodSignature is string targetSignatureValue ? targetSignatureValue.AsSpan () : fallbackSignature;
return type.TryGetInstanceMethod (name, fallback, out method);
}

static bool TryGetStaticMethod (
JniType type,
JniRuntime.ReplacementMethodInfo info,
ReadOnlySpan<char> fallbackName,
ReadOnlySpan<char> fallbackSignature,
[System.Diagnostics.CodeAnalysis.NotNullWhen (true)] out JniMethodInfo? method)
{
if (info.TargetJniMethodNameUtf8 != IntPtr.Zero) {
if (info.TargetJniMethodSignatureUtf8 != IntPtr.Zero)
return type.TryGetStaticMethod (info.TargetJniMethodNameUtf8, info.TargetJniMethodSignatureUtf8, out method);
var signature = info.TargetJniMethodSignature is string targetSignature ? targetSignature.AsSpan () : fallbackSignature;
return type.TryGetStaticMethod (info.TargetJniMethodNameUtf8, signature, out method);
}

var name = info.TargetJniMethodName is string targetName ? targetName.AsSpan () : fallbackName;
if (info.TargetJniMethodSignatureUtf8 != IntPtr.Zero)
return type.TryGetStaticMethod (name, info.TargetJniMethodSignatureUtf8, out method);
var fallback = info.TargetJniMethodSignature is string targetSignatureValue ? targetSignatureValue.AsSpan () : fallbackSignature;
return type.TryGetStaticMethod (name, fallback, out method);
}

static unsafe string GetUtf8String (IntPtr value)
{
return System.Text.Encoding.UTF8.GetString (MemoryMarshal.CreateReadOnlySpanFromNullTerminated ((byte*)value));
}

static string GetTargetTypeNameForDiagnostics (JniRuntime.ReplacementMethodInfo info, JniPeerMembers fallback)
{
if (info.TargetJniTypeUtf8 != IntPtr.Zero)
return GetUtf8String (info.TargetJniTypeUtf8);
if (info.TargetJniType != null)
return info.TargetJniType;
return fallback.JniPeerTypeName;
}

static string GetTargetMethodNameForDiagnostics (JniRuntime.ReplacementMethodInfo info, ReadOnlySpan<char> fallback)
{
if (info.TargetJniMethodNameUtf8 != IntPtr.Zero)
return GetUtf8String (info.TargetJniMethodNameUtf8);
if (info.TargetJniMethodName != null)
return info.TargetJniMethodName;
return fallback.ToString ();
}

static string GetTargetMethodSignatureForDiagnostics (JniRuntime.ReplacementMethodInfo info, ReadOnlySpan<char> fallback)
{
if (info.TargetJniMethodSignatureUtf8 != IntPtr.Zero)
return GetUtf8String (info.TargetJniMethodSignatureUtf8);
if (info.TargetJniMethodSignature != null)
return info.TargetJniMethodSignature;
return fallback.ToString ();
}

internal static void AssertSelf (IJavaPeerable self)
{
if (self == null)
Expand Down
Loading
Loading