Skip to main content

Internals

To best use QuickJS, it helps to understand the basics of how it functions under the hood.

Atoms

Internally, the engine replaces commonly used identifiers, strings, and small integer literals with 32-bit integers called atoms. This saves memory and improves performance.

Bytecode

Like most interpreters, QuickJS does not execute JavaScript directly. It first compiles the program into a bytecode format specifically designed for the engine. This bytecode is then executed in a stack machine by the interpreter loop.

Compiled program structure

In bytecode, a program is represented as a list of atoms that program uses and an entry function called <eval> that wraps the entire program. This function gets evaluated upon execution.

Each function is itself represented by the following:

  • Metadata, like name argument count, maximal stack size, ...
  • A list of local variables and their scope
  • A list of closure variables, which are variables that hold functions themselves and their context (any variables or constant that are accessible from the function)
  • A list of constants that are used within the code. These can contain both values and closures.
  • Debug information (filename, line number, and a mapping from bytecode index to source code file line)
  • The function's executable bytecode as a list of operations

Operations

The list of bytecode operations and their opcodes is defined in quickjs-opcode.h. Each operation has a name, a size (opcode + optional parameters), the number of values it puhses onto the stack, the number of values it pops off the stack, and the type of parameter it takes (like none, atom, i32).

JSValue

Every JavaScript object, from primitives to functions themselves, are internally represented by JSValue structs. Each JSValue has a 64-bit generic value, which for primitives is just the value itself, and is a pointer to the object for more complicated, heap-allocated objects. Additionally, each JSValue has a 64-bit tag that defines its type and its reference count (for heap allocated objects). When running in 32-bit mode, QuickJS uses NaN-boxing for more efficient memory usage.

JSContext

A JSContext is, as the name suggests, the context in which JavaScript code is executed. Global variables and functions are specific to one context, but multiple contexts can be created and used together, and can exchange JSValues.

JSRuntime

Contexts operate within a JSRuntime. This is the state of the engine itself, and acts as the main memory pool for all JSValues. No interaction can occur between different JSRuntimes, they must be kept entirely separate.

Memory management

JavaScript is designed to be garbage collected, meaning JavaScript objects are automatically freed from memory when no reference to them exists anymore. QuickJS implements a basic garbage collector for its JavaScript runtime, but it also uses a reference counting system for JSValues in the C realm.

When native code interacts with JSValues, their lifetime must be managed very delicately. Values are created with "New" functions, like JS_NewInt32, JS_NewObject, can be duplicated with JS_DupValue, and must be freed with JS_FreeValue. In practice, JS_DupValue doesn't actually make a copy, but increments the reference count of the value. JS_FreeValue decrements the reference count and frees the memory when the reference count hits zero.

When a JSRuntime is destroyed, there should not be any JSValues still existing in memory, and by default, QuickJS will show an error if any JSValues still remain. You can define the DUMP_LEAKS macro when compiling QuickJS to see more information about what objects still remain in memory.