Autoboxing inline classes for Java interop?

I’m migrating some code to Kotlin 1.5, and I’d really like to use inline classes for some very widely used library classes. However, these are used by a lot of Java code, and while ABI compatibility is not an issue, source compatibility is. These classes usually follow a kind of “sealed private data class” pattern to avoid the public copy() issue:

sealed class MyValueClass {
    abstract val value: Int
    fun acceptInt(i: Int) { }
    fun acceptMyClass(other: MyValueClass) { }
    fun returnMyClass(): MyValueClass = this

    companion object {
        @JvmStatic fun of(value: Int): MyValueClass = MyValueClassImpl(value)
        @JvmField val ZERO: MyValueClass = MyValueClassImpl(0)
    }

    private data class MyValueClassImpl(override val value: Int): MyValueClass()
}

While this isn’t as elegant as it could be if I could simply use a data class without a copy() function, it works just perfectly. Now when I’m trying to make it an inline class, all kinds of things go wrong as far as Java interop is concerned.

@JvmInline
value class MyValueClass private constructor(val value: Int) {
    fun acceptInt(i: Int) { }
    fun acceptMyClass(other: MyValueClass) { }
    fun returnMyClass(): MyValueClass = this

    companion object {
        @JvmStatic fun of(value: Int): MyValueClass = MyValueClass(value)
        @JvmField val ZERO: MyValueClass = MyValueClass(0)
    }
}

What happens is:

  • the acceptInt function is compiled into a static function accepting two ints (this and i);
  • the same goes for acceptMyClass, as other is an int too;
  • the returnMyClass function returns an int;
  • the of function returns an int too;
  • the ZERO constant doesn’t even compile because a @JvmField can’t be of an inline class type.

And with the name mangling I can’t even use most (all?) of these functions from Java even if I was willing to work with ints directly (thereby turning all this stuff into procedural type-unsafe programming).

What could be really useful here is to somehow get it work transparently with Java, even if at the cost of autoboxing. But I see no magic annotation or compiler option for that. What I really want to have is an unmangled boxed counterpart for every function, so that of returns a MyValueClass instance when called from Java, and an Int when called from Kotlin.

I actually managed to get it done with some hardcore interface abuse:

sealed interface MyValueClass {
    val value: Int
    fun acceptInt(i: Int)
    fun acceptMyClass(other: MyValueClass)
    fun returnMyClass(): MyValueClass

    companion object {
        @JvmStatic fun of(value: Int): MyValueClass = MyValueClassImpl.of(value)
        @JvmField val ZERO: MyValueClass = MyValueClassImpl.ZERO
    }
}

@JvmInline
value class MyValueClassImpl(override val value: Int): MyValueClass {
    override fun acceptInt(i: Int) { }
    override fun acceptMyClass(other: MyValueClass) = acceptMyClass(other as MyValueClassImpl)
    fun acceptMyClass(other: MyValueClassImpl) { }
    override fun returnMyClass() = this

    companion object {
        fun of(value: Int) = MyValueClassImpl(value)
        val ZERO = MyValueClassImpl(0)
    }
}

This isn’t as ugly as using ints directly from Java would be, but it’s still far from perfect. Not only it’s too verbose (which is an acceptable price for an optimization), but the need to use MyValueClassImpl everywhere in Kotlin is very annoying (and makes converting Java code to Kotlin code harder). This is an acceptable price for an optimization as well, but is on the verge of being too complicated.

What’s especially frustrating is that Kotlin already generates a boxing class for a value class. All it needs to do is to add all members to it (with companion object members, I can live with special overloads for Java). But instead of implementing member functions as true members, it simply unboxes this on every call, and then passes the unboxed value to the static implementation, that is mangled and inaccessible from Java (and is useless anyway even if I unmangle it with @JvmName).

1 Like