-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[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
- Loading branch information
Showing
12 changed files
with
343 additions
and
7 deletions.
There are no files selected for viewing
121 changes: 121 additions & 0 deletions
121
src/Java.Interop/Documentation/Java.Interop/JavaPeerableExtensions.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
<?xml version="1.0"?> | ||
<docs> | ||
<member name="T:JavaPeerableExtensions"> | ||
<summary> | ||
Extension methods on <see cref="T:Java.Interop.IJavaPeerable" />. | ||
</summary> | ||
<remarks /> | ||
</member> | ||
<member name="M:GetJniTypeName"> | ||
<summary>Gets the JNI name of the type of the instance <paramref name="self" />.</summary> | ||
<param name="self"> | ||
The <see cref="T:Java.Interop.IJavaPeerable" /> instance | ||
to get the JNI type name of. | ||
</param> | ||
<remarks> | ||
<para> | ||
The JNI type name is the name of the Java type, as it would be | ||
used in Java Native Interface (JNI) API calls. For example, | ||
instead of the Java name <c>java.lang.Object</c>, the JNI name | ||
is <c>java/lang/Object</c>. | ||
</para> | ||
</remarks> | ||
</member> | ||
<member name="M:TryJavaCast"> | ||
<typeparam name="TResult"> | ||
The type to coerce <paramref name="self" /> to. | ||
</typeparam> | ||
<param name="self"> | ||
A <see cref="T:Java.Interop.IJavaPeerable" /> instance | ||
to coerce to type <typeparamref name="TResult" />. | ||
</param> | ||
<param name="result"> | ||
When this method returns, contains a value of type | ||
<typeparamref name="TResult" /> if <paramref name="self" /> can be | ||
coerced to the Java type corresponding to <typeparamref name="TResult" />, | ||
or <c>null</c> if the coercion is not valid. | ||
</param> | ||
<summary> | ||
Try to coerce <paramref name="self" /> to type <typeparamref name="TResult" />, | ||
checking that the coercion is valid on the Java side. | ||
</summary> | ||
<returns> | ||
<see langword="true" /> if <pramref name="self" /> was converted successfully; | ||
otherwise, <see langword="false" />. | ||
</returns> | ||
<remarks> | ||
<block subset="none" type="note"> | ||
Implementations of <see cref="T:Java.Interop.IJavaPeerable" /> consist | ||
of two halves: a <i>Java peer</i> and a <i>managed peer</i>. | ||
The <see cref="P:Java.Interop.IJavaPeerable.PeerReference" /> property | ||
associates the managed peer to the Java peer. | ||
</block> | ||
<block subset="none" type="note"> | ||
The <see cref="T:Java.Interop.JniTypeSignatureAttribute" /> or | ||
<see cref="T:Android.Runtime.RegisterAttribute" /> custom attributes are | ||
used to associated a managed type to a Java type. | ||
</block> | ||
</remarks> | ||
<exception cref="T:System.ArgumentException"> | ||
<para> | ||
The Java peer type for <typeparamref name="TResult" /> could not be found. | ||
</para> | ||
</exception> | ||
<exception cref="T:System.NotSupportedException"> | ||
<para> | ||
The type <typeparamref name="TResult" /> or a <i>Invoker type</i> for | ||
<typeparamref name="TResult" /> does not provide an | ||
<i>activation constructor</i>, a constructor with a singature of | ||
<c>(ref JniObjectReference, JniObjectReferenceOptions)</c> or | ||
<c>(IntPtr, JniHandleOwnership)</c>. | ||
</para> | ||
</exception> | ||
<seealso cref="M:Java.Interop.JavaPeerableExtensions.JavaAs``1(Java.Interop.IJavaPeerable)" /> | ||
</member> | ||
<member name="M:JavaAs"> | ||
<typeparam name="TResult"> | ||
The type to coerce <paramref name="self" /> to. | ||
</typeparam> | ||
<param name="self"> | ||
A <see cref="T:Java.Interop.IJavaPeerable" /> instance | ||
to coerce to type <typeparamref name="TResult" />. | ||
</param> | ||
<summary> | ||
Try to coerce <paramref name="self" /> to type <typeparamref name="TResult" />, | ||
checking that the coercion is valid on the Java side. | ||
</summary> | ||
<returns> | ||
A value of type <typeparamref name="TResult" /> if the Java peer to | ||
<paramref name="self" /> can be coerced to the Java type corresponding | ||
to <typeparamref name="TResult" />; otherwise, <c>null</c>. | ||
</returns> | ||
<remarks> | ||
<block subset="none" type="note"> | ||
Implementations of <see cref="T:Java.Interop.IJavaPeerable" /> consist | ||
of two halves: a <i>Java peer</i> and a <i>managed peer</i>. | ||
The <see cref="P:Java.Interop.IJavaPeerable.PeerReference" /> property | ||
associates the managed peer to the Java peer. | ||
</block> | ||
<block subset="none" type="note"> | ||
The <see cref="T:Java.Interop.JniTypeSignatureAttribute" /> or | ||
<see cref="T:Android.Runtime.RegisterAttribute" /> custom attributes are | ||
used to associated a managed type to a Java type. | ||
</block> | ||
</remarks> | ||
<exception cref="T:System.ArgumentException"> | ||
<para> | ||
The Java peer type for <typeparamref name="TResult" /> could not be found. | ||
</para> | ||
</exception> | ||
<exception cref="T:System.NotSupportedException"> | ||
<para> | ||
The type <typeparamref name="TResult" /> or a <i>Invoker type</i> for | ||
<typeparamref name="TResult" /> does not provide an | ||
<i>activation constructor</i>, a constructor with a singature of | ||
<c>(ref JniObjectReference, JniObjectReferenceOptions)</c> or | ||
<c>(IntPtr, JniHandleOwnership)</c>. | ||
</para> | ||
</exception> | ||
<seealso cref="P:Java.Interop.JavaPeerableExtensions.TryJavaCast``1(Java.Interop.IJavaPeerable)" /> | ||
</member> | ||
</docs> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,51 @@ | ||
#nullable enable | ||
|
||
using System; | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace Java.Interop { | ||
|
||
/// <include file="../Documentation/Java.Interop/JavaPeerableExtensions.xml" path="/docs/member[@name='T:JavaPeerableExtensions']/*" /> | ||
public static class JavaPeerableExtensions { | ||
|
||
/// <include file="../Documentation/Java.Interop/JavaPeerableExtensions.xml" path="/docs/member[@name='M:GetJniTypeName']/*" /> | ||
public static string? GetJniTypeName (this IJavaPeerable self) | ||
{ | ||
JniPeerMembers.AssertSelf (self); | ||
return JniEnvironment.Types.GetJniTypeNameFromInstance (self.PeerReference); | ||
} | ||
|
||
/// <include file="../Documentation/Java.Interop/JavaPeerableExtensions.xml" path="/docs/member[@name='M:TryJavaCast']/*" /> | ||
public static bool TryJavaCast< | ||
[DynamicallyAccessedMembers (JavaObject.Constructors)] | ||
TResult | ||
> (this IJavaPeerable? self, [NotNullWhen (true)] out TResult? result) | ||
where TResult : class, IJavaPeerable | ||
{ | ||
result = JavaAs<TResult> (self); | ||
return result != null; | ||
} | ||
|
||
/// <include file="../Documentation/Java.Interop/JavaPeerableExtensions.xml" path="/docs/member[@name='M:JavaAs']/*" /> | ||
public static TResult? JavaAs< | ||
[DynamicallyAccessedMembers (JavaObject.Constructors)] | ||
TResult | ||
> (this IJavaPeerable? self) | ||
where TResult : class, IJavaPeerable | ||
{ | ||
if (self == null || !self.PeerReference.IsValid) { | ||
return null; | ||
} | ||
|
||
if (self is TResult result) { | ||
return result; | ||
} | ||
|
||
var r = self.PeerReference; | ||
return JniEnvironment.Runtime.ValueManager.CreatePeer ( | ||
ref r, JniObjectReferenceOptions.Copy, | ||
targetType: typeof (TResult)) | ||
as TResult; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
#nullable enable | ||
static Java.Interop.JavaPeerableExtensions.TryJavaCast<TResult>(this Java.Interop.IJavaPeerable? self, out TResult? result) -> bool | ||
static Java.Interop.JavaPeerableExtensions.JavaAs<TResult>(this Java.Interop.IJavaPeerable? self) -> TResult? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
114 changes: 114 additions & 0 deletions
114
tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
using Java.Interop; | ||
|
||
using NUnit.Framework; | ||
|
||
namespace Java.InteropTests; | ||
|
||
[TestFixture] | ||
public class JavaPeerableExtensionsTests { | ||
|
||
[Test] | ||
public void JavaAs_Exceptions () | ||
{ | ||
using var v = new MyJavaInterfaceImpl (); | ||
|
||
// The Java type corresponding to JavaObjectWithMissingJavaPeer doesn't exist | ||
Assert.Throws<ArgumentException>(() => v.JavaAs<JavaObjectWithMissingJavaPeer>()); | ||
|
||
var r = v.PeerReference; | ||
using var o = new JavaObject (ref r, JniObjectReferenceOptions.Copy); | ||
// MyJavaInterfaceImpl doesn't provide an activation constructor | ||
Assert.Throws<NotSupportedException>(() => o.JavaAs<MyJavaInterfaceImpl>()); | ||
#if !__ANDROID__ | ||
// JavaObjectWithNoJavaPeer has no Java peer | ||
Assert.Throws<ArgumentException>(() => v.JavaAs<JavaObjectWithNoJavaPeer>()); | ||
#endif // !__ANDROID__ | ||
} | ||
|
||
[Test] | ||
public void JavaAs_NullSelfReturnsNull () | ||
{ | ||
Assert.AreEqual (null, JavaPeerableExtensions.JavaAs<IAndroidInterface> (null)); | ||
} | ||
|
||
public void JavaAs_InvalidPeerRefReturnsNull () | ||
{ | ||
var v = new MyJavaInterfaceImpl (); | ||
v.Dispose (); | ||
Assert.AreEqual (null, JavaPeerableExtensions.JavaAs<IJavaInterface> (v)); | ||
} | ||
|
||
[Test] | ||
public void JavaAs_InstanceThatDoesNotImplementInterfaceReturnsNull () | ||
{ | ||
using var v = new MyJavaInterfaceImpl (); | ||
Assert.AreEqual (null, JavaPeerableExtensions.JavaAs<IAndroidInterface> (v)); | ||
} | ||
|
||
[Test] | ||
public void JavaAs () | ||
{ | ||
using var impl = new MyJavaInterfaceImpl (); | ||
using var iface = impl.JavaAs<IJavaInterface> (); | ||
Assert.IsNotNull (iface); | ||
Assert.AreEqual ("Hello from Java!", iface.Value); | ||
} | ||
} | ||
|
||
// Note: Java side implements JavaInterface, while managed binding DOES NOT. | ||
[JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] | ||
public class MyJavaInterfaceImpl : JavaObject { | ||
internal const string JniTypeName = "net/dot/jni/test/MyJavaInterfaceImpl"; | ||
|
||
internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (MyJavaInterfaceImpl)); | ||
|
||
public override JniPeerMembers JniPeerMembers { | ||
get {return _members;} | ||
} | ||
|
||
public unsafe MyJavaInterfaceImpl () | ||
: base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None) | ||
{ | ||
const string id = "()V"; | ||
var peer = _members.InstanceMethods.StartCreateInstance (id, GetType (), null); | ||
Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose); | ||
_members.InstanceMethods.FinishCreateInstance (id, this, null); | ||
} | ||
} | ||
|
||
[JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] | ||
interface IJavaInterface : IJavaPeerable { | ||
internal const string JniTypeName = "net/dot/jni/test/JavaInterface"; | ||
|
||
public string Value { | ||
[JniMethodSignatureAttribute("getValue", "()Ljava/lang/String;")] | ||
get; | ||
} | ||
} | ||
|
||
[JniTypeSignature (IJavaInterface.JniTypeName, GenerateJavaPeer=false)] | ||
internal class IJavaInterfaceInvoker : JavaObject, IJavaInterface { | ||
|
||
internal static readonly JniPeerMembers _members = new JniPeerMembers (IJavaInterface.JniTypeName, typeof (IJavaInterfaceInvoker)); | ||
|
||
public override JniPeerMembers JniPeerMembers { | ||
get {return _members;} | ||
} | ||
|
||
public IJavaInterfaceInvoker (ref JniObjectReference reference, JniObjectReferenceOptions options) | ||
: base (ref reference, options) | ||
{ | ||
} | ||
|
||
public unsafe string Value { | ||
get { | ||
const string id = "getValue.()Ljava/lang/String;"; | ||
var r = JniPeerMembers.InstanceMethods.InvokeVirtualObjectMethod (id, this, null); | ||
return JniEnvironment.Strings.ToString (ref r, JniObjectReferenceOptions.CopyAndDispose); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.