Description
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()
?