rquickjs: embedding a QuickJS engine in a Rust program
High level bindings to the quickjs javascript engine
At a glance
- What is it?
- rquickjs is high level Rust bindings to the QuickJS-NG JavaScript engine, with promises bridged to Rust futures, flexible type conversion, custom allocators, custom module loaders, and class support that lets Rust types appear as JavaScript classes. The crate is feature complete and mostly stable, with error handling as the part still expected to move.
- Who is it for?
- Adopt rquickjs if you need to run untrusted or plugin JavaScript inside a Rust process and want promises, futures, and Rust types to cross the boundary cleanly. Do not adopt it if you need Node or WinterCG APIs, because the crate does not aim to provide system and web APIs.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
An embeddable engine, and what that means for footprint
rquickjs is high level bindings to the QuickJS-NG JavaScript engine, which is a fork of the original QuickJS engine by Fabrice Bellard. The stated goal is to be an easy to use and safe wrapper, similar in shape to the rlua library for Lua.
Why an embeddable engine is a different category from a JavaScript runtime is worth spelling out. The QuickJS feature list in the README is about smallness and predictability. It is a small embeddable engine of just a few C files with no external dependency, and a simple hello world program is 210 KiB of x86 code. The interpreter is fast with very low startup time, running the 75000 tests of the ECMAScript Test Suite in about 100 seconds on a single core of a desktop PC, and the complete life cycle of a runtime instance completes in less than 300 microseconds.
Those two numbers are the reason to care. A runtime that can be created and torn down in under a millisecond is not a process you spawn once at boot, it is a resource you create per task. That is the shape that makes sandboxing scripts, evaluating user expressions, or running a plugin per request practical rather than exotic.
The engine supports the ES2020 specification including modules, asynchronous generators, proxies, and BigInt, with nearly complete ES2020 support and full Annex B support for legacy web compatibility. Garbage collection uses reference counting with cycle removal, which gives deterministic behaviour for the common case. The mathematical extensions include BigDecimal, BigFloat, operator overloading, bigint mode, and math mode.
The crate is version 0.14.0, MIT licensed, with a rust-version of 1.87 and edition 2021. The last push to the repository was on 2026-10-19, and the repository publishes no GitHub releases.
Promises on one side, futures on the other
The headline feature of the crate is full integration with async Rust. The ES6 Promises can be handled as Rust futures and vice versa, and the README describes easy integration with almost any async runtime or executor.
That bridge is the part that makes the engine usable in a modern Rust program. Without it, calling into JavaScript from an async context is a blocking call, and a script that awaits a timer or a promise stalls your executor thread. With it, a JavaScript promise becomes something you can await in Rust, and a Rust future can be handed to JavaScript as a thenable. Both directions matter, since a script that calls back into your host code and awaits the result is a common pattern, and that is where the deadlock risk lives if the bridge is one-directional.
Alongside that sits flexible data conversion between Rust and JS, with many widely used Rust types convertible in both directions. The point of a flexible conversion layer is that callers should not have to reach for the raw engine API for common cases, and the type conversion is where most of the ergonomic value of the binding sits.
The feature flags reflect how the crate is put together. The default is std, and there is a full feature that turns on nearly everything except parallel and the async runtime support:
full = [
"std",
"chrono",
"loader",
"dyn-load",
"either",
"indexmap",
"macro",
"phf",
]There is also a full-wasi variant designed for wasm32-wasip1 and wasm32-wasip2, which is the same set minus dyn-load, and a full-async variant that adds futures to full. The dyn-load exclusion for WASI is not arbitrary, since dynamic loading needs a mechanism to load shared libraries at runtime, which is exactly what a WASI sandbox does not offer.
Rust types as JavaScript classes
The class support is the most detailed part of the feature list, and it is what separates this binding from a thin FFI wrapper. There is full support of ES6 classes, and a Rust data type can be represented as a JS class, with data fields accessible via object properties. Both static and instance members are supported, properties can be defined with getters and setters, and constant static properties are supported.
The memory management detail is the one that bites if you miss it. There is support for holding references to JS objects, and the README states that a data type holding such references should implement the Trace trait so the garbage collector works properly. That is the standard shape for a tracing garbage collector, and skipping it means a reference can be collected while Rust still believes it owns a handle, which is a use-after-free rather than a leak.
There is also support for extending defined classes by JS, so a class you define in Rust can be subclassed on the JavaScript side, with the subclass calling through to your implementation.
Two other capabilities sit in the same layer of the design. Support for user-defined allocators means the Runtime can be created with a custom allocator, and using Rust's global allocator is also fully supported. That matters for embedded or sandboxed contexts where you want to account for the memory the engine uses, and for applications that already have a global allocator with its own accounting.
And support for user-defined module resolvers and loaders, which can be combined to get a more flexible solution for a concrete case. Combined with the separate support for bundling JS modules as bytecode using an embed macro, that gives you two ways to get code into the engine, a resolver that decides what a module specifier means at runtime, and a build-time bundler that inlines modules into the binary.
The scope line: no system APIs, no web APIs
The community section of the README draws the boundary explicitly. The crate does not aim to provide system and web APIs, and it notes that the QuickJS library is close to V8 in that regard. If you need APIs from WinterCG or Node, the README points at two community projects: the AWS LLRT Modules, a collection of modules that mimic some of the Node APIs in pure Rust, and Rquickjs Extra, a collection of modules that complement the LLRT modules in pure Rust. There is also Rquickjs Serde, a Serde serializer and deserializer for the rquickjs Value type.
That is a significant architectural boundary to understand before you start. In Node, the environment is a large built-in surface, and a script can read files, open sockets, and reach the network without asking. In rquickjs, the environment is whatever your host code decides to expose. A script gets the standard library that QuickJS provides and nothing else, unless you wire it up.
The consequence is that rquickjs is a good fit for two different use cases, and a poor fit for a third. If you are embedding a scripting language into a Rust application and want to control exactly what the script can touch, the empty-by-default environment is the feature, and the community modules exist for the cases where you need a Node-like surface. If you are trying to run an existing Node application inside your Rust process, you will spend most of your effort reimplementing the Node API rather than embedding it.
The `parallel` feature is worth a separate note, because the README flags it as experimental and says it may not work as expected, and advises using it at your own risk. The rest of the crate is described as feature complete, mostly stable and ready to use, with error handling the one thing that may change in the future.
Platform support is a build-time concern, not a runtime one
The supported platforms section explains a constraint that surprises people the first time they hit it. rquickjs needs to compile a C library, which has its own limitations on supported platforms, and it also needs to generate bindings for that platform. As a result it might not compile on all platforms that Rust supports.
There is a workaround for most of that. You can always try to compile rquickjs with the bindgen feature, and the README says that should work for most platforms. Bindings are shipped for a limited set of platforms, and for those platforms you do not have to enable the bindgen feature. The table lists the platform, whether bindings are shipped, whether it is tested, and whether quickjs itself supports it.
The shipped and tested entries include x86_64-unknown-linux-gnu, i686-unknown-linux-gnu, aarch64-unknown-linux-gnu, loongarch64-unknown-linux-gnu, and the musl variants x86_64-unknown-linux-musl and aarch64-unknown-linux-musl. Both gnu and musl targets are covered for the mainstream architectures, which is the practical requirement for people shipping a binary in a container.
The workspace layout explains the structure. The Cargo.toml declares a workspace with members sys, core, macro, and a set of example crates, and the workspace dependencies name rquickjs-core, rquickjs-macro, and rquickjs-sys, all at version 0.14.0 with default-features off. The sys crate is the raw FFI layer, core is the safe wrapper, and macro is the embed macro for bytecode bundling. The wasm-test directory is excluded from the workspace because it builds only for wasm32-unknown-unknown and is driven by a runner script, which is a good sign for a project that treats the WebAssembly build as a real target rather than an afterthought.
Against embedding V8, and against a WASM sandbox
The main alternative is embedding a larger engine. V8 is what QuickJS is explicitly measured against, and the README's framing is that the QuickJS library is close to V8 in that regard. The difference in approach is size and startup. QuickJS is a few C files with no external dependency and a runtime that can be created in under 300 microseconds.
What you give up is the breadth of the language and the ecosystem. The gap is not primarily in the language, it is in the surrounding tooling, the modules, the debugging support, and the things a modern JavaScript developer expects. For embedding a controlled script inside a Rust service, that gap rarely matters. For running a codebase that assumes a full modern runtime, it does.
The second alternative is not embedding a JavaScript engine at all but isolating the script with WebAssembly. The difference is that a WebAssembly sandbox runs a whole program, with its own runtime and its own module system, while rquickjs runs a script inside a host that controls the environment, including the allocator and the module loader. The trade-off runs the other way from the V8 comparison: WebAssembly gives a stronger isolation boundary and a language-agnostic interface, while rquickjs gives a real JavaScript implementation with fast startup and direct Rust integration.
For a service that evaluates small scripts, often per request, the startup numbers are the deciding factor. Creating and destroying a runtime per evaluation is a cost that a full WASM module load cannot match, and a service that wants to run user-supplied expressions in a sandboxed, disposable context is exactly the case the crate is shaped for.
Editorial conclusion
Adopt rquickjs if you need to run untrusted or plugin JavaScript inside a Rust process and want promises, futures, and Rust types to cross the boundary cleanly. Do not adopt it if you need Node or WinterCG APIs, because the crate does not aim to provide system and web APIs. Verify first whether your target platform is in the shipped bindings table, and plan to enable the bindgen feature where it is not, since rquickjs has to compile a C library.
Frequently asked questions
What is rquickjs and which JavaScript engine does it wrap?
rquickjs is high level Rust bindings to the QuickJS-NG JavaScript engine, a fork of the original QuickJS engine. The goal is an easy to use and safe wrapper, similar to the rlua library, with the crate at version 0.14.0 under the MIT licence.
How does rquickjs handle promises and async Rust?
The crate offers full integration with async Rust. ES6 promises can be handled as Rust futures and vice versa, and the README describes easy integration with almost any async runtime or executor. Async support is enabled through the futures feature, which the full-async feature set turns on.
Does rquickjs provide Node or web APIs?
No. The crate does not aim to provide system and web APIs, and the README notes the QuickJS library is close to V8 in that regard. For Node-like surfaces it points to the AWS LLRT Modules and Rquickjs Extra community projects, both written in pure Rust.
What does the Trace trait requirement mean for Rust types exposed to JavaScript?
Rust data types can be represented as JS classes, and a type that holds references to JS objects should implement the Trace trait so the garbage collector works properly. The crate also supports getters and setters, static and instance members, constant static properties, and extension of defined classes by JavaScript.
What platforms does rquickjs support out of the box?
Rquickjs ships bindings for a limited set of platforms, including x86_64, i686, aarch64, and loongarch64 Linux targets in both gnu and musl variants. It needs to compile a C library and generate bindings, so on other platforms you should try compiling with the bindgen feature, which the README says should work for most platforms.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/delskayn-rquickjs)Was this useful?