Skip to content

Investigation: Bind Java Generics As C# Generics? #918

Open
@jonpryor

Description

@jonpryor

Java generics are based upon type erasure, in which all generic type parameters are "erased" with a corresponding "raw" type. Consider:

package java.util;
public /* partial */ class ArrayList<E> {
    public boolean add(E element) {/* ... */ }
}

When the above is compiled, Java bytecode specifies an ArrayList.add(java.lang.Object), as seen with javap -s java.util.ArrayList:

public class java.util.ArrayList<E> …
  …
  public boolean add(E);
    descriptor: (Ljava/lang/Object;)Z

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:

#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 : Java.Lang.Object {
        [Android.Runtime.Register ("add", "(Ljava/lang/Object;)Z", "GetAdd_Handler")]
        public virtual bool Add(Java.Lang.Object 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>(native__this);
            var element = Java.Lang.Object.GetObject<Java.Lang.Object>(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) {
                    // interface invoker case
                    callbackDeclaringType = Type.GetType (toks [3], throwOnError: true)!;
                }
                #if false
                // Added for .NET 6 compat in xamarin/xamarin-android@80e83ec804 ; ignore
                while (callbackDeclaringType.ContainsGenericParameters) {
                    callbackDeclaringType = callbackDeclaringType.BaseType!;
                }
                #endif  // false
                Func<Delegate> connector = (Func<Delegate>) Delegate.CreateDelegate (typeof (Func<Delegate>),
                    callbackDeclaringType, toks [2]);
                callback = connector ();
            }
        }
    }
}

class MyList : Java.Util.ArrayList {
    public override bool Add (Java.Lang.Object 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);
    }
}

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 the Get…Handler method, and pass off the delegate instance returned by ArrayList.GetAdd_Handler() to JNIEnv::RegisterNatives() (not shown here). There is thus an implicit constraint that we be able to lookup the Get…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 becomes ArrayList<T>, and nothing else changes?

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

Then we enter some "interesting" interactions: if MyList is non-generic:

class MyList : Java.Util.ArrayList<string> {
    public override bool Add (string element) => true;
}

then the app still works without error.

If we make MyList generic:

class MyList<T> : Java.Util.ArrayList<T> {
    public override bool Add (T element) => true;
}

then we need to update our registration line. If our registration is "reified":

Android.Runtime.JNIEnv.RegisterJniNatives (typeof (MyList<string>), MyList_members);

then everything is fine.

If our registration isn't reified:

Android.Runtime.JNIEnv.RegisterJniNatives (typeof (MyList<>), MyList_members);

Then it fails:

Unhandled exception. System.ArgumentException: Late bound operations cannot be performed on types or methods for which ContainsGenericParameters is true. (Parameter 'target')
   at System.Delegate.CreateDelegate(Type type, Type target, String method, Boolean ignoreCase, Boolean throwOnBindFailure)
   at System.Delegate.CreateDelegate(Type type, Type target, String method)
   at Android.Runtime.JNIEnv.RegisterJniNatives(Type type, String methods) in …/Program.cs:line 79
   at App.Main() in …/Program.cs:line 97

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:

partial class MyRunnable<T> : Java.Lang.Object, Java.Lang.IRunnable {
    public void Run() {}
}

When built, we'll create a Java Callable Wrapper for MyRunnable, which has a static constructor which will register the type:

public /* partial */ class MyRunnable_1 /* … */
{
/** @hide */
	public static final String __md_methods;
	static {
		__md_methods = 
			"n_run:()V:GetRunHandler:Java.Lang.IRunnableInvoker, Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\n" +
			"";
		mono.android.Runtime.register ("MyRunnable`1, HelloWorld", MyRunnable_1.class, __md_methods);
	}
}

The mono.android.Runtime.register(…) invocation eventually hits JNIEnv.RegisterJniNatives(). Note that the assembly-qualified name MyRunnable`1, HelloWorld is a non-reified type, akin to typeof(MyRunnable<>).

MyRunnable<T> works on Xamarin.Android, with the restriction that Java code cannot create instances of MyRunnable_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()?

Metadata

Metadata

Assignees

No one assigned

    Labels

    java-interopRuntime bridge between .NET and JavaproposalIssue raised for discussion, we do not even know if the change would be desirable yet

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions