Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 58 additions & 113 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,143 +1,88 @@
# quickjs-emscripten
# CLAUDE.md

## Package Manager
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Use `corepack yarn` to run yarn commands, e.g.:
## Project Overview

- `corepack yarn install`
- `corepack yarn build`
- `corepack yarn build:ts`
quickjs-emscripten provides JavaScript/TypeScript bindings for QuickJS (a JavaScript interpreter) compiled to WebAssembly. It enables safe evaluation of untrusted JavaScript in browsers, Node.js, Deno, Bun, and Cloudflare Workers.

## Building Variants

Variants are WASM/JS builds of QuickJS with different configurations. They are generated by `scripts/prepareVariants.ts`.

To build a variant:

```bash
cd packages/variant-quickjs-<name>
make # builds the C code with emscripten
corepack yarn build:ts # builds the TypeScript wrapper
```

## Emscripten

- Most variants use the default emscripten version (defined in `scripts/prepareVariants.ts` as `DEFAULT_EMSCRIPTEN_VERSION`)
- The asmjs variant uses an older emscripten version (`ASMJS_EMSCRIPTEN_VERSION`) to avoid newer browser APIs
- Emscripten runs via Docker when the local version doesn't match; see `scripts/emcc.sh`

## Testing

Run all tests:

```bash
corepack yarn test
```

Run tests for a specific variant (e.g., quickjs-ng only):
## Common Commands

```bash
cd packages/quickjs-emscripten
npx vitest run -t "quickjs-ng"
yarn check # Run all checks (build, format, tests, type-checking, linting)
yarn build # Full build: codegen + packages + docs
yarn build:codegen # Generate variant packages from templates
yarn build:packages # Build all variant packages in parallel
yarn test # Run all tests
yarn test:fast # Fast tests only (skip async variants)
yarn test:slow # Tests with memory leak detection
yarn lint # ESLint
yarn lint --fix # Auto-fix lint issues
yarn prettier # Format with Prettier
yarn doc # Generate TypeDoc documentation
```

Other test filters:

- `-t "RELEASE_SYNC"` - only release sync variants
- `-t "DEBUG"` - only debug variants
- `-t "QuickJSContext"` - only sync context tests
## Architecture

## Git
### Package Structure

- Never use `git commit --amend` - always create new commits
The monorepo contains these key packages in `packages/`:

## Key Files
1. **@jitl/quickjs-ffi-types** - Low-level FFI types defining the C interface. No WebAssembly linkage.

- `scripts/prepareVariants.ts` - Generates all variant packages from templates
- `scripts/generate.ts` - Generates FFI bindings and symbols
- `templates/Variant.mk` - Makefile template for variants
- `c/interface.c` - C interface to QuickJS exposed to JavaScript
2. **quickjs-emscripten-core** - High-level TypeScript abstractions (QuickJSContext, QuickJSRuntime, QuickJSWASMModule, Lifetime, Scope). Does NOT include WebAssembly - callers must provide a variant.

## QuickJS C API Tips
3. **quickjs-emscripten** - Main user-facing package. Combines core with pre-built WASM variants. Entry point: `getQuickJS()`.

### Value Ownership
4. **Variant packages** (28 total) - Pre-compiled WebAssembly modules with different configurations:
- Naming: `variant-{BASE}-{FORMAT}-{OPTIMIZATION}-{MODE}`
- BASE: `quickjs` or `quickjs-ng`
- FORMAT: `wasmfile` or `singlefile`
- OPTIMIZATION: `release` or `debug`
- MODE: `sync` or `asyncify`

QuickJS has strict ownership semantics. Functions either "consume" (take ownership of) or "borrow" values:
### C/WASM Layer

**Functions that CONSUME values (caller must NOT free afterward):**
- **c/interface.c** - Main C wrapper around QuickJS. Functions prefixed `QTS_` are exported to JavaScript via FFI.
- **vendor/quickjs/** - Official QuickJS C library
- **vendor/quickjs-ng/** - QuickJS-ng fork
- **templates/Variant.mk** - Makefile template for building variants

- `JS_DefinePropertyValue` - consumes `val`
- `JS_DefinePropertyValueStr` - consumes `val`
- `JS_DefinePropertyValueUint32` - consumes `val`
- `JS_SetPropertyValue` - consumes `val`
- `JS_SetPropertyStr` - consumes `val`
- `JS_Throw` - consumes the error value
### Code Generation

**Functions that DUP values internally (caller SHOULD free afterward):**
- `scripts/prepareVariants.ts` - Generates variant packages from templates
- `scripts/generate.ts` - Generates FFI bindings from C function signatures
- `scripts/emcc.sh` - Emscripten wrapper with Docker fallback

- `JS_NewCFunctionData` - calls `JS_DupValue` on data values, so free your reference after
- `JS_SetProperty` - dups the value
## Key Concepts

**Common double-free bug pattern:**
### Memory Management

```c
// WRONG - double free!
JSValue val = JS_NewString(ctx, "hello");
JS_DefinePropertyValueStr(ctx, obj, "name", val, JS_PROP_CONFIGURABLE);
JS_FreeValue(ctx, val); // BUG: val was already consumed!
Handles to QuickJS values must be manually disposed with `.dispose()`. The library supports:
- Explicit `.dispose()` calls
- `using` statement (Stage 3 proposal)
- `Scope` class for batch disposal
- `Lifetime.consume(fn)` for use-then-dispose pattern

// CORRECT
JSValue val = JS_NewString(ctx, "hello");
JS_DefinePropertyValueStr(ctx, obj, "name", val, JS_PROP_CONFIGURABLE);
// No JS_FreeValue needed - value is consumed
```
### Asyncify

### quickjs vs quickjs-ng Differences
Special build variants use Emscripten's ASYNCIFY transform to allow synchronous QuickJS code to call async host functions. Comes with 2x size overhead. Only one async operation can suspend at a time per module.

Some functions have different signatures between bellard/quickjs and quickjs-ng:
### Build Variants

```c
// bellard/quickjs - class ID is global
JS_NewClassID(&class_id);
- RELEASE_SYNC - Default production variant
- RELEASE_ASYNC - Production with asyncify
- DEBUG_SYNC - Development with memory leak detection
- DEBUG_ASYNC - Development with asyncify

// quickjs-ng - class ID is per-runtime
JS_NewClassID(rt, &class_id);
```

Use `#ifdef QTS_USE_QUICKJS_NG` for compatibility:

```c
#ifdef QTS_USE_QUICKJS_NG
JS_NewClassID(rt, &class_id);
#else
JS_NewClassID(&class_id);
#endif
```

### Class Registration

- `JS_NewClassID` allocates a new class ID (only call once globally or per-runtime for ng)
- `JS_NewClass` registers the class definition with a runtime
- `JS_IsRegisteredClass` checks if a class is already registered with a runtime
- Class prototypes default to `JS_NULL` for new classes - set with `JS_SetClassProto` if needed

### Disposal Order

When disposing resources, order matters for finalizers:
## Testing

```typescript
// CORRECT: Free runtime first so GC finalizers can call back to JS
const rt = new Lifetime(ffi.QTS_NewRuntime(), undefined, (rt_ptr) => {
ffi.QTS_FreeRuntime(rt_ptr); // 1. Free runtime - runs GC finalizers
callbacks.deleteRuntime(rt_ptr); // 2. Then delete callbacks
});
```
Main test suite: `packages/quickjs-emscripten/src/quickjs.test.ts`

### GC and Prevent Corruption Assertions
Tests use Vitest. The DEBUG_SYNC variant enables memory leak detection - use `TestQuickJSWASMModule.assertNoMemoryAllocated()` to verify handles are disposed.

If you see assertions like:
- `Assertion failed: i != 0, at: quickjs.c, JS_FreeAtomStruct` - atom hash corruption (often double-free)
- `Assertion failed: list_empty(&rt->gc_obj_list)` - objects leaked
- `Assertion failed: p->gc_obj_type == JS_GC_OBJ_TYPE_JS_OBJECT` - memory corruption
## Build Requirements

These usually indicate memory management bugs: double-frees, use-after-free, or missing frees.
- Node.js 16+
- Yarn 4.0.2 (workspaces)
- Emscripten 3.1.65 (or Docker fallback via `scripts/emcc.sh`)
1 change: 1 addition & 0 deletions Changelog
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2025-12-22: First public version
152 changes: 152 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#CONFIG_PROFILE=y
#CONFIG_X86_32=y
#CONFIG_ARM32=y
#CONFIG_WIN32=y
#CONFIG_SOFTFLOAT=y
#CONFIG_ASAN=y
#CONFIG_GPROF=y
CONFIG_SMALL=y
# consider warnings as errors (for development)
#CONFIG_WERROR=y

ifdef CONFIG_ARM32
CROSS_PREFIX=arm-linux-gnu-
endif

ifdef CONFIG_WIN32
ifdef CONFIG_X86_32
CROSS_PREFIX?=i686-w64-mingw32-
else
CROSS_PREFIX?=x86_64-w64-mingw32-
endif
EXE=.exe
else
CROSS_PREFIX?=
EXE=
endif

HOST_CC=gcc
CC=$(CROSS_PREFIX)gcc
CFLAGS=-Wall -g -MMD -D_GNU_SOURCE -fno-math-errno -fno-trapping-math
HOST_CFLAGS=-Wall -g -MMD -D_GNU_SOURCE -fno-math-errno -fno-trapping-math
ifdef CONFIG_WERROR
CFLAGS+=-Werror
HOST_CFLAGS+=-Werror
endif
ifdef CONFIG_ARM32
CFLAGS+=-mthumb
endif
ifdef CONFIG_SMALL
CFLAGS+=-Os
else
CFLAGS+=-O2
endif
#CFLAGS+=-fstack-usage
ifdef CONFIG_SOFTFLOAT
CFLAGS+=-msoft-float
CFLAGS+=-DUSE_SOFTFLOAT
endif # CONFIG_SOFTFLOAT
HOST_CFLAGS+=-O2
LDFLAGS=-g
HOST_LDFLAGS=-g
ifdef CONFIG_GPROF
CFLAGS+=-p
LDFLAGS+=-p
endif
ifdef CONFIG_ASAN
CFLAGS+=-fsanitize=address -fno-omit-frame-pointer
LDFLAGS+=-fsanitize=address -fno-omit-frame-pointer
endif
ifdef CONFIG_X86_32
CFLAGS+=-m32
LDFLAGS+=-m32
endif
ifdef CONFIG_PROFILE
CFLAGS+=-p
LDFLAGS+=-p
endif

# when cross compiling from a 64 bit system to a 32 bit system, force
# a 32 bit output
ifdef CONFIG_X86_32
MQJS_BUILD_FLAGS=-m32
endif
ifdef CONFIG_ARM32
MQJS_BUILD_FLAGS=-m32
endif

PROGS=mqjs$(EXE) example$(EXE)
TEST_PROGS=dtoa_test libm_test

all: $(PROGS)

MQJS_OBJS=mqjs.o readline_tty.o readline.o mquickjs.o dtoa.o libm.o cutils.o
LIBS=-lm

mqjs$(EXE): $(MQJS_OBJS)
$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)

mquickjs.o: mquickjs_atom.h

mqjs_stdlib: mqjs_stdlib.host.o mquickjs_build.host.o
$(HOST_CC) $(HOST_LDFLAGS) -o $@ $^

mquickjs_atom.h: mqjs_stdlib
./mqjs_stdlib -a $(MQJS_BUILD_FLAGS) > $@

mqjs_stdlib.h: mqjs_stdlib
./mqjs_stdlib $(MQJS_BUILD_FLAGS) > $@

mqjs.o: mqjs_stdlib.h

# C API example
example.o: example_stdlib.h

example$(EXE): example.o mquickjs.o dtoa.o libm.o cutils.o
$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)

example_stdlib: example_stdlib.host.o mquickjs_build.host.o
$(HOST_CC) $(HOST_LDFLAGS) -o $@ $^

example_stdlib.h: example_stdlib
./example_stdlib $(MQJS_BUILD_FLAGS) > $@

%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<

%.host.o: %.c
$(HOST_CC) $(HOST_CFLAGS) -c -o $@ $<

test: mqjs example
./mqjs tests/test_closure.js
./mqjs tests/test_language.js
./mqjs tests/test_loop.js
./mqjs tests/test_builtin.js
# test bytecode generation and loading
./mqjs -o test_builtin.bin tests/test_builtin.js
# @sha256sum -c test_builtin.sha256
./mqjs -b test_builtin.bin
./example tests/test_rect.js

microbench: mqjs
./mqjs tests/microbench.js

octane: mqjs
./mqjs --memory-limit 256M tests/octane/run.js

size: mqjs
size mqjs mqjs.o readline.o cutils.o dtoa.o libm.o mquickjs.o

dtoa_test: tests/dtoa_test.o dtoa.o cutils.o tests/gay-fixed.o tests/gay-precision.o tests/gay-shortest.o
$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)

libm_test: tests/libm_test.o libm.o
$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)

rempio2_test: tests/rempio2_test.o libm.o
$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)

clean:
rm -f *.o *.d *~ tests/*.o tests/*.d tests/*~ test_builtin.bin mqjs_stdlib mqjs_stdlib.h mquickjs_build_atoms mquickjs_atom.h mqjs_example example_stdlib example_stdlib.h $(PROGS) $(TEST_PROGS)

-include $(wildcard *.d)
Loading
Loading