From d63fe4c9f8cf8def6049c9415bb0abf4269ee0f5 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 18 Sep 2026 11:30:09 +0200
Subject: [PATCH 01/19] Localize unsupported Java reference error (#12803)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
> AI-generated fix. Produced by the `nightly-fix-finder` agentic workflow.
### Problem
`JavaSourceUtils` emitted a hardcoded, uncoded error when an `@(Reference)` item was neither a Java source directory nor a `.jar` or `.aar` file. This customer-facing diagnostic bypassed the build task resource system and could not be localized.
### Fix
Added localized error `XA1037` to the English resources and checked-in accessor, and updated `JavaSourceUtils` to emit the coded resource while preserving the message, argument, severity, and control flow. Added the corresponding XA1037 message documentation, index entry, and table-of-contents entry.
### Validation
- `AndroidSdkDirectory="$GITHUB_WORKSPACE/bin/Debug/android-sdk" AndroidNdkDirectory="$GITHUB_WORKSPACE/bin/Debug/android-ndk" ./build.sh Prepare` — passed
- `GRADLE_USER_HOME=/tmp/gh-aw/agent/gradle AndroidSdkDirectory="$GITHUB_WORKSPACE/bin/Debug/android-sdk" AndroidNdkDirectory="$GITHUB_WORKSPACE/bin/Debug/android-ndk" ./dotnet-local.sh build src/AndroidBuildConfig/AndroidBuildConfig.csproj --no-restore -m:1` — passed
- `GRADLE_USER_HOME=/tmp/gh-aw/agent/gradle AndroidSdkDirectory="$GITHUB_WORKSPACE/bin/Debug/android-sdk" AndroidNdkDirectory="$GITHUB_WORKSPACE/bin/Debug/android-ndk" ./dotnet-local.sh build external/Java.Interop/src/Java.Interop.Tools.Maven/Java.Interop.Tools.Maven.csproj --no-restore -m:1` — passed
- `GRADLE_USER_HOME=/tmp/gh-aw/agent/gradle AndroidSdkDirectory="$GITHUB_WORKSPACE/bin/Debug/android-sdk" AndroidNdkDirectory="$GITHUB_WORKSPACE/bin/Debug/android-ndk" ./dotnet-local.sh build src/Xamarin.Android.Tools.Aidl/Xamarin.Android.Tools.Aidl.csproj --no-restore -m:1` — passed
- `GRADLE_USER_HOME=/tmp/gh-aw/agent/gradle AndroidSdkDirectory="$GITHUB_WORKSPACE/bin/Debug/android-sdk" AndroidNdkDirectory="$GITHUB_WORKSPACE/bin/Debug/android-ndk" ./dotnet-local.sh build src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj --no-restore -p:BuildProjectReferences=false -m:1` — passed
### Fix-finder metadata
- Script: `08-string-literal-error-messages`
- Score: `26/30` (actionability: `9`, safety: `9`, scope: `8`)
> Generated by [Nightly Fix Finder](https://github.com/dotnet/android/actions/runs/35045607028) · copilot · gpt56 · 105.2 AIC · ⌖ 27.2 AIC · ⊞ 23.3K · [◷](https://github.com/search?q=repo%3Adotnet%2Fandroid+%22gh-aw-workflow-id%3A+nightly-fix-finder%22&type=pullrequests)
---
Documentation/docs-mobile/TOC.yml | 2 ++
Documentation/docs-mobile/messages/index.md | 1 +
Documentation/docs-mobile/messages/xa1037.md | 25 +++++++++++++++++++
.../Properties/Resources.Designer.cs | 10 ++++++++
.../Properties/Resources.resx | 5 ++++
.../Tasks/JavaSourceUtils.cs | 2 +-
6 files changed, 44 insertions(+), 1 deletion(-)
create mode 100644 Documentation/docs-mobile/messages/xa1037.md
diff --git a/Documentation/docs-mobile/TOC.yml b/Documentation/docs-mobile/TOC.yml
index 6116d95ea22..12e5254caf6 100644
--- a/Documentation/docs-mobile/TOC.yml
+++ b/Documentation/docs-mobile/TOC.yml
@@ -262,6 +262,8 @@
href: messages/xa1035.md
- name: XA1036
href: messages/xa1036.md
+ - name: XA1037
+ href: messages/xa1037.md
- name: XA1038
href: messages/xa1038.md
- name: XA1042
diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md
index abe2d010109..833e4ecf280 100644
--- a/Documentation/docs-mobile/messages/index.md
+++ b/Documentation/docs-mobile/messages/index.md
@@ -147,6 +147,7 @@ or 'Help->Report a Problem' in Visual Studio for Mac.
+ [XA1035](xa1035.md): The 'BundleAssemblies' property is deprecated and it has no effect on the application build. Equivalent functionality is implemented by the 'AndroidUseAssemblyStore' and 'AndroidEnableAssemblyCompression' properties.
+ [XA1036](xa1036.md): AndroidManifest.xml //uses-sdk/@android:minSdkVersion '29' does not match the $(SupportedOSPlatformVersion) value '21' in the project file (if there is no $(SupportedOSPlatformVersion) value in the project file, then a default value has been assumed).
Either change the value in the AndroidManifest.xml to match the $(SupportedOSPlatformVersion) value, or remove the value in the AndroidManifest.xml (and add a $(SupportedOSPlatformVersion) value to the project file if it doesn't already exist).
++ [XA1037](xa1037.md): Unsupported @(Reference) item: {item}
+ [XA1038](xa1038.md): The '{0}' MSBuild property has an invalid value. Value values are {1}.
+ [XA1039](xa1039.md): The Android Support libraries are not supported in .NET 9 and later, please migrate to AndroidX. See https://aka.ms/xamarin/androidx for more details.
+ [XA1040](xa1040.md): The NativeAOT runtime on Android is an experimental feature and not yet suitable for production use. File issues at: https://github.com/dotnet/android/issues
diff --git a/Documentation/docs-mobile/messages/xa1037.md b/Documentation/docs-mobile/messages/xa1037.md
new file mode 100644
index 00000000000..785a76607d4
--- /dev/null
+++ b/Documentation/docs-mobile/messages/xa1037.md
@@ -0,0 +1,25 @@
+---
+title: .NET for Android error XA1037
+description: XA1037 error code
+ms.date: 09/16/2026
+f1_keywords:
+ - "XA1037"
+---
+
+# .NET for Android error XA1037
+
+## Example messages
+
+```
+error XA1037: Unsupported @(Reference) item: {item}
+```
+
+## Issue
+
+The specified `@(Reference)` item is not a supported Java archive or source
+directory.
+
+## Solution
+
+Update the `@(Reference)` item to reference a `.jar` file, an `.aar` file, or
+a Java source directory.
diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs
index 13986a46813..ade085c7ac3 100644
--- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs
+++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs
@@ -917,6 +917,16 @@ public static string XA1036 {
return ResourceManager.GetString("XA1036", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to Unsupported @(Reference) item: {0}.
+ ///
+ public static string XA1037 {
+ get {
+ return ResourceManager.GetString("XA1037", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The '{0}' MSBuild property has an invalid value of '{1}'. A valid value is one of: {2}..
///
diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx
index c85742d1345..476ac8ae805 100644
--- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx
+++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx
@@ -504,6 +504,11 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla
The following are literal names and should not be translated: AndroidManifest.xml, //uses-sdk/@android:minSdkVersion, $(SupportedOSPlatformVersion)
{0} - The minimum SDK version number
{1} - The SupportedOSPlatformVersion property value
+
+
+ Unsupported @(Reference) item: {0}
+ The following are literal names and should not be translated: @(Reference)
+{0} - The unsupported reference item
Use of AppDomain.CreateDomain() detected in assembly: {0}. .NET 6 and higher will only support a single AppDomain, so this API will no longer be available in .NET for Android once .NET 6 is released.
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/JavaSourceUtils.cs b/src/Xamarin.Android.Build.Tasks/Tasks/JavaSourceUtils.cs
index 55be2657710..e578a96a9cc 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/JavaSourceUtils.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/JavaSourceUtils.cs
@@ -130,7 +130,7 @@ string CreateResponseFile ()
AppendArg (response, Path.GetFullPath (r.ItemSpec));
continue;
}
- Log.LogError ($"Unsupported @(Reference) item: {r.ItemSpec}");
+ Log.LogCodedError ("XA1037", Properties.Resources.XA1037, r.ItemSpec);
}
}
AppendArg (response, "--output-javadoc");
From 94a62a6fa6eac67d961e27c1a8a83f016a955c9e Mon Sep 17 00:00:00 2001
From: Jonathan Peppers
Date: Fri, 18 Sep 2026 13:50:13 -0500
Subject: [PATCH 02/19] [Mono.Android] Align CoreCLR activation caching with
Mono (#12816)
Context: https://github.com/dotnet/android/pull/12354
Context: https://github.com/dotnet/android/pull/12409
Context: https://github.com/dotnet/android/issues/7479
Mono's TypeManager already caches peer activation constructors, but
Microsoft.Android.Runtime.JavaMarshalValueManager.TryConstructPeer()
still repeats reflection lookup for CoreCLR. Add the same cache pattern
privately to JavaMarshalValueManager without adding a dependency on the
legacy TypeManager implementation.
Preserve XA-first lookup, JI fallback, missing-constructor results,
constructor matching, existing peer identity, exception behavior, JNI
reference ownership, and constructor-preservation annotations.
Add focused coverage for cached/concurrent resolution and actual
CoreCLR activation, including constructor failures and reference
ownership. The trimmed Release CoreCLR device run passes all 13 new
cases and 3 existing ReflectionCreateProxy cases.
A Pixel 7 benchmark of warm TryConstructPeer activation and disposal
improves from 3161.501 ns to 2915.512 ns (7.78% less time), with managed
allocations reduced from 248 B to 144 B per operation.
Separate matched Jetchat and MAUI sample-content startup checks detect
no statistically clear regression; they do not establish a startup
speedup.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../JavaMarshalValueManager.cs | 55 +++-
.../ActivationConstructorCacheTests.cs | 251 ++++++++++++++++++
.../Mono.Android.NET-Tests.csproj | 1 +
3 files changed, 304 insertions(+), 3 deletions(-)
create mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ActivationConstructorCacheTests.cs
diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs
index 1c026b85e9d..210f29675ff 100644
--- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs
+++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
@@ -15,6 +16,29 @@ sealed class JavaMarshalValueManager : JniRuntime.ReflectionJniValueManager
const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) };
+ static readonly Type[] JIConstructorSignature = new Type [] { typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions) };
+ static readonly ConcurrentDictionary ActivationConstructorCache = new ConcurrentDictionary (1, 3);
+
+ enum ActivationConstructorKind
+ {
+ Missing,
+ XA,
+ JI,
+ }
+
+ readonly record struct ActivationConstructor (ConstructorInfo? Constructor, ActivationConstructorKind Kind);
+
+ // The GetOrAdd factory's key parameter cannot carry constructor-preservation annotations.
+ readonly struct AnnotatedType
+ {
+ public AnnotatedType ([DynamicallyAccessedMembers (Constructors)] Type type)
+ {
+ Type = type;
+ }
+
+ [DynamicallyAccessedMembers (Constructors)]
+ public Type Type { get; }
+ }
public JavaMarshalValueManager ()
{
@@ -76,8 +100,12 @@ protected override bool TryConstructPeer (
[DynamicallyAccessedMembers (Constructors)]
Type type)
{
- var c = type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null);
- if (c != null) {
+ var activation = GetActivationConstructor (type);
+ var c = activation.Constructor;
+ if (c == null)
+ return false;
+
+ if (activation.Kind == ActivationConstructorKind.XA) {
var args = new object[] {
reference.Handle,
JniHandleOwnership.DoNotTransfer,
@@ -86,7 +114,28 @@ protected override bool TryConstructPeer (
JniObjectReference.Dispose (ref reference, options);
return true;
}
- return base.TryConstructPeer (self, ref reference, options, type);
+
+ // Preserve ReflectionJniValueManager's JI fallback, including ref argument copy-back.
+ var jiArgs = new object[] { reference, options };
+ c.Invoke (self, jiArgs);
+ reference = (JniObjectReference) jiArgs [0];
+ JniObjectReference.Dispose (ref reference, options);
+ return true;
+ }
+
+ static ActivationConstructor GetActivationConstructor ([DynamicallyAccessedMembers (Constructors)] Type type)
+ {
+ return ActivationConstructorCache.GetOrAdd (type,
+ static (_, state) => {
+ var constructor = state.Type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null);
+ if (constructor != null)
+ return new ActivationConstructor (constructor, ActivationConstructorKind.XA);
+
+ constructor = state.Type.GetConstructor (ActivationConstructorBindingFlags, null, JIConstructorSignature, null);
+ return new ActivationConstructor (
+ constructor,
+ constructor == null ? ActivationConstructorKind.Missing : ActivationConstructorKind.JI);
+ }, new AnnotatedType (type));
}
protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (true)] out object? result)
diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ActivationConstructorCacheTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ActivationConstructorCacheTests.cs
new file mode 100644
index 00000000000..066a0219af6
--- /dev/null
+++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ActivationConstructorCacheTests.cs
@@ -0,0 +1,251 @@
+#nullable enable
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Android.Runtime;
+using Java.Interop;
+using NUnit.Framework;
+using JavaObject = Java.Interop.JavaObject;
+
+namespace Java.InteropTests;
+
+[TestFixture]
+[Category ("ReflectionActivationCache")]
+public class ActivationConstructorCacheTests
+{
+ const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;
+
+ delegate bool TryConstructPeerDelegate (IJavaPeerable self, ref JniObjectReference reference, JniObjectReferenceOptions options, Type type);
+
+ [TestCase (typeof (XAPeer), 1)]
+ [TestCase (typeof (JIPeer), 2)]
+ [TestCase (typeof (MissingPeer), 2)]
+ public void ResolutionIsCached (Type peerType, int expectedLookups)
+ {
+ AssumeReflectionActivation ();
+ var type = new CountingType (peerType);
+ var resolve = GetResolver ();
+ var activation = resolve.Invoke (null, new object [] { type });
+ Assert.AreEqual (expectedLookups, type.Lookups);
+ for (int i = 0; i < 10; i++)
+ Assert.AreEqual (activation, resolve.Invoke (null, new object [] { type }));
+ Assert.AreEqual (expectedLookups, type.Lookups, "Warm resolution must not enter the reflection binder.");
+ }
+
+ [TestCase (typeof (XAPeer))]
+ [TestCase (typeof (JIPeer))]
+ [TestCase (typeof (MissingPeer))]
+ public void ConcurrentResolutionPublishesConsistentResult (Type peerType)
+ {
+ AssumeReflectionActivation ();
+ var type = new CountingType (peerType);
+ var resolve = GetResolver ();
+ var results = new object? [32];
+ Parallel.For (0, results.Length, i => results [i] = resolve.Invoke (null, new object [] { type }));
+ foreach (var result in results)
+ Assert.AreEqual (results [0], result);
+ var lookups = type.Lookups;
+ Assert.Greater (lookups, 0);
+ Parallel.For (0, results.Length, i => resolve.Invoke (null, new object [] { type }));
+ Assert.AreEqual (lookups, type.Lookups);
+ }
+
+ [TestCase (false, JniObjectReferenceOptions.Copy)]
+ [TestCase (false, JniObjectReferenceOptions.CopyAndDispose)]
+ [TestCase (true, JniObjectReferenceOptions.Copy)]
+ [TestCase (true, JniObjectReferenceOptions.CopyAndDispose)]
+ public void CoreClrConstructsExistingPeerAndPreservesOwnership (bool ji, JniObjectReferenceOptions options)
+ {
+ var construct = GetCoreClrConstructor ();
+ var peerType = ji ? typeof (JIPeer) : typeof (XAPeer);
+ var type = new CountingType (peerType);
+ for (int i = 0; i < 2; i++) {
+ using var source = new JavaObject ();
+ var reference = source.PeerReference.NewLocalRef ();
+ var self = (Java.Lang.Object) RuntimeHelpers.GetUninitializedObject (peerType);
+ ((IJavaPeerable) self).SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable);
+ try {
+ Assert.IsTrue (construct (self, ref reference, options, type));
+ Assert.IsTrue (JniEnvironment.Types.IsSameObject (source.PeerReference, self.PeerReference));
+ if (self is XAPeer xa) {
+ Assert.AreSame (self, xa.ConstructedSelf);
+ Assert.AreEqual (JniHandleOwnership.DoNotTransfer, xa.Transfer);
+ Assert.IsFalse (xa.UsedJI, "XA must win when both constructors exist.");
+ } else if (self is JIPeer jp) {
+ Assert.AreSame (self, jp.ConstructedSelf);
+ Assert.AreEqual (options, jp.Options);
+ }
+ Assert.AreEqual (options == JniObjectReferenceOptions.Copy, reference.IsValid);
+ } finally {
+ self.Dispose ();
+ JniObjectReference.Dispose (ref reference);
+ }
+ }
+ Assert.AreEqual (ji ? 2 : 1, type.Lookups, "Repeated construction must reuse the value manager's cache.");
+ }
+
+ [Test]
+ public void CoreClrMissingConstructorLeavesReferenceAndPeerUntouched ()
+ {
+ var construct = GetCoreClrConstructor ();
+ var type = new CountingType (typeof (MissingPeer));
+ using var source = new JavaObject ();
+ var reference = source.PeerReference.NewLocalRef ();
+ var original = reference;
+ var self = (MissingPeer) RuntimeHelpers.GetUninitializedObject (typeof (MissingPeer));
+ GC.SuppressFinalize (self);
+ try {
+ for (int i = 0; i < 2; i++) {
+ Assert.IsFalse (construct (self, ref reference, JniObjectReferenceOptions.CopyAndDispose, type));
+ Assert.AreEqual (original, reference);
+ Assert.IsFalse (self.PeerReference.IsValid);
+ }
+ Assert.AreEqual (2, type.Lookups);
+ } finally {
+ JniObjectReference.Dispose (ref reference);
+ }
+ }
+
+ [TestCase (typeof (ThrowingXAPeer))]
+ [TestCase (typeof (ThrowingJIPeer))]
+ public void CoreClrThrowingConstructorDoesNotDisposeOrCopyBackReference ([DynamicallyAccessedMembers (Constructors)] Type peerType)
+ {
+ var construct = GetCoreClrConstructor ();
+ var type = new CountingType (peerType);
+ using var source = new JavaObject ();
+ var reference = source.PeerReference.NewLocalRef ();
+ var original = reference;
+ try {
+ for (int i = 0; i < 2; i++) {
+ var self = (Java.Lang.Object) RuntimeHelpers.GetUninitializedObject (peerType);
+ GC.SuppressFinalize (self);
+ var exception = Assert.Throws (() =>
+ construct (self, ref reference, JniObjectReferenceOptions.CopyAndDispose, type));
+ Assert.IsInstanceOf (exception?.InnerException);
+ Assert.AreEqual ("activation failed", exception?.InnerException?.Message);
+ Assert.AreEqual (original, reference);
+ Assert.IsTrue (JniEnvironment.Types.IsSameObject (source.PeerReference, reference));
+ }
+ Assert.AreEqual (peerType == typeof (ThrowingXAPeer) ? 1 : 2, type.Lookups);
+ } finally {
+ JniObjectReference.Dispose (ref reference);
+ }
+ }
+
+ [DynamicDependency (Constructors, typeof (XAPeer))]
+ [DynamicDependency (Constructors, typeof (JIPeer))]
+ [DynamicDependency (Constructors, typeof (MissingPeer))]
+ [DynamicDependency (Constructors, typeof (ThrowingXAPeer))]
+ [DynamicDependency (Constructors, typeof (ThrowingJIPeer))]
+ static void AssumeReflectionActivation ()
+ {
+ if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap)
+ Assert.Ignore ("This test exercises reflection activation, not the generated trimmable typemap.");
+ }
+
+ [UnconditionalSuppressMessage ("Trimming", "IL2111", Justification = "Only the explicitly preserved test peer constructors are resolved, through CountingType.")]
+ [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This test explicitly exercises the reflection value manager with preserved peer constructors.")]
+ static MethodInfo GetResolver ()
+ {
+ var managerType = Type.GetType ("Microsoft.Android.Runtime.JavaMarshalValueManager, Mono.Android", throwOnError: true)
+ ?? throw new InvalidOperationException ("Could not find the CoreCLR reflection value manager.");
+ return managerType.GetMethod ("GetActivationConstructor", BindingFlags.NonPublic | BindingFlags.Static)
+ ?? throw new InvalidOperationException ("Could not find the value manager's activation constructor resolver.");
+ }
+
+ [UnconditionalSuppressMessage ("Trimming", "IL2111", Justification = "Only the explicitly preserved test peer constructors are invoked, through CountingType.")]
+ [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This test explicitly exercises the reflection value manager with preserved peer constructors.")]
+ static TryConstructPeerDelegate GetCoreClrConstructor ()
+ {
+ AssumeReflectionActivation ();
+ if (!AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.IsCoreClrRuntime", out bool isCoreClr) || !isCoreClr)
+ Assert.Ignore ("This test exercises the CoreCLR reflection value manager.");
+ var manager = JniEnvironment.Runtime.ValueManager;
+ Assert.AreEqual ("Microsoft.Android.Runtime.JavaMarshalValueManager", manager.GetType ().FullName);
+ var managerType = Type.GetType ("Microsoft.Android.Runtime.JavaMarshalValueManager, Mono.Android", throwOnError: true)
+ ?? throw new InvalidOperationException ("Could not find the CoreCLR reflection value manager.");
+ var method = managerType.GetMethod ("TryConstructPeer", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
+ ?? throw new InvalidOperationException ("Could not find CoreCLR TryConstructPeer.");
+ return method.CreateDelegate (manager);
+ }
+
+ sealed class CountingType : TypeDelegator
+ {
+ int lookups;
+ public int Lookups => Volatile.Read (ref lookups);
+
+ public CountingType (Type type) : base (type) {}
+
+ public override bool Equals (object? other) => ReferenceEquals (this, other);
+ public override bool Equals (Type? other) => ReferenceEquals (this, other);
+ public override int GetHashCode () => RuntimeHelpers.GetHashCode (this);
+
+ [DynamicallyAccessedMembers (Constructors)]
+ protected override ConstructorInfo? GetConstructorImpl (BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type [] types, ParameterModifier []? modifiers)
+ {
+ Interlocked.Increment (ref lookups);
+ Assert.AreEqual (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, bindingAttr);
+ Assert.IsNull (binder);
+ return base.GetConstructorImpl (bindingAttr, binder, callConvention, types, modifiers);
+ }
+ }
+
+ sealed class XAPeer : Java.Lang.Object
+ {
+ public object? ConstructedSelf;
+ public JniHandleOwnership Transfer;
+ public bool UsedJI;
+
+ internal XAPeer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer)
+ {
+ ConstructedSelf = this;
+ Transfer = transfer;
+ }
+
+ public XAPeer (ref JniObjectReference reference, JniObjectReferenceOptions options) : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer)
+ {
+ UsedJI = true;
+ Construct (ref reference, options);
+ }
+ }
+
+ sealed class JIPeer : Java.Lang.Object
+ {
+ public object? ConstructedSelf;
+ public JniObjectReferenceOptions Options;
+
+ internal JIPeer (ref JniObjectReference reference, JniObjectReferenceOptions options) : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer)
+ {
+ ConstructedSelf = this;
+ Options = options;
+ Construct (ref reference, options);
+ }
+ }
+
+ sealed class MissingPeer : Java.Lang.Object
+ {
+ public MissingPeer () {}
+ }
+
+ sealed class ThrowingXAPeer : Java.Lang.Object
+ {
+ public ThrowingXAPeer (IntPtr handle, JniHandleOwnership transfer) : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer)
+ {
+ throw new InvalidOperationException ("activation failed");
+ }
+ }
+
+ sealed class ThrowingJIPeer : Java.Lang.Object
+ {
+ public ThrowingJIPeer (ref JniObjectReference reference, JniObjectReferenceOptions options) : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer)
+ {
+ reference = default;
+ throw new InvalidOperationException ("activation failed");
+ }
+ }
+}
diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj
index 9c44a461bea..698976ce66c 100644
--- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj
+++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj
@@ -138,6 +138,7 @@
+
From 5fe19c2dda97e5141a483d561663afd9988d9c48 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 18 Sep 2026 13:50:54 -0500
Subject: [PATCH 03/19] Bump com.android.tools:r8 from 9.4.17 to 9.4.24 in
/src/r8 (#12819)
Bumps com.android.tools:r8 from 9.4.17 to 9.4.24.
---
updated-dependencies:
- dependency-name: com.android.tools:r8
dependency-version: 9.4.24
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
src/r8/build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/r8/build.gradle b/src/r8/build.gradle
index 7d295115ba9..fa4ee88b372 100644
--- a/src/r8/build.gradle
+++ b/src/r8/build.gradle
@@ -10,7 +10,7 @@ java {
}
dependencies {
- implementation 'com.android.tools:r8:9.4.17'
+ implementation 'com.android.tools:r8:9.4.24'
}
jar {
From bd533324603a91a890879a63b12bf681ed601360 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?=
Date: Fri, 18 Sep 2026 21:13:00 +0200
Subject: [PATCH 04/19] [Java.Interop] Use native UTF-8 JNI remaps (#12795)
## Motivation
The existing Intune/MAM JNI method-remapping path stores target type and method names as stable NUL-terminated UTF-8 strings in generated native data. The old path materialized those names as managed UTF-16 strings and then encoded them back to UTF-8 for `FindClass` and `GetMethodID`.
That round trip is unnecessary and becomes more important as remapping is reused by larger consumers such as R8.
## Approach
- Add pointer-backed target type, method-name, and method-signature values to `ReplacementMethodInfo`.
- Keep pointer and string representations independent so reading a compatibility string property does not implicitly decode native memory.
- Add `JniType` lookup paths for pointer/pointer and mixed pointer/span member names and signatures.
- Let `JniPeerMembers` retain a replacement type pointer and use it directly for `FindClass` and later member-remapping lookups.
- Keep native pointers in `JniMethodInfo` Debug metadata and decode them only if `Name`, `Signature`, or `ToString()` is explicitly requested.
- Retain the existing string/span paths for custom `JniTypeManager` implementations.
- Document UTF-8 encoding, NUL termination, ownership, and lifetime requirements for every pointer API.
The successful generated-remapping path therefore passes the pregenerated UTF-8 type, method name, and optional signature directly to JNI without copying them or converting them to a managed string.
This PR is method-only and does not add R8, field remapping, reverse-type mapping, inherited-member fallback, or NativeAOT remapping support.
## Relationship to other PRs
- This PR is based directly on `main` and provides the Java.Interop representation and JNI lookup primitives.
- #12796 supplies the generated pointers, performs managed table search, and removes the native remapping P/Invokes.
- #12692 can build on this stack to add the R8-specific field, reverse-type, inherited-member, and NativeAOT pieces.
## Validation
- Java.Interop Debug build.
- Java.Interop `JniPeerMembersTests`: 12 passed, 1 skipped.
- The JVM fixture exercises stable unmanaged UTF-8 type/name/signature storage, signature fallback, instance/static lookup, and instance-to-static remapping.
---
.../Java.Interop/JniMethodInfo.cs | 54 ++++-
.../JniPeerMembers.JniInstanceMethods.cs | 24 +--
.../JniPeerMembers.JniStaticMethods.cs | 10 +-
.../Java.Interop/JniPeerMembers.cs | 145 +++++++++++--
.../Java.Interop/JniRuntime.JniTypeManager.cs | 116 +++++++++-
.../src/Java.Interop/Java.Interop/JniType.cs | 199 ++++++++++++++----
.../src/Java.Interop/PublicAPI.Unshipped.txt | 8 +
.../Java.Interop/JavaVMFixture.cs | 103 +++++++--
.../Java.Interop/JniPeerMembersTests.cs | 37 ++++
.../JniRuntime.JniTypeManagerTests.cs | 78 +++++++
.../Mono.Android-Tests/Remaps.xml | 8 +
11 files changed, 677 insertions(+), 105 deletions(-)
diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniMethodInfo.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniMethodInfo.cs
index 928a53a902c..bd17a8052a3 100644
--- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniMethodInfo.cs
+++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniMethodInfo.cs
@@ -1,6 +1,7 @@
#nullable enable
using System;
+using System.Runtime.InteropServices;
namespace Java.Interop
{
@@ -19,11 +20,12 @@ 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
@@ -31,7 +33,7 @@ public string Name {
public string Signature {
#if DEBUG
- get => signature ?? throw new NotSupportedException ();
+ get => signature ??= GetUtf8String (signatureUtf8);
#else // !DEBUG
get => throw new NotSupportedException ();
#endif // !DEBUG
@@ -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;
@@ -73,4 +118,3 @@ public override string ToString ()
}
}
}
-
diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs
index f9e3092ddfa..0f834bba544 100644
--- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs
+++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs
@@ -105,23 +105,23 @@ public JniMethodInfo GetMethodInfo (string encodedMember)
JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan 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);
}
diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs
index a7f8ce9a096..107b97d763b 100644
--- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs
+++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs
@@ -36,13 +36,11 @@ public JniMethodInfo GetMethodInfo (string encodedMember)
JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan 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;
}
}
diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs
index 1b2242181d2..60d52d916ad 100644
--- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs
+++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
+using System.Runtime.InteropServices;
using System.Threading;
namespace Java.Interop {
@@ -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) {
@@ -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;
@@ -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;
}
@@ -167,6 +190,98 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value)
return isInterface ? this : value.JniPeerMembers;
}
+ JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (ReadOnlySpan method, ReadOnlySpan 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 fallbackName,
+ ReadOnlySpan 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 fallbackName,
+ ReadOnlySpan 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 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 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)
diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs
index a58c1f92da4..ea5f2ab5ba6 100644
--- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs
+++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs
@@ -7,13 +7,23 @@
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Runtime.Versioning;
+using System.Text;
using System.Threading;
namespace Java.Interop {
public partial class JniRuntime {
+ static unsafe string GetUtf8String (IntPtr value)
+ {
+ if (value == IntPtr.Zero)
+ return "";
+
+ return Encoding.UTF8.GetString (MemoryMarshal.CreateReadOnlySpanFromNullTerminated ((byte*)value));
+ }
+
[SuppressMessage ("Design", "CA1034:Nested types should not be visible",
Justification = "Deliberate choice to 'hide' these types from code completion for `Java.Interop.`; see 045b8af7.")]
public struct ReplacementMethodInfo : IEquatable
@@ -24,6 +34,33 @@ public struct ReplacementMethodInfo : IEquatable
public string? TargetJniType {get; set;}
public string? TargetJniMethodName {get; set;}
public string? TargetJniMethodSignature {get; set;}
+ ///
+ /// Gets or sets a pointer to a NUL-terminated UTF-8 JNI type name.
+ ///
+ ///
+ /// Java.Interop does not own or free this memory. A non-zero pointer must remain valid
+ /// and unchanged for the lifetime of the associated , because
+ /// cached JNI type and method metadata may retain and dereference it.
+ ///
+ public IntPtr TargetJniTypeUtf8 {get; set;}
+ ///
+ /// Gets or sets a pointer to a NUL-terminated UTF-8 JNI method name.
+ ///
+ ///
+ /// Java.Interop does not own or free this memory. A non-zero pointer must remain valid
+ /// and unchanged for the lifetime of the associated , because
+ /// cached JNI type and method metadata may retain and dereference it.
+ ///
+ public IntPtr TargetJniMethodNameUtf8 {get; set;}
+ ///
+ /// Gets or sets a pointer to a NUL-terminated UTF-8 JNI method signature.
+ ///
+ ///
+ /// Java.Interop does not own or free this memory. A non-zero pointer must remain valid
+ /// and unchanged for the lifetime of the associated , because
+ /// cached JNI type and method metadata may retain and dereference it.
+ ///
+ public IntPtr TargetJniMethodSignatureUtf8 {get; set;}
public int? TargetJniMethodParameterCount {get; set;}
public bool TargetJniMethodInstanceToStatic {get; set;}
@@ -43,20 +80,30 @@ public bool Equals (ReplacementMethodInfo other)
string.Equals (TargetJniType, other.TargetJniType) &&
string.Equals (TargetJniMethodName, other.TargetJniMethodName) &&
string.Equals (TargetJniMethodSignature, other.TargetJniMethodSignature) &&
+ TargetJniTypeUtf8 == other.TargetJniTypeUtf8 &&
+ TargetJniMethodNameUtf8 == other.TargetJniMethodNameUtf8 &&
+ TargetJniMethodSignatureUtf8 == other.TargetJniMethodSignatureUtf8 &&
TargetJniMethodParameterCount == other.TargetJniMethodParameterCount &&
TargetJniMethodInstanceToStatic == other.TargetJniMethodInstanceToStatic;
}
public override int GetHashCode ()
{
- return (SourceJniType?.GetHashCode () ?? 0) ^
- (SourceJniMethodName?.GetHashCode () ?? 0) ^
- (SourceJniMethodSignature?.GetHashCode () ?? 0) ^
- (TargetJniType?.GetHashCode () ?? 0) ^
- (TargetJniMethodName?.GetHashCode () ?? 0) ^
- (TargetJniMethodSignature?.GetHashCode () ?? 0) ^
- (TargetJniMethodParameterCount?.GetHashCode () ?? 0) ^
- TargetJniMethodInstanceToStatic.GetHashCode ();
+ return HashCode.Combine (
+ SourceJniType,
+ SourceJniMethodName,
+ SourceJniMethodSignature,
+ TargetJniType,
+ TargetJniMethodName,
+ TargetJniMethodSignature,
+ HashCode.Combine (
+ TargetJniTypeUtf8,
+ TargetJniMethodNameUtf8,
+ TargetJniMethodSignatureUtf8,
+ TargetJniMethodParameterCount,
+ TargetJniMethodInstanceToStatic
+ )
+ );
}
public override string ToString ()
@@ -68,6 +115,9 @@ public override string ToString ()
$", {nameof (TargetJniType)} = \"{TargetJniType}\"" +
$", {nameof (TargetJniMethodName)} = \"{TargetJniMethodName}\"" +
$", {nameof (TargetJniMethodSignature)} = \"{TargetJniMethodSignature}\"" +
+ $", {nameof (TargetJniTypeUtf8)} = \"{GetUtf8String (TargetJniTypeUtf8)}\"" +
+ $", {nameof (TargetJniMethodNameUtf8)} = \"{GetUtf8String (TargetJniMethodNameUtf8)}\"" +
+ $", {nameof (TargetJniMethodSignatureUtf8)} = \"{GetUtf8String (TargetJniMethodSignatureUtf8)}\"" +
$", {nameof (TargetJniMethodParameterCount)} = {TargetJniMethodParameterCount?.ToString () ?? "null"}" +
$", {nameof (TargetJniMethodInstanceToStatic)} = {TargetJniMethodInstanceToStatic}" +
$"}}";
@@ -241,14 +291,41 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type)
protected virtual IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimple) => null;
public string? GetReplacementType (string jniSimpleReference)
+ {
+ GetReplacementTypeInfo (jniSimpleReference, out var replacement, out var replacementUtf8);
+ return replacementUtf8 != IntPtr.Zero ? GetUtf8String (replacementUtf8) : replacement;
+ }
+
+ protected virtual string? GetReplacementTypeCore (string jniSimpleReference) => null;
+
+ internal void GetReplacementTypeInfo (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8)
{
AssertValid ();
AssertSimpleReference (jniSimpleReference, nameof (jniSimpleReference));
- return GetReplacementTypeCore (jniSimpleReference);
+ GetReplacementTypeInfoCore (jniSimpleReference, out replacement, out replacementUtf8);
+ if (replacementUtf8 != IntPtr.Zero)
+ replacement = null;
}
- protected virtual string? GetReplacementTypeCore (string jniSimpleReference) => null;
+ ///
+ /// Resolves a replacement JNI type as either a managed string or stable NUL-terminated UTF-8 memory.
+ ///
+ ///
+ /// The default implementation preserves compatibility with string-based type managers by
+ /// calling once and setting
+ /// to zero. Overrides are authoritative and must return
+ /// results equivalent to for every reference.
+ /// A non-zero takes precedence over
+ /// .
+ /// Java.Interop does not own or free non-zero UTF-8 memory, which must remain valid and
+ /// unchanged for the lifetime of the associated .
+ ///
+ protected virtual void GetReplacementTypeInfoCore (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8)
+ {
+ replacement = GetReplacementTypeCore (jniSimpleReference);
+ replacementUtf8 = IntPtr.Zero;
+ }
public IReadOnlyList? GetStaticMethodFallbackTypes (string jniSimpleReference)
{
@@ -286,6 +363,19 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type)
return GetReplacementMethodInfoCore (jniSimpleReference, jniMethodName, jniMethodSignature);
}
+ internal ReplacementMethodInfo? GetReplacementMethodInfo (IntPtr jniSimpleReferenceUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature)
+ {
+ AssertValid ();
+ if (jniSimpleReferenceUtf8 == IntPtr.Zero)
+ throw new ArgumentNullException (nameof (jniSimpleReferenceUtf8));
+ if (jniMethodName.IsEmpty)
+ throw new ArgumentNullException (nameof (jniMethodName));
+ if (jniMethodSignature.IsEmpty)
+ throw new ArgumentNullException (nameof (jniMethodSignature));
+
+ return GetReplacementMethodInfoCore (jniSimpleReferenceUtf8, jniMethodName, jniMethodSignature);
+ }
+
///
/// Resolves member remapping without requiring name and signature strings.
/// The default implementation preserves dispatch to the string overload.
@@ -293,6 +383,12 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type)
protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature)
=> GetReplacementMethodInfoCore (jniSimpleReference, jniMethodName.ToString (), jniMethodSignature.ToString ());
+ ///
+ /// Resolves member remapping with a source JNI type in stable NUL-terminated UTF-8 memory.
+ ///
+ protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (IntPtr jniSimpleReferenceUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature)
+ => GetReplacementMethodInfoCore (GetUtf8String (jniSimpleReferenceUtf8), jniMethodName, jniMethodSignature);
+
// Default implementation is a no-op. Derived classes (e.g. `ReflectionJniTypeManager`)
// provide reflection-based registration. Override to provide custom registration.
public virtual void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods)
diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs
index d23487846e3..25f8e7efd9b 100644
--- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs
+++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs
@@ -53,6 +53,16 @@ public JniType (string classname)
Initialize (ref peer, JniObjectReferenceOptions.CopyAndDispose);
}
+ internal unsafe JniType (IntPtr classname)
+ {
+ if (classname == IntPtr.Zero)
+ throw new ArgumentNullException (nameof (classname));
+
+ var name = MemoryMarshal.CreateReadOnlySpanFromNullTerminated ((byte*)classname);
+ var peer = JniEnvironment.Types.FindClass (name);
+ Initialize (ref peer, JniObjectReferenceOptions.CopyAndDispose);
+ }
+
public JniType (ref JniObjectReference peerReference, JniObjectReferenceOptions transfer)
{
Initialize (ref peerReference, transfer);
@@ -111,6 +121,17 @@ public static JniType GetCachedJniType ([NotNull] ref JniType? cachedType, strin
return cachedType;
}
+ internal static JniType GetCachedJniType ([NotNull] ref JniType? cachedType, IntPtr classname)
+ {
+ if (cachedType != null && cachedType.PeerReference.IsValid)
+ return cachedType;
+ var t = new JniType (classname);
+ if (Interlocked.CompareExchange (ref cachedType, t, null) != null)
+ t.Dispose ();
+ cachedType.RegisterWithRuntime ();
+ return cachedType;
+ }
+
public void Dispose ()
{
if (!PeerReference.IsValid)
@@ -496,6 +517,27 @@ internal bool TryGetInstanceMethod (ReadOnlySpan name, ReadOnlySpan
return method != null;
}
+ internal bool TryGetInstanceMethod (IntPtr name, ReadOnlySpan signature, [NotNullWhen (true)] out JniMethodInfo? method)
+ {
+ var id = GetMemberID (name, signature, MemberKind.InstanceMethod, throwOnError: false);
+ method = id == IntPtr.Zero ? null : CreateMethodInfo (name, signature, id, isStatic: false);
+ return method != null;
+ }
+
+ internal bool TryGetInstanceMethod (IntPtr name, IntPtr signature, [NotNullWhen (true)] out JniMethodInfo? method)
+ {
+ var id = GetMemberID (name, signature, MemberKind.InstanceMethod, throwOnError: false);
+ method = id == IntPtr.Zero ? null : new JniMethodInfo (name, signature, id, isStatic: false);
+ return method != null;
+ }
+
+ internal bool TryGetInstanceMethod (ReadOnlySpan name, IntPtr signature, [NotNullWhen (true)] out JniMethodInfo? method)
+ {
+ var id = GetMemberID (name, signature, MemberKind.InstanceMethod, throwOnError: false);
+ method = id == IntPtr.Zero ? null : CreateMethodInfo (name, signature, id, isStatic: false);
+ return method != null;
+ }
+
internal bool TryGetStaticMethod (ReadOnlySpan name, ReadOnlySpan signature, [NotNullWhen (true)] out JniMethodInfo? method)
{
var id = GetMemberID (name, signature, MemberKind.StaticMethod, throwOnError: false);
@@ -503,6 +545,27 @@ internal bool TryGetStaticMethod (ReadOnlySpan name, ReadOnlySpan si
return method != null;
}
+ internal bool TryGetStaticMethod (IntPtr name, ReadOnlySpan signature, [NotNullWhen (true)] out JniMethodInfo? method)
+ {
+ var id = GetMemberID (name, signature, MemberKind.StaticMethod, throwOnError: false);
+ method = id == IntPtr.Zero ? null : CreateMethodInfo (name, signature, id, isStatic: true);
+ return method != null;
+ }
+
+ internal bool TryGetStaticMethod (IntPtr name, IntPtr signature, [NotNullWhen (true)] out JniMethodInfo? method)
+ {
+ var id = GetMemberID (name, signature, MemberKind.StaticMethod, throwOnError: false);
+ method = id == IntPtr.Zero ? null : new JniMethodInfo (name, signature, id, isStatic: true);
+ return method != null;
+ }
+
+ internal bool TryGetStaticMethod (ReadOnlySpan name, IntPtr signature, [NotNullWhen (true)] out JniMethodInfo? method)
+ {
+ var id = GetMemberID (name, signature, MemberKind.StaticMethod, throwOnError: false);
+ method = id == IntPtr.Zero ? null : CreateMethodInfo (name, signature, id, isStatic: true);
+ return method != null;
+ }
+
static JniMethodInfo CreateMethodInfo (ReadOnlySpan name, ReadOnlySpan signature, IntPtr id, bool isStatic)
{
#if DEBUG
@@ -512,6 +575,24 @@ static JniMethodInfo CreateMethodInfo (ReadOnlySpan name, ReadOnlySpan signature, IntPtr id, bool isStatic)
+ {
+#if DEBUG
+ return new JniMethodInfo (name, signature.ToString (), id, isStatic);
+#else
+ return new JniMethodInfo (id, isStatic);
+#endif
+ }
+
+ static JniMethodInfo CreateMethodInfo (ReadOnlySpan name, IntPtr signature, IntPtr id, bool isStatic)
+ {
+#if DEBUG
+ return new JniMethodInfo (name.ToString (), signature, id, isStatic);
+#else
+ return new JniMethodInfo (id, isStatic);
+#endif
+ }
+
static JniFieldInfo CreateFieldInfo (ReadOnlySpan name, ReadOnlySpan signature, IntPtr id, bool isStatic)
{
#if DEBUG
@@ -530,66 +611,100 @@ enum MemberKind {
unsafe IntPtr GetMemberID (ReadOnlySpan name, ReadOnlySpan signature, MemberKind kind, bool throwOnError = true)
{
- AssertValid ();
-
// Match StringToCoTaskMemUTF8, including unpaired-surrogate replacement
// and embedded-NUL termination, rather than changing to JNI modified UTF-8.
int nameLength = checked (Encoding.UTF8.GetByteCount (name) + 1);
- int signatureLength = checked (Encoding.UTF8.GetByteCount (signature) + 1);
byte[]? rentedName = null;
- byte[]? rentedSignature = null;
try {
if (nameLength > 512)
rentedName = ArrayPool.Shared.Rent (nameLength);
- if (signatureLength > 512)
- rentedSignature = ArrayPool.Shared.Rent (signatureLength);
Span nameBuffer = rentedName == null
? stackalloc byte [nameLength]
: rentedName.AsSpan (0, nameLength);
- Span signatureBuffer = rentedSignature == null
- ? stackalloc byte [signatureLength]
- : rentedSignature.AsSpan (0, signatureLength);
Encoding.UTF8.GetBytes (name, nameBuffer);
nameBuffer [nameLength - 1] = 0;
- Encoding.UTF8.GetBytes (signature, signatureBuffer);
- signatureBuffer [signatureLength - 1] = 0;
- var env = JniEnvironment.EnvironmentPointer;
- IntPtr id;
fixed (byte* nameStart = nameBuffer)
- fixed (byte* signatureStart = signatureBuffer) {
- var namePtr = (IntPtr) nameStart;
- var signaturePtr = (IntPtr) signatureStart;
- id = kind switch {
- MemberKind.InstanceMethod => JniNativeMethods.GetMethodID (env, PeerReference.Handle, namePtr, signaturePtr),
- MemberKind.StaticMethod => JniNativeMethods.GetStaticMethodID (env, PeerReference.Handle, namePtr, signaturePtr),
- MemberKind.InstanceField => JniNativeMethods.GetFieldID (env, PeerReference.Handle, namePtr, signaturePtr),
- MemberKind.StaticField => JniNativeMethods.GetStaticFieldID (env, PeerReference.Handle, namePtr, signaturePtr),
- _ => throw new ArgumentOutOfRangeException (nameof (kind)),
- };
- }
- var thrown = JniNativeMethods.ExceptionOccurred (env);
- if (!throwOnError) {
- if (thrown != IntPtr.Zero) {
- JniEnvironment.Exceptions.ExceptionClear ();
- JniEnvironment.References.RawDeleteLocalRef (env, thrown);
- return IntPtr.Zero;
- }
- Debug.Assert (id != IntPtr.Zero);
- return id;
- }
- var exception = JniEnvironment.GetExceptionForLastThrowable (thrown);
- if (exception != null)
- ExceptionDispatchInfo.Capture (exception).Throw ();
- if (id == IntPtr.Zero)
- throw new InvalidOperationException ("Should not be reached; JNI member lookup should have thrown!");
- return id;
+ return GetMemberID ((IntPtr)nameStart, signature, kind, throwOnError);
} finally {
if (rentedName != null)
ArrayPool.Shared.Return (rentedName);
- if (rentedSignature != null)
- ArrayPool.Shared.Return (rentedSignature);
+ }
+ }
+
+ unsafe IntPtr GetMemberID (IntPtr name, ReadOnlySpan signature, MemberKind kind, bool throwOnError = true)
+ {
+ if (name == IntPtr.Zero)
+ throw new ArgumentNullException (nameof (name));
+ return GetMemberID (signature, name, false, kind, throwOnError);
+ }
+
+ unsafe IntPtr GetMemberID (IntPtr name, IntPtr signature, MemberKind kind, bool throwOnError = true)
+ {
+ AssertValid ();
+ if (name == IntPtr.Zero)
+ throw new ArgumentNullException (nameof (name));
+ if (signature == IntPtr.Zero)
+ throw new ArgumentNullException (nameof (signature));
+
+ var env = JniEnvironment.EnvironmentPointer;
+ IntPtr id = kind switch {
+ MemberKind.InstanceMethod => JniNativeMethods.GetMethodID (env, PeerReference.Handle, name, signature),
+ MemberKind.StaticMethod => JniNativeMethods.GetStaticMethodID (env, PeerReference.Handle, name, signature),
+ MemberKind.InstanceField => JniNativeMethods.GetFieldID (env, PeerReference.Handle, name, signature),
+ MemberKind.StaticField => JniNativeMethods.GetStaticFieldID (env, PeerReference.Handle, name, signature),
+ _ => throw new ArgumentOutOfRangeException (nameof (kind)),
+ };
+ var thrown = JniNativeMethods.ExceptionOccurred (env);
+ if (!throwOnError) {
+ if (thrown != IntPtr.Zero) {
+ JniEnvironment.Exceptions.ExceptionClear ();
+ JniEnvironment.References.RawDeleteLocalRef (env, thrown);
+ return IntPtr.Zero;
+ }
+ Debug.Assert (id != IntPtr.Zero);
+ return id;
+ }
+
+ var exception = JniEnvironment.GetExceptionForLastThrowable (thrown);
+ if (exception != null)
+ ExceptionDispatchInfo.Capture (exception).Throw ();
+ if (id == IntPtr.Zero)
+ throw new InvalidOperationException ("Should not be reached; JNI member lookup should have thrown!");
+ return id;
+ }
+
+ unsafe IntPtr GetMemberID (ReadOnlySpan name, IntPtr signature, MemberKind kind, bool throwOnError = true)
+ {
+ if (signature == IntPtr.Zero)
+ throw new ArgumentNullException (nameof (signature));
+ return GetMemberID (name, signature, true, kind, throwOnError);
+ }
+
+ unsafe IntPtr GetMemberID (ReadOnlySpan value, IntPtr otherValue, bool valueIsName, MemberKind kind, bool throwOnError)
+ {
+ int valueLength = checked (Encoding.UTF8.GetByteCount (value) + 1);
+ byte[]? rentedValue = null;
+ try {
+ if (valueLength > 512)
+ rentedValue = ArrayPool.Shared.Rent (valueLength);
+
+ Span valueBuffer = rentedValue == null
+ ? stackalloc byte [valueLength]
+ : rentedValue.AsSpan (0, valueLength);
+ Encoding.UTF8.GetBytes (value, valueBuffer);
+ valueBuffer [valueLength - 1] = 0;
+
+ fixed (byte* valueStart = valueBuffer) {
+ var valuePointer = (IntPtr)valueStart;
+ return valueIsName
+ ? GetMemberID (valuePointer, otherValue, kind, throwOnError)
+ : GetMemberID (otherValue, valuePointer, kind, throwOnError);
+ }
+ } finally {
+ if (rentedValue != null)
+ ArrayPool.Shared.Return (rentedValue);
}
}
}
diff --git a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt
index 2a9e8e7d8d2..11723423a5b 100644
--- a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt
+++ b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt
@@ -1,5 +1,13 @@
#nullable enable
virtual Java.Interop.JniRuntime.JniTypeManager.GetReplacementMethodInfoCore(string! jniSimpleReference, System.ReadOnlySpan jniMethodName, System.ReadOnlySpan jniMethodSignature) -> Java.Interop.JniRuntime.ReplacementMethodInfo?
+virtual Java.Interop.JniRuntime.JniTypeManager.GetReplacementMethodInfoCore(nint jniSimpleReferenceUtf8, System.ReadOnlySpan jniMethodName, System.ReadOnlySpan jniMethodSignature) -> Java.Interop.JniRuntime.ReplacementMethodInfo?
+virtual Java.Interop.JniRuntime.JniTypeManager.GetReplacementTypeInfoCore(string! jniSimpleReference, out string? replacement, out nint replacementUtf8) -> void
+Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniMethodNameUtf8.get -> nint
+Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniMethodNameUtf8.set -> void
+Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniMethodSignatureUtf8.get -> nint
+Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniMethodSignatureUtf8.set -> void
+Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniTypeUtf8.get -> nint
+Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniTypeUtf8.set -> void
static Java.Interop.JniEnvironment.BeginMarshalMethod(nint jnienv, out Java.Interop.JniTransition transition, out Java.Interop.JniRuntime? runtime) -> bool
static Java.Interop.JniEnvironment.EndMarshalMethod(ref Java.Interop.JniTransition transition) -> void
virtual Java.Interop.JniRuntime.OnEnterMarshalMethod() -> void
diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs
index 99004f98c2b..8b5459df5eb 100644
--- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs
+++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs
@@ -5,6 +5,8 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
using Java.Interop;
@@ -33,6 +35,14 @@ static partial void CreateJavaVM ()
[UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "JavaVMFixtureTypeManager intentionally uses reflection-backed type manager behavior for tests.")]
class JavaVMFixtureTypeManager : JniRuntime.ReflectionJniTypeManager {
+ [Flags]
+ enum ReplacementMethodStorage {
+ Strings = 0,
+ TypeUtf8 = 1,
+ MethodUtf8 = 2,
+ SignatureUtf8 = 4,
+ }
+
Dictionary TypeMappings = new() {
#if !NO_MARSHAL_MEMBER_BUILDER_SUPPORT
[TestType.JniTypeName] = typeof (TestType),
@@ -52,11 +62,23 @@ class JavaVMFixtureTypeManager : JniRuntime.ReflectionJniTypeManager {
[MyDisposableObject.JniTypeName] = typeof (JavaDisposedObject),
[MyJavaInterfaceImpl.JniTypeName] = typeof (MyJavaInterfaceImpl),
};
+ readonly Dictionary utf8Values = new (StringComparer.Ordinal);
+ readonly object utf8ValuesLock = new ();
public JavaVMFixtureTypeManager ()
{
}
+ protected override void Dispose (bool disposing)
+ {
+ lock (utf8ValuesLock) {
+ foreach (var value in utf8Values.Values)
+ Marshal.ZeroFreeCoTaskMemUTF8 (value);
+ utf8Values.Clear ();
+ }
+ base.Dispose (disposing);
+ }
+
protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference)
{
foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference))
@@ -117,19 +139,48 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type)
["net/dot/jni/test/RenameClassBase1"] = "net/dot/jni/test/RenameClassBase2",
};
- protected override string? GetReplacementTypeCore (string jniSimpleReference) =>
- ReplacmentTypes.TryGetValue (jniSimpleReference, out var v)
- ? v
- : null;
+ string? trackedReplacementType;
+ int replacementTypeStringLookupCount;
+ int replacementTypeUtf8LookupCount;
- Dictionary<(string SourceType, string SourceName, string? SourceSignature), (string? TargetType, string? TargetName, string? TargetSignature, int? ParamCount, bool TurnStatic)> ReplacementMethods = new() {
- [("java/lang/Object", "remappedToToString", "()Ljava/lang/String;")] = (null, "toString", null, null, false),
- [("java/lang/Object", "remappedToStaticHashCode", null)] = ("net/dot/jni/test/ObjectHelper", "getHashCodeHelper", null, null, true),
- [("java/lang/Runtime", "remappedToGetRuntime", null)] = (null, "getRuntime", null, null, false),
+ public void TrackReplacementTypeLookups (string jniSimpleReference)
+ {
+ trackedReplacementType = jniSimpleReference;
+ replacementTypeStringLookupCount = 0;
+ replacementTypeUtf8LookupCount = 0;
+ }
+
+ public (int String, int Utf8) GetReplacementTypeLookupCounts ()
+ => (replacementTypeStringLookupCount, replacementTypeUtf8LookupCount);
+
+ protected override string? GetReplacementTypeCore (string jniSimpleReference)
+ {
+ if (jniSimpleReference == trackedReplacementType)
+ Interlocked.Increment (ref replacementTypeStringLookupCount);
+ return ReplacmentTypes.TryGetValue (jniSimpleReference, out var value)
+ ? value
+ : null;
+ }
+
+ protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8)
+ {
+ if (jniSimpleReference == trackedReplacementType)
+ Interlocked.Increment (ref replacementTypeUtf8LookupCount);
+ replacement = null;
+ replacementUtf8 = ReplacmentTypes.TryGetValue (jniSimpleReference, out var value)
+ ? GetUtf8Value (value)
+ : IntPtr.Zero;
+ }
+
+ Dictionary<(string SourceType, string SourceName, string? SourceSignature), (string? TargetType, string? TargetName, string? TargetSignature, int? ParamCount, bool TurnStatic, ReplacementMethodStorage Storage)> ReplacementMethods = new() {
+ [("java/lang/Object", "remappedToToString", "()Ljava/lang/String;")] = (null, "toString", null, null, false, ReplacementMethodStorage.TypeUtf8 | ReplacementMethodStorage.MethodUtf8),
+ [("java/lang/Object", "remappedToStringWithUtf8Signature", "()Ljava/lang/String;")] = (null, "toString", "()Ljava/lang/String;", null, false, ReplacementMethodStorage.SignatureUtf8),
+ [("java/lang/Object", "remappedToStaticHashCode", null)] = ("net/dot/jni/test/ObjectHelper", "getHashCodeHelper", null, null, true, ReplacementMethodStorage.TypeUtf8 | ReplacementMethodStorage.MethodUtf8 | ReplacementMethodStorage.SignatureUtf8),
+ [("java/lang/Runtime", "remappedToGetRuntime", null)] = (null, "getRuntime", null, null, false, ReplacementMethodStorage.Strings),
// NOTE: key must use *post-renamed* value, not pre-renamed value
// NOTE: SourceSignature lacking return type; "closer in spirit" to what `remapping-config.json` allows
- [("net/dot/jni/test/RenameClassBase2", "hashCode", "()")] = ("net/dot/jni/test/RenameClassBase2", "myNewHashCode", null, null, false),
+ [("net/dot/jni/test/RenameClassBase2", "hashCode", "()")] = ("net/dot/jni/test/RenameClassBase2", "myNewHashCode", null, null, false, ReplacementMethodStorage.TypeUtf8 | ReplacementMethodStorage.MethodUtf8),
};
protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature)
@@ -148,13 +199,16 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type)
paramCount++;
}
// Console.Error.WriteLine ($"# jonp: found replacement: ({GetValue (r.TargetType)}, {GetValue (r.TargetName)}, {GetValue (r.TargetSignature)}, {r.ParamCount?.ToString () ?? "null"}, {r.IsStatic})");
+ var targetType = r.TargetType ?? jniSourceType;
+ var targetName = r.TargetName ?? jniMethodName;
+ var targetSignature = r.Storage == ReplacementMethodStorage.Strings ? targetSig ?? jniMethodSignature : targetSig;
return new JniRuntime.ReplacementMethodInfo {
- SourceJniType = jniSourceType,
- SourceJniMethodName = jniMethodName,
- SourceJniMethodSignature = jniMethodSignature,
- TargetJniType = r.TargetType ?? jniSourceType,
- TargetJniMethodName = r.TargetName ?? jniMethodName,
- TargetJniMethodSignature = targetSig ?? jniMethodSignature,
+ TargetJniType = r.Storage.HasFlag (ReplacementMethodStorage.TypeUtf8) ? null : targetType,
+ TargetJniMethodName = r.Storage.HasFlag (ReplacementMethodStorage.MethodUtf8) ? null : targetName,
+ TargetJniMethodSignature = r.Storage.HasFlag (ReplacementMethodStorage.SignatureUtf8) ? null : targetSignature,
+ TargetJniTypeUtf8 = r.Storage.HasFlag (ReplacementMethodStorage.TypeUtf8) ? GetUtf8Value (targetType) : IntPtr.Zero,
+ TargetJniMethodNameUtf8 = r.Storage.HasFlag (ReplacementMethodStorage.MethodUtf8) ? GetUtf8Value (targetName) : IntPtr.Zero,
+ TargetJniMethodSignatureUtf8 = r.Storage.HasFlag (ReplacementMethodStorage.SignatureUtf8) && targetSig != null ? GetUtf8Value (targetSig) : IntPtr.Zero,
TargetJniMethodParameterCount = paramCount,
TargetJniMethodInstanceToStatic = r.TurnStatic,
};
@@ -170,5 +224,24 @@ string GetAlternateMethodSignature ()
// return value == null ? "null" : $"\"{value}\"";
// }
}
+
+ protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (IntPtr jniSourceTypeUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature)
+ {
+ var jniSourceType = Marshal.PtrToStringUTF8 (jniSourceTypeUtf8);
+ if (jniSourceType == null)
+ throw new InvalidOperationException ("The test remapping source type is null.");
+ return GetReplacementMethodInfoCore (jniSourceType, jniMethodName.ToString (), jniMethodSignature.ToString ());
+ }
+
+ IntPtr GetUtf8Value (string value)
+ {
+ lock (utf8ValuesLock) {
+ if (utf8Values.TryGetValue (value, out var pointer))
+ return pointer;
+ pointer = Marshal.StringToCoTaskMemUTF8 (value);
+ utf8Values.Add (value, pointer);
+ return pointer;
+ }
+ }
}
}
diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs
index 51a11362b45..40ed2dc5390 100644
--- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs
+++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs
@@ -18,6 +18,27 @@ public void Ctor_CanReferenceNonexistentType ()
JniPeerMembers.Dispose (members);
}
+#if !ANDROID // Android doesn't allow providing a custom TypeManager
+ [Test]
+ [NonParallelizable]
+ public void HandledReplacementTypeMissDoesNotUseStringFallback ()
+ {
+ var typeManager = JavaVMFixture.TypeManager;
+ Assert.IsNotNull (typeManager);
+ typeManager.TrackReplacementTypeLookups ("java/lang/Double");
+ try {
+ var members = new JniPeerMembers ("java/lang/Double", typeof (MyString));
+ JniPeerMembers.Dispose (members);
+
+ var counts = typeManager.GetReplacementTypeLookupCounts ();
+ Assert.AreEqual (1, counts.Utf8);
+ Assert.AreEqual (0, counts.String);
+ } finally {
+ typeManager.TrackReplacementTypeLookups ("");
+ }
+ }
+#endif // !ANDROID
+
[Test]
[Category ("TrimmableTypeMapUnsupported")]
public void VirtualInvokeOnBaseInvokesMostDerivedJavaMethod ()
@@ -225,6 +246,16 @@ public void ReplaceInstanceMethodName ()
JniObjectReference.Dispose (ref r);
}
+ [Test]
+ [Category ("NativeAOTIgnore")]
+ public void ReplaceInstanceMethodWithUtf8Signature ()
+ {
+ using var o = new JavaLangRemappingTestObject ();
+ // Shouldn't throw; should instead invoke Object.toString()
+ var r = o.remappedToStringWithUtf8Signature ();
+ JniObjectReference.Dispose (ref r);
+ }
+
[Test]
[Category ("NativeAOTIgnore")]
public void ReplaceStaticMethodName ()
@@ -345,6 +376,12 @@ public unsafe JniObjectReference remappedToToString ()
return _members.InstanceMethods.InvokeNonvirtualObjectMethod (id, this, null);
}
+ public unsafe JniObjectReference remappedToStringWithUtf8Signature ()
+ {
+ const string id = "remappedToStringWithUtf8Signature.()Ljava/lang/String;";
+ return _members.InstanceMethods.InvokeNonvirtualObjectMethod (id, this, null);
+ }
+
public unsafe int remappedToStaticHashCode ()
{
const string id = "remappedToStaticHashCode.()I";
diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs
index feb00c0e8e4..f2dd684a8dd 100644
--- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs
+++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs
@@ -1,4 +1,6 @@
+using System;
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
using Java.Interop;
@@ -9,6 +11,55 @@ namespace Java.InteropTests {
[TestFixture]
public class JniRuntimeJniTypeManagerTests : JavaVMFixture {
+ [Test]
+ public void ReplacementMethodInfoToStringConvertsUtf8Pointers ()
+ {
+ var type = Marshal.StringToCoTaskMemUTF8 ("java/lang/Object");
+ var name = Marshal.StringToCoTaskMemUTF8 ("toString");
+ var signature = Marshal.StringToCoTaskMemUTF8 ("()Ljava/lang/String;");
+ try {
+ var info = new JniRuntime.ReplacementMethodInfo {
+ TargetJniTypeUtf8 = type,
+ TargetJniMethodNameUtf8 = name,
+ TargetJniMethodSignatureUtf8 = signature,
+ };
+
+ var value = info.ToString ();
+
+ Assert.That (value, Does.Contain ("TargetJniTypeUtf8 = \"java/lang/Object\""));
+ Assert.That (value, Does.Contain ("TargetJniMethodNameUtf8 = \"toString\""));
+ Assert.That (value, Does.Contain ("TargetJniMethodSignatureUtf8 = \"()Ljava/lang/String;\""));
+ } finally {
+ Marshal.ZeroFreeCoTaskMemUTF8 (type);
+ Marshal.ZeroFreeCoTaskMemUTF8 (name);
+ Marshal.ZeroFreeCoTaskMemUTF8 (signature);
+ }
+ }
+
+ [Test]
+ public void ReplacementTypeInfoSupportsStringAndUtf8Results ()
+ {
+ using var stringManager = new StringReplacementTypeManager ();
+ stringManager.GetReplacementTypeInfo ("java/lang/String", out var stringReplacement, out var stringReplacementUtf8);
+ Assert.AreEqual ("java/lang/Object", stringReplacement);
+ Assert.AreEqual (IntPtr.Zero, stringReplacementUtf8);
+ Assert.AreEqual (1, stringManager.LookupCount);
+
+ var utf8Value = Marshal.StringToCoTaskMemUTF8 ("java/lang/Object");
+ try {
+ using var utf8Manager = new Utf8ReplacementTypeManager (utf8Value);
+ utf8Manager.GetReplacementTypeInfo ("java/lang/Double", out var missingReplacement, out var missingReplacementUtf8);
+ Assert.IsNull (missingReplacement);
+ Assert.AreEqual (IntPtr.Zero, missingReplacementUtf8);
+ utf8Manager.GetReplacementTypeInfo ("java/lang/String", out var replacement, out var replacementUtf8);
+ Assert.IsNull (replacement);
+ Assert.AreEqual (utf8Value, replacementUtf8);
+ Assert.AreEqual ("java/lang/Object", utf8Manager.GetReplacementType ("java/lang/String"));
+ } finally {
+ Marshal.ZeroFreeCoTaskMemUTF8 (utf8Value);
+ }
+ }
+
[Test]
[Category ("TrimmableTypeMapUnsupported")]
[RequiresDynamicCode ("This test uses ReflectionJniTypeManager, which is reflection-based and not NativeAOT-compatible.")]
@@ -34,5 +85,32 @@ public MyTypeManager ()
{
}
}
+
+ class StringReplacementTypeManager : JniRuntime.JniTypeManager {
+
+ public int LookupCount { get; private set; }
+
+ protected override string GetReplacementTypeCore (string jniSimpleReference)
+ {
+ LookupCount++;
+ return jniSimpleReference == "java/lang/String" ? "java/lang/Object" : null;
+ }
+ }
+
+ class Utf8ReplacementTypeManager : JniRuntime.JniTypeManager {
+
+ readonly IntPtr replacement;
+
+ public Utf8ReplacementTypeManager (IntPtr replacement)
+ {
+ this.replacement = replacement;
+ }
+
+ protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string replacement, out IntPtr replacementUtf8)
+ {
+ replacement = jniSimpleReference == "java/lang/String" ? "ignored/string/value" : null;
+ replacementUtf8 = jniSimpleReference == "java/lang/String" ? this.replacement : IntPtr.Zero;
+ }
+ }
}
}
diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml
index 53a299d9d49..643106c7d20 100644
--- a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml
+++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml
@@ -11,6 +11,14 @@
source-method-signature="()Ljava/lang/String;"
target-type="java/lang/Object"
target-method-name="toString" target-method-instance-to-static="false" />
+
Date: Wed, 16 Sep 2026 10:39:08 +0200
Subject: [PATCH 05/19] [runtime] Move JNI type and method remapping to managed
code
Emit the existing Intune remapping tables as a self-describing data block, pass it through JNIEnv initialization, and share UTF-8 binary lookup across MonoVM and CoreCLR without per-call P/Invokes.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../Android.Runtime/JNIEnvInit.cs | 5 +-
.../Android.Runtime/RuntimeNativeMethods.cs | 12 -
.../JniRemappingLookup.cs | 269 +++++++++++++++---
.../JniRemappingAssemblyGenerator.cs | 64 ++++-
src/native/clr/host/host.cc | 2 +-
src/native/clr/host/internal-pinvokes-clr.cc | 13 -
.../clr/host/internal-pinvokes-shared.cc | 1 -
.../include/runtime-base/internal-pinvokes.hh | 2 -
.../clr/include/runtime-base/jni-remapping.hh | 17 --
.../clr/pinvoke-override/precompiled.cc | 6 -
src/native/clr/runtime-base/CMakeLists.txt | 1 -
src/native/clr/runtime-base/jni-remapping.cc | 97 -------
.../xamarin-app-stub/application_dso_stub.cc | 8 +
.../common/include/managed-interface.hh | 13 +-
src/native/mono/monodroid/CMakeLists.txt | 1 -
.../mono/monodroid/internal-pinvokes.cc | 14 -
src/native/mono/monodroid/jni-remapping.cc | 98 -------
src/native/mono/monodroid/jni-remapping.hh | 21 --
src/native/mono/monodroid/monodroid-glue.cc | 2 +-
.../generate-pinvoke-tables.cc | 2 -
.../pinvoke-override/pinvoke-tables.include | 10 +-
.../mono/runtime-base/internal-pinvokes.hh | 2 -
.../xamarin-app-stub/application_dso_stub.cc | 8 +
.../nativeaot/host/internal-pinvoke-stubs.cc | 13 -
.../include/runtime-base/internal-pinvokes.hh | 2 -
25 files changed, 318 insertions(+), 365 deletions(-)
delete mode 100644 src/native/clr/include/runtime-base/jni-remapping.hh
delete mode 100644 src/native/clr/runtime-base/jni-remapping.cc
delete mode 100644 src/native/mono/monodroid/jni-remapping.cc
delete mode 100644 src/native/mono/monodroid/jni-remapping.hh
diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs
index 9e6c6fa599d..889565a3d89 100644
--- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs
+++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs
@@ -31,7 +31,7 @@ internal struct JnienvInitializeArgs {
public int packageNamingPolicy;
public byte ioExceptionType;
public int jniAddNativeMethodRegistrationAttributePresent;
- public bool jniRemappingInUse;
+ public IntPtr jniRemappingData;
public bool marshalMethodsEnabled;
public IntPtr grefGCUserPeerable;
public IntPtr propagateUncaughtExceptionFn;
@@ -39,7 +39,6 @@ internal struct JnienvInitializeArgs {
}
#pragma warning restore 0649
- internal static bool jniRemappingInUse;
internal static bool MarshalMethodsEnabled;
internal static bool PropagateExceptions;
internal static BoundExceptionType BoundExceptionType;
@@ -205,7 +204,7 @@ static void InitializeCommonState (JnienvInitializeArgs args)
Logger.SetLogCategories ((LogCategories)args.logCategories);
gref_gc_threshold = args.grefGcThreshold;
- jniRemappingInUse = args.jniRemappingInUse;
+ JniRemappingLookup.Initialize (args.jniRemappingData);
MarshalMethodsEnabled = args.marshalMethodsEnabled;
java_class_loader = args.grefLoader;
diff --git a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs
index eaad49ce09f..cb00f9c86c3 100644
--- a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs
+++ b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs
@@ -60,18 +60,6 @@ internal unsafe static partial class RuntimeNativeMethods
[UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })]
internal static partial int _monodroid_weak_gref_dec ();
- [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)]
- [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })]
- internal static partial IntPtr _monodroid_lookup_replacement_type (string jniSimpleReference);
-
- [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)]
- [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })]
- internal static partial IntPtr _monodroid_lookup_replacement_method_info (string jniSourceType, string jniMethodName, string jniMethodSignature);
-
- [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)]
- [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })]
- internal static partial IntPtr _monodroid_lookup_replacement_method_info (string jniSourceType, byte* jniMethodName, byte* jniMethodSignature);
-
[LibraryImport (RuntimeConstants.InternalDllName)]
[UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })]
internal static partial void _monodroid_detect_cpu_and_architecture (ref ushort built_for_cpu, ref ushort running_on_cpu, ref byte is64bit);
diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs
index e03745ff054..d323a2e18a0 100644
--- a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs
+++ b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs
@@ -12,14 +12,62 @@ namespace Microsoft.Android.Runtime;
static class JniRemappingLookup
{
-#pragma warning disable CS0649 // Field 'JniRemappingLookup.JniRemappingReplacementMethod.target_type' is never assigned to, and will always have its default value null
- struct JniRemappingReplacementMethod
+ const int StackallocThreshold = 512;
+
+ unsafe struct NativeJniRemappingString
+ {
+ public uint length;
+ public byte* str;
+ }
+
+ unsafe struct NativeJniRemappingReplacementMethod
+ {
+ public byte* target_type;
+ public byte* target_name;
+ public byte is_static;
+ }
+
+ unsafe struct NativeJniRemappingIndexMethodEntry
+ {
+ public NativeJniRemappingString name;
+ public NativeJniRemappingString signature;
+ public NativeJniRemappingReplacementMethod replacement;
+ }
+
+ unsafe struct NativeJniRemappingIndexTypeEntry
+ {
+ public NativeJniRemappingString name;
+ public uint method_count;
+ public NativeJniRemappingIndexMethodEntry* methods;
+ }
+
+ unsafe struct NativeJniRemappingTypeReplacementEntry
+ {
+ public NativeJniRemappingString name;
+ public byte* replacement;
+ }
+
+ unsafe struct NativeJniRemappingData
+ {
+ public NativeJniRemappingTypeReplacementEntry* type_replacements;
+ public NativeJniRemappingIndexTypeEntry* method_replacement_index;
+ public uint type_replacement_count;
+ public uint method_replacement_index_count;
+ }
+
+ static unsafe NativeJniRemappingData* nativeData;
+ static bool isInUse;
+
+ internal static unsafe void Initialize (IntPtr data)
{
- public string? target_type;
- public string? target_name;
- public bool is_static;
+ if (data == IntPtr.Zero) {
+ isInUse = false;
+ return;
+ }
+
+ nativeData = (NativeJniRemappingData*)data;
+ isInUse = nativeData->type_replacement_count > 0 || nativeData->method_replacement_index_count > 0;
}
-#pragma warning restore CS0649
internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSimpleReference, bool useReplacementTypes)
{
@@ -30,7 +78,6 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi
var typeWithPrefix = $"{desugarType}$_CC";
var typeWithSuffix = $"{jniSimpleReference}$-CC";
-
var replacements = new[] {
useReplacementTypes ? GetReplacementType (typeWithPrefix) ?? typeWithPrefix : typeWithPrefix,
useReplacementTypes ? GetReplacementType (typeWithSuffix) ?? typeWithSuffix : typeWithSuffix,
@@ -44,18 +91,36 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi
return replacements;
}
- internal static string? GetReplacementType (string? jniSimpleReference)
+ internal static unsafe string? GetReplacementType (string? jniSimpleReference)
{
- if (jniSimpleReference is null || !JNIEnvInit.jniRemappingInUse) {
+ if (jniSimpleReference is null || !isInUse || jniSimpleReference.Length == 0) {
return null;
}
- IntPtr ret = RuntimeNativeMethods._monodroid_lookup_replacement_type (jniSimpleReference);
- if (ret == IntPtr.Zero) {
- return null;
+ NativeJniRemappingData* data = nativeData;
+ if (data == null) {
+ throw new InvalidOperationException ("JNI remapping data has not been initialized.");
}
- return Marshal.PtrToStringAnsi (ret);
+ int byteCount = Encoding.UTF8.GetByteCount (jniSimpleReference);
+ byte[]? rented = null;
+ try {
+ if (byteCount > StackallocThreshold) {
+ rented = ArrayPool.Shared.Rent (byteCount);
+ }
+
+ Span key = rented == null
+ ? stackalloc byte [byteCount]
+ : rented.AsSpan (0, byteCount);
+ Encoding.UTF8.GetBytes (jniSimpleReference, key);
+
+ byte* replacement = LookupType (data->type_replacements, data->type_replacement_count, key);
+ return replacement == null ? null : Marshal.PtrToStringUTF8 ((IntPtr)replacement);
+ } finally {
+ if (rented != null) {
+ ArrayPool.Shared.Return (rented);
+ }
+ }
}
internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (string jniSourceType, string jniMethodName, string jniMethodSignature)
@@ -63,76 +128,186 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi
internal static unsafe JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (string jniSourceType, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature)
{
- if (!JNIEnvInit.jniRemappingInUse) {
+ if (!isInUse) {
return null;
}
- int nameLength = checked (Encoding.UTF8.GetByteCount (jniMethodName) + 1);
- int signatureLength = checked (Encoding.UTF8.GetByteCount (jniMethodSignature) + 1);
+ NativeJniRemappingData* data = nativeData;
+ if (data == null) {
+ throw new InvalidOperationException ("JNI remapping data has not been initialized.");
+ }
+
+ int sourceTypeLength = Encoding.UTF8.GetByteCount (jniSourceType);
+ int nameLength = Encoding.UTF8.GetByteCount (jniMethodName);
+ int signatureLength = Encoding.UTF8.GetByteCount (jniMethodSignature);
+ byte[]? rentedSourceType = null;
byte[]? rentedName = null;
byte[]? rentedSignature = null;
- IntPtr retInfo;
+ NativeJniRemappingReplacementMethod* method;
try {
- if (nameLength > 512)
+ if (sourceTypeLength > StackallocThreshold)
+ rentedSourceType = ArrayPool.Shared.Rent (sourceTypeLength);
+ if (nameLength > StackallocThreshold)
rentedName = ArrayPool.Shared.Rent (nameLength);
- if (signatureLength > 512)
+ if (signatureLength > StackallocThreshold)
rentedSignature = ArrayPool.Shared.Rent (signatureLength);
+ Span sourceTypeBuffer = rentedSourceType == null
+ ? stackalloc byte [sourceTypeLength]
+ : rentedSourceType.AsSpan (0, sourceTypeLength);
Span nameBuffer = rentedName == null
? stackalloc byte [nameLength]
: rentedName.AsSpan (0, nameLength);
Span signatureBuffer = rentedSignature == null
? stackalloc byte [signatureLength]
: rentedSignature.AsSpan (0, signatureLength);
+ Encoding.UTF8.GetBytes (jniSourceType, sourceTypeBuffer);
Encoding.UTF8.GetBytes (jniMethodName, nameBuffer);
- nameBuffer [nameLength - 1] = 0;
Encoding.UTF8.GetBytes (jniMethodSignature, signatureBuffer);
- signatureBuffer [signatureLength - 1] = 0;
- fixed (byte* name = nameBuffer)
- fixed (byte* signature = signatureBuffer) {
- retInfo = RuntimeNativeMethods._monodroid_lookup_replacement_method_info (jniSourceType, name, signature);
- }
+ method = LookupMethod (data, sourceTypeBuffer, nameBuffer, signatureBuffer);
} finally {
+ if (rentedSourceType != null)
+ ArrayPool.Shared.Return (rentedSourceType);
if (rentedName != null)
ArrayPool.Shared.Return (rentedName);
if (rentedSignature != null)
ArrayPool.Shared.Return (rentedSignature);
}
- if (retInfo == IntPtr.Zero) {
+
+ if (method == null) {
return null;
}
+ if (method->target_type == null || method->target_name == null) {
+ throw new InvalidOperationException (
+ $"JNI remapping entry for `{jniSourceType}.{jniMethodName}{jniMethodSignature}` is missing target information.");
+ }
- var method = Marshal.PtrToStructure (retInfo);
- var targetType = method.target_type ?? throw new InvalidOperationException (
- $"JNI remapping entry for `{jniSourceType}.{jniMethodName}{jniMethodSignature}` is missing a target type.");
- var targetName = method.target_name ?? throw new InvalidOperationException (
- $"JNI remapping entry for `{jniSourceType}.{jniMethodName}{jniMethodSignature}` is missing a target method name.");
var sourceSignature = jniMethodSignature.ToString ();
- var newSignature = sourceSignature;
-
+ var targetSignature = sourceSignature;
int? paramCount = null;
- if (method.is_static) {
+ bool isStatic = method->is_static != 0;
+ if (isStatic) {
paramCount = JniMemberSignature.GetParameterCountFromMethodSignature (sourceSignature) + 1;
- newSignature = $"(L{jniSourceType};" + sourceSignature.Substring ("(".Length);
+ targetSignature = $"(L{jniSourceType};" + sourceSignature.Substring ("(".Length);
}
+ var ret = new JniRuntime.ReplacementMethodInfo {
+ SourceJniType = jniSourceType,
+ SourceJniMethodName = jniMethodName.ToString (),
+ SourceJniMethodSignature = sourceSignature,
+ TargetJniTypeUtf8 = (IntPtr)method->target_type,
+ TargetJniMethodNameUtf8 = (IntPtr)method->target_name,
+ TargetJniMethodSignature = targetSignature,
+ TargetJniMethodParameterCount = paramCount,
+ TargetJniMethodInstanceToStatic = isStatic,
+ };
+
if (Logger.LogAssembly) {
var message = $"Remapping method `{jniSourceType}.{jniMethodName}{jniMethodSignature}` to " +
- $"`{targetType}.{targetName}{newSignature}`; " +
- $"param-count: {paramCount}; instance-to-static? {method.is_static}";
+ $"`{ret.TargetJniType}.{ret.TargetJniMethodName}{targetSignature}`; " +
+ $"param-count: {paramCount}; instance-to-static? {isStatic}";
Logger.Log (LogLevel.Debug, "monodroid-assembly", message);
}
- return new JniRuntime.ReplacementMethodInfo {
- SourceJniType = jniSourceType,
- SourceJniMethodName = jniMethodName.ToString (),
- SourceJniMethodSignature = sourceSignature,
- TargetJniType = targetType,
- TargetJniMethodName = targetName,
- TargetJniMethodSignature = newSignature,
- TargetJniMethodParameterCount = paramCount,
- TargetJniMethodInstanceToStatic = method.is_static,
- };
+ return ret;
+ }
+
+ static unsafe bool Equal (NativeJniRemappingString value, ReadOnlySpan key)
+ {
+ return value.length == (uint)key.Length &&
+ new ReadOnlySpan (value.str, key.Length).SequenceEqual (key);
+ }
+
+ static unsafe int Compare (NativeJniRemappingString value, ReadOnlySpan key)
+ {
+ return new ReadOnlySpan (value.str, checked ((int)value.length)).SequenceCompareTo (key);
+ }
+
+ static unsafe int LowerBoundByName (void* entries, uint count, int entrySize, ReadOnlySpan key)
+ {
+ int left = 0;
+ int right = checked ((int)count);
+ while (left < right) {
+ int middle = left + ((right - left) / 2);
+ var name = *(NativeJniRemappingString*)((byte*)entries + (middle * entrySize));
+ if (Compare (name, key) < 0) {
+ left = middle + 1;
+ } else {
+ right = middle;
+ }
+ }
+ return left;
+ }
+
+ static unsafe byte* LookupType (NativeJniRemappingTypeReplacementEntry* entries, uint count, ReadOnlySpan key)
+ {
+ int index = LowerBoundByName (entries, count, sizeof (NativeJniRemappingTypeReplacementEntry), key);
+ if (index >= checked ((int)count) || !Equal (entries [index].name, key)) {
+ return null;
+ }
+ return entries [index].replacement;
+ }
+
+ static unsafe NativeJniRemappingReplacementMethod* LookupMethod (
+ NativeJniRemappingData* data,
+ ReadOnlySpan sourceType,
+ ReadOnlySpan name,
+ ReadOnlySpan signature)
+ {
+ int typeIndex = LowerBoundByName (
+ data->method_replacement_index,
+ data->method_replacement_index_count,
+ sizeof (NativeJniRemappingIndexTypeEntry),
+ sourceType
+ );
+ if (typeIndex >= checked ((int)data->method_replacement_index_count) ||
+ !Equal (data->method_replacement_index [typeIndex].name, sourceType)) {
+ return null;
+ }
+
+ NativeJniRemappingIndexTypeEntry* type = &data->method_replacement_index [typeIndex];
+ int first = LowerBoundByName (type->methods, type->method_count, sizeof (NativeJniRemappingIndexMethodEntry), name);
+ int count = checked ((int)type->method_count);
+ if (first >= count || !Equal (type->methods [first].name, name)) {
+ return null;
+ }
+
+ int last = first + 1;
+ while (last < count && Equal (type->methods [last].name, name)) {
+ last++;
+ }
+
+ if (signature.Length > 0) {
+ for (int i = first; i < last; i++) {
+ NativeJniRemappingIndexMethodEntry* entry = &type->methods [i];
+ if (entry->signature.length != 0 && Equal (entry->signature, signature)) {
+ return &entry->replacement;
+ }
+ }
+
+ int closeParenthesis = signature.Length - 1;
+ while (closeParenthesis >= 0 && signature [closeParenthesis] != (byte)')') {
+ closeParenthesis--;
+ }
+ int prefixLength = closeParenthesis + 1;
+ if (prefixLength > 0 && prefixLength != signature.Length) {
+ ReadOnlySpan signaturePrefix = signature.Slice (0, prefixLength);
+ for (int i = first; i < last; i++) {
+ NativeJniRemappingIndexMethodEntry* entry = &type->methods [i];
+ if (entry->signature.length != 0 && Equal (entry->signature, signaturePrefix)) {
+ return &entry->replacement;
+ }
+ }
+ }
+ }
+
+ for (int i = first; i < last; i++) {
+ NativeJniRemappingIndexMethodEntry* entry = &type->methods [i];
+ if (entry->signature.length == 0) {
+ return &entry->replacement;
+ }
+ }
+ return null;
}
}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs
index c79f4855a58..d51d799b7a6 100644
--- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs
@@ -50,6 +50,19 @@ class JniRemappingAssemblyGenerator : LlvmIrComposer
{
const string TypeReplacementsVariableName = "jni_remapping_type_replacements";
const string MethodReplacementIndexVariableName = "jni_remapping_method_replacement_index";
+ const string RemappingDataVariableName = "jni_remapping_data";
+
+ sealed class JniRemappingDataContextDataProvider : NativeAssemblerStructContextDataProvider
+ {
+ public override string GetPointedToSymbolName (object data, string fieldName)
+ {
+ return fieldName switch {
+ nameof (JniRemappingData.type_replacements) => TypeReplacementsVariableName,
+ nameof (JniRemappingData.method_replacement_index) => MethodReplacementIndexVariableName,
+ _ => base.GetPointedToSymbolName (data, fieldName),
+ };
+ }
+ }
sealed class JniRemappingTypeReplacementEntryContextDataProvider : NativeAssemblerStructContextDataProvider
{
@@ -185,6 +198,19 @@ sealed class JniRemappingTypeReplacementEntry
public string replacement;
};
+ [NativeAssemblerStructContextDataProvider (typeof(JniRemappingDataContextDataProvider))]
+ sealed class JniRemappingData
+ {
+ [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")]
+ public JniRemappingTypeReplacementEntry type_replacements;
+
+ [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")]
+ public JniRemappingIndexTypeEntry method_replacement_index;
+
+ public uint type_replacement_count;
+ public uint method_replacement_index_count;
+ }
+
List typeReplacementsInput;
List methodReplacementsInput;
@@ -193,9 +219,24 @@ sealed class JniRemappingTypeReplacementEntry
StructureInfo jniRemappingIndexMethodEntryStructureInfo;
StructureInfo jniRemappingIndexTypeEntryStructureInfo;
StructureInfo jniRemappingTypeReplacementEntryStructureInfo;
+ StructureInfo jniRemappingDataStructureInfo;
+ public int ReplacementTypeCount { get; private set; } = 0;
public int ReplacementMethodIndexEntryCount { get; private set; } = 0;
+ static int CompareUtf8 (string left, string right)
+ {
+ byte [] leftBytes = Encoding.UTF8.GetBytes (left);
+ byte [] rightBytes = Encoding.UTF8.GetBytes (right);
+ int min = Math.Min (leftBytes.Length, rightBytes.Length);
+ for (int i = 0; i < min; i++) {
+ if (leftBytes [i] != rightBytes [i]) {
+ return leftBytes [i] < rightBytes [i] ? -1 : 1;
+ }
+ }
+ return leftBytes.Length.CompareTo (rightBytes.Length);
+ }
+
public JniRemappingAssemblyGenerator (TaskLoggingHelper log)
: base (log)
{}
@@ -222,7 +263,8 @@ public JniRemappingAssemblyGenerator (TaskLoggingHelper log, List (jniRemappingTypeReplacementEntryStructureInfo, entry));
}
- typeReplacements.Sort ((StructureInstance l, StructureInstance r) => l.Instance.name.str.CompareTo (r.Instance.name.str));
+ typeReplacements.Sort ((StructureInstance l, StructureInstance r) => CompareUtf8 (l.Instance.name.str, r.Instance.name.str));
+ ReplacementTypeCount = typeReplacements.Count;
var methodIndexTypes = new List> ();
var types = new Dictionary> (StringComparer.Ordinal);
@@ -255,10 +297,10 @@ public JniRemappingAssemblyGenerator (TaskLoggingHelper log, List l, StructureInstance r) => l.Instance.name.str.CompareTo (r.Instance.name.str));
+ kvp.Value.Instance.TypeMethods.Sort ((StructureInstance l, StructureInstance r) => CompareUtf8 (l.Instance.name.str, r.Instance.name.str));
}
- methodIndexTypes.Sort ((StructureInstance l, StructureInstance r) => l.Instance.name.str.CompareTo (r.Instance.name.str));
+ methodIndexTypes.Sort ((StructureInstance l, StructureInstance r) => CompareUtf8 (l.Instance.name.str, r.Instance.name.str));
ReplacementMethodIndexEntryCount = methodIndexTypes.Count;
return (typeReplacements, methodIndexTypes);
@@ -310,6 +352,7 @@ protected override void Construct (LlvmIrModule module)
new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, new JniRemappingIndexTypeEntry ()) { IsZeroInitialized = true },
LlvmIrVariableOptions.GlobalConstant
);
+ AddData (module);
return;
}
@@ -320,6 +363,20 @@ protected override void Construct (LlvmIrModule module)
}
module.AddGlobalVariable (MethodReplacementIndexVariableName, methodIndexTypes, LlvmIrVariableOptions.GlobalConstant);
+ AddData (module);
+ }
+
+ void AddData (LlvmIrModule module)
+ {
+ var data = new JniRemappingData {
+ type_replacement_count = (uint)ReplacementTypeCount,
+ method_replacement_index_count = (uint)ReplacementMethodIndexEntryCount,
+ };
+ module.AddGlobalVariable (
+ RemappingDataVariableName,
+ new StructureInstance (jniRemappingDataStructureInfo, data),
+ LlvmIrVariableOptions.GlobalConstant
+ );
}
void MapStructures (LlvmIrModule module)
@@ -329,6 +386,7 @@ void MapStructures (LlvmIrModule module)
jniRemappingIndexMethodEntryStructureInfo = module.MapStructure ();
jniRemappingIndexTypeEntryStructureInfo = module.MapStructure ();
jniRemappingTypeReplacementEntryStructureInfo = module.MapStructure ();
+ jniRemappingDataStructureInfo = module.MapStructure ();
}
}
}
diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc
index 1e93c54871c..6f74c4d63b9 100644
--- a/src/native/clr/host/host.cc
+++ b/src/native/clr/host/host.cc
@@ -486,7 +486,7 @@ void Host::Java_mono_android_Runtime_initInternal (
init.packageNamingPolicy = static_cast(application_config.package_naming_policy);
init.boundExceptionType = 0; // System
init.jniAddNativeMethodRegistrationAttributePresent = application_config.jni_add_native_method_registration_attribute_present ? 1 : 0;
- init.jniRemappingInUse = application_config.jni_remapping_replacement_type_count > 0 || application_config.jni_remapping_replacement_method_index_entry_count > 0;
+ init.jniRemappingData = &jni_remapping_data;
init.marshalMethodsEnabled = application_config.marshal_methods_enabled;
// GC threshold is 90% of the max GREF count
diff --git a/src/native/clr/host/internal-pinvokes-clr.cc b/src/native/clr/host/internal-pinvokes-clr.cc
index 7c978f9ee08..18971d298fa 100644
--- a/src/native/clr/host/internal-pinvokes-clr.cc
+++ b/src/native/clr/host/internal-pinvokes-clr.cc
@@ -5,7 +5,6 @@
#include
#include
#include
-#include
using namespace xamarin::android;
@@ -27,18 +26,6 @@ bool clr_typemap_java_to_managed (const char *java_type_name, char const** assem
return TypeMapper::java_to_managed (java_type_name, assembly_name, managed_type_token_id);
}
-const char*
-_monodroid_lookup_replacement_type (const char *jniSimpleReference)
-{
- return JniRemapping::lookup_replacement_type (jniSimpleReference);
-}
-
-const JniRemappingReplacementMethod*
-_monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature)
-{
- return JniRemapping::lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature);
-}
-
managed_timing_sequence* monodroid_timing_start (const char *message)
{
if (!FastTiming::enabled ()) [[likely]] {
diff --git a/src/native/clr/host/internal-pinvokes-shared.cc b/src/native/clr/host/internal-pinvokes-shared.cc
index 18bffb5812e..08378e85d49 100644
--- a/src/native/clr/host/internal-pinvokes-shared.cc
+++ b/src/native/clr/host/internal-pinvokes-shared.cc
@@ -5,7 +5,6 @@
#include
#include
#include
-#include
using namespace xamarin::android;
diff --git a/src/native/clr/include/runtime-base/internal-pinvokes.hh b/src/native/clr/include/runtime-base/internal-pinvokes.hh
index a5408b45046..492054bf93e 100644
--- a/src/native/clr/include/runtime-base/internal-pinvokes.hh
+++ b/src/native/clr/include/runtime-base/internal-pinvokes.hh
@@ -23,8 +23,6 @@ extern "C" {
void monodroid_log (xamarin::android::LogLevel level, LogCategories category, const char *message) noexcept;
char* monodroid_TypeManager_get_java_class_name (jclass klass) noexcept;
void monodroid_free (void *ptr) noexcept;
- const char* _monodroid_lookup_replacement_type (const char *jniSimpleReference);
- const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature);
xamarin::android::managed_timing_sequence* monodroid_timing_start (const char *message);
void monodroid_timing_stop (xamarin::android::managed_timing_sequence *sequence, const char *message);
diff --git a/src/native/clr/include/runtime-base/jni-remapping.hh b/src/native/clr/include/runtime-base/jni-remapping.hh
deleted file mode 100644
index f7b421b43cb..00000000000
--- a/src/native/clr/include/runtime-base/jni-remapping.hh
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma once
-
-#include "xamarin-app.hh"
-
-namespace xamarin::android
-{
- class JniRemapping final
- {
- public:
- static auto lookup_replacement_type (const char *jniSimpleReference) noexcept -> const char*;
- static auto lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept -> const JniRemappingReplacementMethod*;
-
- private:
- [[gnu::nonnull (2)]]
- static auto equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept -> bool;
- };
-}
diff --git a/src/native/clr/pinvoke-override/precompiled.cc b/src/native/clr/pinvoke-override/precompiled.cc
index ec6ae2cb522..2ac13cd1c44 100644
--- a/src/native/clr/pinvoke-override/precompiled.cc
+++ b/src/native/clr/pinvoke-override/precompiled.cc
@@ -55,12 +55,6 @@ namespace {
if (entrypoint_name == "monodroid_log"sv) {
return reinterpret_cast (&monodroid_log);
}
- if (entrypoint_name == "_monodroid_lookup_replacement_type"sv) {
- return reinterpret_cast (&_monodroid_lookup_replacement_type);
- }
- if (entrypoint_name == "_monodroid_lookup_replacement_method_info"sv) {
- return reinterpret_cast (&_monodroid_lookup_replacement_method_info);
- }
if (entrypoint_name == "_monodroid_lref_log_delete"sv) {
return reinterpret_cast (&_monodroid_lref_log_delete);
}
diff --git a/src/native/clr/runtime-base/CMakeLists.txt b/src/native/clr/runtime-base/CMakeLists.txt
index 29a9e94e43e..99c9710d291 100644
--- a/src/native/clr/runtime-base/CMakeLists.txt
+++ b/src/native/clr/runtime-base/CMakeLists.txt
@@ -53,7 +53,6 @@ set(XA_RUNTIME_BASE_SOURCES
android-system.cc
android-system-shared.cc
cpu-arch-detect.cc
- jni-remapping.cc
logger.cc
util.cc
)
diff --git a/src/native/clr/runtime-base/jni-remapping.cc b/src/native/clr/runtime-base/jni-remapping.cc
deleted file mode 100644
index 715e5cb662e..00000000000
--- a/src/native/clr/runtime-base/jni-remapping.cc
+++ /dev/null
@@ -1,97 +0,0 @@
-#include
-
-#include
-#include
-
-#include "xamarin-app.hh"
-
-using namespace xamarin::android;
-
-[[gnu::always_inline]]
-auto JniRemapping::equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept -> bool
-{
- if (left.length != static_cast(right_len) || left.str[0] != *right) {
- return false;
- }
-
- if (memcmp (left.str, right, right_len) == 0) {
- return true;
- }
-
- return false;
-}
-
-auto JniRemapping::lookup_replacement_type (const char *jniSimpleReference) noexcept -> const char*
-{
- if (application_config.jni_remapping_replacement_type_count == 0 || jniSimpleReference == nullptr || *jniSimpleReference == '\0') {
- return nullptr;
- }
-
- size_t ref_len = strlen (jniSimpleReference);
- for (size_t i = 0uz; i < application_config.jni_remapping_replacement_type_count; i++) {
- JniRemappingTypeReplacementEntry const& entry = jni_remapping_type_replacements[i];
-
- if (equal (entry.name, jniSimpleReference, ref_len)) {
- return entry.replacement;
- }
- }
-
- return nullptr;
-}
-
-auto JniRemapping::lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept -> const JniRemappingReplacementMethod*
-{
- if (application_config.jni_remapping_replacement_method_index_entry_count == 0 ||
- jniSourceType == nullptr || *jniSourceType == '\0' ||
- jniMethodName == nullptr || *jniMethodName == '\0') {
- return nullptr;
- }
-
- size_t source_type_len = strlen (jniSourceType);
-
- const JniRemappingIndexTypeEntry *type = nullptr;
- for (size_t i = 0uz; i < application_config.jni_remapping_replacement_method_index_entry_count; i++) {
- JniRemappingIndexTypeEntry const& entry = jni_remapping_method_replacement_index[i];
-
- if (!equal (entry.name, jniSourceType, source_type_len)) {
- continue;
- }
-
- type = &jni_remapping_method_replacement_index[i];
- break;
- }
-
- if (type == nullptr || type->method_count == 0 || type->methods == nullptr) {
- return nullptr;
- }
-
- size_t method_name_len = strlen (jniMethodName);
- size_t signature_len = jniMethodSignature == nullptr ? 0uz : strlen (jniMethodSignature);
-
- for (size_t i = 0uz; i < type->method_count; i++) {
- JniRemappingIndexMethodEntry const& entry = type->methods[i];
-
- if (!equal (entry.name, jniMethodName, method_name_len)) {
- continue;
- }
-
- if (entry.signature.length == 0 || equal (entry.signature, jniMethodSignature, signature_len)) {
- return &type->methods[i].replacement;
- }
-
- const char *sig_end = jniMethodSignature + signature_len;
- if (*sig_end == ')') {
- continue;
- }
-
- while (sig_end != jniMethodSignature && *sig_end != ')') {
- sig_end--;
- }
-
- if (equal (entry.signature, jniMethodSignature, static_cast(sig_end - jniMethodSignature) + 1uz)) {
- return &type->methods[i].replacement;
- }
- }
-
- return nullptr;
-}
diff --git a/src/native/clr/xamarin-app-stub/application_dso_stub.cc b/src/native/clr/xamarin-app-stub/application_dso_stub.cc
index 3890aef2639..fd0a9452d24 100644
--- a/src/native/clr/xamarin-app-stub/application_dso_stub.cc
+++ b/src/native/clr/xamarin-app-stub/application_dso_stub.cc
@@ -1,6 +1,7 @@
#include
#include
+#include
#include
// This file MUST have "valid" values everywhere - the DSO it is compiled into is loaded by the
@@ -215,6 +216,13 @@ const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[] = {
},
};
+extern "C" const xamarin::android::JniRemappingData jni_remapping_data {
+ .type_replacements = jni_remapping_type_replacements,
+ .method_replacement_index = jni_remapping_method_replacement_index,
+ .type_replacement_count = 2,
+ .method_replacement_index_count = 2,
+};
+
const char *init_runtime_property_names[] = {
"HOST_RUNTIME_CONTRACT",
"RUNTIME_IDENTIFIER",
diff --git a/src/native/common/include/managed-interface.hh b/src/native/common/include/managed-interface.hh
index ca29461dc05..f0ac8f20868 100644
--- a/src/native/common/include/managed-interface.hh
+++ b/src/native/common/include/managed-interface.hh
@@ -17,6 +17,17 @@ namespace xamarin::android {
using jnienv_propagate_uncaught_exception_fn = void (*)(JNIEnv *env, jobject javaThread, jthrowable javaException);
using jnienv_register_jni_natives_fn = void (*)(const jchar *typeName_ptr, int32_t typeName_len, jclass jniClass, const jchar *methods_ptr, int32_t methods_len);
+ struct JniRemappingData {
+ const void *type_replacements;
+ const void *method_replacement_index;
+ uint32_t type_replacement_count;
+ uint32_t method_replacement_index_count;
+ };
+
+ extern "C" {
+ [[gnu::visibility("default")]] extern const JniRemappingData jni_remapping_data;
+ }
+
// NOTE: Keep this in sync with managed side in src/Mono.Android/Android.Runtime/JNIEnvInit.cs
struct JnienvInitializeArgs {
JavaVM *javaVm;
@@ -32,7 +43,7 @@ namespace xamarin::android {
int packageNamingPolicy;
uint8_t boundExceptionType;
int jniAddNativeMethodRegistrationAttributePresent;
- bool jniRemappingInUse;
+ const JniRemappingData *jniRemappingData;
bool marshalMethodsEnabled;
jobject grefGCUserPeerable;
jnienv_propagate_uncaught_exception_fn propagateUncaughtExceptionFn;
diff --git a/src/native/mono/monodroid/CMakeLists.txt b/src/native/mono/monodroid/CMakeLists.txt
index ca48f41f06f..3b07e5d7080 100644
--- a/src/native/mono/monodroid/CMakeLists.txt
+++ b/src/native/mono/monodroid/CMakeLists.txt
@@ -97,7 +97,6 @@ set(XAMARIN_MONODROID_SOURCES
embedded-assemblies.cc
globals.cc
internal-pinvokes.cc
- jni-remapping.cc
mono-log-adapter.cc
monodroid-glue.cc
monodroid-tracing.cc
diff --git a/src/native/mono/monodroid/internal-pinvokes.cc b/src/native/mono/monodroid/internal-pinvokes.cc
index e7f580e8e41..397a2902b20 100644
--- a/src/native/mono/monodroid/internal-pinvokes.cc
+++ b/src/native/mono/monodroid/internal-pinvokes.cc
@@ -3,7 +3,6 @@
#include "android-system.hh"
#include "globals.hh"
#include "internal-pinvokes.hh"
-#include "jni-remapping.hh"
using namespace xamarin::android;
using namespace xamarin::android::internal;
@@ -276,16 +275,3 @@ monodroid_get_dylib ()
{
return nullptr;
}
-
-const char*
-_monodroid_lookup_replacement_type (const char *jniSimpleReference)
-{
- return JniRemapping::lookup_replacement_type (jniSimpleReference);
-}
-
-const JniRemappingReplacementMethod*
-_monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature)
-{
- return JniRemapping::lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature);
-}
-
diff --git a/src/native/mono/monodroid/jni-remapping.cc b/src/native/mono/monodroid/jni-remapping.cc
deleted file mode 100644
index 4122c8fe68a..00000000000
--- a/src/native/mono/monodroid/jni-remapping.cc
+++ /dev/null
@@ -1,98 +0,0 @@
-#include
-
-#include "logger.hh"
-#include "jni-remapping.hh"
-#include "xamarin-app.hh"
-
-using namespace xamarin::android::internal;
-
-[[gnu::always_inline]] bool
-JniRemapping::equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept
-{
- if (left.length != static_cast(right_len) || left.str[0] != *right) {
- return false;
- }
-
- if (memcmp (left.str, right, right_len) == 0) {
- return true;
- }
-
- return false;
-}
-
-const char*
-JniRemapping::lookup_replacement_type (const char *jniSimpleReference) noexcept
-{
- if (application_config.jni_remapping_replacement_type_count == 0 || jniSimpleReference == nullptr || *jniSimpleReference == '\0') {
- return nullptr;
- }
-
- size_t ref_len = strlen (jniSimpleReference);
- for (size_t i = 0uz; i < application_config.jni_remapping_replacement_type_count; i++) {
- JniRemappingTypeReplacementEntry const& entry = jni_remapping_type_replacements[i];
-
- if (equal (entry.name, jniSimpleReference, ref_len)) {
- return entry.replacement;
- }
- }
-
- return nullptr;
-}
-
-const JniRemappingReplacementMethod*
-JniRemapping::lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept
-{
- if (application_config.jni_remapping_replacement_method_index_entry_count == 0 ||
- jniSourceType == nullptr || *jniSourceType == '\0' ||
- jniMethodName == nullptr || *jniMethodName == '\0') {
- return nullptr;
- }
-
- size_t source_type_len = strlen (jniSourceType);
-
- const JniRemappingIndexTypeEntry *type = nullptr;
- for (size_t i = 0uz; i < application_config.jni_remapping_replacement_method_index_entry_count; i++) {
- JniRemappingIndexTypeEntry const& entry = jni_remapping_method_replacement_index[i];
-
- if (!equal (entry.name, jniSourceType, source_type_len)) {
- continue;
- }
-
- type = &jni_remapping_method_replacement_index[i];
- break;
- }
-
- if (type == nullptr || type->method_count == 0 || type->methods == nullptr) {
- return nullptr;
- }
-
- size_t method_name_len = strlen (jniMethodName);
- size_t signature_len = jniMethodSignature == nullptr ? 0uz : strlen (jniMethodSignature);
-
- for (size_t i = 0uz; i < type->method_count; i++) {
- JniRemappingIndexMethodEntry const& entry = type->methods[i];
-
- if (!equal (entry.name, jniMethodName, method_name_len)) {
- continue;
- }
-
- if (entry.signature.length == 0 || equal (entry.signature, jniMethodSignature, signature_len)) {
- return &type->methods[i].replacement;
- }
-
- const char *sig_end = jniMethodSignature + signature_len;
- if (*sig_end == ')') {
- continue;
- }
-
- while (sig_end != jniMethodSignature && *sig_end != ')') {
- sig_end--;
- }
-
- if (equal (entry.signature, jniMethodSignature, static_cast(sig_end - jniMethodSignature) + 1uz)) {
- return &type->methods[i].replacement;
- }
- }
-
- return nullptr;
-}
diff --git a/src/native/mono/monodroid/jni-remapping.hh b/src/native/mono/monodroid/jni-remapping.hh
deleted file mode 100644
index e76f89e78ff..00000000000
--- a/src/native/mono/monodroid/jni-remapping.hh
+++ /dev/null
@@ -1,21 +0,0 @@
-#if !defined (__JNI_REMAPPING_HH)
-#define __JNI_REMAPPING_HH
-
-#include
-
-#include "xamarin-app.hh"
-
-namespace xamarin::android::internal
-{
- class JniRemapping final
- {
- public:
- static const char* lookup_replacement_type (const char *jniSimpleReference) noexcept;
- static const JniRemappingReplacementMethod* lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature) noexcept;
-
- private:
- [[gnu::nonnull (2)]]
- static bool equal (JniRemappingString const& left, const char *right, size_t right_len) noexcept;
- };
-}
-#endif
diff --git a/src/native/mono/monodroid/monodroid-glue.cc b/src/native/mono/monodroid/monodroid-glue.cc
index 4a5f9e38c40..20c9a24b188 100644
--- a/src/native/mono/monodroid/monodroid-glue.cc
+++ b/src/native/mono/monodroid/monodroid-glue.cc
@@ -826,7 +826,7 @@ MonodroidRuntime::init_android_runtime (JNIEnv *env, jclass runtimeClass, jobjec
init.packageNamingPolicy = static_cast(application_config.package_naming_policy);
init.boundExceptionType = application_config.bound_exception_type;
init.jniAddNativeMethodRegistrationAttributePresent = application_config.jni_add_native_method_registration_attribute_present ? 1 : 0;
- init.jniRemappingInUse = application_config.jni_remapping_replacement_type_count > 0 || application_config.jni_remapping_replacement_method_index_entry_count > 0;
+ init.jniRemappingData = &jni_remapping_data;
init.marshalMethodsEnabled = application_config.marshal_methods_enabled;
java_System_identityHashCode = env->GetStaticMethodID (java_System, "identityHashCode", "(Ljava/lang/Object;)I");
diff --git a/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc b/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc
index dc6570c2a23..69d6a196cc3 100644
--- a/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc
+++ b/src/native/mono/pinvoke-override/generate-pinvoke-tables.cc
@@ -51,8 +51,6 @@ const std::vector internal_pinvoke_names = {
"_monodroid_gref_log_delete",
"_monodroid_gref_log_new",
"monodroid_log",
- "_monodroid_lookup_replacement_type",
- "_monodroid_lookup_replacement_method_info",
"_monodroid_lref_log_delete",
"_monodroid_lref_log_new",
"_monodroid_max_gref_get",
diff --git a/src/native/mono/pinvoke-override/pinvoke-tables.include b/src/native/mono/pinvoke-override/pinvoke-tables.include
index e26bde46029..173b4d4f347 100644
--- a/src/native/mono/pinvoke-override/pinvoke-tables.include
+++ b/src/native/mono/pinvoke-override/pinvoke-tables.include
@@ -11,12 +11,11 @@
namespace {
#if INTPTR_MAX == INT64_MAX
//64-bit internal p/invoke table
- std::array internal_pinvokes {{
+ std::array internal_pinvokes {{
{0x2b3b0ca1d14076da, "monodroid_get_dylib", reinterpret_cast(&monodroid_get_dylib)},
{0x37307e5fddf709dc, "_monodroid_weak_gref_dec", reinterpret_cast(&_monodroid_weak_gref_dec)},
{0x3b2467e7eadd4a6a, "_monodroid_lref_log_new", reinterpret_cast(&_monodroid_lref_log_new)},
{0x3c5532ecdab53f89, "set_world_accessable", reinterpret_cast(&set_world_accessable)},
- {0x423c8f539a2c56d2, "_monodroid_lookup_replacement_type", reinterpret_cast(&_monodroid_lookup_replacement_type)},
{0x4b1956138764939a, "_monodroid_gref_log_new", reinterpret_cast(&_monodroid_gref_log_new)},
{0x4d5b5b488f736058, "path_combine", reinterpret_cast(&path_combine)},
{0x5a2614d15e2fdc2e, "monodroid_strdup_printf", reinterpret_cast(&monodroid_strdup_printf)},
@@ -35,7 +34,6 @@ namespace {
{0xb9bae9c43fb05089, "xamarin_app_init", reinterpret_cast(&xamarin_app_init)},
{0xbe5a300beec69c35, "monodroid_get_system_property", reinterpret_cast(&monodroid_get_system_property)},
{0xbfbb924fbe190616, "monodroid_dylib_mono_free", reinterpret_cast(&monodroid_dylib_mono_free)},
- {0xc2a21d3f6c8ccc24, "_monodroid_lookup_replacement_method_info", reinterpret_cast(&_monodroid_lookup_replacement_method_info)},
{0xc5b4690e13898fa3, "monodroid_timing_start", reinterpret_cast(&monodroid_timing_start)},
{0xcaab0a3ab6057bbd, "_monodroid_gref_inc", reinterpret_cast(&_monodroid_gref_inc)},
{0xcc873ea8493d1dd5, "monodroid_embedded_assemblies_set_assemblies_prefix", reinterpret_cast(&monodroid_embedded_assemblies_set_assemblies_prefix)},
@@ -576,14 +574,13 @@ constexpr hash_t system_security_cryptography_native_android_library_hash = 0x18
constexpr hash_t system_globalization_native_library_hash = 0x28b5c8fca080abd5;
#else
//32-bit internal p/invoke table
- std::array internal_pinvokes {{
+ std::array internal_pinvokes {{
{0xb7a486a, "monodroid_TypeManager_get_java_class_name", reinterpret_cast(&monodroid_TypeManager_get_java_class_name)},
{0xf562bd9, "monodroid_embedded_assemblies_set_assemblies_prefix", reinterpret_cast(&monodroid_embedded_assemblies_set_assemblies_prefix)},
{0x1bef8dce, "_monodroid_gref_inc", reinterpret_cast(&_monodroid_gref_inc)},
{0x1f1e0ee9, "_monodroid_gref_dec", reinterpret_cast(&_monodroid_gref_dec)},
{0x2aea7c33, "_monodroid_max_gref_get", reinterpret_cast(&_monodroid_max_gref_get)},
{0x3227d81a, "monodroid_timing_start", reinterpret_cast(&monodroid_timing_start)},
- {0x333d4835, "_monodroid_lookup_replacement_method_info", reinterpret_cast(&_monodroid_lookup_replacement_method_info)},
{0x395808e5, "monodroid_dylib_mono_free", reinterpret_cast(&monodroid_dylib_mono_free)},
{0x42b41fe4, "send_uninterrupted", reinterpret_cast(&send_uninterrupted)},
{0x4b58e0da, "monodroid_get_dylib", reinterpret_cast(&monodroid_get_dylib)},
@@ -601,7 +598,6 @@ constexpr hash_t system_globalization_native_library_hash = 0x28b5c8fca080abd5;
{0xb02468aa, "_monodroid_gref_get", reinterpret_cast(&_monodroid_gref_get)},
{0xbe8d7701, "_monodroid_gref_log_new", reinterpret_cast(&_monodroid_gref_log_new)},
{0xc0d097a7, "_monodroid_lref_log_new", reinterpret_cast(&_monodroid_lref_log_new)},
- {0xc439b5d7, "_monodroid_lookup_replacement_type", reinterpret_cast(&_monodroid_lookup_replacement_type)},
{0xc5146c54, "_monodroid_gref_log_delete", reinterpret_cast(&_monodroid_gref_log_delete)},
{0xc58eafa5, "java_interop_free", reinterpret_cast(&java_interop_free)},
{0xd91f3619, "create_public_directory", reinterpret_cast(&create_public_directory)},
@@ -1141,6 +1137,6 @@ constexpr hash_t system_security_cryptography_native_android_library_hash = 0x93
constexpr hash_t system_globalization_native_library_hash = 0xa66f1e5a;
#endif
-constexpr size_t internal_pinvokes_count = 39;
+constexpr size_t internal_pinvokes_count = 37;
constexpr size_t dotnet_pinvokes_count = 510;
} // end of anonymous namespace
diff --git a/src/native/mono/runtime-base/internal-pinvokes.hh b/src/native/mono/runtime-base/internal-pinvokes.hh
index bff70b0e9fa..005a9c53afe 100644
--- a/src/native/mono/runtime-base/internal-pinvokes.hh
+++ b/src/native/mono/runtime-base/internal-pinvokes.hh
@@ -44,6 +44,4 @@ void* monodroid_dylib_mono_new ([[maybe_unused]] const char *libmono_path);
void monodroid_dylib_mono_free ([[maybe_unused]] void *mono_imports);
int monodroid_dylib_mono_init (void *mono_imports, [[maybe_unused]] const char *libmono_path);
void* monodroid_get_dylib ();
-const char* _monodroid_lookup_replacement_type (const char *jniSimpleReference);
-const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature);
void _monodroid_detect_cpu_and_architecture (unsigned short *built_for_cpu, unsigned short *running_on_cpu, unsigned char *is64bit);
diff --git a/src/native/mono/xamarin-app-stub/application_dso_stub.cc b/src/native/mono/xamarin-app-stub/application_dso_stub.cc
index 6ed48fac62c..2588b1da487 100644
--- a/src/native/mono/xamarin-app-stub/application_dso_stub.cc
+++ b/src/native/mono/xamarin-app-stub/application_dso_stub.cc
@@ -1,6 +1,7 @@
#include
#include
+#include
#include "xamarin-app.hh"
#include
@@ -310,3 +311,10 @@ const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[] = {
.replacement = "another/replacement/java/type",
},
};
+
+extern "C" const xamarin::android::JniRemappingData jni_remapping_data {
+ .type_replacements = jni_remapping_type_replacements,
+ .method_replacement_index = jni_remapping_method_replacement_index,
+ .type_replacement_count = 2,
+ .method_replacement_index_count = 2,
+};
diff --git a/src/native/nativeaot/host/internal-pinvoke-stubs.cc b/src/native/nativeaot/host/internal-pinvoke-stubs.cc
index 1e7dd83833b..f46f8f8944f 100644
--- a/src/native/nativeaot/host/internal-pinvoke-stubs.cc
+++ b/src/native/nativeaot/host/internal-pinvoke-stubs.cc
@@ -32,19 +32,6 @@ bool clr_typemap_java_to_managed (
pinvoke_unreachable ();
}
-const char* _monodroid_lookup_replacement_type ([[maybe_unused]] const char *jniSimpleReference)
-{
- pinvoke_unreachable ();
-}
-
-const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (
- [[maybe_unused]] const char *jniSourceType,
- [[maybe_unused]] const char *jniMethodName,
- [[maybe_unused]] const char *jniMethodSignature)
-{
- pinvoke_unreachable ();
-}
-
managed_timing_sequence* monodroid_timing_start ([[maybe_unused]] const char *message)
{
pinvoke_unreachable ();
diff --git a/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh b/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh
index 60ff24596fc..14d25414941 100644
--- a/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh
+++ b/src/native/nativeaot/include/runtime-base/internal-pinvokes.hh
@@ -26,8 +26,6 @@ extern "C" {
void monodroid_log (xamarin::android::LogLevel level, LogCategories category, const char *message) noexcept;
char* monodroid_TypeManager_get_java_class_name (jclass klass) noexcept;
void monodroid_free (void *ptr) noexcept;
- const char* _monodroid_lookup_replacement_type (const char *jniSimpleReference);
- const JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature);
xamarin::android::managed_timing_sequence* monodroid_timing_start (const char *message);
void monodroid_timing_stop (xamarin::android::managed_timing_sequence *sequence, const char *message);
From 2ce6ff3b8fed8f51b08c30df8d3c70f249f88dbe Mon Sep 17 00:00:00 2001
From: Simon Rozsival
Date: Wed, 16 Sep 2026 12:41:13 +0200
Subject: [PATCH 06/19] [runtime] Compare JNI remaps without transcoding
Use System.Text.Ascii for the common UTF-8/UTF-16 comparison path and a streaming Rune fallback for non-ASCII names. Keep replacement types and exact method signatures in generated native memory, avoid source metadata strings, and cache generator UTF-8 sort keys.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../Android.Runtime/AndroidRuntime.cs | 10 +
.../JniRemappingLookup.cs | 300 ++++++++++++------
.../TrimmableTypeMapTypeManager.cs | 6 +
.../JniRemappingAssemblyGenerator.cs | 16 +-
.../Resources/RemapActivity.java | 4 +
.../Resources/RemapActivity.xml | 6 +
.../Tests/InstallAndRunTests.cs | 18 +-
7 files changed, 261 insertions(+), 99 deletions(-)
diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs
index 41c8d513a9f..e47622ab714 100644
--- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs
+++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs
@@ -382,6 +382,11 @@ protected override IEnumerable GetSimpleReferences (Type type)
return JniRemappingLookup.GetReplacementType (jniSimpleReference);
}
+ protected override IntPtr GetReplacementTypeUtf8Core (string jniSimpleReference)
+ {
+ return JniRemappingLookup.GetReplacementTypeUtf8 (jniSimpleReference);
+ }
+
protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature)
{
return JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature);
@@ -392,6 +397,11 @@ protected override IEnumerable GetSimpleReferences (Type type)
return JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature);
}
+ protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (IntPtr jniSourceTypeUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature)
+ {
+ return JniRemappingLookup.GetReplacementMethodInfo (jniSourceTypeUtf8, jniMethodName, jniMethodSignature);
+ }
+
protected override Type? GetInvokerTypeCore (Type type)
{
if (type.IsInterface || type.IsAbstract) {
diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs
index d323a2e18a0..57f84ea630e 100644
--- a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs
+++ b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs
@@ -12,8 +12,6 @@ namespace Microsoft.Android.Runtime;
static class JniRemappingLookup
{
- const int StackallocThreshold = 512;
-
unsafe struct NativeJniRemappingString
{
public uint length;
@@ -93,119 +91,88 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi
internal static unsafe string? GetReplacementType (string? jniSimpleReference)
{
- if (jniSimpleReference is null || !isInUse || jniSimpleReference.Length == 0) {
- return null;
- }
+ IntPtr replacement = GetReplacementTypeUtf8 (jniSimpleReference);
+ return replacement == IntPtr.Zero ? null : Marshal.PtrToStringUTF8 (replacement);
+ }
+
+ internal static unsafe IntPtr GetReplacementTypeUtf8 (string? jniSimpleReference)
+ {
+ if (jniSimpleReference is null || !isInUse || jniSimpleReference.Length == 0)
+ return IntPtr.Zero;
NativeJniRemappingData* data = nativeData;
- if (data == null) {
+ if (data == null)
throw new InvalidOperationException ("JNI remapping data has not been initialized.");
- }
-
- int byteCount = Encoding.UTF8.GetByteCount (jniSimpleReference);
- byte[]? rented = null;
- try {
- if (byteCount > StackallocThreshold) {
- rented = ArrayPool.Shared.Rent (byteCount);
- }
- Span key = rented == null
- ? stackalloc byte [byteCount]
- : rented.AsSpan (0, byteCount);
- Encoding.UTF8.GetBytes (jniSimpleReference, key);
-
- byte* replacement = LookupType (data->type_replacements, data->type_replacement_count, key);
- return replacement == null ? null : Marshal.PtrToStringUTF8 ((IntPtr)replacement);
- } finally {
- if (rented != null) {
- ArrayPool.Shared.Return (rented);
- }
- }
+ return (IntPtr)LookupType (data->type_replacements, data->type_replacement_count, jniSimpleReference);
}
internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (string jniSourceType, string jniMethodName, string jniMethodSignature)
=> GetReplacementMethodInfo (jniSourceType, jniMethodName.AsSpan (), jniMethodSignature.AsSpan ());
internal static unsafe JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (string jniSourceType, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature)
+ => GetReplacementMethodInfo (jniSourceType.AsSpan (), IntPtr.Zero, jniMethodName, jniMethodSignature);
+
+ internal static unsafe JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (IntPtr jniSourceTypeUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature)
+ {
+ if (jniSourceTypeUtf8 == IntPtr.Zero)
+ throw new ArgumentNullException (nameof (jniSourceTypeUtf8));
+ return GetReplacementMethodInfo (default, jniSourceTypeUtf8, jniMethodName, jniMethodSignature);
+ }
+
+ static unsafe JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (
+ ReadOnlySpan jniSourceType,
+ IntPtr jniSourceTypeUtf8,
+ ReadOnlySpan jniMethodName,
+ ReadOnlySpan jniMethodSignature)
{
- if (!isInUse) {
+ if (!isInUse)
return null;
- }
NativeJniRemappingData* data = nativeData;
- if (data == null) {
+ if (data == null)
throw new InvalidOperationException ("JNI remapping data has not been initialized.");
- }
- int sourceTypeLength = Encoding.UTF8.GetByteCount (jniSourceType);
- int nameLength = Encoding.UTF8.GetByteCount (jniMethodName);
- int signatureLength = Encoding.UTF8.GetByteCount (jniMethodSignature);
- byte[]? rentedSourceType = null;
- byte[]? rentedName = null;
- byte[]? rentedSignature = null;
- NativeJniRemappingReplacementMethod* method;
- try {
- if (sourceTypeLength > StackallocThreshold)
- rentedSourceType = ArrayPool.Shared.Rent (sourceTypeLength);
- if (nameLength > StackallocThreshold)
- rentedName = ArrayPool.Shared.Rent (nameLength);
- if (signatureLength > StackallocThreshold)
- rentedSignature = ArrayPool.Shared.Rent (signatureLength);
-
- Span sourceTypeBuffer = rentedSourceType == null
- ? stackalloc byte [sourceTypeLength]
- : rentedSourceType.AsSpan (0, sourceTypeLength);
- Span nameBuffer = rentedName == null
- ? stackalloc byte [nameLength]
- : rentedName.AsSpan (0, nameLength);
- Span signatureBuffer = rentedSignature == null
- ? stackalloc byte [signatureLength]
- : rentedSignature.AsSpan (0, signatureLength);
- Encoding.UTF8.GetBytes (jniSourceType, sourceTypeBuffer);
- Encoding.UTF8.GetBytes (jniMethodName, nameBuffer);
- Encoding.UTF8.GetBytes (jniMethodSignature, signatureBuffer);
-
- method = LookupMethod (data, sourceTypeBuffer, nameBuffer, signatureBuffer);
- } finally {
- if (rentedSourceType != null)
- ArrayPool.Shared.Return (rentedSourceType);
- if (rentedName != null)
- ArrayPool.Shared.Return (rentedName);
- if (rentedSignature != null)
- ArrayPool.Shared.Return (rentedSignature);
- }
+ byte* matchedSignature;
+ NativeJniRemappingReplacementMethod* method = jniSourceTypeUtf8 == IntPtr.Zero
+ ? LookupMethod (data, jniSourceType, jniMethodName, jniMethodSignature, out matchedSignature)
+ : LookupMethod (data, GetNullTerminatedUtf8Span (jniSourceTypeUtf8), jniMethodName, jniMethodSignature, out matchedSignature);
- if (method == null) {
+ if (method == null)
return null;
- }
if (method->target_type == null || method->target_name == null) {
+ string sourceType = GetSourceTypeForDiagnostics (jniSourceType, jniSourceTypeUtf8);
throw new InvalidOperationException (
- $"JNI remapping entry for `{jniSourceType}.{jniMethodName}{jniMethodSignature}` is missing target information.");
+ $"JNI remapping entry for `{sourceType}.{jniMethodName}{jniMethodSignature}` is missing target information.");
}
- var sourceSignature = jniMethodSignature.ToString ();
- var targetSignature = sourceSignature;
int? paramCount = null;
bool isStatic = method->is_static != 0;
+ string? targetSignature = null;
if (isStatic) {
+ string sourceType = GetSourceTypeForDiagnostics (jniSourceType, jniSourceTypeUtf8);
+ string sourceSignature = jniMethodSignature.ToString ();
paramCount = JniMemberSignature.GetParameterCountFromMethodSignature (sourceSignature) + 1;
- targetSignature = $"(L{jniSourceType};" + sourceSignature.Substring ("(".Length);
+ targetSignature = $"(L{sourceType};" + sourceSignature.Substring ("(".Length);
}
var ret = new JniRuntime.ReplacementMethodInfo {
- SourceJniType = jniSourceType,
- SourceJniMethodName = jniMethodName.ToString (),
- SourceJniMethodSignature = sourceSignature,
TargetJniTypeUtf8 = (IntPtr)method->target_type,
TargetJniMethodNameUtf8 = (IntPtr)method->target_name,
TargetJniMethodSignature = targetSignature,
+ TargetJniMethodSignatureUtf8 = isStatic ? IntPtr.Zero : (IntPtr)matchedSignature,
TargetJniMethodParameterCount = paramCount,
TargetJniMethodInstanceToStatic = isStatic,
};
if (Logger.LogAssembly) {
- var message = $"Remapping method `{jniSourceType}.{jniMethodName}{jniMethodSignature}` to " +
- $"`{ret.TargetJniType}.{ret.TargetJniMethodName}{targetSignature}`; " +
+ string sourceType = GetSourceTypeForDiagnostics (jniSourceType, jniSourceTypeUtf8);
+ string targetType = Marshal.PtrToStringUTF8 ((IntPtr)method->target_type) ?? "";
+ string targetName = Marshal.PtrToStringUTF8 ((IntPtr)method->target_name) ?? "";
+ string effectiveTargetSignature = targetSignature ??
+ (matchedSignature == null ? jniMethodSignature.ToString () : Marshal.PtrToStringUTF8 ((IntPtr)matchedSignature) ?? "");
+ var message = $"Remapping method `{sourceType}.{jniMethodName}{jniMethodSignature}` to " +
+ $"`{targetType}.{targetName}{effectiveTargetSignature}`; " +
$"param-count: {paramCount}; instance-to-static? {isStatic}";
Logger.Log (LogLevel.Debug, "monodroid-assembly", message);
}
@@ -213,17 +180,92 @@ internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSi
return ret;
}
+ static string GetSourceTypeForDiagnostics (ReadOnlySpan jniSourceType, IntPtr jniSourceTypeUtf8)
+ {
+ if (jniSourceTypeUtf8 == IntPtr.Zero)
+ return jniSourceType.ToString ();
+ return Marshal.PtrToStringUTF8 (jniSourceTypeUtf8) ?? "";
+ }
+
static unsafe bool Equal (NativeJniRemappingString value, ReadOnlySpan key)
{
return value.length == (uint)key.Length &&
new ReadOnlySpan (value.str, key.Length).SequenceEqual (key);
}
+ static unsafe bool Equal (NativeJniRemappingString value, ReadOnlySpan key, bool keyIsAscii)
+ {
+ ReadOnlySpan utf8 = new ReadOnlySpan (value.str, checked ((int)value.length));
+ return keyIsAscii
+ ? Ascii.Equals (utf8, key)
+ : CompareUtf8ToUtf16 (utf8, key) == 0;
+ }
+
static unsafe int Compare (NativeJniRemappingString value, ReadOnlySpan key)
{
return new ReadOnlySpan (value.str, checked ((int)value.length)).SequenceCompareTo (key);
}
+ static unsafe int Compare (NativeJniRemappingString value, ReadOnlySpan key, bool keyIsAscii)
+ {
+ ReadOnlySpan utf8 = new ReadOnlySpan (value.str, checked ((int)value.length));
+ return keyIsAscii ? CompareUtf8ToAscii (utf8, key) : CompareUtf8ToUtf16 (utf8, key);
+ }
+
+ static int CompareUtf8ToAscii (ReadOnlySpan utf8, ReadOnlySpan ascii)
+ {
+ int commonLength = Math.Min (utf8.Length, ascii.Length);
+ if (Ascii.Equals (utf8.Slice (0, commonLength), ascii.Slice (0, commonLength)))
+ return utf8.Length.CompareTo (ascii.Length);
+
+ for (int i = 0; i < commonLength; i++) {
+ int result = utf8 [i].CompareTo ((byte)ascii [i]);
+ if (result != 0)
+ return result;
+ }
+ return utf8.Length.CompareTo (ascii.Length);
+ }
+
+ static int CompareUtf8ToUtf16 (ReadOnlySpan utf8, ReadOnlySpan utf16)
+ {
+ // Generated table strings and runtime JNI names are well-formed Unicode. Replacement behavior
+ // below only keeps the comparator deterministic if malformed input reaches this internal API.
+ while (!utf8.IsEmpty && !utf16.IsEmpty) {
+ while (!utf8.IsEmpty && !utf16.IsEmpty && utf8 [0] < 0x80 && utf16 [0] < 0x80) {
+ int result = utf8 [0].CompareTo ((byte)utf16 [0]);
+ if (result != 0)
+ return result;
+ utf8 = utf8.Slice (1);
+ utf16 = utf16.Slice (1);
+ }
+ if (utf8.IsEmpty || utf16.IsEmpty)
+ break;
+
+ OperationStatus utf8Status = Rune.DecodeFromUtf8 (utf8, out Rune utf8Rune, out int utf8Consumed);
+ if (utf8Status != OperationStatus.Done) {
+ utf8Rune = Rune.ReplacementChar;
+ utf8Consumed = 1;
+ }
+
+ OperationStatus utf16Status = Rune.DecodeFromUtf16 (utf16, out Rune utf16Rune, out int utf16Consumed);
+ if (utf16Status != OperationStatus.Done) {
+ utf16Rune = Rune.ReplacementChar;
+ utf16Consumed = 1;
+ }
+
+ int runeComparison = utf8Rune.Value.CompareTo (utf16Rune.Value);
+ if (runeComparison != 0)
+ return runeComparison;
+
+ utf8 = utf8.Slice (utf8Consumed);
+ utf16 = utf16.Slice (utf16Consumed);
+ }
+
+ if (utf8.IsEmpty)
+ return utf16.IsEmpty ? 0 : -1;
+ return 1;
+ }
+
static unsafe int LowerBoundByName (void* entries, uint count, int entrySize, ReadOnlySpan key)
{
int left = 0;
@@ -240,21 +282,47 @@ static unsafe int LowerBoundByName (void* entries, uint count, int entrySize, Re
return left;
}
+ static unsafe int LowerBoundByName (void* entries, uint count, int entrySize, ReadOnlySpan key, bool keyIsAscii)
+ {
+ int left = 0;
+ int right = checked ((int)count);
+ while (left < right) {
+ int middle = left + ((right - left) / 2);
+ var name = *(NativeJniRemappingString*)((byte*)entries + (middle * entrySize));
+ if (Compare (name, key, keyIsAscii) < 0) {
+ left = middle + 1;
+ } else {
+ right = middle;
+ }
+ }
+ return left;
+ }
+
+ static unsafe byte* LookupType (NativeJniRemappingTypeReplacementEntry* entries, uint count, ReadOnlySpan key)
+ {
+ bool keyIsAscii = Ascii.IsValid (key);
+ int index = LowerBoundByName (entries, count, sizeof (NativeJniRemappingTypeReplacementEntry), key, keyIsAscii);
+ if (index >= checked ((int)count) || !Equal (entries [index].name, key, keyIsAscii))
+ return null;
+ return entries [index].replacement;
+ }
+
static unsafe byte* LookupType (NativeJniRemappingTypeReplacementEntry* entries, uint count, ReadOnlySpan key)
{
int index = LowerBoundByName (entries, count, sizeof (NativeJniRemappingTypeReplacementEntry), key);
- if (index >= checked ((int)count) || !Equal (entries [index].name, key)) {
+ if (index >= checked ((int)count) || !Equal (entries [index].name, key))
return null;
- }
return entries [index].replacement;
}
static unsafe NativeJniRemappingReplacementMethod* LookupMethod (
NativeJniRemappingData* data,
ReadOnlySpan sourceType,
- ReadOnlySpan name,
- ReadOnlySpan signature)
+ ReadOnlySpan name,
+ ReadOnlySpan signature,
+ out byte* matchedSignature)
{
+ matchedSignature = null;
int typeIndex = LowerBoundByName (
data->method_replacement_index,
data->method_replacement_index_count,
@@ -262,42 +330,75 @@ static unsafe int LowerBoundByName (void* entries, uint count, int entrySize, Re
sourceType
);
if (typeIndex >= checked ((int)data->method_replacement_index_count) ||
- !Equal (data->method_replacement_index [typeIndex].name, sourceType)) {
+ !Equal (data->method_replacement_index [typeIndex].name, sourceType))
return null;
- }
NativeJniRemappingIndexTypeEntry* type = &data->method_replacement_index [typeIndex];
- int first = LowerBoundByName (type->methods, type->method_count, sizeof (NativeJniRemappingIndexMethodEntry), name);
+ return LookupMethod (type, name, signature, out matchedSignature);
+ }
+
+ static unsafe NativeJniRemappingReplacementMethod* LookupMethod (
+ NativeJniRemappingData* data,
+ ReadOnlySpan sourceType,
+ ReadOnlySpan name,
+ ReadOnlySpan signature,
+ out byte* matchedSignature)
+ {
+ matchedSignature = null;
+ bool sourceTypeIsAscii = Ascii.IsValid (sourceType);
+ int typeIndex = LowerBoundByName (
+ data->method_replacement_index,
+ data->method_replacement_index_count,
+ sizeof (NativeJniRemappingIndexTypeEntry),
+ sourceType,
+ sourceTypeIsAscii
+ );
+ if (typeIndex >= checked ((int)data->method_replacement_index_count) ||
+ !Equal (data->method_replacement_index [typeIndex].name, sourceType, sourceTypeIsAscii))
+ return null;
+
+ NativeJniRemappingIndexTypeEntry* type = &data->method_replacement_index [typeIndex];
+ return LookupMethod (type, name, signature, out matchedSignature);
+ }
+
+ static unsafe NativeJniRemappingReplacementMethod* LookupMethod (
+ NativeJniRemappingIndexTypeEntry* type,
+ ReadOnlySpan name,
+ ReadOnlySpan signature,
+ out byte* matchedSignature)
+ {
+ matchedSignature = null;
+ bool nameIsAscii = Ascii.IsValid (name);
+ int first = LowerBoundByName (type->methods, type->method_count, sizeof (NativeJniRemappingIndexMethodEntry), name, nameIsAscii);
int count = checked ((int)type->method_count);
- if (first >= count || !Equal (type->methods [first].name, name)) {
+ if (first >= count || !Equal (type->methods [first].name, name, nameIsAscii))
return null;
- }
int last = first + 1;
- while (last < count && Equal (type->methods [last].name, name)) {
+ while (last < count && Equal (type->methods [last].name, name, nameIsAscii))
last++;
- }
if (signature.Length > 0) {
+ bool signatureIsAscii = Ascii.IsValid (signature);
for (int i = first; i < last; i++) {
NativeJniRemappingIndexMethodEntry* entry = &type->methods [i];
- if (entry->signature.length != 0 && Equal (entry->signature, signature)) {
+ if (entry->signature.length != 0 && Equal (entry->signature, signature, signatureIsAscii)) {
+ matchedSignature = entry->signature.str;
return &entry->replacement;
}
}
int closeParenthesis = signature.Length - 1;
- while (closeParenthesis >= 0 && signature [closeParenthesis] != (byte)')') {
+ while (closeParenthesis >= 0 && signature [closeParenthesis] != ')')
closeParenthesis--;
- }
int prefixLength = closeParenthesis + 1;
if (prefixLength > 0 && prefixLength != signature.Length) {
- ReadOnlySpan signaturePrefix = signature.Slice (0, prefixLength);
+ ReadOnlySpan signaturePrefix = signature.Slice (0, prefixLength);
+ bool signaturePrefixIsAscii = signatureIsAscii || Ascii.IsValid (signaturePrefix);
for (int i = first; i < last; i++) {
NativeJniRemappingIndexMethodEntry* entry = &type->methods [i];
- if (entry->signature.length != 0 && Equal (entry->signature, signaturePrefix)) {
+ if (entry->signature.length != 0 && Equal (entry->signature, signaturePrefix, signaturePrefixIsAscii))
return &entry->replacement;
- }
}
}
}
@@ -310,4 +411,13 @@ static unsafe int LowerBoundByName (void* entries, uint count, int entrySize, Re
}
return null;
}
+
+ static unsafe ReadOnlySpan GetNullTerminatedUtf8Span (IntPtr value)
+ {
+ byte* start = (byte*)value;
+ int length = 0;
+ while (start [length] != 0)
+ length++;
+ return new ReadOnlySpan