From c28c9d0d34fbf05a756515b0b0a2a5cbc3340c77 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Jul 2026 12:44:29 +0200 Subject: [PATCH 1/4] Decouple Java proxies from ManagedPeer Register JavaProxyObject native methods directly from managed code and remove the no-op JavaProxyThrowable registration. Preserve the separate trimmable identity proxy behavior and add focused JVM coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 563374a5-ebc3-438c-b134-665c89c28361 --- .../Java.Interop/JavaProxyObject.cs | 12 ++-- .../net/dot/jni/internal/JavaProxyObject.java | 7 --- .../dot/jni/internal/JavaProxyThrowable.java | 7 --- .../JniValueMarshalerContractTests.cs | 60 +++++++++++++++++++ .../net/dot/jni/internal/JavaProxyObject.java | 7 +-- 5 files changed, 69 insertions(+), 24 deletions(-) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs index 909629b188b..8fe0d17e78a 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs @@ -15,12 +15,14 @@ sealed class JavaProxyObject : JavaObject, IEquatable static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaProxyObject)); static readonly ConditionalWeakTable CachedValues = new ConditionalWeakTable (); - [JniAddNativeMethodRegistrationAttribute] - static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args) + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "JavaProxyObject is only used by reflection-based JniRuntime.JniValueManager implementations.")] + static JavaProxyObject () { - args.Registrations.Add (new JniNativeMethodRegistration ("equals", "(Ljava/lang/Object;)Z", new EqualsMarshalMethod (Equals))); - args.Registrations.Add (new JniNativeMethodRegistration ("hashCode", "()I", new GetHashCodeMarshalMethod (GetHashCode))); - args.Registrations.Add (new JniNativeMethodRegistration ("toString", "()Ljava/lang/String;", new ToStringMarshalMethod (ToString))); + _members.JniPeerType.RegisterNativeMethods ( + new JniNativeMethodRegistration ("equals", "(Ljava/lang/Object;)Z", new EqualsMarshalMethod (Equals)), + new JniNativeMethodRegistration ("hashCode", "()I", new GetHashCodeMarshalMethod (GetHashCode)), + new JniNativeMethodRegistration ("toString", "()Ljava/lang/String;", new ToStringMarshalMethod (ToString)) + ); } public override JniPeerMembers JniPeerMembers { diff --git a/external/Java.Interop/src/Java.Interop/java/net/dot/jni/internal/JavaProxyObject.java b/external/Java.Interop/src/Java.Interop/java/net/dot/jni/internal/JavaProxyObject.java index 013cb070876..094baffc411 100644 --- a/external/Java.Interop/src/Java.Interop/java/net/dot/jni/internal/JavaProxyObject.java +++ b/external/Java.Interop/src/Java.Interop/java/net/dot/jni/internal/JavaProxyObject.java @@ -8,12 +8,6 @@ extends java.lang.Object implements GCUserPeerable { - static { - net.dot.jni.ManagedPeer.registerNativeMembers ( - JavaProxyObject.class, - ""); - } - ArrayList managedReferences = new ArrayList(); @Override @@ -35,4 +29,3 @@ public void jiClearManagedReferences () managedReferences.clear (); } } - diff --git a/external/Java.Interop/src/Java.Interop/java/net/dot/jni/internal/JavaProxyThrowable.java b/external/Java.Interop/src/Java.Interop/java/net/dot/jni/internal/JavaProxyThrowable.java index 066025ac58a..f5379edf403 100644 --- a/external/Java.Interop/src/Java.Interop/java/net/dot/jni/internal/JavaProxyThrowable.java +++ b/external/Java.Interop/src/Java.Interop/java/net/dot/jni/internal/JavaProxyThrowable.java @@ -8,12 +8,6 @@ extends java.lang.Error implements GCUserPeerable { - static { - net.dot.jni.ManagedPeer.registerNativeMembers ( - JavaProxyThrowable.class, - ""); - } - ArrayList managedReferences = new ArrayList(); public JavaProxyThrowable () { @@ -33,4 +27,3 @@ public void jiClearManagedReferences () managedReferences.clear (); } } - diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs index 40a6440889d..28a9f659eab 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs @@ -584,6 +584,41 @@ public class JniValueMarshaler_object_ContractTests : JniValueMarshalerContractT protected override object Value {get {return value;}} protected override bool UsesProxy {get {return true;}} + [Test] + public unsafe void ProxyObjectMethodsDelegateToManagedValue () + { + var value = new ProxyValue (42); + var equalValue = new ProxyValue (42); + var otherValue = new ProxyValue (43); + var valueState = marshaler.CreateGenericObjectReferenceArgumentState (value); + var equalState = marshaler.CreateGenericObjectReferenceArgumentState (equalValue); + var otherState = marshaler.CreateGenericObjectReferenceArgumentState (otherValue); + + try { + using (var proxyType = new JniType ("net/dot/jni/internal/JavaProxyObject")) { + var equals = proxyType.GetInstanceMethod ("equals", "(Ljava/lang/Object;)Z"); + var hashCode = proxyType.GetInstanceMethod ("hashCode", "()I"); + var toString = proxyType.GetInstanceMethod ("toString", "()Ljava/lang/String;"); + var args = stackalloc JniArgumentValue [1]; + + args [0] = new JniArgumentValue (equalState.ReferenceValue); + Assert.IsTrue (JniEnvironment.InstanceMethods.CallBooleanMethod (valueState.ReferenceValue, equals, args)); + + args [0] = new JniArgumentValue (otherState.ReferenceValue); + Assert.IsFalse (JniEnvironment.InstanceMethods.CallBooleanMethod (valueState.ReferenceValue, equals, args)); + + Assert.AreEqual (value.GetHashCode (), JniEnvironment.InstanceMethods.CallIntMethod (valueState.ReferenceValue, hashCode)); + + var proxyString = JniEnvironment.InstanceMethods.CallObjectMethod (valueState.ReferenceValue, toString); + Assert.AreEqual (value.ToString (), JniEnvironment.Strings.ToString (ref proxyString, JniObjectReferenceOptions.CopyAndDispose)); + } + } finally { + marshaler.DestroyGenericArgumentState (otherValue, ref otherState); + marshaler.DestroyGenericArgumentState (equalValue, ref equalState); + marshaler.DestroyGenericArgumentState (value, ref valueState); + } + } + [Test] public void SpecificTypesAreUsed () { @@ -599,6 +634,31 @@ public void SpecificTypesAreUsed () Assert.AreEqual ("net/dot/jni/internal/JavaProxyObject", JniEnvironment.Types.GetJniTypeNameFromInstance (s.ReferenceValue)); marshaler.DestroyGenericArgumentState (value, ref s); } + + sealed class ProxyValue { + + readonly int value; + + public ProxyValue (int value) + { + this.value = value; + } + + public override bool Equals (object obj) + { + return obj is ProxyValue other && value == other.value; + } + + public override int GetHashCode () + { + return value; + } + + public override string ToString () + { + return $"ProxyValue({value})"; + } + } } [TestFixture] diff --git a/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java b/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java index f18107b89e5..071ffdad070 100644 --- a/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java +++ b/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java @@ -10,11 +10,8 @@ { ArrayList managedReferences = new ArrayList (); - // This trimmable runtime copy cannot use Java.Interop's native object methods: - // those are registered through ManagedPeer.registerNativeMembers, which is not - // supported in the trimmable typemap path. - // Trimmable proxies use Java identity semantics: equals/hashCode/toString - // do not delegate to the wrapped .NET object. + // Trimmable proxies use Java identity semantics instead of Java.Interop's + // native equals/hashCode/toString callbacks. @Override public boolean equals (Object obj) { From 3764fd5651577cd9cad78f72f6ad8dec18257398 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Jul 2026 12:54:15 +0200 Subject: [PATCH 2/4] Use UCO callbacks for JavaProxyObject Register the proxy natives with UTF-8 JniNativeMethod entries and direct unmanaged function pointers, avoiding delegate marshaling and AOT suppressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 563374a5-ebc3-438c-b134-665c89c28361 --- .../Java.Interop/JavaProxyObject.cs | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs index 8fe0d17e78a..27b9e42df13 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs @@ -15,14 +15,22 @@ sealed class JavaProxyObject : JavaObject, IEquatable static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaProxyObject)); static readonly ConditionalWeakTable CachedValues = new ConditionalWeakTable (); - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "JavaProxyObject is only used by reflection-based JniRuntime.JniValueManager implementations.")] - static JavaProxyObject () + static unsafe JavaProxyObject () { - _members.JniPeerType.RegisterNativeMethods ( - new JniNativeMethodRegistration ("equals", "(Ljava/lang/Object;)Z", new EqualsMarshalMethod (Equals)), - new JniNativeMethodRegistration ("hashCode", "()I", new GetHashCodeMarshalMethod (GetHashCode)), - new JniNativeMethodRegistration ("toString", "()Ljava/lang/String;", new ToStringMarshalMethod (ToString)) - ); + using (var proxyType = new JniType ("net/dot/jni/internal/JavaProxyObject"u8)) { + Span methods = stackalloc JniNativeMethod [3]; + fixed (byte* equalsName = "equals"u8, equalsSignature = "(Ljava/lang/Object;)Z"u8) + fixed (byte* hashCodeName = "hashCode"u8, hashCodeSignature = "()I"u8) + fixed (byte* toStringName = "toString"u8, toStringSignature = "()Ljava/lang/String;"u8) { + methods [0] = new JniNativeMethod (equalsName, equalsSignature, + (IntPtr) (delegate* unmanaged) &Equals); + methods [1] = new JniNativeMethod (hashCodeName, hashCodeSignature, + (IntPtr) (delegate* unmanaged) &GetHashCode); + methods [2] = new JniNativeMethod (toStringName, toStringSignature, + (IntPtr) (delegate* unmanaged) &ToString); + JniEnvironment.Types.RegisterNatives (proxyType.PeerReference, methods); + } + } } public override JniPeerMembers JniPeerMembers { @@ -74,30 +82,26 @@ public override bool Equals (object? obj) } } - // TODO: Keep in sync with the code generated by ExportedMemberBuilder - [UnmanagedFunctionPointer (CallingConvention.Winapi)] - delegate bool EqualsMarshalMethod (IntPtr jnienv, IntPtr n_self, IntPtr n_value); - static bool Equals (IntPtr jnienv, IntPtr n_self, IntPtr n_value) + [UnmanagedCallersOnly] + static byte Equals (IntPtr jnienv, IntPtr n_self, IntPtr n_value) { var envp = new JniTransition (jnienv); try { var self = (JavaProxyObject?) JniEnvironment.Runtime.ValueManager.PeekPeer (new JniObjectReference (n_self)); var r_value = new JniObjectReference (n_value); var value = JniEnvironment.Runtime.ValueManager.GetValue (ref r_value, JniObjectReferenceOptions.Copy); - return self?.Equals (value) ?? false; + return self?.Equals (value) == true ? (byte) 1 : (byte) 0; } catch (Exception e) when (JniEnvironment.Runtime.ExceptionShouldTransitionToJni (e)) { envp.SetPendingException (e); - return false; + return 0; } finally { envp.Dispose (); } } - // TODO: Keep in sync with the code generated by ExportedMemberBuilder - [UnmanagedFunctionPointer (CallingConvention.Winapi)] - delegate int GetHashCodeMarshalMethod (IntPtr jnienv, IntPtr n_self); + [UnmanagedCallersOnly] static int GetHashCode (IntPtr jnienv, IntPtr n_self) { var envp = new JniTransition (jnienv); @@ -114,8 +118,7 @@ static int GetHashCode (IntPtr jnienv, IntPtr n_self) } } - [UnmanagedFunctionPointer (CallingConvention.Winapi)] - delegate IntPtr ToStringMarshalMethod (IntPtr jnienv, IntPtr n_self); + [UnmanagedCallersOnly] static IntPtr ToString (IntPtr jnienv, IntPtr n_self) { var envp = new JniTransition (jnienv); From 193d927e095cd12e05dab865bbdfcadef2f65ffb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Jul 2026 13:00:15 +0200 Subject: [PATCH 3/4] Register proxy natives before first allocation Perform the one-time UCO registration under the proxy cache lock immediately before constructing the first JavaProxyObject, avoiding static-constructor timing and allowing retries after registration failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 563374a5-ebc3-438c-b134-665c89c28361 --- .../src/Java.Interop/Java.Interop/JavaProxyObject.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs index 27b9e42df13..1015c9feb95 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs @@ -14,8 +14,9 @@ sealed class JavaProxyObject : JavaObject, IEquatable static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaProxyObject)); static readonly ConditionalWeakTable CachedValues = new ConditionalWeakTable (); + static bool nativeMethodsRegistered; - static unsafe JavaProxyObject () + static unsafe void RegisterNativeMethods () { using (var proxyType = new JniType ("net/dot/jni/internal/JavaProxyObject"u8)) { Span methods = stackalloc JniNativeMethod [3]; @@ -31,6 +32,7 @@ static unsafe JavaProxyObject () JniEnvironment.Types.RegisterNatives (proxyType.PeerReference, methods); } } + nativeMethodsRegistered = true; } public override JniPeerMembers JniPeerMembers { @@ -76,6 +78,9 @@ public override bool Equals (object? obj) lock (CachedValues) { if (CachedValues.TryGetValue (value, out var proxy)) return proxy; + // Register before JavaObject's constructor allocates the Java peer. + if (!nativeMethodsRegistered) + RegisterNativeMethods (); proxy = new JavaProxyObject (value); CachedValues.Add (value, proxy); return proxy; From 4ed8b478564d42b38f10aedbb67084e14a715466 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Jul 2026 13:05:19 +0200 Subject: [PATCH 4/4] Limit unsafe scope in proxy registration Keep RegisterNativeMethods safe at the signature level and scope pointer operations to an explicit unsafe block. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 563374a5-ebc3-438c-b134-665c89c28361 --- .../Java.Interop/JavaProxyObject.cs | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs index 1015c9feb95..b08c8c4b8ef 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs @@ -16,20 +16,22 @@ sealed class JavaProxyObject : JavaObject, IEquatable static readonly ConditionalWeakTable CachedValues = new ConditionalWeakTable (); static bool nativeMethodsRegistered; - static unsafe void RegisterNativeMethods () + static void RegisterNativeMethods () { - using (var proxyType = new JniType ("net/dot/jni/internal/JavaProxyObject"u8)) { - Span methods = stackalloc JniNativeMethod [3]; - fixed (byte* equalsName = "equals"u8, equalsSignature = "(Ljava/lang/Object;)Z"u8) - fixed (byte* hashCodeName = "hashCode"u8, hashCodeSignature = "()I"u8) - fixed (byte* toStringName = "toString"u8, toStringSignature = "()Ljava/lang/String;"u8) { - methods [0] = new JniNativeMethod (equalsName, equalsSignature, - (IntPtr) (delegate* unmanaged) &Equals); - methods [1] = new JniNativeMethod (hashCodeName, hashCodeSignature, - (IntPtr) (delegate* unmanaged) &GetHashCode); - methods [2] = new JniNativeMethod (toStringName, toStringSignature, - (IntPtr) (delegate* unmanaged) &ToString); - JniEnvironment.Types.RegisterNatives (proxyType.PeerReference, methods); + unsafe { + using (var proxyType = new JniType ("net/dot/jni/internal/JavaProxyObject"u8)) { + Span methods = stackalloc JniNativeMethod [3]; + fixed (byte* equalsName = "equals"u8, equalsSignature = "(Ljava/lang/Object;)Z"u8) + fixed (byte* hashCodeName = "hashCode"u8, hashCodeSignature = "()I"u8) + fixed (byte* toStringName = "toString"u8, toStringSignature = "()Ljava/lang/String;"u8) { + methods [0] = new JniNativeMethod (equalsName, equalsSignature, + (IntPtr) (delegate* unmanaged) &Equals); + methods [1] = new JniNativeMethod (hashCodeName, hashCodeSignature, + (IntPtr) (delegate* unmanaged) &GetHashCode); + methods [2] = new JniNativeMethod (toStringName, toStringSignature, + (IntPtr) (delegate* unmanaged) &ToString); + JniEnvironment.Types.RegisterNatives (proxyType.PeerReference, methods); + } } } nativeMethodsRegistered = true;