Skip to content

Latest commit

 

History

History
215 lines (152 loc) · 6.29 KB

File metadata and controls

215 lines (152 loc) · 6.29 KB

quickjs-emscripten

Package Manager

Use corepack pnpm to run pnpm commands, e.g.:

  • corepack pnpm install
  • corepack pnpm build
  • corepack pnpm build:ts

Building Variants

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

To build a variant:

cd packages/variant-quickjs-<name>
make           # builds the C code with emscripten
corepack pnpm 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:

corepack pnpm test

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

cd packages/quickjs-emscripten
npx vitest run -t "quickjs-ng"

Other test filters:

  • -t "RELEASE_SYNC" - only release sync variants
  • -t "DEBUG" - only debug variants
  • -t "QuickJSContext" - only sync context tests

Git

  • Never use git commit --amend - always create new commits. It's better to have a history of what was actually done since branches get rebased/squashed anyway when merged.
  • Before pushing, run pnpm run prettier and pnpm run lint to fix formatting and lint issues. No need to check after every commit - just before pushing.

CI Workflow

After pushing a PR, monitor CI using the gh CLI:

# Check CI status for a PR
gh pr checks <PR_NUMBER>

# View a specific job's progress
gh run view --job=<JOB_ID>

# Get logs for failed steps
gh run view <RUN_ID> --log-failed

When CI fails:

  1. Get the failed logs with gh run view <RUN_ID> --log-failed | tail -100
  2. Fix the issue locally
  3. Commit the fix (new commit, don't amend)
  4. Push and repeat until CI passes

The full CI build takes ~20 minutes due to emscripten compilation.

Key Files

  • 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

QuickJS C API Tips

Value Ownership

QuickJS has strict ownership semantics. Functions either "consume" (take ownership of) or "borrow" values:

Functions that CONSUME values (caller must NOT free afterward):

  • 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

Functions that DUP values internally (caller SHOULD free afterward):

  • JS_NewCFunctionData - calls JS_DupValue on data values, so free your reference after
  • JS_SetProperty - dups the value

Common double-free bug pattern:

// 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!

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

quickjs vs quickjs-ng Differences

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

// bellard/quickjs - class ID is global
JS_NewClassID(&class_id);

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

Use #ifdef QTS_USE_QUICKJS_NG for compatibility:

#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:

// 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
});

GC and Prevent Corruption Assertions

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

These usually indicate memory management bugs: double-frees, use-after-free, or missing frees.

C Code Style in interface.c

Naming Conventions

  • QTS_ prefix for exported FFI functions (defined in interface.h, called from JavaScript)
  • qts_ prefix for internal C helper functions
  • static keyword for file-local functions and variables
  • Use bool from <stdbool.h> for boolean values (already included)

Code Organization

  • Section comments with // ---- separators for logical grouping
  • Forward declarations at the top of sections when needed
  • Constants defined near related functions

Error Handling Patterns

  • Check JS_IsException(value) for error conditions
  • Use JS_GetException(ctx) to retrieve the exception value
  • Clean up resources in reverse order of allocation
  • Return JS_EXCEPTION or NULL to signal errors to callers

Example

// ----------------------------------------------------------------------------
// Section Name

// Forward declaration
static JSValue qts_helper_function(JSContext *ctx, JSValueConst obj);

// Internal helper - not exported
static bool qts_check_something(JSContext *ctx, JSValueConst obj) {
  if (!JS_IsObject(obj)) return false;
  // ... implementation
  return true;
}

// Exported FFI function
JSValue *QTS_DoSomething(JSContext *ctx, JSValueConst *obj) {
  JSValue result = qts_helper_function(ctx, *obj);
  if (JS_IsException(result)) {
    return jsvalue_to_heap(JS_EXCEPTION);
  }
  return jsvalue_to_heap(result);
}