-
Notifications
You must be signed in to change notification settings - Fork 53
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
Investigation: Bind Java Generics As C# Generics? #918
Comments
For simplicity, the "non-reified" registration demo, all at once: #nullable enable
using System;
using System.Reflection;
namespace Java.Lang {
public class Object {
public static T GetObject<T>(IntPtr value)
{
return Activator.CreateInstance<T>();
}
}
}
namespace Java.Util {
public class ArrayList<T> : Java.Lang.Object {
[Android.Runtime.Register ("add", "(Ljava/lang/Object;)Z", "GetAdd_Handler")]
public virtual bool Add(T element) => false;
static Delegate? cb_add_;
static Delegate GetAdd_Handler ()
{
if (cb_add_ == null)
cb_add_ = (Func<IntPtr, IntPtr, IntPtr, bool>) n_Add;
return cb_add_;
}
static bool n_Add (IntPtr jnienv, IntPtr native__this, IntPtr native_element)
{
var __self = Java.Lang.Object.GetObject<ArrayList<T>>(native__this);
var element = Java.Lang.Object.GetObject<T>(native_element);
return __self.Add (element);
}
}
}
namespace Android.Runtime {
[AttributeUsage (AttributeTargets.Method)]
public class RegisterAttribute : Attribute {
public RegisterAttribute(string name, string signature, string connector)
{
}
}
public static class JNIEnv {
public static void RegisterJniNatives (Type type, string methods)
{
if (string.IsNullOrEmpty (methods)) {
return;
}
string[] members = methods.Split ('\n');
for (int i = 0; i < members.Length; ++i) {
string method = members [i];
if (string.IsNullOrEmpty (method))
continue;
string[] toks = members [i].Split (new[]{':'}, 4);
Delegate callback;
if (toks [2] == "__export__") {
continue;
}
Type callbackDeclaringType = type;
if (toks.Length == 4) {
callbackDeclaringType = Type.GetType (toks [3], throwOnError: true)!;
}
Func<Delegate> connector = (Func<Delegate>) Delegate.CreateDelegate (typeof (Func<Delegate>),
callbackDeclaringType, toks [2]);
callback = connector ();
}
}
}
}
class MyList<T> : Java.Util.ArrayList<T> {
public override bool Add (T element) => true;
}
class App {
public static void Main ()
{
string MyList_members =
"add:(Ljava/lang/Object;)Z:GetAdd_Handler\n" +
"";
Android.Runtime.JNIEnv.RegisterJniNatives (typeof (MyList<>), MyList_members);
}
} |
Part of the answer is to move marshal methods outside of the declaring type, as was done with interfaces. This is kinda/sorta hinted at/formalized in Issue #795, with the namespace jnimarshalmethods.java.util {
static partial class ArrayList {
static Delegate? cb_add_;
static Delegate GetAdd_Handler () => …;
}
} We would then use the "interface-form" namespace Java.Util {
public class ArrayList<T> : Java.Lang.Object {
[Android.Runtime.Register ("add", "(Ljava/lang/Object;)Z", "GetAdd_Handler:jnimarshalmethods.java.util.ArrayList")]
public virtual bool Add(T element) {…}
}
} However, we still need to have only a single delegate instance returned from namespace jnimarshalmethods.java.util {
static partial class ArrayList {
static bool n_Add (IntPtr jnienv, IntPtr native__this, IntPtr native_element)
{
// what goes here?
}
}
} The "what goes here" is the primary question about making this work, in anything resembling a "general" case. |
…and calls the entire (There's another issue with Thus suggests that marshal methods should remain in the declaring class, so that How should More investigation required. |
Is there a benefit to having both a non-generic static marshal method AND a non-generic instance marshal method? It seems like we could use the static invoker to resolve the namespace Java.Util
{
interface IArrayListInvokerInterface
{
bool Add_Invoker (IntPtr native_element);
static Delegate? cb_add_;
static Delegate GetAdd_Handler () => cb_add_ ??= (Func<IntPtr, IntPtr, IntPtr, bool>) n_Add;
static bool n_Add (IntPtr jnienv, IntPtr native__this, IntPtr native_element)
{
var __self = Java.Lang.Object.GetObject<IArrayListInvokerInterface> (native__this);
return __self.Add_Invoker (native_element);
}
}
public class ArrayList<T> : Java.Lang.Object, IArrayListInvokerInterface
{
[Android.Runtime.Register ("add", "(Ljava/lang/Object;)Z", "GetAdd_Handler:Java.Util.IArrayListInvokerInterface")]
public virtual bool Add (T element) => false;
bool IArrayListInvokerInterface.Add_Invoker (IntPtr native_element)
{
var element = Java.Lang.Object.GetObject<T> (native_element);
return Add (element);
}
}
} Placing the static invokers in an |
I think your idea is to register the I think that could (eventually) work with "Java.Interop" bindings (#909), but not "Xamarin.Android" bindings, unless we constrain In Xamarin.Android, Or we update Xamarin.Android to similarly use The downside to this "indirection" is that you're paying for for an indirection for all Java-to-managed invocations. This could be "fine" if there were a way to avoid the indirection (sigh |
I guess the indirection is only paid for on generic types. Non-generic types could still be done with only the static marshal method. It seems likely that there will be some additional cost for generic types, no matter which route we take. |
I think I successfully implemented my suggestion as a prototype here: It uses the "constrain to JLO" method as that seemed easier to implement. It appears to be about 2-3% slower than the method we use today.
|
For completeness, I thought I'd explore a reflection based solution (with caching) to determine the performance impact. This method would not require "invoker" interfaces, by adding a single public method to public static class JavaObjectExtensions
{
delegate void JavaDelegate0 ();
delegate void JavaDelegate1 (Java.Lang.Object? p0);
delegate void JavaDelegate2 (Java.Lang.Object? p0, Java.Lang.Object? p1);
static Dictionary<int, object> method_delegates = new Dictionary<int, object> ();
public static object? Invoke (this IJavaObject peerable, int signature, string method, params Java.Lang.Object? [] args)
{
// Our cached versions have to be unique per T (List<string> is different than List<int>)
// Hash code needs to be fast, and a unique combination of type and method signature
// ie: better than this
signature = peerable.GetType ().GetHashCode () + signature;
if (args.Length == 0) {
var del = GetOrCreateDelegate<JavaDelegate0> (peerable, signature, method);
del ();
return null;
}
if (args.Length == 1) {
var del = GetOrCreateDelegate<JavaDelegate1> (peerable, signature, method);
del (args [0]);
return null;
}
if (args.Length == 2) {
var del = GetOrCreateDelegate<JavaDelegate2> (peerable, signature, method);
del (args [0], args [1]);
return null;
}
// etc.
throw new NotImplementedException ("DynamicInvoke fallback not implemented");
}
static T GetOrCreateDelegate<T> (IJavaObject peerable, int signature, string method) where T : Delegate
{
// Check for cached delegate
if (method_delegates.TryGetValue (signature, out var d))
return (T) d;
var type = peerable.GetType ();
var member = type.GetMethod (method);
if (member is null)
throw new NotImplementedException ("Could not find requested invoker methods");
var del = member.CreateDelegate<T> (peerable);
method_delegates.Add (signature, del);
return del;
}
} This can be called from any static void n_PerformanceMethod_Ljava_lang_Object_ (IntPtr jnienv, IntPtr native__this, IntPtr native_p0)
{
var __this = global::Java.Lang.Object.GetObject<IJavaObject> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
var p0 = global::Java.Lang.Object.GetObject<global::Java.Lang.Object> (native_p0, JniHandleOwnership.DoNotTransfer);
__this!.Invoke (1, "InvokePerformanceMethod", p0);
} public partial class GenericType<T> : global::Java.Lang.Object where T : global::Java.Lang.Object
{
public virtual unsafe void PerformanceMethod (T p0) { }
public void InvokePerformanceMethod (global::Java.Lang.Object obj) => PerformanceMethod (obj.JavaCast<T> ());
} The performance isn't as bad as expected, thanks to the caching. Generally less than 1 additional ms per 1K calls.
Pros:
Cons:
|
Fixes: #967 Context: dotnet/android-libraries#504 Context: #424 Context: #586 Context: #918 Java generics continues to be a "difficulty" in creating bindings. Consider [`ActivityResultContracts.RequestPermission`][0]: // Java public abstract /* partial */ class ActivityResultContract<I, O> { public abstract Intent createIntent(Context context, I input); } public /* partial */ class /* ActivityResultContracts. */ RequestPermission extends ActivityResultContract<String, Boolean> { @OverRide public Intent createIntent(Context context, String input) {…} } The JNI Signature for `ActivityResultContracts.RequestPermission.createIntent()` is `(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Intent;`, i.e. `class-parse` & `generator` believe that the `input` parameter is of type `String`, which we bind by default as `string`. Thus: // C# public abstract partial class ActivityResultContract { public abstract Intent CreateIntent (Context? context, Java.Lang.Object? input) => … } public partial class /* ActivityResultContracts. */ RequestPermission { public override Intent CreateIntent (Context? context, string? input) => … } This fails to compile with a [CS0115][1]: 'RequestPermission.CreateIntent(Context?, string?)': no suitable method found to override as the `input` parameter of `RequestPermission.CreateIntent()` changes from `Java.Lang.Object?` to `string?`. We can attempt to address this via Metadata: <attr path="/api/package[@name='androidx.activity.result.contract']/class[@name='ActivityResultContracts.RequestPermission']/method[@name='createIntent' and count(parameter)=2 and parameter[1][@type='android.content.Context'] and parameter[2][@type='java.lang.String']]" name="managedType" >Java.Lang.Object</attr> This fixes one error, as `generator` now emits: public partial class /* ActivityResultContracts. */ RequestPermission { [Register ("createIntent", "(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Intent;", "")] public override unsafe global::Android.Content.Intent CreateIntent (global::Android.Content.Context context, global::Java.Lang.Object input) { const string __id = "createIntent.(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Intent;"; IntPtr native_input = JNIEnv.NewString (input); try { JniArgumentValue* __args = stackalloc JniArgumentValue [2]; __args [0] = new JniArgumentValue ((context == null) ? IntPtr.Zero : ((global::Java.Lang.Object) context).Handle); __args [1] = new JniArgumentValue (native_input); var __rm = _members.InstanceMethods.InvokeAbstractObjectMethod (__id, this, __args); return global::Java.Lang.Object.GetObject<global::Android.Content.Intent> (__rm.Handle, JniHandleOwnership.TransferLocalRef); } finally { JNIEnv.DeleteLocalRef (native_input); global::System.GC.KeepAlive (context); global::System.GC.KeepAlive (input); } } } The `override` method declaration is correct. However, this introduces a [CS1503][2] error: IntPtr native_input = JNIEnv.NewString (input); // Error CS1503 Argument 1: cannot convert from 'Java.Lang.Object' to 'string?' The workaround is to remove `createIntent()` ~entirely, and manually bind it, as done in dotnet/android-libraries#512. Fix this issue by always emitting a cast to `(string)` as part of the `JNIEnv.NewString()` invocation, instead emitting: IntPtr native_input = JNIEnv.NewString ((string?) input); This works because `Java.Lang.Object` defines an [explicit conversion to `string?`][3], and if a `Java.Lang.String` instance is provided to the `input` parameter, it's equivalent to calling `.ToString()`. This fix allows the original suggested Metadata solution to work. [0]: https://developer.android.com/reference/androidx/activity/result/contract/ActivityResultContracts.RequestPermission [1]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs0115 [2]: https://docs.microsoft.com/en-us/dotnet/csharp/misc/cs1503 [3]: https://github.com/xamarin/xamarin-android/blob/a58d4e9706455227eabb6e5b5103b25da716688b/src/Mono.Android/Java.Lang/Object.cs#L434-L439
Java generics are based upon type erasure, in which all generic type parameters are "erased" with a corresponding "raw" type. Consider:
When the above is compiled, Java bytecode specifies an
ArrayList.add(java.lang.Object)
, as seen withjavap -s java.util.ArrayList
:The descriptor is what's important to Java.Interop, as that's what is used for JNI method lookup.
Here is an abbreviated version of what a Java.Interop binding looks like:
Notice that the Java
ArrayList<E>
type parameter is not present.The reason for this is in part a design decision that "marshal methods" be located within the declaring type. For
ArrayList
,ArrayList.n_Add()
is a marshal method, and in order to use that marshal method, we use Reflection to lookup and invoke theGet…Handler
method, and pass off the delegate instance returned byArrayList.GetAdd_Handler()
toJNIEnv::RegisterNatives()
(not shown here). There is thus an implicit constraint that we be able to lookup theGet…Handler()
method via Reflection, and invoke it.Another reason is that when invoking
JNIEnv::RegisterNatives()
, you can only register methods for a class, not for an instance. Consequently, all function pointers (delegates) must be "known" and available at class registration time.What happens if
ArrayList
becomesArrayList<T>
, and nothing else changes?Then we enter some "interesting" interactions: if
MyList
is non-generic:then the app still works without error.
If we make
MyList
generic:then we need to update our registration line. If our registration is "reified":
then everything is fine.
If our registration isn't reified:
Then it fails:
The above discussion raises the question: how does Xamarin.Android cause a non-reified generic type to be registered?
Firstly, there are few restrictions on writing generic types; this is perfectly fine:
When built, we'll create a Java Callable Wrapper for
MyRunnable
, which has a static constructor which will register the type:The
mono.android.Runtime.register(…)
invocation eventually hitsJNIEnv.RegisterJniNatives()
. Note that the assembly-qualified nameMyRunnable`1, HelloWorld
is a non-reified type, akin totypeof(MyRunnable<>)
.MyRunnable<T>
works on Xamarin.Android, with the restriction that Java code cannot create instances ofMyRunnable_1
; they can only be created by C# code. However, once created, Java code can use those instances without any problem.Thus, the question: How do we bind Java generic types as C# generic types, while staying within the limitations
JNIEnv::RegisterNatives()
?The text was updated successfully, but these errors were encountered: