From 7a058c0ee50d39dc5783d2b2770ea5e0f1001a56 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Mon, 8 Jul 2024 13:49:12 -0400 Subject: [PATCH] [Java.Interop] Add `IJavaPeerable.JavaAs()` extension method (#1234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/dotnet/java-interop/issues/10 Fixes: https://github.com/dotnet/android/issues/9038 Context: 1adb7964a2033c83c298c070f2d1ab896d92671b 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. 1adb7964), 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()` extension method][1] comes in: we can use `.JavaCast()` 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(); animatable.Start(); The problem with `.JavaCast()` is that it always throws on failure: var someOtherIface = drawable.JavaCast(); // 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( this IJavaPeerable self); public static bool TryJavaCast( this IJavaPeerable self, out TResult? result); } The `.JavaAs()` 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()?.Start(); The `.TryJavaCast()` 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(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 --- .../Java.Interop/JavaPeerableExtensions.xml | 121 ++++++++++++++++++ src/Java.Interop/Java.Interop/JavaObject.cs | 3 +- .../Java.Interop/JavaPeerableExtensions.cs | 36 ++++++ .../JniRuntime.JniValueManager.cs | 36 +++++- src/Java.Interop/Java.Interop/ManagedPeer.cs | 1 - src/Java.Interop/PublicAPI.Unshipped.txt | 2 + .../Java.Interop-Tests.csproj | 2 + .../JavaPeerableExtensionsTests.cs | 114 +++++++++++++++++ .../Java.Interop/JavaVMFixture.cs | 3 + .../Java.Interop/JniPeerMembersTests.cs | 13 +- .../java/net/dot/jni/test/JavaInterface.java | 6 + .../net/dot/jni/test/MyJavaInterfaceImpl.java | 13 ++ 12 files changed, 343 insertions(+), 7 deletions(-) create mode 100644 src/Java.Interop/Documentation/Java.Interop/JavaPeerableExtensions.xml create mode 100644 tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs create mode 100644 tests/Java.Interop-Tests/java/net/dot/jni/test/JavaInterface.java create mode 100644 tests/Java.Interop-Tests/java/net/dot/jni/test/MyJavaInterfaceImpl.java 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; + } +}