Help us shape the future of Go! Take the 2026 Go Developer Survey

quickjs

package module
v0.26.11 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: BSD-3-Clause Imports: 19 Imported by: 0

README

quickjs (liustack fork)

This repository is a fork of two modernc.org modules, merged into one Go module:

Upstream module Upstream repository Here
modernc.org/quickjs (the Go API) https://gitlab.com/cznic/quickjs repository root, package github.com/liustack/quickjs
modernc.org/libquickjs (QuickJS transpiled to Go by ccgo) https://gitlab.com/cznic/libquickjs lib/, package github.com/liustack/quickjs/lib

It starts from quickjs v0.25.0 and libquickjs v0.13.1 (QuickJS release 2026-06-04, commit 3d5e064), with these changes:

  • A VM that runs out of memory inside JSON.parse, or anything else that converts a string with non-ASCII characters to UTF-8, no longer leaks that string. JS_ToCStringLen2 kept a reference to its argument when allocating the UTF-8 copy failed, so the string outlived VM.Close. QuickJS, quickjs-ng and libquickjs still had this bug when the fork was made. Test: lib/tocstring_oom_test.go.

  • VM.Call and VM.CallValue report the exception thrown while evaluating the function expression (for example out of memory, or a name that is not defined) instead of cannot call a non-function.

  • A string that cannot be converted to Go comes back as the error, not as "". Converting needs memory for a string with non-ASCII characters and for one built by concatenation, so near the memory limit Eval, Call, the JSON of an Object, Value.MarshalJSON and host function arguments used to get an empty string, and the out of memory exception stayed in the context. A BigInt whose toString throws now reports that exception instead of a made-up error. ErrorFromValue has no error result: a field it cannot read stays empty, and the exception is dropped. Reading the name and message of a thrown value that is not an object (throw null, which the engine throws near the memory limit, throw undefined, a symbol) no longer leaves a TypeError in the context. Test: gostring_test.go.

  • A Go string made from a JS string is valid UTF-8. A JS string can hold a lone surrogate, half of a surrogate pair without the other half, which code makes when it cuts a string inside an emoji. JS_ToCStringLen2 wrote it as the three bytes of its code point, which is not UTF-8, so a page rendered to Go had bytes a browser shows as three replacement characters. Now each lone surrogate becomes U+FFFD, as Go's utf16.Decode, TextEncoder and Node's Buffer make it. Test: surrogate_test.go.

  • A Go string that is not valid UTF-8 becomes a JS string with U+FFFD for each byte that does not start a valid sequence, as Go decodes it (for range, []rune, utf16.Encode) and as encoding/json writes it, so a string passed as it is and one inside a struct, which goes as JSON, arrive the same. This covers arguments of Call, results of Go functions, map keys and values, NewString, NewAtom, the message of an error a Go function returns, function names and module names. The engine used to skip the bytes after an invalid one, ASCII included: "\x80a" became a single U+FFFD and lost the a, "\xc0\x80" gave one U+FFFD instead of two, and the three-byte form of a surrogate (CESU-8, 0xED 0xA0 and on) became that surrogate, so two of them made an emoji. A sequence cut short, such as "\xe4\xb8", now gives one U+FFFD per byte, where a browser's TextDecoder gives one for the sequence. Source code is not changed: the parser still reports invalid UTF-8 as a SyntaxError, and still takes a surrogate in the three-byte form. Test: invalidutf8_test.go.

  • Page code can no longer catch an interrupt (VM.Interrupt or the eval timeout) through a promise or an iterator. The engine already skipped every catch and finally block for an interrupt, but its own handlers of an exception went on with it: an async function and a promise job turned it into a rejection, which an await, a .catch or a .then handler then received, and so did the Promise constructor, Promise.try, Promise.all and the other combinators, async generators and modules. A built-in that runs an iterator's return method on an error (Array.from, ...) ran it, then threw the interrupt again as an error a catch could take, and any later error replaced it. Now an uncatchable exception stays pending until the Go call that ran the script takes it, and while it is pending every call into javascript fails at once. So no catch, finally, iterator return, generator finally or rejection handler runs, and the call returns the interrupt, as it does for a script without promises. Where a built-in only records the error, such as a resolve function that rejects its promise when reading then throws, the code after it runs on until its next function call, which fails. ExecutePendingJobs stops at the job that failed with an interrupt and leaves the rest queued, where it used to run them all. Tests: uncatchable_test.go, internal/fixlib/uncatchable_test.go.

  • Running out of memory is uncatchable too, like an interrupt, and the call returns *Error{Name: "InternalError", Message: "out of memory"}. Page code used to catch it, so a framework catching every error to show a fallback reported it as an error of the page, and a finally block or a rejection handler ran with no memory left. Every way the engine runs out of memory now ends this way: the engine's own out of memory error, the regexp engine running out of memory (it used to throw InternalError: out of memory in regexp execution, or a SyntaxError when compiling a pattern), and an error the engine could not build. That last case used to throw a catchable null, and an error whose message could not be allocated got the exception marker as its message, which read back as nonsense. Now the engine builds the message first, and if the message, the error object or setting the message fails, it throws null, uncatchable, which the VM reports as the same out of memory error. A script that throws null itself still gets null. Page code cannot fill memory on purpose and catch the failure any more: allocating until it fails ends the call. The VM stays usable after the call, and Close frees everything. Tests: uncatchable_test.go, lib/outofmemory_test.go, internal/fixlib/outofmemory_test.go.

  • An interrupt or running out of memory inside a Go function that javascript called ends the whole call too. That covers converting the function's arguments or result, and javascript the function evaluates on the same VM (Eval, Call and the like), from a function registered with RegisterFunc or RegisterHostFunc, a Then callback, or the module loader and normalizer. The Go function still gets the error, but the exception stays pending until it returns, so further calls into javascript fail at once. Whatever the function returns, the script that called it gets the original uncatchable exception. It used to get a catchable TypeError wrapping the message (for example calling take: argument conversion error: InternalError: out of memory), or the function's own result when it dropped the error, so a timeout in nested javascript could be caught or lost and the script ran on. The intl package now takes its host functions off the global object before its install changes anything, so that an install cut short this way leaves none of them in sight once the next use has installed. Tests: hostuncatchable_test.go, intl/install_test.go.

  • Recursing without end throws RangeError: Maximum call stack size exceeded, worded as in V8, which a script can catch, with the default settings. It used to kill the process with a Go stack overflow: the engine sets a new runtime's stack size to 1 MB, meant as bytes, but the Go port's stack check counts TLS stack slots, so the limit was a million slots, far more than a goroutine's stack holds. A new runtime now starts with the port's own default, lib.MaxStackSlots, as SetMaxStackSize already said, and that default is 10000 slots rather than 1000: a React render of a page of tifa's example shop takes about 470. At the default, recursing without end takes up to 128 MB of goroutine stack before it throws. Running out of stack used to be InternalError: stack overflow. The parser and JSON.parse now report nesting too deep for the stack with the same RangeError, as V8's parser does, rather than SyntaxError: stack overflow. A regular expression nested too deep still fails to compile with SyntaxError: stack overflow. Tests: stackoverflow_test.go, internal/fixlib/stack_test.go.

  • A panic in a Go function that javascript called (RegisterFunc, RegisterHostFunc, a Then callback, the module loader or normalizer) no longer unwinds through the engine. The VM recovers it, ends the javascript with an uncatchable InternalError (panic in Go function <name>: <value>) as for an interrupt, and panics again with the same value in the Go code that called javascript: in Eval or the like, or in the Go function one level up when a Go function called the javascript. Page code sees no catch or finally run for it. The panic used to unwind through the engine, which kept pointing at stack frames that were gone. The VM looked fine after a recover, but the next error it made walked those frames, forever when a new frame took the place of an old one, so the eval timeout of that VM never fired again. The panic value is the same, but its stack trace now starts where the VM panics again. Tests: hostpanic_test.go.

  • Compiling a script, a function body, eval code or a regexp with little memory left fails with the same out of memory error. The engine used to crash there (a Go panic on a write before or past the code buffer, or a double free), report a SyntaxError the script does not have, or, in Compile, return bytecode cut short, bytecode with the out of memory error left pending, or errors.New("OOM"). The parser went on after its code buffer failed to grow and patched code in place at positions it had noted, past what the buffer held (a Go panic on an empty buffer, or a write past the end of a full one), and the passes after it assumed the buffer was whole. Now the parser stops at the next token once the code buffer of the function it parses failed, and checks it before each patch that does not wait for a token, the buffer takes no more bytes once it failed, and each pass that reads it back stops with the out of memory error. A scope the parser fails to push marks the function's code lost too, and its scopes then stay as they are, each later push failing with the out of memory error: the pop after a failed push used to take off the scope around it, until a pop read before the scope array. The pass that resolves jumps frees, when it fails, the jump records of the labels it has not reached yet, which used to leak. When shrinking the line table of a function fails, the compile keeps the table and leaves no error behind: it used to work and leave the out of memory error pending, for the next call to report. Tests: compileoom_test.go, internal/fixlib/compile_test.go.

  • Making an error no longer formats its stack. The engine records the frames (function name, bytecode and position) and formats the text the first time stack is read, with the same result as before. Code that makes many errors and reads few of their stacks, such as the development build of React (one error per JSX element), runs much faster. On darwin/arm64, a server render of a 100-item React component took 11.2 ms at the median with the development build, against 42.1 ms before. The production build of the same component, the production build of a four-route app and a JSON and array benchmark stayed within 1% of before. Top-level code that makes 500 errors per run took 1.8 ms instead of 3.2 ms. stack is an ordinary writable, configurable data property, so the error has the same shape as one whose stack was formatted at once. Once read, it is the string. What is different:

    • While the stack is pending, the value of stack is an internal object that holds the recorded frames. A pending error so carries three extra small allocations: that object, its property list and the frame record (about 150 bytes for a short stack). Assigning, defining or deleting stack frees them like any other value. The internal object belongs to a private class. The C patch makes it a built-in class of every runtime. In Go, its class ID is taken when the package loads, and a runtime registers the class the first time it records a lazy stack, a one-time cost of about a kilobyte.
    • No code sees the internal object. Everything that reads the value of stack formats it first: property reads in scripts and through the C API, Object.getOwnPropertyDescriptor(s) and JS_GetOwnProperty, and redefining a frozen stack with a value (the engine compares the old and the new value). What only looks at keys or attributes leaves it pending: Object.keys, Object.freeze, Object.seal, Object.isFrozen, Object.isSealed, propertyIsEnumerable, __lookupGetter__, __lookupSetter__, and the checks a proxy whose target is the error makes after a has, deleteProperty or ownKeys trap. stack is not enumerable, so spreading, Object.assign, Object.entries and JSON.stringify skip it.
    • Through a proxy, apart from those three checks, stack is formatted as before. A lookup of the attributes through a proxy (Object.keys, Object.freeze or propertyIsEnumerable on the proxy, for example) and the checks a proxy makes after a get, set, defineProperty or getOwnPropertyDescriptor trap read the error's full descriptor, which formats the stack. Results, trap calls and their order are the same as when the stack was formatted at once. The cost is memory: with almost none left, such an operation can fail with out of memory, where the upstream engine had already formatted the stack when the error was made.
    • Reading a property named stack whose value is an object takes the slow path of the interpreter, which is where the stack gets formatted. Other property reads do not change. In a loop of 160,000 reads, o.x took 1% less time than before, o.stack holding a number 5% more, o[k] 1% more, and an inherited o.x 9% less. A read takes about 19 ns there, so no change is more than 2 ns per read.
    • Until its stack is read or the error is freed, an error keeps the bytecode of every function on its stack alive, and with it the realm (global object) each function was compiled in. Most of these stay alive anyway. A function that runs once and makes an error that something keeps is the exception, together with every function it defines: one kept error from a function that defines 2,000 others kept 1.30 MB where 0.30 MB stayed before. An error made in one realm and kept by another keeps the first realm and everything its globals hold: with a 4 MiB ArrayBuffer in the first realm, the runtime used 4.31 MB where 67 KB stayed before, until the stack was read. Keeping only the file name and the line table would need either a copy of the line table for every frame of every error, which adds work and memory to every error made, the case this change is for, or a reference count of its own for the line table, which changes a struct of the engine and so needs a regeneration of lib/.
    • Reading a stack frees its three allocations, but the holes they leave between objects that are still alive are reused only by later allocations of the same sizes. After making 10,000 errors, keeping them and reading every stack, the allocator held 3.60 MB, against 2.06 MB when the stacks were formatted at once.
    • Errors made while top-level code (a script, an eval or a module body) is on the stack are still formatted at once, so that an error a library keeps from when it loaded does not keep that code alive. So are errors that cannot take new properties and errors that already have a stack.
    • If there is no memory left when stack is first read, the read throws the out of memory error, which ends the call, and the stack stays pending, so a later read can give the text. Reading the property descriptor formats the stack the same way and so can throw too. When an error whose stack is still pending is printed (console.log, JS_PrintValue), formatting the stack never throws and never leaves a pending exception: out of memory there prints the error without its stack, and a later read can still give it. This covers the stack only. The rest of the print, such as the message, can run out of memory and leave an exception, as in the upstream engine. Printing an error whose stack is already a string works as it did before.
    • If the text cannot be formatted when a stack is built at once (any allocation of the formatting can fail: growing the text buffer, converting a function or file name to UTF-8, making the final string), stack becomes null and no exception is left, in Go and in the C patch alike. The upstream engine also gives null when the text buffer cannot grow. When converting a function name fails, it instead keeps a half-formatted stack with the name shown as <anonymous> and leaves an exception pending.

    Tests: errorstack_test.go, errorstack_unix_test.go, lib/backtrace_test.go, lib/backtrace_round2_test.go, lib/backtrace_round3_test.go, lib/backtrace_proxy_test.go, internal/fixlib/main_test.go.

  • A script's local time is UTC in every VM, whatever zone the process has. The engine used to take the zone from libc's localtime, which reads TZ. The process shares that libc and that variable with Go's time.Local, its logs and SQLite (through modernc.org/libc), so setting TZ=UTC to make server rendered pages the same on every machine changed all of those too. Now the engine's offset from UTC is always 0 and the process's zone is left alone. Intl, from package intl, defaults to UTC as well. What this changes for scripts:

    • getTimezoneOffset() is 0, and getHours, getDate, getDay and the other local getters give the same values as their getUTC* counterparts. The local setters (setHours, ...) set UTC fields.
    • new Date(2026, 0, 1), Date.parse of a date and time without an offset ('2026-01-01T13:05:00') and the other local forms read the fields as UTC.
    • toString, toDateString and toTimeString print UTC with GMT+0000. The engine's own toLocaleString, toLocaleDateString and toLocaleTimeString print UTC too. Package intl replaces those three, and its versions print UTC unless given a timeZone.

    A script that needs another zone names it to Intl, with package intl registered: d.toLocaleString('zh-CN', {timeZone: 'Asia/Shanghai'}) or new Intl.DateTimeFormat('en-US', {timeZone: 'America/New_York', hour: 'numeric'}).formatToParts(d). Without intl the engine has no way to show another zone. Tests: dateutc_unix_test.go, internal/fixlib/main_test.go.

  • Package intl formats in every locale Chromium has, from CLDR 48.2 (cldr-json 48.2.3), where upstream formats en-US only. It covers Intl.DateTimeFormat, Intl.NumberFormat, Intl.PluralRules and the toLocale* methods, and its output is compared with Chromium 156 on about 60,000 expressions (intl/golden_test.go). Without a locale argument the locale is en-US, and without a timeZone option the zone is UTC: Intl and the toLocale* methods never read the host's settings, and neither does the engine's Date (see above). Register only puts stand-ins on the VM. The first use of Intl or a toLocale* method installs the rest, from bytecode compiled once per process. The package documentation lists what is not implemented and where the output differs from Chromium.

    What it costs, measured on darwin/arm64 against the en-US version: a program that uses intl is 1.7 MB larger (1.3 MB of it is the locale data). Register takes 0.09 ms and 9 KB of the VM's memory, against 0.85 ms and 46 KB before. The first use in a VM then takes 0.15 ms and 42 KB more. A language is decoded into the Go heap the first time it is used, once per process, in about 0.3 ms and 100 to 180 KB. The first use of any language also decodes about 1 MB of shared data. Formatting a date takes 3.3 µs in en-US (4.3 µs before) and 3.7 µs in zh-CN, a number 2.4 µs in en-US (2.4 µs before) and 2.7 µs in de.

  • Package intl tells daylight saving time from standard time as ICU does, whichever form of the tz data Go reads. In the main form, which Linux distributions and Go's time/tzdata ship, Europe/Dublin's summer time is its standard time and its winter time is daylight saving time, an hour behind. Africa/Casablanca, Africa/El_Aaiun, Africa/Windhoek and Europe/Prague (1946 to 1947) have such negative daylight saving time too. ICU builds from the rearguard form, where daylight saving time is always ahead of standard time, and macOS ships that form. Go's IsDST says what the data says, so on Linux the names of these zones came out swapped: Dublin in winter was Irish Standard Time, the name Chromium gives its summer. For these zones and their links the flag now comes from a table of the rearguard form of tzdb 2026d, the release in Chromium 156's ICU (intl/tzdst_table.go, see "Updating the intl locale data"). When Go reads another release and the table has another offset for a time, or the time is past 2037, the flag is the one of the zone's last period with the same offset. Tests: intl/tzdst_test.go.

  • The eval timeout (SetEvalTimeout) runs on the monotonic clock. It used to run on the wall clock, so setting the system clock while a script ran (NTP, a person, a machine waking from sleep) moved the deadline: set back, the timeout fired that much later or never, and set forward, at once. Even without a jump the two clocks drift apart by a few microseconds per 100 ms, so a server that keeps its own deadline on Go's monotonic clock could see the engine interrupt before that deadline. A timeout too long for the clock, up to math.MaxInt64, never fires: the deadline used to wrap around to the past and interrupt the call at once. Test: clock_test.go.

  • VM.Interrupt can be called from any goroutine while the VM closes and after Close, where it does nothing. It used to read the address of the interrupt flag, which Close frees and then clears, so a watchdog firing just as the script finished could write to freed memory or panic on a nil pointer. The race detector also reported a race when Interrupt stopped a script and the VM was closed after the call returned. That one was not real: the two goroutines are ordered by the atomic flag, but the flag is in C memory, outside Go's heap, where the race detector ignores atomic operations. Interrupt and Close now meet at a mutex in Go memory that Close never clears. Test: interrupt_test.go.

  • *Error says whether the call ended with an exception no script could catch, and why, so that an embedder need not compare names and messages, which a script can copy. Uncatchable is true when the VM ran out of memory (OutOfMemory, also when the engine could not build the error and threw null), when Interrupt or the eval timeout stopped the script (Interrupted), and when a Go function that javascript called panicked (neither of the two). The same holds for the error a Go function gets from javascript it evaluates. What a script throws is never uncatchable, even new InternalError('interrupted') or {name: 'InternalError', message: 'out of memory'}, which used to look exactly like the real thing. Taking an uncatchable error runs no page code: Name is InternalError, Message and Stack are the error's own properties, and Error() is InternalError: <message>. It used to call an Error.prototype.toString or a name getter that page code installed, which could rewrite the message. An interrupt that runs out of memory building its error counts as OutOfMemory. Tests: uncatchable_test.go, hostuncatchable_test.go, hostpanic_test.go.

  • RegisterFunc and RegisterHostFunc no longer abort the process when the global object already has the name, for example after globalThis.g = 1, or for Array or undefined. They installed the function with JS_SetPropertyFunctionList, which is meant for new objects and aborts when the property exists. Now the function replaces a configurable property, as Object.defineProperty would, and stays writable, configurable and not enumerable. A property that is not configurable (undefined, NaN, Infinity, a script's var or function) is kept, and so is a global object that is not extensible: the call returns an error. A name is now taken once per VM as javascript sees it, after invalid UTF-8 is replaced, so "\xc0\x80" after "\xe4\xb8" (both two U+FFFD) is func already registered rather than an abort, and a name with a NUL byte, which ended the name, is an error. A name in brackets is a string too: the function lists read "[Symbol.iterator]" as that symbol and aborted on other names in brackets. Test: register_test.go.

The engine lives in the same module so that a fix to it ships together with the Go API, without waiting for an upstream release of either module.

go get github.com/liustack/quickjs

The license files of both projects are kept: LICENSE and LICENSE-LIBQUICKJS at the root, and lib/LICENSE, lib/LICENSE-LIBQUICKJS and lib/LICENSE_TEST262 for the engine. intl/LICENSE-DATA holds the notices of the locale data intl embeds, from Unicode CLDR and Chromium's ICU data: a program that imports intl distributes that data and has to include those notices in its documentation or with its copies. Maintenance notes are at the end of this file.

The rest of this section is the upstream README.


Go Reference LiberaPay receives patrons

logo_png

quickjs

Package quickjs is a pure Go embeddable Javascript engine. It supports the ECMA script 14 (ES2023) specification including modules, asynchronous generators, proxies and BigInt.

See also the original C QuickJS library.

The engine has no Intl: ECMA-402 is not implemented by the C library this package wraps. Package quickjs/intl adds Intl.DateTimeFormat, Intl.NumberFormat and Intl.PluralRules, in every locale Chromium has and over Go's time zone database, for a VM that opts in by calling its Register — once per VM, including one created to replace another, because Intl lives on that VM's global object.

Resolving IANA time zone names needs a zoneinfo database. Go reads the operating system's, or the copy time/tzdata embeds in the binary. That import belongs in a program's main package rather than in a library, so neither package makes it for you.

Maintaining the fork

Branches: main carries the fork. master and the other upstream branches mirror gitlab.com/cznic/quickjs and get no commits of their own.

Remotes, set up once in a fresh clone. The engine remotes must not fetch tags, because libquickjs tags (v0.13.1, ...) would clash with the tags of this repository:

git remote add upstream https://gitlab.com/cznic/quickjs.git
git remote add upstream-lib https://gitlab.com/cznic/libquickjs.git
git config remote.upstream-lib.tagOpt --no-tags
git fetch upstream
git fetch upstream-lib 'refs/tags/*:refs/tags/lib/*'   # engine tags as lib/v0.13.2, ...
Taking a new engine release

lib/ was added with git subtree, so an upstream libquickjs release merges in with its history:

git subtree pull --prefix lib upstream-lib <libquickjs tag, e.g. v0.13.2>

Conflicts to expect:

  • lib/ccgo_*.go: every upstream release regenerates these files, so the two lines added to XJS_ToCStringLen2, the calls into lib/backtrace.go (lazy error stacks), the getTimezoneOffset that answers 0 (local time is UTC) and the calls into lib/uncatchable.go (uncatchable exceptions) and lib/outofmemory.go (out of memory), the compiler's out of memory checks, and the stack limit of a new runtime conflict. Take the upstream version of every generated file, then put the changes back:

    git checkout --theirs -- 'lib/ccgo_*.go' 'lib/internal/run-test262/ccgo_*.go'
    go run ./internal/fixlib
    

    fixlib also rewrites modernc.org/libquickjs imports in hand-written files under lib/. go run ./internal/fixlib -check lists files that still need changes. If fixlib reports that XJS_ToCStringLen2 changed shape, read the new upstream code: QuickJS may have fixed the leak itself. If it reports that _getTimezoneOffset changed shape, or that the engine calls libc's local time functions (localtime, mktime, tzset, ...) somewhere else, upstream reads the local zone some new way: make that answer UTC too and teach fixTimezone about it. If it reports that _build_backtrace, _get_prop_string, JS_GetPropertyInternal, JS_GetOwnPropertyInternal, the interpreter's get_field, js_print_error, js_print_object or JS_ComputeMemoryUsage changed shape, compare the new upstream build_backtrace with lib/backtrace.go, which formats the same text from the recorded frames and must change with it. If it reports that XJS_Throw, _JS_CallInternal or _JS_IteratorClose changed shape, put the uncatchable check of lib/uncatchable.go back where the C change has it. If it reports that XJS_ThrowOutOfMemory, _JS_ThrowError2, the regexp functions or the string table changed shape, compare the new upstream code with lib/outofmemory.go: _js_throw_error2 replaces the end of JS_ThrowError2 and must build the same error. If it reports that Xdbuf_claim, _push_scope, _pop_scope, _js_create_function, _resolve_variables, _resolve_labels, the loops whose code the parser moves, _js_parse_error_v, _next_token, _js_parse_class, _js_parse_destructuring_element, the bytecode writer or Xlre_compile changed shape, read the new upstream code against the C change: the parser must stop once a code buffer failed and patch no code in place after that, and every pass that reads back a code buffer must stop when that buffer failed (internal/fixlib/compile.go lists each place). If it reports that XJS_NewRuntime2 or _JS_ThrowStackOverflow changed shape, a new runtime must still start with a stack size of 0 (the port's MaxStackSlots), and running out of stack must still throw a RangeError (internal/fixlib/stack.go). The same holds when it reports that the stack check of _next_token or _json_next_token changed shape: the tokenizers must throw that RangeError too. If it reports a call of XJS_GetException that is not in exceptionHandlers or exceptionReaders (internal/fixlib/uncatchable.go), read the new caller: if it hands the exception to a script (a rejection, a handler call, a new throw), add it to exceptionHandlers, so that it gets _js_get_exception. fixlib also audits how the engine uses property slots (the JSProperty that holds a value), because a pending stack must be formatted before anything hands the value on. It parses each generated function and follows slot pointers from where they are made (an object's property array, the slot find_own_property returns) through variables (=, := and var), stack cells, pointer arithmetic, call arguments and return values, whatever their names. A slot pointer that goes anywhere else (stored into a structure, in an assignment of several results, arithmetic on a stack cell other than a step to the next cell) is listed as a use there. For every function they reach it lists each use: reads and writes of the value or of another field, calls the pointer is passed to, stores, and which uses come before the function's first call into lib/backtrace.go. It also counts each function's descriptor lookups. internal/fixlib/slotsites.go holds the expected list with the reason each function is safe. When a release adds a function that uses slots, or changes how one does, fixlib stops and prints the difference: check whether the change can see a pending stack, wire it to lib/backtrace.go if so, and update the list (go run ./internal/fixlib -slots prints it). It also requires every property lookup in the interpreter loop (JS_CallInternal) to be a known guarded read or the put_field write. What the audit does not see: a slot pointer stored into a structure is listed as a use where it is stored, but not followed to where it is loaded again. Indirect calls are not followed. A function that no one passes a slot pointer to is not audited, so a new helper only shows up once something calls it with one. The allocator functions that resize or free the property array are listed as uses but not looked into. The order check only compares positions in the source with the first guard of the function. And slot addresses made some other way than from the property array, such as a raw offset into the object, are not recognized. So a change in these places needs a review by hand.

  • lib/internal/patch/quickjs.diff: holds the same changes in C, so that a regeneration keeps them. Keep the JS_ToCStringLen2 hunk unless upstream QuickJS has fixed the bug, the getTimezoneOffset hunk, and the lazy stack hunks (JSBacktrace, the built-in JS_CLASS_BACKTRACE class, js_backtrace_build and the js_backtrace_* calls in JS_GetPropertyInternal, JS_GetOwnPropertyInternal, JS_DefineProperty, the interpreter's GET_FIELD_INLINE, get_prop_string, js_print_error, js_print_object, JS_ComputeMemoryUsage, the callers that only need the attributes of a property (js_get_own_property_flags)), and the uncatchable exception hunks (js_uncatchable_pending, js_get_exception and its callers, JS_IsUncatchableException, and the checks in JS_Throw, JS_CallInternal and JS_IteratorClose), and the out of memory hunks (JS_ThrowOutOfMemory, JS_ThrowError2, the regexp callers of JS_ThrowOutOfMemory), and the compiler hunks (dbuf_claim in cutils.c, push_scope, pop_scope, js_create_function, resolve_variables, resolve_labels, the two code moves of the parser, js_parse_error_v, next_token, the two patches in js_parse_class, the test removal in js_parse_destructuring_element, JS_WriteObjectAtoms, JS_WriteObject2 and the error label of lre_compile in libregexp.c), and the stack hunks (the stack size JS_NewRuntime2 sets, and JS_ThrowStackOverflow). After a regeneration that includes them, lib/backtrace.go, lib/uncatchable.go, lib/outofmemory.go and their wiring in fixlib are no longer needed.

  • lib/go.mod, lib/go.sum: they do not exist here. When upstream changed them, delete them again (git rm lib/go.mod lib/go.sum) and bring any newer requirement into the root go.mod.

  • lib/internal/run-test262/go.mod is ours. Upstream has none there.

  • lib/internal/run-test262/ccgo_*.go: ccgo links a whole copy of the engine into the test262 runner, so as generated the runner tests that copy and not lib/. fixlib turns each engine API function of the runner into a call of the same function in lib/ (lib/test262_runner.go exports the one ccgo leaves unexported). Take the upstream files and run fixlib, as for lib/ccgo_*.go.

This fork does not regenerate lib/ccgo_*.go itself. lib/generator.go still assumes it is the root of its own module.

Taking a new release of the Go API
git merge <upstream tag>

Upstream commits import modernc.org/libquickjs and modernc.org/quickjs. After the merge, drop modernc.org/libquickjs from go.mod, run grep -rn 'modernc.org/quickjs\|modernc.org/libquickjs' --include='*.go' . and switch what it finds to github.com/liustack/quickjs. The only expected leftover is the Symbol key "modernc.org/quickjs/intl" in intl/stub.js, which is a runtime marker, not an import path.

intl/ has been rewritten here, so an upstream change to it conflicts. Keep this fork's version and bring over only what the upstream change fixes, checked with the golden test below.

Updating the intl locale data

intl/cldr.bin is generated from cldr-json and intl/tzdst_table.go from tzdb, and both are checked in, so neither a build nor go test needs the network or Node. To generate them again:

go run ./intl/internal/gen -fetch    # downloads what is missing into a cache directory
go test ./intl                       # includes the golden test against Chromium

The tzdb step needs make, awk and zic (-zic names another zic). It unpacks the release in tzdbVersion in intl/internal/gen/tzdb.go, after checking its SHA-256 against tzdbSHA256, makes main.zi and rearguard.zi with the release's Makefile and compiles both. The table lists the zones and links whose daylight saving flag differs between the two forms before 2038, with their periods in the rearguard form, which is what ICU builds from. The generator also copies a few zones in both forms to intl/testdata/tzdata for the tests. To move to a new release, take the one in the ICU data of the Chromium the golden test records with: the TZVersion of zoneinfo64.res in its icudtl.dat (see below for where to get that file). Set tzdbVersion and tzdbSHA256 (the SHA-256 of tzdata<version>.tar.gz from https://data.iana.org/time-zones/releases/, whose signature is the .asc next to it) and run the two commands.

To move to a new CLDR release, set cldrTag in intl/internal/gen/main.go to the cldr-json tag and run the two commands. The golden test then shows every expression whose output changed. Where the new CLDR changed a value Chromium does not have yet, add the value Chromium has to browserOverrides in intl/internal/gen/overrides.go, with where it comes from. The generator stops on an override CLDR has caught up with, so the list only ever holds live differences.

To compare with a newer Chromium, record its answers again. This needs Node and a Chromium that Playwright drives. Set it up once:

npm install -g playwright@1.64.0                # Chromium 156.0.8078.4, as recorded here
npx playwright@1.64.0 install chromium
export PLAYWRIGHT="$(npm root -g)/playwright"   # what record.cjs loads

Each Playwright release drives one Chromium build, so a newer Chromium needs a newer Playwright: its browsers.json names the build.

Then, from the repository root:

CLDR="$HOME/cldr-json"                                         # any directory for the downloads
go run ./intl/internal/gen -fetch -cldr "$CLDR"                # the list of CLDR locales
node intl/testdata/chromium/record.cjs availability "$CLDR"    # the locales Chromium has
go run ./intl/internal/gen -icudtl "$(node intl/testdata/chromium/record.cjs icudtl)"
go run ./intl/internal/gen -fetch -cldr "$CLDR"                # data for those locales
go test ./intl -run TestGolden -args -golden.corpus            # writes corpus.json
node intl/testdata/chromium/record.cjs golden                  # writes golden.json.gz
go test ./intl
rm -rf "$CLDR"

record.cjs icudtl prints where the browser keeps icudtl.dat, its ICU data file. The same file, byte for byte, is common/icudtl.dat in Chromium's copy of ICU, at the commit Chromium's DEPS pins for src/third_party/icu (5aa52620 for 156.0.8078.4), so summarizing it needs no browser:

curl -s 'https://chromium.googlesource.com/chromium/deps/icu/+/5aa526207171ecadf31597f0980a7b7ad8872f33/common/icudtl.dat?format=TEXT' |
	base64 -d > icudtl.dat
go run ./intl/internal/gen -icudtl icudtl.dat

The generator writes a summary of it to intl/testdata/chromium/icu.json.gz and makes every locale take each resource from where Chromium's ICU finds it, so a language whose data Chromium leaves out formats with root's or English data as it does in Chromium. To record another browser, set CHROMIUM to its executable for every record.cjs command.

Update the Chromium and ICU versions in the package documentation and here, and look at each new difference: fix the code when Chromium is right, or add the cause to goldenDifferences when the difference is in the data.

Checks
go test -short ./...                                     # all packages
go test -vet=off -run 'TestToCString' ./lib              # the leak reproduction
go test -run 'TestErrorStack' .                          # stack text and memory of lazy error stacks
go test -vet=off -run 'TestBacktrace' ./lib              # shapes, retries and writes of pending stacks
go run ./internal/fixlib -check                          # fork changes present in lib/
go run ./intl/internal/gen -fetch -check                 # intl data is what the generator makes
go test ./internal/fixlib                                # fixlib notices new reads of property values
for t in linux/amd64 linux/arm64 darwin/arm64 windows/amd64; do
	GOOS=${t%/*} GOARCH=${t#*/} go build ./...
done

lib/internal/run-test262 is a module of its own, so its 69 MiB of transpiled test runner stay out of the module zip. Test262 in lib/ builds it with go build -C. The runner runs the engine in lib/ (see fixlib), so it checks the changes made there.

Documentation

Overview

Package quickjs is a pure Go embeddable Javascript engine. It supports the ECMA script 14 (ES2023) specification including modules, asynchronous generators, proxies and BigInt.

See also the original C QuickJS library.

Internationalization

The engine has no Intl. ECMA-402 is not implemented by the C library this package wraps, so a page or bundle reaching for Intl.DateTimeFormat gets a ReferenceError, and the toLocale* methods it ships ignore both their locale and their timeZone argument. Package github.com/liustack/quickjs/intl provides Intl.DateTimeFormat and Intl.NumberFormat, en-US over Go's time zone database, for a VM that opts in by calling its Register.

Performance

Geomeans of time/op over a set of benchmarks, relative to CCGO, lower number is better. Detailed results available in the testdata/benchmarks directory.

 CCGO: github.com/liustack/quickjs/lib
 GOJA: github.com/dop251/goja@v0.0.0-20260723142020-b4aef50fa347
  QJS: github.com/fastschema/qjs@v0.0.6

	                        CCGO     GOJA     QJS
	-----------------------------------------------
	        darwin/amd64    1.000    1.575    1.300
	        darwin/arm64    1.000    1.069    1.080
	       freebsd/amd64    1.000    1.943    1.472    (qemu)
	       freebsd/arm64    1.000    1.803    1.030    (qemu)
	           linux/386    1.000    3.082   95.312    (qemu)
	         linux/amd64    1.000    1.419    1.213
	           linux/arm    1.000    3.859  139.339
	         linux/arm64    1.000    1.611    1.267
	       linux/loong64    1.000    1.555   85.539
	       linux/ppc64le    1.000    2.286   75.183
	       linux/riscv64    1.000    1.656   76.609
	         linux/s390x    1.000    2.016   68.871    (qemu)
	       windows/amd64    1.000    2.254    1.438
	       windows/arm64    1.000    2.002    1.610
	-----------------------------------------------
	                        CCGO     GOJA     QJS

Notes

Parts of the documentation were copied from the quickjs documentation, see LICENSE-QUICKJS for details.

Supported platforms and architectures

These combinations of GOOS and GOARCH are currently supported

OS      Arch
-------------
darwin  amd64
darwin  arm64
freebsd amd64
freebsd arm64
linux   386
linux   amd64
linux   arm
linux   arm64
linux   loong64
linux   ppc64le
linux   riscv64
linux   s390x
windows amd64
windows arm64

Builders

Builder results are available here.

Change Log

2026-07-27: Upgrade to QuickJS release 2026-06-04. The bytecode format changed, code produced by VM.Compile using an older release will no longer load.

2025-10-10: Upgrade to QuickJS release 2025-09-13.

Example (Ping)

Multiple concurrent Javascript virtual machines communicating via Go channels.

package main // import "github.com/liustack/quickjs"

import (
	"fmt"
)

// Multiple concurrent Javascript virtual machines communicating via Go channels.
func main() {
	tx := make(chan string, 1)
	rx := make(chan string, 1)
	client, _ := NewVM()
	defer client.Close()
	registerFuncs(client, tx, rx)
	go func() { // Start the server.
		server, _ := NewVM()
		defer server.Close()
		registerFuncs(server, rx, tx)
		server.Eval("send(receive()+' reply');", EvalGlobal)
	}()
	fmt.Println(client.Eval("send('ping'); receive();", EvalGlobal)) // Ping the server.
}

func registerFuncs(m *VM, tx, rx chan string) {
	m.RegisterFunc("send", func(s string) { tx <- s }, false)
	m.RegisterFunc("receive", func() string { return <-rx }, false)
}
Output:
ping reply <nil>

Index

Examples

Constants

View Source
const (
	EvalGlobal = lib.MJS_EVAL_TYPE_GLOBAL // global code
	EvalModule = lib.MJS_EVAL_TYPE_MODULE // module code
)

Eval flags.

Variables

View Source
var (

	// UndefinedValue is a Value representing Javascript value 'undefined'. It is
	// not associated with any particular VM.
	UndefinedValue = Value{/* contains filtered or unexported fields */}
)

Functions

func Version

func Version() string

Version returns the underlying QuickJS version.

Types

type Atom

type Atom = lib.TJSAtom

Atom is an unique identifier of, for example, a string value. Atom values are VM-specific.

type Error

type Error struct {
	Name         string // e.g. "ReferenceError", "TypeError"
	Message      string // the error message
	Stack        string // stack trace, one "    at ...\n" frame per line
	LineNumber   int    // line where the error occurred (parse errors only)
	ColumnNumber int    // column where the error occurred (parse errors only)
	FileName     string // file name where the error occurred (parse errors only)

	// Uncatchable reports that the call ended with an exception no catch
	// or finally block of the script ran for: the VM ran out of memory
	// (OutOfMemory), Interrupt or the eval timeout stopped it
	// (Interrupted), or a Go function that javascript called panicked
	// (neither of the two). Whatever a script throws itself is never
	// uncatchable, whatever its name and message.
	Uncatchable bool
	OutOfMemory bool // the VM ran out of memory (Uncatchable is true)
	Interrupted bool // Interrupt or the eval timeout stopped the script (Uncatchable is true)
	// contains filtered or unexported fields
}

Error holds structured information from a JavaScript exception. The error returned by VM.Eval, VM.Call and the other eval-family methods is an *Error; recover the fields with errors.As:

var e *quickjs.Error
if errors.As(err, &e) {
	fmt.Println(e.Stack)
}

Its Error method reports the same message string the engine has always returned, so existing code comparing the error text keeps working.

Only parse/syntax errors carry the FileName, LineNumber and ColumnNumber properties. For runtime errors those fields are zero and the source location is available only from Stack.

Uncatchable, OutOfMemory and Interrupted tell an error the engine ended the call with from one the script threw, without comparing names or messages, which a script can copy:

if errors.As(err, &e) && e.OutOfMemory {
	// the script needs a higher SetMemoryLimit
}

func ErrorFromValue

func ErrorFromValue(v Value) *Error

ErrorFromValue extracts structured error information from JavaScript error value 'v', typically an Error instance caught from a JS exception.

'v' is borrowed: ErrorFromValue does not free it and the caller remains responsible for its lifetime. Like all VM operations it must be called from the VM's own goroutine. See Error for which fields are populated.

A field that cannot be read is left empty: the string form of 'v' (the result of Error) when its toString throws, a property whose getter throws, and any string field when there is no memory for its UTF-8 copy. The exception that reading throws is dropped, so ErrorFromValue leaves no exception pending.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface. It returns the same string the engine produces for the exception, unchanged from earlier releases.

type HostFunc

type HostFunc func(args []any) (any, error)

HostFunc is a Go function exposed to JavaScript by RegisterHostFunc. It receives the call arguments already converted to Go values and returns a single value to convert back to JS, or a non-nil error which is thrown as a JavaScript exception.

type ModuleLoaderFunc

type ModuleLoaderFunc func(vm *VM, moduleName string) (string, error)

ModuleLoaderFunc defines the signature for a custom module loader. It must return the module's source code (Javascript).

type ModuleNormalizeFunc

type ModuleNormalizeFunc func(vm *VM, moduleBaseName, moduleName string) (string, error)

ModuleNormalizeFunc defines the signature for a custom module normalizer. It receives the base name (the module making the import) and the requested name, and should return the normalized, absolute module name.

type Object

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

Object represents the value of a Javascript object, but not the javascript object instance itself. Do not compare instances of Object.

func (*Object) Into

func (o *Object) Into(v any) error

Into helps you quickly deserialize an Object into a native Go type. This uses encoding/json.Unmarshal internally, so the target type must be compatible with that package.

Example

JSON unmarshalling into native Go struct.

m, _ := NewVM()
defer m.Close()
obj, _ := m.Eval("obj = {a: 42+314, b: 'foo'}; obj;", EvalGlobal)
var dst struct {
	A int    `json:"a"`
	B string `json:"b"`
}
obj.(*Object).Into(&dst)
fmt.Printf("%#v\n", dst)
Output:
struct { A int "json:\"a\""; B string "json:\"b\"" }{A:356, B:"foo"}

func (*Object) MarshalJSON

func (o *Object) MarshalJSON() (r []byte, err error)

MarshalJSON implements encoding/json.Marshaler.

Example

JSON marshalling.

m, _ := NewVM()
defer m.Close()
obj, _ := m.Eval("obj = {a: 42+314, b: 'foo'}; obj;", EvalGlobal)
s, _ := (obj.(*Object).MarshalJSON())
fmt.Printf("%s\n", s)
Output:
{"a":356,"b":"foo"}

func (*Object) String

func (o *Object) String() string

String implements fmt.Stringer.

Example

JSON marshalling.

m, _ := NewVM()
defer m.Close()
obj, _ := m.Eval("obj = {a: 42+314, b: 'foo'}; obj;", EvalGlobal)
fmt.Printf("%s\n", obj)
Output:
{"a":356,"b":"foo"}

type PromiseCapability

type PromiseCapability struct {
	Promise Value
	Resolve Value
	Reject  Value
}

PromiseCapability holds a Promise along with its resolve and reject functions, obtained without evaluating JS source.

func (*PromiseCapability) Dup

Dup duplicates the capability — its promise and both resolving functions — returning a new PromiseCapability that must itself be released via Free.

func (*PromiseCapability) Free

func (pc *PromiseCapability) Free()

Free releases all values held by the capability.

type Undefined

type Undefined struct{}

Undefined represents the Javascript value "undefined".

func (Undefined) String

func (u Undefined) String() string

String implements fmt.Stringer.

type Unsupported

type Unsupported struct{}

Unsupported represents an unsupported Javascript value.

func (Unsupported) String

func (u Unsupported) String() string

String implements fmt.Stringer.

type VM

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

VM represents a Javascript context (or Realm). Each VM has its own global objects and system objects.

Note: VM is not safe for concurrent use by multiple goroutines, except for VM.Interrupt.

func NewVM

func NewVM() (*VM, error)

NewVM returns a newly created VM.

func (*VM) Call

func (m *VM) Call(function string, args ...any) (r any, err error)

Call evaluates 'function(args...)' and returns the resulting (value, error).

Argument types must be one of:

Go argument type                        Javascript argument type
----------------------------------------------------------------
nil                                     null
Undefined                               undefined
string                                  string
int*/uint* (value in int32 range)       int
int*/uint* (value out of int32 range)   float
bool                                    bool
float64                                 float64
*math/big.Int                           BigInt
*Object                                 object
Value                                   native Javascript Value
any other type                          object from JSON produced by encoding.json/Marshall(arg)
Example (Function)

Call a Javascript function.

m, _ := NewVM()
defer m.Close()
fmt.Println(m.Call("parseInt", "1234"))
Output:
1234 <nil>
Example (Method)

Call a Javascript method.

m, _ := NewVM()
defer m.Close()
fmt.Println(m.Call("Math.abs", -1234))
Output:
1234 <nil>

func (*VM) CallValue

func (m *VM) CallValue(function string, args ...any) (r Value, err error)

CallValue is like Call but returns (Value, error) like EvalValue

Note: See the Value documentation for details about manual memory management of Value objects.

func (*VM) Close

func (m *VM) Close() error

Close releases the resources held by 'm'. That includes the module loader and normalizer set by VM.SetModuleLoader: Close removes them, so a closed VM keeps nothing they close over reachable.

func (*VM) Compile

func (m *VM) Compile(javascript string, flags int) ([]byte, error)

Compile compiles the bytecode representation of the passed script.

The returned bytecode can be later executed using EvalBytecode or EvalBytecodeValue.

Note: The bytecode format is linked to a given QuickJS version.

func (*VM) Eval

func (m *VM) Eval(javascript string, flags int) (r any, err error)

Eval evaluates a script or module source in 'javascript'.

Javascript result type  Go result type                          Go result error
-------------------------------------------------------------------------------
exception               nil                                     non-nil
null                    nil                                     nil
undefined               Undefined                               nil
string                  string                                  nil
int                     int                                     nil
bool                    bool                                    nil
float64                 float64                                 nil
BigInt                  *math/big.Int                           nil
object                  *Object                                 nil
any other type          Unsupported                             nil
Example (Exception)

Getting exception.

m, _ := NewVM()
defer m.Close()
fmt.Println(m.Eval("throw new Error('failed');", EvalGlobal))
Output:
<nil> Error: failed
Example (Expression)

Evaluate a simple Javascript expression.

m, _ := NewVM()
defer m.Close()
fmt.Println(m.Eval("1+2", EvalGlobal))
Output:
3 <nil>
Example (Object)

Object example.

m, _ := NewVM()
defer m.Close()
fmt.Println(m.Eval("obj = {a: 42+314, b: 'foo'}; obj;", EvalGlobal))
Output:
{"a":356,"b":"foo"} <nil>

func (*VM) EvalBytecode

func (m *VM) EvalBytecode(bytecode []byte) (r any, err error)

EvalBytecode is like Eval but uses QuickJS bytecode.

Note: The bytecode format is linked to a given QuickJS version. Moreover, no security check is done before its execution. Hence the bytecode should not be loaded from untrusted sources.

Note: Javascript 'this' is always set to the global context.

func (*VM) EvalBytecodeValue

func (m *VM) EvalBytecodeValue(bytecode []byte) (r Value, err error)

EvalBytecodeValue is like EvalValue but operates on QuickJS bytecode.

Exceptions thrown during evaluation of the script are returned as Go errors.

If no error is returned, the caller must properly handle the returned Value using Dup/Free.

Note: See the Value documentation for details about manual memory management of Value objects.

func (*VM) EvalThis

func (m *VM) EvalThis(obj Value, javascript string, flags int) (r any, err error)

EvalThis is like eval but sets javascript 'this' to 'obj'.

func (*VM) EvalThisValue

func (m *VM) EvalThisValue(obj Value, javascript string, flags int) (r Value, err error)

EvalThisValue is like EvalValue but sets javascript 'this' to obj.

func (*VM) EvalValue

func (m *VM) EvalValue(javascript string, flags int) (r Value, err error)

EvalValue evaluates a script or module source in 'javascript' and returns the resulting Value, or an error, if any.

Exceptions thrown during evaluation of the script are returned as Go errors.

If no error is returned, the caller must properly handle the returned Value using Dup/Free.

Note: See the Value documentation for details about manual memory management of Value objects.

func (*VM) ExecutePendingJobs

func (m *VM) ExecutePendingJobs() (n int, err error)

ExecutePendingJobs runs queued Javascript jobs — Promise reactions (the .then/.catch/.finally callbacks) and async/await continuations — until the queue is empty, returning the number of jobs executed. The runtime does not run these itself: an embedding event loop calls ExecutePendingJobs at the end of each turn to drain the microtask queue (so a resolved Promise's .then runs, a pending await resumes, etc.). An exception thrown by a job is returned as an error — the first one — while the remaining jobs still run; a self-feeding job queue is bounded so it cannot hang the host.

func (*VM) GetProperty

func (m *VM) GetProperty(this Value, prop Atom) (r any, err error)

GetProperty returns this.prop.

func (*VM) GetPropertyValue

func (m *VM) GetPropertyValue(this Value, prop Atom) (r Value, err error)

GetPropertyValue returns this.prop.

Note: See the Value documentation for details about manual memory management of Value objects.

func (*VM) GlobalObject

func (m *VM) GlobalObject() (r Value)

GlobalObject returns m's global object.

Note: See the Value documentation for details about manual memory management of Value objects.

func (*VM) Interrupt

func (m *VM) Interrupt()

Interrupt requests 'm' to interrupt Javascript evaluation. Interrupt can be called asynchronously, from any goroutine, at any time m is evaluating a script. The request stays pending until the outermost call into javascript returns, so javascript a host function evaluates on m meanwhile does not clear it. Interrupt is also safe while m closes and after Close, where it does nothing, so a watchdog need not know whether the script it guards has finished.

Example
const timeout = time.Second
m, _ := NewVM()
defer m.Close()

go func() {
	time.Sleep(timeout)
	m.Interrupt()
}()

t0 := time.Now()
r, err := m.Eval(`
function f() {
	var sink;
	for (var i = 0; i < 10000; i++) {
		sink += 42;
		sink -= 42;
	}
}

(function() {
	for (var i = 0; i < 10000; i++) {
		f();
	}
	return 42;
})();
`, EvalGlobal)
d := time.Since(t0)
step := timeout / 5
d = d / step * step
fmt.Println(r, err, d)
Output:
<nil> InternalError: interrupted 1s

func (*VM) NewAtom

func (m *VM) NewAtom(s string) (r Atom, err error)

NewAtom returns an unique indentifier of 's' or an error, if any.

func (*VM) NewFloat64

func (m *VM) NewFloat64(n float64) Value

NewFloat64 returns a new Value from 'n'.

Note: See the Value documentation for details about manual memory management of Value objects.

func (*VM) NewInt

func (m *VM) NewInt(n int) Value

NewInt returns a new Value from 'n'.

Note: See the Value documentation for details about manual memory management of Value objects.

func (*VM) NewObjectValue

func (m *VM) NewObjectValue() (r Value, err error)

NewObjectValue returns a new Value representing '{}'.

func (*VM) NewPromiseCapability

func (m *VM) NewPromiseCapability() (*PromiseCapability, error)

NewPromiseCapability creates a new JavaScript Promise along with its resolve and reject functions.

The returned PromiseCapability must be freed via Free when no longer needed.

To expose a promise to JS from a Go function registered with RegisterFunc, return pc.Promise from the Go function. The resolve/reject values captured by the closure need no Dup because the C API already holds a reference for each:

m.RegisterFunc("delayed", func(delay int) Value {
    pc, err := m.NewPromiseCapability()
    if err != nil { panic(err) }
    youreventloop.Schedule(func() {
        time.Sleep(time.Duration(delay) * time.Millisecond)
        pc.Resolve.Call(UndefinedValue, "done")
        pc.Resolve.Free()
        pc.Reject.Free()
        // pc.Promise.Free() // <- only if returned pc.Promise.Dup()!
    })
    return pc.Promise
}, false)

func (*VM) NewString

func (m *VM) NewString(s string) (r Value, err error)

NewString returns a new Value from 's'.

Note: See the Value documentation for details about manual memory management of Value objects.

func (*VM) RegisterFunc

func (m *VM) RegisterFunc(name string, f any, wantThis bool) error

RegisterFunc registers a Go function 'f' and makes it callable from Javascript.

The 'f' argument can be a regular Go function, a closure, a method expression or a method value. All of them are called 'Go function' below.

The Go function can have zero or more parameters. If 'wantThis' is true then the first parameter of the Go function will get the Javascript value of 'this'. Depending on context, 'this' can be Javascript null or undefined.

Go functions with multiple results return them as an Javascript array.

Go nil errors are converted to Javascript null.

Go non-nil errors are converted to Javascript strings using the Error() method.

Any Go <-> Javascript failing type conversion between arguments/return values throws a Javascript type error exception.

There is a limit on the total number of currently registered Go functions.

The function becomes the property 'name' of the global object: writable, configurable and not enumerable. A configurable property already there, such as one a script assigned or a built-in like Array, is replaced. A property that is not configurable, such as undefined, NaN, Infinity or one a script declared with var or function, is kept and RegisterFunc returns an error, as it does when the global object is not extensible. Registering a name a Go function of this VM already has returns an error too, even when a script has since deleted or replaced that function. The name is the one javascript sees: invalid UTF-8 is replaced as Go decodes it (see Eval), so two Go strings can be the same name, and a NUL byte, which would end the name, is an error.

Note: The 'name' argument should be a valid Javascript identifier. It is not currently enforced but this may change later.

For binding host objects (a DOM, console, fetch, …) prefer RegisterHostFunc, which gives the more convenient single-return / error-throws-exception shape.

Example (Error)

Call error returning Go function from Javascript.

m, _ := NewVM()
defer m.Close()
m.RegisterFunc("gofunc", func(a int) error {
	if a < 0 {
		return fmt.Errorf("negative")
	}
	return nil
}, false)
fmt.Println(m.Eval("gofunc(-1)", EvalGlobal))
fmt.Println(m.Eval("gofunc(1)", EvalGlobal))
Output:
negative <nil>
<nil> <nil>
Example (MultipleReturn)

Call multiple return Go function from Javascript.

m, _ := NewVM()
defer m.Close()
m.RegisterFunc("gofunc", func(a, b int) (int, int) { return 10 * a, 100 * b }, false)
fmt.Println(m.Eval("gofunc(2, 3)", EvalGlobal))
Output:
[20,300] <nil>
Example (SingleReturn)

Call single return Go function from Javascript.

m, _ := NewVM()
defer m.Close()
m.RegisterFunc("gofunc", func(a, b, c int) int { return a + b*c }, false)
fmt.Println(m.Eval("gofunc(2, 3, 5)", EvalGlobal))
Output:
17 <nil>
Example (ThisNonNull)

Passing Javascript 'this' to a Go function.

m, _ := NewVM()
defer m.Close()
m.RegisterFunc("gofunc", func(this any) any { return this }, true)
fmt.Println(m.Eval("var obj = { foo: 314, method: gofunc }; obj.method()", EvalGlobal))
Output:
{"foo":314} <nil>
Example (ThisNonNull2)

Passing Javascript 'this' to a Go function.

m, _ := NewVM()
defer m.Close()
m.RegisterFunc("gofunc", func(this any, n int) (any, int) { return this, 10 * n }, true)
fmt.Println(m.Eval("var obj = { foo: 314, method: gofunc }; obj.method(42)", EvalGlobal))
Output:
[{"foo":314},420] <nil>
Example (ThisNull)

Passing undefined Javascript 'this' to a Go function.

m, _ := NewVM()
defer m.Close()
m.RegisterFunc("gofunc", func(this any) any { return this }, true)
fmt.Println(m.Eval("gofunc()", EvalGlobal))
Output:
undefined <nil>
Example (ThisNull2)

Passing undefined Javascript 'this' to a Go function.

m, _ := NewVM()
defer m.Close()
m.RegisterFunc("gofunc", func(this any, n int) (any, int) { return this, 10 * n }, true)
fmt.Println(m.Eval("gofunc(42)", EvalGlobal))
Output:
[null,420] <nil>
Example (Void)

Call void Go function from Javascript.

m, _ := NewVM()
defer m.Close()
m.RegisterFunc("gofunc", func(a, b, c int) { fmt.Println(a + b*c) }, false)
fmt.Println(m.Eval("gofunc(2, 3, 5)", EvalGlobal))
Output:
17
undefined <nil>

func (*VM) RegisterHostFunc

func (m *VM) RegisterHostFunc(name string, fn HostFunc) error

RegisterHostFunc exposes fn to JavaScript as a global function named name, using the calling convention an embedding host wants: every JS call argument arrives in the single []any slice (already converted to Go values), fn returns exactly one value to convert back to JS, and a non-nil error is thrown as a JavaScript exception.

This differs from RegisterFunc, whose reflection mapping turns a Go multiple-return into a JS array and converts a returned error like any other value (so a (value, error) pair becomes the array [value, error] rather than a value-or-throw). RegisterHostFunc is the better fit for binding host objects; RegisterFunc remains for direct, typed Go calls.

The name is handled as by RegisterFunc, including when the global object already has it.

func (*VM) SetCanBlock

func (m *VM) SetCanBlock(value bool)

SetCanBlock configures m's blocking mode.

func (*VM) SetDefaultModuleLoader

func (m *VM) SetDefaultModuleLoader()

SetDefaultModuleLoader will enable loading module using the default module loader.

Example

Enabling the module loader.

m, _ := NewVM()
defer m.Close()
m.SetDefaultModuleLoader()
// testdata/power.js:
//  export const name = "Power";
//
//  export function square(x) {
//  	return x*x;
//  }
//
//  export function cube(x) {
//  	return x*x*x;
//  }
m.Eval("import * as Power from './testdata/power.js'; globalThis.Power = Power;", EvalModule)
fmt.Println(m.Eval("[Power.square(2), Power.cube(2)];", EvalGlobal))
Output:
[4,8] <nil>

func (*VM) SetEvalTimeout

func (m *VM) SetEvalTimeout(d time.Duration) error

SetEvalTimeout sets the timeout for the various eval functions. Passing zero 'd' disables timeouts. The timeout starts with the outermost call into javascript: a host function that evaluates more javascript on m runs inside the time left to the call that got it there. The timeout runs on the monotonic clock, so setting the system clock does not move it. A timeout too long for that clock (about 292 years) never fires.

Example
m, _ := NewVM()
defer m.Close()

for _, timeout := range []time.Duration{100 * time.Millisecond, time.Second, 3 * time.Second} {
	m.SetEvalTimeout(timeout)
	t0 := time.Now()
	r, err := m.Eval(`
function f() {
	var sink;
	for (var i = 0; i < 10000; i++) {
		sink += 42;
		sink -= 42;
	}
}

(function() {
	for (var i = 0; i < 10000; i++) {
		f();
	}
	return 42;
})();
`, EvalGlobal)
	d := time.Since(t0)
	min := timeout / 2
	max := timeout * 3 / 2
	fmt.Println(r, err, timeout, d >= min && d <= max)
}
Output:
<nil> InternalError: interrupted 100ms true
<nil> InternalError: interrupted 1s true
<nil> InternalError: interrupted 3s true

func (*VM) SetGCThreshold

func (m *VM) SetGCThreshold(threshold uintptr)

SetGCThreshold sets the memory threshold at which a GC will be performed, in bytes.

func (*VM) SetMaxStackSize

func (m *VM) SetMaxStackSize(threshold uintptr)

SetMaxStackSize limits the JS call depth, in libc.TLS stack slots (~one per JS call frame in the CGo-free port). Deep recursion then throws a catchable RangeError instead of overflowing the Go goroutine stack. 0 restores the MaxStackSlots default.

func (*VM) SetMemoryLimit

func (m *VM) SetMemoryLimit(limit uintptr)

SetMemoryLimit limits m's maxmimum memory usage, in bytes. Small values of 'limit' are not honored because the VM needs to allocate memory also for the exception object itself. The particular value of "small" is unspecified and subject to change without notice.

func (*VM) SetModuleLoader

func (m *VM) SetModuleLoader(loader ModuleLoaderFunc, normalize ModuleNormalizeFunc)

SetModuleLoader configures custom Go functions to load and normalize Javascript modules. Passing nil for 'loader' removes the custom loader, and so does Close; until then the engine keeps 'loader' and 'normalize', and whatever they close over, reachable. If 'normalize' is nil, the engine falls back to the default QuickJS module normalizer.

func (*VM) SetProperty

func (m *VM) SetProperty(this Value, prop Atom, val any) (err error)

SetProperty sets this.prop = val.

func (*VM) SetPropertyValue

func (m *VM) SetPropertyValue(this Value, prop Atom, val Value) (err error)

SetPropertyValue sets this.prop = val.

func (*VM) StdAddHelpers

func (m *VM) StdAddHelpers() error

StdAddHelpers adds the 'print' and 'console' global objects to 'm'.

type Value

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

Value represents a native Javascript value. Values are reference counted and their lifetime is managed by an independent Javascript garbage collector. To avoid memory corruption/leaks caused by tripping the Javascript GC, a Value must not

  • be copied. Use the Dup method instead.
  • become unreachable without calling its Free method.
  • be used after its Free() method was called.
  • outlive its VM.

It is recommended to use native Go values instead of Value where possible.

When passing a Value down the call stack use Dup. For example in main

v, _ := EvalValue(someScript)
defer v.Free()
foo(v.Dup()) // Instead of foo(v)

In 'foo' Free must be used. For example

func foo(v Value) {
        defer v.Free()
        ...
}

This ensures the/only topmost Free marks 'v' eligible for garbage collection.

Beware that the correct setup/handling becomes more complicated when using closures, Values are sent through a channel etc. In particular, if a goroutine 1 passes a Dup of 'v' to goroutine 2 and goroutine 1 completes and thus frees 'v' before goroutine 2 completes, the reference counting mechanism will fail. In other words, every Free must be strictly paired with the Dup that preceded obtaining the Value and the Dup/Free calls must respect the original nesting. This is correct.

Dup             // in main
        Dup     // in foo
        Free    // in foo
Free            // in main

This will fail, for example in the above discussed goroutines scenario.

Dup            // in g1
        Dup    // in g2
Free           // in g1
        Free   // in g2

The fix might be in this case to arrange goroutine 1 to wait for goroutine 2 to complete before executing Free in goroutine 1.

See TestMemgrind for an example of how to detect memory leaks caused not only by improper Value use.

func (Value) Any

func (v Value) Any() (r any, err error)

Any attemtps to convert 'v' to any using the same rules as there are for the return value of VM.Eval.

func (Value) Call

func (v Value) Call(this Value, args ...any) (r any, err error)

Call evaluates 'v(args...)' with 'this' as the receiver and returns the resulting (value, error). 'v' must be a Javascript function. Pass UndefinedValue as 'this' when the function does not use it.

resolve.Call(quickjs.UndefinedValue, "done")
reject.Call(quickjs.UndefinedValue, err.Error())

Argument types are converted using the same rules as in VM.Call and the result is unpacked using the same rules as there are for the return value of VM.Eval. Note that a Go error is not one of the recognized types, so it is marshaled to JSON like any other value — and because the standard error types have no exported fields that yields '{}'. Pass err.Error() instead.

Note: A Value passed to a Go function registered by VM.RegisterFunc is valid only for the duration of that call. To call it later, for example to settle a promise after the Go function has returned, retain it using Value.Dup and release it using Value.Free when done.

Note: See the Value documentation for details about manual memory management of Value objects.

func (Value) CallValue

func (v Value) CallValue(this Value, args ...any) (r Value, err error)

CallValue is like Value.Call but returns (Value, error) like VM.EvalValue.

Note: See the Value documentation for details about manual memory management of Value objects.

func (Value) Dup

func (v Value) Dup() Value

Dup returns a copy of 'v' while updating its reference count.

func (*Value) Free

func (v *Value) Free()

Free marks 'v' as no longer used and updates its reference count. 'v' must not be used afterwards.

func (Value) GetProperty deprecated

func (v Value) GetProperty(this Value, prop Atom) (r any, err error)

GetProperty returns v.prop. The 'this' argument is ignored.

Deprecated: The 'this' argument is vestigial, the property is always looked up on 'v'. Use Value.Property instead.

func (Value) GetPropertyValue

func (v Value) GetPropertyValue(prop Atom) (r Value, err error)

GetPropertyValue returns v.prop.

func (Value) IsUndefined

func (v Value) IsUndefined() bool

IsUndefined reports whether 'v' represents the Javascript value 'undefined'.

func (Value) MarshalJSON

func (v Value) MarshalJSON() (r []byte, err error)

MarshalJSON implements encoding/json.Marshaler.

func (Value) Property

func (v Value) Property(prop Atom) (r any, err error)

Property returns v.prop. It is Value.GetProperty without the vestigial 'this' argument.

func (Value) SetProperty

func (v Value) SetProperty(prop Atom, val any) (err error)

SetProperty sets v.prop = val.

func (Value) SetPropertyValue

func (v Value) SetPropertyValue(prop Atom, val Value) (err error)

SetPropertyValue sets v.prop = val.

func (Value) Then

func (v Value) Then(onFulfilled, onRejected func(Value)) (_ Value, err error)

Then attaches fulfillment and rejection handlers to the Promise v, returning the new promise for chaining. Either onFulfilled or onRejected may be nil.

func (*Value) VM

func (v *Value) VM() *VM

VM returns the VM associated with 'v'.

Directories

Path Synopsis
Package fmt exposes some functionality of the Go stdlib fmt package to a quickjs.VM.
Package fmt exposes some functionality of the Go stdlib fmt package to a quickjs.VM.
internal
fixlib command
Command fixlib reapplies this fork's changes to the engine under lib/ after its files were taken from upstream libquickjs, as happens when a git subtree pull hits a conflict in a generated ccgo_*.go file and the upstream version is checked out.
Command fixlib reapplies this fork's changes to the engine under lib/ after its files were taken from upstream libquickjs, as happens when a git subtree pull hits a conflict in a generated ccgo_*.go file and the upstream version is checked out.
Package intl implements ECMA-402, the ECMAScript Internationalization API, for a quickjs.VM.
Package intl implements ECMA-402, the ECMAScript Internationalization API, for a quickjs.VM.
internal/gen command
Command gen builds intl/cldr.bin, the locale data package intl formats with, from the JSON distribution of CLDR (github.com/unicode-org/cldr-json).
Command gen builds intl/cldr.bin, the locale data package intl formats with, from the JSON distribution of CLDR (github.com/unicode-org/cldr-json).
Package libquickjs is a pure Go embeddable Javascript engine.
Package libquickjs is a pure Go embeddable Javascript engine.

Jump to

Keyboard shortcuts

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