qjs4j is a native Java implementation of QuickJS - a complete reimplementation of the QuickJS JavaScript engine in pure Java, with zero external dependencies.
Verified on JDK 17, 21 and 25; the artifact is compiled for JDK 17. The CI matrix runs the
engine on 17 and 21 across Linux, macOS and Windows — 25 is verified by running the suite with
-PtestJavaVersion=25, and adding it to the matrix is recommended, because JDK 25 withdrew the
atomic access modes from byte-array view VarHandles that Atomics depends on and a matrix
stopping at 21 did not notice.
qjs4j targets ES2024. Conformance is measured, not claimed: the Test262 quick partition runs
97,325 interpretations (every file that is not noStrict, module or raw is executed twice,
once sloppy and once strict, as INTERPRETING.md requires) and the long-running partition a further
4,282, and the runner exits nonzero on any failure. Reproduce with
./gradlew test262Quick test262LongRunning against a ../test262 checkout.
The suite is not the whole specification, and the known limitations below list what is understood not to work. See the detailed feature list for per-feature status.
qjs4j includes features not present in the original QuickJS:
- Float16Array: IEEE 754 half-precision (16-bit) floating point typed array support
- ES2024 Features: Promise.withResolvers, Object.groupBy, Map.groupBy
- ShadowRealm (runtime-gated): Proposal feature support for test262 compatibility, enabled via
JSRuntimeOptions.setShadowRealmEnabled(true) - Module System: ES6 modules with dynamic
import()— see the limitation below. Modules are located and read only through the host hookJSRuntimeOptions.setModuleLoader(IJSModuleLoader); a runtime without a loader loads no module, so scripts get no file-system access by default.JSFileSystemModuleLoaderloads from disk, andnew JSFileSystemModuleLoader(root)refuses anything whose real path is outsideroot. The command-line interpreter installs an unrestricted file-system loader. - Microtask Queue: Full ES2020-compliant microtask infrastructure
- Internationalization (Intl):
Collator,DateTimeFormat,DisplayNames,DurationFormat,ListFormat,Locale,NumberFormat,PluralRules,RelativeTimeFormat,Segmenter - Temporal (runtime-gated): enabled via
JSRuntimeOptions.setTemporalEnabled(true) - Top-level await:
awaitat module scope
-
Module source is transformed textually, not parsed. The parser validates
import/exportsyntax and then discards it, so modules are implemented by rewriting module source into ordinary script source: imports become temporary accessors on the global object, and exports become generated bindings declared beside the author's own code. Three consequences are known to be wrong, and each is pinned as atestKnownLimitation*test insrc/test/java/com/caoccao/qjs4j/core/JSModuleKnownLimitationTest.java, which is the authoritative list — those tests assert the wrong answer on purpose, so fixing a defect makes one fail:- an imported binding is not usable from a closure retained after
JSContext.eval()returns, and is not live; - the transformer's generated bookkeeping bindings are visible to a direct
evalinside the module; - an import attribute naming an unsupported module type is ignored rather than refused.
Real module environment records, with indirect bindings the compiler can capture, are the fix for all three, and are a dedicated milestone. Loading and linking are otherwise separated from evaluation: a graph that names a module it cannot load, or a name nothing exports, fails before any module body runs.
- an imported binding is not usable from a closure retained after
-
Resource limits bound data blocks, not the heap.
JSRuntimeOptions.setMaxDataBlockBytes(long)(formerlysetMaxMemoryUsage, now deprecated) counts every byte allocated for anArrayBufferorSharedArrayBufferand refuses an allocation past the ceiling with a catchableRangeError. The default ceiling is 64 MiB, and a single data block is one Javabyte[], so no buffer or typed array exceedsInteger.MAX_VALUE - 8bytes even with the ceiling disabled; V8 allocatesnew ArrayBuffer(2 ** 31), qjs4j refuses it. Objects, arrays, strings and bytecode are ordinary Java allocations bounded by-Xmx.setMaxCallDepth(int)bounds the interpreter's call depth — 10,000 frames by default, about V8's — not the JVM's own stack. Each frame also occupies Java stack (about 1.4 KiB for a plain call), so the full default depth needs a thread of about 16 MiB; on a smaller stack the JVM runs out first, which is also reported as a catchableRangeError. The command-line interpreter runs scripts on a 256 MiB thread. -
Host APIs are not part of the engine.
setTimeout,queueMicrotask,structuredClone,TextEncoder,URLand the like are host functions, not ECMAScript, and an embedder provides the ones it wants. The command-line interpreter addsqueueMicrotask,setTimeout,setInterval,clearTimeoutandclearIntervalwith Node's ordering, and runs timers until none is left. -
Strings are at most 227 − 1 code units (
JSString.MAX_LENGTH, 134 million), a quarter of V8's 229 − 24; longer results raiseRangeError: Invalid string length. A string at V8's limit is over a gigabyte ofchardata on the Java heap, and building one needs about twice that, so on an ordinary heap the JVM would run out of memory first — the failure the limit exists to turn into a catchable error. -
WeakMap/WeakSetare ephemeron-correct but not enumerable, as the specification requires; a collection that dies while its keys live leaves entries to be pruned lazily.
See ASYNC_AWAIT_ENHANCEMENTS.md for async/await implementation details.
- Features: Complete list of implemented JavaScript features
- Migration Status: Migration progress from QuickJS C to Java
- Async/Await: Async/await and iteration implementation
The snippets below use 0.1.1, the latest release on Maven Central. The version in
build.gradle.kts is the next version under development and is not published until it is tagged.
dependencies {
implementation("com.caoccao.qjs4j:qjs4j:0.1.1")
}dependencies {
implementation 'com.caoccao.qjs4j:qjs4j:0.1.1'
}<dependency>
<groupId>com.caoccao.qjs4j</groupId>
<artifactId>qjs4j</artifactId>
<version>0.1.1</version>
</dependency>import com.caoccao.qjs4j.core.*;
// Create a JavaScript runtime and context
try (JSContext context = new JSContext(new JSRuntime())) {
// Evaluate JavaScript code
JSValue result = context.eval("2 + 2");
System.out.println(result); // 4
// Work with objects
JSValue obj = context.eval("({ name: 'qjs4j', version: '1.0' })");
if (obj instanceof JSObject jsObj) {
JSValue name = jsObj.get("name");
System.out.println(name); // qjs4j
}
// Use modern JavaScript features
JSValue promise = context.eval("Promise.resolve(42)");
// Process microtasks to settle promises
context.processMicrotasks();
}./gradlew buildJava formatting is enforced by Spotless with Eclipse JDT. Run ./gradlew spotlessApply to format sources and sort members, or ./gradlew spotlessCheck to verify them without editing files. Both build and check include the formatting check. The formatter uses four-space indentation and the member order SF,SI,F,I,C,M,SM,T, with field sorting enabled; its settings are in build.gradle.kts and config/eclipse-java-formatter.properties.
The Gradle toolchain compiles and tests against JDK 17 wherever the build is launched from, so a
JDK 17 installation must be discoverable (Gradle will provision one if it is not). Gradle's Kotlin
DSL compiles the build script before any toolchain is selected, so the launching JDK also has to
be one the wrapper's Gradle release understands: Gradle 9.4.1 accepts JDK 17 through 25. On a newer
JDK the build aborts with a bare version number — upgrade the wrapper, or set JAVA_HOME to a
supported release.
Test JVM: ./gradlew test -PtestJavaVersion=21 runs the suite on JDK 21 instead of 17, and
-PtestJavaVersion=25 on JDK 25. Running the engine — not just launching Gradle — on each release
matters, because their behaviour differs: JDK 25 withdrew the atomic access modes from byte-array
view VarHandles, which Atomics depends on.
qjs4j is organized into modular packages:
- core: Runtime components (JSValue types, JSContext, JSRuntime)
- vm: Virtual machine with bytecode execution and stack management
- builtins: JavaScript built-in objects and prototype methods
- compiler: Parser, lexer, bytecode compiler, and AST
Key technical features:
- Shape-based optimization with hidden classes
- Proper SameValueZero equality for Map/Set
- Complete iterator and async iterator protocols
- Full prototype-based inheritance
- Ephemeron-correct
WeakMap/WeakSet: entries live on the key, so a value cannot keep its own key alive
Apache License 2.0 - see LICENSE file for details.