-
Notifications
You must be signed in to change notification settings - Fork 538
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
Comments
I like this idea, and we should do it in dotnet/java-interop as an extension method on |
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 public T? AsJavaCast<T> (this IJavaPeerable? instance) where T : class, IJavaPeerable |
@jpobst: I agree with your thoughts on the "Try" pattern; We could alternatively do Usage-wise, I think bool-returning 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 a.JavaAs<B>()?.SomeMethodSpecificToB();
if (a.JavaAs<B>() is B b) { // … but why?!
} Maybe both We can also debate |
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
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
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 theIAnimatable
interface. In debug builds this works because theAnimationDrawable
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: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?
Relevant log output
No response
The text was updated successfully, but these errors were encountered: