-
Notifications
You must be signed in to change notification settings - Fork 58
[Java.Interop] Add .JavaAs()
extension method
#1234
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
Changes from all commits
27236f2
5ac2e33
647a22b
31045b8
2069911
3bfdc2d
18329a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> |
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; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -276,16 +276,45 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type) | |
if (disposed) | ||
throw new ObjectDisposedException (GetType ().Name); | ||
|
||
if (!reference.IsValid) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
@@ -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; | ||
|
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? |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
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); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.