Can I somehow transform a `.kt` file with an objec...
# announcements
v
Can I somehow transform a
.kt
file with an object expression in it to the actual instance? Like with using
K2JVMCompiler
for example? Are there any examples for that?
m
So you want to compile an kt file at runtime and thenn use an object thats defined in it?
v
Exactly
h
I have done it with regular classes instead of an objekt. I used the convention that the file name is the class name, otherwise you need to extract that somehow. You can do the same for companion objects and objects i think. The compilation of the kt file has to happen before of course but that's rather easy. You compile kt to a class file, load the class file with a classloader and you do newInstance or sth. You can then invoke methods by name, implement a host interface or whatever you like :)
v
Nice, that helped me a lot, thanks:
Copy code
K2JVMCompiler().run {
    val args = K2JVMCompilerArguments().apply {
        freeArgs = listOf(file("src/main/kotlin/net/kautler/dao/ResultSerializer.kt").absolutePath)
        classpathAsList = sequenceOf(buildscript.classLoader, GroovyObject::class.java.classLoader)
                .flatMap(ClassLoader::classPathFromTypicalResourceUrls)
                .toList()
        destinationAsFile = ...
        noReflect = true
        noStdlib = true
        skipRuntimeVersionCheck = true
    }
    exec(
            PrintingMessageCollector(System.out, WITHOUT_PATHS, true),
            Services.EMPTY,
            args
    )
}
Now I just have to try to load and use it and then have to try whether I can leave out some of those compiler flags 🙂
h
Nice :) when you don't know anything about the object, No shared interface or anything, you can load the class and access the object instance with reflection by convention name, the convention can be seen here https://jonnyzzz.com/blog/2019/02/04/companion-and-object/#:~:text=There%20are%20two%20ways%20to,that%20may%20have%20own%20constructor.
v
Nah, it's my own code, I know exactly what Object is in there. Just have to see how I get it loaded and accessed now
Yay, it's working 😄
Copy code
URLClassLoader(
        arrayOf(classesDir.toURI().toURL()),
        buildscript.classLoader
)
        .loadClass("net.kautler.dao.ResultSerializer")
        .getField("INSTANCE")
        .get(null)
        .cast<KSerializer<Result>>()
        .let { Json(Stable.copy(prettyPrint = true)).stringify(it, this) }
        .also { reportFile.writeText(it) }
Ah great and now i came to the conclusion that in 99.8% of cases the file is already compiles as side-effect so I can just use it and only kick off the embedded compiler if really necessary. 😄
In case you care @Hanno the K2JVMCompiler is interal API and not intended to be used by us. But thankfully Ilya helped me to get it working otherwise by using Kotlin scripting support. There are classloading issues when doing this in a Gradle build script where I need it, otherwise I could have simply used JSR-223 support or
BasicJvmScriptingHost
. But using the compiler and evaluator directly worked even from a Gradle build script. So what I have now is:
Copy code
file("build/dependencyUpdates/report.json")
        .apply { parentFile.mkdirs() }
        .also { reportFile ->
            val resultSerializer = resultSerializerByClassLoader ?: resultSerializerByScriptingHost
            Json(Stable.copy(prettyPrint = true))
                    .stringify(resultSerializer, this)
                    .also { reportFile.writeText(it) }
        }

/* ... */

@Suppress("UNCHECKED_CAST")
val resultSerializerByClassLoader
    get() = layout
            .buildDirectory
            .dir("classes/kotlin/main")
            .get()
            .let { classesDir ->
                if (!classesDir.file("net/kautler/dao/ResultSerializer.class").asFile.isFile) {
                    return@let null
                }

                URLClassLoader(
                        arrayOf(classesDir.asFile.toURI().toURL()),
                        buildscript.classLoader
                )
                        .loadClass("net.kautler.dao.ResultSerializer")
                        .kotlin
                        .objectInstance
                        as KSerializer<Result>?
            }

@Suppress("UNCHECKED_CAST")
val resultSerializerByScriptingHost
    get() = runBlocking {
        JvmScriptCompiler(defaultJvmScriptingHostConfiguration)(
                """
                    ${file("src/main/kotlin/net/kautler/dao/ResultSerializer.kt").readText()}
                    ResultSerializer
                """.toScriptSource(),
                ScriptCompilationConfiguration {
                    jvm {
                        dependenciesFromCurrentContext(
                                "gradle-versions-plugin",
                                "kotlinx-serialization-runtime",
                                "groovy-all"
                        )
                    }
                }
        ).onSuccess { BasicJvmScriptEvaluator()(it) }
    }
            .valueOrThrow()
            .returnValue
            .let { it as Value }
            .value
            as KSerializer<Result>
🙂
h
Thanks for letting me know! I knew that, i took how it works from i think it was the gradle kotlin plugin. I also tried to use Scripting first but it's soooooo much slower... And since i am using all that in a private Project which is a very performance focused game engine, i don't care too much about the small changes that happen every now and then :) but yea, for your use case that seems to be more appropriate. Alternative would be to declare an adhoc sourceset, add a compilation task, load the resulting class files :)
v
Well, for my specific case in 99.8% of cases the file is already compiled anyway. :-)