Skip to content
This repository was archived by the owner on Aug 27, 2026. It is now read-only.
This repository was archived by the owner on Aug 27, 2026. It is now read-only.

Binding a Kotlin library results in bad code #464

Closed

Description

@mattleibow

I have noticed that binding Kotlin code results in bad code. For example, I am binding okio v2.0.0

There are a few bits of code that cannot be compiled:

  1. It tries to bind classes with a - prefix. This looks to be a way to mark them as deprecated or internal or something. This is easy to remove by:
    <remove-node path="/api/package/class[starts-with(@name,'-')]" />
  2. It tries to bind methods with a - prefix, again easy to remove with:
    <remove-node path="/api/package/class/method[starts-with(@name,'-')]" />
  3. It tries to bind methods with $ in the parameter names:
    public static final int codePointAt(@NotNull byte[] $receiver, int index)
    There are other areas where it works fine, so this looks to be just in the finally blocks. I am able to fix this by setting the managedName:
    <attr path="/api/package[@name='okio']/class/method[parameter[1][@name='$receiver']]/parameter[1]" name="managedName">receiver</attr>
    Without this adjustment, the generator results in partially corrected code:
    • Declaration: public static unsafe int CodePointAt (byte[] _receiver_, int index)
    • Code: try { ... } finally { if ($receiver != null) { ... } }

There are also cases where this could be improved overall:

  1. With Kotlin, you can have extension methods (the reason for the $receiver parameter name). This means that Xamarin could potentially automatically generate extension methods - or have the option to do so.
  2. There are also cases where internal members are public in C#:
    internal val AssertionError.isAndroidGetsocknameError: Boolean get() { ... }
    https://github.com/square/okio/blob/okio-parent-2.0.0/okio/jvm/src/main/java/okio/Okio.kt#L212

I am working on a tool that runs after the api.xml is created to create a transforms.xml file that tries to fix and resolve members. A very rough thing that seems to be working: xamarin/XamarinComponents#642

Activity

  1. mattleibow commented on Aug 14, 2019

    @mattleibow
    MemberAuthor

    Given:

    public fun createTempDir(prefix: String = "tmp", suffix: String? = null, directory: File? = null): File

    I also notice a few bad methods ending in $default which I think may have to do with default parameters:

    // Metadata.xml XPath method reference: path="/api/package[@name='kotlin.io']/class[@name='FilesKt__UtilsKt']/method[@name='createTempDir$default' and count(parameter)=5 and parameter[1][@type='java.lang.String'] and parameter[2][@type='java.lang.String'] and parameter[3][@type='java.io.File'] and parameter[4][@type='int'] and parameter[5][@type='java.lang.Object']]"
    [Register ("createTempDir$default", "(Ljava/lang/String;Ljava/lang/String;Ljava/io/File;ILjava/lang/Object;)Ljava/io/File;", "")]
    public static unsafe global::Java.IO.File CreateTempDir$default (string p0, string p1, global::Java.IO.File p2, int p3, global::Java.Lang.Object p4)
    
    // Metadata.xml XPath method reference: path="/api/package[@name='kotlin.io']/class[@name='FilesKt__UtilsKt']/method[@name='createTempDir' and count(parameter)=3 and parameter[1][@type='java.lang.String'] and parameter[2][@type='java.lang.String'] and parameter[3][@type='java.io.File']]"
    [Register ("createTempDir", "(Ljava/lang/String;Ljava/lang/String;Ljava/io/File;)Ljava/io/File;", "")]
    public static unsafe global::Java.IO.File CreateTempDir (string p0, string p1, global::Java.IO.File p2)
    

    To test, I wrote this Kotlin:

    fun carrot(): Array<File> {
        return arrayOf(
            createTempDir(),
            createTempDir("names"),
            createTempDir("names", "suffix"),
            createTempDir("names", "suffix", File("path")),
            createTempDir(suffix = "suffix"))
    }

    Which results in this Java code:

      public static final File[] carrot() {
        return new File[] {
            FilesKt.createTempDir$default(null, null, null, 7, null), 
            FilesKt.createTempDir$default("names", null, null, 6, null), 
            FilesKt.createTempDir$default("names", "suffix", null, 4, null), 
            FilesKt.createTempDir("names", "suffix", new File("path")), 
            FilesKt.createTempDir$default(null, "suffix", null, 5, null)
          };
      }
  2. mattleibow commented on Aug 15, 2019

    @mattleibow
    MemberAuthor

    A few more weird things - trying to bind Kotlin 1.3:

    public fun Random.nextULong(): ULong = nextLong().toULong()
    public fun Random.nextULong(until: ULong): ULong = nextULong(0uL, until)

    The Java side has -:

    public static final long nextULong(@NotNull Random $this$nextULong)
    public static final long nextULong-V1Xi4fY(@NotNull Random $this$nextULong, long until)

    I think things are changing a bit, not sure why this is not normal overloads. Even the Java in Intellij is invalid:
    Screenshot 2019-08-15 at 03 47 34

  3. mattleibow commented on Aug 15, 2019

    @mattleibow
    MemberAuthor

    I found some things about the way companion objects function. They are effectively singletons. Instead of static methods, we have static singletons that have instance methods.

    Docs:

    Java Code

    // consumed: JavaTestingGround.log("...");
    public class JavaTestingGround {
        public void test() {
        }
        public static void log(String message) {
        }
    }

    Kotlin Code

    // consumed: KotlinTestingGround.log("...")
    class KotlinTestingGround {
        fun test() {
        }
        companion object {
            fun log(message: String) {
            }
        }
    }

    Decompiled Kotlin Code

    // consumed: KotlinTestingGround.Companion.log("...");
    public final class KotlinTestingGround {
        public static final Companion Companion = new Companion(null);
        public final void test() {
        }
        public static final class Companion {
            private Companion() {
            }
            public final void log(String message) {
            }
        }
    }

    One thing to note is that the name "Companion" is not fixed and can be changed:

    Kotlin Code

    class KotlinTestingGround {
        companion object More {
            fun log(message: String) {
            }
        }
    }

    Decompiled Java Code

    public final class KotlinTestingGround {
        public static final More More = new More(null);
        public static final class More {
            private More() {
            }
            public final void log(String message) {
            }
        }
    }
  4. mattleibow commented on Aug 15, 2019

    @mattleibow
    MemberAuthor

    For the case of extension methods in Kotlin:

    Docs:

    Kotlin Code

    // consumed:
    //     val obj = CoolObject()
    //     obj.coolMethod("...")
    // but NOT: CoolObject.coolMethod(obj, "...")
    class CoolObject {
    }
    fun CoolObject.coolMethod(name: String) {
    }

    Decompiled Java Code

    // consumed:
    //     CoolObject obj = new CoolObject();
    //     CoolObjectKt.coolMethod(obj, "...");
    public final class CoolObject {
    }
    public final class CoolObjectKt {
        public static final void coolMethod(CoolObject $this$coolMethod, String name) {
    
        }
    }
  5. mattleibow commented on Aug 19, 2019

    @mattleibow
    MemberAuthor

    Kotlin also has a set of IFunction# types that basically are the same thing as Func<T> in .NET.

    For example, if you have a collection, there is an extension method map which is used line the Select in Linq:

    Kotlin:

    val names = people.map { it.name }

    The equivalent C#:

    var names = people.Select(p => p.Name);

    The Kotlin binding is missing support for creating extension methods, but it can be seen here:

    public unsafe static IList Map (Java.Lang.Object[] array, IFunction1 mapper);

    If we had to use this:

    var people = new[]
    {
    	new Person { Name = "Matthew" },
    	new Person {Name = "Ruth" },
    	new Person {Name = "Benjamin" },
    	new Person {Name = "Hannah" }
    };
    
    var names = ArraysKt.Map(people, new Mapper());
    
    class Person : Java.Lang.Object
    {
    	public string Name { get; set; }
    }
    
    class Mapper : Java.Lang.Object, IFunction1
    {
    	public Java.Lang.Object Invoke(Java.Lang.Object p0)
    	{
    		var person = p0 as Person;
    		return person.Name;
    	}
    }
  6. mattleibow commented on Aug 19, 2019

    @mattleibow
    MemberAuthor

    Another thing with extension methods is that Kotlin creates non-static classes that are built up in layers. For example, the extensions for arrays are in several files:

    • ArraysKt which is empty, but extends ArraysKt___ArraysKt (3 underscores)
    • ArraysKt___ArraysKt contains many methods and extends ArraysKt___ArraysJvmKt (3 underscores)
    • ArraysKt___ArraysJvmKt contains a few more methods and extends ArraysKt__ArraysKt (2 underscores)
    • ArraysKt__ArraysKt contains a couple methods and extends ArraysKt__ArraysJVMKt (2 underscores)
    • ArraysKt__ArraysJVMKt contains a couple methods

    All of these methods are static, so I could make the class static. But, this would break the inheritance. I could also make the objects not extend anything, but then you would never be able to find any thing because you wouldn't know to look in a type with 3 underscores. I could also rename the class to something more useful, but this is not always easy if there are many classes.

    In addition to these generated names, you can also mix normal methods and extension methods in a class. This means that the extension methods are not automatically applied and have to be explicitly imported.

    Docs: https://kotlinlang.org/docs/reference/extensions.html#companion-object-extensions

    If the extension method is in a plain class:

    class Test {
        String.count() { }
    }

    Then, it can't be access outside the class - which is something we can ignore since bindings are not in the class.

    If the extension method is in a companion object:

    class Test {
        companion object {
            String.count() { }
        }
    }

    Then, this has to be explicitly imported before being used:

    import com.example.Test.Companion.count

    One possible issue is that the companion object is NOT static, so will have to be referenced via the Companion property in C#: Test.Companion.count(...)

  7. added this to the milestone on Oct 31, 2019
  8. jpobst commented on Feb 3, 2020

    @jpobst
    Contributor

    Our Kotlin support has been substantially improved in 16.5, largely working from this list. I think everything from this list has been added or turned out to not be workable. (For example, Kotlin extension methods are not in static classes, so we have left them as-is.)

  9. modified the milestones: , Published: 10.2 (d16-5) on Mar 1, 2021
  10. locked and limited conversation to collaborators on Apr 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions