Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,27 @@ sealed class JavaProxyObject : JavaObject, IEquatable<JavaProxyObject>

static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaProxyObject));
static readonly ConditionalWeakTable<object, JavaProxyObject> CachedValues = new ConditionalWeakTable<object, JavaProxyObject> ();
static bool nativeMethodsRegistered;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 ⚠️ JNI interopnativeMethodsRegistered is a process-wide static flag, but the previous approach registered the callbacks from a Java static { ManagedPeer.registerNativeMembers(...) } initializer, which re-runs every time the class is loaded (e.g., under a fresh classloader / recreated runtime). With this flag, if a second JniRuntime is created in the same process — or the class is reloaded under a new classloader — registration is skipped and the new class's equals/hashCode/toString natives are left unregistered, leading to UnsatisfiedLinkError at runtime. On Android this is fine (single runtime per process), but please confirm no test/runtime-restart scenario reuses this stale flag. If reuse is possible, key the flag off the runtime instance rather than a plain bool.

(Rule: JNI native registration lifecycle)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is fine, we are only targeting Android.


[JniAddNativeMethodRegistrationAttribute]
static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args)
static void RegisterNativeMethods ()
{
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)));
unsafe {
using (var proxyType = new JniType ("net/dot/jni/internal/JavaProxyObject"u8)) {
Comment thread
simonrozsival marked this conversation as resolved.
Span<JniNativeMethod> 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<IntPtr, IntPtr, IntPtr, byte>) &Equals);
methods [1] = new JniNativeMethod (hashCodeName, hashCodeSignature,
(IntPtr) (delegate* unmanaged<IntPtr, IntPtr, int>) &GetHashCode);
methods [2] = new JniNativeMethod (toStringName, toStringSignature,
(IntPtr) (delegate* unmanaged<IntPtr, IntPtr, IntPtr>) &ToString);
JniEnvironment.Types.RegisterNatives (proxyType.PeerReference, methods);
}
}
}
nativeMethodsRegistered = true;
}

public override JniPeerMembers JniPeerMembers {
Expand Down Expand Up @@ -66,36 +80,35 @@ 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;
}
}

// 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);
Expand All @@ -112,8 +125,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@
extends java.lang.Object
implements GCUserPeerable
{
static {
net.dot.jni.ManagedPeer.registerNativeMembers (
JavaProxyObject.class,
"");
}

ArrayList<Object> managedReferences = new ArrayList<Object>();

@Override
Expand All @@ -35,4 +29,3 @@ public void jiClearManagedReferences ()
managedReferences.clear ();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@
extends java.lang.Error
implements GCUserPeerable
{
static {
net.dot.jni.ManagedPeer.registerNativeMembers (
JavaProxyThrowable.class,
"");
}

ArrayList<Object> managedReferences = new ArrayList<Object>();

public JavaProxyThrowable () {
Expand All @@ -33,4 +27,3 @@ public void jiClearManagedReferences ()
managedReferences.clear ();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
{
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@
{
ArrayList<Object> managedReferences = new ArrayList<Object> ();

// 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)
{
Expand Down
Loading