QuickJS是一个小型并且可嵌入的Javascript引擎,它支持ES2020规范,包括模块,异步生成器和代理器。
它可选支持数学扩展,例如大整数 (BigInt),大浮点数 (BigFloat) 以及运算符重载。
官方站点:https://bellard.org/quickjs/
中文站点:https://github.com/quickjs-zh/
QuickJS QQ群1:598609506。
中文Wiki:https://github.com/quickjs-zh/QuickJS/wiki
- 小巧且易于嵌入:仅需少量 C 文件,无外部依赖;在 x86 上,一个简易 “hello world” 程序约 210 KiB。
- 启动极快的解释器:在台式机的单核上,运行 ECMAScript 测试套件[2] 的 77000 个测试用例用时不到 2 分钟。运行时实例的完整生命周期在不到 300 微秒内完成。
- 几乎完整的 ES2023 支持,包括模块、异步生成器以及完整的 Annex B(传统 Web 兼容性)。部分 ES2024[3] 的特性也已支持。
- 在选择 ES2023 特性时,几乎通过 100% 的 ECMAScript 测试套件用例。
- 可将 JavaScript 源代码编译为无外部依赖的可执行文件。
- 采用引用计数并结合循环删除的垃圾回收(降低内存占用并具备确定性行为)。
- 以 JavaScript 实现的带上下文着色与补全的命令行解释器。
- 内置体积小的标准库,提供 C 标准库封装。
提供 Makefile,可在 Linux 或 MacOS/X 上编译。通过在 Linux 主机上使用 MingGW 工具进行交叉编译,可获得初步的 Windows 支持。
如需选择特定编译选项,请编辑 Makefile 顶部,然后运行 make。
若希望将二进制和支持文件安装到 /usr/local(使用 QuickJS 并非必需),可使用 root 身份执行:
make install
注意:在某些操作系统上,原子操作不可用或需要特定库。如果遇到相关错误,可以在 Makefile 的 LIBS 变量中添加 -latomics,或在 quickjs.c 中禁用 CONFIG_ATOMICS。
qjs 是命令行解释器(REPL)。可以将 JavaScript 文件和/或表达式作为参数传入以执行:
./qjs examples/hello.js
qjsc 是命令行编译器:
./qjsc -o hello examples/hello.js
./hello
生成一个不依赖外部库的 hello 可执行文件。
usage: qjs [options] [file [args]]
选项:
-h, --help
列出选项。
-e `EXPR` , --eval `EXPR`
执行表达式 EXPR。
-i, --interactive
进入交互模式(���命令行上提供了文件时,默认不为交互模式)。
-m, --module
作为 ES6 模块加载(默认=自动检测)。当文件扩展名为 .mjs 或源码的第一个关键词为 import 时自动检测为模块。
--script
作为 ES6 脚本加载(默认=自动检测)。
-I file, --include file
包含一个额外的文件。
高级选项:
--std
即使加载的不是模块,也让 std 与 os 模块可用。
-d, --dump
转储内存使用统计信息。
-q, --quit
仅实例化解释器后立即退出。
usage: qjsc [options] [files]
选项:
-c
仅将字节码输出到 C 文件。默认输出为可执行文件。
-e
在 C 文件中输出 main() 和字节码。默认输出为可执行文件。
-o output
设置输出文件名(默认=out.c 或 a.out)。
-N cname
设置生成数据的 C 名称。
-m
编译为 JavaScript 模块(默认=自动检测)。
-D module_name
编译一个动态加载的模块及其依赖。当代码使用 import 关键字或构造 os.Worker 时需要该选项,因为编译器无法静态发现动态加载模块的名称。
-M module_name[,cname]
为外部 C 模块添加初始化代码。参见 c_module 示例。
-x
字节序交换输出(仅用于交叉编译)。
-flto
使用链接时优化(LTO)。编译更慢,但可执行文件更小更快。当使用 -fno-x 类选项时自动启用。
-fno-[eval|string-normalize|regexp|json|proxy|map|typedarray|promise|bigint]
禁用所选语言特性,以生成更小的可执行文件。
运行 make test 以执行 QuickJS 存档中包含的少量内置测试。
QuickJS 存档中包含 test262 运行器。可以将 test262 测试安装到 QuickJS 源码目录:
git clone https://github.com/tc39/test262.git test262
cd test262
patch -p1 < ../tests/test262.patch
cd ..
该补丁添加了特定实现的 harness 函数,并优化了低效的正则字符类和 Unicode 属性转义测试(测试本身未修改,仅优化了一个慢速字符串初始化函数)。
运行测试:
make test2
配置文件 test262.conf(旧 ES5.1 测试使用 test262o.conf[4])包含运行各种测试的选项。可根据特性或文件名排除测试。
文件 test262_errors.txt 包含当前错误列表。运行器在出现新错误或纠正/修改现有错误时显示消息。使用 -u 选项更新当前错误列表(或执行 make test2-update)。
文件 test262_report.txt 包含所有测试的日���,便于更清晰地分析特定错误。在发生崩溃时,最后一行对应失败的测试。
使用语法 ./run-test262 -c test262.conf -f filename.js 运行单个测试;使用 ./run-test262 -c test262.conf N 从第 N 个测试开始执行。
更多信息可运行 ./run-test262 查看 test262 运行器的命令行选项。
通过 eshost 从 test262-harness[5] 调用时,run-test262 接受 -N 选项。除非需要在相同条件下比较 QuickJS 与其它引擎,否则不建议使用这种方式运行测试,速度会慢很多(通常半小时,而直接方式约 100 秒)。
几乎完整支持 ES2023 规范,包括 Annex B(遗留 Web 兼容性)及与 Unicode 相关的特性。
目前尚未支持的特性:
- 尾调用[6]
ECMA402(国际化 API)未支持。
完全支持 ES6 模块。默认名称解析规则如下:
- 以
.或..开头的模块名相对当前模块路径。 - 不以
.或..开头的模块名为系统模块,例如std或os。 - 以
.so结尾的模块为使用 QuickJS C API 的原生模块。
标准库默认包含在命令行解释器中。它包含两个模块 std 与 os,以及少量全局对象。
scriptArgs
提供命令行参数。第一个参数是脚本名称。
print(...args)
打印参数,参数之间以空格分隔,并在末尾追加换行。
console.log(...args)
与 print() 相同。
std 模块为 libc 的 stdlib.h、stdio.h 提供封装,并包含一些其它实用功能。
可用导出:
exit(n):退出进程。
evalScript(str, options = undefined):将字符串 str 作为脚本(全局 eval)执行。options 为可选对象,支持:
backtrace_barrier(布尔,默认 false):为 true 时,错误回溯不展示evalScript之下的堆栈帧。async(布尔,默认 false):为 true 时,脚本可使用await,并返回一个 Promise;该 Promise 解析为一个包含value字段的对象,value为脚本返回的值。
loadScript(filename):将文件 filename 作为脚本(全局 eval)执行。
loadFile(filename):按 UTF-8 编码读取文件 filename 并以字符串返回;I/O 错误返回 null。
open(filename, flags, errorObj = undefined):打开文件(libc fopen() 的封装)。返回 FILE 对象,或在 I/O 错误时返回 null。若提供 errorObj,其 errno 属性会设置为错误码(或为 0 表示无错)。
popen(command, flags, errorObj = undefined):通过创建管道打开进程(libc popen() 封装)。返回 FILE 对象或 null;errorObj.errno 同上。
fdopen(fd, flags, errorObj = undefined):从文件句柄打开文件(libc fdopen() 封装)。返回 FILE 对象或 null;errorObj.errno 同上。
tmpfile(errorObj = undefined):打开临时文件。返回 FILE 对象或 null;errorObj.errno 同上。
puts(str):等价于 std.out.puts(str)。
printf(fmt, ...args):等价于 std.out.printf(fmt, ...args)。
sprintf(fmt, ...args):等价于 libc 的 sprintf()。
in、out、err:libc 文件 stdin、stdout、stderr 的封装。
SEEK_SET、SEEK_CUR、SEEK_END:seek() 的常量。
Error:枚举对象,包含常见错误的整数值(可定义附加错误码):EINVAL、EIO、EACCES、EEXIST、ENOSPC、ENOSYS、EBUSY、ENOENT、EPERM、EPIPE。
strerror(errno):返回描述错误码 errno 的字符串。
gc():手动触发循环删除算法。循环删除在需要时自动启动;该函数在特定内存限制或测试时有用。
getenv(name):返回环境变量 name 的值;未定义返回 undefined。
setenv(name, value):将环境变量 name 设置为字符串 value。
unsetenv(name):删除环境变量 name。
getenviron():以键值对对象形式返回所有环境变量。
urlGet(url, options = undefined):使用命令行工具 curl 下载 url。options 可包含:
binary(布尔,默认 false):为 true 时响应为ArrayBuffer;否则为字符串(假定 UTF-8)。full(布尔,默认 false):为 true 时返回对象,包含response(响应内容)、responseHeaders(以 CRLF 分隔的响应头)、status(状态码)。当发生协议或网络错误时response为null。若full为 false,且状态码范围为 200–299,直接返回响应;否则返回null。
parseExtJSON(str):以 JSON.parse 的超集解析 str,与 JSON5 非常接近。支持:单行/多行注释、未加引号的属性(ASCII 仅标识符)、数组/对象末尾多余逗号、单引号字符串、\v 转义与以反斜杠续行的多行字符串、将 \f/\v 视为空白字符、数字前导加号或仅小数点、十六进制(0x)、八进制(0o)、二进制(0b)整数、接受 NaN 与 Infinity 为数字。
FILE 原型:
close():关闭文件;成功返回 0,I/O 错误返回 -errno。
puts(str):按 UTF-8 输出字符串。
printf(fmt, ...args):格式化输出。支持与标准 C printf 相同的格式。整数类型(如 %d)会将 Number 或 BigInt 截断为 32 位;使用 l 修饰符(如 %ld)截断为 64 位。
flush():刷新缓冲文件。
seek(offset, whence):定位到指定文件位置(whence 使用 std.SEEK_*)。offset 可为 Number 或 BigInt;成功返回 0,I/O 错误返回 -errno。
tell():返回当前文件位置。
tello():以 BigInt 返回当前文件位置。
eof():到达文件末尾返回 true。
fileno():返回关联的 OS 句柄。
error():如发生错误返回 true。
clearerr():清除错误标志。
read(buffer, position, length):将文件中从字节位置 position 开始的 length 字节读入 ArrayBuffer buffer(libc fread 封装)。
write(buffer, position, length):将 ArrayBuffer buffer 中从字节位置 position 开始的 length 字节写入文件(libc fwrite 封装)。
getline():返回文件的下一行(假定 UTF-8 编码),不包含尾随换行符。
readAsString(max_size = undefined):读取 max_size 字节并按 UTF-8 返回字符串;未指定时读取至文件结束。
getByte():返回文件的下一个字节;到达末尾返回 -1。
putByte(c):写入一个字节。
os 模块提供操作系统相关功能:底层文件访问、信号、计时器、异步 I/O、工作线程(线程)。
OS 函数通常在成功时返回 0,或返回 OS 特定的负错误码。
可用导出(节选):
open(filename, flags, mode = 0o666):打开文件。返回句柄,或错误时返回小于 0 的值。
O_RDONLY、O_WRONLY、O_RDWR、O_APPEND、O_CREAT、O_EXCL、O_TRUNC:POSIX 打开标志。
O_TEXT(Windows 特有):以文本模式打开文件;默认为二进制模式。
close(fd):关闭文件句柄 fd。
seek(fd, offset, whence):在文件中定位。whence 使用 std.SEEK_*;offset 可为 Number 或 BigInt;若为 BigInt,返回值也为 BigInt。
read(fd, buffer, offset, length):从句柄 fd 的字节位置 offset 开始,读取 length 字节到 ArrayBuffer buffer。返回读取字节数,错误时返回小于 0。
write(fd, buffer, offset, length):将 ArrayBuffer buffer 的 length 字节从字节位置 offset 写入句柄 fd。返回写入字节数,错误时返回小于 0。
isatty(fd):若 fd 是 TTY(终端)句柄返回 true。
ttyGetWinSize(fd):返回 TTY 大小 [width, height] 或不可用时返回 null。
ttySetRaw(fd):将 TTY 设置为原始模式。
remove(filename):删除文件。成功返回 0,或返回 -errno。
rename(oldname, newname):重命名文件。成功返回 0,或返回 -errno。
realpath(path):返回 [str, err],其中 str 为 path 的规范化绝对路径,err 为错误码。
getcwd():返回 [str, err],其中 str 为当前工作目录,err 为错误码。
chdir(path):切换当前目录。成功返回 0,或返回 -errno。
mkdir(path, mode = 0o777):在 path 创建目录。成功返回 0,或返回 -errno。
stat(path) / lstat(path):返回 [obj, err],obj 为文件状态对象(字段:dev, ino, mode, nlink, uid, gid, rdev, size, blocks, atime, mtime, ctime,时间以毫秒为单位);lstat() 与 stat() 相同,但返回链接本身的信息。
S_IFMT、S_IFIFO、S_IFCHR、S_IFDIR、S_IFBLK、S_IFREG、S_IFSOCK、S_IFLNK、S_ISGID、S_ISUID:用于解释 stat() 返回的 mode 属性的常量,取值与 C 系统头文件 sys/stat.h 相同。
utimes(path, atime, mtime):修改文件 path 的访问/修改时间(毫秒为单位)。成功返回 0,或返回 -errno。
symlink(target, linkpath):��� linkpath 创建指向字符串 target 的链接。成功返回 0,或返回 -errno。
readlink(path):返回 [str, err],其中 str 为链接目标,err 为错误码。
readdir(path):返回 [array, err],其中 array 为目录 path 下的文件名数组,err 为错误码。
setReadHandler(fd, func):为文件句柄 fd 添加读处理器;每次 fd 有待处理数据时调用。每个句柄只支持一个读处理器;传 func = null 删除处理器。
setWriteHandler(fd, func):为文件句柄 fd 添加写处理器;每次 fd 可写时调用。每个句柄只支持一个写处理器;传 func = null 删除处理器。
signal(signal, func):在信号 signal 发生时调用函数 func。每个信号编号只支持一个处理器;传 null 设置默认处理器,传 undefined 忽略信号。信号处理器仅可在主线程定义。
SIGINT、SIGABRT、SIGFPE、SIGILL、SIGSEGV、SIGTERM:POSIX 信号编号。
kill(pid, sig):向进程 pid 发送信号 sig。
exec(args[, options]):以参数 args 执行子进程。options 为可选对象,支持:
block(布尔,默认 true):为 true 时阻塞等待进程结束;返回正数退出码,或若被信号中断则返回负的信号编号。为 false 时不阻塞,返回子进程 pid。usePath(布尔,默认 true):为 true 时在PATH环境变量中搜索执行文件。file(字符串,默认args[0]):设置要执行的文件。cwd(字符串):设置子进程工作目录。stdin/stdout/stderr:设置子进程的标准流句柄。env(对象):以键值对设置子进程环境;未提供时沿用当前进程环境。uid(整数):若提供则使用setuid设置子进程 uid。gid(整数):若提供则使用setgid设置子进程 gid。
getpid():返回当前进程 ID。
waitpid(pid, options):Unix 系统调用 waitpid。返回 [ret, status];错误时 ret 为 -errno。
WNOHANG:waitpid 的 options 参数常量。
dup(fd)、dup2(oldfd, newfd):Unix 系统调用。
pipe():Unix 系统调用。成功返回两个句柄 [read_fd, write_fd],错误时返回 null。
sleep(delay_ms):睡眠 delay_ms 毫秒。
sleepAsync(delay_ms):异步睡眠 delay_ms 毫秒,返回 Promise。例如:
await os.sleepAsync(500);
now():返回以毫秒为单位的时间戳,精度高于 Date.now();起始时间未定义,通常不受系统时钟调整影响。
setTimeout(func, delay):在 delay 毫秒后调用函数 func;返回计时器句柄。
clearTimeout(handle):取消计时器。
platform:返回表示平台的字符串:"linux"、"darwin"、"win32" 或 "js"。
Worker(module_filename):构造函数,用于创建新线程(worker),API 接近 WebWorkers。module_filename 为在线程中执行的模块文件名;与动态导入模块一样,路径相对当前脚本或模块。线程默认不共享数据,通过消息通信。不支持嵌套 worker。示例见 tests/test_worker.js。
Worker 类的静态属性:
parent:在创建的 worker 中,Worker.parent 表示父 worker,用于发送/接收消息。
Worker 实例的属性:
postMessage(msg):向对应 worker 发送消息。msg 使用与 HTML 结构化克隆算法相似的方式在��标 worker 中克隆;SharedArrayBuffer 在 worker 间共享。当前限制:暂不支持 Map 与 Set。
onmessage(getter/setter):设置在接收消息时调用的函数。该函数接收一个参数,为包含 data 属性的对象,data 即收到的消息。只要存在至少一个非 null 的 onmessage 处理器,线程不会被终止。
C API 设计简洁高效。C API 定义在头文件 quickjs.h 中。
JSRuntime 表示与对象堆对应的 JavaScript 运行时。可同时存在多个运行时,但它们不能交换对象;在同一运行时内不支持多线程。
JSContext 表示 JavaScript 上下文(或 Realm)。每个 JSContext 都有自己的全局对象和系统对象。每个 JSRuntime 中可存在多个 JSContext,并且它们可以共享对象,类似于浏览器中同源的多个框架共享 JavaScript 对象。
JSValue 表示一个 JavaScript 值,可为原始类型或对象。采用引用计数,因此需要显式复制(JS_DupValue(),增加引用计数)或释放(JS_FreeValue(),减少引用计数)JSValue。
可使用 JS_NewCFunction() 创建 C 函数。JS_SetPropertyFunctionList() 可便捷地将函数、setter 与 getter 属性添加到给定对象。
与其他嵌入式 JavaScript 引擎不同,QuickJS 没有隐式栈,因此 C 函数以普通的 C 参数接收其入参。一般规则是:C 函数以常量 JSValue 作为参数(无需释放),并返回一个新分配(活跃)的 JSValue。
异常:多数 C 函数可能返回 JavaScript 异常。必须由 C 代码显式测试并处理它。特定 JSValue 值 JS_EXCEPTION 表示发生了异常。实际的异常对象存储在 JSContext 中,可通过 JS_GetException() 获取。
使用 JS_Eval() 评估脚本或模块源码。
若脚本或模块已由 qjsc 编译为字节码,则可通过调用 js_std_eval_binary() 进行评估。优势是无需编译,因而更快且更小;如果不需要 eval,可将编译器从可执行文件中移除。
注意:字节码格式与具体的 QuickJS 版本绑定,且在执行前不会进行安全检查。因此不应从不受信任的来源加载字节码。这也是 qjsc 中没有将字节码输出到二进制文件的选项的原因。
可以将 C 的不透明数据附加到 JavaScript 对象上。不透明数据的类型由对象的类 ID(JSClassID)确定。因此,第一步是注册新的类 ID 与 JS 类(JS_NewClassID()、JS_NewClass())。然后可使用 JS_NewObjectClass() 创建该类的对象,并通过 JS_GetOpaque() / JS_SetOpaque() 获取或设置不透明指针。
在定义新的 JS 类时,可以声明析构函数,在对象销毁时调用;它应用于释放 C 资源,但不应在其中执行 JS 代码。可提供 gc_mark 方法,以便循环删除算法找出被该对象引用的其它对象。还可提供其它方法来定义异类对象行为。
类 ID 在全局范围分配(适用于所有运行时)。JSClass 在每个 JSRuntime 中分配。使用 JS_SetClassProto() 可在给定 JSContext 中为特定类定义原型;JS_NewObjectClass() 在创建的对象中设置该原型。
示例见 quickjs-libc.c。
支持动态或静态链接的原生 ES6 模块。参见示例 test_bjson 与 bjson.so。标准库 quickjs-libc.c 也是原生模块的良好示例。
使用 JS_SetMemoryLimit() 为给定的 JSRuntime 设置全局内存分配限制。
可通过 JS_NewRuntime2() 提供自定义内存分配函数。
可通过 JS_SetMaxStackSize() 设置最大系统栈大小。
使用 JS_SetInterruptHandler() 设置一个回调,该回调在引擎执行代码时被定期调用。可用它来实现执行超时。
命令行解释器使用它来实现 Ctrl-C 处理器。
编译器直接生成字节码而不使用解析树等中间表示,因此非常快速。生成的字节码上会执行多轮优化。
选择基于栈的字节码,因为它简单且能生成紧凑代码。
对每个函数,最大栈大小在编译期计算,因此无需在运行时执行栈溢出检查。
调试信息维护了单独的压缩行号表。
对闭包变量的访问进行了优化,几乎与局部变量一样快。
严格模式下的直接 eval 得到优化。
qjsc 编译器从 JavaScript 文件生成 C 源代码。默认情况下,C 源代码由系统编译器(gcc 或 clang)编译。
生成的 C 源包含已编译函数或模块的字节码。若需要完整的可执行文件,它还包含一个 main() 函数,其中含有必要的 C 代码��初始化 JavaScript 引擎,并加载与执行已编译的函数和模块。
JavaScript 代码可与 C 模块混合使用。
为生成更小的可执行文件,可禁用特定的 JavaScript 特性,尤其是 eval 或正则表达式。代码移除依赖系统编译器的链接时优化(LTO)。
qjsc 通过编译脚本或模块,然后序列化为二进制格式工作。该格式的一个子集(不包含函数或模块)可用作二进制 JSON。示例 test_bjson.js 展示了其用法。
警告:二进制 JSON 格式可能在不通知的情况下更改,因此不应用于存储持久数据。test_bjson.js 示例仅用于测试二进制对象格式的函数。
字符串存储为 8 位或 16 位字符数组。因此,随机访问字符始终很快。
C API 提供将 JavaScript 字符串转换为 C UTF-8 字符串的函数。最常见的情况是 JavaScript 字符串仅包含 ASCII 字符,此时不涉及复制。
对象形状(对象原型、属性名与标志)在对象间共享以节省内存。
无洞(除数组结尾外)的数组得到优化。
TypedArray 访问得到优化。
对象属性名和部分字符串作为原子(唯一字符串)存储,以节省内存并便于快速比较。原子以 32 位整数表示;原子范围的一半保留用于从 0 到 2^{31}-1 的立即整数文本。
数值以 32 位有符号整数或 64 位 IEEE-754 浮点表示。多数运算在 32 位整数情况下有快速路径。
使用引用计数自动且确定性地释放对象;当已分配内存过大时执行单独的循环删除过程。循环删除仅依赖引用计数与对象内容,因此无需在 C 代码中操作显式垃圾回收根。
JSValue 是可表示原始类型(如 Number、String 等)或对象的 JavaScript 值。32 位版本使用 NaN boxing 存储 64 位浮点数;表示方式经过优化,以便高效测试 32 位整数和引用计数值。
在 64 位代码中,JSValue 为 128 位宽,不使用 NaN boxing;其理由是 64 位环境下内存占用不那么关键。
两种情况下(32 位或 64 位),JSValue 恰好占用两个 CPU 寄存器,因此可被 C 函数高效返回。
引擎对函数调用进行了优化以保证速度。系统栈持有 JavaScript 的参数与局部变量。
实现了一个专用正则表达式引擎:体积小、效率高,支持所有 ES2023 特性,包括 Unicode 属性。与 JavaScript 编译器类似,直接生成字节码而不使用解析树。
使用具有显式栈的回溯实现,避免系统栈上的递归。对简单量词做了专门优化以避免递归。
完整的正则库(不含 Unicode 库)在 x86 上约 15 KiB。
实现了一个专用 Unicode 库,以避免依赖 ICU 等大型外部库。所有 Unicode 表在保持合理访问速���的同时采用压缩存储。
该库支持大小写转换、Unicode 规范化、Unicode 脚本查询、Unicode 通用类别查询以及所有 Unicode 二值属性。
完整的 Unicode 库在 x86 上约 45 KiB。
BigInt 采用二进制补码表示。额外使用一个短 BigInt 值以优化小 BigInt 的性能。
QuickJS 以 MIT 许可证发布。
除非另有说明,QuickJS 源码版权归 Fabrice Bellard 和 Charlie Gordon 所有。
- https://tc39.es/ecma262/2023
- https://github.com/tc39/test262
- https://tc39.es/ecma262/
- 旧 ES5.1 测试可通过
git clone --single-branch --branch es5-tests https://github.com/tc39/test262.git test262o获取 - https://github.com/bterlson/test262-harness 目录
-
QuickJS-iOS iOS下的QuickJS库
-
QuickJS-Android Android下的QuickJS库
-
quickjs-rs Rust的QuickJS库
-
quickjspp C++的QuickJS库
-
go-quickjs Go的QuickJS库
-
txiki.js The tiny JavaScript runtime built with QuickJS, libuv and ❤️
-
QuickJS-Pascal Quickjs FreePascal / Delphi Bindings