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 (start, length); + } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 59856a9db82..263008c6c2d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -378,12 +378,18 @@ protected override IEnumerable GetTypeSignaturesCore (Type typ protected override string? GetReplacementTypeCore (string jniSimpleReference) => JniRemappingLookup.GetReplacementType (jniSimpleReference); + protected override IntPtr GetReplacementTypeUtf8Core (string jniSimpleReference) + => JniRemappingLookup.GetReplacementTypeUtf8 (jniSimpleReference); + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) => JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) => JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (IntPtr jniSourceTypeUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) + => JniRemappingLookup.GetReplacementMethodInfo (jniSourceTypeUtf8, jniMethodName, jniMethodSignature); + // The rest of the APIs are unsupported - they are not needed internally anywhere anyway protected override Type? GetInvokerTypeCore (Type type) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index d51d799b7a6..541bc845ab1 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -220,14 +220,15 @@ sealed class JniRemappingData StructureInfo jniRemappingIndexTypeEntryStructureInfo; StructureInfo jniRemappingTypeReplacementEntryStructureInfo; StructureInfo jniRemappingDataStructureInfo; + readonly Dictionary utf8SortKeys = new Dictionary (StringComparer.Ordinal); public int ReplacementTypeCount { get; private set; } = 0; public int ReplacementMethodIndexEntryCount { get; private set; } = 0; - static int CompareUtf8 (string left, string right) + int CompareUtf8 (string left, string right) { - byte [] leftBytes = Encoding.UTF8.GetBytes (left); - byte [] rightBytes = Encoding.UTF8.GetBytes (right); + byte [] leftBytes = GetUtf8SortKey (left); + byte [] rightBytes = GetUtf8SortKey (right); int min = Math.Min (leftBytes.Length, rightBytes.Length); for (int i = 0; i < min; i++) { if (leftBytes [i] != rightBytes [i]) { @@ -237,6 +238,15 @@ static int CompareUtf8 (string left, string right) return leftBytes.Length.CompareTo (rightBytes.Length); } + byte [] GetUtf8SortKey (string value) + { + if (!utf8SortKeys.TryGetValue (value, out byte [] bytes)) { + bytes = Encoding.UTF8.GetBytes (value); + utf8SortKeys.Add (value, bytes); + } + return bytes; + } + public JniRemappingAssemblyGenerator (TaskLoggingHelper log) : base (log) {} diff --git a/tests/MSBuildDeviceIntegration/Resources/RemapActivity.java b/tests/MSBuildDeviceIntegration/Resources/RemapActivity.java index 2be2dc9b667..6e5d919fbd5 100644 --- a/tests/MSBuildDeviceIntegration/Resources/RemapActivity.java +++ b/tests/MSBuildDeviceIntegration/Resources/RemapActivity.java @@ -7,6 +7,10 @@ public void onMyCreate (android.os.Bundle bundle) { Log.d ("*REMAP-TEST*", "RemapActivity.onMyCreate() invoked!"); super.onCreate(bundle); } + + public void méthodeCible () { + Log.d ("*REMAP-TEST*", "RemapActivity.méthodeCible() invoked!"); + } } class ViewHelper { diff --git a/tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml b/tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml index 283a3557045..9851ef65b1d 100644 --- a/tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml +++ b/tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml @@ -10,4 +10,10 @@ source-method-name="setOnClickListener" target-type="example/ViewHelper" target-method-name="mySetOnClickListener" target-method-instance-to-static="true" /> + diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index a50837a873b..3750b69c39f 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -2553,7 +2553,18 @@ public void TypeAndMemberRemapping ([Values] bool isRelease, [Values (AndroidRun }, }; proj.SetRuntime (runtime); - proj.MainActivity = proj.DefaultMainActivity.Replace (": Activity", ": global::Example.RemapActivity"); + proj.MainActivity = proj.DefaultMainActivity + .Replace (": Activity", ": global::Example.RemapActivity") + .Replace ("//${AFTER_ONCREATE}", """ + unsafe { + var members = new Java.Interop.JniPeerMembers ("example/RemapActivity", typeof (global::Example.RemapActivity)); + try { + members.InstanceMethods.InvokeNonvirtualVoidMethod ("méthodeSource.()V", this, null); + } finally { + Java.Interop.JniPeerMembers.Dispose (members); + } + } +"""); var builder = CreateApkBuilder (); Assert.IsTrue (builder.Build (proj), "`dotnet build` should succeed"); RunProjectAndAssert (proj, builder); @@ -2572,6 +2583,11 @@ public void TypeAndMemberRemapping ([Values] bool isRelease, [Values (AndroidRun logcatOutput, "View.setOnClickListener() wasn't remapped to ViewHelper.mySetOnClickListener()!" ); + StringAssert.Contains ( + "RemapActivity.méthodeCible() invoked!", + logcatOutput, + "The non-ASCII method name wasn't remapped!" + ); } [Test] From 4acf7fe8ab47b1826c4fb22a0f4bcb9d8406d73e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 16 Sep 2026 12:48:53 +0200 Subject: [PATCH 07/19] [tests] Cover non-ASCII JNI remap lookup Exercise allocation-free UTF-8/UTF-16 comparison for a non-ASCII replacement type and method name through the CoreCLR device remapping path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml | 1 + tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml b/tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml index 9851ef65b1d..67db7a9d2b1 100644 --- a/tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml +++ b/tests/MSBuildDeviceIntegration/Resources/RemapActivity.xml @@ -1,5 +1,6 @@ + Date: Wed, 16 Sep 2026 14:07:46 +0200 Subject: [PATCH 08/19] [runtime] Bound mixed ASCII comparison rescans Compare UTF-8 and UTF-16 ASCII names in vector-sized chunks so binary-search ordering rescans at most one chunk after Ascii.Equals reports a mismatch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../JniRemappingLookup.cs | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs index 57f84ea630e..32ee274b4b3 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs @@ -12,6 +12,8 @@ namespace Microsoft.Android.Runtime; static class JniRemappingLookup { + const int AsciiComparisonChunkSize = 16; + unsafe struct NativeJniRemappingString { public uint length; @@ -215,13 +217,28 @@ static unsafe int Compare (NativeJniRemappingString value, ReadOnlySpan ke 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); + int offset = 0; + while (commonLength - offset >= AsciiComparisonChunkSize) { + ReadOnlySpan utf8Chunk = utf8.Slice (offset, AsciiComparisonChunkSize); + ReadOnlySpan asciiChunk = ascii.Slice (offset, AsciiComparisonChunkSize); + if (!Ascii.Equals (utf8Chunk, asciiChunk)) { + for (int i = 0; i < AsciiComparisonChunkSize; i++) { + int result = utf8Chunk [i].CompareTo ((byte)asciiChunk [i]); + if (result != 0) + return result; + } + } + offset += AsciiComparisonChunkSize; + } - for (int i = 0; i < commonLength; i++) { - int result = utf8 [i].CompareTo ((byte)ascii [i]); - if (result != 0) - return result; + ReadOnlySpan utf8Tail = utf8.Slice (offset, commonLength - offset); + ReadOnlySpan asciiTail = ascii.Slice (offset, commonLength - offset); + if (!Ascii.Equals (utf8Tail, asciiTail)) { + for (int i = 0; i < utf8Tail.Length; i++) { + int result = utf8Tail [i].CompareTo ((byte)asciiTail [i]); + if (result != 0) + return result; + } } return utf8.Length.CompareTo (ascii.Length); } From b40538679e67e813389cc5939f9bff8d0137d9cf Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 16 Sep 2026 17:20:11 +0200 Subject: [PATCH 09/19] [build] Suppress generated remapping pointer warnings The native assembler populates the JniRemappingData table pointers during code generation, so suppress CS0649 for those two fields just like the existing generated methods pointer. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Utilities/JniRemappingAssemblyGenerator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index 541bc845ab1..bd119612a77 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -202,10 +202,12 @@ sealed class JniRemappingTypeReplacementEntry sealed class JniRemappingData { [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] +#pragma warning disable CS0649 // Field is populated during native code generation public JniRemappingTypeReplacementEntry type_replacements; [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] public JniRemappingIndexTypeEntry method_replacement_index; +#pragma warning restore CS0649 public uint type_replacement_count; public uint method_replacement_index_count; From 2af8f5e0e54ce04813a3fb390b87ffa952b46d5c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 16 Sep 2026 23:41:16 +0200 Subject: [PATCH 10/19] [tests] Update CoreCLR R8 APK size reference Update the Xamarin.Forms CoreCLR R8 Release APK description from the matching failed CI test attachment. macOS and Windows produced the same package size; their assembly stores differed by only 16 bytes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc index de61cb7ae8c..cf43b2803ff 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc @@ -32,7 +32,7 @@ "Size": 9457592 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2818048 + "Size": 2819424 }, "lib/arm64-v8a/libcoreclr.so": { "Size": 4843824 From 6d624531dcebeefdab05501cb59ec621fa8e5398 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 18 Sep 2026 11:43:42 +0200 Subject: [PATCH 11/19] [build] Reuse cached UTF-8 remap keys Use the generator UTF-8 sort-key cache when computing serialized string lengths so each distinct remapping string is encoded only once. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Utilities/JniRemappingAssemblyGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index bd119612a77..e61e25ffa43 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -336,7 +336,7 @@ uint GetLength (string str) return 0; } - return (uint)Encoding.UTF8.GetBytes (str).Length; + return (uint)GetUtf8SortKey (str).Length; } } From 0300c1a4a610c43d2065bf9f3a443d01dfdd2faa Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 18 Sep 2026 11:47:57 +0200 Subject: [PATCH 12/19] [build] Compute UTF-8 remap lengths without allocation Use Encoding.UTF8.GetByteCount for serialized lengths so signatures and other non-sort values are not retained in the sort-key cache. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Utilities/JniRemappingAssemblyGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index e61e25ffa43..094a5664ca7 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -336,7 +336,7 @@ uint GetLength (string str) return 0; } - return (uint)GetUtf8SortKey (str).Length; + return (uint)Encoding.UTF8.GetByteCount (str); } } From 25652d27d76e6d087f9089eec1874eb1d76803f5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 18 Sep 2026 11:52:54 +0200 Subject: [PATCH 13/19] [runtime] Use one replacement type lookup Override the combined replacement-type lookup in both Android type managers so generated UTF-8 table misses do not fall through into a second compatibility string search. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/AndroidRuntime.cs | 5 +++-- .../TrimmableTypeMapTypeManager.cs | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index e47622ab714..327ce0d55e8 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -382,9 +382,10 @@ protected override IEnumerable GetSimpleReferences (Type type) return JniRemappingLookup.GetReplacementType (jniSimpleReference); } - protected override IntPtr GetReplacementTypeUtf8Core (string jniSimpleReference) + protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8) { - return JniRemappingLookup.GetReplacementTypeUtf8 (jniSimpleReference); + replacement = null; + replacementUtf8 = JniRemappingLookup.GetReplacementTypeUtf8 (jniSimpleReference); } protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 263008c6c2d..a64c08f9f26 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -378,8 +378,11 @@ protected override IEnumerable GetTypeSignaturesCore (Type typ protected override string? GetReplacementTypeCore (string jniSimpleReference) => JniRemappingLookup.GetReplacementType (jniSimpleReference); - protected override IntPtr GetReplacementTypeUtf8Core (string jniSimpleReference) - => JniRemappingLookup.GetReplacementTypeUtf8 (jniSimpleReference); + protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8) + { + replacement = null; + replacementUtf8 = JniRemappingLookup.GetReplacementTypeUtf8 (jniSimpleReference); + } protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) => JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); From 7a277d2c8a05c6fab2f4982e0a5b3e1c58d00236 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 17 Sep 2026 22:05:42 +0200 Subject: [PATCH 14/19] [Java.Interop] Extend JNI member remapping contract Add target descriptors, field remapping, and renamed-owner fallback while preserving existing custom type-manager defaults. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../JniPeerMembers.JniInstanceFields.cs | 33 ++- .../JniPeerMembers.JniInstanceMethods.cs | 43 +++- .../JniPeerMembers.JniStaticFields.cs | 33 ++- .../JniPeerMembers.JniStaticMethods.cs | 8 + .../Java.Interop/JniPeerMembers.cs | 72 +++++- .../Java.Interop/JniRuntime.JniTypeManager.cs | 90 +++++++ .../JniRuntime.ReflectionJniTypeManager.cs | 2 + .../src/Java.Interop/Java.Interop/JniType.cs | 14 + .../src/Java.Interop/PublicAPI.Unshipped.txt | 24 ++ .../Java.Interop-Tests.csproj | 3 + .../Java.Interop/JavaVMFixture.cs | 42 +++ .../Java.Interop/JniPeerMembersTests.cs | 241 ++++++++++++++++++ .../java/net/dot/jni/test/FieldRemapBase.java | 23 ++ .../net/dot/jni/test/FieldRemapDerived.java | 9 + .../dot/jni/test/FieldRemapRenamedBase.java | 23 ++ 15 files changed, 650 insertions(+), 10 deletions(-) create mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java create mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java create mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapRenamedBase.java diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs index 18b6fbd6ee6..aafb1156963 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs @@ -29,8 +29,39 @@ public JniFieldInfo GetFieldInfo (string encodedMember) return InstanceFields.GetOrAdd (encodedMember, static (member, fields) => { ReadOnlySpan field, signature; JniPeerMembers.GetNameAndSignature (member, out field, out signature); - return fields.Members.JniPeerType.GetInstanceField (field, signature); + return fields.GetFieldInfo (field, signature); }, this); } + + JniFieldInfo GetFieldInfo (ReadOnlySpan field, ReadOnlySpan signature) + { + var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, field, signature); + if (newField.HasValue) { + var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName; + var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field; + var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature; + + using var t = new JniType (typeName); + if (t.TryGetInstanceField (fieldName, fieldSig, out var f)) { + return f; + } + } + if (Members.JniPeerType.TryGetInstanceField (field, signature, out var originalField)) { + return originalField; + } + + newField = JniPeerMembers.GetBaseReplacementFieldInfo (Members.ManagedPeerType, field, signature); + if (newField.HasValue) { + var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName; + var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field; + var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature; + + using var t = new JniType (typeName); + if (t.TryGetInstanceField (fieldName, fieldSig, out var f)) { + return f; + } + } + return Members.JniPeerType.GetInstanceField (field, signature); + } }} } 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 0f834bba544..d40b91c6d3d 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 @@ -24,12 +24,16 @@ internal JniInstanceMethods (JniPeerMembers members) declaringType.FullName)); DeclaringType = declaringType; - jniPeerType = new JniType (info.Name); + targetJniTypeName = info.Name; + jniPeerType = new JniType (targetJniTypeName); jniPeerType.RegisterWithRuntime (); } JniPeerMembers? members; JniType? jniPeerType; + readonly string? targetJniTypeName; + + string TargetJniTypeName => targetJniTypeName ?? Members.JniPeerTypeName; internal JniPeerMembers Members => members ?? throw new InvalidOperationException (); @@ -60,7 +64,22 @@ public JniMethodInfo GetConstructor (string signature) if (signature == null) throw new ArgumentNullException (nameof (signature)); return InstanceMethods.GetOrAdd (signature, static (member, methods) => - methods.JniPeerType.GetConstructor (member.AsSpan ()), this); + methods.GetConstructorCore (member), this); + } + + JniMethodInfo GetConstructorCore (string signature) + { + // Constructors are never renamed, but their parameter types can be, so the descriptor + // still has to be translated. + var newMethod = JniPeerMembers.GetReplacementMethodInfo (TargetJniTypeName, "", signature); + if (newMethod.HasValue) { + var info = newMethod.Value; + using var t = CreateTargetType (info, TargetJniTypeName); + if (TryGetInstanceMethod (t, info, "", signature, out var m)) { + return m; + } + } + return JniPeerType.GetConstructor (signature.AsSpan ()); } internal JniInstanceMethods GetConstructorsForType (Type declaringType) @@ -115,14 +134,30 @@ JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signa m.StaticRedirect = CreateTargetType (info, Members); return m; } - if (TryGetInstanceMethod (t, info, method, signature, out m)) { + if (TryGetInstanceMethod (t, info, method, signature, out m)) return m; - } 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}`!"); } + if (JniPeerType.TryGetInstanceMethod (method, signature, out m)) + return m; + + newMethod = JniPeerMembers.GetBaseReplacementMethodInfo (DeclaringType, method, signature); + if (newMethod.HasValue) { + var info = newMethod.Value; + using var t = CreateTargetType (info, TargetJniTypeName); + if (info.TargetJniMethodInstanceToStatic && + TryGetStaticMethod (t, info, method, signature, out m)) { + m.ParameterCount = info.TargetJniMethodParameterCount; + m.StaticRedirect = CreateTargetType (info, TargetJniTypeName); + return m; + } + if (TryGetInstanceMethod (t, info, method, signature, out m)) + return m; + } + return JniPeerType.GetInstanceMethod (method, signature); } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs index e31a7f25f8f..80bf3b57e5e 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs @@ -24,10 +24,41 @@ public JniFieldInfo GetFieldInfo (string encodedMember) return StaticFields.GetOrAdd (encodedMember, static (member, fields) => { ReadOnlySpan field, signature; JniPeerMembers.GetNameAndSignature (member, out field, out signature); - return fields.Members.JniPeerType.GetStaticField (field, signature); + return fields.GetFieldInfo (field, signature); }, this); } + JniFieldInfo GetFieldInfo (ReadOnlySpan field, ReadOnlySpan signature) + { + var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, field, signature); + if (newField.HasValue) { + var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName; + var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field; + var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature; + + using var t = new JniType (typeName); + if (t.TryGetStaticField (fieldName, fieldSig, out var f)) { + return f; + } + } + if (Members.JniPeerType.TryGetStaticField (field, signature, out var originalField)) { + return originalField; + } + + newField = JniPeerMembers.GetBaseReplacementFieldInfo (Members.ManagedPeerType, field, signature); + if (newField.HasValue) { + var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName; + var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field; + var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature; + + using var t = new JniType (typeName); + if (t.TryGetStaticField (fieldName, fieldSig, out var f)) { + return f; + } + } + return Members.JniPeerType.GetStaticField (field, signature); + } + internal void Dispose () { Clear (ref staticFields); 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 107b97d763b..2ebf29c81c4 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 @@ -47,6 +47,14 @@ JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signa if (Members.JniPeerType.TryGetStaticMethod (method, signature, out m)) { return m; } + newMethod = JniPeerMembers.GetBaseReplacementMethodInfo (Members.ManagedPeerType, method, signature); + if (newMethod.HasValue) { + var info = newMethod.Value; + using var t = CreateTargetType (info, Members); + if (TryGetStaticMethod (t, info, method, signature, out m)) { + return m; + } + } m = FindInFallbackTypes (method, signature); if (m != null) { 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 60d52d916ad..5e5007f5275 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs @@ -40,6 +40,8 @@ public JniPeerTypeNameInfo (string sourceName, string? targetName, IntPtr target static JniPeerTypeNameInfo GetReplacementType (string jniPeerTypeName) { + if (jniPeerTypeName == null) + throw new ArgumentNullException (nameof (jniPeerTypeName)); var typeManager = JniEnvironment.Runtime.TypeManager; typeManager.GetReplacementTypeInfo (jniPeerTypeName, out var replacement, out var replacementUtf8); return new JniPeerTypeNameInfo (jniPeerTypeName, replacement, replacementUtf8); @@ -83,7 +85,7 @@ static JniPeerTypeNameInfo GetReplacementType (string jniPeerTypeName) static JniPeerMembers CreatePeerMembers (string jniPeerTypeName, Type managedPeerType) { - return new JniPeerMembers (new JniPeerTypeNameInfo (jniPeerTypeName, null, IntPtr.Zero), managedPeerType, checkManagedPeerType: false); + return new JniPeerMembers (GetReplacementType (jniPeerTypeName), managedPeerType, checkManagedPeerType: false); } JniType? jniPeerType; @@ -198,14 +200,17 @@ protected virtual JniPeerMembers GetPeerMembers (IJavaPeerable value) } static JniType CreateTargetType (JniRuntime.ReplacementMethodInfo info, JniPeerMembers fallback) + { + return CreateTargetType (info, fallback.JniPeerTypeName); + } + + static JniType CreateTargetType (JniRuntime.ReplacementMethodInfo info, string fallbackTypeName) { 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); + return new JniType (fallbackTypeName); } static bool TryGetInstanceMethod ( @@ -282,6 +287,65 @@ static string GetTargetMethodSignatureForDiagnostics (JniRuntime.ReplacementMeth return fallback.ToString (); } + // Member keys use the replaced type name but retain the managed member name and signature. + internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo ( + string jniTypeName, + ReadOnlySpan method, + ReadOnlySpan signature) + { + return JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (jniTypeName, method, signature); + } + + internal static JniRuntime.ReplacementMethodInfo? GetBaseReplacementMethodInfo ( + Type managedPeerType, + ReadOnlySpan method, + ReadOnlySpan signature) + { + var typeManager = JniEnvironment.Runtime.TypeManager; + for (Type? baseType = managedPeerType.BaseType; baseType != null; baseType = baseType.BaseType) { + var baseSignature = typeManager.GetTypeSignature (baseType); + string? effectiveBaseType = baseSignature.SimpleReference; + if (effectiveBaseType == null) { + continue; + } + effectiveBaseType = typeManager.GetReplacementType (effectiveBaseType) ?? effectiveBaseType; + var info = typeManager.GetReplacementMethodInfo (effectiveBaseType, method, signature); + if (info != null) { + return info; + } + } + return null; + } + + internal static JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo ( + string jniTypeName, + ReadOnlySpan field, + ReadOnlySpan signature) + { + return JniEnvironment.Runtime.TypeManager.GetReplacementFieldInfo (jniTypeName, field, signature); + } + + internal static JniRuntime.ReplacementFieldInfo? GetBaseReplacementFieldInfo ( + Type managedPeerType, + ReadOnlySpan field, + ReadOnlySpan signature) + { + var typeManager = JniEnvironment.Runtime.TypeManager; + for (Type? baseType = managedPeerType.BaseType; baseType != null; baseType = baseType.BaseType) { + var baseSignature = typeManager.GetTypeSignature (baseType); + string? effectiveBaseType = baseSignature.SimpleReference; + if (effectiveBaseType == null) { + continue; + } + effectiveBaseType = typeManager.GetReplacementType (effectiveBaseType) ?? effectiveBaseType; + var info = typeManager.GetReplacementFieldInfo (effectiveBaseType, field, signature); + if (info != null) { + return info; + } + } + return null; + } + 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 ea5f2ab5ba6..93f5cad6b51 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 @@ -127,6 +127,61 @@ public override string ToString () public static bool operator!=(ReplacementMethodInfo a, ReplacementMethodInfo b) => !a.Equals (b); } + [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 ReplacementFieldInfo : IEquatable + { + public string? SourceJniType {get; set;} + public string? SourceJniFieldName {get; set;} + public string? SourceJniFieldSignature {get; set;} + public string? TargetJniType {get; set;} + public string? TargetJniFieldName {get; set;} + public string? TargetJniFieldSignature {get; set;} + + public override bool Equals (object? obj) + { + if (obj is ReplacementFieldInfo o) { + return Equals (o); + } + return false; + } + + public bool Equals (ReplacementFieldInfo other) + { + return string.Equals (SourceJniType, other.SourceJniType) && + string.Equals (SourceJniFieldName, other.SourceJniFieldName) && + string.Equals (SourceJniFieldSignature, other.SourceJniFieldSignature) && + string.Equals (TargetJniType, other.TargetJniType) && + string.Equals (TargetJniFieldName, other.TargetJniFieldName) && + string.Equals (TargetJniFieldSignature, other.TargetJniFieldSignature); + } + + public override int GetHashCode () + { + return (SourceJniType?.GetHashCode () ?? 0) ^ + (SourceJniFieldName?.GetHashCode () ?? 0) ^ + (SourceJniFieldSignature?.GetHashCode () ?? 0) ^ + (TargetJniType?.GetHashCode () ?? 0) ^ + (TargetJniFieldName?.GetHashCode () ?? 0) ^ + (TargetJniFieldSignature?.GetHashCode () ?? 0); + } + + public override string ToString () + { + return $"{nameof (ReplacementFieldInfo)} {{ " + + $"{nameof (SourceJniType)} = \"{SourceJniType}\"" + + $", {nameof (SourceJniFieldName)} = \"{SourceJniFieldName}\"" + + $", {nameof (SourceJniFieldSignature)} = \"{SourceJniFieldSignature}\"" + + $", {nameof (TargetJniType)} = \"{TargetJniType}\"" + + $", {nameof (TargetJniFieldName)} = \"{TargetJniFieldName}\"" + + $", {nameof (TargetJniFieldSignature)} = \"{TargetJniFieldSignature}\"" + + $"}}"; + } + + public static bool operator==(ReplacementFieldInfo a, ReplacementFieldInfo b) => a.Equals (b); + public static bool operator!=(ReplacementFieldInfo a, ReplacementFieldInfo b) => !a.Equals (b); + } + /// public partial class JniTypeManager : IDisposable, ISetRuntime { @@ -389,6 +444,41 @@ protected virtual void GetReplacementTypeInfoCore (string jniSimpleReference, ou protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (IntPtr jniSimpleReferenceUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) => GetReplacementMethodInfoCore (GetUtf8String (jniSimpleReferenceUtf8), jniMethodName, jniMethodSignature); + public ReplacementFieldInfo? GetReplacementFieldInfo (string jniSimpleReference, string jniFieldName, string jniFieldSignature) + { + AssertValid (); + AssertSimpleReference (jniSimpleReference, nameof (jniSimpleReference)); + if (string.IsNullOrEmpty (jniFieldName)) { + throw new ArgumentNullException (nameof (jniFieldName)); + } + if (string.IsNullOrEmpty (jniFieldSignature)) { + throw new ArgumentNullException (nameof (jniFieldSignature)); + } + + return GetReplacementFieldInfoCore (jniSimpleReference, jniFieldName, jniFieldSignature); + } + + protected virtual ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSimpleReference, string jniFieldName, string jniFieldSignature) => null; + + internal ReplacementFieldInfo? GetReplacementFieldInfo (string jniSimpleReference, ReadOnlySpan jniFieldName, ReadOnlySpan jniFieldSignature) + { + AssertValid (); + AssertSimpleReference (jniSimpleReference, nameof (jniSimpleReference)); + if (jniFieldName.IsEmpty) + throw new ArgumentNullException (nameof (jniFieldName)); + if (jniFieldSignature.IsEmpty) + throw new ArgumentNullException (nameof (jniFieldSignature)); + + return GetReplacementFieldInfoCore (jniSimpleReference, jniFieldName, jniFieldSignature); + } + + /// + /// Resolves field remapping without requiring name and signature strings. + /// The default implementation preserves dispatch to the string overload. + /// + protected virtual ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSimpleReference, ReadOnlySpan jniFieldName, ReadOnlySpan jniFieldSignature) + => GetReplacementFieldInfoCore (jniSimpleReference, jniFieldName.ToString (), jniFieldSignature.ToString ()); + // 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/JniRuntime.ReflectionJniTypeManager.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs index 8f8a47e3f9b..f515c53d536 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs @@ -342,6 +342,8 @@ IEnumerable CreateGetTypesForSimpleReferenceEnumerator (string jniSimpleRe protected override ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, string jniMethodName, string jniMethodSignature) => null; + protected override ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSimpleReference, string jniFieldName, string jniFieldSignature) => null; + public override void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) { TryRegisterNativeMembers (nativeClass, type, 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 25f8e7efd9b..be0f27a7884 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs @@ -566,6 +566,20 @@ internal bool TryGetStaticMethod (ReadOnlySpan name, IntPtr signature, [No return method != null; } + internal bool TryGetInstanceField (ReadOnlySpan name, ReadOnlySpan signature, [NotNullWhen (true)] out JniFieldInfo? field) + { + var id = GetMemberID (name, signature, MemberKind.InstanceField, throwOnError: false); + field = id == IntPtr.Zero ? null : CreateFieldInfo (name, signature, id, isStatic: false); + return field != null; + } + + internal bool TryGetStaticField (ReadOnlySpan name, ReadOnlySpan signature, [NotNullWhen (true)] out JniFieldInfo? field) + { + var id = GetMemberID (name, signature, MemberKind.StaticField, throwOnError: false); + field = id == IntPtr.Zero ? null : CreateFieldInfo (name, signature, id, isStatic: true); + return field != null; + } + static JniMethodInfo CreateMethodInfo (ReadOnlySpan name, ReadOnlySpan signature, IntPtr id, bool isStatic) { #if DEBUG diff --git a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt index 11723423a5b..154b67e611e 100644 --- a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt @@ -8,6 +8,7 @@ Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniMethodSignatureUtf8.get - Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniMethodSignatureUtf8.set -> void Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniTypeUtf8.get -> nint Java.Interop.JniRuntime.ReplacementMethodInfo.TargetJniTypeUtf8.set -> void +virtual Java.Interop.JniRuntime.JniTypeManager.GetReplacementFieldInfoCore(string! jniSimpleReference, System.ReadOnlySpan jniFieldName, System.ReadOnlySpan jniFieldSignature) -> Java.Interop.JniRuntime.ReplacementFieldInfo? 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 @@ -127,3 +128,26 @@ override Java.Interop.JniRuntime.ReflectionJniTypeManager.RegisterNativeMembers( override Java.Interop.JniRuntime.ReflectionJniTypeManager.RegisterNativeMembers(Java.Interop.JniType! nativeClass, System.Type! type, System.ReadOnlySpan methods) -> void virtual Java.Interop.JniRuntime.ReflectionJniValueManager.TryConstructPeer(Java.Interop.IJavaPeerable! self, ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options, System.Type! type) -> bool virtual Java.Interop.JniRuntime.ReflectionJniValueManager.CreateNonArrayListValue(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options, System.Type! targetType) -> object? +Java.Interop.JniRuntime.JniTypeManager.GetReplacementFieldInfo(string! jniSimpleReference, string! jniFieldName, string! jniFieldSignature) -> Java.Interop.JniRuntime.ReplacementFieldInfo? +Java.Interop.JniRuntime.ReplacementFieldInfo +Java.Interop.JniRuntime.ReplacementFieldInfo.Equals(Java.Interop.JniRuntime.ReplacementFieldInfo other) -> bool +Java.Interop.JniRuntime.ReplacementFieldInfo.ReplacementFieldInfo() -> void +Java.Interop.JniRuntime.ReplacementFieldInfo.SourceJniFieldName.get -> string? +Java.Interop.JniRuntime.ReplacementFieldInfo.SourceJniFieldName.set -> void +Java.Interop.JniRuntime.ReplacementFieldInfo.SourceJniFieldSignature.get -> string? +Java.Interop.JniRuntime.ReplacementFieldInfo.SourceJniFieldSignature.set -> void +Java.Interop.JniRuntime.ReplacementFieldInfo.SourceJniType.get -> string? +Java.Interop.JniRuntime.ReplacementFieldInfo.SourceJniType.set -> void +Java.Interop.JniRuntime.ReplacementFieldInfo.TargetJniFieldName.get -> string? +Java.Interop.JniRuntime.ReplacementFieldInfo.TargetJniFieldName.set -> void +Java.Interop.JniRuntime.ReplacementFieldInfo.TargetJniFieldSignature.get -> string? +Java.Interop.JniRuntime.ReplacementFieldInfo.TargetJniFieldSignature.set -> void +Java.Interop.JniRuntime.ReplacementFieldInfo.TargetJniType.get -> string? +Java.Interop.JniRuntime.ReplacementFieldInfo.TargetJniType.set -> void +override Java.Interop.JniRuntime.ReflectionJniTypeManager.GetReplacementFieldInfoCore(string! jniSimpleReference, string! jniFieldName, string! jniFieldSignature) -> Java.Interop.JniRuntime.ReplacementFieldInfo? +override Java.Interop.JniRuntime.ReplacementFieldInfo.Equals(object? obj) -> bool +override Java.Interop.JniRuntime.ReplacementFieldInfo.GetHashCode() -> int +override Java.Interop.JniRuntime.ReplacementFieldInfo.ToString() -> string! +static Java.Interop.JniRuntime.ReplacementFieldInfo.operator !=(Java.Interop.JniRuntime.ReplacementFieldInfo a, Java.Interop.JniRuntime.ReplacementFieldInfo b) -> bool +static Java.Interop.JniRuntime.ReplacementFieldInfo.operator ==(Java.Interop.JniRuntime.ReplacementFieldInfo a, Java.Interop.JniRuntime.ReplacementFieldInfo b) -> bool +virtual Java.Interop.JniRuntime.JniTypeManager.GetReplacementFieldInfoCore(string! jniSimpleReference, string! jniFieldName, string! jniFieldSignature) -> Java.Interop.JniRuntime.ReplacementFieldInfo? diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj index 1d43a2ca427..7df4b7ff57c 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj @@ -36,6 +36,9 @@ + + + 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 8b5459df5eb..c161713b816 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 @@ -137,6 +137,7 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type) Dictionary ReplacmentTypes = new() { ["net/dot/jni/test/RenameClassBase1"] = "net/dot/jni/test/RenameClassBase2", + [FieldRemapBase.JniTypeName] = FieldRemapBase.RuntimeJniTypeName, }; string? trackedReplacementType; @@ -181,8 +182,49 @@ protected override void GetReplacementTypeInfoCore (string jniSimpleReference, o // 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, ReplacementMethodStorage.TypeUtf8 | ReplacementMethodStorage.MethodUtf8), + + // Renamed parameter types: the target descriptor is pinned explicitly, which is what + // `target-method-signature` carries. + [("java/lang/StringBuilder", "", "(Lnet/dot/jni/test/RenamedInt;)V")] = (null, "", "(I)V", null, false, ReplacementMethodStorage.Strings), + [("java/lang/StringBuilder", "indexOf", "(Lnet/dot/jni/test/RenamedString;)I")] = (null, "indexOf", "(Ljava/lang/String;)I", null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "hiddenInstanceMethod", "()I")] = (null, "remappedInstanceMethod", null, null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "hiddenStaticMethod", "()I")] = (null, "remappedStaticMethod", null, null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "inheritedInstanceMethod", "()I")] = (null, "remappedInheritedInstanceMethod", null, null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "inheritedStaticMethod", "()I")] = (null, "remappedInheritedStaticMethod", null, null, false, ReplacementMethodStorage.Strings), + [(FieldRemapDerived.JniTypeName, "inheritedInstanceMethod", "()I")] = (null, "missingInstanceMethod", null, null, false, ReplacementMethodStorage.Strings), + [(FieldRemapDerived.JniTypeName, "inheritedStaticMethod", "()I")] = (null, "missingStaticMethod", null, null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "remappedSpecificity", "(I)I")] = (null, "specificityExact", "(I)I", null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "remappedSpecificity", "(I)")] = (null, "specificityParameters", "(I)V", null, false, ReplacementMethodStorage.Strings), + [(FieldRemapBase.RuntimeJniTypeName, "remappedSpecificity", null)] = (null, "specificityWildcard", null, null, false, ReplacementMethodStorage.Strings), + }; + + Dictionary<(string SourceType, string SourceName, string? SourceSignature), (string? TargetType, string? TargetName, string? TargetSignature)> ReplacementFields = new() { + [("java/lang/Math", "remappedToPi", "D")] = (null, "PI", null), + [("java/io/ByteArrayInputStream", "remappedToPos", "I")] = (null, "pos", null), + [(FieldRemapBase.RuntimeJniTypeName, "hiddenInstanceField", "Z")] = (null, "remappedInstanceField", null), + [(FieldRemapBase.RuntimeJniTypeName, "hiddenStaticField", "Ljava/lang/String;")] = (null, "remappedStaticField", null), + [(FieldRemapBase.RuntimeJniTypeName, "inheritedInstanceField", "Z")] = (null, "remappedInheritedInstanceField", null), + [(FieldRemapBase.RuntimeJniTypeName, "inheritedStaticField", "Ljava/lang/String;")] = (null, "remappedInheritedStaticField", null), + [(FieldRemapDerived.JniTypeName, "inheritedInstanceField", "Z")] = (null, "missingInstanceField", null), + [(FieldRemapDerived.JniTypeName, "inheritedStaticField", "Ljava/lang/String;")] = (null, "missingStaticField", null), }; + protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, string jniFieldName, string jniFieldSignature) + { + if (!ReplacementFields.TryGetValue ((jniSourceType, jniFieldName, jniFieldSignature), out var r) && + !ReplacementFields.TryGetValue ((jniSourceType, jniFieldName, null), out r)) { + return null; + } + return new JniRuntime.ReplacementFieldInfo { + SourceJniType = jniSourceType, + SourceJniFieldName = jniFieldName, + SourceJniFieldSignature = jniFieldSignature, + TargetJniType = r.TargetType ?? jniSourceType, + TargetJniFieldName = r.TargetName ?? jniFieldName, + TargetJniFieldSignature = r.TargetSignature ?? jniFieldSignature, + }; + } + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) { // Console.Error.WriteLine ($"# jonp: looking for replacement method for (\"{jniSourceType}\", \"{jniMethodName}\", \"{jniMethodSignature}\")"); 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 40ed2dc5390..33ccb9f8371 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 @@ -226,6 +226,215 @@ public void MethodLookupForNonexistentStaticMethodWillTryFallbacks () } } + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void ReplaceStaticFieldName () + { + // Resolves `java.lang.Math.PI`, not the nonexistent `remappedToPi`. + var info = JavaLangRemappingTestMath._members.StaticFields.GetFieldInfo ("remappedToPi.D"); + Assert.IsNotNull (info); + Assert.IsTrue (info.IsStatic); + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void ReplaceInstanceFieldName () + { + // Resolves `java.io.ByteArrayInputStream.pos`, not the nonexistent `remappedToPos`. + var info = JavaIoRemappingTestStream._members.InstanceFields.GetFieldInfo ("remappedToPos.I"); + Assert.IsNotNull (info); + Assert.IsFalse (info.IsStatic); + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void DeclaredInstanceFieldHidesBaseFieldRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapDerived.JniTypeName); + var expected = type.GetInstanceField ("hiddenInstanceField", "Z"); + var remapped = type.GetInstanceField ("remappedInstanceField", "Z"); + var actual = members.InstanceFields.GetFieldInfo ("hiddenInstanceField.Z"); + + Assert.AreEqual (expected.ID, actual.ID); + Assert.AreNotEqual (remapped.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void DeclaredStaticFieldHidesBaseFieldRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapDerived.JniTypeName); + var expected = type.GetStaticField ("hiddenStaticField", "Ljava/lang/String;"); + var remapped = type.GetStaticField ("remappedStaticField", "Ljava/lang/String;"); + var actual = members.StaticFields.GetFieldInfo ("hiddenStaticField.Ljava/lang/String;"); + + Assert.AreEqual (expected.ID, actual.ID); + Assert.AreNotEqual (remapped.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void FailedCurrentInstanceFieldRemapFallsBackToBaseRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapBase.RuntimeJniTypeName); + var expected = type.GetInstanceField ("remappedInheritedInstanceField", "Z"); + var actual = members.InstanceFields.GetFieldInfo ("inheritedInstanceField.Z"); + + Assert.AreEqual (expected.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void FailedCurrentStaticFieldRemapFallsBackToBaseRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapBase.RuntimeJniTypeName); + var expected = type.GetStaticField ("remappedInheritedStaticField", "Ljava/lang/String;"); + var actual = members.StaticFields.GetFieldInfo ("inheritedStaticField.Ljava/lang/String;"); + + Assert.AreEqual (expected.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void DeclaredInstanceMethodHidesBaseMethodRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapDerived.JniTypeName); + var expected = type.GetInstanceMethod ("hiddenInstanceMethod", "()I"); + var remapped = type.GetInstanceMethod ("remappedInstanceMethod", "()I"); + var actual = members.InstanceMethods.GetMethodInfo ("hiddenInstanceMethod.()I"); + + Assert.AreEqual (expected.ID, actual.ID); + Assert.AreNotEqual (remapped.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void DeclaredStaticMethodHidesBaseMethodRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapDerived.JniTypeName); + var expected = type.GetStaticMethod ("hiddenStaticMethod", "()I"); + var remapped = type.GetStaticMethod ("remappedStaticMethod", "()I"); + var actual = members.StaticMethods.GetMethodInfo ("hiddenStaticMethod.()I"); + + Assert.AreEqual (expected.ID, actual.ID); + Assert.AreNotEqual (remapped.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void FailedCurrentInstanceMethodRemapFallsBackToRenamedBaseRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapBase.RuntimeJniTypeName); + var expected = type.GetInstanceMethod ("remappedInheritedInstanceMethod", "()I"); + var actual = members.InstanceMethods.GetMethodInfo ("inheritedInstanceMethod.()I"); + + Assert.AreEqual (expected.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void FailedCurrentStaticMethodRemapFallsBackToRenamedBaseRemap () + { + var members = new JniPeerMembers (FieldRemapDerived.JniTypeName, typeof (FieldRemapDerived)); + try { + using var type = new JniType (FieldRemapBase.RuntimeJniTypeName); + var expected = type.GetStaticMethod ("remappedInheritedStaticMethod", "()I"); + var actual = members.StaticMethods.GetMethodInfo ("inheritedStaticMethod.()I"); + + Assert.AreEqual (expected.ID, actual.ID); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public unsafe void MethodRemappingPrefersSpecificSignatures () + { + var members = new JniPeerMembers (FieldRemapBase.JniTypeName, typeof (FieldRemapBase)); + try { + var intArgument = new JniArgumentValue (1); + Assert.AreEqual (101, members.StaticMethods.InvokeInt32Method ("remappedSpecificity.(I)I", &intArgument)); + + intArgument = new JniArgumentValue (2); + members.StaticMethods.InvokeVoidMethod ("remappedSpecificity.(I)V", &intArgument); + using var type = new JniType (FieldRemapBase.RuntimeJniTypeName); + var valueField = type.GetStaticField ("specificityValue", "I"); + Assert.AreEqual (202, JniEnvironment.StaticFields.GetStaticIntField (type.PeerReference, valueField)); + + var longArgument = new JniArgumentValue (3L); + Assert.AreEqual (303, members.StaticMethods.InvokeInt32Method ("remappedSpecificity.(J)I", &longArgument)); + } finally { + JniPeerMembers.Dispose (members); + } + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void ReplacementConstructorUsesTargetSignature () + { + // The declared parameter type does not exist; the replacement pins `(I)V` instead. + var ctor = JavaLangRemappingTestStringBuilder._members.InstanceMethods.GetConstructor ("(Lnet/dot/jni/test/RenamedInt;)V"); + Assert.IsNotNull (ctor); + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void ReplacementMethodUsesTargetSignature () + { + // The declared parameter type does not exist; the replacement pins `(Ljava/lang/String;)I` instead. + var method = JavaLangRemappingTestStringBuilder._members.InstanceMethods.GetMethodInfo ("indexOf.(Lnet/dot/jni/test/RenamedString;)I"); + Assert.IsNotNull (method); + } + [Test] [Category ("NativeAOTIgnore")] [Category ("TrimmableTypeMapUnsupported")] @@ -389,6 +598,38 @@ public unsafe int remappedToStaticHashCode () } } + [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] + class JavaLangRemappingTestMath : JavaObject { + internal const string JniTypeName = "java/lang/Math"; + internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaLangRemappingTestMath)); + } + + [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] + class JavaIoRemappingTestStream : JavaObject { + internal const string JniTypeName = "java/io/ByteArrayInputStream"; + internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaIoRemappingTestStream)); + } + + [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] + class JavaLangRemappingTestStringBuilder : JavaObject { + internal const string JniTypeName = "java/lang/StringBuilder"; + internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaLangRemappingTestStringBuilder)); + } + + [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] + class FieldRemapBase : JavaObject { + internal const string JniTypeName = "net/dot/jni/test/FieldRemapBase"; + internal const string RuntimeJniTypeName = "net/dot/jni/test/FieldRemapRenamedBase"; + static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (FieldRemapBase)); + + public override JniPeerMembers JniPeerMembers => _members; + } + + [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] + class FieldRemapDerived : FieldRemapBase { + internal new const string JniTypeName = "net/dot/jni/test/FieldRemapDerived"; + } + [JniTypeSignature (JavaLangRemappingTestRuntime.JniTypeName, GenerateJavaPeer=false)] internal class JavaLangRemappingTestRuntime : JavaObject { internal const string JniTypeName = "java/lang/Runtime"; diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java new file mode 100644 index 00000000000..c1ea18bf978 --- /dev/null +++ b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapBase.java @@ -0,0 +1,23 @@ +package net.dot.jni.test; + +public class FieldRemapBase +{ + public boolean hiddenInstanceField; + public boolean remappedInstanceField; + public boolean remappedInheritedInstanceField; + public static String hiddenStaticField = "base"; + public static String remappedStaticField = "remapped"; + public static String remappedInheritedStaticField = "inherited"; + + public int hiddenInstanceMethod () { return 10; } + public int remappedInstanceMethod () { return 11; } + public int remappedInheritedInstanceMethod () { return 12; } + public static int hiddenStaticMethod () { return 20; } + public static int remappedStaticMethod () { return 21; } + public static int remappedInheritedStaticMethod () { return 22; } + + public static int specificityExact (int value) { return value + 100; } + public static int specificityValue; + public static void specificityParameters (int value) { specificityValue = value + 200; } + public static int specificityWildcard (long value) { return (int)value + 300; } +} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java new file mode 100644 index 00000000000..4116fb21f2b --- /dev/null +++ b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapDerived.java @@ -0,0 +1,9 @@ +package net.dot.jni.test; + +public class FieldRemapDerived extends FieldRemapBase +{ + public boolean hiddenInstanceField; + public static String hiddenStaticField = "derived"; + public int hiddenInstanceMethod () { return 12; } + public static int hiddenStaticMethod () { return 22; } +} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapRenamedBase.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapRenamedBase.java new file mode 100644 index 00000000000..d1a06f46ca5 --- /dev/null +++ b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/FieldRemapRenamedBase.java @@ -0,0 +1,23 @@ +package net.dot.jni.test; + +public class FieldRemapRenamedBase +{ + public boolean hiddenInstanceField; + public boolean remappedInstanceField; + public boolean remappedInheritedInstanceField; + public static String hiddenStaticField = "base"; + public static String remappedStaticField = "remapped"; + public static String remappedInheritedStaticField = "inherited"; + + public int hiddenInstanceMethod () { return 10; } + public int remappedInstanceMethod () { return 11; } + public int remappedInheritedInstanceMethod () { return 12; } + public static int hiddenStaticMethod () { return 20; } + public static int remappedStaticMethod () { return 21; } + public static int remappedInheritedStaticMethod () { return 22; } + + public static int specificityExact (int value) { return value + 100; } + public static int specificityValue; + public static void specificityParameters (int value) { specificityValue = value + 200; } + public static int specificityWildcard (long value) { return (int)value + 300; } +} From f8e92ce78b778e0d7b3dd7a8bf5ba1273546ecf7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 17 Sep 2026 22:05:46 +0200 Subject: [PATCH 15/19] [runtime] Extend generated JNI remapping data Emit reverse type, field, and target descriptor tables and consume them in Microsoft.Android.Runtime and the trimmable type map. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../JniRemappingLookup.cs | 156 ++++++- .../TrimmableTypeMap.cs | 8 +- .../TrimmableTypeMapTypeManager.cs | 36 +- .../TrimmableTypeMapValueManager.cs | 3 +- .../Tasks/GenerateJniRemappingNativeCode.cs | 66 ++- .../GenerateJniRemappingNativeCodeTests.cs | 258 +++++++++++ .../JniRemappingAssemblyGenerator.cs | 429 +++++++++++++++--- .../common/include/managed-interface.hh | 4 + 8 files changed, 882 insertions(+), 78 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs index 32ee274b4b3..de6acee45d0 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs @@ -24,9 +24,17 @@ unsafe struct NativeJniRemappingReplacementMethod { public byte* target_type; public byte* target_name; + public byte* target_signature; public byte is_static; } + unsafe struct NativeJniRemappingReplacementField + { + public byte* target_type; + public byte* target_name; + public byte* target_signature; + } + unsafe struct NativeJniRemappingIndexMethodEntry { public NativeJniRemappingString name; @@ -47,12 +55,30 @@ unsafe struct NativeJniRemappingTypeReplacementEntry public byte* replacement; } + unsafe struct NativeJniRemappingIndexFieldEntry + { + public NativeJniRemappingString name; + public NativeJniRemappingString signature; + public NativeJniRemappingReplacementField replacement; + } + + unsafe struct NativeJniRemappingIndexFieldTypeEntry + { + public NativeJniRemappingString name; + public uint field_count; + public NativeJniRemappingIndexFieldEntry* fields; + } + unsafe struct NativeJniRemappingData { public NativeJniRemappingTypeReplacementEntry* type_replacements; + public NativeJniRemappingTypeReplacementEntry* reverse_type_replacements; public NativeJniRemappingIndexTypeEntry* method_replacement_index; + public NativeJniRemappingIndexFieldTypeEntry* field_replacement_index; public uint type_replacement_count; + public uint reverse_type_replacement_count; public uint method_replacement_index_count; + public uint field_replacement_index_count; } static unsafe NativeJniRemappingData* nativeData; @@ -66,11 +92,19 @@ internal static unsafe void Initialize (IntPtr data) } nativeData = (NativeJniRemappingData*)data; - isInUse = nativeData->type_replacement_count > 0 || nativeData->method_replacement_index_count > 0; + isInUse = + nativeData->type_replacement_count > 0 || + nativeData->reverse_type_replacement_count > 0 || + nativeData->method_replacement_index_count > 0 || + nativeData->field_replacement_index_count > 0; } internal static IReadOnlyList GetStaticMethodFallbackTypes (string jniSimpleReference, bool useReplacementTypes) { + if (useReplacementTypes) { + jniSimpleReference = GetReverseType (jniSimpleReference) ?? jniSimpleReference; + } + int slash = jniSimpleReference.LastIndexOf ('/'); var desugarType = slash > 0 ? $"{jniSimpleReference.Substring (0, slash + 1)}Desugar{jniSimpleReference.Substring (slash + 1)}" @@ -109,6 +143,22 @@ internal static unsafe IntPtr GetReplacementTypeUtf8 (string? jniSimpleReference return (IntPtr)LookupType (data->type_replacements, data->type_replacement_count, jniSimpleReference); } + internal static unsafe string? GetReverseType (string? jniSimpleReference) + { + if (jniSimpleReference is null || !isInUse || jniSimpleReference.Length == 0) + return null; + + NativeJniRemappingData* data = nativeData; + if (data == null) + throw new InvalidOperationException ("JNI remapping data has not been initialized."); + + byte* replacement = LookupType ( + data->reverse_type_replacements, + data->reverse_type_replacement_count, + jniSimpleReference); + return replacement == null ? null : Marshal.PtrToStringUTF8 ((IntPtr)replacement); + } + internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (string jniSourceType, string jniMethodName, string jniMethodSignature) => GetReplacementMethodInfo (jniSourceType, jniMethodName.AsSpan (), jniMethodSignature.AsSpan ()); @@ -155,14 +205,18 @@ internal static unsafe IntPtr GetReplacementTypeUtf8 (string? jniSimpleReference string sourceType = GetSourceTypeForDiagnostics (jniSourceType, jniSourceTypeUtf8); string sourceSignature = jniMethodSignature.ToString (); paramCount = JniMemberSignature.GetParameterCountFromMethodSignature (sourceSignature) + 1; - targetSignature = $"(L{sourceType};" + sourceSignature.Substring ("(".Length); + targetSignature = method->target_signature == null + ? $"(L{sourceType};" + sourceSignature.Substring ("(".Length) + : Marshal.PtrToStringUTF8 ((IntPtr)method->target_signature); } var ret = new JniRuntime.ReplacementMethodInfo { TargetJniTypeUtf8 = (IntPtr)method->target_type, TargetJniMethodNameUtf8 = (IntPtr)method->target_name, TargetJniMethodSignature = targetSignature, - TargetJniMethodSignatureUtf8 = isStatic ? IntPtr.Zero : (IntPtr)matchedSignature, + TargetJniMethodSignatureUtf8 = isStatic + ? IntPtr.Zero + : (IntPtr)(method->target_signature == null ? matchedSignature : method->target_signature), TargetJniMethodParameterCount = paramCount, TargetJniMethodInstanceToStatic = isStatic, }; @@ -182,6 +236,55 @@ internal static unsafe IntPtr GetReplacementTypeUtf8 (string? jniSimpleReference return ret; } + internal static JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo (string jniSourceType, string jniFieldName, string jniFieldSignature) + => GetReplacementFieldInfo (jniSourceType, jniFieldName.AsSpan (), jniFieldSignature.AsSpan ()); + + internal static unsafe JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfo ( + string jniSourceType, + ReadOnlySpan jniFieldName, + ReadOnlySpan jniFieldSignature) + { + if (!isInUse) + return null; + + NativeJniRemappingData* data = nativeData; + if (data == null) + throw new InvalidOperationException ("JNI remapping data has not been initialized."); + + NativeJniRemappingReplacementField* field = LookupField ( + data, + jniSourceType, + jniFieldName, + jniFieldSignature); + if (field == null) + return null; + if (field->target_type == null || field->target_name == null) { + throw new InvalidOperationException ( + $"JNI remapping entry for `{jniSourceType}.{jniFieldName}:{jniFieldSignature}` is missing target information."); + } + + string targetType = Marshal.PtrToStringUTF8 ((IntPtr)field->target_type) ?? ""; + string targetName = Marshal.PtrToStringUTF8 ((IntPtr)field->target_name) ?? ""; + string targetSignature = field->target_signature == null + ? jniFieldSignature.ToString () + : Marshal.PtrToStringUTF8 ((IntPtr)field->target_signature) ?? ""; + + if (Logger.LogAssembly) { + var message = $"Remapping field `{jniSourceType}.{jniFieldName}:{jniFieldSignature}` to " + + $"`{targetType}.{targetName}:{targetSignature}`"; + Logger.Log (LogLevel.Debug, "monodroid-assembly", message); + } + + return new JniRuntime.ReplacementFieldInfo { + SourceJniType = jniSourceType, + SourceJniFieldName = jniFieldName.ToString (), + SourceJniFieldSignature = jniFieldSignature.ToString (), + TargetJniType = targetType, + TargetJniFieldName = targetName, + TargetJniFieldSignature = targetSignature, + }; + } + static string GetSourceTypeForDiagnostics (ReadOnlySpan jniSourceType, IntPtr jniSourceTypeUtf8) { if (jniSourceTypeUtf8 == IntPtr.Zero) @@ -429,6 +532,53 @@ static unsafe int LowerBoundByName (void* entries, uint count, int entrySize, Re return null; } + static unsafe NativeJniRemappingReplacementField* LookupField ( + NativeJniRemappingData* data, + ReadOnlySpan sourceType, + ReadOnlySpan name, + ReadOnlySpan signature) + { + bool sourceTypeIsAscii = Ascii.IsValid (sourceType); + int typeIndex = LowerBoundByName ( + data->field_replacement_index, + data->field_replacement_index_count, + sizeof (NativeJniRemappingIndexFieldTypeEntry), + sourceType, + sourceTypeIsAscii); + if (typeIndex >= checked ((int)data->field_replacement_index_count) || + !Equal (data->field_replacement_index [typeIndex].name, sourceType, sourceTypeIsAscii)) + return null; + + NativeJniRemappingIndexFieldTypeEntry* type = &data->field_replacement_index [typeIndex]; + bool nameIsAscii = Ascii.IsValid (name); + int first = LowerBoundByName ( + type->fields, + type->field_count, + sizeof (NativeJniRemappingIndexFieldEntry), + name, + nameIsAscii); + int count = checked ((int)type->field_count); + if (first >= count || !Equal (type->fields [first].name, name, nameIsAscii)) + return null; + + int last = first + 1; + while (last < count && Equal (type->fields [last].name, name, nameIsAscii)) + last++; + + bool signatureIsAscii = Ascii.IsValid (signature); + for (int i = first; i < last; i++) { + NativeJniRemappingIndexFieldEntry* entry = &type->fields [i]; + if (entry->signature.length != 0 && Equal (entry->signature, signature, signatureIsAscii)) + return &entry->replacement; + } + for (int i = first; i < last; i++) { + NativeJniRemappingIndexFieldEntry* entry = &type->fields [i]; + if (entry->signature.length == 0) + return &entry->replacement; + } + return null; + } + static unsafe ReadOnlySpan GetNullTerminatedUtf8Span (IntPtr value) { byte* start = (byte*)value; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index ca6be17f1c5..2ce9320463d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -173,6 +173,7 @@ internal static JavaPeerProxy[] GetProxyArrayCacheEntry (object cacheEntry) /// JavaPeerProxy? GetProxyForJniClass (string className, Type? targetType) { + className = JniRemappingLookup.GetReverseType (className) ?? className; var cacheEntry = GetProxyCacheEntryForJniName (className); if (cacheEntry is JavaPeerProxy singleProxy) { return targetType is null || TargetTypeMatches (targetType, singleProxy.TargetType) @@ -267,7 +268,8 @@ bool TryResolveProxyFromSealedTargetType ( var targetClass = default (JniObjectReference); try { - targetClass = JniEnvironment.Types.FindClass (targetProxy.JniName); + string runtimeJniName = JniRemappingLookup.GetReplacementType (targetProxy.JniName) ?? targetProxy.JniName; + targetClass = JniEnvironment.Types.FindClass (runtimeJniName); var reference = new JniObjectReference (handle); if (JniEnvironment.Types.IsInstanceOf (reference, targetClass)) { proxy = targetProxy; @@ -403,7 +405,8 @@ static JniMethodInfo GetClassGetInterfacesMethod () try { objClass = JniEnvironment.Types.GetObjectClass (selfRef); try { - targetClass = JniEnvironment.Types.FindClass (targetJniName); + string runtimeJniName = JniRemappingLookup.GetReplacementType (targetJniName) ?? targetJniName; + targetClass = JniEnvironment.Types.FindClass (runtimeJniName); } catch (Java.Lang.ClassNotFoundException) { // FindClass throws for managed types whose Java peer class is // not present in the APK (e.g. test types annotated with @@ -555,6 +558,7 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa return; } + className = JniRemappingLookup.GetReverseType (className) ?? className; var cacheEntry = s_instance.GetProxyCacheEntryForJniName (className); if (cacheEntry is JavaPeerProxy[] proxies && proxies.Length == 0) { return; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index a64c08f9f26..6b4ff814d22 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -196,6 +196,15 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl yield return builtInType; } + // The type map is keyed by the JNI names the managed code declares, so a name that was + // renamed in the packaged application has to be translated back first. + if (GetOriginalSimpleReference (jniSimpleReference) is string originalReference) { + foreach (var type in TrimmableTypeMap.Instance.GetTargetTypes (originalReference)) { + yield return type; + } + yield break; + } + foreach (var type in TrimmableTypeMap.Instance.GetTargetTypes (jniSimpleReference)) { yield return type; } @@ -210,11 +219,21 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl return builtInType; } - if (TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var type)) { - return type; + if (GetOriginalSimpleReference (jniSimpleReference) is string originalReference) { + return TrimmableTypeMap.Instance.TryGetTargetType (originalReference, out var type) ? type : null; + } + + return TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var directType) ? directType : null; + } + + static string? GetOriginalSimpleReference (string jniSimpleReference) + { + var original = JniRemappingLookup.GetReverseType (jniSimpleReference); + if (original is null || string.Equals (original, jniSimpleReference, StringComparison.Ordinal)) { + return null; } - return null; + return original; } // Lookup of the built-in managed type for a JNI simple reference, e.g., string, bool?, int?, etc. @@ -271,7 +290,8 @@ static JniTypeSignature GetTypeSignatureUncached (Type type) while (currentType is not null) { if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (currentType, out var jniName)) { - return new (jniName, rank, keyword: false); + string runtimeJniName = JniRemappingLookup.GetReplacementType (jniName) ?? jniName; + return new (runtimeJniName, rank, keyword: false); } currentType = currentType.BaseType; @@ -370,7 +390,7 @@ protected override IEnumerable GetTypeSignaturesCore (Type typ return signature.IsValid ? [signature] : []; } - // Remapping APIs for InTune support + // Remapping APIs, used by the Intune/MAM mapping and generated JNI runtime remapping protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) => JniRemappingLookup.GetStaticMethodFallbackTypes (jniSimpleReference, useReplacementTypes: true); @@ -393,6 +413,12 @@ protected override void GetReplacementTypeInfoCore (string jniSimpleReference, o protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (IntPtr jniSourceTypeUtf8, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) => JniRemappingLookup.GetReplacementMethodInfo (jniSourceTypeUtf8, jniMethodName, jniMethodSignature); + protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, string jniFieldName, string jniFieldSignature) + => JniRemappingLookup.GetReplacementFieldInfo (jniSourceType, jniFieldName, jniFieldSignature); + + protected override JniRuntime.ReplacementFieldInfo? GetReplacementFieldInfoCore (string jniSourceType, ReadOnlySpan jniFieldName, ReadOnlySpan jniFieldSignature) + => JniRemappingLookup.GetReplacementFieldInfo (jniSourceType, jniFieldName, jniFieldSignature); + // The rest of the APIs are unsupported - they are not needed internally anywhere anyway protected override Type? GetInvokerTypeCore (Type type) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs index b2344f8fd2a..483ee7b3d22 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs @@ -192,7 +192,8 @@ static bool IsIncompatibleCast ( var instanceClass = JniEnvironment.Types.GetObjectClass (reference); JniObjectReference targetClass = default; try { - targetClass = JniEnvironment.Types.FindClass (targetJniName); + string runtimeJniName = JniRemappingLookup.GetReplacementType (targetJniName) ?? targetJniName; + targetClass = JniEnvironment.Types.FindClass (runtimeJniName); if (!JniEnvironment.Types.IsAssignableFrom (instanceClass, targetClass)) { // Match the legacy cast diagnostic when assembly logging is enabled. diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs index 16cf42c3533..3ca615ccdb4 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs @@ -19,11 +19,16 @@ internal sealed class JniRemappingNativeCodeInfo { public int ReplacementTypeCount { get; } public int ReplacementMethodIndexEntryCount { get; } + public int ReverseTypeCount { get; } + public int ReplacementFieldIndexEntryCount { get; } - public JniRemappingNativeCodeInfo (int replacementTypeCount, int replacementMethodIndexEntryCount) + public JniRemappingNativeCodeInfo (int replacementTypeCount, int replacementMethodIndexEntryCount, + int reverseTypeCount = 0, int replacementFieldIndexEntryCount = 0) { ReplacementTypeCount = replacementTypeCount; ReplacementMethodIndexEntryCount = replacementMethodIndexEntryCount; + ReverseTypeCount = reverseTypeCount; + ReplacementFieldIndexEntryCount = replacementFieldIndexEntryCount; } } @@ -39,6 +44,9 @@ public JniRemappingNativeCodeInfo (int replacementTypeCount, int replacementMeth public bool GenerateEmptyCode { get; set; } + /// Table sizes produced by the last run, exposed for focused validation. + internal JniRemappingNativeCodeInfo? NativeCodeInfo { get; private set; } + public override bool RunTask () { if (!GenerateEmptyCode) { @@ -56,13 +64,15 @@ public override bool RunTask () void GenerateEmpty () { - Generate (new JniRemappingAssemblyGenerator (Log), typeReplacementsCount: 0); + Generate (new JniRemappingAssemblyGenerator (Log)); } void Generate (string remappingXmlFilePath) { var typeReplacements = new List (); + var reverseTypeReplacements = new List (); var methodReplacements = new List (); + var fieldReplacements = new List (); var readerSettings = new XmlReaderSettings { XmlResolver = null, @@ -72,14 +82,14 @@ void Generate (string remappingXmlFilePath) if (reader.MoveToContent () != XmlNodeType.Element || reader.LocalName != "replacements") { Log.LogCodedError ("XA1045", Properties.Resources.XA1045, remappingXmlFilePath); } else { - ReadXml (reader, typeReplacements, methodReplacements, remappingXmlFilePath); + ReadXml (reader, typeReplacements, reverseTypeReplacements, methodReplacements, fieldReplacements, remappingXmlFilePath); } } - Generate (new JniRemappingAssemblyGenerator (Log, typeReplacements, methodReplacements), typeReplacements.Count); + Generate (new JniRemappingAssemblyGenerator (Log, typeReplacements, reverseTypeReplacements, methodReplacements, fieldReplacements)); } - void Generate (JniRemappingAssemblyGenerator jniRemappingComposer, int typeReplacementsCount) + void Generate (JniRemappingAssemblyGenerator jniRemappingComposer) { LLVMIR.LlvmIrModule module = jniRemappingComposer.Construct (); @@ -94,14 +104,25 @@ void Generate (JniRemappingAssemblyGenerator jniRemappingComposer, int typeRepla } } + NativeCodeInfo = new JniRemappingNativeCodeInfo ( + jniRemappingComposer.ReplacementTypeCount, + jniRemappingComposer.ReplacementMethodIndexEntryCount, + jniRemappingComposer.ReverseTypeCount, + jniRemappingComposer.ReplacementFieldIndexEntryCount + ); + BuildEngine4.RegisterTaskObjectAssemblyLocal ( ProjectSpecificTaskObjectKey (JniRemappingNativeCodeInfoKey), - new JniRemappingNativeCodeInfo (typeReplacementsCount, jniRemappingComposer.ReplacementMethodIndexEntryCount), + NativeCodeInfo, RegisteredTaskObjectLifetime.Build ); } - void ReadXml (XmlReader reader, List typeReplacements, List methodReplacements, string remappingXmlFilePath) + void ReadXml (XmlReader reader, List typeReplacements, + List reverseTypeReplacements, + List methodReplacements, + List fieldReplacements, + string remappingXmlFilePath) { bool haveAllAttributes; @@ -119,6 +140,14 @@ void ReadXml (XmlReader reader, List typeReplacemen } typeReplacements.Add (new JniRemappingTypeReplacement (from, to)); + } else if (MonoAndroidHelper.StringEquals ("reverse-type", reader.LocalName)) { + haveAllAttributes &= GetRequiredAttribute ("from", out string from); + haveAllAttributes &= GetRequiredAttribute ("to", out string to); + if (!haveAllAttributes) { + continue; + } + + reverseTypeReplacements.Add (new JniRemappingTypeReplacement (from, to)); } else if (MonoAndroidHelper.StringEquals ("replace-method", reader.LocalName)) { haveAllAttributes &= GetRequiredAttribute ("source-type", out string sourceType); haveAllAttributes &= GetRequiredAttribute ("source-method-name", out string sourceMethodName); @@ -136,10 +165,31 @@ void ReadXml (XmlReader reader, List typeReplacemen } string sourceMethodSignature = reader.GetAttribute ("source-method-signature"); + // Optional: inputs which predate it (for example the Intune/MAM mapping) keep + // the source signature on the target method. + string targetMethodSignature = reader.GetAttribute ("target-method-signature"); methodReplacements.Add ( new JniRemappingMethodReplacement ( sourceType, sourceMethodName, sourceMethodSignature, - targetType, targetMethodName, isStatic + targetType, targetMethodName, targetMethodSignature, isStatic + ) + ); + } else if (MonoAndroidHelper.StringEquals ("replace-field", reader.LocalName)) { + haveAllAttributes &= GetRequiredAttribute ("source-type", out string sourceType); + haveAllAttributes &= GetRequiredAttribute ("source-field-name", out string sourceFieldName); + haveAllAttributes &= GetRequiredAttribute ("target-type", out string targetType); + haveAllAttributes &= GetRequiredAttribute ("target-field-name", out string targetFieldName); + + if (!haveAllAttributes) { + continue; + } + + string sourceFieldSignature = reader.GetAttribute ("source-field-signature"); + string targetFieldSignature = reader.GetAttribute ("target-field-signature"); + fieldReplacements.Add ( + new JniRemappingFieldReplacement ( + sourceType, sourceFieldName, sourceFieldSignature, + targetType, targetFieldName, targetFieldSignature ) ); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs new file mode 100644 index 00000000000..4102257d129 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs @@ -0,0 +1,258 @@ +#nullable enable + +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Microsoft.Build.Framework; +using NUnit.Framework; +using Xamarin.Android.Tasks; +using Xamarin.ProjectTools; + +namespace Xamarin.Android.Build.Tests.Tasks { + + [TestFixture] + public class GenerateJniRemappingNativeCodeTests : BaseTest { + + List? errors; + List? warnings; + MockBuildEngine? engine; + string? directory; + + const string Abi = "arm64-v8a"; + + [SetUp] + public void Setup () + { + errors = new List (); + warnings = new List (); + engine = new MockBuildEngine (TestContext.Out, errors, warnings); + directory = Path.Combine (Root, "temp", TestName); + if (Directory.Exists (directory)) { + Directory.Delete (directory, recursive: true); + } + Directory.CreateDirectory (directory); + } + + string TestDirectory { + get { + return directory ?? throw new AssertionException ("The test directory must be initialized."); + } + } + + List Errors { + get { + return errors ?? throw new AssertionException ("The build error collection must be initialized."); + } + } + + string RunTask (string remappingXml) + { + string xmlPath = Path.Combine (TestDirectory, "remap.xml"); + File.WriteAllText (xmlPath, remappingXml); + + var task = new GenerateJniRemappingNativeCode { + BuildEngine = engine, + OutputDirectory = TestDirectory, + SupportedAbis = [Abi], + RemappingXmlFilePath = new Microsoft.Build.Utilities.TaskItem (xmlPath), + }; + + Assert.IsTrue (task.Execute (), $"Task should have succeeded. Errors: {string.Join ("; ", Errors.Select (e => e.Message))}"); + LastNativeCodeInfo = task.NativeCodeInfo; + + return File.ReadAllText (Path.Combine (TestDirectory, $"jni_remap.{Abi}.ll")); + } + + GenerateJniRemappingNativeCode.JniRemappingNativeCodeInfo? LastNativeCodeInfo { get; set; } + + GenerateJniRemappingNativeCode.JniRemappingNativeCodeInfo Info { + get { + return LastNativeCodeInfo ?? throw new AssertionException ("The task must provide native code information."); + } + } + + [Test] + public void EmptyCodeEmitsAllTablesAndZeroCounts () + { + var task = new GenerateJniRemappingNativeCode { + BuildEngine = engine, + OutputDirectory = TestDirectory, + SupportedAbis = [Abi], + GenerateEmptyCode = true, + }; + + Assert.IsTrue (task.Execute (), "Task should have succeeded."); + + string ll = File.ReadAllText (Path.Combine (TestDirectory, $"jni_remap.{Abi}.ll")); + foreach (string symbol in new [] { + "jni_remapping_type_replacements", + "jni_remapping_reverse_type_replacements", + "jni_remapping_method_replacement_index", + "jni_remapping_field_replacement_index", + }) { + StringAssert.Contains ($"@{symbol}", ll, $"`{symbol}` must always be emitted."); + } + + StringAssert.Contains ("@jni_remapping_data = dso_local local_unnamed_addr constant %struct.JniRemappingData", ll); + StringAssert.IsMatch ( + @"i32 0,\s+i32 0,\s+i32 0,\s+i32 0", + ll, + "The generated remapping data counts must all be zero."); + + var info = task.NativeCodeInfo ?? throw new AssertionException ("The task must provide native code information."); + Assert.AreEqual (0, info.ReplacementTypeCount); + Assert.AreEqual (0, info.ReverseTypeCount); + Assert.AreEqual (0, info.ReplacementMethodIndexEntryCount); + Assert.AreEqual (0, info.ReplacementFieldIndexEntryCount); + } + + [Test] + public void MissingTargetMethodSignatureIsBackwardCompatible () + { + // The Intune/MAM mapping shape: no `target-method-signature`, wildcard source signature. + string ll = RunTask ( + """ + + + + + """); + + Assert.AreEqual (1, Info.ReplacementTypeCount); + Assert.AreEqual (0, Info.ReverseTypeCount, "No reverse entries in a legacy document."); + Assert.AreEqual (1, Info.ReplacementMethodIndexEntryCount); + Assert.AreEqual (0, Info.ReplacementFieldIndexEntryCount); + StringAssert.Contains ("com/microsoft/intune/MAMActivity", ll); + // The wildcard signature is emitted as a zero-length string, and the absent target + // signature as a null pointer. + StringAssert.Contains ("ptr null", ll, "An absent target-method-signature must be a null pointer."); + } + + [Test] + public void TypeTablesAreSortedForBinarySearch () + { + string ll = RunTask ( + """ + + + + + + + + """); + + AssertOrdered (ll, "aa/First", "mm/Middle", "zz/Last"); + Assert.AreEqual (3, Info.ReplacementTypeCount); + Assert.AreEqual (2, Info.ReverseTypeCount); + } + + [Test] + public void MethodsAndFieldsUseStableLookupOrder () + { + string ll = RunTask ( + """ + + + + + + + + + + """); + + // Exact descriptors precede parameter-only descriptors and wildcards so MonoVM's + // single scan cannot let a general remap shadow a specific one. + int methodsStart = ll.IndexOf ("@mm_0 =", System.StringComparison.Ordinal); + int methodsEnd = ll.IndexOf ("@jni_remapping_method_replacement_index", methodsStart, System.StringComparison.Ordinal); + Assert.Greater (methodsStart, -1); + Assert.Greater (methodsEnd, methodsStart); + string methodArray = ll.Substring (methodsStart, methodsEnd - methodsStart); + AssertOrdered ( + methodArray, + "ptr @.JniRemappingString.1_str", + "ptr @.JniRemappingString.2_str", + "ptr @.JniRemappingString.3_str", + "ptr null", + "ptr @.JniRemappingString.4_str"); + AssertOrdered (ll, "c\"af", "c\"zf"); + Assert.AreEqual (1, Info.ReplacementMethodIndexEntryCount); + Assert.AreEqual (1, Info.ReplacementFieldIndexEntryCount); + } + + [Test] + public void MemberArraySymbolsAreCollisionProofAndValidLlvm () + { + string ll = RunTask ( + """ + + + + + + """); + + StringAssert.Contains ("@mm_0", ll); + StringAssert.Contains ("@mm_1", ll); + StringAssert.Contains ("@mf_0", ll); + + string binUtils = Path.Combine (TestEnvironment.OSBinDirectory, "binutils", "bin"); + var compile = new CompileNativeAssembly { + BuildEngine = engine, + Sources = [new Microsoft.Build.Utilities.TaskItem (Path.Combine (TestDirectory, $"jni_remap.{Abi}.ll"))], + DebugBuild = false, + WorkingDirectory = TestDirectory, + AndroidBinUtilsDirectory = binUtils, + }; + Assert.IsTrue (compile.Execute (), $"Generated LLVM IR should compile. Errors: {string.Join ("; ", Errors.Select (e => e.Message))}"); + FileAssert.Exists (Path.Combine (TestDirectory, $"jni_remap.{Abi}.o")); + } + + [Test] + public void Utf8OrderingMatchesNativeMemcmp () + { + // '_' (0x5F) sorts after 'Z' (0x5A) but before 'a' (0x61); a culture-sensitive + // comparison would order these differently, and the native binary search would break. + Assert.Less (JniRemappingAssemblyGenerator.CompareUtf8 (Utf8 ("Z"), Utf8 ("_")), 0); + Assert.Less (JniRemappingAssemblyGenerator.CompareUtf8 (Utf8 ("_"), Utf8 ("a")), 0); + Assert.Less (JniRemappingAssemblyGenerator.CompareUtf8 (Utf8 ("a"), Utf8 ("ab")), 0); + Assert.AreEqual (0, JniRemappingAssemblyGenerator.CompareUtf8 (Utf8 ("a/B"), Utf8 ("a/B"))); + + static byte [] Utf8 (string s) => System.Text.Encoding.UTF8.GetBytes (s); + } + + static void AssertOrdered (string haystack, params string [] needles) + { + int previous = -1; + string previousNeedle = ""; + foreach (string needle in needles) { + int index = haystack.IndexOf (needle, previous + 1, System.StringComparison.Ordinal); + Assert.Greater (index, previous, $"`{needle}` must appear after `{previousNeedle}`."); + previous = index; + previousNeedle = needle; + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index 094a5664ca7..67d6483bfb9 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -31,10 +31,18 @@ sealed class JniRemappingMethodReplacement public string TargetType { get; } public string TargetMethod { get; } + /// + /// The JNI method descriptor to use on the target type, or null when the source + /// signature is used unchanged. Remapping inputs which predate this attribute (for example + /// the Intune/MAM mapping) leave it unset. + /// + public string TargetMethodSignature { get; } + public bool TargetIsStatic { get; } public JniRemappingMethodReplacement (string sourceType, string sourceMethod, string sourceMethodSignature, - string targetType, string targetMethod, bool targetIsStatic) + string targetType, string targetMethod, string targetMethodSignature, + bool targetIsStatic) { SourceType = sourceType; SourceMethod = sourceMethod; @@ -42,14 +50,40 @@ public JniRemappingMethodReplacement (string sourceType, string sourceMethod, st TargetType = targetType; TargetMethod = targetMethod; + TargetMethodSignature = targetMethodSignature; TargetIsStatic = targetIsStatic; } } + sealed class JniRemappingFieldReplacement + { + public string SourceType { get; } + public string SourceField { get; } + public string SourceFieldSignature { get; } + + public string TargetType { get; } + public string TargetField { get; } + public string TargetFieldSignature { get; } + + public JniRemappingFieldReplacement (string sourceType, string sourceField, string sourceFieldSignature, + string targetType, string targetField, string targetFieldSignature) + { + SourceType = sourceType; + SourceField = sourceField; + SourceFieldSignature = sourceFieldSignature; + + TargetType = targetType; + TargetField = targetField; + TargetFieldSignature = targetFieldSignature; + } + } + class JniRemappingAssemblyGenerator : LlvmIrComposer { const string TypeReplacementsVariableName = "jni_remapping_type_replacements"; + const string ReverseTypeReplacementsVariableName = "jni_remapping_reverse_type_replacements"; const string MethodReplacementIndexVariableName = "jni_remapping_method_replacement_index"; + const string FieldReplacementIndexVariableName = "jni_remapping_field_replacement_index"; const string RemappingDataVariableName = "jni_remapping_data"; sealed class JniRemappingDataContextDataProvider : NativeAssemblerStructContextDataProvider @@ -58,7 +92,9 @@ public override string GetPointedToSymbolName (object data, string fieldName) { return fieldName switch { nameof (JniRemappingData.type_replacements) => TypeReplacementsVariableName, + nameof (JniRemappingData.reverse_type_replacements) => ReverseTypeReplacementsVariableName, nameof (JniRemappingData.method_replacement_index) => MethodReplacementIndexVariableName, + nameof (JniRemappingData.field_replacement_index) => FieldReplacementIndexVariableName, _ => base.GetPointedToSymbolName (data, fieldName), }; } @@ -143,6 +179,67 @@ public override string GetComment (object data, string fieldName) } } + sealed class JniRemappingIndexFieldTypeEntryContextDataProvider : NativeAssemblerStructContextDataProvider + { + public override string GetComment (object data, string fieldName) + { + var entry = EnsureType (data); + + if (MonoAndroidHelper.StringEquals ("name", fieldName)) { + return $" name: {entry.name.str}"; + } + + return String.Empty; + } + + public override string GetPointedToSymbolName (object data, string fieldName) + { + var entry = EnsureType (data); + + if (MonoAndroidHelper.StringEquals ("fields", fieldName)) { + return entry.FieldsArraySymbolName; + } + + return base.GetPointedToSymbolName (data, fieldName); + } + + public override ulong GetBufferSize (object data, string fieldName) + { + var entry = EnsureType (data); + if (MonoAndroidHelper.StringEquals ("fields", fieldName)) { + return (ulong)entry.TypeFields.Count; + } + + return 0; + } + } + + sealed class JniRemappingIndexFieldEntryContextDataProvider : NativeAssemblerStructContextDataProvider + { + public override string GetComment (object data, string fieldName) + { + var entry = EnsureType (data); + + if (MonoAndroidHelper.StringEquals ("name", fieldName)) { + return $" name: {entry.name.str}"; + } + + if (MonoAndroidHelper.StringEquals ("replacement", fieldName)) { + return $" replacement: {entry.replacement.target_type}.{entry.replacement.target_name}"; + } + + if (MonoAndroidHelper.StringEquals ("signature", fieldName)) { + if (entry.signature.length == 0) { + return String.Empty; + } + + return $"signature: {entry.signature.str}"; + } + + return String.Empty; + } + } + sealed class JniRemappingString { public uint length; @@ -153,9 +250,17 @@ sealed class JniRemappingReplacementMethod { public string target_type; public string target_name; + public string target_signature; public bool is_static; }; + sealed class JniRemappingReplacementField + { + public string target_type; + public string target_name; + public string target_signature; + }; + [NativeAssemblerStructContextDataProvider (typeof(JniRemappingIndexMethodEntryContextDataProvider))] sealed class JniRemappingIndexMethodEntry { @@ -188,6 +293,38 @@ sealed class JniRemappingIndexTypeEntry public List> TypeMethods; }; + [NativeAssemblerStructContextDataProvider (typeof(JniRemappingIndexFieldEntryContextDataProvider))] + sealed class JniRemappingIndexFieldEntry + { + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingString name; + + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingString signature; + + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingReplacementField replacement; + }; + + [NativeAssemblerStructContextDataProvider (typeof(JniRemappingIndexFieldTypeEntryContextDataProvider))] + sealed class JniRemappingIndexFieldTypeEntry + { + [NativeAssembler (UsesDataProvider = true)] + public JniRemappingString name; + public uint field_count; + + [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value - populated during native code generation + public JniRemappingIndexFieldEntry fields; +#pragma warning restore CS0649 + + [NativeAssembler (Ignore = true)] + public string FieldsArraySymbolName; + + [NativeAssembler (Ignore = true)] + public List> TypeFields; + }; + [NativeAssemblerStructContextDataProvider (typeof(JniRemappingTypeReplacementEntryContextDataProvider))] sealed class JniRemappingTypeReplacementEntry { @@ -205,27 +342,50 @@ sealed class JniRemappingData #pragma warning disable CS0649 // Field is populated during native code generation public JniRemappingTypeReplacementEntry type_replacements; + [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] + public JniRemappingTypeReplacementEntry reverse_type_replacements; + [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] public JniRemappingIndexTypeEntry method_replacement_index; + + [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] + public JniRemappingIndexFieldTypeEntry field_replacement_index; #pragma warning restore CS0649 public uint type_replacement_count; + public uint reverse_type_replacement_count; public uint method_replacement_index_count; + public uint field_replacement_index_count; + } + + sealed class GeneratedTables + { + public List> TypeReplacements; + public List> ReverseTypeReplacements; + public List> MethodIndexTypes; + public List> FieldIndexTypes; } List typeReplacementsInput; + List reverseTypeReplacementsInput; List methodReplacementsInput; + List fieldReplacementsInput; StructureInfo jniRemappingStringStructureInfo; StructureInfo jniRemappingReplacementMethodStructureInfo; + StructureInfo jniRemappingReplacementFieldStructureInfo; StructureInfo jniRemappingIndexMethodEntryStructureInfo; StructureInfo jniRemappingIndexTypeEntryStructureInfo; + StructureInfo jniRemappingIndexFieldEntryStructureInfo; + StructureInfo jniRemappingIndexFieldTypeEntryStructureInfo; StructureInfo jniRemappingTypeReplacementEntryStructureInfo; StructureInfo jniRemappingDataStructureInfo; readonly Dictionary utf8SortKeys = new Dictionary (StringComparer.Ordinal); public int ReplacementTypeCount { get; private set; } = 0; + public int ReverseTypeCount { get; private set; } = 0; public int ReplacementMethodIndexEntryCount { get; private set; } = 0; + public int ReplacementFieldIndexEntryCount { get; private set; } = 0; int CompareUtf8 (string left, string right) { @@ -253,91 +413,216 @@ public JniRemappingAssemblyGenerator (TaskLoggingHelper log) : base (log) {} - public JniRemappingAssemblyGenerator (TaskLoggingHelper log, List typeReplacements, List methodReplacements) + public JniRemappingAssemblyGenerator (TaskLoggingHelper log, + List typeReplacements, + List reverseTypeReplacements, + List methodReplacements, + List fieldReplacements) : base (log) { this.typeReplacementsInput = typeReplacements ?? throw new ArgumentNullException (nameof (typeReplacements)); + this.reverseTypeReplacementsInput = reverseTypeReplacements ?? throw new ArgumentNullException (nameof (reverseTypeReplacements)); this.methodReplacementsInput = methodReplacements ?? throw new ArgumentNullException (nameof (methodReplacements)); + this.fieldReplacementsInput = fieldReplacements ?? throw new ArgumentNullException (nameof (fieldReplacements)); } - (List>? typeReplacements, List>? methodIndexTypes) Init () + /// + /// Orders UTF-8 encoded names exactly the way the native lookup's memcmp-based + /// comparison does, so the runtime can binary-search the emitted tables. + /// + internal static int CompareUtf8 (byte [] left, byte [] right) + { + int min = Math.Min (left.Length, right.Length); + for (int i = 0; i < min; i++) { + if (left [i] != right [i]) { + return left [i] < right [i] ? -1 : 1; + } + } + + if (left.Length == right.Length) { + return 0; + } + + return left.Length < right.Length ? -1 : 1; + } + + static byte [] Utf8 (string str) => String.IsNullOrEmpty (str) ? [] : Encoding.UTF8.GetBytes (str); + + GeneratedTables Init () { if (typeReplacementsInput == null) { - return (null, null); + return null; + } + + var ret = new GeneratedTables { + TypeReplacements = MakeTypeReplacements (typeReplacementsInput), + ReverseTypeReplacements = MakeTypeReplacements (reverseTypeReplacementsInput), + MethodIndexTypes = MakeMethodIndex (), + FieldIndexTypes = MakeFieldIndex (), + }; + + ReplacementTypeCount = ret.TypeReplacements.Count; + ReverseTypeCount = ret.ReverseTypeReplacements.Count; + ReplacementMethodIndexEntryCount = ret.MethodIndexTypes.Count; + ReplacementFieldIndexEntryCount = ret.FieldIndexTypes.Count; + + return ret; + } + + List> MakeTypeReplacements (List input) + { + var sorted = new List<(byte [] key, JniRemappingTypeReplacement replacement)> (input.Count); + foreach (JniRemappingTypeReplacement tr in input) { + sorted.Add ((Utf8 (tr.From), tr)); } + sorted.Sort ((l, r) => CompareUtf8 (l.key, r.key)); - var typeReplacements = new List> (); - foreach (JniRemappingTypeReplacement mtr in typeReplacementsInput) { + var ret = new List> (sorted.Count); + foreach ((byte [] key, JniRemappingTypeReplacement tr) in sorted) { var entry = new JniRemappingTypeReplacementEntry { - name = MakeJniRemappingString (mtr.From), - replacement = mtr.To, + name = MakeJniRemappingString (tr.From, key), + replacement = tr.To, }; - typeReplacements.Add (new StructureInstance (jniRemappingTypeReplacementEntryStructureInfo, entry)); + ret.Add (new StructureInstance (jniRemappingTypeReplacementEntryStructureInfo, entry)); } - 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); + return ret; + } + + List> MakeMethodIndex () + { + var types = new Dictionary methods)> (StringComparer.Ordinal); foreach (JniRemappingMethodReplacement mmr in methodReplacementsInput) { - if (!types.TryGetValue (mmr.SourceType, out StructureInstance typeEntry)) { - var entry = new JniRemappingIndexTypeEntry { - name = MakeJniRemappingString (mmr.SourceType), - MethodsArraySymbolName = MakeMethodsArrayName (mmr.SourceType), - TypeMethods = new List> (), + if (!types.TryGetValue (mmr.SourceType, out var typeEntry)) { + typeEntry = (Utf8 (mmr.SourceType), new List<(byte [], byte [], JniRemappingMethodReplacement)> ()); + types.Add (mmr.SourceType, typeEntry); + } + + typeEntry.methods.Add ((Utf8 (mmr.SourceMethod), Utf8 (mmr.SourceMethodSignature), mmr)); + } + + var sortedTypes = new List methods)>> (types); + sortedTypes.Sort ((l, r) => CompareUtf8 (l.Value.key, r.Value.key)); + + var ret = new List> (sortedTypes.Count); + for (int typeIndex = 0; typeIndex < sortedTypes.Count; typeIndex++) { + var kvp = sortedTypes [typeIndex]; + var methods = kvp.Value.methods; + // Keep exact descriptors before parameter-only descriptors and wildcards, matching + // the specificity passes used by both native runtimes. + methods.Sort ((l, r) => { + int cmp = CompareUtf8 (l.nameKey, r.nameKey); + if (cmp != 0) { + return cmp; + } + cmp = GetMethodSignatureSpecificity (l.method.SourceMethodSignature).CompareTo ( + GetMethodSignatureSpecificity (r.method.SourceMethodSignature)); + return cmp != 0 ? cmp : CompareUtf8 (l.signatureKey, r.signatureKey); + }); + + var typeMethods = new List> (methods.Count); + foreach ((byte [] nameKey, byte [] signatureKey, JniRemappingMethodReplacement mmr) in methods) { + var method = new JniRemappingIndexMethodEntry { + name = MakeJniRemappingString (mmr.SourceMethod, nameKey), + signature = MakeJniRemappingString (mmr.SourceMethodSignature, signatureKey), + replacement = new JniRemappingReplacementMethod { + target_type = mmr.TargetType, + target_name = mmr.TargetMethod, + target_signature = mmr.TargetMethodSignature, + is_static = mmr.TargetIsStatic, + }, }; - typeEntry = new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, entry); - methodIndexTypes.Add (typeEntry); - types.Add (mmr.SourceType, typeEntry); + typeMethods.Add (new StructureInstance (jniRemappingIndexMethodEntryStructureInfo, method)); } - var method = new JniRemappingIndexMethodEntry { - name = MakeJniRemappingString (mmr.SourceMethod), - signature = MakeJniRemappingString (mmr.SourceMethodSignature), - replacement = new JniRemappingReplacementMethod { - target_type = mmr.TargetType, - target_name = mmr.TargetMethod, - is_static = mmr.TargetIsStatic, - }, + var entry = new JniRemappingIndexTypeEntry { + name = MakeJniRemappingString (kvp.Key, kvp.Value.key), + method_count = (uint)typeMethods.Count, + MethodsArraySymbolName = MakeMembersArrayName ("mm", typeIndex), + TypeMethods = typeMethods, }; - - typeEntry.Instance.TypeMethods.Add (new StructureInstance (jniRemappingIndexMethodEntryStructureInfo, method)); + ret.Add (new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, entry)); + ret.Add (new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, entry)); } - foreach (var kvp in types) { - kvp.Value.Instance.method_count = (uint)kvp.Value.Instance.TypeMethods.Count; - kvp.Value.Instance.TypeMethods.Sort ((StructureInstance l, StructureInstance r) => CompareUtf8 (l.Instance.name.str, r.Instance.name.str)); - } + return ret; + } - methodIndexTypes.Sort ((StructureInstance l, StructureInstance r) => CompareUtf8 (l.Instance.name.str, r.Instance.name.str)); - ReplacementMethodIndexEntryCount = methodIndexTypes.Count; + List> MakeFieldIndex () + { + var types = new Dictionary fields)> (StringComparer.Ordinal); - return (typeReplacements, methodIndexTypes); + foreach (JniRemappingFieldReplacement mfr in fieldReplacementsInput) { + if (!types.TryGetValue (mfr.SourceType, out var typeEntry)) { + typeEntry = (Utf8 (mfr.SourceType), new List<(byte [], byte [], JniRemappingFieldReplacement)> ()); + types.Add (mfr.SourceType, typeEntry); + } - string MakeMethodsArrayName (string typeName) - { - return $"mm_{typeName.Replace ('/', '_')}"; + typeEntry.fields.Add ((Utf8 (mfr.SourceField), Utf8 (mfr.SourceFieldSignature), mfr)); } - JniRemappingString MakeJniRemappingString (string str) - { - return new JniRemappingString { - length = GetLength (str), - str = str, + var sortedTypes = new List fields)>> (types); + sortedTypes.Sort ((l, r) => CompareUtf8 (l.Value.key, r.Value.key)); + + var ret = new List> (sortedTypes.Count); + for (int typeIndex = 0; typeIndex < sortedTypes.Count; typeIndex++) { + var kvp = sortedTypes [typeIndex]; + var fields = kvp.Value.fields; + fields.Sort ((l, r) => { + int cmp = CompareUtf8 (l.nameKey, r.nameKey); + return cmp != 0 ? cmp : CompareUtf8 (l.signatureKey, r.signatureKey); + }); + + var typeFields = new List> (fields.Count); + foreach ((byte [] nameKey, byte [] signatureKey, JniRemappingFieldReplacement mfr) in fields) { + var field = new JniRemappingIndexFieldEntry { + name = MakeJniRemappingString (mfr.SourceField, nameKey), + signature = MakeJniRemappingString (mfr.SourceFieldSignature, signatureKey), + replacement = new JniRemappingReplacementField { + target_type = mfr.TargetType, + target_name = mfr.TargetField, + target_signature = mfr.TargetFieldSignature, + }, + }; + + typeFields.Add (new StructureInstance (jniRemappingIndexFieldEntryStructureInfo, field)); + } + + var entry = new JniRemappingIndexFieldTypeEntry { + name = MakeJniRemappingString (kvp.Key, kvp.Value.key), + field_count = (uint)typeFields.Count, + FieldsArraySymbolName = MakeMembersArrayName ("mf", typeIndex), + TypeFields = typeFields, }; + + ret.Add (new StructureInstance (jniRemappingIndexFieldTypeEntryStructureInfo, entry)); } - uint GetLength (string str) - { - if (String.IsNullOrEmpty (str)) { - return 0; - } + return ret; + } + + static string MakeMembersArrayName (string prefix, int typeIndex) + { + return $"{prefix}_{typeIndex}"; + } - return (uint)Encoding.UTF8.GetByteCount (str); + static int GetMethodSignatureSpecificity (string signature) + { + if (String.IsNullOrEmpty (signature)) { + return 2; } + return signature [signature.Length - 1] == ')' ? 1 : 0; + } + + static JniRemappingString MakeJniRemappingString (string str, byte [] utf8) + { + return new JniRemappingString { + length = (uint)utf8.Length, + str = str, + }; } protected override void Construct (LlvmIrModule module) @@ -345,12 +630,10 @@ protected override void Construct (LlvmIrModule module) module.DefaultStringGroup = "jremap"; MapStructures (module); - List>? typeReplacements; - List>? methodIndexTypes; - (typeReplacements, methodIndexTypes) = Init (); + GeneratedTables tables = Init (); - if (typeReplacements == null) { + if (tables == null) { module.AddGlobalVariable ( typeof(StructureInstance), TypeReplacementsVariableName, @@ -358,23 +641,46 @@ protected override void Construct (LlvmIrModule module) LlvmIrVariableOptions.GlobalConstant ); + module.AddGlobalVariable ( + typeof(StructureInstance), + ReverseTypeReplacementsVariableName, + new StructureInstance (jniRemappingTypeReplacementEntryStructureInfo, new JniRemappingTypeReplacementEntry ()) { IsZeroInitialized = true }, + LlvmIrVariableOptions.GlobalConstant + ); + module.AddGlobalVariable ( typeof(StructureInstance), MethodReplacementIndexVariableName, new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, new JniRemappingIndexTypeEntry ()) { IsZeroInitialized = true }, LlvmIrVariableOptions.GlobalConstant ); + + module.AddGlobalVariable ( + typeof(StructureInstance), + FieldReplacementIndexVariableName, + new StructureInstance (jniRemappingIndexFieldTypeEntryStructureInfo, new JniRemappingIndexFieldTypeEntry ()) { IsZeroInitialized = true }, + LlvmIrVariableOptions.GlobalConstant + ); + AddData (module); return; } - module.AddGlobalVariable (TypeReplacementsVariableName, typeReplacements, LlvmIrVariableOptions.GlobalConstant); + module.AddGlobalVariable (TypeReplacementsVariableName, tables.TypeReplacements, LlvmIrVariableOptions.GlobalConstant); + module.AddGlobalVariable (ReverseTypeReplacementsVariableName, tables.ReverseTypeReplacements, LlvmIrVariableOptions.GlobalConstant); - foreach (StructureInstance entry in methodIndexTypes) { + foreach (StructureInstance entry in tables.MethodIndexTypes) { module.AddGlobalVariable (entry.Instance.MethodsArraySymbolName, entry.Instance.TypeMethods, LlvmIrVariableOptions.LocalConstant); } - module.AddGlobalVariable (MethodReplacementIndexVariableName, methodIndexTypes, LlvmIrVariableOptions.GlobalConstant); + module.AddGlobalVariable (MethodReplacementIndexVariableName, tables.MethodIndexTypes, LlvmIrVariableOptions.GlobalConstant); + + foreach (StructureInstance entry in tables.FieldIndexTypes) { + module.AddGlobalVariable (entry.Instance.FieldsArraySymbolName, entry.Instance.TypeFields, LlvmIrVariableOptions.LocalConstant); + } + + module.AddGlobalVariable (FieldReplacementIndexVariableName, tables.FieldIndexTypes, LlvmIrVariableOptions.GlobalConstant); + AddData (module); } @@ -382,7 +688,9 @@ void AddData (LlvmIrModule module) { var data = new JniRemappingData { type_replacement_count = (uint)ReplacementTypeCount, + reverse_type_replacement_count = (uint)ReverseTypeCount, method_replacement_index_count = (uint)ReplacementMethodIndexEntryCount, + field_replacement_index_count = (uint)ReplacementFieldIndexEntryCount, }; module.AddGlobalVariable ( RemappingDataVariableName, @@ -395,8 +703,11 @@ void MapStructures (LlvmIrModule module) { jniRemappingStringStructureInfo = module.MapStructure (); jniRemappingReplacementMethodStructureInfo = module.MapStructure (); + jniRemappingReplacementFieldStructureInfo = module.MapStructure (); jniRemappingIndexMethodEntryStructureInfo = module.MapStructure (); jniRemappingIndexTypeEntryStructureInfo = module.MapStructure (); + jniRemappingIndexFieldEntryStructureInfo = module.MapStructure (); + jniRemappingIndexFieldTypeEntryStructureInfo = module.MapStructure (); jniRemappingTypeReplacementEntryStructureInfo = module.MapStructure (); jniRemappingDataStructureInfo = module.MapStructure (); } diff --git a/src/native/common/include/managed-interface.hh b/src/native/common/include/managed-interface.hh index f0ac8f20868..5bf794af12a 100644 --- a/src/native/common/include/managed-interface.hh +++ b/src/native/common/include/managed-interface.hh @@ -19,9 +19,13 @@ namespace xamarin::android { struct JniRemappingData { const void *type_replacements; + const void *reverse_type_replacements; const void *method_replacement_index; + const void *field_replacement_index; uint32_t type_replacement_count; + uint32_t reverse_type_replacement_count; uint32_t method_replacement_index_count; + uint32_t field_replacement_index_count; }; extern "C" { From f7b4401b50054d508984f327059b0919baf7bc18 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 17 Sep 2026 23:25:03 +0200 Subject: [PATCH 16/19] [runtime] Address JNI remapping review Keep legacy native stubs layout-compatible, retain redirected static member owners, and use the nullable string helpers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/Java.Interop/JniFieldInfo.cs | 3 +- .../Java.Interop/JniPeerMembers.JniFields.cs | 36 +++++++++---------- .../JniPeerMembers.JniStaticFields.cs | 31 ++++++++++++---- .../JniPeerMembers.JniStaticMethods.cs | 26 ++++++++++---- .../Java.Interop/JavaVMFixture.cs | 2 ++ .../Java.Interop/JniPeerMembersTests.cs | 27 ++++++++++++++ .../JniRemappingAssemblyGenerator.cs | 4 +-- src/native/clr/include/xamarin-app.hh | 3 +- .../xamarin-app-stub/application_dso_stub.cc | 6 ++++ .../xamarin-app-stub/application_dso_stub.cc | 6 ++++ .../mono/xamarin-app-stub/xamarin-app.hh | 3 +- 11 files changed, 108 insertions(+), 39 deletions(-) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniFieldInfo.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniFieldInfo.cs index a02ec4c8c4f..80de1a3e1aa 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniFieldInfo.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniFieldInfo.cs @@ -10,6 +10,8 @@ public sealed class JniFieldInfo public bool IsStatic {get; private set;} + internal JniType? StaticRedirect; + internal bool IsValid { get {return ID != IntPtr.Zero;} } @@ -70,4 +72,3 @@ public override string ToString () } } } - diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniFields.cs index ea9d0cb5155..e087e110f1d 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniFields.cs @@ -202,109 +202,109 @@ partial class JniStaticFields { public bool GetBooleanValue (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticBooleanField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticBooleanField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, bool value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticBooleanField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticBooleanField (GetFieldDeclaringType (f).PeerReference, f, value); } public sbyte GetSByteValue (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticByteField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticByteField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, sbyte value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticByteField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticByteField (GetFieldDeclaringType (f).PeerReference, f, value); } public char GetCharValue (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticCharField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticCharField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, char value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticCharField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticCharField (GetFieldDeclaringType (f).PeerReference, f, value); } public short GetInt16Value (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticShortField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticShortField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, short value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticShortField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticShortField (GetFieldDeclaringType (f).PeerReference, f, value); } public int GetInt32Value (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticIntField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticIntField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, int value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticIntField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticIntField (GetFieldDeclaringType (f).PeerReference, f, value); } public long GetInt64Value (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticLongField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticLongField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, long value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticLongField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticLongField (GetFieldDeclaringType (f).PeerReference, f, value); } public float GetSingleValue (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticFloatField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticFloatField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, float value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticFloatField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticFloatField (GetFieldDeclaringType (f).PeerReference, f, value); } public double GetDoubleValue (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticDoubleField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticDoubleField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, double value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticDoubleField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticDoubleField (GetFieldDeclaringType (f).PeerReference, f, value); } public JniObjectReference GetObjectValue (string encodedMember) { var f = GetFieldInfo (encodedMember); - return JniEnvironment.StaticFields.GetStaticObjectField (Members.JniPeerType.PeerReference, f); + return JniEnvironment.StaticFields.GetStaticObjectField (GetFieldDeclaringType (f).PeerReference, f); } public void SetValue (string encodedMember, JniObjectReference value) { var f = GetFieldInfo (encodedMember); - JniEnvironment.StaticFields.SetStaticObjectField (Members.JniPeerType.PeerReference, f, value); + JniEnvironment.StaticFields.SetStaticObjectField (GetFieldDeclaringType (f).PeerReference, f, value); } }} } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs index 80bf3b57e5e..01a393b2441 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs @@ -36,9 +36,15 @@ JniFieldInfo GetFieldInfo (ReadOnlySpan field, ReadOnlySpan signatur var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field; var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature; - using var t = new JniType (typeName); - if (t.TryGetStaticField (fieldName, fieldSig, out var f)) { - return f; + JniType? t = new JniType (typeName); + try { + if (t.TryGetStaticField (fieldName, fieldSig, out var f)) { + f.StaticRedirect = t; + t = null; + return f; + } + } finally { + t?.Dispose (); } } if (Members.JniPeerType.TryGetStaticField (field, signature, out var originalField)) { @@ -51,17 +57,28 @@ JniFieldInfo GetFieldInfo (ReadOnlySpan field, ReadOnlySpan signatur var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field; var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature; - using var t = new JniType (typeName); - if (t.TryGetStaticField (fieldName, fieldSig, out var f)) { - return f; + JniType? t = new JniType (typeName); + try { + if (t.TryGetStaticField (fieldName, fieldSig, out var f)) { + f.StaticRedirect = t; + t = null; + return f; + } + } finally { + t?.Dispose (); } } return Members.JniPeerType.GetStaticField (field, signature); } + JniType GetFieldDeclaringType (JniFieldInfo field) + { + return field.StaticRedirect ?? Members.JniPeerType; + } + internal void Dispose () { - Clear (ref staticFields); + Clear (ref staticFields, static field => field.StaticRedirect?.Dispose ()); } }} } 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 2ebf29c81c4..cd655bcad31 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 @@ -21,7 +21,7 @@ internal JniStaticMethods (JniPeerMembers members) internal void Dispose () { - Clear (ref staticMethods); + Clear (ref staticMethods, static method => method.StaticRedirect?.Dispose ()); } public JniMethodInfo GetMethodInfo (string encodedMember) @@ -39,9 +39,15 @@ JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signa var newMethod = Members.GetReplacementMethodInfo (method, signature); if (newMethod.HasValue) { var info = newMethod.Value; - using var t = CreateTargetType (info, Members); - if (TryGetStaticMethod (t, info, method, signature, out m)) { - return m; + JniType? t = CreateTargetType (info, Members); + try { + if (TryGetStaticMethod (t, info, method, signature, out m)) { + m.StaticRedirect = t; + t = null; + return m; + } + } finally { + t?.Dispose (); } } if (Members.JniPeerType.TryGetStaticMethod (method, signature, out m)) { @@ -50,9 +56,15 @@ JniMethodInfo GetMethodInfo (ReadOnlySpan method, ReadOnlySpan signa newMethod = JniPeerMembers.GetBaseReplacementMethodInfo (Members.ManagedPeerType, method, signature); if (newMethod.HasValue) { var info = newMethod.Value; - using var t = CreateTargetType (info, Members); - if (TryGetStaticMethod (t, info, method, signature, out m)) { - return m; + JniType? t = CreateTargetType (info, Members); + try { + if (TryGetStaticMethod (t, info, method, signature, out m)) { + m.StaticRedirect = t; + t = null; + return m; + } + } finally { + t?.Dispose (); } } m = FindInFallbackTypes (method, signature); 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 c161713b816..7cbfef3a1f3 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 @@ -177,6 +177,7 @@ protected override void GetReplacementTypeInfoCore (string jniSimpleReference, o [("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/Object", "remappedStaticAbs", "(I)I")] = ("java/lang/Math", "abs", null, null, false, ReplacementMethodStorage.Strings), [("java/lang/Runtime", "remappedToGetRuntime", null)] = (null, "getRuntime", null, null, false, ReplacementMethodStorage.Strings), // NOTE: key must use *post-renamed* value, not pre-renamed value @@ -200,6 +201,7 @@ protected override void GetReplacementTypeInfoCore (string jniSimpleReference, o Dictionary<(string SourceType, string SourceName, string? SourceSignature), (string? TargetType, string? TargetName, string? TargetSignature)> ReplacementFields = new() { [("java/lang/Math", "remappedToPi", "D")] = (null, "PI", null), + [("java/lang/Object", "remappedStaticPi", "D")] = ("java/lang/Math", "PI", null), [("java/io/ByteArrayInputStream", "remappedToPos", "I")] = (null, "pos", null), [(FieldRemapBase.RuntimeJniTypeName, "hiddenInstanceField", "Z")] = (null, "remappedInstanceField", null), [(FieldRemapBase.RuntimeJniTypeName, "hiddenStaticField", "Ljava/lang/String;")] = (null, "remappedStaticField", null), 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 33ccb9f8371..bb5c9f440ad 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 @@ -237,6 +237,22 @@ public void ReplaceStaticFieldName () Assert.IsTrue (info.IsStatic); } + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void ReplacedStaticFieldRetainsTargetOwner () + { + Assert.AreEqual (Math.PI, JavaLangRemappingTestObject.remappedStaticPi ()); + } + + [Test] + [Category ("NativeAOTIgnore")] + [Category ("TrimmableTypeMapUnsupported")] + public void ReplacedStaticMethodRetainsTargetOwner () + { + Assert.AreEqual (5, JavaLangRemappingTestObject.remappedStaticAbs (-5)); + } + [Test] [Category ("NativeAOTIgnore")] [Category ("TrimmableTypeMapUnsupported")] @@ -596,6 +612,17 @@ public unsafe int remappedToStaticHashCode () const string id = "remappedToStaticHashCode.()I"; return _members.InstanceMethods.InvokeVirtualInt32Method (id, this, null); } + + public static unsafe double remappedStaticPi () + { + return _members.StaticFields.GetDoubleValue ("remappedStaticPi.D"); + } + + public static unsafe int remappedStaticAbs (int value) + { + var argument = new JniArgumentValue (value); + return _members.StaticMethods.InvokeInt32Method ("remappedStaticAbs.(I)I", &argument); + } } [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index 67d6483bfb9..878105dbd0f 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -446,7 +446,7 @@ internal static int CompareUtf8 (byte [] left, byte [] right) return left.Length < right.Length ? -1 : 1; } - static byte [] Utf8 (string str) => String.IsNullOrEmpty (str) ? [] : Encoding.UTF8.GetBytes (str); + static byte [] Utf8 (string str) => str.IsNullOrEmpty () ? [] : Encoding.UTF8.GetBytes (str); GeneratedTables Init () { @@ -611,7 +611,7 @@ static string MakeMembersArrayName (string prefix, int typeIndex) static int GetMethodSignatureSpecificity (string signature) { - if (String.IsNullOrEmpty (signature)) { + if (signature.IsNullOrEmpty ()) { return 2; } return signature [signature.Length - 1] == ')' ? 1 : 0; diff --git a/src/native/clr/include/xamarin-app.hh b/src/native/clr/include/xamarin-app.hh index c15776e2f27..32ce747fd2d 100644 --- a/src/native/clr/include/xamarin-app.hh +++ b/src/native/clr/include/xamarin-app.hh @@ -243,8 +243,7 @@ struct JniRemappingReplacementMethod { const char *target_type; const char *target_name; - // const char *target_signature; - // const int32_t param_count; + const char *target_signature; const bool is_static; }; 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 fd0a9452d24..ac0a2721fdf 100644 --- a/src/native/clr/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/clr/xamarin-app-stub/application_dso_stub.cc @@ -153,6 +153,7 @@ static const JniRemappingIndexMethodEntry some_java_type_one_methods[] = { .replacement = { .target_type = "some/java/target_type_one", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = false, } }, @@ -173,6 +174,7 @@ static const JniRemappingIndexMethodEntry some_java_type_two_methods[] = { .replacement = { .target_type = "some/java/target_type_two", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = true, } }, @@ -218,9 +220,13 @@ const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[] = { extern "C" const xamarin::android::JniRemappingData jni_remapping_data { .type_replacements = jni_remapping_type_replacements, + .reverse_type_replacements = nullptr, .method_replacement_index = jni_remapping_method_replacement_index, + .field_replacement_index = nullptr, .type_replacement_count = 2, + .reverse_type_replacement_count = 0, .method_replacement_index_count = 2, + .field_replacement_index_count = 0, }; const char *init_runtime_property_names[] = { 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 2588b1da487..7a8d7940432 100644 --- a/src/native/mono/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/mono/xamarin-app-stub/application_dso_stub.cc @@ -249,6 +249,7 @@ static const JniRemappingIndexMethodEntry some_java_type_one_methods[] = { .replacement = { .target_type = "some/java/target_type_one", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = false, } }, @@ -269,6 +270,7 @@ static const JniRemappingIndexMethodEntry some_java_type_two_methods[] = { .replacement = { .target_type = "some/java/target_type_two", .target_name = "new_method_name", + .target_signature = nullptr, .is_static = true, } }, @@ -314,7 +316,11 @@ const JniRemappingTypeReplacementEntry jni_remapping_type_replacements[] = { extern "C" const xamarin::android::JniRemappingData jni_remapping_data { .type_replacements = jni_remapping_type_replacements, + .reverse_type_replacements = nullptr, .method_replacement_index = jni_remapping_method_replacement_index, + .field_replacement_index = nullptr, .type_replacement_count = 2, + .reverse_type_replacement_count = 0, .method_replacement_index_count = 2, + .field_replacement_index_count = 0, }; diff --git a/src/native/mono/xamarin-app-stub/xamarin-app.hh b/src/native/mono/xamarin-app-stub/xamarin-app.hh index d2dacdd9263..fb8c94c68c3 100644 --- a/src/native/mono/xamarin-app-stub/xamarin-app.hh +++ b/src/native/mono/xamarin-app-stub/xamarin-app.hh @@ -283,8 +283,7 @@ struct JniRemappingReplacementMethod { const char *target_type; const char *target_name; - // const char *target_signature; - // const int32_t param_count; + const char *target_signature; const bool is_static; }; From 79564816d2959e589ac05985ef4ccbdf825c701c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 18 Sep 2026 11:16:12 +0200 Subject: [PATCH 17/19] [tests] Fix Android remapping test compile Qualify System.Math in the shared test fixture and cache reverse JNI type lookup results used by activation paths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniPeerMembersTests.cs | 2 +- .../Microsoft.Android.Runtime/JniRemappingLookup.cs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) 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 bb5c9f440ad..c4de6371314 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 @@ -242,7 +242,7 @@ public void ReplaceStaticFieldName () [Category ("TrimmableTypeMapUnsupported")] public void ReplacedStaticFieldRetainsTargetOwner () { - Assert.AreEqual (Math.PI, JavaLangRemappingTestObject.remappedStaticPi ()); + Assert.AreEqual (global::System.Math.PI, JavaLangRemappingTestObject.remappedStaticPi ()); } [Test] diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs index de6acee45d0..67859211720 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs @@ -2,6 +2,7 @@ using System; using System.Buffers; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; @@ -83,9 +84,11 @@ unsafe struct NativeJniRemappingData static unsafe NativeJniRemappingData* nativeData; static bool isInUse; + static readonly ConcurrentDictionary reverseTypes = new (StringComparer.Ordinal); internal static unsafe void Initialize (IntPtr data) { + reverseTypes.Clear (); if (data == IntPtr.Zero) { isInUse = false; return; @@ -148,6 +151,12 @@ internal static unsafe IntPtr GetReplacementTypeUtf8 (string? jniSimpleReference if (jniSimpleReference is null || !isInUse || jniSimpleReference.Length == 0) return null; + string replacement = reverseTypes.GetOrAdd (jniSimpleReference, static source => LookupReverseType (source)); + return string.Equals (replacement, jniSimpleReference, StringComparison.Ordinal) ? null : replacement; + } + + static unsafe string LookupReverseType (string jniSimpleReference) + { NativeJniRemappingData* data = nativeData; if (data == null) throw new InvalidOperationException ("JNI remapping data has not been initialized."); @@ -156,7 +165,9 @@ internal static unsafe IntPtr GetReplacementTypeUtf8 (string? jniSimpleReference data->reverse_type_replacements, data->reverse_type_replacement_count, jniSimpleReference); - return replacement == null ? null : Marshal.PtrToStringUTF8 ((IntPtr)replacement); + return replacement == null + ? jniSimpleReference + : Marshal.PtrToStringUTF8 ((IntPtr)replacement) ?? jniSimpleReference; } internal static JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfo (string jniSourceType, string jniMethodName, string jniMethodSignature) From 2e39a9e1866c1dd30f16a8d8c4e44ffaadec94bc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 18 Sep 2026 12:51:38 +0200 Subject: [PATCH 18/19] [tests] Keep custom remapping fixtures desktop-only Android cannot install JavaVMFixture's custom type manager, so keep the synthetic remapping tests on the desktop Java.Interop test path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs | 3 +++ 1 file changed, 3 insertions(+) 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 c4de6371314..e61132ff1fa 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 @@ -226,6 +226,8 @@ public void MethodLookupForNonexistentStaticMethodWillTryFallbacks () } } +#if !__ANDROID__ + // These tests use JavaVMFixture's custom JniTypeManager, which Android does not support. [Test] [Category ("NativeAOTIgnore")] [Category ("TrimmableTypeMapUnsupported")] @@ -450,6 +452,7 @@ public void ReplacementMethodUsesTargetSignature () var method = JavaLangRemappingTestStringBuilder._members.InstanceMethods.GetMethodInfo ("indexOf.(Lnet/dot/jni/test/RenamedString;)I"); Assert.IsNotNull (method); } +#endif // !__ANDROID__ [Test] [Category ("NativeAOTIgnore")] From 00cfd3bf9afa6981d62a33dbd5672d7c10342222 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 18 Sep 2026 13:04:10 +0200 Subject: [PATCH 19/19] [build] Reconcile remapping generator after rebase Remove duplicated method-index entries carried through the lower-layer generator optimization so each native table is emitted once. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../JniRemappingAssemblyGenerator.cs | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index 878105dbd0f..f3942e0a629 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -380,35 +380,11 @@ sealed class GeneratedTables StructureInfo jniRemappingIndexFieldTypeEntryStructureInfo; StructureInfo jniRemappingTypeReplacementEntryStructureInfo; StructureInfo jniRemappingDataStructureInfo; - readonly Dictionary utf8SortKeys = new Dictionary (StringComparer.Ordinal); - public int ReplacementTypeCount { get; private set; } = 0; public int ReverseTypeCount { get; private set; } = 0; public int ReplacementMethodIndexEntryCount { get; private set; } = 0; public int ReplacementFieldIndexEntryCount { get; private set; } = 0; - int CompareUtf8 (string left, string right) - { - byte [] leftBytes = GetUtf8SortKey (left); - byte [] rightBytes = GetUtf8SortKey (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); - } - - byte [] GetUtf8SortKey (string value) - { - if (!utf8SortKeys.TryGetValue (value, out byte [] bytes)) { - bytes = Encoding.UTF8.GetBytes (value); - utf8SortKeys.Add (value, bytes); - } - return bytes; - } - public JniRemappingAssemblyGenerator (TaskLoggingHelper log) : base (log) {} @@ -545,7 +521,6 @@ List> MakeMethodIndex () TypeMethods = typeMethods, }; ret.Add (new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, entry)); - ret.Add (new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, entry)); } return ret;