diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs
index 55af4f7b4..ad8d04c35 100644
--- a/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs
+++ b/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs
@@ -32,7 +32,7 @@ partial class CreationOptions {
public JniValueManager? ValueManager {get; set;}
}
- JniValueManager? valueManager;
+ internal JniValueManager? valueManager;
public JniValueManager ValueManager {
get => valueManager ?? throw new NotSupportedException ();
}
@@ -271,6 +271,28 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type)
return type;
}
+ public IJavaPeerable? GetPeer (
+ JniObjectReference reference,
+ [DynamicallyAccessedMembers (Constructors)]
+ Type? targetType = null)
+ {
+ if (disposed) {
+ throw new ObjectDisposedException (GetType ().Name);
+ }
+
+ if (!reference.IsValid) {
+ return null;
+ }
+
+ var peeked = PeekPeer (reference);
+ if (peeked != null &&
+ (targetType == null ||
+ targetType.IsAssignableFrom (peeked.GetType ()))) {
+ return peeked;
+ }
+ return CreatePeer (ref reference, JniObjectReferenceOptions.Copy, targetType);
+ }
+
public virtual IJavaPeerable? CreatePeer (
ref JniObjectReference reference,
JniObjectReferenceOptions transfer,
diff --git a/src/Java.Interop/PublicAPI.Unshipped.txt b/src/Java.Interop/PublicAPI.Unshipped.txt
index d4be03231..5a788dfa6 100644
--- a/src/Java.Interop/PublicAPI.Unshipped.txt
+++ b/src/Java.Interop/PublicAPI.Unshipped.txt
@@ -5,5 +5,6 @@ virtual Java.Interop.JniRuntime.OnEnterMarshalMethod() -> void
virtual Java.Interop.JniRuntime.OnUserUnhandledException(ref Java.Interop.JniTransition transition, System.Exception! e) -> void
Java.Interop.JavaException.JavaException(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions transfer, Java.Interop.JniObjectReference throwableOverride) -> void
Java.Interop.JavaException.SetJavaStackTrace(Java.Interop.JniObjectReference peerReferenceOverride = default(Java.Interop.JniObjectReference)) -> void
+Java.Interop.JniRuntime.JniValueManager.GetPeer(Java.Interop.JniObjectReference reference, System.Type? targetType = null) -> Java.Interop.IJavaPeerable?
Java.Interop.JniTypeSignatureAttribute.InvokerType.get -> System.Type?
Java.Interop.JniTypeSignatureAttribute.InvokerType.set -> void
diff --git a/tests/Java.Interop-Tests/Java.Interop-Tests.csproj b/tests/Java.Interop-Tests/Java.Interop-Tests.csproj
index 0677be887..cbb684413 100644
--- a/tests/Java.Interop-Tests/Java.Interop-Tests.csproj
+++ b/tests/Java.Interop-Tests/Java.Interop-Tests.csproj
@@ -3,6 +3,8 @@
$(DotNetTargetFramework)
false
+ true
+ ..\..\product.snk
true
$(DefineConstants);NO_MARSHAL_MEMBER_BUILDER_SUPPORT;NO_GC_BRIDGE_SUPPORT
diff --git a/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs b/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs
new file mode 100644
index 000000000..a112f155c
--- /dev/null
+++ b/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs
@@ -0,0 +1,297 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+
+using Java.Interop;
+
+using NUnit.Framework;
+
+namespace Java.InteropTests {
+
+ // Android doesn't support `[NonParallelizable]`, but runs tests sequentially by default.
+#if !__ANDROID__
+ // Modifies JniRuntime.valueManager instance field; can't be done in parallel
+ [NonParallelizable]
+#endif // !__ANDROID__
+ public abstract class JniRuntimeJniValueManagerContract : JavaVMFixture {
+
+ protected abstract Type ValueManagerType {
+ get;
+ }
+
+ protected virtual JniRuntime.JniValueManager CreateValueManager ()
+ {
+ var manager = Activator.CreateInstance (ValueManagerType) as JniRuntime.JniValueManager;
+ return manager ?? throw new InvalidOperationException ($"Could not create instance of `{ValueManagerType}`!");
+ }
+
+#pragma warning disable CS8618
+ JniRuntime.JniValueManager systemManager;
+ JniRuntime.JniValueManager valueManager;
+#pragma warning restore CS8618
+
+ [SetUp]
+ public void CreateVM ()
+ {
+ systemManager = JniRuntime.CurrentRuntime.valueManager!;
+ valueManager = CreateValueManager ();
+ valueManager.OnSetRuntime (JniRuntime.CurrentRuntime);
+ JniRuntime.CurrentRuntime.valueManager = valueManager;
+ }
+
+ [TearDown]
+ public void DestroyVM ()
+ {
+ JniRuntime.CurrentRuntime.valueManager = systemManager;
+ systemManager = null!;
+ valueManager?.Dispose ();
+ valueManager = null!;
+ }
+
+ [Test]
+ public void AddPeer ()
+ {
+ }
+
+ int GetSurfacedPeersCount ()
+ {
+ return valueManager.GetSurfacedPeers ().Count;
+ }
+
+ [Test]
+ public void AddPeer_NoDuplicates ()
+ {
+ int startPeerCount = GetSurfacedPeersCount ();
+ using (var v = new MyDisposableObject ()) {
+ // MyDisposableObject ctor implicitly calls AddPeer();
+ Assert.AreEqual (startPeerCount + 1, GetSurfacedPeersCount (), DumpPeers ());
+ valueManager.AddPeer (v);
+ Assert.AreEqual (startPeerCount + 1, GetSurfacedPeersCount (), DumpPeers ());
+ }
+ }
+
+ [Test]
+ public void ConstructPeer_ImplicitViaBindingConstructor_PeerIsInSurfacedPeers ()
+ {
+ int startPeerCount = GetSurfacedPeersCount ();
+
+ var g = new GetThis ();
+ var surfaced = valueManager.GetSurfacedPeers ();
+ Assert.AreEqual (startPeerCount + 1, surfaced.Count);
+
+ var found = false;
+ foreach (var pr in surfaced) {
+ if (!pr.SurfacedPeer.TryGetTarget (out var p))
+ continue;
+ if (object.ReferenceEquals (g, p)) {
+ found = true;
+ }
+ }
+ Assert.IsTrue (found);
+
+ var localRef = g.PeerReference.NewLocalRef ();
+ g.Dispose ();
+ Assert.AreEqual (startPeerCount, GetSurfacedPeersCount ());
+ Assert.IsNull (valueManager.PeekPeer (localRef));
+ JniObjectReference.Dispose (ref localRef);
+ }
+
+ [Test]
+ public void ConstructPeer_ImplicitViaBindingMethod_PeerIsInSurfacedPeers ()
+ {
+ int startPeerCount = GetSurfacedPeersCount ();
+
+ var g = new GetThis ();
+ var surfaced = valueManager.GetSurfacedPeers ();
+ Assert.AreEqual (startPeerCount + 1, surfaced.Count);
+
+ var found = false;
+ foreach (var pr in surfaced) {
+ if (!pr.SurfacedPeer.TryGetTarget (out var p))
+ continue;
+ if (object.ReferenceEquals (g, p)) {
+ found = true;
+ }
+ }
+ Assert.IsTrue (found);
+
+ var localRef = g.PeerReference.NewLocalRef ();
+ g.Dispose ();
+ Assert.AreEqual (startPeerCount, GetSurfacedPeersCount ());
+ Assert.IsNull (valueManager.PeekPeer (localRef));
+ JniObjectReference.Dispose (ref localRef);
+ }
+
+
+ [Test]
+ public void CollectPeers ()
+ {
+ // TODO
+ }
+
+ [Test]
+ public void CreateValue ()
+ {
+ using (var o = new JavaObject ()) {
+ var r = o.PeerReference;
+ var x = (IJavaPeerable) valueManager.CreateValue (ref r, JniObjectReferenceOptions.Copy)!;
+ Assert.AreNotSame (o, x);
+ x.Dispose ();
+
+ x = valueManager.CreateValue (ref r, JniObjectReferenceOptions.Copy);
+ Assert.AreNotSame (o, x);
+ x!.Dispose ();
+ }
+ }
+
+ [Test]
+ public void GetValue_ReturnsAlias ()
+ {
+ var local = new JavaObject ();
+ local.UnregisterFromRuntime ();
+ Assert.IsNull (valueManager.PeekValue (local.PeerReference));
+ // GetObject must always return a value (unless handle is null, etc.).
+ // However, since we called local.UnregisterFromRuntime(),
+ // JniRuntime.PeekObject() is null (asserted above), but GetObject() must
+ // **still** return _something_.
+ // In this case, it returns an _alias_.
+ // TODO: "most derived type" alias generation. (Not relevant here, but...)
+ var p = local.PeerReference;
+ var alias = JniRuntime.CurrentRuntime.ValueManager.GetValue (ref p, JniObjectReferenceOptions.Copy);
+ Assert.AreNotSame (local, alias);
+ alias!.Dispose ();
+ local.Dispose ();
+ }
+
+ [Test]
+ public void GetValue_ReturnsNullWithNullHandle ()
+ {
+ var r = new JniObjectReference ();
+ var o = valueManager.GetValue (ref r, JniObjectReferenceOptions.Copy);
+ Assert.IsNull (o);
+ }
+
+ [Test]
+ public void GetValue_ReturnsNullWithInvalidSafeHandle ()
+ {
+ var invalid = new JniObjectReference ();
+ Assert.IsNull (valueManager.GetValue (ref invalid, JniObjectReferenceOptions.CopyAndDispose));
+ }
+
+ [Test]
+ public unsafe void GetValue_FindBestMatchType ()
+ {
+#if !NO_MARSHAL_MEMBER_BUILDER_SUPPORT
+ using (var t = new JniType (TestType.JniTypeName)) {
+ var c = t.GetConstructor ("()V");
+ var o = t.NewObject (c, null);
+ using (var w = valueManager.GetValue (ref o, JniObjectReferenceOptions.CopyAndDispose)) {
+ Assert.AreEqual (typeof (TestType), w!.GetType ());
+ Assert.IsTrue (((TestType) w).ExecutedActivationConstructor);
+ }
+ }
+#endif // !NO_MARSHAL_MEMBER_BUILDER_SUPPORT
+ }
+
+ [Test]
+ public void PeekPeer ()
+ {
+ Assert.IsNull (valueManager.PeekPeer (new JniObjectReference ()));
+
+ using (var v = new MyDisposableObject ()) {
+ Assert.IsNotNull (valueManager.PeekPeer (v.PeerReference));
+ Assert.AreSame (v, valueManager.PeekPeer (v.PeerReference));
+ }
+ }
+
+ [Test]
+ public void PeekValue ()
+ {
+ JniObjectReference lref;
+ using (var o = new JavaObject ()) {
+ lref = o.PeerReference.NewLocalRef ();
+ Assert.AreSame (o, valueManager.PeekValue (lref));
+ }
+ // At this point, the Java-side object is kept alive by `lref`,
+ // but the wrapper instance has been disposed, and thus should
+ // be unregistered, and thus unfindable.
+ Assert.IsNull (valueManager.PeekValue (lref));
+ JniObjectReference.Dispose (ref lref);
+ }
+
+ [Test]
+ public void PeekValue_BoxedObjects ()
+ {
+ var marshaler = valueManager.GetValueMarshaler