Add bindings to Kotlin JSR 223 engine

I am trying to use a Kotlin JSR 223 engine to expose Kotlin as a scripting language in a Java GUI. The GUI has support for multiple languages already and injects variables through the engine scope bindings. This works for all languages but Kotlin does not see any of the bindings. Conversely, any variables that are defined in Kotlin, are not available to other languages. I assume that these issues are related.

I created a minimum working example. I used Java because the code base is Java but I also translated to Kotlin to confirm that the issue also occurs there.

  1. Java MWE
    import kotlin.jvm.JvmClassMappingKt;
    import kotlin.reflect.KClass;
    import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineFactoryBase;
    import org.jetbrains.kotlin.cli.common.repl.ScriptArgsWithTypes;
    import org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngine;
    import org.jetbrains.kotlin.script.jsr223.KotlinStandardJsr223ScriptTemplate;
    
    import javax.script.Bindings;
    import javax.script.ScriptContext;
    import javax.script.ScriptEngine;
    import javax.script.ScriptException;
    import java.io.File;
    import java.net.URL;
    import java.net.URLClassLoader;
    import java.util.Arrays;
    import java.util.stream.Collectors;
    
    public class MWE {
    
        private static class Factory extends KotlinJsr223JvmScriptEngineFactoryBase {
    
            @Override
            public ScriptEngine getScriptEngine() {
                return new KotlinJsr223JvmLocalScriptEngine(
                        this,
                        Arrays.stream(((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs()).map(URL::getFile).map(File::new).collect(Collectors.toList()),
                        KotlinStandardJsr223ScriptTemplate.class.getCanonicalName(),
                        (ctx, types) -> new ScriptArgsWithTypes(new Object[]{ctx.getBindings(ScriptContext.ENGINE_SCOPE)}, types == null ? new KClass[0] : types),
                        new KClass[] {JvmClassMappingKt.getKotlinClass(Bindings.class) }
                );
            }
        }
    
        public static void main(String... args) throws ScriptException {
            final ScriptEngine engine = new Factory().getScriptEngine();
            engine.getBindings(ScriptContext.ENGINE_SCOPE).put("abc", 1);
            System.out.println(engine.eval("1"));
            System.out.println(engine.eval("abc"));
        }
    }
    
  2. Kotlin MWE
    import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineFactoryBase
    import org.jetbrains.kotlin.cli.common.repl.ScriptArgsWithTypes
    import org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngine
    import org.jetbrains.kotlin.script.jsr223.KotlinStandardJsr223ScriptTemplate
    import java.io.File
    import java.net.URL
    import java.net.URLClassLoader
    import java.util.*
    import java.util.stream.Collectors
    import javax.script.Bindings
    import javax.script.ScriptContext
    import javax.script.ScriptEngine
    import kotlin.reflect.KClass
    
    private class Factory : KotlinJsr223JvmScriptEngineFactoryBase() {
        override fun getScriptEngine(): ScriptEngine {
            return KotlinJsr223JvmLocalScriptEngine(
                    this,
                    Arrays.stream((ClassLoader.getSystemClassLoader() as URLClassLoader).urLs).map { obj: URL -> obj.file }.map { File(it) }.collect(Collectors.toList()),
                    KotlinStandardJsr223ScriptTemplate::class.java.canonicalName,
                    { ctx: ScriptContext, types: Array<out KClass<out Any>>? ->
                        ScriptArgsWithTypes(arrayOf<Any?>(ctx.getBindings(ScriptContext.ENGINE_SCOPE)), types ?: emptyArray())
                    },
                    arrayOf(Bindings::class))
        }
    }
    
    fun main() {
        val engine = Factory().scriptEngine
        engine.getBindings(ScriptContext.ENGINE_SCOPE)["abc"] = 1
        println(engine.eval("1"))
        println(engine.eval("abc"))
    }
    
  3. relevant output (equivalent in both cases, just posting the Kotlin MWE output):
    1
    Exception in thread "main" javax.script.ScriptException: error: unresolved reference: abc
    abc
    ^
    
        at org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineBase.asJsr223EvalResult(KotlinJsr223JvmScriptEngineBase.kt:104)
        at org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineBase.compileAndEval(KotlinJsr223JvmScriptEngineBase.kt:63)
        at org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineBase.eval(KotlinJsr223JvmScriptEngineBase.kt:31)
        at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264)
        at MWEKt.main(MWE.kt:33)
        at MWEKt.main(MWE.kt)
    

The engine can clearly evaluate a statement that does not rely on information in the bindings: It prints 1 for the first call engine.eval("1"). For the second call, engine.eval("abc"), it does not find/use abc in the bindings.

Do I use the engine incorrectly?

There is also another exception

java.lang.NoClassDefFoundError: com/sun/jna/Native

but it does not crash the application so I don’t know if it is relevant. I can share the entire stack trace if this seems relevant.

1 Like

For completeness, I use kotlin version 1.4.10:

$ mvn dependency:tree
org.scijava:scripting-kotlin:jar:0.1.2-SNAPSHOT
+- org.scijava:scijava-common:jar:2.83.3:compile
|  +- org.scijava:parsington:jar:2.0.0:compile
|  \- org.bushe:eventbus:jar:1.4:compile
+- org.jetbrains.kotlin:kotlin-compiler-embeddable:jar:1.4.10:compile
|  +- org.jetbrains.kotlin:kotlin-stdlib:jar:1.4.10:compile
|  |  +- org.jetbrains.kotlin:kotlin-stdlib-common:jar:1.4.10:compile
|  |  \- org.jetbrains:annotations:jar:13.0:compile
|  +- org.jetbrains.kotlin:kotlin-reflect:jar:1.4.10:runtime
|  +- org.jetbrains.kotlin:kotlin-daemon-embeddable:jar:1.4.10:runtime
|  \- org.jetbrains.intellij.deps:trove4j:jar:1.0.20181211:compile
+- org.jetbrains.kotlin:kotlin-script-util:jar:1.4.10:compile
|  +- org.jetbrains.kotlin:kotlin-scripting-jvm:jar:1.4.10:compile
|  |  \- org.jetbrains.kotlin:kotlin-scripting-common:jar:1.4.10:compile
|  \- org.jetbrains.kotlin:kotlin-daemon-client:jar:1.4.10:compile
|     \- org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:1.3.7:compile
+- org.jetbrains.kotlin:kotlin-script-runtime:jar:1.4.10:runtime
+- org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:jar:1.4.10:runtime
|  \- org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:jar:1.4.10:runtime
+- junit:junit:jar:4.13:test
|  \- org.hamcrest:hamcrest-core:jar:1.3:test
\- org.scijava:scijava-common:jar:tests:2.83.3:test

I found out I can access the bindings via

engine.eval("bindings[\"abc\"]")

This gives me the expected result but I wonder what the best way forward is. Should I wrap the KotlinJsr223JvmLocalScriptEngine and replace all words xyz in the evaluation string with bindings["xyz"] if xyz is in bindings? That way, I would also need to make sure that there is no clash between values in bindings and reserved Kotlin keywords like when (the bindings are shared between engines).

If there is any way the KotlinJsr223JvmLocalScriptEngine could do all that for me, that would be much preferred.

Perhaps your dependencies are out of date?

YouTrack suggests this was fixed : Support JSR 223 bindings directly via script variables

And the dependencies Kotlin’s source examples look quite different: Kotlin Scripting Examples: using Kotlin scripting via JSR 223 API

Note: I haven’t used the scripting engine, just looked around out of curiosity.

1 Like

That was indeed the solution. I first tried to update to the latest Kotlin release but that didn’t solve it, I had to update the actual dependencies. This is my mvn dependency:tree after the updates:

org.scijava:scripting-kotlin:jar:0.1.2-SNAPSHOT
+- org.scijava:scijava-common:jar:2.83.3:compile
|  +- org.scijava:parsington:jar:2.0.0:compile
|  \- org.bushe:eventbus:jar:1.4:compile
+- org.jetbrains.kotlin:kotlin-compiler-embeddable:jar:1.4.21:compile
|  +- org.jetbrains.kotlin:kotlin-script-runtime:jar:1.4.21:runtime
|  +- org.jetbrains.kotlin:kotlin-reflect:jar:1.4.21:runtime
|  +- org.jetbrains.kotlin:kotlin-daemon-embeddable:jar:1.4.21:runtime
|  \- org.jetbrains.intellij.deps:trove4j:jar:1.0.20181211:runtime
+- org.jetbrains.kotlin:kotlin-stdlib:jar:1.4.21:compile
|  +- org.jetbrains.kotlin:kotlin-stdlib-common:jar:1.4.21:compile
|  \- org.jetbrains:annotations:jar:13.0:compile
+- org.jetbrains.kotlin:kotlin-scripting-jsr223:jar:1.4.21:runtime
|  +- org.jetbrains.kotlin:kotlin-scripting-common:jar:1.4.21:runtime
|  |  \- org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:1.3.7:runtime
|  +- org.jetbrains.kotlin:kotlin-scripting-jvm:jar:1.4.21:runtime
|  +- org.jetbrains.kotlin:kotlin-scripting-jvm-host:jar:1.4.21:runtime
|  \- org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:jar:1.4.21:runtime
|     \- org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:jar:1.4.21:runtime
+- junit:junit:jar:4.13:test
|  \- org.hamcrest:hamcrest-core:jar:1.3:test
\- org.scijava:scijava-common:jar:tests:2.83.3:test

I don’t believe I can mark an answer as a solution here, otherwise I’d mark yours @nickallendev