Nested inline functions with reified type parameters and type erasure

Last week, a user called " Michal Zhradnk Nono3551" asked a very interesting question on StackOverflow, which is definetly more interesting than you might think at a first glance.

In the code he posted, he has multiple inlineable functions which hold a single, reifiable type parameter. He passes them from one function to another and, after two passes, he tries to deserialize a JSON-String using Gson based on the type parameter he has passed two times.

As a result, the following exception has been thrown:
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.xbionicsphere.x_card.entities.Token

I was already able to explain most of “the magic” happening there, including why Gson tries to cast LinkedTreeMap, a Gson-internal type, to the user-defined type Token. In short, Gson handled it like it was an instance of java.lang.Object (not a subtype of it) and therefore applied its special handler to it. Thus, the type information must have been erased during compilation. You’ll find a snippet of Gson’s source code in the linked discussion.

The problem is: I am unable to explain why the generic type information has been erased. I’ve played around with different type constellations, but even with the following code snippet which includes almost every language feature/“data type” he used, I’m still unable to reproduce the issue.

fun main() {
    test<ArrayList<String>>().run();
}

inline fun <reified T : Any> test() : Runnable {
    var result: Runnable? = null

    val job = GlobalScope.launch {
        result = suspendCoroutine<Runnable> { continuation ->
            val runnable = Runnable {

                val deserialized: TestContainer<T> = "{\"data\": [\"abc\"]}".jsonToObject()
                println(deserialized.data)
            }

            continuation.resumeWith(Result.success(runnable));
        }
    }

    return runBlocking {
        job.join()
        return@runBlocking result!!
    }

}

inline fun <reified T>String.jsonToObject() : T {
    return GsonBuilder().create().fromJson(this, object : TypeToken<T>() {}.type)
}
data class TestContainer<T : Any>(val data: T)

I’ve also tested the type arguments passed to the type varialbe T multiple times and in even more diverse constellations, the type was always correct.

You can find the original question here. I hope posting a link to a question on StackOverflow doesn’t pose a problem since I’m not allowed to copy his code either and the question doesn’t get a lot of attention on the other platform as it already has been marked as solved.

Thank you very much in advance!

2 Likes

The type passed to jsonToObject is HttpResponse<T> where T is the type parameter of sendRequestAsync.

It seems like the type parameter T of sendRequestAsync is not replaced in the bytecode when it is never used directly in any way. Adding null is T above val brokenObject “fixes” the code

Here’s a simpler repro that appears to be the same issue:

private inline fun <reified T> topMethod() {
        innerMethod {
            accessClass<T>()
        }
    }

    private fun innerMethod(block: () -> Unit) {
        block()
    }

    private inline fun <reified Z> accessClass() { Z::class }

Calling topMethod() throws java.lang.UnsupportedOperationException: This function has a reified type parameter and thus can only be inlined at compilation time, not called directly.

1 Like

I didn’t see any existing issues that matched so I created Issue KT-34051

1 Like