I get it. The problem I ran into was that invokeFunction would throw a NoSuchMethodException because the functions exposed by the user script did not exist in the bindings from the default scope:
ScriptContext context = new SimpleScriptContext(); context.setBindings(nashorn.createBindings(), ScriptContext.ENGINE_SCOPE); engine.eval(customScriptSource, context); ((Invocable) engine).invokeFunction(name, args);
So, I needed to get the function out of context by name and call it explicitly like this:
JSObject function = (JSObject) context.getAttribute(name, ScriptContext.ENGINE_SCOPE); function.call(null, args);
This will call a function that exists in your newly created context. You can also call 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 exception (a Throwable exception wrapped in a RuntimeException or NashornException that was NashornException using the JavaScript stack stack information), so you may need to explicitly handle this if you want to provide useful feedback.
Thus, threads cannot step over each other because each thread has a separate context. I was also able to share custom runtime between threads and make sure that state changes in mutable objects subjected to custom execution were isolated by context.
To do this, I create an instance of CompiledScript that contains a compiled view 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 the script, I evaluate the compiled source, and then evaluate the script in this context too:
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 checked this with multiple threads, and I was able to verify that state changes were limited by contexts belonging to individual threads. This is because the compiled view is executed in a specific context, which means that the instances of something that it displays are tied to that context.
One of the small drawbacks is that you can unjustifiably overestimate the definitions of objects for objects that do not need a stream-dependent state. To get around this, evaluate them directly on the engine, which will add the bindings for these objects to the ENGINE_SCOPE engine:
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- ENGINE_SCOPE context with the ENGINE_SCOPE engine:
context.getBindings(ScriptContext.ENGINE_SCOPE).putAll(engine.getBindings(ScriptContext.ENGINE_SCOPE));
One thing you will need to do is make sure that all such objects that you have opened have been frozen. Otherwise, you can override or add properties to them.