Skip to content

Commit 7b45b16

Browse files
[Java.Interop] JniValueManager.CreatePeer() tries more types (#1328)
Context: 77a6bf8 Context: dotnet/android#9962 While investigating dotnet/android#9962, we observed "curious" behavior around `JniRuntime.JniValueManager.CreatePeer()`, simplified in this unit test fragment: JniObjectReference float_ref; using (var value = new Java.Lang.Float (1.0f)) { float_ref = value.PeerReference.NewLocalRef (); } var peer = JniEnvironment.Runtime.ValueManager.CreatePeer ( ref float_ref, JniObjectReferenceOptions.Copy, targetType: null); Assert.IsNotNull (peer, "Could not create Java.Lang.Float peer!"); Assert.AreEqual ( typeof (Java.Lang.Float), peer.GetType (), $"Peer type mismatch: expected Java.Lang.Float, got {peer.GetType ()}"); What *actually* happened was that the type assertion failed: Error Message: Peer type mismatch: expected Java.Lang.Float, got Java.Interop.JavaObject Expected: <Java.Lang.Float> But was: <Java.Interop.JavaObject> `float_ref` is a `java.lang.Float`. Why is `CreatePeer()` creating a `JavaObject`? The reason why `CreatePeer()` created a `JavaObject` instance is tied to the semantics of `JniRuntime.JniTypeManager.GetType(JniTypeSignature)`, which in turn is influenced by 77a6bf8, which basically said "wouldn't it be cool if `float?` could be projected as `java.lang.Float`"? This would allow `null` to be represented: partial class ExportTest { [JavaCallable ("staticActionNullableFloat", Signature="(Ljava/lang/Float;)V")] public static void StaticActionNullableFloat (float? f) { } } It makes sense! (Kinda?) However, because of *when* "builtin" mappings are checked within `JniRuntime.JniTypeManager.GetType(JniTypeSignature)` -- *very early* -- this meant that when looking up the `Type` for `java/lang/Float`, `GetType(JniTypeSignature)` would return `System.Single?` and *not* `Java.Lang.Float`. As `System.Single?` does not implement `IJavaPeerable`, `JniRuntime.JniValueManager.CreatePeerInstance()` would *skip over* it, and ultimately hit `JavaObject` as a fallback. Fix this by having `JniRuntime.JniValueManager.CreatePeerInstance()` call `JniRuntime.JniTypeManager.GetTypes(JniTypeSignature)` instead of `.GetType(JniTypeSignature)`. This will allow it to find the binding for `Java.Lang.Float`, which *ISA* `IJavaPeerable`, allowing an instance with the appropriate type to be created and returned by `CreatePeer()`.
1 parent 5852e6e commit 7b45b16

File tree

6 files changed

+60
-5
lines changed

6 files changed

+60
-5
lines changed

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

+15-5
Original file line numberDiff line numberDiff line change
@@ -354,15 +354,13 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type)
354354
{
355355
var jniTypeName = JniEnvironment.Types.GetJniTypeNameFromClass (klass);
356356

357-
Type? type = null;
358357
while (jniTypeName != null) {
359358
JniTypeSignature sig;
360359
if (!JniTypeSignature.TryParse (jniTypeName, out sig))
361360
return null;
362361

363-
type = Runtime.TypeManager.GetType (sig);
364-
365-
if (type != null && targetType.IsAssignableFrom (type)) {
362+
Type? type = GetTypeAssignableTo (sig, targetType);
363+
if (type != null) {
366364
var peer = TryCreatePeerInstance (ref reference, transfer, type);
367365

368366
if (peer != null) {
@@ -382,6 +380,18 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type)
382380
JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose);
383381

384382
return TryCreatePeerInstance (ref reference, transfer, targetType);
383+
384+
[UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "Types returned here should be preserved via other means.")]
385+
[return: DynamicallyAccessedMembers (Constructors)]
386+
Type? GetTypeAssignableTo (JniTypeSignature sig, Type targetType)
387+
{
388+
foreach (var t in Runtime.TypeManager.GetTypes (sig)) {
389+
if (targetType.IsAssignableFrom (t)) {
390+
return t;
391+
}
392+
}
393+
return null;
394+
}
385395
}
386396

387397
IJavaPeerable? TryCreatePeerInstance (
@@ -637,7 +647,7 @@ public JniValueMarshaler GetValueMarshaler (Type type)
637647
return marshaler.Value;
638648
}
639649

640-
650+
641651
var listType = GetListType (type);
642652
if (listType != null) {
643653
var elementType = listType.GenericTypeArguments [0];

tests/Java.Base-Tests/Java.Base/JavaVMFixture.cs

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ static partial void CreateJavaVM ()
1717
var c = new TestJVM (
1818
jars: new[]{ "java.base-tests.jar" },
1919
typeMappings: new Dictionary<string, Type> () {
20+
["java/lang/Float"] = typeof (Java.Lang.Float),
2021
["example/MyIntConsumer"] = typeof (MyIntConsumer),
2122
["example/MyRunnable"] = typeof (MyRunnable),
2223
[JavaInvoker.JniTypeName] = typeof (JavaInvoker),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
3+
using Java.Interop;
4+
5+
using NUnit.Framework;
6+
7+
namespace Java.BaseTests;
8+
9+
[TestFixture]
10+
public class JniRuntimeJniValueManagerContractExtras : JavaVMFixture {
11+
12+
[Test]
13+
public void CreatePeer_FloatIsNotNullableSingle ()
14+
{
15+
JniObjectReference float_ref;
16+
#pragma warning disable CS0618
17+
using (var value = new Java.Lang.Float (1.0f)) {
18+
float_ref = value.PeerReference.NewLocalRef ();
19+
}
20+
#pragma warning restore CS0618
21+
try {
22+
var peer = JniEnvironment.Runtime.ValueManager.CreatePeer (ref float_ref, JniObjectReferenceOptions.Copy, targetType: null);
23+
Assert.IsNotNull (peer, "Could not create Java.Lang.Float peer!");
24+
Assert.AreEqual (
25+
typeof (Java.Lang.Float),
26+
peer!.GetType (),
27+
$"Peer type mismatch: expected Java.Lang.Float, got {peer.GetType ()}");
28+
} finally {
29+
JniObjectReference.Dispose (ref float_ref);
30+
}
31+
}
32+
}

tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public void GetTypeSignature_Type ()
3333
AssertGetJniTypeInfoForType (typeof (bool), "Z", true, 0);
3434
AssertGetJniTypeInfoForType (typeof (void), "V", true, 0);
3535

36+
AssertGetJniTypeInfoForType (typeof (float?), "java/lang/Float", true, 0);
37+
3638
AssertGetJniTypeInfoForType (typeof (JavaObject), "java/lang/Object", false, 0);
3739

3840
// Enums are their underlying type
@@ -130,6 +132,7 @@ static void AssertGetJniTypeInfoForType (Type type, string jniType, bool isKeywo
130132
Assert.AreEqual (typeof (float), GetType ("F"));
131133
Assert.AreEqual (typeof (double), GetType ("D"));
132134
Assert.AreEqual (typeof (string), GetType ("java/lang/String"));
135+
Assert.AreEqual (typeof (float?), GetType ("java/lang/Float"));
133136
Assert.AreEqual (null, GetType ("com/example/does/not/exist"));
134137
Assert.AreEqual (null, GetType ("Lcom/example/does/not/exist;"));
135138
Assert.AreEqual (null, GetType ("[Lcom/example/does/not/exist;"));

tests/Java.Interop.Export-Tests/Java.Interop/ExportTest.cs

+5
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ public static void StaticActionFloat (float f)
108108
{
109109
}
110110

111+
// TODO: [JavaCallable ("staticActionNullableFloat", Signature="(Ljava/lang/Float;)V")]
112+
public static void StaticActionNullableFloat (float? f)
113+
{
114+
}
115+
111116
[JavaCallable ("staticFuncThisMethodTakesLotsOfParameters", Signature="(ZBCSIJFDLjava/lang/Object;Ljava/lang/String;Ljava/util/ArrayList;Ljava/lang/String;Ljava/lang/Object;DFJ)Z")]
112117
public static bool StaticFuncThisMethodTakesLotsOfParameters (
113118
bool a,

tests/Java.Interop.Export-Tests/java/net/dot/jni/test/ExportType.java

+4
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public void testMethods () {
103103

104104
staticActionInt (1);
105105
staticActionFloat (2.0f);
106+
// staticActionNullableFloat (3.0f);
106107

107108
boolean r = staticFuncThisMethodTakesLotsOfParameters (
108109
false,
@@ -144,6 +145,9 @@ public void testMethods () {
144145
public void staticActionFloat (float f) {n_StaticActionFloat (f);}
145146
private native void n_StaticActionFloat (float f);
146147

148+
// public void staticActionNullableFloat (Float f) {n_StaticActionNullableFloat (f);}
149+
// private native void n_StaticActionNullableFloat (Float f);
150+
147151
ArrayList<Object> managedReferences = new ArrayList<Object>();
148152

149153
public void jiAddManagedReference (java.lang.Object obj)

0 commit comments

Comments
 (0)