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?
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.