MicroQuickJS: A Lightweight JavaScript Engine for Embedded Systems
Summary
MicroQuickJS (MQuickJS for short) is a JavaScript engine tailored for embedded systems. It runs JavaScript programs with just 10 kB of RAM and requires approximately 100 kB of ROM (ARM Thumb-2 code) including the C library, boasting performance comparable to QuickJS. This article details its features, usage, and technical nuances.
I. Getting to Know MicroQuickJS: A JavaScript Solution for Embedded Scenarios
Are you searching for a JavaScript engine that can run on resource-constrained embedded devices? MicroQuickJS (commonly referred to as MQuickJS) might be exactly what you need. Specifically designed for embedded systems, its standout feature is extreme resource efficiency—needing only 10 kB of RAM to execute JavaScript programs and around 100 kB of ROM (based on ARM Thumb-2 code) for the entire engine, including the C library. Yet, its performance rivals that of QuickJS.
Unlike mainstream JavaScript engines, MQuickJS supports only a subset of JavaScript (closely aligned with ES5) and operates exclusively in a “stricter mode.” In this mode, certain error-prone or inefficient JavaScript constructs are prohibited, making the code safer and more suitable for the stability and efficiency requirements of embedded systems.
While MQuickJS shares significant code with QuickJS, its internal mechanisms have been extensively modified to reduce memory consumption. For instance, it employs a tracing garbage collector, the virtual machine does not use the CPU stack, and strings are stored in UTF-8 encoding—all design choices optimized to maximize performance in limited embedded resources.
II. Getting Started with MQuickJS: A Guide to the REPL Tool mqjs
The most straightforward way to experience MQuickJS is through its REPL tool, mqjs. Whether you want to run scripts, execute expressions, or generate bytecode, mqjs handles it all seamlessly.
1. Basic Usage of mqjs
Using mqjs is intuitive—simply enter mqjs in the command line followed by the appropriate options. Its key options are as follows:
-
-hor--help: List all available options -
-eor--eval EXPR: Evaluate the expressionEXPR -
-ior--interactive: Enter interactive mode -
-Ior--include file: Include an additional file -
-dor--dump: Output memory usage statistics -
--memory-limit n: Limit memory usage tonbytes (supports units likekfor kilobytes,mfor megabytes) -
--no-column: Omit column numbers from debug information (retains only line numbers) -
-o FILE: Save bytecode toFILE -
-m32: Force 32-bit bytecode output (use with-o)
2. Running JavaScript Programs
To run a JavaScript file while limiting memory usage (e.g., to 10 kB), use the following command:
./mqjs --memory-limit 10k tests/mandelbrot.js
This command executes the mandelbrot.js script in the tests directory with a 10 kB memory limit, making it ideal for testing how programs perform in resource-constrained environments.
3. Generating and Running Bytecode
A key advantage of MQuickJS is its ability to save compiled bytecode to persistent storage (such as files or ROM) for reuse on embedded devices.
To generate bytecode, use this command:
./mqjs -o mandelbrot.bin tests/mandelbrot.js
This compiles mandelbrot.js and saves the bytecode to mandelbrot.bin. You can then run the bytecode file just like a regular script:
./mqjs mandelbrot.bin
Note that the bytecode format depends on the CPU’s endianness and word length (32-bit or 64-bit). If you’re using a 64-bit CPU and need to generate bytecode for a 32-bit embedded system, add the -m32 option:
./mqjs -o mandelbrot_32.bin -m32 tests/mandelbrot.js
Additionally, to save storage space, use the --no-column option to remove column numbers from debug information (keeping only line numbers), further reducing the bytecode file size.
III. MQuickJS’s “Stricter Mode”: Safer and More Efficient Code Constraints
MQuickJS always operates in a “stricter mode,” similar to using "use strict" in standard JavaScript but with tighter restrictions. The design philosophy behind this mode is: stricter mode is a subset of JavaScript, so code that runs in MQuickJS will work in other JavaScript engines, but the reverse is not true.
Specifically, what constraints does stricter mode impose?
1. No Undeclared Global Variables or with Statements
In stricter mode, all global variables must be declared with var—assigning values to undeclared variables will throw an error. Additionally, the with keyword is completely prohibited, as it obscures scope, increases error risk, and degrades performance.
2. Arrays Cannot Have “Holes”
This is one of the most distinctive constraints in MQuickJS. In standard JavaScript, you might write code like this:
a = [];
a[0] = 1;
a[10] = 2; // Creates a hole in the array
In MQuickJS, however, this “skipping indices” assignment will throw a TypeError. Arrays cannot have holes—assignments are only allowed when extending the array at the end (e.g., a[0] = 1 extends the array length to 1, which is valid).
If your use case truly requires an “array-like object with holes,” use a regular object instead:
a = {};
a[0] = 1;
a[10] = 2; // Valid—objects can have non-consecutive properties
Furthermore, new Array(len) still works as expected, but array elements are initialized to undefined; array literals with holes (e.g., [1, , 3]) will throw a syntax error.
3. Only Global eval is Supported—Direct eval is Prohibited
In JavaScript, the eval function behaves differently depending on how it’s called: direct calls (e.g., eval('1+2')) can access and modify local variables, while indirect calls (e.g., (1, eval)('1+2')) execute only in the global scope.
To avoid the complexity and security risks associated with eval, MQuickJS supports only indirect, global eval. Thus, writing eval('1+2') directly is prohibited—you must use the indirect call syntax.
4. No Value Boxing
In standard JavaScript, you can create “boxed” primitive value objects using new Number(1), new String('hello'), etc. However, MQuickJS does not support this syntax. In most cases, this is unnecessary anyway—using primitive values directly (e.g., 1, 'hello') is sufficient.
IV. JavaScript Subset Reference: What Features Does MQuickJS Support?
MQuickJS’s supported JavaScript features are based on ES5, with specific adjustments and extensions. If you plan to write code for MQuickJS, it’s essential to understand its supported capabilities and limitations.
1. Array Objects
-
No holes: As mentioned earlier, arrays cannot have unassigned index positions. -
Numeric properties handled directly by the array: They are not forwarded to the prototype chain, eliminating the risk of prototype pollution. -
Out-of-bounds assignment throws errors: Assignments are only allowed when extending the array at the end (e.g., assigning to index 2 when the array length is 2 is valid, increasing the length to 3). -
lengthis a getter/setter: Thelengthproperty of arrays is controlled by getters and setters on the prototype chain—modifyinglengthadjusts array elements accordingly.
2. Object Properties
All object properties are writable, enumerable, and configurable. This means you can freely modify or delete object properties and iterate over them using for...in loops.
3. for...in Loops
for...in loops only iterate over an object’s own properties (excluding those on the prototype chain). However, to maintain consistency with standard JavaScript, it’s recommended to use the following pattern:
for(var prop in obj) {
if (obj.hasOwnProperty(prop)) {
// Process prop
}
}
Even better, use for...of loops with Object.keys(obj) for greater clarity:
for(var prop of Object.keys(obj)) {
// Process prop
}
4. Function Objects
The prototype, length, and name properties of function objects are getters/setters. Additionally, C functions cannot have their own properties, but C constructors behave as expected.
5. Global Object
The global object exists but is not recommended for extensive use. It cannot contain getters/setters, and properties created directly on the global object are not visible as global variables in executing scripts.
6. Variables in try...catch
The variable following the catch keyword is a regular variable, adhering to the same scope rules as other variables.
7. Regular Expressions (Regexp)
-
Case folding supports only ASCII characters: Case conversion for non-ASCII characters may not behave as expected. -
Matching is based on Unicode code points: /./matches a single Unicode code point, unlike standard regex with theuflag, which matches UTF-16 code units.
8. Strings
The toLowerCase() and toUpperCase() methods handle only ASCII characters—conversion for non-ASCII characters may not take effect.
9. Dates
Currently, only the Date.now() method is supported, which retrieves the current timestamp.
10. ES5 Extension Features
MQuickJS also supports several extensions beyond ES5 to enhance developer productivity:
-
for...ofloops: Currently limited to arrays—custom iterators are not supported (yet). -
Typed arrays: Facilitate handling binary data. -
\u{hex}syntax in strings: Supports representing characters using Unicode code points. -
Math functions: Includes imul,clz32,fround,trunc,log2,log10, etc. -
Exponentiation operator ( **): e.g.,2**3denotes 2 to the power of 3. -
Regular expression flags: Supports s(dotall),y(sticky), andu(unicode), though Unicode properties are not supported in unicode mode. -
String methods: codePointAt,replaceAll,trimStart,trimEnd. -
globalThisglobal property: Provides a unified way to access the global object.
V. C API: Integrating MQuickJS into Embedded Systems
If you want to integrate MQuickJS into a C project, its C API is designed to be lightweight, with almost no dependencies on the C standard library (it does not use functions like malloc(), free(), or printf()), making it ideal for embedded scenarios.
1. Engine Initialization
To initialize MQuickJS, you need to provide a memory buffer—the engine will only allocate memory within this buffer and not use any system memory outside of it. Example code:
JSContext *ctx;
uint8_t mem_buf[8192]; // 8192-byte memory buffer
ctx = JS_NewContext(mem_buf, sizeof(mem_buf), &js_stdlib);
// Use the engine...
JS_FreeContext(ctx); // Release the context (mainly to call destructors of user objects)
JS_FreeContext is not strictly necessary, as the engine does not allocate system memory. Its primary purpose is to invoke the destructors of user-defined objects.
2. Memory Management: Key Considerations for Garbage Collection
MQuickJS uses a compacting garbage collector, unlike the reference-counting mechanism used by many other engines. When using the C API, keep the following points in mind:
-
No need to explicitly free values: There is no JS_FreeValue()function—the garbage collector handles this automatically. -
Object addresses may change: Each time JS memory is allocated, object addresses can shift. Therefore, avoid using JSValuevariables directly in C code unless they are temporary (used between MQuickJS API calls). -
Use JSGCRefto manage references: ForJSValues that need to be preserved long-term, useJS_PushGCRef()to obtain aJSGCRefpointer, which automatically updates when objects move. Always release it withJS_PopGCRef()when done.
Example code:
JSValue my_js_func(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv)
{
JSGCRef obj1_ref, obj2_ref;
JSValue *obj1, *obj2, ret;
ret = JS_EXCEPTION;
// Obtain two GCRef pointers
obj1 = JS_PushGCRef(ctx, &obj1_ref);
obj2 = JS_PushGCRef(ctx, &obj2_ref);
// Create the first object
*obj1 = JS_NewObject(ctx);
if (JS_IsException(*obj1))
goto fail;
// Create the second object (obj1's address may change, but access via obj1_ref is safe)
*obj2 = JS_NewObject(ctx);
if (JS_IsException(*obj2))
goto fail;
// Set a property on obj1 (obj1 and obj2's addresses may change again, but references are safe)
JS_SetPropertyStr(ctx, *obj1, "x", *obj2);
ret = *obj1;
fail:
// Release GCRefs
JS_PopGCRef(ctx, &obj2_ref);
JS_PopGCRef(ctx, &obj1_ref);
return ret;
}
When debugging on a PC, define the DEBUG_GC macro to force the JS allocator to move objects on every allocation. This helps quickly detect invalid JSValue usage.
3. Standard Library: Efficient Implementation in ROM
MQuickJS’s standard library is compiled into C structures using a custom tool (mquickjs_build.c), which can be stored directly in ROM. This results in extremely fast standard library initialization with almost no RAM usage.
mqjs_stdlib.c provides an example of a standard library used by the mqjs tool, which compiles to mqjs_stdlib.h. Refer to this example if you need to customize the standard library.
example.c is a complete sample program using the MQuickJS C API, serving as a useful reference for integration.
4. Persistent Bytecode: Running from ROM
Bytecode generated by mqjs can run directly from ROM but requires relocation first (using JS_RelocateBytecode()). The relocated bytecode can be loaded with JS_LoadBytecode() and executed like a regular script using JS_Run() (see mqjs.c for implementation details).
Note that bytecode format is not backward-compatible, and no validation is performed before execution. Only run bytecode from trusted sources.
5. Math Library and Floating-Point Emulation
MQuickJS includes a lightweight math library (libm.c). If the target CPU lacks floating-point support, it also provides a floating-point emulator that is smaller than the one included with the GCC toolchain, ensuring mathematical operations work correctly.
VI. Internal Mechanisms: Differences Between MQuickJS and QuickJS
While MQuickJS shares some code with QuickJS, its internal mechanisms have been optimized for embedded scenarios. The key differences are as follows:
1. Garbage Collection
MQuickJS uses a tracing and compacting garbage collector instead of QuickJS’s reference-counting system. This design offers several advantages:
-
Smaller object size: Each allocated memory block requires only a few extra bits to store GC information. -
No memory fragmentation: Compacting collection reorganizes memory to reduce fragmentation. -
No dependency on system malloc: The engine has its own memory allocator, operating entirely within the user-provided buffer.
2. Value and Object Representation
Values in MQuickJS are the same size as a CPU word (32 bits on 32-bit CPUs, 64 bits on 64-bit CPUs) and can store the following:
-
31-bit integers (with 1 bit used as a tag). -
Single Unicode code points (which can be strings of 1 or 2 16-bit code units). -
64-bit floating-point numbers (with a small exponent range on 64-bit CPUs). -
Pointers to memory blocks (memory blocks store type tags internally).
JavaScript objects require at least 3 CPU words (12 bytes on 32-bit CPUs), with the exact size depending on the object type. Properties are stored in a hash table, with each property requiring at least 3 CPU words. Properties of standard library objects can be stored directly in ROM to save RAM.
Property keys are of type JSValue (unlike QuickJS’s specific key type) and can only be strings or 31-bit positive integers. String keys are interned (globally unique) to reduce memory usage.
Strings are stored internally in UTF-8 (QuickJS uses 8-bit or 16-bit arrays). While surrogate pairs are not stored explicitly, they are still correctly displayed when iterating over 16-bit code units in JavaScript, balancing compatibility and memory efficiency.
C functions can be stored as a single value (reducing overhead), but no additional properties can be added in this case. Most standard library functions use this storage method.
3. Standard Library
The entire standard library resides in ROM, generated at compile time. Only a handful of objects need to be created in RAM, resulting in extremely fast engine initialization.
4. Bytecode
Bytecode is stack-based (similar to QuickJS) but references atoms via an indirect table. Line and column number information is compressed using exponential-Golomb coding to reduce storage overhead.
5. Compilation Process
The parser is based on a modified version of QuickJS’s parser, avoiding recursive calls to ensure controlled C stack usage. The compilation process does not generate an Abstract Syntax Tree (AST); instead, it generates bytecode in a single pass with various optimization techniques, eliminating QuickJS’s multiple optimization rounds—making it more suitable for resource-constrained scenarios.
VII. Testing and Benchmarking: Verifying MQuickJS’s Performance
To validate MQuickJS’s functionality and performance, you can use the following tests and benchmarks:
1. Basic Tests
Running the basic test suite is simple:
make test
This command executes a series of core tests to verify that the engine’s essential features work correctly.
2. QuickJS Microbenchmarks
To evaluate MQuickJS’s speed, run QuickJS’s microbenchmarks:
make microbench
This test allows you to directly compare performance differences between MQuickJS and QuickJS across various operations.
3. Octane Benchmark
MQuickJS also supports V8’s Octane benchmark (requires a modified version adapted for stricter mode). Related test files and patches can be downloaded from here.
To run the Octane benchmark:
make octane
This test provides a comprehensive assessment of the engine’s performance in complex application scenarios.
VIII. License Information
MQuickJS is released under the MIT License. Unless otherwise stated, its source code is copyrighted by Fabrice Bellard and Charlie Gordon. This means you can freely use, modify, and distribute MQuickJS in both commercial and open-source projects, provided you retain the original copyright notice.
IX. FAQ: Common Questions About MQuickJS
1. What scenarios is MQuickJS best suited for?
MQuickJS is ideal for resource-constrained embedded systems, such as IoT sensors, microcontrollers, and other devices that need to run JavaScript but have limited RAM and ROM.
2. Why doesn’t MQuickJS support certain JavaScript features?
To minimize memory usage and improve execution efficiency, MQuickJS retains only the core subset of ES5 and select extensions, removing error-prone or inefficient features (e.g., the with statement, array holes).
3. How do I integrate MQuickJS into my embedded project?
First, refer to example.c to understand basic usage. Next, prepare a memory buffer based on your project’s requirements and initialize the JS context. Finally, load and execute scripts or bytecode using the C API. For custom standard libraries, refer to the implementation in mqjs_stdlib.c.
4. Is bytecode portable across different architectures?
No. Bytecode format depends on the CPU’s endianness and word length (32-bit or 64-bit). However, 64-bit devices can generate 32-bit bytecode using the -m32 option for use on 32-bit embedded systems.
5. Does MQuickJS’s garbage collection affect real-time performance?
Tracing garbage collection pauses program execution, which may impact real-time performance. However, due to MQuickJS’s low memory footprint, garbage collection frequency and duration are typically manageable, making it suitable for scenarios with non-critical real-time requirements.
Through this article, you should have a comprehensive understanding of MicroQuickJS. Whether you’re looking to run JavaScript on embedded devices or explore the inner workings of lightweight engines, MQuickJS is a tool worth delving into. Its design philosophy—enabling efficient JavaScript execution in limited resources—opens up new possibilities for scripted development in the embedded domain.

