Skip to content

Commit 4f3be33

Browse files
committed
[Java.Interop] Add JniRuntime.JniValueManager.GetPeer()
Context: https://github.com/dotnet/android/pull/9630/files#r1891090085 Context: dotnet/android#9716 As part of NativeAOT prototyping within dotnet/android, we need to update `Java.Lang.Object.GetObject<T>()` so that it uses `Java.Interop.JniRuntime.JniValueManager` APIs instead of its own `TypeManager.CreateInstance()` invocation, as `TypeManager.CreateInstance()` hits P/Invokes which don't currently work in the NativeAOT sample environment. However, updated it to *what*? The obvious thing to do would be to use `JniRuntime.JniValueManager.GetValue()`: partial class Object { internal static IJavaPeerable? GetObject (IntPtr handle, JniHandleOwnershipt ransfer, Type? type = null) { var r = PeekObject (handle, type); if (r != null) { JNIEnv.DeleteRef (handle, transfer); return r; } var reference = new JniObjectReference (handle); r = (IJavaPeerable) JNIEnvInit.ValueManager.GetValue ( ref reference, JniObjectReferenceOptions.Copy, type); JNIEnv.DeleteRef (handle, transfer); return r; } } The problem is that this blows up good: <System.InvalidCastException: Arg_InvalidCastException at Java.Lang.Object.GetObject(IntPtr , JniHandleOwnership , Type ) at Android.Runtime.JNIEnv.<CreateNativeArrayElementToManaged>g__GetObject|74_11(IntPtr , Type ) at Android.Runtime.JNIEnv.<>c.<CreateNativeArrayElementToManaged>b__74_9(Type type, IntPtr source, Int32 index) at Android.Runtime.JNIEnv.GetObjectArray(IntPtr , Type[] ) at Java.InteropTests.JnienvTest.<>c__DisplayClass26_0.<MoarThreadingTests>b__1()> because somewhere in that stack trace we have a `java.lang.Integer` instance, and `.GetValue(Integer_ref, …)` returns a `System.Int32` containing the underling value, *not* an `IJavaPeerable` value for the `java.lang.Integer` instance. Consider: var i_class = new JniType ("java/lang/Integer"); var i_ctor = i_class.GetConstructor ("(I)V"); JniArgumentValue* i_args = stackalloc JniArgumentValue [1]; i_args [0] = new JniArgumentValue (42); var i_value = i_class.NewObject (i_ctor, i_args); var v = JniEnvironment.Runtime.ValueManager.GetValue (ref i_value, JniObjectReferenceOptions.CopyAndDispose, null); Console.WriteLine ($"v? {v} {v?.GetType ()}"); which prints `v? 42 System.Int32`. This was expected and desirable, until we try to use `GetValue()` for `Object.GetObject<T>()`; the semantics don't match. Add a new `JniRuntime.JniValueManager.GetPeer()` method, which better matches the semantics that `Object.GetObject<T>()` requires, allowing: partial class Object { internal static IJavaPeerable? GetObject (IntPtr handle, JniHandleOwnershipt ransfer, Type? type = null) { var r = JNIEnvInit.ValueManager.GetPeer (new JniObjectReference (handle)); JNIEnv.DeleteRef (handle, transfer); return r; } } Finally, add a new `JniRuntimeJniValueManagerContract` unit test, so that we have "more formalized" semantic requirements on `JniRuntime.JniValueManager` implementations.
1 parent 9b1d878 commit 4f3be33

File tree

5 files changed

+320
-1
lines changed

5 files changed

+320
-1
lines changed

src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs

+23-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ partial class CreationOptions {
3232
public JniValueManager? ValueManager {get; set;}
3333
}
3434

35-
JniValueManager? valueManager;
35+
internal JniValueManager? valueManager;
3636
public JniValueManager ValueManager {
3737
get => valueManager ?? throw new NotSupportedException ();
3838
}
@@ -271,6 +271,28 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type)
271271
return type;
272272
}
273273

274+
public IJavaPeerable? GetPeer (
275+
JniObjectReference reference,
276+
[DynamicallyAccessedMembers (Constructors)]
277+
Type? targetType = null)
278+
{
279+
if (disposed) {
280+
throw new ObjectDisposedException (GetType ().Name);
281+
}
282+
283+
if (!reference.IsValid) {
284+
return null;
285+
}
286+
287+
var peeked = PeekPeer (reference);
288+
if (peeked != null &&
289+
(targetType == null ||
290+
targetType.IsAssignableFrom (peeked.GetType ()))) {
291+
return peeked;
292+
}
293+
return CreatePeer (ref reference, JniObjectReferenceOptions.Copy, targetType);
294+
}
295+
274296
public virtual IJavaPeerable? CreatePeer (
275297
ref JniObjectReference reference,
276298
JniObjectReferenceOptions transfer,

src/Java.Interop/PublicAPI.Unshipped.txt

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ virtual Java.Interop.JniRuntime.OnEnterMarshalMethod() -> void
55
virtual Java.Interop.JniRuntime.OnUserUnhandledException(ref Java.Interop.JniTransition transition, System.Exception! e) -> void
66
Java.Interop.JavaException.JavaException(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions transfer, Java.Interop.JniObjectReference throwableOverride) -> void
77
Java.Interop.JavaException.SetJavaStackTrace(Java.Interop.JniObjectReference peerReferenceOverride = default(Java.Interop.JniObjectReference)) -> void
8+
Java.Interop.JniRuntime.JniValueManager.GetPeer(Java.Interop.JniObjectReference reference, System.Type? targetType = null) -> Java.Interop.IJavaPeerable?
89
Java.Interop.JniTypeSignatureAttribute.InvokerType.get -> System.Type?
910
Java.Interop.JniTypeSignatureAttribute.InvokerType.set -> void

tests/Java.Interop-Tests/Java.Interop-Tests.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<PropertyGroup>
44
<TargetFramework>$(DotNetTargetFramework)</TargetFramework>
55
<IsPackable>false</IsPackable>
6+
<SignAssembly>true</SignAssembly>
7+
<AssemblyOriginatorKeyFile>..\..\product.snk</AssemblyOriginatorKeyFile>
68
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
79
<DefineConstants>$(DefineConstants);NO_MARSHAL_MEMBER_BUILDER_SUPPORT;NO_GC_BRIDGE_SUPPORT</DefineConstants>
810
</PropertyGroup>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
#nullable enable
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Reflection;
7+
using System.Threading;
8+
9+
using Java.Interop;
10+
11+
using NUnit.Framework;
12+
13+
namespace Java.InteropTests {
14+
15+
// Modifies JniRuntime.valueManager instance field; can't be done in parallel
16+
[NonParallelizable]
17+
public abstract class JniRuntimeJniValueManagerContract : JavaVMFixture {
18+
19+
protected abstract Type ValueManagerType {
20+
get;
21+
}
22+
23+
protected virtual JniRuntime.JniValueManager CreateValueManager ()
24+
{
25+
var manager = Activator.CreateInstance (ValueManagerType) as JniRuntime.JniValueManager;
26+
return manager ?? throw new InvalidOperationException ($"Could not create instance of `{ValueManagerType}`!");
27+
}
28+
29+
#pragma warning disable CS8618
30+
JniRuntime.JniValueManager systemManager;
31+
JniRuntime.JniValueManager valueManager;
32+
#pragma warning restore CS8618
33+
34+
[SetUp]
35+
public void CreateVM ()
36+
{
37+
systemManager = JniRuntime.CurrentRuntime.valueManager!;
38+
valueManager = CreateValueManager ();
39+
valueManager.OnSetRuntime (JniRuntime.CurrentRuntime);
40+
JniRuntime.CurrentRuntime.valueManager = valueManager;
41+
}
42+
43+
[TearDown]
44+
public void DestroyVM ()
45+
{
46+
JniRuntime.CurrentRuntime.valueManager = systemManager;
47+
systemManager = null!;
48+
valueManager?.Dispose ();
49+
valueManager = null!;
50+
}
51+
52+
[Test]
53+
public void AddPeer ()
54+
{
55+
}
56+
57+
int GetSurfacedPeersCount ()
58+
{
59+
return valueManager.GetSurfacedPeers ().Count;
60+
}
61+
62+
[Test]
63+
public void AddPeer_NoDuplicates ()
64+
{
65+
int startPeerCount = GetSurfacedPeersCount ();
66+
using (var v = new MyDisposableObject ()) {
67+
// MyDisposableObject ctor implicitly calls AddPeer();
68+
Assert.AreEqual (startPeerCount + 1, GetSurfacedPeersCount (), DumpPeers ());
69+
valueManager.AddPeer (v);
70+
Assert.AreEqual (startPeerCount + 1, GetSurfacedPeersCount (), DumpPeers ());
71+
}
72+
}
73+
74+
[Test]
75+
public void ConstructPeer_ImplicitViaBindingConstructor_PeerIsInSurfacedPeers ()
76+
{
77+
int startPeerCount = GetSurfacedPeersCount ();
78+
79+
var g = new GetThis ();
80+
var surfaced = valueManager.GetSurfacedPeers ();
81+
Assert.AreEqual (startPeerCount + 1, surfaced.Count);
82+
83+
var found = false;
84+
foreach (var pr in surfaced) {
85+
if (!pr.SurfacedPeer.TryGetTarget (out var p))
86+
continue;
87+
if (object.ReferenceEquals (g, p)) {
88+
found = true;
89+
}
90+
}
91+
Assert.IsTrue (found);
92+
93+
var localRef = g.PeerReference.NewLocalRef ();
94+
g.Dispose ();
95+
Assert.AreEqual (startPeerCount, GetSurfacedPeersCount ());
96+
Assert.IsNull (valueManager.PeekPeer (localRef));
97+
JniObjectReference.Dispose (ref localRef);
98+
}
99+
100+
[Test]
101+
public void ConstructPeer_ImplicitViaBindingMethod_PeerIsInSurfacedPeers ()
102+
{
103+
int startPeerCount = GetSurfacedPeersCount ();
104+
105+
var g = new GetThis ();
106+
var surfaced = valueManager.GetSurfacedPeers ();
107+
Assert.AreEqual (startPeerCount + 1, surfaced.Count);
108+
109+
var found = false;
110+
foreach (var pr in surfaced) {
111+
if (!pr.SurfacedPeer.TryGetTarget (out var p))
112+
continue;
113+
if (object.ReferenceEquals (g, p)) {
114+
found = true;
115+
}
116+
}
117+
Assert.IsTrue (found);
118+
119+
var localRef = g.PeerReference.NewLocalRef ();
120+
g.Dispose ();
121+
Assert.AreEqual (startPeerCount, GetSurfacedPeersCount ());
122+
Assert.IsNull (valueManager.PeekPeer (localRef));
123+
JniObjectReference.Dispose (ref localRef);
124+
}
125+
126+
127+
[Test]
128+
public void CollectPeers ()
129+
{
130+
// TODO
131+
}
132+
133+
[Test]
134+
public void CreateValue ()
135+
{
136+
using (var o = new JavaObject ()) {
137+
var r = o.PeerReference;
138+
var x = (IJavaPeerable) valueManager.CreateValue (ref r, JniObjectReferenceOptions.Copy)!;
139+
Assert.AreNotSame (o, x);
140+
x.Dispose ();
141+
142+
x = valueManager.CreateValue<IJavaPeerable> (ref r, JniObjectReferenceOptions.Copy);
143+
Assert.AreNotSame (o, x);
144+
x!.Dispose ();
145+
}
146+
}
147+
148+
[Test]
149+
public void GetValue_ReturnsAlias ()
150+
{
151+
var local = new JavaObject ();
152+
local.UnregisterFromRuntime ();
153+
Assert.IsNull (valueManager.PeekValue (local.PeerReference));
154+
// GetObject must always return a value (unless handle is null, etc.).
155+
// However, since we called local.UnregisterFromRuntime(),
156+
// JniRuntime.PeekObject() is null (asserted above), but GetObject() must
157+
// **still** return _something_.
158+
// In this case, it returns an _alias_.
159+
// TODO: "most derived type" alias generation. (Not relevant here, but...)
160+
var p = local.PeerReference;
161+
var alias = JniRuntime.CurrentRuntime.ValueManager.GetValue<IJavaPeerable> (ref p, JniObjectReferenceOptions.Copy);
162+
Assert.AreNotSame (local, alias);
163+
alias!.Dispose ();
164+
local.Dispose ();
165+
}
166+
167+
[Test]
168+
public void GetValue_ReturnsNullWithNullHandle ()
169+
{
170+
var r = new JniObjectReference ();
171+
var o = valueManager.GetValue (ref r, JniObjectReferenceOptions.Copy);
172+
Assert.IsNull (o);
173+
}
174+
175+
[Test]
176+
public void GetValue_ReturnsNullWithInvalidSafeHandle ()
177+
{
178+
var invalid = new JniObjectReference ();
179+
Assert.IsNull (valueManager.GetValue (ref invalid, JniObjectReferenceOptions.CopyAndDispose));
180+
}
181+
182+
[Test]
183+
public unsafe void GetValue_FindBestMatchType ()
184+
{
185+
#if !NO_MARSHAL_MEMBER_BUILDER_SUPPORT
186+
using (var t = new JniType (TestType.JniTypeName)) {
187+
var c = t.GetConstructor ("()V");
188+
var o = t.NewObject (c, null);
189+
using (var w = valueManager.GetValue<IJavaPeerable> (ref o, JniObjectReferenceOptions.CopyAndDispose)) {
190+
Assert.AreEqual (typeof (TestType), w!.GetType ());
191+
Assert.IsTrue (((TestType) w).ExecutedActivationConstructor);
192+
}
193+
}
194+
#endif // !NO_MARSHAL_MEMBER_BUILDER_SUPPORT
195+
}
196+
197+
[Test]
198+
public void PeekPeer ()
199+
{
200+
Assert.IsNull (valueManager.PeekPeer (new JniObjectReference ()));
201+
202+
using (var v = new MyDisposableObject ()) {
203+
Assert.IsNotNull (valueManager.PeekPeer (v.PeerReference));
204+
Assert.AreSame (v, valueManager.PeekPeer (v.PeerReference));
205+
}
206+
}
207+
208+
[Test]
209+
public void PeekValue ()
210+
{
211+
JniObjectReference lref;
212+
using (var o = new JavaObject ()) {
213+
lref = o.PeerReference.NewLocalRef ();
214+
Assert.AreSame (o, valueManager.PeekValue (lref));
215+
}
216+
// At this point, the Java-side object is kept alive by `lref`,
217+
// but the wrapper instance has been disposed, and thus should
218+
// be unregistered, and thus unfindable.
219+
Assert.IsNull (valueManager.PeekValue (lref));
220+
JniObjectReference.Dispose (ref lref);
221+
}
222+
223+
[Test]
224+
public void PeekValue_BoxedObjects ()
225+
{
226+
var marshaler = valueManager.GetValueMarshaler<object> ();
227+
var ad = AppDomain.CurrentDomain;
228+
229+
var proxy = marshaler.CreateGenericArgumentState (ad);
230+
Assert.AreSame (ad, valueManager.PeekValue (proxy.ReferenceValue));
231+
marshaler.DestroyGenericArgumentState (ad, ref proxy);
232+
233+
var ex = new InvalidOperationException ("boo!");
234+
proxy = marshaler.CreateGenericArgumentState (ex);
235+
Assert.AreSame (ex, valueManager.PeekValue (proxy.ReferenceValue));
236+
marshaler.DestroyGenericArgumentState (ex, ref proxy);
237+
}
238+
239+
void AllNestedRegistrationScopeTests ()
240+
{
241+
AddPeer ();
242+
AddPeer_NoDuplicates ();
243+
ConstructPeer_ImplicitViaBindingConstructor_PeerIsInSurfacedPeers ();
244+
CreateValue ();
245+
GetValue_FindBestMatchType ();
246+
GetValue_ReturnsAlias ();
247+
GetValue_ReturnsNullWithInvalidSafeHandle ();
248+
GetValue_ReturnsNullWithNullHandle ();
249+
PeekPeer ();
250+
PeekValue ();
251+
PeekValue_BoxedObjects ();
252+
}
253+
254+
string DumpPeers ()
255+
{
256+
return DumpPeers (valueManager.GetSurfacedPeers ());
257+
}
258+
259+
static string DumpPeers (IEnumerable<JniSurfacedPeerInfo> peers)
260+
{
261+
return string.Join ("," + Environment.NewLine, peers);
262+
}
263+
264+
265+
// also test:
266+
// Singleton scenario
267+
// Types w/o "activation" constructors -- need to support checking parent scopes
268+
// nesting of scopes
269+
// Adding an instance already added in a previous scope?
270+
}
271+
272+
public abstract class JniRuntimeJniValueManagerContract<T> : JniRuntimeJniValueManagerContract {
273+
274+
protected override Type ValueManagerType => typeof (T);
275+
}
276+
277+
#if !NETCOREAPP
278+
[TestFixture]
279+
public class JniRuntimeJniValueManagerContract_Mono : JniRuntimeJniValueManagerContract {
280+
static Type MonoRuntimeValueManagerType = Type.GetType ("Java.Interop.MonoRuntimeValueManager, Java.Runtime.Environment", throwOnError:true)!;
281+
282+
protected override Type ValueManagerType => MonoRuntimeValueManagerType;
283+
}
284+
#endif // !NETCOREAPP
285+
286+
[TestFixture]
287+
public class JniRuntimeJniValueManagerContract_NoGCIntegration : JniRuntimeJniValueManagerContract {
288+
static Type ManagedValueManagerType = Type.GetType ("Java.Interop.ManagedValueManager, Java.Runtime.Environment", throwOnError:true)!;
289+
290+
protected override Type ValueManagerType => ManagedValueManagerType;
291+
}
292+
}

tests/TestJVM/TestJVM.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<TargetFramework>$(DotNetTargetFramework)</TargetFramework>
55
<Nullable>enable</Nullable>
66
<IsPackable>false</IsPackable>
7+
<SignAssembly>true</SignAssembly>
8+
<AssemblyOriginatorKeyFile>..\..\product.snk</AssemblyOriginatorKeyFile>
79
</PropertyGroup>
810

911
<Import Project="..\..\TargetFrameworkDependentValues.props" />

0 commit comments

Comments
 (0)