Skip to content

Commit 27236f2

Browse files
committed
[Java.Interop] Add .JavaAs() extension method
Fixes: dotnet/android#9038 Context: 1adb796 Imagine the following Java type hierarchy: // Java public abstract class Drawable { public static Drawable createFromStream(IntputStream is, String srcName) {…} // … } public interface Animatable { public void start(); // … } /* package */ class SomeAnimatableDrawable extends Drawable implements Animatable { // … } Further imagine that a call to `Drawable.createFromStream()` returns an instance of `SomeAnimatableDrawable`. What does the *binding* `Drawable.CreateFromStream()` return? // C# var drawable = Drawable.CreateFromStream(input, name); The binding `Drawable.CreateFromStream()` look at the runtime type of the value returned, see that it's of type `SomeAnimatableDrawable`, and look for an existing binding of that type. If no such binding is found -- which will be the case here, as `SomeAnimatableDrawable` is package-private -- then we check the value's base class, ad infinitum, until we hit a type that we *do* have a binding for (or fail catastrophically when we can't find a binding for `java.lang.Object`). See also [`TypeManager.CreateInstance()`][0], which is similar to the code within `JniRuntime.JniValueManager.GetPeerConstructor()`. Any interfaces implemented by Java value are not consulted. Only the base class hiearchy. For the sake of discussion, assume that `drawable` will be an instance of `DrawableInvoker` (e.g. 1adb796), akin to: internal class DrawableInvoker : Drawable { // … } Further imagine that we want to invoke `Animatable` methods on `drawable`. How do we do this? This is where the [`.JavaCast<TResult>()` extension method][1] comes in: we can use `.JavaCast<TResult>()` to perform a Java-side type check the type cast, which returns a value which can be used to invoke methods on the specified type: var animatable = drawable.JavaCast<IAnimatable>(); animatable.Start(); The problem with `.JavaCast<TResult>()` is that it always throws on failure: var someOtherIface = drawable.JavaCast<ISomethingElse>(); // throws some exception… @mattleibow requests an "exception-free JavaCast overload" so that he can *easily* use type-specific functionality *optionally*. Add the following extension methods on `IJavaPeerable`: static class JavaPeerableExtensions { public static TResult? JavaAs<TResult>( this IJavaPeerable self); public static bool TryJavaCast<TResult>( this IJavaPeerable self, out TResult? result); } The `.JavaAs<TResult>()` extension method mirrors the C# `as` operator, returning `null` if the type coercion would fail. This makes it useful for one-off invocations: drawable.JavaAs<IAnimatable>()?.Start(); The `.TryJavaCast<TResult>()` extension method follows the `TryParse()` pattern, returning true if the type coercion succeeds and the output `result` parameter is non-null, and false otherwise. This allows "nicely scoping" things within an `if`: if (drawable.TryJavaCast<IAnimatable>(out var animatable)) { animatable.Start(); // … animatable.Stop(); } [0]: https://github.com/dotnet/android/blob/06bb1dc6a292ef5618a3bb6ecca3ca869253ff2e/src/Mono.Android/Java.Interop/TypeManager.cs#L276-L291 [1]: https://github.com/dotnet/android/blob/06bb1dc6a292ef5618a3bb6ecca3ca869253ff2e/src/Mono.Android/Android.Runtime/Extensions.cs#L9-L17
1 parent ccafbe6 commit 27236f2

File tree

8 files changed

+204
-5
lines changed

8 files changed

+204
-5
lines changed

src/Java.Interop/Java.Interop/JavaPeerableExtensions.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#nullable enable
22

33
using System;
4+
using System.Diagnostics.CodeAnalysis;
45

56
namespace Java.Interop {
67

@@ -11,5 +12,40 @@ public static class JavaPeerableExtensions {
1112
JniPeerMembers.AssertSelf (self);
1213
return JniEnvironment.Types.GetJniTypeNameFromInstance (self.PeerReference);
1314
}
15+
16+
public static bool TryJavaCast<
17+
[DynamicallyAccessedMembers (JavaObject.ConstructorsAndInterfaces)]
18+
TResult
19+
> (this IJavaPeerable? self, [NotNullWhen (true)] out TResult? result)
20+
where TResult : class, IJavaPeerable
21+
{
22+
result = JavaAs<TResult> (self);
23+
return result != null;
24+
}
25+
26+
public static TResult? JavaAs<
27+
[DynamicallyAccessedMembers (JavaObject.ConstructorsAndInterfaces)]
28+
TResult
29+
> (this IJavaPeerable? self)
30+
where TResult : class, IJavaPeerable
31+
{
32+
if (self == null) {
33+
return null;
34+
}
35+
36+
if (self is TResult result) {
37+
return result;
38+
}
39+
40+
if (!self.PeerReference.IsValid) {
41+
throw new ObjectDisposedException (self.GetType ().FullName);
42+
}
43+
44+
var r = self.PeerReference;
45+
return JniEnvironment.Runtime.ValueManager.CreatePeer (
46+
ref r, JniObjectReferenceOptions.Copy,
47+
targetType: typeof (TResult))
48+
as TResult;
49+
}
1450
}
1551
}

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,16 +276,45 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type)
276276
if (disposed)
277277
throw new ObjectDisposedException (GetType ().Name);
278278

279+
if (!reference.IsValid) {
280+
return null;
281+
}
282+
279283
targetType = targetType ?? typeof (JavaObject);
280284
targetType = GetPeerType (targetType);
281285

282286
if (!typeof (IJavaPeerable).IsAssignableFrom (targetType))
283287
throw new ArgumentException ($"targetType `{targetType.AssemblyQualifiedName}` must implement IJavaPeerable!", nameof (targetType));
284288

285-
var ctor = GetPeerConstructor (reference, targetType);
286-
if (ctor == null)
289+
var targetSig = Runtime.TypeManager.GetTypeSignature (targetType);
290+
if (!targetSig.IsValid || targetSig.SimpleReference == null) {
291+
throw new ArgumentException ($"Could not determine Java type corresponding to `{targetType.AssemblyQualifiedName}`.", nameof (targetType));
292+
}
293+
294+
var refClass = JniEnvironment.Types.GetObjectClass (reference);
295+
JniObjectReference targetClass;
296+
try {
297+
targetClass = JniEnvironment.Types.FindClass (targetSig.SimpleReference);
298+
} catch (Exception e) {
299+
JniObjectReference.Dispose (ref refClass);
300+
throw new ArgumentException ($"Could not find Java class `{targetSig.SimpleReference}`.",
301+
nameof (targetType),
302+
e);
303+
}
304+
305+
if (!JniEnvironment.Types.IsAssignableFrom (refClass, targetClass)) {
306+
JniObjectReference.Dispose (ref refClass);
307+
JniObjectReference.Dispose (ref targetClass);
308+
return null;
309+
}
310+
311+
JniObjectReference.Dispose (ref targetClass);
312+
313+
var ctor = GetPeerConstructor (ref refClass, targetType);
314+
if (ctor == null) {
287315
throw new NotSupportedException (string.Format ("Could not find an appropriate constructable wrapper type for Java type '{0}', targetType='{1}'.",
288316
JniEnvironment.Types.GetJniTypeNameFromInstance (reference), targetType));
317+
}
289318

290319
var acts = new object[] {
291320
reference,
@@ -303,11 +332,10 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type)
303332
static readonly Type ByRefJniObjectReference = typeof (JniObjectReference).MakeByRefType ();
304333

305334
ConstructorInfo? GetPeerConstructor (
306-
JniObjectReference instance,
335+
ref JniObjectReference klass,
307336
[DynamicallyAccessedMembers (Constructors)]
308337
Type fallbackType)
309338
{
310-
var klass = JniEnvironment.Types.GetObjectClass (instance);
311339
var jniTypeName = JniEnvironment.Types.GetJniTypeNameFromClass (klass);
312340

313341
Type? type = null;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
static Java.Interop.JavaPeerableExtensions.TryJavaCast<TResult>(this Java.Interop.IJavaPeerable? self, out TResult? result) -> bool
3+
static Java.Interop.JavaPeerableExtensions.JavaAs<TResult>(this Java.Interop.IJavaPeerable? self) -> TResult?

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\CallVirtualFromConstructorBase.java" />
4242
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\CallVirtualFromConstructorDerived.java" />
4343
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\GetThis.java" />
44+
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\JavaInterface.java" />
45+
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\MyJavaInterfaceImpl.java" />
4446
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\ObjectHelper.java" />
4547
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\RenameClassBase1.java" />
4648
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\RenameClassBase2.java" />
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
using Java.Interop;
6+
7+
using NUnit.Framework;
8+
9+
namespace Java.InteropTests;
10+
11+
[TestFixture]
12+
public class JavaPeerableExtensionsTests {
13+
14+
[Test]
15+
public void JavaAs_Exceptions ()
16+
{
17+
using var v = new MyJavaInterfaceImpl ();
18+
19+
// The Java type corresponding to JavaObjectWithMissingJavaPeer doesn't exist
20+
Assert.Throws<ArgumentException>(() => v.JavaAs<JavaObjectWithMissingJavaPeer>());
21+
22+
var r = v.PeerReference;
23+
using var o = new JavaObject (ref r, JniObjectReferenceOptions.Copy);
24+
// MyJavaInterfaceImpl doesn't provide an activation constructor
25+
Assert.Throws<NotSupportedException>(() => o.JavaAs<MyJavaInterfaceImpl>());
26+
#if !__ANDROID__
27+
// JavaObjectWithNoJavaPeer has no Java peer
28+
Assert.Throws<ArgumentException>(() => v.JavaAs<JavaObjectWithNoJavaPeer>());
29+
#endif // !__ANDROID__
30+
}
31+
32+
[Test]
33+
public void JavaAs_NullSelfReturnsNull ()
34+
{
35+
Assert.AreEqual (null, JavaPeerableExtensions.JavaAs<IAndroidInterface> (null));
36+
}
37+
38+
[Test]
39+
public void JavaAs_InstanceThatDoesNotImplementInterfaceReturnsNull ()
40+
{
41+
using var v = new MyJavaInterfaceImpl ();
42+
Assert.AreEqual (null, JavaPeerableExtensions.JavaAs<IAndroidInterface> (v));
43+
}
44+
45+
[Test]
46+
public void JavaAs ()
47+
{
48+
using var impl = new MyJavaInterfaceImpl ();
49+
using var iface = impl.JavaAs<IJavaInterface> ();
50+
Assert.IsNotNull (iface);
51+
Assert.AreEqual ("Hello from Java!", iface.Value);
52+
}
53+
}
54+
55+
// Note: Java side implements JavaInterface, while managed binding DOES NOT.
56+
[JniTypeSignature (JniTypeName, GenerateJavaPeer=false)]
57+
class MyJavaInterfaceImpl : JavaObject {
58+
internal const string JniTypeName = "net/dot/jni/test/MyJavaInterfaceImpl";
59+
60+
internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (MyJavaInterfaceImpl));
61+
62+
public override JniPeerMembers JniPeerMembers {
63+
get {return _members;}
64+
}
65+
66+
public unsafe MyJavaInterfaceImpl ()
67+
: base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
68+
{
69+
const string id = "()V";
70+
var peer = _members.InstanceMethods.StartCreateInstance (id, GetType (), null);
71+
Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose);
72+
_members.InstanceMethods.FinishCreateInstance (id, this, null);
73+
}
74+
}
75+
76+
[JniTypeSignature (JniTypeName, GenerateJavaPeer=false)]
77+
interface IJavaInterface : IJavaPeerable {
78+
internal const string JniTypeName = "net/dot/jni/test/JavaInterface";
79+
80+
public string Value {
81+
[JniMethodSignatureAttribute("getValue", "()Ljava/lang/String;")]
82+
get;
83+
}
84+
}
85+
86+
internal class IJavaInterfaceInvoker : JavaObject, IJavaInterface {
87+
88+
internal static readonly JniPeerMembers _members = new JniPeerMembers (IJavaInterface.JniTypeName, typeof (IJavaInterfaceInvoker));
89+
90+
public override JniPeerMembers JniPeerMembers {
91+
get {return _members;}
92+
}
93+
94+
public IJavaInterfaceInvoker (ref JniObjectReference reference, JniObjectReferenceOptions options)
95+
: base (ref reference, options)
96+
{
97+
}
98+
99+
public unsafe string Value {
100+
get {
101+
const string id = "getValue.()Ljava/lang/String;";
102+
var r = JniPeerMembers.InstanceMethods.InvokeVirtualObjectMethod (id, this, null);
103+
return JniEnvironment.Strings.ToString (ref r, JniObjectReferenceOptions.CopyAndDispose);
104+
}
105+
}
106+
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,23 @@ public override unsafe int hashCode ()
264264
interface IAndroidInterface : IJavaPeerable {
265265
internal const string JniTypeName = "net/dot/jni/test/AndroidInterface";
266266

267-
private static JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (IAndroidInterface), isInterface: true);
267+
internal static JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (IAndroidInterface), isInterface: true);
268268

269269
public static unsafe string getClassName ()
270270
{
271271
var s = _members.StaticMethods.InvokeObjectMethod ("getClassName.()Ljava/lang/String;", null);
272272
return JniEnvironment.Strings.ToString (ref s, JniObjectReferenceOptions.CopyAndDispose);
273273
}
274274
}
275+
276+
internal class IAndroidInterfaceInvoker : JavaObject, IAndroidInterface {
277+
278+
public override JniPeerMembers JniPeerMembers => IAndroidInterface._members;
279+
280+
public IAndroidInterfaceInvoker (ref JniObjectReference reference, JniObjectReferenceOptions options)
281+
: base (ref reference, options)
282+
{
283+
}
284+
}
275285
#endif // NET
276286
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package net.dot.jni.test;
2+
3+
public interface JavaInterface {
4+
5+
String getValue();
6+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package net.dot.jni.test;
2+
3+
public class MyJavaInterfaceImpl
4+
implements JavaInterface
5+
{
6+
public String getValue() {
7+
return "Hello from Java!";
8+
}
9+
}

0 commit comments

Comments
 (0)