Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Executing a function in a specific context in Nashorn

I have a custom Nashorn runtime that I set up with some global functions and objects - some of these are stateless and some of these are stateful. Against this runtime, I am running some custom scripts.

For each execution, I am planning on creating a new context that is backed by the global context:

myContext.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
engine.eval(myScript, myContext);

Based on what I read, any modifications to the global scope (from the perspective of the script) will be limited to the new context I created.

These scripts, when evaluated, expose some objects (with well-defined names and method-names). I can invoke a method on the object by casting engine to Invocable. But how do I know the context in which the function will run? Is that even an issue, or is the execution context of that function set up based on the context in which it was evaluated?

What behavior can I expect in a multithreaded situation where all threads share the same script-engine instance, and they all try to run the same script (which exposes a global object). When I then invoke the method on the object, in which context will the function run? How will it know which instance of the object to to use?

I was expecting to see an invoke method where I can specify the context, but this doesn't seem to be the case. Is there a way to do this, or am I going about this completely wrong?

I know that an easy way to get around this is to create a new script-engine instance per execution, but as I understand, I would lose optimizations (especially on the shared code). That being said, would pre-compiling help here?

like image 808
Vivin Paliath Avatar asked Nov 09 '15 23:11

Vivin Paliath


People also ask

Is Nashorn deprecated?

The Nashorn engine has been deprecated in JDK 11 as part of JEP 335 and and has been removed from JDK15 as part of JEP 372. GraalVM can step in as a replacement for JavaScript code previously executed on the Nashorn engine. GraalVM provides all the features for JavaScript previously provided by Nashorn.

What is Nashorn in Java 8?

With Java 8, Nashorn, a much improved javascript engine is introduced, to replace the existing Rhino. Nashorn provides 2 to 10 times better performance, as it directly compiles the code in memory and passes the bytecode to JVM.

What is JDK Nashorn?

Nashorn is the only JavaScript engine included in the JDK. However, you can use any script engine compliant with JSR 223: Scripting for the Java Platform or implement your own; see Scripting Languages and Java in Java Platform, Standard Edition Java Scripting Programmer's Guide .


1 Answers

I figured this out. The problem I was running into was that invokeFunction would throw a NoSuchMethodException because the functions exposed by the custom script didn't exist in the bindings from the engine's default scope:

ScriptContext context = new SimpleScriptContext();
context.setBindings(nashorn.createBindings(), ScriptContext.ENGINE_SCOPE);
engine.eval(customScriptSource, context);
((Invocable) engine).invokeFunction(name, args); //<- NoSuchMethodException thrown

So what I had to do was pull out the function from the context by name and call it explicitly like so:

JSObject function = (JSObject) context.getAttribute(name, ScriptContext.ENGINE_SCOPE);
function.call(null, args); //call to JSObject#isFunction omitted brevity 

This will call the function that exists in your newly-created context. You can also invoke methods on objects this way:

JSObject object = (JSObject) context.getAttribute(name, ScriptContext.ENGINE_SCOPE);
JSObject method = (JSObject) object.getMember(name);
method.call(object, args);

call throws an unchecked exception (either Throwable wrapped in a RuntimeException or NashornException that has been initialized with JavaScript stackframe information) so you may have to explicitly handle that if you want to provide useful feedback.

This way threads can't step over each other because there is a separate context per thread. I was also able to share custom runtime-code between the threads and ensure that state changes to mutable-objects exposed by the custom-runtime were isolated by context.

To do this, I create a CompiledScript instance that contains a compiled representation of my custom runtime-library:

public class Runtime {

    private ScriptEngine engine;
    private CompiledScript compiledRuntime;

    public Runtime() {
        engine = new NashornScriptEngineFactory().getScriptEngine("-strict");
        String source = new Scanner(
            this.getClass().getClassLoader().getResourceAsStream("runtime/runtime.js")
        ).useDelimiter("\\Z").next();

        try {
            compiledRuntime = ((Compilable) engine).compile(source);
        } catch(ScriptException e) {
            ...
        }
    }

    ...
}

Then when I need to execute a script I evaluate the compiled source, and then evaluate the script against that context as well:

ScriptContext context = new SimpleScriptContext();
context.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);

//Exception handling omitted for brevity

//Evaluate the compiled runtime in our new context
compiledRuntime.eval(context); 

//Evaluate the source in the same context
engine.eval(source, context);

//Call a function
JSObject jsObject = (JSObject) context.getAttribute(function, ScriptContext.ENGINE_SCOPE);
jsObject.call(null, args);

I tested this out with multiple threads and I was able to make sure that state changes were limited to the contexts that belong to individual threads. This is because the compiled representation is executed within a specific context, which means that instances of anything exposed by it are scoped to that context.

One small disadvantage here is that you may be needlessly reevaluating object definitions for objects that don't need to have thread-specific state. To get around this, evaluate them on the engine directly, which will add bindings for those objects to the engine's ENGINE_SCOPE:

public Runtime() {
    ...
    String shared = new Scanner(
        this.getClass().getClassLoader().getResourceAsStream("runtime/shared.js")
    ).useDelimiter("\\Z").next();

    try {
        ...        
        nashorn.eval(shared);
        ...
    } catch(ScriptException e) {
        ...
    }
}

Then later, you can populate the thread-specific context from the engine's ENGINE_SCOPE:

context.getBindings(ScriptContext.ENGINE_SCOPE).putAll(engine.getBindings(ScriptContext.ENGINE_SCOPE));

One thing you will need to do is make sure that any such objects that you expose, have been frozen. Otherwise it is possible to redefine or add properties to them.

like image 192
Vivin Paliath Avatar answered Nov 01 '22 08:11

Vivin Paliath