Repository navigation
Kotlin & method overloading & name mangling, oh my! #525
Description
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?
Activity
jonpryor commentedon Nov 22, 2019
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 commentedon Nov 22, 2019
Solution 2: We could bind them as non-
virtualmethods.We cannot bind "name mangled" methods as C#
virtualmethods, 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-
virtualmethod, we can expose the method to C# code, while preventing the subclass scenario. This allows the KotlinExampleBase.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 commentedon Nov 22, 2019
Solution 2(b): Update
class-parse&generatorso thatgeneratorcan "know" that the method takes aUInt. This would allow us to bindfoo(UInt)asFoo(uint), which can be overloaded, thus avoiding the CS0111 error:[XA.Tools.Bytecode] Add Kotlin support to our binding process. (#505)
mattleibow commentedon Nov 22, 2019
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.
jonpryor commentedon Dec 2, 2019
Aside (via offline discussion): if we go with 2(b), we'll need to add
JniArgumentValueconstructor overloads for the unsigned types, so that the generated code can compile:Alternatively, we could insert casts:
mattleibow commentedon Dec 2, 2019
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?
[XA.Tools.Bytecode] Add Kotlin support to our binding process. (#505)
jonpryor commentedon Dec 6, 2019
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 commentedon Dec 6, 2019
Open Questions
What happens with
UIntArray?It looks like
UIntArraybecomes anint[]:mattleibow commentedon Dec 6, 2019
Sometimes I don't use my words 😑
[generator] Support Kotlin's unsigned types (#539)
[generator] Support Kotlin's unsigned types (#539)
[generator] Support Kotlin's unsigned types (#539) (#553)
Bump to xamarin/Java.Interop/d16-5@c0cc7700
[JNIEnv] Add support for Kotlin's unsigned types. (#4054)
[JNIEnv] Add support for Kotlin's unsigned types. (#4054)
[generator] Support Kotlin's unsigned types (#539)
[generator] Support Kotlin's unsigned types (dotnet#539)