spidermonkey

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: MIT Imports: 15 Imported by: 0

README

go-spidermonkey

PkgGoDev

SpiderMonkey in pure Go — run untrusted JavaScript anywhere Go runs. No cgo, no WebAssembly runtime, one static binary.

The engine is Firefox's SpiderMonkey, and every guest script is sandboxed: a host watchdog can stop a runaway loop from outside, and the guest gets no filesystem, network, or host environment unless you hand it one.

It is measurably SpiderMonkey: CI runs the official test262 conformance suite — the full suite, ICU, ES modules, SharedArrayBuffer/Atomics and multi-agent tests included — and this embedding passes 52,266 of the 53,406 tests in the revision test262.fyi measures the big engines against (97.9% counting skips against us; 98.0% of the 53,329 it hosts). That is within 0.4 points of the SpiderMonkey nightly shell (98.3% of the same revision), and in the core language and built-ins categories it matches or exceeds it — the whole gap is Intl/ICU (details).

SpiderMonkey is compiled to wasm32-wasi by goccy/spidermonkey-wasm, then translated ahead of time into Go by wasm2go. What you import is ordinary Go that builds and links like any other package.

js, err := spidermonkey.New(spidermonkey.Config{})
if err != nil {
    log.Fatal(err)
}
defer js.Close()

r, err := js.Eval(context.Background(), "[1, 2, 3].map(x => x * 2).join(',')")
if err != nil {
    log.Fatal(err) // a host/transport failure, or ctx cancelled
}
if r.Error != nil {
    log.Fatal(r.Error) // the script threw
}
fmt.Println(r.Value.String()) // 2,4,6

Eval reports anything the script does wrong in the Result, not as a Go error: Error is non-nil and carries the exception and its stack, and Value is valid only when Error is nil. A Go error means the host side failed or ctx was cancelled. Output a host console writes goes to Config.Stdout / Stderr — SpiderMonkey has no I/O of its own, so nothing reaches the host's streams unless a host function puts it there.

The global persists across calls, so one JS instance behaves like a REPL. Microtasks are drained before Eval returns, so a top-level Promise.resolve().then(…) has run by the time you see the result.

Isolation

Each JS instance owns its own wasm instance — its own linear memory, its own JavaScript runtime. Two instances share nothing and run concurrently. A single instance serialises its own calls, so it is safe to use from several goroutines, but it executes one script at a time.

Stopping a runaway script

Pass a context to Eval; cancelling it (a timeout, a deadline, an explicit cancel()) interrupts the running script:

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

_, err := js.Eval(ctx, "while (true) {}")
// err == context.DeadlineExceeded — the loop was interrupted

Cancellation runs no guest code: the harness sets an interrupt flag in the instance's linear memory from another goroutine, and SpiderMonkey's interpreter notices it at the next bytecode loop head. That matters — running guest code on another goroutine would corrupt the instance's C stack.

The termination is an uncatchable exception. A script cannot swallow it:

try { while (true) {} } catch (e) { 'swallowed' }   // never yields 'swallowed'

Interruption only lands at bytecode loop heads, so a single long-running primitive — a pathological regex, a huge sort — is not preempted until it returns to the interpreter loop.

What a script can reach

print() and console.log/info/warn/error, plus the JavaScript standard library. Nothing else. There is no fetch, no timers, no file or network access: SpiderMonkey has no I/O of its own, the embedding installs no builtin that reaches any, and the wasm is linked with no host-socket or host-subprocess capability. Every import the module has is wasi_snapshot_preview1.

Conformance: test262

The claim "the engine is SpiderMonkey" is measured, not asserted. CI runs the official ECMAScript conformance suite — tc39/test262, vendored as the test262/suite submodule, pinned to revision f2d1435 — the same revision test262.fyi measures the big engines against, so the numbers below are directly comparable — on every change:

area pass / run rate
language 23,285 / 23,710 98.2%
built-ins 23,316 / 23,594 98.8%
intl402 3,013 / 3,341 90.2%
annexB 1,058 / 1,086 97.4%
harness 116 / 116 100%
staging 1,478 / 1,482 99.7%
total 52,266 / 53,329 98.0%

That is the FULL suite: ICU is compiled in (Intl.*, Temporal, regexp \p{...}, Unicode normalization and case folding all run), ES modules load through the host registry (module-flagged tests, dynamic-import), and the engine is built for wasi-threads, so SharedArrayBuffer, the whole Atomics family including Atomics.waitAsync, and test262's multi-agent tests ($262.agent.*) run for real — every agent is a SpiderMonkey thread that wasm2go hosts on a goroutine.

Measured against SpiderMonkey

Counting skipped tests against us — the way test262.fyi counts — this embedding passes 52,266 of all 53,406 tests (97.9%). On the same revision the SpiderMonkey nightly shell (154.0a1) passes 52,489 (98.3%); with experimental flags, 52,640 (98.6%). The 223-test, 0.4-point gap is not spread across the suite — it is almost entirely Intl:

area this embedding (FF 147) SpiderMonkey nightly (154) Δ
built-ins 23,316 23,272 +44
language 23,285 23,230 +55
staging 1,478 1,451 +27
harness 116 116 0
annexB 1,058 1,084 −26
intl402 3,013 3,336 −323

In the core ECMAScript categories this build matches or exceeds the nightly shell — it does not carry the nightly's regressions — and the only material deficit is intl402, where the engine is seven Firefox versions behind (147 vs 154) and its bundled ICU4X data does not yet cover every locale the newer Intl proposals exercise. Closing that gap is engine-build work in goccy/spidermonkey-wasm, not a limit of running SpiderMonkey as Go.

Only 77 tests are skipped, each accounted with its printed reason: 64 ShadowRealm (a proposal off by default in the stock SpiderMonkey shell too), 11 $262.AbstractModuleSource (host hook not exposed), and 2 CanBlockIsFalse (this embedding's main agent may block, so the premise does not apply). The skip set is decided by probing the running engine, so a feature the build actually ships is never hidden — Atomics.pause, for one, is shipped and runs.

The 1,063 failures are pinned one by one in test262/expectations.json — CI is green exactly when the delta is the documented one, so a regression and a silent improvement both fail the run. The negative-test judge matches each expected error by its exact constructor name and phase, so a green run cannot be bought with a lenient check.

make test262   # inits the submodule, then TEST262=1 go test -run TestTest262 .

Bounding what a script consumes

Config field Bounds On breach
MaxMemoryBytes the wasm linear memory — GC heap, engine data and the C stack together the instance traps and is dead

MaxMemoryBytes is the one cap. It sizes the wasm linear memory the guest grows into, and everything the engine allocates — the GC heap, its own data, the C stack — lives inside it. It protects the host process, not the script: several SpiderMonkey allocation paths are infallible and abort rather than throw, so reaching the cap kills the instance rather than surfacing a catchable error. Runaway recursion is the case the engine still catches on its own — it raises a catchable InternalError: too much recursion well before the stack exhausts the cap.

Raising MaxMemoryBytes costs interpreter construction time, not resident memory: the pages are mapped, not touched, and a booted instance's resident set is the same whatever the cap. Linear memory only ever grows — wasm has no shrink, and the guest's allocator reuses freed space only when its free list can serve the request — so a script that churns large allocations creeps toward the cap even with a small live set. Raise the cap before concluding a script leaks.

To stop a script that runs too long rather than allocates too much, cancel the context you pass to Eval (see Stopping a runaway script).

The zero Config is a usable, sandboxed instance: 256 MiB wasm memory, an empty environment, and stdio that goes nowhere.

Performance

bench/ runs the same JavaScript on three engines. Apple M5, Go 1.26, -benchtime 10x:

fib(30) loop sum (1e6) boot allocs on the loop
go-spidermonkey 171 ms 37 ms 0.3 ms 27
goja (pure Go) 182 ms 48 ms 2 µs 2,000,000
node (V8, JIT) ~3 ms ~1 ms 43 ms

Node's row subtracts its 43 ms process startup, which every one of its iterations pays. It is not a peer: V8 JITs, and a JIT cannot emit machine code from inside a wasm sandbox, so this engine runs SpiderMonkey's portable baseline interpreter — enabled at runtime since the v0.2.4 bundle (spidermonkey-wasm v0.2.5), which also transpiles the engine's atomic accesses to inline Go intrinsics. The ceiling is there to be honest about the cost of the sandbox.

Against goja — the like-for-like comparison, since both interpret — go-spidermonkey is now faster on both workloads: a little ahead on the call-bound fib, about a quarter faster on the dispatch-bound loop. And it does that with 27 allocations against goja's two million: the guest's values live inside the wasm instance's linear memory, so they never touch the Go heap and never enter a Go GC cycle.

Boot costs about 0.3 ms. The instance's 256 MiB linear memory is an mmap'd copy-on-write mapping, so it reserves address space but stays almost entirely non-resident until the guest writes to it — and it never lands on the Go heap (a live instance holds ~0.1 MiB of it). goja boots in microseconds. If you create an interpreter per request, that difference is the one to weigh; if you keep a pool of them, it disappears.

cd bench && go test -bench . -benchmem ./...
cd bench && go test -run TestMemoryFootprint -v ./...

License

  • The Go source code of this repository is licensed under MIT. That covers everything written or generated here — interpreter.go, the generated bridge spidermonkey.go, the tests and the benchmarks.
  • The SpiderMonkey engine is not MIT. It reaches your program through the spidermonkeywasm2go dependency — SpiderMonkey (Mozilla Firefox 147.0.4) translated to Go — which is a derivative work of SpiderMonkey and keeps SpiderMonkey's own license, the Mozilla Public License, Version 2.0. That license text lives in that repository; no SpiderMonkey-derived bytes are vendored here.
Using go-spidermonkey in your own project
  • As a library dependency (source distribution): your repository contains no SpiderMonkey-derived bytes — only an import path and a go.mod entry. License your own code however you like (MIT, proprietary, ...); no MPL text needs to accompany it. Your users receive go-spidermonkey and spidermonkeywasm2go from their own origins, under their own licenses.
  • Shipping a compiled binary: the binary embeds the translated engine, whose files are under the MPL 2.0. The MPL is file-level copyleft: it reaches only those already-MPL engine files (their source form must remain available under the MPL), and expressly does not extend to your own code that merely links against them (§1.10, §3.3). So your application keeps its own license; only the engine files retain theirs.

Documentation

Overview

Package spidermonkey runs untrusted JavaScript on Mozilla's SpiderMonkey engine from pure Go. The engine is exactly ECMA-262: host surfaces like console and setTimeout are not built in — an embedder adds them with Global().DefineFunc.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Throw added in v0.2.0

func Throw(v Value) error

Throw wraps v so a host Func can throw it verbatim: `return nil, Throw(v)` makes the guest see v thrown (a SyntaxError instance stays a SyntaxError), whereas returning an ordinary error surfaces as a generic Error with that message.

Types

type AgentID added in v0.2.0

type AgentID uint64

AgentID identifies one spawned agent within a cluster.

type Agents added in v0.2.0

type Agents struct {
	// contains filtered or unexported fields
}

Agents is the interpreter's agent cluster — the host surface over ECMA-262 agents. The spec defines what an agent IS (its own thread of execution and realm, sharing nothing with other agents but SharedArrayBuffer memory) and leaves creation and communication to the host; this type IS that host policy, implemented in Go. The engine bridge contributes only the thread mechanics (Spawn) and structured-clone transport; the queues, the broadcast latch and the lifecycle tracking all live here, so richer topologies ($262.agent, Web Workers, worker_threads with per-agent channels) are adapters over this type rather than engine changes.

Two delivery models, both over the same structured-clone transport:

  • Broadcast(v) latches ONE value that every agent's broadcast receive gets (test262's $262.agent). A SharedArrayBuffer in v SHARES its memory with each receiver; everything else is a deep copy.
  • Send(to, v) delivers v to ONE agent's FIFO inbox (postMessage to a specific worker). The agent reads it with its inbox receive.

Either direction back to the host is an agent's post(v), popped with Receive() — which reports the sender, so a Worker's onmessage can route.

func (*Agents) Alive added in v0.2.0

func (a *Agents) Alive() int

Alive reports how many spawned agents have not yet exited.

func (*Agents) Broadcast added in v0.2.0

func (a *Agents) Broadcast(v Value) error

Broadcast latches v as THE broadcast and wakes every agent blocked in receive. Agents that call receive later get the same value. A previously latched value is superseded (its clone is retained until Close, since a slow receiver may still be reading it).

func (*Agents) Interrupt added in v0.5.0

func (a *Agents) Interrupt(id AgentID) (bool, error)

Interrupt stops one agent whatever it is doing: its script ends with an uncatchable exception (guest JS cannot swallow it) and the agent leaves instead of resuming its pump.

This is the forceful counterpart to a cooperative stop. Everything else here is a MESSAGE, which an agent only sees between job-queue drains — and a runaway `while(true){}` never drains, so no message can reach it. Interrupt goes through the agent's own engine context instead, which also wakes one parked in Atomics.wait or idling with nothing to do.

Asynchronous: it returns once the agent is signalled, not once it has gone (the agent may be mid-bytecode on its own thread) — poll IsAlive to wait for the exit. Reports false when no agent with this id is running, in which case there is nothing left to stop.

func (*Agents) IsAlive added in v0.5.0

func (a *Agents) IsAlive(id AgentID) bool

IsAlive reports whether the agent with id has not yet exited. A Worker adapter polls this to fire 'exit' even when an agent ends without posting a farewell (e.g. an uncaught error during evaluation).

func (*Agents) Receive added in v0.2.0

func (a *Agents) Receive() (from AgentID, v Value, ok bool, err error)

Receive pops the next value an agent posted, with the id of the sender (so a Worker adapter can route it to the right onmessage). ok is false when the queue is empty. The value is deserialized into this interpreter's runtime: an object arrives as an *Object, a SharedArrayBuffer still shares its memory.

func (*Agents) Send added in v0.2.0

func (a *Agents) Send(to AgentID, v Value) error

Send delivers v to one agent's FIFO inbox (postMessage to a specific worker) and wakes it if it is blocked in its inbox receive. A SharedArrayBuffer in v shares its memory with the agent; everything else is a deep copy.

func (*Agents) Spawn added in v0.2.0

func (a *Agents) Spawn(glue, src string) (AgentID, error)

Spawn runs an agent on a new thread (a goroutine), its own realm, sharing only SharedArrayBuffer memory with this interpreter. `glue` is trusted adapter setup (evaluated with the agent-primitive prelude, which exposes globalThis.__agent__ = { receive(cb) (blocking broadcast), recv() (blocking inbox), tryRecv()/NO_MSG (non-blocking inbox poll), post(v), sleep(ms), leaving(), monotonicNow() }); `src` is the user source, evaluated as its OWN separate script so its strict directive, line numbers and module-ness are untouched. After both evaluate, the agent drains its job queue until leaving() (or Close). Returns the agent's id, so the host can Send to it and match its posts.

type Config

type Config struct {
	// MaxMemoryBytes caps the interpreter's wasm linear memory — the single
	// ceiling on everything it can allocate (GC heap, engine data, C stack).
	// It sizes the allocation the guest grows into; untouched pages never page
	// in, so headroom is nearly free. Hitting it aborts the guest (the host
	// gets an error and the instance is spent), so it is a hard host-protection
	// backstop, not a limit a script recovers from. Zero means the default.
	MaxMemoryBytes int

	// Env is the environment the guest sees (and that host functions may read
	// via the Config handed to them). nil means an empty environment — the host
	// process os.Environ() is NOT leaked.
	Env []string

	// Stdin, Stdout, Stderr are for HOST FUNCTIONS. SpiderMonkey has no I/O of
	// its own, so these do nothing until a host function uses them (a future
	// Node-compatible console/process). Unset streams are sandboxed (empty
	// stdin, discarded output), never the host process's.
	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer

	// FS is the filesystem the guest sees: it backs the default ES module
	// loader and every host-function file operation (the compat packages'
	// node:fs). SpiderMonkey itself has no filesystem API, so it does nothing
	// on its own. nil means modules must be supplied by a custom
	// SetModuleLoader and no filesystem is reachable.
	//
	// FS is ALSO the single point of filesystem access control: an
	// implementation enforces its own policy inside its Open/OpenFile/Stat/…
	// methods (deny a path with fs.ErrPermission, hide it with
	// fs.ErrNotExist), exactly as a sheena fs.Volume does with its
	// Refuse/Hide/Access rules. There is deliberately no separate access
	// callback — a plain fs.FS grants read access to everything it exposes
	// (the embedder chose to expose it), and a policy-carrying FS restricts
	// reads and writes uniformly from one place. An FS that also implements
	// WritableFS accepts writes; a plain fs.FS is read-only (writes surface
	// as EROFS).
	FS fs.FS

	// Dial is the outbound-connection whitelist, called before each connect:
	// host is the name the guest requested and that resolved to ip (or "" for a
	// literal-IP dial with no preceding lookup), ip is the dotted-quad/IPv6
	// being dialed, and port is the port. Returning false denies the
	// connection; a nil Dial denies ALL outbound connections. Passing host lets
	// a policy match host and port jointly (e.g. "example.com only on 443") — an
	// IP alone cannot be tied back to the name resolved. The connection is made
	// to the exact ip that was approved (the name is resolved once, before the
	// hook), so a DNS answer cannot smuggle a different address past the
	// allow-list. This mirrors wasm2go's WASI dial hook
	// (func(network, host, ip string, port int) bool).
	Dial func(network, host, ip string, port int) bool

	// Resolve is the name-resolution whitelist. It is called with the host
	// being resolved before each lookup; returning false denies it, and a nil
	// Resolve denies ALL name resolution (so a hostname connection needs both
	// Resolve and Dial; a literal-IP connection needs only Dial). This is where
	// a hostname policy such as "block example.com" is enforced.
	Resolve func(host string) bool

	// Listen gates inbound sockets. It is called with the network and local
	// address before each listen; returning false denies it, and a nil Listen
	// denies ALL inbound sockets.
	Listen func(network, addr string) bool

	// Exec is the subprocess whitelist. It is called before every process spawn
	// with the executable path and full argv; returning false denies the spawn,
	// and a nil Exec denies ALL spawns. Spawning runs a HOST binary, so a
	// sandbox that allows subprocess should set this to a strict allow-list.
	Exec func(path string, argv []string) bool
}

Config configures a JS interpreter. The zero value is a usable, fully sandboxed interpreter: no host environment leaks, no host stdio is attached, no filesystem is reachable, and the memory cap takes its default.

type Func added in v0.2.0

type Func func(cfg Config, args []Value) (Value, error)

Func is a Go function callable from guest JavaScript. It receives the interpreter's Config (so it can reach Env, stdio and FS) and the call's arguments — primitives as data, objects and functions as *Object handles with identity preserved. The returned Value crosses back the same way. A returned error surfaces in the guest as a thrown Error.

fn may re-enter its interpreter — navigate *Object arguments (Get/Set/Call) or even Eval — because the interpreter's invoke lock is released for the callback's duration: the guest is paused waiting for fn's reply, so re-entry continues from the current stack, exactly like a native function calling back into the engine. *Object arguments also stay valid after the evaluation returns (their handles pin the objects), so retaining them for later works too.

type JS added in v0.2.0

type JS struct {
	// contains filtered or unexported fields
}

JS is one isolated SpiderMonkey interpreter: its own wasm instance (its own linear memory) and one runtime. Several run concurrently and in isolation.

func New added in v0.2.0

func New(cfg Config) (*JS, error)

New creates a sandboxed interpreter from cfg. The zero Config is a usable, fully sandboxed interpreter.

func (*JS) Agents added in v0.2.0

func (js *JS) Agents() *Agents

Agents returns the interpreter's agent cluster.

func (*JS) Close added in v0.2.0

func (js *JS) Close() error

Close destroys the interpreter and releases its resources. Agents blocked in receive are released first (they unwind with an error) so the engine's shutdown join can complete.

func (*JS) Eval added in v0.2.0

func (js *JS) Eval(ctx context.Context, src string) (Result, error)

Eval runs src as a classic script to synchronous completion — draining the microtask queue — and returns the result. ctx aborts a runaway script: on cancellation the script is interrupted and Eval returns ctx.Err(). Eval does NOT drive timers or other async work; use RunJobs for that. A JavaScript throw is reported in Result (OK=false, Error), not as a Go error.

func (*JS) EvalModule added in v0.2.0

func (js *JS) EvalModule(ctx context.Context, specifier, src string) (ModuleResult, error)

EvalModule compiles and runs src as an ES module registered under specifier, resolving its imports through the module loader (the default Config.FS loader, or one set with SetModuleLoader) and draining the job queue. ctx aborts a runaway module. Like Eval, a JavaScript-level failure is in Result, not a Go error.

func (*JS) Global added in v0.2.0

func (js *JS) Global() *Object

Global returns the interpreter's global object. Defining a function on it is the host-surface opt-in: a fresh interpreter is pure ECMA-262 and exposes nothing until the embedder adds something.

js.Global().DefineFunc("print", func(cfg Config, args []Value) (Value, error) {
	...
})

func (*JS) NewBytes added in v0.3.0

func (js *JS) NewBytes(data []byte) (*Object, error)

NewBytes copies data into a fresh guest Uint8Array and returns its handle. This is the []byte -> JS direction of the binary bridge; the reverse is Object.Bytes. The returned Object is a Value, so it can be passed to Call, Set, or returned from a host Func like any other value. The caller owns the handle (Free releases the host's pin; the guest keeps the array alive as long as it references it).

func (*JS) NewFunction added in v0.3.0

func (js *JS) NewFunction(name string, fn Func) (*Object, error)

NewFunction returns a fresh guest function object backed by fn — the Go analogue of syscall/js's FuncOf. Unlike DefineFunc it attaches to nothing: the embedder composes it into any structure — an object property (Set), a callback argument (Call), an underlyingSource.pull — and identity is preserved wherever it flows. name is only the function's `name` property.

The caller owns the handle (Free releases the host's pin; the guest keeps the function alive as long as it references it). The Go-side registration lives for the interpreter's lifetime — it cannot be released earlier, since the guest may hold and call the function long after the host dropped its handle.

func (*JS) NewObject added in v0.2.0

func (js *JS) NewObject() (*Object, error)

NewObject creates a fresh, empty JavaScript object owned by this interpreter.

func (*JS) RegisterModuleResolver added in v0.5.0

func (js *JS) RegisterModuleResolver(prefix string, loader ModuleLoader)

RegisterModuleResolver installs loader for module specifiers beginning with prefix (e.g. "node:"), so several packages can each claim a specifier namespace without clobbering one another or the fallback loader. The longest matching registered prefix wins; specifiers matching no prefix go to the fallback (SetModuleLoader, or the default Config.FS loader). An empty prefix sets the fallback, exactly like SetModuleLoader. Registering nil removes the prefix's resolver. Not safe to call concurrently with evaluation.

func (*JS) RunJobs added in v0.2.0

func (js *JS) RunJobs(ctx context.Context) iter.Seq2[Result, error]

RunJobs drives the ECMA-262 job queue, yielding the result of each pump that ran work and ending when the queue is idle or ctx is done. Pending-but-not-due work (a timer, an Atomics.waitAsync timeout) is awaited internally, bounded by ctx, so every yielded Result reflects real progress. Break to stop early:

for r, err := range js.RunJobs(ctx) {
	if err != nil { break }
	// r.Stdout, r.OK, ...
}

func (*JS) SetModuleLoader added in v0.2.0

func (js *JS) SetModuleLoader(loader ModuleLoader)

SetModuleLoader installs the fallback loader — the one consulted when no resolver registered with RegisterModuleResolver matches the specifier. It is called on a registry miss and returns the module's source. It replaces the default Config.FS loader. Pass nil to disable fallback loading (imports then fall back to the "module not registered" failure).

func (*JS) TakeUnhandledRejections added in v0.5.0

func (js *JS) TakeUnhandledRejections() ([]Rejection, error)

TakeUnhandledRejections hands back every rejection still unhandled and forgets them. It is what `unhandledRejection` / `unhandledrejection` are built on: such a rejection is visible ONLY to the engine, since an async function's promise is created by the engine and so escapes any Promise wrapper installed from the host.

Call it at a microtask checkpoint — after Eval or after RunJobs runs dry — so a rejection the guest went on to handle in the same tick is not reported; the engine retracts those as soon as a handler is attached. Draining is destructive: each rejection is reported exactly once, however often this is called.

The caller owns any *Object in the result and must Free it.

type JSError added in v0.2.0

type JSError struct {
	// Message is the exception's stringification plus its stack, or a phrase
	// like "JS execution interrupted".
	Message string
}

JSError is a JavaScript-level failure: an uncaught throw, a compile error, a failed module import, or an interrupt. It is what Result.Error / ModuleResult .Error hold; it is distinct from a Go error returned by Eval/EvalModule, which signals a host/transport failure (a wasm trap, an encoding problem).

func (*JSError) Error added in v0.2.0

func (e *JSError) Error() string

type ModuleLoader added in v0.5.0

type ModuleLoader func(cfg Config, specifier, referrer string) (string, error)

ModuleLoader resolves a module specifier to its source. It receives the interpreter's Config, the resolved module specifier, and the specifier of the importing module (referrer).

type ModuleResult added in v0.2.0

type ModuleResult struct {
	// Error is non-nil when the module threw, failed to compile, failed to
	// resolve an import, or was interrupted.
	Error error
}

ModuleResult is the outcome of running an ES module with EvalModule. A module has NO completion value — its output is its side effects and (in future) its exports — so there is no Value field.

type Object added in v0.2.0

type Object struct {
	// contains filtered or unexported fields
}

Object is a handle to a JavaScript object (or function) owned by a JS interpreter. An Object IS a Value — it implements the Value interface — so it can be passed anywhere a Value is expected, and property navigation (Get) can return either a primitive Value or another Object.

The handle preserves identity: an Object that crosses the bridge in either direction refers to the SAME guest object, so mutations are visible on both sides. The handle pins the object against garbage collection until Free (or the interpreter's Close).

func (*Object) Bool added in v0.2.0

func (o *Object) Bool() bool

func (*Object) Bytes added in v0.3.0

func (o *Object) Bytes() ([]byte, error)

Bytes copies the object's binary contents into a fresh Go []byte. It accepts a Uint8Array, any other ArrayBuffer view (Int32Array, DataView, ...) — read as its raw byte window — an ArrayBuffer, or a SharedArrayBuffer. Any other object is an error. This is the JS -> []byte direction of the binary bridge; the reverse is JS.NewBytes.

func (*Object) Call added in v0.2.0

func (o *Object) Call(args ...Value) (Value, error)

Call invokes the object as a function with `this` undefined. A guest throw comes back as a *JSError.

func (*Object) CallMethod added in v0.2.0

func (o *Object) CallMethod(name string, args ...Value) (Value, error)

CallMethod invokes o[name](args...) with `this` bound to o.

func (*Object) DefineConstructor added in v0.2.0

func (o *Object) DefineConstructor(name, key string, fn Func) error

DefineConstructor defines a host-backed CONSTRUCTOR name on the object: `new name(...)` from JS runs fn, and the Value fn returns (typically an *Object built with NewObject) becomes the instance. This is how a real host class — `new Worker(src)` and the like — is defined from Go.

key lets several constructors share a name across objects with distinct Go callbacks; pass name itself for the common case.

func (*Object) DefineFunc added in v0.2.0

func (o *Object) DefineFunc(name string, fn Func) error

DefineFunc defines a host-backed function name on the object; calling it from JS runs fn with the interpreter's Config and the call arguments. Each definition dispatches under its own hidden key, so defining the same name on two objects never makes their Go callbacks collide. Like NewFunction, the Go-side registration lives for the interpreter's lifetime.

func (*Object) Export added in v0.2.0

func (o *Object) Export() any

func (*Object) Float added in v0.2.0

func (o *Object) Float() float64

func (*Object) Free added in v0.2.0

func (o *Object) Free() error

Free releases the object handle (its GC pin). The object itself lives on in the guest; only the host's reference is dropped. The global object (from Global) is freed by Close and must not be freed here.

Freeing twice is a no-op rather than a fault. A handle IS a pointer to the guest's GC root for this object, so a second release would delete it twice and corrupt the guest heap — with no symptom until something much later walks the root list, typically surfacing as an unrelated crash inside teardown. Ownership of a handle is easy to lose track of across the host's close paths, so the primitive absorbs it instead of every caller having to.

func (*Object) Get added in v0.2.0

func (o *Object) Get(name string) (Value, error)

Get returns the property name of the object as a Value — a primitive carries its data, an object or function comes back as an *Object with identity preserved. A missing property is Undefined; a property whose access throws (a getter) is returned as a *JSError.

func (*Object) Int added in v0.2.0

func (o *Object) Int() int

func (*Object) IsFunction added in v0.2.0

func (o *Object) IsFunction() bool

IsFunction reports whether the object is callable (Call will work).

func (*Object) IsObject added in v0.2.0

func (o *Object) IsObject() bool

func (*Object) IsUndefined added in v0.2.0

func (o *Object) IsUndefined() bool

func (*Object) New added in v0.3.0

func (o *Object) New(args ...Value) (Value, error)

New constructs `new o(args...)` — the Go analogue of syscall/js's Value.New. o must be a constructor (a class or a function); the returned Value is the new instance. A guest throw during construction comes back as a *JSError.

func (*Object) Object added in v0.2.0

func (o *Object) Object() *Object

func (*Object) Set added in v0.2.0

func (o *Object) Set(name string, v Value) error

Set sets the property name of the object to v — a primitive Value or an *Object (which keeps its identity: the guest sees the same object).

func (*Object) String added in v0.2.0

func (o *Object) String() string

String returns the object's JS ToString — o.toString() run guest-side — or "[object Object]" when that is not reachable (a Value cannot surface an error).

type Rejection added in v0.5.0

type Rejection struct {
	Reason  Value
	Promise Value
}

Rejection is one promise rejection that nothing handled: the reason it was rejected with, and the promise itself.

type Result added in v0.2.0

type Result struct {
	// Value is the script's completion value, valid only when Error is nil.
	// (Scripts have a completion value; modules do not — see ModuleResult.)
	// A primitive completion carries its data and type; an object or function
	// completion is an *Object with identity preserved — the same guest object,
	// navigable and callable.
	Value Value
	// Error is non-nil when the script threw, failed to compile, or was
	// interrupted.
	Error error
}

Result is the outcome of running a script with Eval. Error is nil on success; a non-nil Error (a *JSError) means the script threw, failed to compile, or was interrupted.

There is no Stdout/Stderr: the engine is pure ECMA-262 and has no I/O of its own. Guest output is whatever the embedder wires up — define a `console` or `print` with Global().DefineFunc and route it to any io.Writer.

type Value added in v0.2.0

type Value interface {
	String() string
	Float() float64
	Int() int
	Bool() bool
	IsUndefined() bool
	IsObject() bool
	// Object returns the value as an *Object when IsObject, else nil.
	Object() *Object
	// Export returns the default Go representation of the value (JS → Go).
	Export() any
	// contains filtered or unexported methods
}

Value is any JavaScript value — a primitive (number, string, boolean, null, undefined) or an Object. It is an interface precisely because a JS value is a single family spanning all of these: the concrete kinds (primitives and *Object) implement it, and code that holds a Value need not care which it is.

Primitives cross the guest/host boundary as data; objects and functions cross as a handle (an *Object) so they keep their identity and can still be navigated and called. String coerces like JS ToString for primitives; Float, Int and Bool coerce to the Go type (returning the zero value when the Value is not that kind); Export returns the default Go representation.

func Null added in v0.2.0

func Null() Value

Null is the JS null value (distinct from Undefined).

func Undefined added in v0.2.0

func Undefined() Value

Undefined is the Value meaning "no value".

func ValueOf added in v0.2.0

func ValueOf(x any) Value

ValueOf wraps a Go value as a host Value. An existing Value passes through; bool, string and the numeric types become the matching JS primitive; nil becomes undefined. A composite Go value (slice, array, map, struct — anything encoding/json can marshal) materializes guest-side as a FRESH JS Array/Object each time it crosses: it carries data, not identity. When the guest must see the same object across calls, build it once with NewObject/Set instead.

type WritableFS added in v0.5.0

type WritableFS interface {
	fs.FS

	// OpenFile opens name with OS-style flags (os.O_RDONLY, os.O_CREATE,
	// os.O_TRUNC, os.O_APPEND, ...). When the flags request write access, the
	// returned file also implements io.Writer.
	OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error)

	// Mkdir creates a directory. The parent must already exist.
	Mkdir(name string, perm fs.FileMode) error

	// Remove removes a file or an empty directory.
	Remove(name string) error

	// Rename moves oldname to newname, replacing a non-directory target.
	Rename(oldname, newname string) error
}

WritableFS is the interface upgrade a Config.FS may implement to accept writes. Host functions that need to create or modify files assert this interface on Config.FS (the fs.ReadDirFS idiom): when the assertion fails, the filesystem behaves as a read-only mount and writes surface to the guest as permission errors. Reads always go through the plain fs.FS methods.

An implementation is the single point of filesystem access control: it enforces its own policy inside these methods and inside the fs.FS read methods (return fs.ErrPermission to deny, fs.ErrNotExist to hide). A sheena fs.Volume satisfies this interface through a tiny adapter (its OpenFile returns sheena's fs.File, which is an io/fs.File; the rest match by embedding), so a sheena sandbox's Refuse/Hide/Access rules carry straight into the guest.

The memfs package provides an unrestricted in-memory implementation, useful in tests and for isolating instances from the host filesystem entirely.

Directories

Path Synopsis
compat
cfworkers
Package cfworkers runs Cloudflare-Workers-style modules — `export default { fetch(request, env, ctx) }` — on go-spidermonkey behind Go's net/http.
Package cfworkers runs Cloudflare-Workers-style modules — `export default { fetch(request, env, ctx) }` — on go-spidermonkey behind Go's net/http.
internal/eventloop
Package eventloop provides the macro-task loop the compat packages share: host-side timers (setTimeout/setInterval), completions posted from other goroutines (async ops like fetch), and the microtask drain between tasks.
Package eventloop provides the macro-task loop the compat packages share: host-side timers (setTimeout/setInterval), completions posted from other goroutines (async ops like fetch), and the microtask drain between tasks.
nodejs
Package nodejs is the Node.js compatibility layer (docs/nodejs-compat-plan.md Phase 3): the node: core modules (path, events, util, buffer, fs, ...), process with Node's nextTick ordering, Buffer, CommonJS require with node_modules resolution, and ESM⇄CJS interop — installed explicitly:
Package nodejs is the Node.js compatibility layer (docs/nodejs-compat-plan.md Phase 3): the node: core modules (path, events, util, buffer, fs, ...), process with Node's nextTick ordering, Buffer, CommonJS require with node_modules resolution, and ESM⇄CJS interop — installed explicitly:
web
Package web installs the WinterTC (minimum common Web API) vocabulary on a go-spidermonkey interpreter: console, TextEncoder/TextDecoder, atob/btoa, URL/URLSearchParams, AbortController, queueMicrotask, structuredClone, performance.now, crypto.getRandomValues/randomUUID, ReadableStream, fetch, and setTimeout/setInterval.
Package web installs the WinterTC (minimum common Web API) vocabulary on a go-spidermonkey interpreter: console, TextEncoder/TextDecoder, atob/btoa, URL/URLSearchParams, AbortController, queueMicrotask, structuredClone, performance.now, crypto.getRandomValues/randomUUID, ReadableStream, fetch, and setTimeout/setInterval.
Package memfs provides a writable in-memory filesystem implementing spidermonkey.WritableFS.
Package memfs provides a writable in-memory filesystem implementing spidermonkey.WritableFS.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL