Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Java.Interop] Add .JavaAs() extension method #1234

Merged
merged 7 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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<
jonpryor marked this conversation as resolved.
Show resolved Hide resolved
[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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though this check being first means I'm fibbing about "always checking for fundamental errors"; if the value were always null, for whatever reason, we'd never get around to "sanity checking" whether it's even possible. 🤔

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));
}
Comment on lines +45 to +50
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a test that tries to cast to a random, incorrect type like System.Guid? (One that isn't even a Java thing) Would that case throw?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the case you are suggesting is compiler prevented by the generic constraint?

public static TResult? JavaAs<TResult> (this IJavaPeerable? self)
    where TResult : class, IJavaPeerable
{ ... }

Or perhaps I misunderstand your comment.


[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
Loading