Running Kotlin Scripts with bindings from a Kotlin program using ScriptEngine

Hello,

I am currently implementing a library that is able to run Kotlin scripts (*.kts) from a Kotlin program. The code kept in the .kts files is mostly configuration that I want to change it at runtime if needed.

The class responsible with running the scripts looks like this:

package net.andreinc.mapneat.scripting

import org.jetbrains.kotlin.cli.common.environment.setIdeaIoUseFallback
import java.io.File
import java.lang.IllegalStateException
import java.nio.charset.Charset
import javax.script.*

object KotlinScriptRunner {

init {
    setIdeaIoUseFallback()
}

private val scriptManager = ScriptEngineManager(currentClassLoader())
private val scriptEngine : ScriptEngine = scriptManager.getEngineByExtension("kts")!!

fun getScriptEngine() : ScriptEngine {
    return this.scriptEngine
}

private fun readResource(resource: String, charset: Charset = Charsets.UTF_8) : String {
    val resourceUrl = javaClass.classLoader?.getResource(resource)
    return resourceUrl!!.readText(charset)
}

private fun currentClassLoader() : ClassLoader {
    return Thread.currentThread().contextClassLoader
}

fun eval(scriptContent: String, compileFirst: Boolean = true, bindings: Map<String, String> = mapOf()) : Any {
    if (compileFirst) {
        when (scriptEngine) {
            is Compilable -> {
                val compiledCode = scriptEngine.compile(scriptContent)
                return compiledCode.eval(scriptEngine.createBindings().apply { putAll(bindings) })
            }
            else -> throw IllegalStateException("Kotlin is not an instance of Compilable. Cannot compile script.")
        }
    }
    else {
        return scriptEngine.eval(scriptContent, scriptEngine.createBindings().apply { putAll(bindings) })
    }
}

fun evalFile(file: String, readAsResource: Boolean, compileFirst: Boolean = true, bindings: Map<String, String> = mapOf()) : Any {
    val content : String = if (readAsResource) readResource(file) else File(file).readText()
    return eval(content, compileFirst, bindings)
}

}

As you can see the eval method allows me to send bindings as a map (variables that I can pass to the script).

Problem is in the Script those variables have to be accessed from bindings[variableName]. E.g .kts:

json(bindings["json"] as String) {
    "name" *= "$.name"
    "a" /= bindings["a"] as String
}.getPrettyString()

Problem is that in IntelliJ bindings[] is an unresolved reference and the Scripts gets invalidated. I can run the script from the command-line, but in IntelliJ i have problems, because this invalidates my project:

  • Has anyone of you knows how to handle this situation in IntelliJ?
  • Is there another way to pass bindings to the Kotlin Script?
  • Given the experimental nature of the code I am using, is the code looking ok from your perspective?
1 Like

You can have a look at a simple implementation I played with today. I am giving context by using an implicit receiver: https://github.com/bjonnh/konbu/blob/26fa43c4819f773fa0d94fd8ea219eddb2316d4b/src/main/kotlin/net/nprod/konbu/main.kt

1 Like

@bjonnh I’ve tried to wrap my head around this solution. I kinda understand what you did there, but where did you found the documentation for this ? I’ve looked through the web and couldn’t find any documentation about implicit receivers.

Do you have any source for this ?

Also how “stable” is going to be this approach in the long run, looking at the code I see “experimental” in the package name, this means that the API might break in the future, correct ?

All in all, thanks for the help, I really appreciate it!

@nomemory there is really little doc so i looked at kotlinls scripting repository where they have examples and used github’s code search (super useful for poorly documented things). For the implicit receivers i found that by looking in intellij code completion and found it that way then looked into kotlin’s code to understand how it works…

As it is not documented i expect those things to break yeah…

As you can see it was quite an adventure…

@bjonnh I did the same, and I believe I found a small bug https://youtrack.jetbrains.com/issue/KT-43176 regarding the implementation.

I eventually end-up using your repo and the kotlin repos to understand how to use the API. Eventually I made it work. Quite an adventure.

Thanks for the suggestion.

2 Likes