We have a very specific use case in Moshi where we...
# ksp
z
We have a very specific use case in Moshi where we want to invoke the defaults constructor of a class to respect default parameter values when we those keys are absent from json. This constructor is synthetic but present in bytecode, so currently we reflectively look up the constructor to invoke it. If we could generate bytecode, we could just make the bytecode invoke this constructor directly and make the generated adapter invoke this non-synthetic bridge method
e
this is a somewhat ridiculous idea, but… would it be possible to generate a Java file with some
@kotlin.Metadata
annotations to simulate a Kotlin file defining an inline function with bytecode to call the synthetic constructor?
t
@Zac Sweers Can you point me to the code in Moshi? Not sure if we were able to read the synthetic constructor from the library for generated code to call, but if it is readable via reflection, KSP should be able to do it, too.
z
we compute the signature of where we know it will be. The idea is that if we can just generate the bytecode directly (like a simple class with a bridging static method), we can call the generated bytecode and it in turn can reference the real constructor under the hood at runtime without any reflection
errr scratch that I mean we compute the mask requirements to access the constructor
we get the signature from kotlinx-metadata
here’s the bytecode generating prototype https://github.com/square/moshi/tree/z/generateBytecode We could never get it to work because kapt didn’t support generating bytecode directly (it wouldn’t include them on the compile classpath later)
s
Maybe KSP can expose some IR transforms or something similar? It kinda still is a compiler plugin 🙂
t
The function with mask is implementation specific, although kotlinc probably would never change it because of binary compatibility. On the other hand, there should be a helper generated by compiler which does exactly what you want already, IIRC. Let me ask someone who is more familiar with this.
Just checked the implementation of default constructor. Looks like that the default constructor is only generated when all parameters have default values. It also doesn't fit the use case where some arguments are specified and others come from default.
However, the compiler should transform all the call sites to the target with mask. I think you can simply generate calls in Kotlin and don't have to worry about the default arguments.
When generating Java or bytecode, you'll have to deal with the mask, though. A workaround is requiring users to annotate the constructors / functions of interest with
@JvmOverloads
for the processor: https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#overloads-generation Having said that, bytecode generation is probably still not needed. The function with mask is public and should be callable (although not recommended) in Java as well. Pardon me if I still get you wrong.
z
Looks like that the default constructor is only generated when all parameters have default values.
This isn’t quite true, it will be generated when any of them have a default parameter.
the compiler should transform all the call sites to the target with mask. I think you can simply generate calls in Kotlin and don’t have to worry about the default arguments.
Normally yes, but in our case we don’t know until runtime which arguments are present. That’s why we do this with reflection for now. This is effectively recreating the behavior of kotlin-reflect’s
KFunction.callBy()
functionality
t
Hi @Zac Sweers, we'll support generated bytecode. Here is the tracking issue: https://github.com/google/ksp/issues/95
🎉 2
z
Awesome! Happy to help test too with that Moshi use case
t
Cool! and thanks in advance 😀
👍 2
z
put up a prototype PR here if it’s of any help https://github.com/ZacSweers/MoshiX/pull/43
👍 2