I want to load a kotlin script inside my program. I want to get the result of the script (the object returned) and use it in my kotlin code.
Here's what I have for my main method:
fun loadScript(scriptPath: String): SomeClass? {
val scriptFile = File(scriptPath)
val cl = SomeClass::class.java.classLoader
val evalConfig = ScriptCompilationConfiguration {
jvm {
this.dependenciesFromClassloader(classLoader = cl, wholeClasspath = true)
}
ide {
acceptedLocations(ScriptAcceptedLocation.Everywhere)
}
defaultImports("model.*")
}
val evalResult = BasicJvmScriptingHost().eval(
scriptFile.toScriptSource(),
evalConfig,
ScriptEvaluationConfiguration {
jvm {
baseClassLoader(cl)
}
}
)
return when (val result = evalResult) {
is ResultWithDiagnostics.Success -> {
println(SomeClass::class.java.classLoader)
println(result.value.returnValue.scriptInstance!!::class.java.classLoader)
result.value.returnValue.scriptInstance as SomeClass
}
is ResultWithDiagnostics.Failure -> {
println("Script execution failed: ${result.reports}")
null
}
}
}
fun main() {
val scriptResult = loadScript("script.kts") //?: return
println(scriptResult)
scriptResult!!.hello()
}
Here's the SomeClass code:
package model
class SomeClass {
fun hello() {
println("Hello World")
}
}
fun getSomeObject() = SomeClass()
Here's the script code:
getSomeObject()
My issue is that i keep getting the following error:
Exception in thread "main" java.lang.ClassCastException: class Script cannot be cast to class model.SomeClass (Script is in unnamed module of loader org.jetbrains.kotlin.scripting.compiler.plugin.impl.CompiledScriptClassLoader @526fc044; model.SomeClass is in unnamed module of loader 'app')
at MainKt.loadScript(Main.kt:272)
at MainKt.main(Main.kt:283)
at MainKt.main(Main.kt)
When I try to print the class loader I see i have:
jdk.internal.loader.ClassLoaders$AppClassLoader@76ed5528
for SomeClass::class.java.classLoader
and
org.jetbrains.kotlin.scripting.compiler.plugin.impl.CompiledScriptClassLoader@526fc044
for result.value.returnValue.scriptInstance!!::class.java.classLoader
Of course I understand the issue is with the class loader and that despite having the same name, the SomeClass object instanciated in the script is different than the SomeClass I try to cast into. What I don't understand is why this is happening when I specify the same class loader in the ScriptCompilationConfiguration
and ScriptEvaluationConfiguration
I found the answer by looking around some more. Basically result.value.returnValue.scriptInstance
cannot be cast as SomeClass
because it is not even the result of the script in the first place. result.value.returnValue
is in fact of the type of a sealed class named ResultValue
which represents the result of the script. It is either:
An object (the last expression of the script)
Unit
(if there's nothing returned)
Error
NotEvaluated
The first one being what I want I just have to cast result.value.returnValue
into ResultValue.Value
and retrieve the value
property.
Here's a simple example using BasicJvmScriptingHost
:
fun main() {
val scriptResult = loadSomeClass("script.test.kts") ?: return
println(scriptResult)
println(scriptResult::class)
scriptResult.hello()
}
fun loadSomeClass(scriptPath: String): SomeClass? {
val evaluationResult = BasicJvmScriptingHost().eval(
File(scriptPath).toScriptSource(),
ScriptCompilationConfiguration {
jvm {
// This is necessary
dependenciesFromClassloader(wholeClasspath = true)
}
defaultImports(SomeClass::class)
defaultImports("model.*")
},
ScriptEvaluationConfiguration()
)
// Unwrapping the results
return when(evaluationResult) {
is ResultWithDiagnostics.Success -> {
when(val returnedValue = evaluationResult.value.returnValue) {
is ResultValue.Error -> TODO()
is ResultValue.NotEvaluated -> TODO()
is ResultValue.Unit -> TODO()
is ResultValue.Value -> {
// Value property is only available in this branch
val scriptReturnedObject = returnedValue.value as SomeClass
scriptReturnedObject
}
}
}
is ResultWithDiagnostics.Failure -> {
println("Script execution failed: ${evaluationResult.reports}")
null
}
}
}
Here's another example using the KotlinScript
annotation to provide the ScriptCompilationConfiguration
to the eval()
method:
fun main() {
val scriptResult = loadSomeClass2("script.kts") ?: return
println(scriptResult)
println(scriptResult::class)
scriptResult.hello()
}
fun loadSomeClass2(scriptPath: String): SomeClass? {
val config = createJvmCompilationConfigurationFromTemplate<TestScript>()
val evaluationResult = BasicJvmScriptingHost().eval(
File(scriptPath).toScriptSource(),
config,
ScriptEvaluationConfiguration()
)
// Unwrapping the results
return when(evaluationResult) {
is ResultWithDiagnostics.Success -> {
when(val returnedValue = evaluationResult.value.returnValue) {
is ResultValue.Error -> TODO()
is ResultValue.NotEvaluated -> TODO()
is ResultValue.Unit -> TODO()
is ResultValue.Value -> {
// Value property is only available in this branch
val scriptReturnedObject = returnedValue.value as SomeClass
scriptReturnedObject
}
}
}
is ResultWithDiagnostics.Failure -> {
println("Script execution failed: ${evaluationResult.reports}")
null
}
}
}
const val TEST_FILE_EXTENSION = "test.kts"
@KotlinScript(
displayName = "Test Script",
fileExtension = TEST_FILE_EXTENSION,
compilationConfiguration = TestScriptConfiguration::class
)
abstract class TestScript
object TestScriptConfiguration : ScriptCompilationConfiguration(
{
defaultImports(SomeClass::class)
defaultImports("model.*")
ide {
acceptedLocations(ScriptAcceptedLocation.Everywhere)
}
jvm {
dependenciesFromCurrentContext(wholeClasspath = true)
}
}
) {
private fun readResolve(): Any = TestScriptConfiguration
}
And lastly I found a third way albeit too simple to execute kotlin script code and retrieve the value (not sure if imports could work). I wanted to include here in case someone is interested:
// Using Script Engine
fun main() {
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
val script = """
val x = 10
val y = 20
x + y // Last expression, this will be the result
""".trimIndent()
val result = engine.eval(script)
println(result) // Output: 30
}
I hope this helps someone =D