Skip to content

Commit 7a058c0

Browse files
authored
[Java.Interop] Add IJavaPeerable.JavaAs() extension method (#1234)
Fixes: #10 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); // What is the runtime type of `drawable`? The binding `Drawable.CreateFromStream()` look at the runtime type of the value returned, sees that it's of type `SomeAnimatableDrawable`, and looks 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 if 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 hierarchy is consulted. Consequently, the runtime type of `drawable` would be the `Drawable` binding; however, as `Drawable` is an `abstract` type, the runtime type will *actually* be `DrawableInvoker` (see e.g. 1adb796), akin to: // emitted by `generator`… 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 for the desired type, 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 to `IJavaPeerable`: static partial 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 the runtime type of `self` is not implicitly convertible to the Java type corresponding to `TResult`. This makes it useful for one-off invocations: drawable.JavaAs<IAnimatable>()?.Start(); The `.TryJavaCast<TResult>()` extension method follows the [`TryParse()` pattern][2], 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 [2]: https://learn.microsoft.com/dotnet/standard/design-guidelines/exceptions-and-performance#try-parse-pattern
1 parent 6f9defa commit 7a058c0

12 files changed

+343
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?xml version="1.0"?>
2+
<docs>
3+
<member name="T:JavaPeerableExtensions">
4+
<summary>
5+
Extension methods on <see cref="T:Java.Interop.IJavaPeerable" />.
6+
</summary>
7+
<remarks />
8+
</member>
9+
<member name="M:GetJniTypeName">
10+
<summary>Gets the JNI name of the type of the instance <paramref name="self" />.</summary>
11+
<param name="self">
12+
The <see cref="T:Java.Interop.IJavaPeerable" /> instance
13+
to get the JNI type name of.
14+
</param>
15+
<remarks>
16+
<para>
17+
The JNI type name is the name of the Java type, as it would be
18+
used in Java Native Interface (JNI) API calls. For example,
19+
instead of the Java name <c>java.lang.Object</c>, the JNI name
20+
is <c>java/lang/Object</c>.
21+
</para>
22+
</remarks>
23+
</member>
24+
<member name="M:TryJavaCast">
25+
<typeparam name="TResult">
26+
The type to coerce <paramref name="self" /> to.
27+
</typeparam>
28+
<param name="self">
29+
A <see cref="T:Java.Interop.IJavaPeerable" /> instance
30+
to coerce to type <typeparamref name="TResult" />.
31+
</param>
32+
<param name="result">
33+
When this method returns, contains a value of type
34+
<typeparamref name="TResult" /> if <paramref name="self" /> can be
35+
coerced to the Java type corresponding to <typeparamref name="TResult" />,
36+
or <c>null</c> if the coercion is not valid.
37+
</param>
38+
<summary>
39+
Try to coerce <paramref name="self" /> to type <typeparamref name="TResult" />,
40+
checking that the coercion is valid on the Java side.
41+
</summary>
42+
<returns>
43+
<see langword="true" /> if <pramref name="self" /> was converted successfully;
44+
otherwise, <see langword="false" />.
45+
</returns>
46+
<remarks>
47+
<block subset="none" type="note">
48+
Implementations of <see cref="T:Java.Interop.IJavaPeerable" /> consist
49+
of two halves: a <i>Java peer</i> and a <i>managed peer</i>.
50+
The <see cref="P:Java.Interop.IJavaPeerable.PeerReference" /> property
51+
associates the managed peer to the Java peer.
52+
</block>
53+
<block subset="none" type="note">
54+
The <see cref="T:Java.Interop.JniTypeSignatureAttribute" /> or
55+
<see cref="T:Android.Runtime.RegisterAttribute" /> custom attributes are
56+
used to associated a managed type to a Java type.
57+
</block>
58+
</remarks>
59+
<exception cref="T:System.ArgumentException">
60+
<para>
61+
The Java peer type for <typeparamref name="TResult" /> could not be found.
62+
</para>
63+
</exception>
64+
<exception cref="T:System.NotSupportedException">
65+
<para>
66+
The type <typeparamref name="TResult" /> or a <i>Invoker type</i> for
67+
<typeparamref name="TResult" /> does not provide an
68+
<i>activation constructor</i>, a constructor with a singature of
69+
<c>(ref JniObjectReference, JniObjectReferenceOptions)</c> or
70+
<c>(IntPtr, JniHandleOwnership)</c>.
71+
</para>
72+
</exception>
73+
<seealso cref="M:Java.Interop.JavaPeerableExtensions.JavaAs``1(Java.Interop.IJavaPeerable)" />
74+
</member>
75+
<member name="M:JavaAs">
76+
<typeparam name="TResult">
77+
The type to coerce <paramref name="self" /> to.
78+
</typeparam>
79+
<param name="self">
80+
A <see cref="T:Java.Interop.IJavaPeerable" /> instance
81+
to coerce to type <typeparamref name="TResult" />.
82+
</param>
83+
<summary>
84+
Try to coerce <paramref name="self" /> to type <typeparamref name="TResult" />,
85+
checking that the coercion is valid on the Java side.
86+
</summary>
87+
<returns>
88+
A value of type <typeparamref name="TResult" /> if the Java peer to
89+
<paramref name="self" /> can be coerced to the Java type corresponding
90+
to <typeparamref name="TResult" />; otherwise, <c>null</c>.
91+
</returns>
92+
<remarks>
93+
<block subset="none" type="note">
94+
Implementations of <see cref="T:Java.Interop.IJavaPeerable" /> consist
95+
of two halves: a <i>Java peer</i> and a <i>managed peer</i>.
96+
The <see cref="P:Java.Interop.IJavaPeerable.PeerReference" /> property
97+
associates the managed peer to the Java peer.
98+
</block>
99+
<block subset="none" type="note">
100+
The <see cref="T:Java.Interop.JniTypeSignatureAttribute" /> or
101+
<see cref="T:Android.Runtime.RegisterAttribute" /> custom attributes are
102+
used to associated a managed type to a Java type.
103+
</block>
104+
</remarks>
105+
<exception cref="T:System.ArgumentException">
106+
<para>
107+
The Java peer type for <typeparamref name="TResult" /> could not be found.
108+
</para>
109+
</exception>
110+
<exception cref="T:System.NotSupportedException">
111+
<para>
112+
The type <typeparamref name="TResult" /> or a <i>Invoker type</i> for
113+
<typeparamref name="TResult" /> does not provide an
114+
<i>activation constructor</i>, a constructor with a singature of
115+
<c>(ref JniObjectReference, JniObjectReferenceOptions)</c> or
116+
<c>(IntPtr, JniHandleOwnership)</c>.
117+
</para>
118+
</exception>
119+
<seealso cref="P:Java.Interop.JavaPeerableExtensions.TryJavaCast``1(Java.Interop.IJavaPeerable)" />
120+
</member>
121+
</docs>

src/Java.Interop/Java.Interop/JavaObject.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ namespace Java.Interop
88
[JniTypeSignature ("java/lang/Object", GenerateJavaPeer=false)]
99
unsafe public class JavaObject : IJavaPeerable
1010
{
11-
internal const DynamicallyAccessedMemberTypes ConstructorsAndInterfaces = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.Interfaces;
11+
internal const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;
12+
internal const DynamicallyAccessedMemberTypes ConstructorsAndInterfaces = Constructors | DynamicallyAccessedMemberTypes.Interfaces;
1213

1314
readonly static JniPeerMembers _members = new JniPeerMembers ("java/lang/Object", typeof (JavaObject));
1415

Original file line numberDiff line numberDiff line change
@@ -1,15 +1,51 @@
11
#nullable enable
22

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

56
namespace Java.Interop {
67

8+
/// <include file="../Documentation/Java.Interop/JavaPeerableExtensions.xml" path="/docs/member[@name='T:JavaPeerableExtensions']/*" />
79
public static class JavaPeerableExtensions {
810

11+
/// <include file="../Documentation/Java.Interop/JavaPeerableExtensions.xml" path="/docs/member[@name='M:GetJniTypeName']/*" />
912
public static string? GetJniTypeName (this IJavaPeerable self)
1013
{
1114
JniPeerMembers.AssertSelf (self);
1215
return JniEnvironment.Types.GetJniTypeNameFromInstance (self.PeerReference);
1316
}
17+
18+
/// <include file="../Documentation/Java.Interop/JavaPeerableExtensions.xml" path="/docs/member[@name='M:TryJavaCast']/*" />
19+
public static bool TryJavaCast<
20+
[DynamicallyAccessedMembers (JavaObject.Constructors)]
21+
TResult
22+
> (this IJavaPeerable? self, [NotNullWhen (true)] out TResult? result)
23+
where TResult : class, IJavaPeerable
24+
{
25+
result = JavaAs<TResult> (self);
26+
return result != null;
27+
}
28+
29+
/// <include file="../Documentation/Java.Interop/JavaPeerableExtensions.xml" path="/docs/member[@name='M:JavaAs']/*" />
30+
public static TResult? JavaAs<
31+
[DynamicallyAccessedMembers (JavaObject.Constructors)]
32+
TResult
33+
> (this IJavaPeerable? self)
34+
where TResult : class, IJavaPeerable
35+
{
36+
if (self == null || !self.PeerReference.IsValid) {
37+
return null;
38+
}
39+
40+
if (self is TResult result) {
41+
return result;
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

+32-4
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;

src/Java.Interop/Java.Interop/ManagedPeer.cs

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ namespace Java.Interop {
1818
/* static */ sealed class ManagedPeer : JavaObject {
1919

2020
internal const string JniTypeName = "net/dot/jni/ManagedPeer";
21-
internal const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;
2221
internal const DynamicallyAccessedMemberTypes ConstructorsMethodsNestedTypes = Constructors | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes;
2322

2423

+2
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

+2
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" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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+
public void JavaAs_InvalidPeerRefReturnsNull ()
39+
{
40+
var v = new MyJavaInterfaceImpl ();
41+
v.Dispose ();
42+
Assert.AreEqual (null, JavaPeerableExtensions.JavaAs<IJavaInterface> (v));
43+
}
44+
45+
[Test]
46+
public void JavaAs_InstanceThatDoesNotImplementInterfaceReturnsNull ()
47+
{
48+
using var v = new MyJavaInterfaceImpl ();
49+
Assert.AreEqual (null, JavaPeerableExtensions.JavaAs<IAndroidInterface> (v));
50+
}
51+
52+
[Test]
53+
public void JavaAs ()
54+
{
55+
using var impl = new MyJavaInterfaceImpl ();
56+
using var iface = impl.JavaAs<IJavaInterface> ();
57+
Assert.IsNotNull (iface);
58+
Assert.AreEqual ("Hello from Java!", iface.Value);
59+
}
60+
}
61+
62+
// Note: Java side implements JavaInterface, while managed binding DOES NOT.
63+
[JniTypeSignature (JniTypeName, GenerateJavaPeer=false)]
64+
public class MyJavaInterfaceImpl : JavaObject {
65+
internal const string JniTypeName = "net/dot/jni/test/MyJavaInterfaceImpl";
66+
67+
internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (MyJavaInterfaceImpl));
68+
69+
public override JniPeerMembers JniPeerMembers {
70+
get {return _members;}
71+
}
72+
73+
public unsafe MyJavaInterfaceImpl ()
74+
: base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
75+
{
76+
const string id = "()V";
77+
var peer = _members.InstanceMethods.StartCreateInstance (id, GetType (), null);
78+
Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose);
79+
_members.InstanceMethods.FinishCreateInstance (id, this, null);
80+
}
81+
}
82+
83+
[JniTypeSignature (JniTypeName, GenerateJavaPeer=false)]
84+
interface IJavaInterface : IJavaPeerable {
85+
internal const string JniTypeName = "net/dot/jni/test/JavaInterface";
86+
87+
public string Value {
88+
[JniMethodSignatureAttribute("getValue", "()Ljava/lang/String;")]
89+
get;
90+
}
91+
}
92+
93+
[JniTypeSignature (IJavaInterface.JniTypeName, GenerateJavaPeer=false)]
94+
internal class IJavaInterfaceInvoker : JavaObject, IJavaInterface {
95+
96+
internal static readonly JniPeerMembers _members = new JniPeerMembers (IJavaInterface.JniTypeName, typeof (IJavaInterfaceInvoker));
97+
98+
public override JniPeerMembers JniPeerMembers {
99+
get {return _members;}
100+
}
101+
102+
public IJavaInterfaceInvoker (ref JniObjectReference reference, JniObjectReferenceOptions options)
103+
: base (ref reference, options)
104+
{
105+
}
106+
107+
public unsafe string Value {
108+
get {
109+
const string id = "getValue.()Ljava/lang/String;";
110+
var r = JniPeerMembers.InstanceMethods.InvokeVirtualObjectMethod (id, this, null);
111+
return JniEnvironment.Strings.ToString (ref r, JniObjectReferenceOptions.CopyAndDispose);
112+
}
113+
}
114+
}

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

+3
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ class JavaVMFixtureTypeManager : JniRuntime.JniTypeManager {
4141
[CallVirtualFromConstructorDerived.JniTypeName] = typeof (CallVirtualFromConstructorDerived),
4242
[CrossReferenceBridge.JniTypeName] = typeof (CrossReferenceBridge),
4343
[GetThis.JniTypeName] = typeof (GetThis),
44+
[IAndroidInterface.JniTypeName] = typeof (IAndroidInterface),
45+
[IJavaInterface.JniTypeName] = typeof (IJavaInterface),
4446
[JavaDisposedObject.JniTypeName] = typeof (JavaDisposedObject),
4547
[JavaObjectWithMissingJavaPeer.JniTypeName] = typeof (JavaObjectWithMissingJavaPeer),
4648
[MyDisposableObject.JniTypeName] = typeof (JavaDisposedObject),
49+
[MyJavaInterfaceImpl.JniTypeName] = typeof (MyJavaInterfaceImpl),
4750
};
4851

4952
public JavaVMFixtureTypeManager ()

0 commit comments

Comments
 (0)