Skip to content

Latest commit

 

History

294 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

QuickJs4J

QuickJs4J lets you safely and easily run JavaScript from Java using a sandboxed environment.

Why Use QuickJs4J?

QuickJs4J provides a secure and efficient way to execute JavaScript within Java. By running code in a sandbox, it ensures:

  • Memory safety – JavaScript runs in isolation, protecting your application from crashes or memory leaks.
  • No system access by default – JavaScript cannot access the filesystem, network, or other sensitive resources unless explicitly allowed.
  • Portability – Being pure Java bytecode, it runs wherever the JVM does.
  • Native-image friendly – Compatible with GraalVM's native-image for fast, lightweight deployments.
  • Forward compatible - regular Java bytecode is generated, the version of QuickJS4J you’re currently using will remain compatible even when you upgrade to a newer Java runtime.

Whether you're embedding scripting capabilities or isolating untrusted code, QuickJs4J is designed for safe and seamless integration.

How it works

There are a few steps to achieve the result:

  • compile QuickJS to WebAssembly
  • translate the QuickJS payload to pure Java bytecode using Endive Compiler
  • run QuickJS directly from Java without using JNI
  • ship an extremely small and self contained jar that can run wherever the JVM can go!

Quick Start

Add QuickJs4J as a standard Maven dependency:

<dependency>
    <groupId>io.roastedroot</groupId>
    <artifactId>quickjs4j</artifactId>
</dependency>

Then run a simple "Hello World" example:

import io.roastedroot.quickjs4j.core.Runner;

try (var runner = Runner.builder().build()) {
    runner.compileAndExec("console.log(\"Hello QuickJs4J!\");");
    System.out.println(runner.stdout());
}

Note: You must explicitly print your JavaScript program’s output using System.out.

QuickJs4J runs JavaScript in a secure, sandboxed environment. To simplify communication, it allows you to bind Java methods so they can be called directly from JavaScript.

import io.roastedroot.quickjs4j.core.Engine;
import io.roastedroot.quickjs4j.core.Runner;
import io.roastedroot.quickjs4j.annotations.HostFunction;
import io.roastedroot.quickjs4j.annotations.Builtins;

@Builtins("from_java")
class JavaApi {
    @HostFunction("my_java_func")
    public String add(int x, int y) {
        return "hello " + (x + y);
    }

    @HostFunction("my_java_check")
    public void check(String value) {
        assert("hello 42".equals(value));
    }
}

var engine =
    Engine.builder()
          .addBuiltins(JavaApi_Builtins.toBuiltins(new JavaApi()))
          .build();

try (var runner = Runner.builder().withEngine(engine).build()) {
    runner.exec("from_java.my_java_check(from_java.my_java_func(40, 2));");
}

Calling JavaScript from Java

To invoke functions defined in a JavaScript or TypeScript library, define an interface like this:

import io.roastedroot.quickjs4j.core.Engine;
import io.roastedroot.quickjs4j.core.Runner;
import io.roastedroot.quickjs4j.annotations.GuestFunction;
import io.roastedroot.quickjs4j.annotations.Invokables;

@Invokables("from_js")
interface JsApi {
    @GuestFunction
    String sub(int x, int y);
}

var engine =
    Engine.builder()
          .addInvokables(JsApi_Invokables.toInvokables())
          .build();

// Inlined for demo; normally loaded from a packaged distribution file
String jsLibrary = "function sub(x, y) { return \"hello js \" + (x - y); };";

try (var runner = Runner.builder().withEngine(engine).build()) {
    var jsApi = JsApi_Invokables.create(jsLibrary, runner);
    System.out.println(jsApi.sub(3, 1));
}

Enabling Annotation Processing

Configure the annotation processor in your Maven pom.xml:

<dependencies>
  <dependency>
    <groupId>io.roastedroot</groupId>
    <artifactId>quickjs4j-annotations</artifactId>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <annotationProcessorPaths>
          <path>
            <groupId>io.roastedroot</groupId>
            <artifactId>quickjs4j-processor</artifactId>
          </path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
  </plugins>
</build>

Passing Java Object References

Sometimes, you may want to pass a Java object reference to JavaScript without serializing it (i.e., keeping it only in Java memory). Use HostRefs as shown below:

import io.roastedroot.quickjs4j.annotations.HostRefParam;
import io.roastedroot.quickjs4j.annotations.ReturnsHostRef;

@ReturnsHostRef
@HostFunction("my_java_ref")
public String myRef() {
    return "a Java string not visible in JS";
}

@HostFunction("my_java_ref_check")
public void myRefCheck(@HostRefParam String value) {
    ...
}

try (var myTestModule = new MyJsTestModule()) {
    myTestModule.exec("my_java_ref_check(my_java_ref());");
}

High Level API

An higher level API is exposed for convenience to wrap everything up for the most common use cases. You can use the ScriptInterface annotation:

import io.roastedroot.quickjs4j.annotations.ScriptInterface;

public class CalculatorContext {
    public void log(String message) {
        System.out.println("LOG>> " + message);
    }
}

@ScriptInterface(context = CalculatorContext.class)
public interface Calculator {
    int add(int term1, int term2);

    int subtract(int term1, int term2);
}

try (var calculator = new Calculator_Proxy(jsLibrary, new CalculatorContext()) {
    calculator.add(1, 2);
    calculator.subtract(3, 1);
}

NOTE: currently only basic use is supported.

JSR-223 Scripting Engine

QuickJs4J also provides a JSR-223 (javax.script) compliant scripting engine with Compilable and Invocable support:

import javax.script.*;

ScriptEngine engine = new ScriptEngineManager().getEngineByName("quickjs4j");

engine.eval("1 + 2");  // 3

engine.put("name", "World");
engine.eval("'Hello, ' + name");  // "Hello, World"

engine.eval("function add(a, b) { return a + b; }");
((Invocable) engine).invokeFunction("add", 3, 4);  // 7

See the full JSR-223 scripting documentation for details on bindings, Compilable, Invocable (invokeFunction, invokeMethod, getInterface), output redirection, and more.

Building a JS/TS Library

To build your JavaScript/TypeScript library, refer to this example.

Key points:

  • Output an ECMAScript module using tools like esbuild:

    esbuild your_file.js --format=esm
  • The library must export the expected functions.

  • The annotation processor will generate a .mjs file at:

    target/classes/META-INF/quickjs4j/builtin_name.mjs
    

    This file bridges your Java and JS code.

Building the Project

To build this project, you'll need:

  • JDK 11 or newer
  • Maven
mvn clean install

No Rust toolchain needed. The inlay Maven plugin downloads the Javy plugin wasm from ghcr.io/roastedroot/quickjs4j-javy-plugin, pinned by digest in wkg.lock.

Working on the Javy plugin

rustup target add wasm32-wasip1  # Only needed once

make -C javy-plugin build        # writes ./javy_quickjs4j_plugin.wasm (gitignored)
mvn clean install

inlay skips the download when javy_quickjs4j_plugin.wasm already exists, so your local build wins; delete it to go back to the published one.

Pushes to main touching javy-plugin/** publish a new snapshot and re-pin wkg.lock (wasm-publish.yml). Releases retag the pinned digest, so every release ships an immutable wasm matching its jars. To re-pin by hand: mvn generate-sources -pl core -Dinlay.update.

Acknowledgements

This project stands on the shoulders of giants:

  • QuickJS – a small, embeddable JavaScript engine
  • Javy – a toolchain for compiling JavaScript to WebAssembly
  • Endive – a native JVM WebAssembly runtime

About

Run JavaScript from Java in a safe sandbox.

Resources

Stars

87 stars

Watchers

3 watching

Forks

Releases

Used by

Contributors

Languages