A JSR-223 (javax.script) compliant JavaScript engine backed by QuickJS running on WebAssembly.
It supports the standard ScriptEngine API plus the Compilable and Invocable interfaces.
<dependency>
<groupId>io.roastedroot</groupId>
<artifactId>quickjs4j-scripting-experimental</artifactId>
<version>${quickjs4j.version}</version>
</dependency>The engine registers itself via SPI, so ScriptEngineManager can discover it by name, file extension, or MIME type:
import javax.script.*;
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("quickjs4j");
// or: manager.getEngineByExtension("js");
// or: manager.getEngineByMimeType("application/x-javascript");You can also create an engine directly:
import io.roastedroot.quickjs4j.scripting.JsScriptEngine;
JsScriptEngine engine = new JsScriptEngine();eval() returns the value of the last expression — no return keyword needed:
engine.eval("1 + 2"); // 3
engine.eval("'hello'"); // "hello"
engine.eval("true"); // true
engine.eval("null"); // null
engine.eval("var x = 3"); // null (declarations don't produce values)Bindings pass Java values into the JavaScript scope with correct types preserved (numbers stay numbers, booleans stay booleans):
engine.put("x", 42);
engine.put("pi", 3.14);
engine.put("flag", true);
engine.put("name", "world");
engine.eval("x + 10"); // 52 (not "4210")
engine.eval("'hello ' + name"); // "hello world"
engine.eval("flag === true"); // trueThe JavaScript runtime persists between eval() calls. Variables declared with var and function declarations are added to the global scope:
engine.eval("var counter = 0");
engine.eval("function increment() { return ++counter; }");
engine.eval("increment()"); // 1
engine.eval("increment()"); // 2
engine.eval("counter"); // 2Note:
letandconstdeclarations are block-scoped to eacheval()call and do not persist. Usevaror assign toglobalThisfor cross-eval state.
The engine implements javax.script.Compilable. Compile a script once and evaluate it repeatedly, potentially with different bindings:
Compilable compilable = (Compilable) engine;
CompiledScript compiled = compilable.compile("x * 2");
engine.put("x", 5);
compiled.eval(); // 10
engine.put("x", 7);
compiled.eval(); // 14The engine implements javax.script.Invocable.
Call a top-level JavaScript function by name:
engine.eval("function add(a, b) { return a + b; }");
Invocable invocable = (Invocable) engine;
Object result = invocable.invokeFunction("add", 3, 4); // 7If the function does not exist, a NoSuchMethodException is thrown:
try {
invocable.invokeFunction("nonExistent");
} catch (NoSuchMethodException e) {
// "nonExistent"
}Call a method on a JavaScript object. The first argument (thiz) is a String identifying the variable name in JavaScript's global scope:
engine.eval("var calc = {"
+ " add: function(a, b) { return a + b; },"
+ " multiply: function(a, b) { return a * b; }"
+ "}");
Invocable invocable = (Invocable) engine;
invocable.invokeMethod("calc", "add", 2, 3); // 5
invocable.invokeMethod("calc", "multiply", 2, 3); // 6Limitation: Because values cross a WASM boundary via JSON serialization, live JavaScript object references cannot be held in Java. Unlike Nashorn or JRuby, the
thizparameter must be aStringnaming a variable in JavaScript's global scope — not a Java object returned from a previouseval(). Passing a non-String object throwsIllegalArgumentException.
Create a Java interface proxy backed by JavaScript functions. Each interface method call is delegated to a JavaScript function of the same name.
Define a Java interface:
public interface Calculator {
Object add(int a, int b);
Object multiply(int a, int b);
}Bind it to top-level functions:
engine.eval("function add(a, b) { return a + b; }");
engine.eval("function multiply(a, b) { return a * b; }");
Calculator calc = ((Invocable) engine).getInterface(Calculator.class);
calc.add(2, 3); // 5
calc.multiply(2, 3); // 6Or bind it to methods on a JavaScript object using the two-argument form. As with invokeMethod, thiz must be a String variable name:
engine.eval("var math = {"
+ " add: function(a, b) { return a + b; },"
+ " multiply: function(a, b) { return a * b; }"
+ "}");
Calculator calc = ((Invocable) engine).getInterface("math", Calculator.class);
calc.add(3, 4); // 7
calc.multiply(3, 4); // 12By default, console.log and console.error output is written to the ScriptContext writers (which default to System.out and System.err).
You can redirect output by providing a custom ScriptContext:
ScriptContext ctx = new SimpleScriptContext();
StringWriter out = new StringWriter();
StringWriter err = new StringWriter();
ctx.setWriter(out);
ctx.setErrorWriter(err);
ctx.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
engine.eval("console.log('hello')", ctx);
out.toString(); // "hello\n"
engine.eval("console.error('oops')", ctx);
err.toString(); // "oops\n"JsScriptEngine implements AutoCloseable. Use try-with-resources to ensure the underlying WASM runtime is released:
try (JsScriptEngine engine = new JsScriptEngine()) {
engine.eval("1 + 1"); // 2
}JavaScript syntax errors and runtime errors are thrown as javax.script.ScriptException:
try {
engine.eval("function {{{");
} catch (ScriptException e) {
// syntax error
}
try {
engine.eval("undeclaredVar");
} catch (ScriptException e) {
// ReferenceError: 'undeclaredVar' is not defined
}import javax.script.*;
import io.roastedroot.quickjs4j.scripting.JsScriptEngine;
public interface Greeter {
Object greet(String name);
Object greetAll(String names);
}
public class Example {
public static void main(String[] args) throws Exception {
try (JsScriptEngine engine = new JsScriptEngine()) {
// Define functions
engine.eval(
"function greet(name) { return 'Hello, ' + name + '!'; }\n" +
"function greetAll(names) {\n" +
" return names.split(',').map(n => greet(n.trim())).join(' ');\n" +
"}"
);
// Use via invokeFunction
Invocable inv = (Invocable) engine;
System.out.println(inv.invokeFunction("greet", "World"));
// Hello, World!
// Use via getInterface
Greeter greeter = inv.getInterface(Greeter.class);
System.out.println(greeter.greetAll("Alice, Bob"));
// Hello, Alice! Hello, Bob!
}
}
}