dotnet / java-interop

Java.Interop provides open-source bindings of Java's Java Native Interface (JNI) for use with .NET managed languages such as C#
Other
189 stars 48 forks source link

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

Closed jonpryor closed 1 week ago

jonpryor commented 2 weeks ago

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);

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(), 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. 1adb7964), 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 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();
}
jonpryor commented 2 weeks ago

Will remain a Draft until we create & review a corresponding dotnet/android PR for integration testing…

AmrAlSayed0 commented 2 weeks ago

I guess this PR fixes this issue #10 too