Skip to content

Kotlin & method overloading & name mangling, oh my! #525

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

Closed
jonpryor opened this issue Nov 22, 2019 · 9 comments
Closed

Kotlin & method overloading & name mangling, oh my! #525

jonpryor opened this issue Nov 22, 2019 · 9 comments
Milestone

Comments

@jonpryor
Copy link
Member

Context: #505

PR #505 improves support for binding certain Kotlin constructs.

In "the perfect is the enemy of the good" manner, we will be merging PR #505 even though there's a known issue with it, in part because we don't currently know how to fix this scenario:

Method overloads and "name mangling".

In various situations, Kotlin will emit member names which are not valid Java identifiers. For example, consider this Kotlin class, which overloads the ExampleBase.foo() method to accept both Int and UInt parameters:

package example;

public open class ExampleBase {
	public open fun foo(value : Int) {
	}
	public open fun foo(value : UInt) {
	}
}

The use of a UInt parameter type causes Kotlin to "name mangle" the method name, generating a method name which is not a valid Java identifier:

$ kotlinc hello.kt
$ javap -cp . example.ExampleBase
Compiled from "hello.kt"
public class example.ExampleBase {
  public void foo(int);
  public void foo-WZ4Q5Ns(int);
  public example.ExampleBase();
}

With PR #505 we can create an API description:

<api
  api-source="class-parse">
  <package
    name="example"
    jni-name="example">
    <class
      abstract="false"
      deprecated="not deprecated"
      jni-extends="Ljava/lang/Object;"
      extends="java.lang.Object"
      extends-generic-aware="java.lang.Object"
      final="false"
      name="ExampleBase"
      jni-signature="Lexample/ExampleBase;"
      source-file-name="hello.kt"
      static="false"
      visibility="public">
      <constructor
        deprecated="not deprecated"
        final="false"
        name="ExampleBase"
        static="false"
        visibility="public"
        bridge="false"
        synthetic="false"
        jni-signature="()V" />
      <method
        abstract="false"
        deprecated="not deprecated"
        final="false"
        name="foo"
        native="false"
        return="void"
        jni-return="V"
        static="false"
        synchronized="false"
        visibility="public"
        bridge="false"
        synthetic="false"
        jni-signature="(I)V">
        <parameter
          name="value"
          type="int"
          jni-type="I" />
      </method>
      <method
        abstract="false"
        deprecated="not deprecated"
        final="false"
        name="foo-WZ4Q5Ns"
        native="false"
        return="void"
        jni-return="V"
        static="false"
        synchronized="false"
        visibility="public"
        bridge="false"
        synthetic="false"
        jni-signature="(I)V">
        <parameter
          name="value"
          type="int"
          jni-type="I" />
      </method>
    </class>
  </package>
</api>

The problem comes when we attempt to generate C# source to bind the above API description. It cannot compile, because ExampleBase.Foo(int) is emitted twice:

namespace Example {
	[global::Android.Runtime.Register ("example/ExampleBase", DoNotGenerateAcw=true)]
	public partial class ExampleBase : Java.Lang.Object {

		[Register ("foo", "(I)V", "GetFoo_IHandler")]
		public virtual unsafe void Foo (int value)
		{
			const string __id = "foo.(I)V";
			try {
				JniArgumentValue* __args = stackalloc JniArgumentValue [1];
				__args [0] = new JniArgumentValue (value);
				_members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args);
			} finally {
			}
		}

		[Register ("foo-WZ4Q5Ns", "(I)V", "GetFoo_IHandler")]
		public virtual unsafe void Foo (int value)
		{
			const string __id = "foo-WZ4Q5Ns.(I)V";
			try {
				JniArgumentValue* __args = stackalloc JniArgumentValue [1];
				__args [0] = new JniArgumentValue (value);
				_members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args);
			} finally {
			}
		}
	}
}

Attempting to compile the above will cause a CS0111 error to be generated:

src/Example.ExampleBase.cs(98,30): error CS0111: Type 'ExampleBase' already defines a member called 'Foo' with the same parameter types

This can be fixed with Metadata.xml, to "rename" one of the members. (This isn't necessarily ideal, but it's possible.)

<!-- Untested, but go with it for expository purposes -->
<attr path="/api/package[@name='example']/class[@name='ExampleBase']/method[@name='foo-WZ4Q5Ns']" name="managedName">Foo2</attr>

Were we to do so, we'll find that subclassing won't work:

class MyExample : Example.ExampleBase {
    public override void Foo2 (int value) {}
}

Subclassing cannot work, because we use Java as an intermediary, and the Java intermediary -- as generated by src/Java.Interop.Tools.JavaCallableWrappers -- uses the [Register] attribute, which will still specify foo-WZ4Q5Ns. We would thus (presumably; untested) get a Java Callable Wrapper for MyExample of:

/* partial */ class MyExample extends example.ExampleBase /* ... */ {
    public void foo-WZ4Q5Ns(int value) {
        n_foo-WZ4Q5Ns (value);
    }
    native void n_foo-WZ4Q5Ns (int value);
}

This cannot compile.

How do we address this?

@jonpryor
Copy link
Member Author

Solution 1: We could just remove them from the API binding entirely. This would give us the "same" view as Java does of a Kotlin class.

@jonpryor
Copy link
Member Author

Solution 2: We could bind them as non-virtual methods.

We cannot bind "name mangled" methods as C# virtual methods, because there's no way to make the subclass scenario work as long as we're using Java as an intermediate language.

(We could fix this by using Kotlin as the intermediate language, or by emitting Java byte code directly.)

If we bind the method as a non-virtual method, we can expose the method to C# code, while preventing the subclass scenario. This allows the Kotlin ExampleBase.foo(UInt) method to be invokable.

(We'd still need to know how to name the method so that we avoid the CS0111 errors.)

@jonpryor
Copy link
Member Author

Solution 2(b): Update class-parse & generator so that generator can "know" that the method takes a UInt. This would allow us to bind foo(UInt) as Foo(uint), which can be overloaded, thus avoiding the CS0111 error:

namespace Example {
	public partial class ExampleBase : Java.Lang.Object {
		[Register ("foo", "(I)V", "GetFoo_IHandler")]
		public virtual unsafe void Foo (int value) {...}

		[Register ("foo-WZ4Q5Ns", "(I)V", "GetFoo_IHandler")]
		public unsafe void Foo (uint value) {...}
	}
}

jonpryor pushed a commit that referenced this issue Nov 22, 2019
Context: #525

Various fixes and enhancements to allow better default Kotlin
libraries bindings:

  * Hide Kotlin-internal classes, constructors, and methods.

  * Hide implementation methods (`*-impl*`).

  * Rename extension method parameter like
    `$this$decodeBase64` to `obj`. 

  * Use Kotlin provided method parameter names instead of
    `p0`, `p1`, etc.

  * Rename any method with a `-<mangling>` to drop the invalid part,
    e.g. `add-H4uI21a` is bound as `add`.
    ("Name mangling" like this is how Kotlin ensures `add(UInt)` and
    `add(Int)` don't clash when compiled to [Java][1].)

Note that the final bullet point -- "removing" name mangling -- may
result in C# code which is either not valid or not entirely usable.
See Issue #525 for details.  This will be addressed "later".

[1]: https://kotlinlang.org/docs/reference/inline-classes.html#mangling
@mattleibow
Copy link
Member

I am thinking that Solution 2(b) is actually the most correct solution - we are binding Kotlin here, and it just so happens to use the same IL as Java. All the metadata ins there, so the generator logic is more correct to check for Kotlin data, then the Java data.

@jpobst jpobst added this to the d16-5 milestone Nov 22, 2019
@jonpryor
Copy link
Member Author

jonpryor commented Dec 2, 2019

Aside (via offline discussion): if we go with 2(b), we'll need to add JniArgumentValue constructor overloads for the unsigned types, so that the generated code can compile:

		[Register ("foo-WZ4Q5Ns", "(I)V", "GetFoo_IHandler")]
		public unsafe void Foo (uint value)
		{
			const string __id = "foo-WZ4Q5Ns.(I)V";
			try {
				JniArgumentValue* __args = stackalloc JniArgumentValue [1];
				__args [0] = new JniArgumentValue (value);  // hits JniArgumentValue(uint) constructor
				_members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args);
			} finally {
			}

Alternatively, we could insert casts:

__args [0] = new JniArgumentValue (unchecked((int) value));

@mattleibow
Copy link
Member

It might be better to add constructor overloads so that any processing is done in the core android as opposed to each library. But then this might cause issues if an old android is used and it does not have these new constructors.

And, I don't think Java is adding new types to their IL any time soon, so potentially breaking for older Android might not be worth it.

Wow, look at my brain wandering. Unless I am wrong, we can't use new constructors as these new libraries won't work on a slightly older Android?

jonpryor pushed a commit that referenced this issue Dec 4, 2019
Context: #525

Various fixes and enhancements to allow better default Kotlin
libraries bindings:

  * Hide Kotlin-internal classes, constructors, and methods.

  * Hide implementation methods (`*-impl*`).

  * Rename extension method parameter like
    `$this$decodeBase64` to `obj`. 

  * Use Kotlin provided method parameter names instead of
    `p0`, `p1`, etc.

  * Rename any method with a `-<mangling>` to drop the invalid part,
    e.g. `add-H4uI21a` is bound as `add`.
    ("Name mangling" like this is how Kotlin ensures `add(UInt)` and
    `add(Int)` don't clash when compiled to [Java][1].)

Note that the final bullet point -- "removing" name mangling -- may
result in C# code which is either not valid or not entirely usable.
See Issue #525 for details.  This will be addressed "later".

[1]: https://kotlinlang.org/docs/reference/inline-classes.html#mangling
@jonpryor
Copy link
Member Author

jonpryor commented Dec 6, 2019

we can't use new constructors as these new libraries won't work on a slightly older Android?

It took me some thinking to quite understand where you're going, but I think I have it now:

The problem isn't "an old Android version".

The problem is "an old Xamarin.Android version": binding assemblies built against a hypothetical newer Xamarin.Android containing a JniArgumentValue(uint) constructor -- and using that constructor -- would not be usable on all previous versions of Xamarin.Android.

That's a hell of a compat problem.

That reason alone is enough to kill the JniArgumentValue(uint) constructor overload idea, and necessitate the insertion of casts into the generated source code.

@jonpryor
Copy link
Member Author

jonpryor commented Dec 6, 2019

Open Questions

What happens with UIntArray?

It looks like UIntArray becomes an int[]:

package example;

open class UnsignedMethods {
	public open fun uintArray(value: UIntArray) {
	}
}
$ javap -c -s -cp . example.UnsignedMethods
Compiled from "hello.kt"
public class example.UnsignedMethods {
  public void uintArray--ajY-9A(int[]);
    descriptor: ([I)V
    Code:
       0: aload_1
       1: ldc           #30                 // String value
       3: invokestatic  #36                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
       6: return
}

@mattleibow
Copy link
Member

mattleibow commented Dec 6, 2019

It took me some thinking to quite understand where you're going

Sometimes I don't use my words 😑

jpobst added a commit that referenced this issue Jan 7, 2020
Fixes: #525

Context: dotnet/android#4054
Context: https://github.com/Kotlin/KEEP/blob/13b67668ccc5b4741ecc37d0dd050fd77227c035/proposals/unsigned-types.md
Context: https://kotlinlang.org/docs/reference/basic-types.html#unsigned-integers

Another place where Kotlin makes use of "name mangling" -- see also
commit f3553f4 -- is in the use of unsigned types such as `UInt`.
At the JVM ABI level, Kotlin treats unsigned types as their signed
counterparts, e.g. `kotlin.UInt` is an `int` and `kotlin.UIntArray`
is an `int[]`:

	// Kotlin
	public class Example {
	  public fun value(value: UInt) : UInt {
	    return value
	  }
	  public fun array(value: UIntArray) : UIntArray {
	    return value
	  }
	}

	// `javap` output:
	public final class Example {
	  public final int value-WZ4Q5Ns(int);
	  public final int[] array--ajY-9A(int[]);
	}

Kotlin uses Java Annotations to determine whether a parameter or
return type is actually an unsigned type instead of a signed type.

Update `Xamarin.Android.Tools.Bytecode` and `generator` to bind e.g.:

  * `kotlin.UInt` as `System.UInt32`
  * `kotlin.UIntArray` as a `System.UInt32[]`

and likewise for the other unsigned types `ushort`, `ulong`, `ubyte`.

In order to do this, we pretend that they are native Java types and
just translate a few places where we need to tell Java the real type.

~~ Xamarin.Android.Tools.Bytecode / class-parse ~~

When we read the Kotlin metadata in the Java bytecode, if we come
across one of these types we store it within an additional
`FieldInfo.KotlinType` property that we can access later.  When we
are generating the XML we check this additional flag and if it's one
of our types we emit it instead of the native Java type.

For example:

	<method
	  abstract="false"
	  deprecated="not deprecated"
	  final="false"
	  name="unsignedAbstractMethod-WZ4Q5Ns"
	  native="false"
	  return="uint"
	  jni-return="I"
	  static="false"
	  synchronized="false"
	  visibility="public"
	  bridge="false"
	  synthetic="false"
	  jni-signature="(I)I">
	  <parameter
	    name="value"
	    type="uint"
	    jni-type="I" />
	</method>

Here we see that even though `@jni-return` is `I` -- meaning `int` --
the `@return` property is `uint`.  Likewise `parameter/@jni-type` and
`parameter/@type`.  The JNI ABI is `int`, but we bind in C# as `uint`.

~~ ApiXmlAdjuster ~~

Update `JavaTypeReference` to contain unsigned types:

	UInt   = new JavaTypeReference ("uint");
	UShort = new JavaTypeReference ("ushort");
	ULong  = new JavaTypeReference ("ulong");
	UByte  = new JavaTypeReference ("ubyte");

~~ generator ~~

`generator` has the 4 new types added to the `SymbolTable` as
`SimpleSymbols`:

	AddType (new SimpleSymbol ("0", "uint",    "uint",   "I", returnCast: "(uint)"));
	AddType (new SimpleSymbol ("0", "ushort",  "ushort", "S", returnCast: "(ushort)"));
	AddType (new SimpleSymbol ("0", "ulong",   "ulong",  "J", returnCast: "(ulong)"));
	AddType (new SimpleSymbol ("0", "ubyte",   "byte",   "B", returnCast: "(byte)"));

There are 2 fixups we have to make because we use `GetIntValue(...)`,
etc. instead of having unsigned versions:

  * Override name of which method to call, e.g.: `GetIntValue()`
    instead of `GetUintValue()`.
  * Cast the `int` value returned to `uint`.  This is accomplished
    via the new `ISymbol.ReturnCast` property.

~~ A Note On API Compatibility ~~

Bindings which use Kotlin Unsigned Types will *only* work on
Xamarin.Android 10.2.0 or later ("Visual Studio 16.5").

The problem is that while we *can* emit C# source code which will
*compile* against older versions of Xamarin.Android, if they use
arrays they will not *run* under older versions of Xamarin.Android.

For example, imagine this binding code for the above Kotlin
`Example.array()` method:

	// C# Binding of Example.array()
	partial class Example {
	  public unsafe uint[] Array(uint[] value)
	  {
	    const string      __id          = "array--ajY-9A.([I)[I";
	    IntPtr            native_value  = JNIEnv.NewArray ((int[]) (object) value); // Works!...ish?
	    JniArgumentValue* __args        = stackalloc JniArgumentValue [1];
	    __args [0]                      = new JniArgumentValue (native_value);

	    JniObjectReference r            = _members.InstanceMethods.InvokeVirtualIntMethod (__id, this, __args);
	    return (uint[]) JNIEnv.GetArray (r.Handle, JniHandleOwnership.DoNotTransfer, typeof (uint));
	  }
	}

That could conceivably *compile* against older Xamarin.Android
versions.  However, that cannot *run* against older Xamarin.Android
versions, as *eventually* `JNIEnv.GetArray()` will hit some
dictionaries to determine how to marshal `IntPtr` to a `uint[]`, at
which point things will fail because there is no such mapping *until*
Xamarin.Android 10.2.0.

We feel that a "hard" ABI requirement will have more "graceful"
failure conditions than a solution which doesn't add ABI requirements.
In this case, if you create a Kotlin library binding which exposes
unsigned types, attempting to build an app in Release configuration
against older Xamarin.Android versions will result in a linker error,
as the required `JNIEnv` methods will not be resolvable.

(cherry picked from commit 71afce5)
jpobst added a commit that referenced this issue Jan 7, 2020
Fixes: #525

Context: dotnet/android#4054
Context: https://github.com/Kotlin/KEEP/blob/13b67668ccc5b4741ecc37d0dd050fd77227c035/proposals/unsigned-types.md
Context: https://kotlinlang.org/docs/reference/basic-types.html#unsigned-integers

Another place where Kotlin makes use of "name mangling" -- see also
commit f3553f4 -- is in the use of unsigned types such as `UInt`.
At the JVM ABI level, Kotlin treats unsigned types as their signed
counterparts, e.g. `kotlin.UInt` is an `int` and `kotlin.UIntArray`
is an `int[]`:

	// Kotlin
	public class Example {
	  public fun value(value: UInt) : UInt {
	    return value
	  }
	  public fun array(value: UIntArray) : UIntArray {
	    return value
	  }
	}

	// `javap` output:
	public final class Example {
	  public final int value-WZ4Q5Ns(int);
	  public final int[] array--ajY-9A(int[]);
	}

Kotlin uses Java Annotations to determine whether a parameter or
return type is actually an unsigned type instead of a signed type.

Update `Xamarin.Android.Tools.Bytecode` and `generator` to bind e.g.:

  * `kotlin.UInt` as `System.UInt32`
  * `kotlin.UIntArray` as a `System.UInt32[]`

and likewise for the other unsigned types `ushort`, `ulong`, `ubyte`.

In order to do this, we pretend that they are native Java types and
just translate a few places where we need to tell Java the real type.

~~ Xamarin.Android.Tools.Bytecode / class-parse ~~

When we read the Kotlin metadata in the Java bytecode, if we come
across one of these types we store it within an additional
`FieldInfo.KotlinType` property that we can access later.  When we
are generating the XML we check this additional flag and if it's one
of our types we emit it instead of the native Java type.

For example:

	<method
	  abstract="false"
	  deprecated="not deprecated"
	  final="false"
	  name="unsignedAbstractMethod-WZ4Q5Ns"
	  native="false"
	  return="uint"
	  jni-return="I"
	  static="false"
	  synchronized="false"
	  visibility="public"
	  bridge="false"
	  synthetic="false"
	  jni-signature="(I)I">
	  <parameter
	    name="value"
	    type="uint"
	    jni-type="I" />
	</method>

Here we see that even though `@jni-return` is `I` -- meaning `int` --
the `@return` property is `uint`.  Likewise `parameter/@jni-type` and
`parameter/@type`.  The JNI ABI is `int`, but we bind in C# as `uint`.

~~ ApiXmlAdjuster ~~

Update `JavaTypeReference` to contain unsigned types:

	UInt   = new JavaTypeReference ("uint");
	UShort = new JavaTypeReference ("ushort");
	ULong  = new JavaTypeReference ("ulong");
	UByte  = new JavaTypeReference ("ubyte");

~~ generator ~~

`generator` has the 4 new types added to the `SymbolTable` as
`SimpleSymbols`:

	AddType (new SimpleSymbol ("0", "uint",    "uint",   "I", returnCast: "(uint)"));
	AddType (new SimpleSymbol ("0", "ushort",  "ushort", "S", returnCast: "(ushort)"));
	AddType (new SimpleSymbol ("0", "ulong",   "ulong",  "J", returnCast: "(ulong)"));
	AddType (new SimpleSymbol ("0", "ubyte",   "byte",   "B", returnCast: "(byte)"));

There are 2 fixups we have to make because we use `GetIntValue(...)`,
etc. instead of having unsigned versions:

  * Override name of which method to call, e.g.: `GetIntValue()`
    instead of `GetUintValue()`.
  * Cast the `int` value returned to `uint`.  This is accomplished
    via the new `ISymbol.ReturnCast` property.

~~ A Note On API Compatibility ~~

Bindings which use Kotlin Unsigned Types will *only* work on
Xamarin.Android 10.2.0 or later ("Visual Studio 16.5").

The problem is that while we *can* emit C# source code which will
*compile* against older versions of Xamarin.Android, if they use
arrays they will not *run* under older versions of Xamarin.Android.

For example, imagine this binding code for the above Kotlin
`Example.array()` method:

	// C# Binding of Example.array()
	partial class Example {
	  public unsafe uint[] Array(uint[] value)
	  {
	    const string      __id          = "array--ajY-9A.([I)[I";
	    IntPtr            native_value  = JNIEnv.NewArray ((int[]) (object) value); // Works!...ish?
	    JniArgumentValue* __args        = stackalloc JniArgumentValue [1];
	    __args [0]                      = new JniArgumentValue (native_value);

	    JniObjectReference r            = _members.InstanceMethods.InvokeVirtualIntMethod (__id, this, __args);
	    return (uint[]) JNIEnv.GetArray (r.Handle, JniHandleOwnership.DoNotTransfer, typeof (uint));
	  }
	}

That could conceivably *compile* against older Xamarin.Android
versions.  However, that cannot *run* against older Xamarin.Android
versions, as *eventually* `JNIEnv.GetArray()` will hit some
dictionaries to determine how to marshal `IntPtr` to a `uint[]`, at
which point things will fail because there is no such mapping *until*
Xamarin.Android 10.2.0.

We feel that a "hard" ABI requirement will have more "graceful"
failure conditions than a solution which doesn't add ABI requirements.
In this case, if you create a Kotlin library binding which exposes
unsigned types, attempting to build an app in Release configuration
against older Xamarin.Android versions will result in a linker error,
as the required `JNIEnv` methods will not be resolvable.

(cherry picked from commit 71afce5)
jonpryor pushed a commit that referenced this issue Jan 7, 2020
Fixes: #525

Context: dotnet/android#4054
Context: https://github.com/Kotlin/KEEP/blob/13b67668ccc5b4741ecc37d0dd050fd77227c035/proposals/unsigned-types.md
Context: https://kotlinlang.org/docs/reference/basic-types.html#unsigned-integers

Another place where Kotlin makes use of "name mangling" -- see also
commit f3553f4 -- is in the use of unsigned types such as `UInt`.
At the JVM ABI level, Kotlin treats unsigned types as their signed
counterparts, e.g. `kotlin.UInt` is an `int` and `kotlin.UIntArray`
is an `int[]`:

	// Kotlin
	public class Example {
	  public fun value(value: UInt) : UInt {
	    return value
	  }
	  public fun array(value: UIntArray) : UIntArray {
	    return value
	  }
	}

	// `javap` output:
	public final class Example {
	  public final int value-WZ4Q5Ns(int);
	  public final int[] array--ajY-9A(int[]);
	}

Kotlin uses Java Annotations to determine whether a parameter or
return type is actually an unsigned type instead of a signed type.

Update `Xamarin.Android.Tools.Bytecode` and `generator` to bind e.g.:

  * `kotlin.UInt` as `System.UInt32`
  * `kotlin.UIntArray` as a `System.UInt32[]`

and likewise for the other unsigned types `ushort`, `ulong`, `ubyte`.

In order to do this, we pretend that they are native Java types and
just translate a few places where we need to tell Java the real type.

~~ Xamarin.Android.Tools.Bytecode / class-parse ~~

When we read the Kotlin metadata in the Java bytecode, if we come
across one of these types we store it within an additional
`FieldInfo.KotlinType` property that we can access later.  When we
are generating the XML we check this additional flag and if it's one
of our types we emit it instead of the native Java type.

For example:

	<method
	  abstract="false"
	  deprecated="not deprecated"
	  final="false"
	  name="unsignedAbstractMethod-WZ4Q5Ns"
	  native="false"
	  return="uint"
	  jni-return="I"
	  static="false"
	  synchronized="false"
	  visibility="public"
	  bridge="false"
	  synthetic="false"
	  jni-signature="(I)I">
	  <parameter
	    name="value"
	    type="uint"
	    jni-type="I" />
	</method>

Here we see that even though `@jni-return` is `I` -- meaning `int` --
the `@return` property is `uint`.  Likewise `parameter/@jni-type` and
`parameter/@type`.  The JNI ABI is `int`, but we bind in C# as `uint`.

~~ ApiXmlAdjuster ~~

Update `JavaTypeReference` to contain unsigned types:

	UInt   = new JavaTypeReference ("uint");
	UShort = new JavaTypeReference ("ushort");
	ULong  = new JavaTypeReference ("ulong");
	UByte  = new JavaTypeReference ("ubyte");

~~ generator ~~

`generator` has the 4 new types added to the `SymbolTable` as
`SimpleSymbols`:

	AddType (new SimpleSymbol ("0", "uint",    "uint",   "I", returnCast: "(uint)"));
	AddType (new SimpleSymbol ("0", "ushort",  "ushort", "S", returnCast: "(ushort)"));
	AddType (new SimpleSymbol ("0", "ulong",   "ulong",  "J", returnCast: "(ulong)"));
	AddType (new SimpleSymbol ("0", "ubyte",   "byte",   "B", returnCast: "(byte)"));

There are 2 fixups we have to make because we use `GetIntValue(...)`,
etc. instead of having unsigned versions:

  * Override name of which method to call, e.g.: `GetIntValue()`
    instead of `GetUintValue()`.
  * Cast the `int` value returned to `uint`.  This is accomplished
    via the new `ISymbol.ReturnCast` property.

~~ A Note On API Compatibility ~~

Bindings which use Kotlin Unsigned Types will *only* work on
Xamarin.Android 10.2.0 or later ("Visual Studio 16.5").

The problem is that while we *can* emit C# source code which will
*compile* against older versions of Xamarin.Android, if they use
arrays they will not *run* under older versions of Xamarin.Android.

For example, imagine this binding code for the above Kotlin
`Example.array()` method:

	// C# Binding of Example.array()
	partial class Example {
	  public unsafe uint[] Array(uint[] value)
	  {
	    const string      __id          = "array--ajY-9A.([I)[I";
	    IntPtr            native_value  = JNIEnv.NewArray ((int[]) (object) value); // Works!...ish?
	    JniArgumentValue* __args        = stackalloc JniArgumentValue [1];
	    __args [0]                      = new JniArgumentValue (native_value);

	    JniObjectReference r            = _members.InstanceMethods.InvokeVirtualIntMethod (__id, this, __args);
	    return (uint[]) JNIEnv.GetArray (r.Handle, JniHandleOwnership.DoNotTransfer, typeof (uint));
	  }
	}

That could conceivably *compile* against older Xamarin.Android
versions.  However, that cannot *run* against older Xamarin.Android
versions, as *eventually* `JNIEnv.GetArray()` will hit some
dictionaries to determine how to marshal `IntPtr` to a `uint[]`, at
which point things will fail because there is no such mapping *until*
Xamarin.Android 10.2.0.

We feel that a "hard" ABI requirement will have more "graceful"
failure conditions than a solution which doesn't add ABI requirements.
In this case, if you create a Kotlin library binding which exposes
unsigned types, attempting to build an app in Release configuration
against older Xamarin.Android versions will result in a linker error,
as the required `JNIEnv` methods will not be resolvable.

(cherry picked from commit 71afce5)
jonpryor added a commit to dotnet/android that referenced this issue Jan 8, 2020
jonpryor pushed a commit to dotnet/android that referenced this issue Jan 8, 2020
Fixes: dotnet/java-interop#525
Fixes: dotnet/java-interop#543

Context: dotnet/java-interop@476597b...4565369
Context: dotnet/java-interop@71afce5
Context: https://github.com/Kotlin/KEEP/blob/13b67668ccc5b4741ecc37d0dd050fd77227c035/proposals/unsigned-types.md
Context: https://kotlinlang.org/docs/reference/basic-types.html#unsigned-integers

Bumps to xamarin/Java.Interop/master@45653697

  * dotnet/java-interop@4565369: [generator] Improve generic type lookup (#552)
  * dotnet/java-interop@71afce5: [generator] Support Kotlin's unsigned types (#539)
  * dotnet/java-interop@f26bc27: [Java.Interop] use Dictionary<string, *>(StringComparer.Ordinal) (#550)

Kotlin has experimental (!) support for unsigned types, providing the
following value types:

  * `kotlin.UByte`: marshals as a Java `byte`/JNI `B`;
    equivalent to C# `byte`
  * `kotlin.UInt`: marshals as an Java `int`/JNI `I`;
    equivalent to C# `uint`
  * `kotlin.ULong`: marshals as a Java `long`/JNI `J`;
    equivalent to C# `ulong`
  * `kotlin.UShort`: marshals as a Java `short`/JNI `S`;
    equivalent to C# `ushort`

Kotlin also provides arrays of these types:

  * `kotlin.UByteArray`: marshals as a Java `byte[]`/JNI `[B`;
    equivalent to C# `byte[]`, 
  * `kotlin.UIntArray`: marshals as an Java `int[]`/JNI `[I`;
    equivalent to C# `uint[]`.
  * `kotlin.ULongArray`: marshals as a Java `long[]`/JNI `[J`;
    equivalent to C# `ulong[]`.
  * `kotlin.UShortArray`: marshals as a Java `short[]`/JNI `[S`;
    equivalent to C# `ushort[]`.

Kotlin methods which contain unsigned types are "mangled", containing
characters which cannot be part of a valid Java identifier.  As such,
they cannot be invoked from Java code.

They *can* be invoked via JNI.

To bind these, we have two options:

 1. We could ignore all members which use unsigned types, or
 2. We could present a "Java-esque" view of the methods.

(1) is potentially problematic, as it means that abstract classes and
interfaces which contain them could become unbindable, making life
more complicated than is necessarily ideal.

(2) is viable, but ugly.

Consider this Kotlin type:

	// Kotlin
	package example;
	public open class ExampleBase {
	  public fun foo(value : UInt) {}
	}

Run `javap` on the resulting output, and we observe:

	Compiled from "hello.kt"
	public class example.ExampleBase {
	  public final void foo-WZ4Q5Ns(int);
	  public example.ExampleBase();
	}

We could bind `example.ExampleBase.foo-WZ4Q5Ns(int)` in C# by
replacing invalid characters with `_`, e.g.:

	// C# Binding
	partial class ExampleBase : Java.Lang.Object {
	  [Register ("foo-WZ4Q5Ns", …)]
	  public void Foo_WZ4Q5Ns (int value) {…}
	}

...but that would be *really* ugly.

We could instead just take everything before the `-` as the method
name for the C# method name -- resulting in `ExampleBase.Foo()` --
which results in a friendlier name from C#, but opens us up to
"collisions" with method overloads:

	// Kotlin
	public open class ExampleBase2 {
	  public fun foo(value : Int) {}
	  public fun foo(value : UInt) {}
	}

The C# `ExampleBase` type can't bind *both* `ExampleBase2.foo()`
methods as `Foo(int)`.

The chosen solution is to "more natively" support Kotlin unsigned
types, which allows reasonable disambiguation:

	// C# binding
	partial class ExampleBase2 : Java.Lang.Object {
	  [Register ("foo", …)]
	  public void Foo (int value) {…}

	  [Register ("foo-WZ4Q5Ns", …)]
	  public void Foo (uint value) {…}
	}

Java.Interop added support for emitting C# binding code which supports
Kotlin unsigned types.

For `kotlin.UByte`, `kotlin.UInt`, `kotlin.ULong`, and `kotlin.UShort`,
no additional runtime changes are needed to support the binding.

The `*Array` counterparts *do* require additional runtime changes.

Consider this Kotlin type:

	// Kotlin
	class UnsignedInstanceMethods {
	  public open fun uintArrayInstanceMethod (value: UIntArray) : UIntArray {
	    return value;
	  }
	}

The binding for this method would be:

	// C# binding
	partial class UnsignedInstanceMethods {
	  [Register ("uintArrayInstanceMethod--ajY-9A", "([I)[I", "")]
	  public unsafe uint[] UintArrayInstanceMethod (uint[] value)
	  {
	    const string __id = "uintArrayInstanceMethod--ajY-9A.([I)[I";
	    IntPtr native_value = JNIEnv.NewArray ((int[])(object)value);
	    try {
	      JniArgumentValue* __args = stackalloc JniArgumentValue [1];
	      __args [0] = new JniArgumentValue (native_value);
	      var __rm = _members.InstanceMethods.InvokeNonvirtualObjectMethod (__id, this, __args);
	      return (uint[]) JNIEnv.GetArray (__rm.Handle, JniHandleOwnership.TransferLocalRef, typeof (uint));
	    } finally {
	      if (value != null) {
	        JNIEnv.DeleteLocalRef (native_value);
	      }
	    }
	  }
	}

The problem is the `JNIEnv.GetArray(…, typeof(uint))` invocation.
This eventually hits various dictionaries within `JNIEnv`, which
requires additional support so that `uint` can be marshaled properly.

Previous versions of Xamarin.Android do not contain support for
marshaling arrays of unsigned types.

As such, support for using Kotlin bindings which contain unsigned
types will require Xamarin.Android 10.2.0 and later.


~~ Installer ~~

Add `protobuf-net.dll` to our installers, as it is required to read
the metadata that Kotlin puts within `.class` files.


~~ Unit Tests ~~

On-device tests for calling Kotlin code that uses unsigned types have
been added.

As we don't currently have a way of acquiring the Kotlin compiler for
builds, the test `.jar` and Kotlin's
`org.jetbrains.kotlin.kotlin-stdlib.jar` are copied into the repo.
They are *not* redistributed.

The source of the test `.jar` is provided for future use.


~~ Warnings ~~

Kotlin unsigned types are still experimental.  If Kotlin changes how
they are represented in Java bytecode, bindings which use them will
break.

Methods which use unsigned types *cannot* be virtual, nor can they
be used in bound interfaces.  This is because Java source code is
currently used as an "intermediary" for Java <-> Managed transitions,
and Java cannot override Kotlin "mangled" methods.  If the virtual
method is declared on a class, it will be bound as a non-`virtual`
method.  If the method is on an interface, it will be bound, but the
interface will not actually be implementable; a
[XA4213 build-time error][0] will be generated.

[0]: dotnet/java-interop@3bf5333
jonpryor pushed a commit to dotnet/android that referenced this issue Jan 8, 2020
Fixes: dotnet/java-interop#525
Fixes: dotnet/java-interop#543

Context: dotnet/java-interop@71afce5
Context: https://github.com/Kotlin/KEEP/blob/13b67668ccc5b4741ecc37d0dd050fd77227c035/proposals/unsigned-types.md
Context: https://kotlinlang.org/docs/reference/basic-types.html#unsigned-integers

Kotlin has experimental (!) support for unsigned types, providing the
following value types:

  * `kotlin.UByte`: marshals as a Java `byte`/JNI `B`;
    equivalent to C# `byte`
  * `kotlin.UInt`: marshals as an Java `int`/JNI `I`;
    equivalent to C# `uint`
  * `kotlin.ULong`: marshals as a Java `long`/JNI `J`;
    equivalent to C# `ulong`
  * `kotlin.UShort`: marshals as a Java `short`/JNI `S`;
    equivalent to C# `ushort`

Kotlin also provides arrays of these types:

  * `kotlin.UByteArray`: marshals as a Java `byte[]`/JNI `[B`;
    equivalent to C# `byte[]`,
  * `kotlin.UIntArray`: marshals as an Java `int[]`/JNI `[I`;
    equivalent to C# `uint[]`.
  * `kotlin.ULongArray`: marshals as a Java `long[]`/JNI `[J`;
    equivalent to C# `ulong[]`.
  * `kotlin.UShortArray`: marshals as a Java `short[]`/JNI `[S`;
    equivalent to C# `ushort[]`.

Kotlin methods which contain unsigned types are "mangled", containing
characters which cannot be part of a valid Java identifier.  As such,
they cannot be invoked from Java code.

They *can* be invoked via JNI.

To bind these, we have two options:

 1. We could ignore all members which use unsigned types, or
 2. We could present a "Java-esque" view of the methods.

(1) is potentially problematic, as it means that abstract classes and
interfaces which contain them could become unbindable, making life
more complicated than is necessarily ideal.

(2) is viable, but ugly.

Consider this Kotlin type:

	// Kotlin
	package example;
	public open class ExampleBase {
	  public fun foo(value : UInt) {}
	}

Run `javap` on the resulting output, and we observe:

	Compiled from "hello.kt"
	public class example.ExampleBase {
	  public final void foo-WZ4Q5Ns(int);
	  public example.ExampleBase();
	}

We could bind `example.ExampleBase.foo-WZ4Q5Ns(int)` in C# by
replacing invalid characters with `_`, e.g.:

	// C# Binding
	partial class ExampleBase : Java.Lang.Object {
	  [Register ("foo-WZ4Q5Ns", …)]
	  public void Foo_WZ4Q5Ns (int value) {…}
	}

...but that would be *really* ugly.

We could instead just take everything before the `-` as the method
name for the C# method name -- resulting in `ExampleBase.Foo()` --
which results in a friendlier name from C#, but opens us up to
"collisions" with method overloads:

	// Kotlin
	public open class ExampleBase2 {
	  public fun foo(value : Int) {}
	  public fun foo(value : UInt) {}
	}

The C# `ExampleBase` type can't bind *both* `ExampleBase2.foo()`
methods as `Foo(int)`.

The chosen solution is to "more natively" support Kotlin unsigned
types, which allows reasonable disambiguation:

	// C# binding
	partial class ExampleBase2 : Java.Lang.Object {
	  [Register ("foo", …)]
	  public void Foo (int value) {…}

	  [Register ("foo-WZ4Q5Ns", …)]
	  public void Foo (uint value) {…}
	}

Java.Interop added support for emitting C# binding code which supports
Kotlin unsigned types.

For `kotlin.UByte`, `kotlin.UInt`, `kotlin.ULong`, and `kotlin.UShort`,
no additional runtime changes are needed to support the binding.

The `*Array` counterparts *do* require additional runtime changes.

Consider this Kotlin type:

	// Kotlin
	class UnsignedInstanceMethods {
	  public open fun uintArrayInstanceMethod (value: UIntArray) : UIntArray {
	    return value;
	  }
	}

The binding for this method would be:

	// C# binding
	partial class UnsignedInstanceMethods {
	  [Register ("uintArrayInstanceMethod--ajY-9A", "([I)[I", "")]
	  public unsafe uint[] UintArrayInstanceMethod (uint[] value)
	  {
	    const string __id = "uintArrayInstanceMethod--ajY-9A.([I)[I";
	    IntPtr native_value = JNIEnv.NewArray ((int[])(object)value);
	    try {
	      JniArgumentValue* __args = stackalloc JniArgumentValue [1];
	      __args [0] = new JniArgumentValue (native_value);
	      var __rm = _members.InstanceMethods.InvokeNonvirtualObjectMethod (__id, this, __args);
	      return (uint[]) JNIEnv.GetArray (__rm.Handle, JniHandleOwnership.TransferLocalRef, typeof (uint));
	    } finally {
	      if (value != null) {
	        JNIEnv.DeleteLocalRef (native_value);
	      }
	    }
	  }
	}

The problem is the `JNIEnv.GetArray(…, typeof(uint))` invocation.
This eventually hits various dictionaries within `JNIEnv`, which
requires additional support so that `uint` can be marshaled properly.

Previous versions of Xamarin.Android do not contain support for
marshaling arrays of unsigned types.

As such, support for using Kotlin bindings which contain unsigned
types will require Xamarin.Android 10.2.0 and later.

~~ Installer ~~

Add `protobuf-net.dll` to our installers, as it is required to read
the metadata that Kotlin puts within `.class` files.

~~ Unit Tests ~~

On-device tests for calling Kotlin code that uses unsigned types have
been added.

As we don't currently have a way of acquiring the Kotlin compiler for
builds, the test `.jar` and Kotlin's
`org.jetbrains.kotlin.kotlin-stdlib.jar` are copied into the repo.
They are *not* redistributed.

The source of the test `.jar` is provided for future use.

~~ Warnings ~~

Kotlin unsigned types are still experimental.  If Kotlin changes how
they are represented in Java bytecode, bindings which use them will
break.

Methods which use unsigned types *cannot* be virtual, nor can they
be used in bound interfaces.  This is because Java source code is
currently used as an "intermediary" for Java <-> Managed transitions,
and Java cannot override Kotlin "mangled" methods.  If the virtual
method is declared on a class, it will be bound as a non-`virtual`
method.  If the method is on an interface, it will be bound, but the
interface will not actually be implementable; a
[XA4213 build-time error][0] will be generated.

[0]: dotnet/java-interop@3bf5333
@jpobst jpobst modified the milestones: d16-5, Published: 10.2 (d16-5) Mar 1, 2021
@github-actions github-actions bot locked and limited conversation to collaborators Apr 13, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants