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

Missing exception-free JavaCast overload #9038

Closed
mattleibow opened this issue Jun 18, 2024 · 3 comments · Fixed by dotnet/java-interop#1234 or #9066
Closed

Missing exception-free JavaCast overload #9038

mattleibow opened this issue Jun 18, 2024 · 3 comments · Fixed by dotnet/java-interop#1234 or #9066
Assignees
Labels
Area: Mono.Android Issues with the Android API binding (Mono.Android.dll).

Comments

@mattleibow
Copy link
Member

Android framework version

net8.0-android

Affected platform version

.NET 8

Description

When writing some code that does not have a specific .NET type, or if the linker removes the .NET type because it is not used, there is no way to check to see if the type is implementing an interface or has a specific base type.

For example, I am using Glide to load a Drawable and then I want to see if it implements the IAnimatable interface. In debug builds this works because the AnimationDrawable type happens to be in the .NET type system. When the linker runs through the code, that type is never referenced so it is removed. This causes the runtime behavior to be wrong for an if block like this:

if (myDrawableFromJava is IAnimatable)

Similarly, if I have a slim binding (or chose not to bind everything) then the .NET type will never exist at all.

The app still runs because it uses the DrawableInvoker type to do things. This invoker does not have the interface to the .NET type check fails.

To get around this issue, I want to use the JavaCast<T>() extension method, but that now throws an exception if the type is not what I am casting.

Is there an overload or alternative that will allow be to check the types on the Java side to see if an interface or base type is valid and then return the instance that is of that type?

Steps to Reproduce

This is the core issue: dotnet/maui#22874

Did you find any workaround?

public static TResult? TryJavaCast<[DynamicallyAccessedMembers (Constructors)] TResult>(this IJavaObject? instance)
	where TResult : class, IJavaObject
{
	try
	{
		return instance.JavaCast<TResult>();
	}
	catch
	{
		return null;
	}
}

Relevant log output

No response

@mattleibow mattleibow added the needs-triage Issues that need to be assigned. label Jun 18, 2024
@jpobst jpobst added Area: Mono.Android Issues with the Android API binding (Mono.Android.dll). and removed needs-triage Issues that need to be assigned. labels Jun 18, 2024
@jonpryor
Copy link
Member

I like this idea, and we should do it in dotnet/java-interop as an extension method on IJavaPeerable. :-)

@jpobst
Copy link
Contributor

jpobst commented Jun 27, 2024

This probably merits some discussion on what the API should look like. To fit the standard "Try" pattern, the API would be:

public bool TryJavaCast<T> (this IJavaPeerable? instance, out T? result) where T : class, IJavaPeerable

If we want to simply return T?, perhaps we should call it AsJavaCast to by symmetric with a C# "as" cast?

public T? AsJavaCast<T> (this IJavaPeerable? instance) where T : class, IJavaPeerable

@jonpryor
Copy link
Member

@jpobst: I agree with your thoughts on the "Try" pattern; TryJavaCast<T>() having a non-bool return type feels wrong.

We could alternatively do .JavaAs<T>() for "symmetry" with the as operator.

Usage-wise, I think bool-returning TryJavaCast() might be better?

if (a.TryJavaCast<B>(out var b)) {
    // use `b`
}

// vs
var b = a.JavaAs<B>();
if (b != null) {
    // use `b`
}

If you're going to need an if anyway, you might as well go with the bool TryJavaParse<T>() idea. However, if you want to use the null conditional operator or pattern matching, .JavaAs<T>() might be better:

a.JavaAs<B>()?.SomeMethodSpecificToB();

if (a.JavaAs<B>() is B b) { // … but why?!
}

Maybe both Try and As variants?

We can also debate .AsJavaCast<T>() vs. .JavaAs<T>(). .JavaAs<T>() came to mind earlier todays, before I had a chance to elaborate. However, .JavaAs<T>() might be "too cute".

jonpryor added a commit to dotnet/java-interop that referenced this issue Jun 28, 2024
jonpryor added a commit to dotnet/java-interop that referenced this issue Jun 29, 2024
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);

The binding `Drawable.CreateFromStream()` look at the runtime type
of the value returned, see that it's of type `SomeAnimatableDrawable`,
and look 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 when 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 hiearchy.

For the sake of discussion, assume that `drawable` will be an
instance of `DrawableInvoker` (e.g. 1adb796), akin to:

	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 the type cast, 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 on `IJavaPeerable`:

	static 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 type coercion would fail.
This makes it useful for one-off invocations:

	drawable.JavaAs<IAnimatable>()?.Start();

The `.TryJavaCast<TResult>()` extension method follows the
`TryParse()` pattern, 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
jonpryor added a commit that referenced this issue Jul 8, 2024
jonpryor added a commit that referenced this issue Jul 8, 2024
Fixes: #9038

Changes: dotnet/java-interop@ccafbe6...7a058c0

  * dotnet/java-interop@7a058c0e: [Java.Interop] Add `IJavaPeerable.JavaAs()` extension method (dotnet/java-interop#1234)
  * dotnet/java-interop@6f9defa5: Bump to dotnet/android-tools@3debf8e0 (dotnet/java-interop#1236)
  * dotnet/java-interop@6923fb89: [ci] Add dependabot branches to build triggers (dotnet/java-interop#1233)
  * dotnet/java-interop@573028f3: [ci] Use macOS 13 Ventura build agents (dotnet/java-interop#1235)

dotnet/java-interop@7a058c0e adds a new `IJavaPeerable.JavaAs<T>()`
extension method, to perform type casts which return `null` when
the cast will not succeed instead of throwing an exception.

Update `AndroidValueManager.CreatePeer()` to check for Java-side
type compatibility (by way of `TypeManager.CreateInstance()`).
Previously, this would not throw:

	var instance        = …
	var r               = instance.PeerReference;
	var wrap_instance   = JniRuntime.CurrentRuntime.ValueManager.CreatePeer (
	        reference: ref r,
	        options: JniObjectReferenceOptions.Copy,
	        targetType: typeof (SomeInterfaceThatInstanceDoesNotImplement));
	// `wrap_instance` is not null, `.CreatePeer()` does not throw

	wrap_instance.SomeMethod();
	// will throw, due to a type mismatch.

`AndroidValueManager.CreatePeer()` will now return `null` if the
Java instance referenced by the `reference:` parameter is not
convertible to `targetType`, as per [`JNIEnv::IsAssignableFrom()`][0].

[0]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#IsAssignableFrom
@github-actions github-actions bot locked and limited conversation to collaborators Aug 8, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Area: Mono.Android Issues with the Android API binding (Mono.Android.dll).
Projects
None yet
3 participants