GitHub - aperturerobotics/go-quickjs-wasi-reactor: Run quickjs efficiently under Go

5 min read Original article ↗

go-quickjs-wasi-reactor

GoDoc Widget Go Report Card Widget

A Go module that embeds the QuickJS-NG WASI WebAssembly runtime (reactor model).

Related Projects

Variants

This repository provides the reactor model WASM binary for re-entrant execution. If you only need simple blocking execution where QuickJS runs to completion in _start(), see the command model variant above.

About QuickJS-NG

QuickJS is a small and embeddable JavaScript engine. It aims to support the latest ECMAScript specification.

This project uses QuickJS-NG, a community-driven fork of the original QuickJS project by Fabrice Bellard and Charlie Gordon. Both projects are actively maintained; QuickJS-NG focuses on community contributions and features like the WASI reactor build used here.

Purpose

This module provides easy access to the QuickJS-NG JavaScript engine compiled to WebAssembly with WASI support using the reactor model. The WASM binary is embedded directly in the Go module, making it easy to use QuickJS in Go applications without external dependencies.

Reactor Model

Unlike the standard WASI "command" model that blocks in _start(), the reactor model exports the raw QuickJS C API functions, enabling full control over the JavaScript runtime lifecycle from the host environment.

The reactor exports the complete QuickJS C API including:

Core Runtime:

  • JS_NewRuntime, JS_FreeRuntime - Runtime lifecycle
  • JS_NewContext, JS_FreeContext - Context lifecycle
  • JS_Eval - Evaluate JavaScript code
  • JS_Call - Call JavaScript functions

Standard Library (quickjs-libc.h):

  • js_init_module_std, js_init_module_os, js_init_module_bjson - Module initialization
  • js_std_init_handlers, js_std_free_handlers - I/O handler setup
  • js_std_add_helpers - Add console.log, print, etc.
  • js_std_loop_once - Run one iteration of the event loop (non-blocking)
  • js_std_poll_io - Poll for I/O events

Memory Management:

  • malloc, free, realloc, calloc - For host to allocate memory

Features

  • Embeds the QuickJS-NG WASI reactor WebAssembly binary
  • Provides version information about the embedded QuickJS release
  • High-level Go API via the wazero-quickjs subpackage
  • Update script to build and copy from local QuickJS checkout

Packages

Root Package (github.com/aperturerobotics/go-quickjs-wasi-reactor)

Provides the embedded WASM binary and version information:

package main

import (
    "fmt"
    quickjswasi "github.com/aperturerobotics/go-quickjs-wasi-reactor"
)

func main() {
    // Access the embedded WASM binary
    wasmBytes := quickjswasi.QuickJSWASM
    fmt.Printf("QuickJS WASM size: %d bytes\n", len(wasmBytes))

    // Get version information
    fmt.Printf("QuickJS version: %s\n", quickjswasi.Version)
    fmt.Printf("Download URL: %s\n", quickjswasi.DownloadURL)
}

Wazero QuickJS Library (github.com/aperturerobotics/go-quickjs-wasi-reactor/wazero-quickjs)

High-level Go API for running JavaScript with wazero:

package main

import (
    "context"
    "embed"
    "os"

    quickjs "github.com/aperturerobotics/go-quickjs-wasi-reactor/wazero-quickjs"
    "github.com/tetratelabs/wazero"
)

//go:embed scripts
var scriptsFS embed.FS

func main() {
    ctx := context.Background()
    r := wazero.NewRuntime(ctx)
    defer r.Close(ctx)

    config := wazero.NewModuleConfig().
        WithStdout(os.Stdout).
        WithStderr(os.Stderr).
        WithFS(scriptsFS)  // Mount embedded filesystem

    qjs, _ := quickjs.NewQuickJS(ctx, r, config)
    defer qjs.Close(ctx)

    // Option 1: Initialize with CLI args to load script via WASI filesystem
    qjs.Init(ctx, []string{"qjs", "--std", "scripts/main.js"})

    // Option 2: Initialize with --std and eval code directly
    // qjs.Init(ctx, []string{"qjs", "--std"})
    // qjs.Eval(ctx, `console.log("Hello from QuickJS!");`, false)

    // Run event loop until idle
    qjs.RunLoop(ctx)
}

Initialization Options

// Basic runtime with std modules available for import
qjs.Init(ctx, nil)
qjs.Eval(ctx, `import * as std from 'qjs:std'; std.printf("Hello\n")`, true)

// With --std flag to expose std, os, bjson as globals
qjs.Init(ctx, []string{"qjs", "--std"})
qjs.Eval(ctx, `std.printf("Hello\n")`, false)  // std is already global

// With script args accessible via scriptArgs global
qjs.Init(ctx, []string{"qjs", "script.js", "--verbose"})
qjs.Eval(ctx, `console.log(scriptArgs)`, false)  // ['qjs', 'script.js', '--verbose']

Non-Blocking Event Loop

The reactor model enables cooperative scheduling with the host:

// Instead of blocking RunLoop(), use LoopOnce() for fine-grained control
for {
    result, err := qjs.LoopOnce(ctx)
    if err != nil {
        return err
    }

    switch {
    case result == quickjs.LoopIdle:
        // No pending work - done
        return nil
    case result == quickjs.LoopError:
        return errors.New("JavaScript error")
    case result == 0:
        // More microtasks pending - continue immediately
        continue
    case result > 0:
        // Timer pending - host can do other work before next iteration
        time.Sleep(time.Duration(result) * time.Millisecond)
    }
}

I/O Polling

For scripts that use os.setReadHandler(), the host must poll for I/O:

// Poll for I/O events (non-blocking)
result, _ := qjs.PollIO(ctx, 0)

// Poll with timeout (blocking up to 100ms)
result, _ := qjs.PollIO(ctx, 100)

See the wazero-quickjs README for more details.

Command-Line REPL

A command-line JavaScript runner with interactive REPL mode is provided:

# Install
go install github.com/aperturerobotics/go-quickjs-wasi-reactor/wazero-quickjs/repl@master

# Interactive REPL
repl

# Run a JavaScript file
repl script.js

# Run as ES module
repl script.mjs --module

Updating

To update to the latest QuickJS-NG reactor build from a local checkout:

# Set QUICKJS_DIR to your quickjs checkout (default: ../quickjs)
QUICKJS_DIR=/path/to/quickjs ./update-quickjs.bash

This script will:

  1. Read the current branch and commit from your local quickjs checkout
  2. Copy the qjs-wasi-reactor.wasm file
  3. Generate version information constants

Building the WASM file

To build the WASM file from source:

cd /path/to/quickjs

# Create build directory
mkdir -p build-wasi-reactor && cd build-wasi-reactor

# Configure with WASI SDK
cmake .. \
  -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake \
  -DQJS_WASI_REACTOR=ON \
  -DCMAKE_BUILD_TYPE=Release

# Build
make -j

# Copy output
cp qjs.wasm ../qjs-wasi-reactor.wasm

Testing

go test ./...
cd wazero-quickjs && go test ./...

License

This module is released under the same license as the embedded QuickJS-NG project.

MIT