kotlinclassloaderkotlin-script

Running a Kotlin script in Kotlin code resulting in ClassCastException due to class loader differences


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


Solution

  • 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:

    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