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