bellard/mquickjs

GitHub: bellard/mquickjs

一款面向嵌入式系统的超轻量 JavaScript 引擎,在极低内存占用下安全运行受限子集脚本。

Stars: 5737 | Forks: 214

# MicroQuickJS ## 简介 MicroQuickJS(又称 MQuickJS)是一款面向 嵌入式系统的 JavaScript 引擎。它使用仅约 10 kB 的 RAM 即可编译并运行 JavaScript 程序。 整个引擎仅需约 100 kB 的 ROM(包括 C 库,ARM Thumb-2 代码)。其速度与 QuickJS 相当。 MQuickJS 仅支持 [ES5 的子集](#javascript-subset-reference)。它实现了一种 **更严格模式**,在此模式下,某些易错或低效的 JavaScript 构造被禁止。 尽管 MQuickJS 与 QuickJS 共享大量代码,但为了减少内存占用,其内部实现 有所不同。特别是,它依赖追踪式垃圾回收器,虚拟机不使用 CPU 栈,且字符串以 UTF-8 形式存储。 ## REPL REPL 是 `mqjs`。用法如下: ``` usage: mqjs [options] [file [args]] -h --help list options -e --eval EXPR evaluate EXPR -i --interactive go to interactive mode -I --include file include an additional file -d --dump dump the memory usage stats --memory-limit n limit the memory usage to 'n' bytes --no-column no column number in debug information -o FILE save the bytecode to FILE -m32 force 32 bit bytecode output (use with -o) -b --allow-bytecode allow bytecode in input file ``` 编译并运行一个程序,仅使用 10 kB RAM: ``` ./mqjs --memory-limit 10k tests/mandelbrot.js ``` 除了正常执行脚本外,`mqjs` 还可以将编译后的字节码输出到持久化存储(文件或 ROM): ``` ./mqjs -o mandelbrot.bin tests/mandelbrot.js ``` 随后可以运行编译后的字节码,如同普通脚本: ``` ./mqjs -b mandelbrot.bin ``` 字节码格式取决于 CPU 的字节序和字长(32 或 64 位)。在 64 位 CPU 上,可使用选项 `-m32` 生成可在 32 位嵌入式系统上运行的 32 位字节码。 若希望节省存储空间,可使用 `--no-column` 选项移除列号调试信息(仅保留行号)。 ## 更严格模式 MQuickJS 仅支持 JavaScript 的子集(主要为 ES5)。它始终处于 **更严格模式**,其中部分易错的 JavaScript 功能被禁用。其基本思想是:更严格模式是 JavaScript 的一个子集,因此仍可在其他 JavaScript 引擎中正常工作。以下是主要要点: - 仅允许 **严格模式** 构造,因此禁止 `with` 关键字,且全局变量必须使用 `var` 声明。 - 数组不能有空洞。在数组末尾之后写入元素是不允许的: ``` a = [] a[0] = 1; // OK to extend the array length a[10] = 2; // TypeError ``` 如果需要带空洞的类数组对象,请改用普通对象: ``` a = {} a[0] = 1; a[10] = 2; ``` `new Array(len)` 仍按预期工作,但数组元素会被初始化为 `undefined`。 带有空洞的数组字面量是语法错误: ``` [ 1, , 3 ] // SyntaxError ``` - 仅支持全局 `eval`,因此无法访问或修改局部变量: ``` eval('1 + 2'); // forbidden (1, eval)('1 + 2'); // OK ``` - 不支持值装箱:`new Number(1)` 不被支持,也完全没有必要。 ## JavaScript 子集参考 - 仅支持严格模式,强调与 ES5 兼容。 - `Array` 对象: - 不存在空洞。 - 数值属性始终由数组对象处理,不会转发到其原型。 - 超出边界的设置是错误,除非设置在数组末尾。 - `length` 属性是数组原型上的 getter/setter。 - 所有属性均可写、可枚举、可配置。 - `for in` 仅迭代对象自身的属性。应使用以下常见模式以获得与标准 JavaScript 一致的行为: ``` for(var prop in obj) { if (obj.hasOwnProperty(prop)) { ... } } ``` 建议优先使用 `for of`,它对数组同样支持: ``` for(var prop of Object.keys(obj)) { ... } ``` - 函数对象的 `prototype`、`length` 和 `name` 为 getter/setter。 - C 函数不能拥有自己的属性(C 构造函数行为符合预期)。 - 全局对象受支持,但其使用不推荐。它不能包含 getter/setter,且在其中直接创建的属性在执行脚本中不可见。 - `catch` 关键字绑定的变量是普通变量。 - 不支持直接 `eval`。仅支持间接(全局)`eval`。 - 不支持值装箱(例如 `new Number(1)`)。 - 正则表达式: - 大小写折叠仅适用于 ASCII 字符。 - 匹配仅限 Unicode,即 `/./` 匹配一个 Unicode 码点,而非 `u` 标志下的 UTF-16 字符。 - 字符串:`toLowerCase` / `toUpperCase` 仅处理 ASCII 字符。 - 日期:仅支持 `Date.now()`。 ES5 扩展: - 支持 `for of`,但仅迭代数组,不支持自定义迭代器(目前)。 - 支持类型化数组。 - 字符串字面量接受 `\u{hex}`。 - 数学函数:`imul`、`clz32`、`fround`、`trunc`、`log2`、`log10`。 - 支持指数运算符。 - 正则表达式:`.`、`y` 和 `u` 标志被接受。在 Unicode 模式下,不支持 Unicode 属性。 - 字符串函数:`codePointAt`、`replaceAll`、`trimStart`、`trimEnd`。 - 支持全局属性 `globalThis`。 ## C API ### 引擎初始化 MQuickJS 几乎不依赖 C 库。特别是它不使用 `malloc()`、`free()` 或 `printf()`。创建 MQuickJS 上下文时,必须提供一个内存缓冲区。引擎仅在该缓冲区中分配内存: ``` JSContext *ctx; uint8_t mem_buf[8192]; ctx = JS_NewContext(mem_buf, sizeof(mem_buf), &js_stdlib); ... JS_FreeContext(ctx); ``` 仅在需要调用用户对象的终结器时才需要 `JS_FreeContext(ctx)`,因为引擎不会分配系统内存。 ### 内存管理 C API 与 QuickJS 非常相似(参见 `mquickjs.h`)。然而,由于存在紧凑式垃圾回收器,存在重要区别: 1. 显式释放值并非必需(无需 `JS_FreeValue()`)。 2. 对象地址在每次 JS 分配时都可能移动。一般规则是避免在 C 中使用 `JSValue` 类型的变量。它们仅应在 MQuickJS API 调用之间作为临时值使用。其他情况下,始终使用指向 `JSValue` 的指针。`JS_PushGCRef()` 返回一个临时不透明 `JSValue` 指针,存储在 `JSGCRef` 变量中;`JS_PopGCRef()` 用于释放该临时引用。当对象移动时,`JSGCRef` 中的不透明值会自动更新。示例: ``` JSValue my_js_func(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv) { JSGCRef obj1_ref, obj2_ref; JSValue *obj1, *obj2, ret; ret = JS_EXCEPTION; obj1 = JS_PushGCRef(ctx, &obj1_ref); obj2 = JS_PushGCRef(ctx, &obj2_ref); *obj1 = JS_NewObject(ctx); if (JS_IsException(*obj1)) goto fail; *obj2 = JS_NewObject(ctx); // obj1 may move if (JS_IsException(*obj2)) goto fail; JS_SetPropertyStr(ctx, *obj1, "x", *obj2); // obj1 and obj2 may move ret = *obj1; fail: PopGCRef(ctx, &obj2_ref); PopGCRef(ctx, &obj1_ref); return ret; } ``` 在 PC 上运行时,可定义 `DEBUG_GC` 以强制 JS 分配器每次分配都移动对象。这是检查未使用无效 JSValue 的好方法。 ### 标准库 标准库通过自定义工具(`mquickjs_build.c`)编译为 C 结构,可存储在 ROM 中。因此标准库实例化非常迅速,且几乎不需要 RAM。`mqjs` 的标准库示例位于 `mqjs_stdlib.c`,其编译结果为 `mqjs_stdlib.h`。 `example.c` 是一个使用 MQuickJS C API 的完整示例。 ### 持久化字节码 `mqjs` 生成的字节码可在 ROM 中执行。此时必须先重定位字节码(参见 `JS_RelocateBytecode()`),然后使用 `JS_LoadBytecode()` 实例化,并通过 `JS_Run()` 运行(参见 `mqjs.c`)。 与 QuickJS 一样,不保证字节码级别的向后兼容性。此外,字节码在执行前不会进行验证。仅运行来自可信来源的 JavaScript 字节码。 ### 数学库与浮点仿真 MQuickJS 包含其自身的微型数学库(位于 `libm.c`)。此外,若 CPU 不支持浮点运算,它还包含一个浮点仿真器,可能比 GCC 工具链提供的更小。 ## 内部实现与与 QuickJS 的比较 ### 垃圾回收 采用追踪式且紧凑的垃圾回收器,替代引用计数。这允许更小的对象。GC 为每个分配的内存块增加少量开销。此外,避免了内存碎片。 引擎拥有自己的内存分配器,不依赖 C 库的 malloc。 ### 值与对象表示 值的大小与 CPU 字长相同(因此在 32 位 CPU 上为 32 位)。一个值可以包含: - 31 位整数(1 位标签) - 单个 Unicode 码点(因此是长度为 1 或 2 个 16 位代码单位的字符串) - 64 位浮点数(带有小指数,使用 64 位 CPU 字) - 指向内存块的指针。内存块在内存中存储标签。 JavaScript 对象至少需要 3 个 CPU 字(因此在 32 位 CPU 上为 12 字节)。根据对象类型,可能分配更多数据。属性存储在哈希表中。每个属性至少需要 3 个 CPU 字。属性可为 ROM 中的标准库对象。 属性键是 JSValue 类型,这与 QuickJS 不同(QuickJS特定类型)。它们可以是字符串或正 31 位整数。字符串属性键会被内部化(唯一)。 字符串在内部以 WTF-8(UTF-8 + 未配对代理)形式存储,而非 QuickJS 中的 8 或 16 位数组。代理对不会显式存储,但在遍历 16 位代码单位时仍可见。因此在保持与 JavaScript 和 UTF-8 完全兼容的同时,字符串处理得以实现。 C 函数可以存储为单个值以减少开销。在这种情况下,不能添加其他属性。大多数标准库函数都以这种方式存储。 ### 标准库 整个标准库位于 ROM 中,编译时生成。仅在 RAM 中创建少量对象。因此引擎实例化速度非常快。 ### 字节码 这是一种基于栈的字节码(与 QuickJS 类似)。然而,字节码通过间接表引用原子。 行号和列号信息使用 [指数哥伦布编码](https://en.wikipedia.org/wiki/Exponential-Golomb_coding) 进行压缩。 ### 编译 解析器与 QuickJS 非常接近,但避免了递归,因此 C 栈使用量有确定上限。没有抽象语法树。字节码在一次遍历中生成,并包含多项优化技巧(QuickJS 有多轮优化)。 ## 测试与基准 运行基础测试: ``` make test ``` 运行 QuickJS 微基准测试: ``` make microbench ``` 可下载更多测试以及在更严格模式下运行的 Octane 基准补丁版本: [此处](https://bellard.org/mquickjs/mquickjs-extras.tar.xz): 运行 V8 Octane 基准: ``` make octane ``` ## 许可证 MQuickJS 在 MIT 许可证下发布。 除非另有说明,MQuickJS 源代码版权归 Fabrice Bellard 和 Charlie Gordon 所有。
标签:ES5子集, IoT, JavaScript引擎, MicroQuickJS, MQuickJS, REPL, ROM, UTF-8字符串, 严格模式, 代码压缩, 低内存, 内存优化, 内存限制, 可执行文件, 垃圾回收, 字节码编译, 实时脚本, 客户端加密, 嵌入式开发, 嵌入式系统, 开源, 快速启动, 性能优化, 持久化字节码, 数据可视化, 检测绕过, 轻量级运行时, 追踪GC