diff --git a/src/Java.Interop/Documentation/Java.Interop/JavaPeerableExtensions.xml b/src/Java.Interop/Documentation/Java.Interop/JavaPeerableExtensions.xml new file mode 100644 index 000000000..49d0c854c --- /dev/null +++ b/src/Java.Interop/Documentation/Java.Interop/JavaPeerableExtensions.xml @@ -0,0 +1,121 @@ + + + + + Extension methods on . + + + + + Gets the JNI name of the type of the instance . + + The instance + to get the JNI type name of. + + + + 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 java.lang.Object, the JNI name + is java/lang/Object. + + + + + + The type to coerce to. + + + A instance + to coerce to type . + + + When this method returns, contains a value of type + if can be + coerced to the Java type corresponding to , + or null if the coercion is not valid. + + + Try to coerce to type , + checking that the coercion is valid on the Java side. + + + if was converted successfully; + otherwise, . + + + + Implementations of consist + of two halves: a Java peer and a managed peer. + The property + associates the managed peer to the Java peer. + + + The or + custom attributes are + used to associated a managed type to a Java type. + + + + + The Java peer type for could not be found. + + + + + The type or a Invoker type for + does not provide an + activation constructor, a constructor with a singature of + (ref JniObjectReference, JniObjectReferenceOptions) or + (IntPtr, JniHandleOwnership). + + + + + + + The type to coerce to. + + + A instance + to coerce to type . + + + Try to coerce to type , + checking that the coercion is valid on the Java side. + + + A value of type if the Java peer to + can be coerced to the Java type corresponding + to ; otherwise, null. + + + + Implementations of consist + of two halves: a Java peer and a managed peer. + The property + associates the managed peer to the Java peer. + + + The or + custom attributes are + used to associated a managed type to a Java type. + + + + + The Java peer type for could not be found. + + + + + The type or a Invoker type for + does not provide an + activation constructor, a constructor with a singature of + (ref JniObjectReference, JniObjectReferenceOptions) or + (IntPtr, JniHandleOwnership). + + + + + diff --git a/src/Java.Interop/Java.Interop/JavaObject.cs b/src/Java.Interop/Java.Interop/JavaObject.cs index df666821d..111ec735a 100644 --- a/src/Java.Interop/Java.Interop/JavaObject.cs +++ b/src/Java.Interop/Java.Interop/JavaObject.cs @@ -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)); diff --git a/src/Java.Interop/Java.Interop/JavaPeerableExtensions.cs b/src/Java.Interop/Java.Interop/JavaPeerableExtensions.cs index e6e7e1691..8bb9dbb55 100644 --- a/src/Java.Interop/Java.Interop/JavaPeerableExtensions.cs +++ b/src/Java.Interop/Java.Interop/JavaPeerableExtensions.cs @@ -1,15 +1,51 @@ #nullable enable using System; +using System.Diagnostics.CodeAnalysis; namespace Java.Interop { + /// public static class JavaPeerableExtensions { + /// public static string? GetJniTypeName (this IJavaPeerable self) { JniPeerMembers.AssertSelf (self); return JniEnvironment.Types.GetJniTypeNameFromInstance (self.PeerReference); } + + /// + public static bool TryJavaCast< + [DynamicallyAccessedMembers (JavaObject.Constructors)] + TResult + > (this IJavaPeerable? self, [NotNullWhen (true)] out TResult? result) + where TResult : class, IJavaPeerable + { + result = JavaAs (self); + return result != null; + } + + /// + 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; + } } } diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs index 263bbdf08..88f973ec0 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs @@ -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, @@ -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; diff --git a/src/Java.Interop/Java.Interop/ManagedPeer.cs b/src/Java.Interop/Java.Interop/ManagedPeer.cs index 6a9834954..11997ba2e 100644 --- a/src/Java.Interop/Java.Interop/ManagedPeer.cs +++ b/src/Java.Interop/Java.Interop/ManagedPeer.cs @@ -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; diff --git a/src/Java.Interop/PublicAPI.Unshipped.txt b/src/Java.Interop/PublicAPI.Unshipped.txt index 7dc5c5811..3d4565551 100644 --- a/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/src/Java.Interop/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +static Java.Interop.JavaPeerableExtensions.TryJavaCast(this Java.Interop.IJavaPeerable? self, out TResult? result) -> bool +static Java.Interop.JavaPeerableExtensions.JavaAs(this Java.Interop.IJavaPeerable? self) -> TResult? diff --git a/tests/Java.Interop-Tests/Java.Interop-Tests.csproj b/tests/Java.Interop-Tests/Java.Interop-Tests.csproj index 51fa6e6df..0677be887 100644 --- a/tests/Java.Interop-Tests/Java.Interop-Tests.csproj +++ b/tests/Java.Interop-Tests/Java.Interop-Tests.csproj @@ -41,6 +41,8 @@ + + diff --git a/tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs b/tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs new file mode 100644 index 000000000..33df401c6 --- /dev/null +++ b/tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs @@ -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(() => v.JavaAs()); + + var r = v.PeerReference; + using var o = new JavaObject (ref r, JniObjectReferenceOptions.Copy); + // MyJavaInterfaceImpl doesn't provide an activation constructor + Assert.Throws(() => o.JavaAs()); +#if !__ANDROID__ + // JavaObjectWithNoJavaPeer has no Java peer + Assert.Throws(() => v.JavaAs()); +#endif // !__ANDROID__ + } + + [Test] + public void JavaAs_NullSelfReturnsNull () + { + Assert.AreEqual (null, JavaPeerableExtensions.JavaAs (null)); + } + + public void JavaAs_InvalidPeerRefReturnsNull () + { + var v = new MyJavaInterfaceImpl (); + v.Dispose (); + Assert.AreEqual (null, JavaPeerableExtensions.JavaAs (v)); + } + + [Test] + public void JavaAs_InstanceThatDoesNotImplementInterfaceReturnsNull () + { + using var v = new MyJavaInterfaceImpl (); + Assert.AreEqual (null, JavaPeerableExtensions.JavaAs (v)); + } + + [Test] + public void JavaAs () + { + using var impl = new MyJavaInterfaceImpl (); + using var iface = impl.JavaAs (); + 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); + } + } +} diff --git a/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs b/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs index bbb8c0c5e..ee3413795 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs @@ -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 () diff --git a/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs b/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs index f225f86bc..ab40dc794 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs @@ -264,7 +264,7 @@ public override unsafe int hashCode () interface IAndroidInterface : IJavaPeerable { internal const string JniTypeName = "net/dot/jni/test/AndroidInterface"; - private static JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (IAndroidInterface), isInterface: true); + internal static JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (IAndroidInterface), isInterface: true); public static unsafe string getClassName () { @@ -272,5 +272,16 @@ public static unsafe string getClassName () return JniEnvironment.Strings.ToString (ref s, JniObjectReferenceOptions.CopyAndDispose); } } + + [JniTypeSignature (IAndroidInterface.JniTypeName, GenerateJavaPeer=false)] + internal class IAndroidInterfaceInvoker : JavaObject, IAndroidInterface { + + public override JniPeerMembers JniPeerMembers => IAndroidInterface._members; + + public IAndroidInterfaceInvoker (ref JniObjectReference reference, JniObjectReferenceOptions options) + : base (ref reference, options) + { + } + } #endif // NET } diff --git a/tests/Java.Interop-Tests/java/net/dot/jni/test/JavaInterface.java b/tests/Java.Interop-Tests/java/net/dot/jni/test/JavaInterface.java new file mode 100644 index 000000000..c1e55895a --- /dev/null +++ b/tests/Java.Interop-Tests/java/net/dot/jni/test/JavaInterface.java @@ -0,0 +1,6 @@ +package net.dot.jni.test; + +public interface JavaInterface { + + String getValue(); +} diff --git a/tests/Java.Interop-Tests/java/net/dot/jni/test/MyJavaInterfaceImpl.java b/tests/Java.Interop-Tests/java/net/dot/jni/test/MyJavaInterfaceImpl.java new file mode 100644 index 000000000..3667fa0b7 --- /dev/null +++ b/tests/Java.Interop-Tests/java/net/dot/jni/test/MyJavaInterfaceImpl.java @@ -0,0 +1,13 @@ +package net.dot.jni.test; + +public class MyJavaInterfaceImpl + implements JavaInterface, Cloneable +{ + public String getValue() { + return "Hello from Java!"; + } + + public Object clone() { + return this; + } +}