QuickJS Java is a Java library that allows you to embed and execute JavaScript code using the highly efficient QuickJS engine by Fabrice Bellard. It leverages a Rust-built WebAssembly (Wasm) library, which in turn uses rquickjs, and executes it on the Java Virtual Machine (JVM) using Chicory, a pure-Java WebAssembly runtime without native dependencies.
While several mature JavaScript runtimes exist for Java, such as Nashorn and GraalVM JS, they often integrate deeply with the Java runtime, potentially posing security or stability concerns by allowing extensive access to the JVM.
QuickJS Java offers a distinct alternative with a focus on security, stability, and ease of deployment:
- Secure Isolation: Provides a lean, well-defined, and type-safe interface. JavaScript scripts can only access objects explicitly passed into the runtime, offering a sandboxed environment with no inherent access to the broader Java application.
- Resource Control: Easily impose hard limits on script execution time and memory consumption, mitigating the impact of malicious or faulty scripts.
- Ideal for Scripting: Perfect for integrating small, well-defined calculation or validation scripts. Its safe nature allows trusted users to write scripts without compromising application integrity.
- Zero Native Dependencies: Unlike traditional approaches that require platform-specific JNI libraries, this library uses WebAssembly and Chicory, eliminating native dependencies at runtime. This simplifies deployment across different environments.
Comparison with quickjs-java: This project is a successor to quickjs-java. The key difference is the approach to interfacing with QuickJS:
quickjs-javauses custom Rust-based JNI libraries, offering direct interaction without serialization overhead but requiring platform-specific builds.quickjs-wasm-javaemploys WebAssembly, removing native library requirements but introducing serialization (MessagePack) overhead for complex data types. This is partly mitigated by native wrappers for JS objects and arrays, allowing direct manipulation without constant serialization/deserialization.
Other JVM-based QuickJS projects:
- Quack: "Quack provides Java (Android and desktop) bindings to JavaScript engines."
- QuickJS - KT: "Run your JavaScript code in Kotlin, asynchronously."
To build the project, you need:
- Java 21 or newer
- Rust with
cargo - The
wasm32-wasip1target for Rust. Install it using:rustup target add wasm32-wasip1
Optionally one can use wasm-opt to further optimize the build. Install it using:
cargo install wasm-optThe project uses Maven and is pre-configured to build the WebAssembly library using the exec-maven-plugin.
-
Standard Debug Build: A simple
mvn clean installwill build and test the entire library. This uses a faster debug build of the Wasm library, suitable for development, but with lower runtime performance. Thechicory-compiler-maven-pluginwill operate with interpreter fallback enabled. -
Optimized Release Build: For optimal performance, use the
releaseMaven profile. This triggers a fully optimized release build of the Rust Wasm library:mvn -P release clean install
If
wasm-optis available, you can invoke it during the build using thewasm-optprofilemvn -P release,wasm-opt clean install
The
chicory-compiler-maven-pluginthen compiles the Wasm bytecode to native code. This approach offers superior performance compared to interpreter mode, faster startup times, and fewer dependencies than Chicory's runtime compiler, making it ideal for GraalVM native images.
To use the library, add the following Maven dependency to your pom.xml. Replace [current version] with the appropriate version number.
<dependency>
<groupId>io.github.stefanrichterhuber</groupId>
<artifactId>quickjs-wasm-java</artifactId>
<version>[current version]</version>
</dependency>Here's a basic example demonstrating how to initialize the runtime, create a context, and execute JavaScript:
import io.github.stefanrichterhuber.quickjswasmjava.QuickJSContext;
import io.github.stefanrichterhuber.quickjswasmjava.QuickJSRuntime;
import java.util.function.BiFunction;
public class QuickJSExample {
public static void main(String[] args) {
try (QuickJSRuntime runtime = new QuickJSRuntime();
QuickJSContext context = runtime.createContext()) {
// Export a Java function to JavaScript
BiFunction<Integer, Integer, Integer> add = (a, b) -> {
System.out.println("Java function 'add' called with: " + a + ", " + b);
return a + b;
};
context.setGlobal("add", add);
// Evaluate JavaScript code
Object result = context.eval("add(1, 2)");
System.out.println("Result from JavaScript: " + result); // Expected: 3
// Accessing global variables from Java
context.eval("let message = 'Hello from QuickJS!';");
Object message = context.getGlobal("message");
System.out.println("Message from JavaScript: " + message); // Expected: "Hello from QuickJS!"
// Error handling
try {
context.eval("throw new Error('Something went wrong in JS!');");
} catch (Exception e) {
System.err.println("JavaScript error caught: " + e.getMessage());
}
}
}
}The context object io.github.stefanrichterhuber.quickjswasmjava.QuickJSContext is the main entry point to interact with the JavaScript runtime. It provides a set of methods to interact with the JavaScript runtime, set global variables, evaluate scripts, and more.
It also implements javax.script.Invocable, allowing to invoke JavaScript functions from Java or map them to Java interfaces.
public interface TestInterface {
int add(int a, int b);
int substract(int a, int b);
}
try (QuickJSRuntime runtime = new QuickJSRuntime();
QuickJSContext context = runtime.createContext()) {
context.eval("function add(a, b) { return a + b; }; function substract(a, b) { return a - b; }; ");
TestInterface testInterface = context.getInterface(TestInterface.class);
assertEquals(3, testInterface.add(1, 2));
assertEquals(1, testInterface.substract(2, 1));
}Moreover the JSR-223 specification is implemented on a best effort basis, allowing it to be used as a script engine in Java applications.
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("QuickJS");
Bindings bindings = engine.createBindings();
bindings.put("a", 10);
bindings.put("b", 20);
Object result = engine.eval("a + b", bindings);
assertEquals(30, result);There is some preliminary high-level async / await support. The special io.github.stefanrichterhuber.quickjswasmjava.QuickJSContext#evalAsync returns wraps any promise into a java.util.concurrent.CompletableFuture and any java.util.concurrent.CompletableFuture is wrapped as a promise. As of now, however, the runtime is not thread-safe, so ensure (by using suitable Executors, for example) to have all interactions with the JS runtime in one thread.
try (QuickJSRuntime runtime = new QuickJSRuntime(); QuickJSContext context = runtime.createContext()) {
CompletableFuture<Integer> cf = new CompletableFuture<>();
Supplier<CompletableFuture<Integer>> answer = () -> {
return cf;
};
context.setGlobal("answer", answer);
CompletableFuture<?> result = context.evalAsync("await answer()");
assertFalse(((CompletableFuture) result).isDone());
while (context.poll()) {
Thread.sleep(10);
}
cf.complete(42);
while (context.poll()) {
Thread.sleep(10);
}
assertTrue(((CompletableFuture) result).isDone());
assertEquals(42, ((CompletableFuture) result).join());
}For more comprehensive examples and detailed usage patterns, refer to the unit tests: io.github.stefanrichterhuber.quickjswasmjava.QuickJSContextTest.
The library handles seamless translation between supported Java and JavaScript types. Most Java values are copied by value into the JavaScript context. However, for efficient interaction with complex data structures, io.github.stefanrichterhuber.quickjswasmjava.QuickJSArray and io.github.stefanrichterhuber.quickjswasmjava.QuickJSObject act as thin wrappers over native QuickJS arrays and objects. Changes made to these wrapper objects from either Java or JavaScript are directly reflected on the other side, avoiding costly serialization/deserialization for large structures. Both QuickJSArray and QuickJSObject can contain any other supported Java object, allowing for deeply nested structures. The object QuickJSObject adds a method getInterface to map JavaScript functions within the object to Java interfaces and invokeFunction to invoke JavaScript in the object functions from Java.
| Java Type | JS Type | Remark |
|---|---|---|
null |
null / undefined |
JavaScript undefined is translated to Java null. |
java.lang.Boolean |
boolean |
Internally handles boxing/unboxing for boolean values. |
java.lang.Double / java.lang.Float |
number |
Internally handles boxing/unboxing for floating-point numbers. |
java.lang.Integer |
number |
Internally handles boxing/unboxing for integer numbers. |
java.lang.String |
string |
|
java.util.concurrent.CompletableFuture |
promise |
Bidirectional wrapping of CompletableFuture to promises. |
io.github.stefanrichterhuber.quickjswasmjava.QuickJSException |
Error (exception) |
JavaScript exceptions are translated to QuickJSException objects in Java. Each exception includes a message and a stack trace (with exact line and column numbers). Java exceptions thrown within callbacks are transformed into JavaScript exceptions and then returned to Java as QuickJSException. Note that the original Java stack trace is lost in this process. |
io.github.stefanrichterhuber.quickjswasmjava.QuickJSArray<Object> |
array |
Wraps native JavaScript arrays. Values can be any supported type, including mixed types and nested lists/maps. Changes are reflected bi-directionally. |
io.github.stefanrichterhuber.quickjswasmjava.QuickJSObject<String, Object> |
object |
Wraps native JavaScript objects. Keys can be strings, numbers, or booleans. Values can be any supported type, including mixed types and nested lists/maps. Changes are reflected bi-directionally. |
java.util.List<Object> |
array |
Any java.util.List (not a QuickJSArray) is copied by value to the JavaScript context. If returned to Java, it is translated into a QuickJSArray. |
java.util.Map<String, Object> |
object |
Any java.util.Map (not a QuickJSObject) is copied by value to the JavaScript context. Keys must be strings. If returned to Java, it is translated into a QuickJSObject. |
io.github.stefanrichterhuber.quickjswasmjava.QuickJSFunction |
function |
Native JavaScript functions are exported to Java as QuickJSFunction objects. |
java.util.function.Function<P, R> |
function |
Java Function objects can be exported to JavaScript. If a JavaScript function is transferred back to Java that originated from a Function<P, R>, it is translated to a java.util.function.Function<java.util.List<Object>, Object> where the List contains the JavaScript arguments. |
java.util.function.BiFunction<P, Q, R> |
function |
Java BiFunction objects can be exported to JavaScript. If a JavaScript function is transferred back to Java that originated from a BiFunction<P, Q, R>, it is translated to a java.util.function.Function<java.util.List<Object>, Object> where the List contains the JavaScript arguments. |
java.util.function.Consumer<P> |
function |
Java Consumer objects can be exported to JavaScript. If a JavaScript function is transferred back to Java that originated from a Consumer<P>, it is translated to a java.util.function.Function<java.util.List<Object>, Object> (returning null). |
java.util.function.BiConsumer<P, Q> |
function |
Java BiConsumer objects can be exported to JavaScript. If a JavaScript function is transferred back to Java that originated from a BiConsumer<P, Q>, it is translated to a java.util.function.Function<java.util.List<Object>, Object> (returning null). |
java.util.function.Supplier<R> |
function |
Java Supplier objects can be exported to JavaScript. If a JavaScript function is transferred back to Java that originated from a Supplier<R>, it is translated to a java.util.function.Function<java.util.List<Object>, Object> (with an empty argument List). |
The library is fundamentally composed of two main components:
- Java Library: Provides a type-safe and user-friendly interface for interacting with JavaScript.
- Wasm Library: Implements the actual QuickJS runtime using Rust and
rquickjs, compiled to WebAssembly.
The Java library acts as a wrapper, exposing typesafe interaction points. The core entry point is io.github.stefanrichterhuber.quickjswasmjava.QuickJSRuntime, which manages the WebAssembly instance and resource constraints. It creates io.github.stefanrichterhuber.quickjswasmjava.QuickJSContext objects, each representing a unique JavaScript execution context.
Key Dependencies:
log4j2: For unified logging (Java and Rust).Chicory: The WebAssembly runtime for executing the Wasm module.MessagePack: Used for efficient serialization of data between Java and Rust.
Resource Management:
To prevent memory leaks, native QuickJS objects (contexts, runtimes, functions, objects, and arrays) must be properly managed. While QuickJSContext manages the lifecycle of functions, objects, and arrays created within it (closing them when the context is closed), and QuickJSRuntime manages all its created contexts (closing them when the runtime is closed), it is highly recommended to explicitly close both QuickJSRuntime and QuickJSContext instances when they are no longer needed, ideally using Java's try-with-resources statement.
The Wasm library is built with Rust, leveraging rquickjs to interface with QuickJS. It targets wasm32-wasip1, currently the only supported target for Chicory, to communicate with the JVM.
A central JSJavaProxy struct facilitates type conversion between Java and JavaScript. This struct represents all transferable types and handles data serialization (using MessagePack and serde) for cross-runtime communication.
The wasm_macros crate, specifically its wasm_export macro, is crucial. It simplifies the definition of exported Rust functions by:
- Hiding the serialization/deserialization of
JSJavaProxyobjects. - Managing the pointer logic necessary to address native QuickJS objects (runtime, context, arrays, objects) outside of Rust's standard lifetime model. This allows for clean and lean function signatures in Rust.
Logging:
The log crate is used on the Rust side. All Rust log messages are forwarded to the Java side and processed by log4j2 under the logger name io.github.stefanrichterhuber.quickjswasmjava.native.WasmLib.
Please report any issues or feature requests on the GitHub Issues page.
Licensed under MIT License (LICENSE or http://opensource.org/licenses/MIT)