Skip to content

Commit

Permalink
[Java.Interop] Add IJavaPeerable.JavaAs() extension method (#1234)
Browse files Browse the repository at this point in the history
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
jonpryor committed Jul 8, 2024
1 parent 6f9defa commit 7a058c0
Show file tree
Hide file tree
Showing 12 changed files with 343 additions and 7 deletions.
121 changes: 121 additions & 0 deletions src/Java.Interop/Documentation/Java.Interop/JavaPeerableExtensions.xml
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>
3 changes: 2 additions & 1 deletion src/Java.Interop/Java.Interop/JavaObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ namespace Java.Interop
[JniTypeSignature ("java/lang/Object", GenerateJavaPeer=false)]
unsafe public class JavaObject : IJavaPeerable
{
internal const DynamicallyAccessedMemberTypes ConstructorsAndInterfaces = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.Interfaces;
internal const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;
internal const DynamicallyAccessedMemberTypes ConstructorsAndInterfaces = Constructors | DynamicallyAccessedMemberTypes.Interfaces;

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

Expand Down
36 changes: 36 additions & 0 deletions src/Java.Interop/Java.Interop/JavaPeerableExtensions.cs
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;
}
}
}
36 changes: 32 additions & 4 deletions src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,16 +276,45 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type)
if (disposed)
throw new ObjectDisposedException (GetType ().Name);

if (!reference.IsValid) {
return null;
}

targetType = targetType ?? typeof (JavaObject);
targetType = GetPeerType (targetType);

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

var ctor = GetPeerConstructor (reference, targetType);
if (ctor == null)
var targetSig = Runtime.TypeManager.GetTypeSignature (targetType);
if (!targetSig.IsValid || targetSig.SimpleReference == null) {
throw new ArgumentException ($"Could not determine Java type corresponding to `{targetType.AssemblyQualifiedName}`.", nameof (targetType));
}

var refClass = JniEnvironment.Types.GetObjectClass (reference);
JniObjectReference targetClass;
try {
targetClass = JniEnvironment.Types.FindClass (targetSig.SimpleReference);
} catch (Exception e) {
JniObjectReference.Dispose (ref refClass);
throw new ArgumentException ($"Could not find Java class `{targetSig.SimpleReference}`.",
nameof (targetType),
e);
}

if (!JniEnvironment.Types.IsAssignableFrom (refClass, targetClass)) {
JniObjectReference.Dispose (ref refClass);
JniObjectReference.Dispose (ref targetClass);
return null;
}

JniObjectReference.Dispose (ref targetClass);

var ctor = GetPeerConstructor (ref refClass, targetType);
if (ctor == null) {
throw new NotSupportedException (string.Format ("Could not find an appropriate constructable wrapper type for Java type '{0}', targetType='{1}'.",
JniEnvironment.Types.GetJniTypeNameFromInstance (reference), targetType));
}

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

ConstructorInfo? GetPeerConstructor (
JniObjectReference instance,
ref JniObjectReference klass,
[DynamicallyAccessedMembers (Constructors)]
Type fallbackType)
{
var klass = JniEnvironment.Types.GetObjectClass (instance);
var jniTypeName = JniEnvironment.Types.GetJniTypeNameFromClass (klass);

Type? type = null;
Expand Down
1 change: 0 additions & 1 deletion src/Java.Interop/Java.Interop/ManagedPeer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ namespace Java.Interop {
/* static */ sealed class ManagedPeer : JavaObject {

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


Expand Down
2 changes: 2 additions & 0 deletions src/Java.Interop/PublicAPI.Unshipped.txt
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?
2 changes: 2 additions & 0 deletions tests/Java.Interop-Tests/Java.Interop-Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\CallVirtualFromConstructorBase.java" />
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\CallVirtualFromConstructorDerived.java" />
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\GetThis.java" />
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\JavaInterface.java" />
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\MyJavaInterfaceImpl.java" />
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\ObjectHelper.java" />
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\RenameClassBase1.java" />
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\RenameClassBase2.java" />
Expand Down
114 changes: 114 additions & 0 deletions tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs
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);
}
}
}
3 changes: 3 additions & 0 deletions tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ class JavaVMFixtureTypeManager : JniRuntime.JniTypeManager {
[CallVirtualFromConstructorDerived.JniTypeName] = typeof (CallVirtualFromConstructorDerived),
[CrossReferenceBridge.JniTypeName] = typeof (CrossReferenceBridge),
[GetThis.JniTypeName] = typeof (GetThis),
[IAndroidInterface.JniTypeName] = typeof (IAndroidInterface),
[IJavaInterface.JniTypeName] = typeof (IJavaInterface),
[JavaDisposedObject.JniTypeName] = typeof (JavaDisposedObject),
[JavaObjectWithMissingJavaPeer.JniTypeName] = typeof (JavaObjectWithMissingJavaPeer),
[MyDisposableObject.JniTypeName] = typeof (JavaDisposedObject),
[MyJavaInterfaceImpl.JniTypeName] = typeof (MyJavaInterfaceImpl),
};

public JavaVMFixtureTypeManager ()
Expand Down
Loading

0 comments on commit 7a058c0

Please sign in to comment.