QuickJS 集成与实践指南
目录
第 1 章:QuickJS 概述与特性全览
“在不足 1 MB 的体积中,实现完整 ECMAScript 规范的奇迹。”
1.1 QuickJS 是什么
QuickJS 是一个由 Fabrice Bellard(著名程序员,QEMU、FFmpeg、TinyCC 的作者)与 Charlie Gordon 于 2019 年推出的轻量级 ECMAScript 引擎。 它完全遵循 ECMAScript 标准(目前支持到 ES2023 主要特性),能够在几百 KB 的二进制体积内执行现代 JavaScript,包括:
async/await异步语法Promise、Proxy、SymbolBigInt、BigFloat、BigDecimal- 模块系统 (
import/export) - 垃圾回收、异常系统、原型链等完整特性
QuickJS 设计目标是:
“在极小体积内提供一个完整、可嵌入、可独立运行的现代 JavaScript 环境。”
1.2 为什么选择 QuickJS
1.2.1 相较于 V8 / JavaScriptCore
| 特性 | QuickJS | V8 / JSC |
|---|---|---|
| 实现语言 | 纯 C (C99) | C++ |
| 体积 | 约 600–900 KB | 20–50 MB |
| JIT 支持 | ❌ 解释执行 | ✅ |
| 启动时间 | 极快 | 较慢 |
| 内存占用 | 极低(< 5 MB) | 高(> 50 MB) |
| 嵌入复杂度 | 简单(单文件即可) | 高(需多库依赖) |
| 规范覆盖度 | 完整 ES2023 | 完整 |
| 应用场景 | 嵌入式、CLI、沙箱 | 浏览器、Node.js |
QuickJS 并非为高性能服务器设计,而是为:
- 嵌入式系统;
- 桌面工具;
- 游戏引擎脚本;
- 规则引擎 / SaaS 平台;
- 边缘计算(Edge Runtime)
提供一套安全、轻量、无外部依赖的脚本执行环境。
1.3 核心特性一览
✅ ECMAScript 支持完整
支持所有现代语法特性:
- class / async / await
- Proxy / Reflect / Symbol
- BigInt / BigFloat / BigDecimal
- 模块系统 (ESM / import/export)
- 生成器 (generator) / 异步生成器
- 结构赋值 / 模板字符串
✅ 可嵌入的 Runtime + Context 架构
QuickJS 的核心是两层结构:
| 层级 | 作用 |
|---|---|
| JSRuntime | 管理内存、GC、模块缓存、原子表 |
| JSContext | 保存执行环境、变量作用域、异常状态 |
一个 Runtime 可包含多个 Context,实现沙箱隔离。
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
JS_Eval(ctx, "print('Hello, QuickJS!')", ...);
✅ 字节码编译与独立执行
QuickJS 附带工具 qjsc:
- 将 JS 源码编译为字节码;
- 或导出为
.c文件; - 可生成独立可执行文件。
qjsc -o hello hello.js
./hello
生成的可执行文件不依赖 Node.js 或其他运行时,非常适合部署与嵌入。
✅ 高精度数值类型(扩展)
QuickJS 独有的 BigFloat / BigDecimal 扩展模块,
支持任意精度浮点计算与算术操作符重载,适合金融/科学计算。
"use math";
let x = 1.234567890123456789p;
let y = 10p;
print(x * y); // 高精度输出
✅ 内建标准模块
| 模块 | 功能 |
|---|---|
std |
文件、命令行、I/O |
os |
操作系统调用 |
big |
任意精度运算 |
worker |
多线程 worker 支持(实验性) |
这些模块在 CLI 模式中可直接启用,在嵌入模式中可自定义。
✅ 可裁剪、可定制
QuickJS 的源代码非常模块化,主要模块包括:
quickjs.c
quickjs-libc.c
libbf.c // BigFloat/BigDecimal
libregexp.c // RegExp 实现
libunicode.c // Unicode 支持
cutils.c
在嵌入式场景可通过裁剪 libbf.c 与 libunicode.c 将体积降至 400KB 以下。
1.4 QuickJS 架构总览图
flowchart TB
A["JavaScript 源代码"]
A --> B["Parser / Compiler"]
B --> C["Bytecode Generator"]
C --> D["Interpreter (VM Core)"]
D --> E["Garbage Collector"]
D --> F["JSRuntime / Context"]
F --> G["Native API / C Bindings"]
G --> H["Host Application (C/C++/Go/Rust)"]
1.5 适用场景与行业用途
| 领域 | 应用场景 |
|---|---|
| 嵌入式设备 | 配置脚本、自动化逻辑 |
| 游戏引擎 | AI、UI、剧情脚本 |
| SaaS 系统 | 动态规则、可编程策略引擎 |
| CLI 工具 | 可扩展命令行 |
| Web 平台 | WASM 内嵌执行器 |
| 教育/实验 | 学习 JS 规范与实现 |
1.6 QuickJS 的设计哲学
Bellard 在 QuickJS 文档中写道:
“Small is not about less features, but about perfect balance.”
QuickJS 的设计哲学可以总结为:
| 原则 | 说明 |
|---|---|
| 完全标准化 | 不引入非规范语义 |
| 可预测 | 不使用多线程或 JIT |
| 可移植 | 单文件 C 实现,无外部依赖 |
| 可嵌入 | 清晰 API,支持多 Context |
| 可控内存 | 手动 GC 与可插拔分配器 |
1.7 QuickJS 与 Duktape / JerryScript 对比
| 特性 | QuickJS | Duktape | JerryScript |
|---|---|---|---|
| 体积 | ~900KB | ~500KB | ~200KB |
| 语法覆盖 | 完整 ES2023 | ES5.1 | ES5 |
| 性能 | 较高 | 中等 | 较低 |
| 模块系统 | ✅ ESM | ❌ | ❌ |
| BigInt 支持 | ✅ | ❌ | ❌ |
| 异步 | ✅ Promise | ❌ | ❌ |
| C 接口复杂度 | 简洁 | 中 | 中 |
| 嵌入友好度 | 高 | 高 | 高 |
QuickJS 在语法与功能完整性上远胜同级轻量引擎。
第 2 章:QuickJS 的编译与构建
“QuickJS 是极少数可以一行命令构建、跨平台运行的现代脚本引擎。”
2.1 源码获取与结构概览
QuickJS 的源码非常整洁,通常包含以下文件:
quickjs/
├── Makefile
├── quickjs.c
├── quickjs.h
├── quickjs-libc.c
├── libbf.c / libbf.h # BigFloat / BigDecimal
├── libregexp.c / libunicode.c # RegExp & Unicode
├── cutils.c / list.h # 工具函数
├── qjs.c # 解释器 CLI
├── qjsc.c # 字节码编译器
├── run-test262.c # ECMAScript 测试
├── repl.js # REPL 示例
└── README.md
主要目标文件:
qjs:解释器qjsc:编译器libquickjs.a:静态库(嵌入项目使用)
2.2 在 Linux 上构建
QuickJS 原生支持 GNU Make:
git clone https://github.com/bellard/quickjs.git
cd quickjs
make
输出:
qjs # 命令行解释器
qjsc # 编译器
libquickjs.a
可选参数:
| 命令 | 说明 |
|---|---|
make qjs |
仅编译解释器 |
make qjsc |
仅编译编译器 |
make test |
运行 ES 测试集 |
make clean |
清理构建 |
2.3 Windows 编译方法
使用 MinGW 或 MSYS2:
pacman -S make gcc
git clone https://github.com/bellard/quickjs.git
cd quickjs
make CC=gcc
生成 qjs.exe 与 qjsc.exe。
如需使用 MSVC,可手动创建 quickjs.sln 并包含以下源文件:
quickjs.c
quickjs-libc.c
libbf.c
libregexp.c
libunicode.c
cutils.c
添加 -D_CRT_SECURE_NO_WARNINGS -D__USE_MINGW_ANSI_STDIO 等宏。
2.4 macOS 编译
macOS 自带 clang,可直接构建:
brew install gmake
gmake
可使用 brew install quickjs 安装预编译版本。
2.5 嵌入式裁剪构建
在嵌入式系统中,可按需裁剪:
# mini build
OBJS = quickjs.c cutils.c libregexp.c
CFLAGS += -DCONFIG_NO_BIGNUM
或仅保留核心 VM:
gcc -Os -fdata-sections -ffunction-sections -Wl,--gc-sections \
-DCONFIG_NO_BIGNUM quickjs.c cutils.c -o mini-qjs
最终体积可控制在 300–400KB。
2.6 跨平台交叉编译
示例:为 ARM64 构建
make CROSS_PREFIX=aarch64-linux-gnu- CC=aarch64-linux-gnu-gcc
输出:
qjs-aarch64
qjsc-aarch64
libquickjs-arm64.a
适用于树莓派、OpenWrt、路由器、嵌入式网关等。
2.7 构建可共享库(动态链接)
QuickJS 默认生成静态库,如需动态库:
gcc -shared -fPIC quickjs.c quickjs-libc.c -o libquickjs.so
嵌入时链接:
gcc main.c -L. -lquickjs -lm -o app
2.8 构建自定义配置
通过宏控制功能启用/禁用:
| 宏定义 | 含义 |
|---|---|
CONFIG_BIGNUM |
启用 BigFloat/BigDecimal |
CONFIG_VERSION |
定义版本号 |
CONFIG_LTO |
启用链接时优化 |
CONFIG_NO_LINE_NUMBERS |
去除调试信息 |
CONFIG_NO_OS_MODULE |
移除 os 模块 |
CONFIG_NO_STD_MODULE |
移除 std 模块 |
示例:
gcc -Os -DCONFIG_NO_OS_MODULE -c quickjs.c
2.9 验证构建结果
./qjs
> console.log("Hello QuickJS");
Hello QuickJS
或:
./qjsc -o test test.js
./test
输出即表明运行成功。
2.10 与项目集成的基础目录结构
在嵌入式项目中常见结构如下:
/project
├── src/
│ ├── main.c
│ └── bindings.c
├── deps/
│ └── quickjs/
│ ├── quickjs.c
│ ├── quickjs.h
│ └── libbf.c ...
├── include/
├── build/
└── Makefile
示例 Makefile:
OBJS = src/main.o src/bindings.o deps/quickjs/quickjs.o
CFLAGS = -Iinclude -Ideps/quickjs -Os
LDFLAGS = -lm
app: $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
2.11 在 CMake 工程中使用 QuickJS
add_library(quickjs STATIC
quickjs.c
quickjs-libc.c
libbf.c
libregexp.c
libunicode.c
cutils.c)
add_executable(app main.c)
target_link_libraries(app PRIVATE quickjs m)
构建:
cmake -Bbuild && cmake --build build -j
2.12 常见构建问题与解决方案
| 问题 | 原因 | 解决 |
|---|---|---|
undefined reference to pow |
未链接数学库 | 添加 -lm |
malloc_trim 未定义 |
glibc 版本过低 | 移除 malloc_trim 调用 |
| Unicode 编译错误 | 缺少 libunicode.c | 添加文件或禁用 Unicode |
| 文件过大 | 启用 -Os + 去除调试信息 |
|
| ARM 编译失败 | 未定义 endian 宏 | 添加 -D__LITTLE_ENDIAN__ |
2.13 QuickJS-NG 与官方差异
QuickJS-NG 是社区维护版本,特点:
- 更新到 ES2023;
- 支持更多系统(Windows, macOS);
- 新增模块加载器;
- 修复 GC 泄漏;
- 添加 CMake 构建支持。
安装:
git clone https://github.com/quickjs-ng/quickjs.git
cmake -Bbuild -DCONFIG_BIGNUM=ON
cmake --build build
2.14 构建总结
| 类型 | 推荐构建方式 |
|---|---|
| 通用 Linux | make |
| Windows (MSYS2) | make CC=gcc |
| macOS | gmake 或 brew install |
| 嵌入式 | -Os + CONFIG_NO_BIGNUM |
| 跨平台 | CMake |
| 动态库 | gcc -shared -fPIC |
第 3 章:QuickJS 基础 API 使用与运行时模型
“在 QuickJS 中,Runtime 是世界,Context 是宇宙的一个快照。”
3.1 Runtime / Context 基本概念
QuickJS 的执行模型分为两层:
| 层级 | 概念 | 职责 |
|---|---|---|
| JSRuntime | 全局运行时环境 | 管理内存、GC、模块缓存、字符串原子池、JobQueue |
| JSContext | 执行上下文 | 保存作用域链、变量、函数、异常、全局对象 |
每个 JSRuntime 可包含多个 JSContext,
例如,一个 SaaS 平台可以为每个租户创建独立 Context。
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
销毁:
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
3.2 Runtime 生命周期与 GC 管理
创建与销毁
JSRuntime *rt = JS_NewRuntime();
JS_SetMaxStackSize(rt, 1 << 20); // 设置栈上限(1MB)
JS_FreeRuntime(rt);
手动触发 GC
JS_RunGC(rt);
自定义内存分配器
JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque);
你可以接入自己的分配器(如内存池或统计分配)。
3.3 Context 的创建与作用
JSContext 是运行 JavaScript 的独立空间。
它持有:
- 全局对象;
- 当前模块系统;
- 执行栈;
- 异常信息。
创建
JSContext *ctx = JS_NewContext(rt);
获取全局对象
JSValue global = JS_GetGlobalObject(ctx);
执行脚本
const char *code = "let x = 3 + 4; x;";
JSValue result = JS_Eval(ctx, code, strlen(code),
"<input>", JS_EVAL_TYPE_GLOBAL);
3.4 运行 JS 脚本的多种方式
| 函数 | 说明 |
|---|---|
JS_Eval |
执行源码(字符串) |
JS_EvalFile |
执行文件内容 |
JS_EvalFunction |
执行已编译的函数 |
JS_Call |
调用 JS 函数对象 |
JS_NewCFunction |
创建 C 函数并注册 |
示例:
JSValue result = JS_Eval(ctx, "1 + 2 * 3", -1, "<eval>", JS_EVAL_TYPE_GLOBAL);
int32_t val;
JS_ToInt32(ctx, &val, result);
printf("Result: %d\n", val); // 输出 7
JS_FreeValue(ctx, result);
3.5 JSValue 类型系统
QuickJS 所有 JS 对象都用 JSValue 表示。
内部是一个带类型标签的联合体。
typedef union JSValueUnion {
int32_t int32;
double float64;
void *ptr;
#if JS_SHORT_BIG_INT_BITS == 32
int32_t short_big_int;
#else
int64_t short_big_int;
#endif
} JSValueUnion;
typedef struct JSValue {
JSValueUnion u;
int64_t tag;
} JSValue;
常用类型判定函数:
| 函数 | 判断类型 |
|---|---|
JS_IsUndefined(v) |
undefined |
JS_IsNull(v) |
null |
JS_IsBool(v) |
布尔 |
JS_IsNumber(v) |
数字 |
JS_IsString(v) |
字符串 |
JS_IsObject(v) |
对象 |
JS_IsFunction(ctx,v) |
函数 |
3.6 从 JSValue 到 C 类型的转换
| 函数 | 功能 |
|---|---|
JS_ToBool(ctx, val) |
转换为 bool |
JS_ToInt32(ctx, &dst, val) |
转换为 int |
JS_ToInt64(ctx, &dst, val) |
转换为 long |
JS_ToFloat64(ctx, &dst, val) |
转换为 double |
JS_ToCString(ctx, val) |
转换为 C 字符串 |
JS_FreeCString(ctx, str) |
释放字符串 |
示例:
JSValue result = JS_Eval(ctx, "3.14 * 2", -1, "<eval>", JS_EVAL_TYPE_GLOBAL);
double d;
JS_ToFloat64(ctx, &d, result);
printf("%f\n", d);
JS_FreeValue(ctx, result);
3.7 从 C 到 JS 的值构造
| 函数 | 构造类型 |
|---|---|
JS_NewBool(ctx, b) |
布尔 |
JS_NewInt32(ctx, n) |
整数 |
JS_NewFloat64(ctx, f) |
浮点数 |
JS_NewString(ctx, str) |
字符串 |
JS_NewObject(ctx) |
空对象 |
JS_NewArray(ctx) |
空数组 |
示例:
JSValue obj = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, "Alice"));
JS_SetPropertyStr(ctx, obj, "age", JS_NewInt32(ctx, 23));
3.8 属性访问与修改
JSValue obj = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, obj, "score", JS_NewInt32(ctx, 99));
JSValue val = JS_GetPropertyStr(ctx, obj, "score");
int32_t score;
JS_ToInt32(ctx, &score, val);
也可以使用索引:
JS_SetPropertyUint32(ctx, arr, 0, JS_NewInt32(ctx, 10));
3.9 异常处理机制
QuickJS 没有 C 层异常传播,
所有错误都返回 JS_EXCEPTION,需手动检查。
JSValue ret = JS_Eval(ctx, "throw new Error('oops')", -1, "<eval>", 0);
if (JS_IsException(ret)) {
JSValue exc = JS_GetException(ctx);
const char *msg = JS_ToCString(ctx, exc);
fprintf(stderr, "Error: %s\n", msg);
JS_FreeCString(ctx, msg);
JS_FreeValue(ctx, exc);
}
3.10 JobQueue 与 Promise 驱动
QuickJS 的事件循环由 JS_ExecutePendingJob() 负责:
JSContext *job_ctx;
while (JS_IsJobPending(rt)) {
JS_ExecutePendingJob(rt, &job_ctx);
}
可手动集成到你的宿主事件循环中,实现 async/await。
3.11 模块加载与上下文隔离
每个 JSContext 都拥有独立模块表,可实现脚本沙箱。
JSContext *sandbox = JS_NewContext(rt);
JS_Eval(sandbox, "print('isolated');", -1, "<eval>", JS_EVAL_TYPE_GLOBAL);
3.12 销毁顺序与资源回收
1️⃣ 释放所有 JSValue:
JS_FreeValue(ctx, val);
2️⃣ 释放 Context:
JS_FreeContext(ctx);
3️⃣ 最后释放 Runtime:
JS_FreeRuntime(rt);
⚠️ 注意:不要在 GC 过程中释放 Context,否则会崩溃。
第 4 章:C/C++ 与 JavaScript 的交互机制
“C 是地基,JS 是灵魂。QuickJS 的嵌入点,是这两者的桥梁。”
4.1 注册 C 函数到 JS
注册方式核心是 JS_NewCFunction + JS_SetPropertyStr。
static JSValue js_add(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
int a, b;
JS_ToInt32(ctx, &a, argv[0]);
JS_ToInt32(ctx, &b, argv[1]);
return JS_NewInt32(ctx, a + b);
}
void register_functions(JSContext *ctx)
{
JSValue global = JS_GetGlobalObject(ctx);
JS_SetPropertyStr(ctx, global, "add",
JS_NewCFunction(ctx, js_add, "add", 2));
JS_FreeValue(ctx, global);
}
运行:
console.log(add(2,3)); // 5
4.2 暴露 C 模块到 JS
可以使用模块定义器注册一整个 C 模块。
static const JSCFunctionListEntry mylib_funcs[] = {
JS_CFUNC_DEF("sum", 2, js_add),
JS_PROP_STRING_DEF("version", "1.0", 0),
JS_PROP_INT32_DEF("magic", 42, 0),
};
static int js_mylib_init(JSContext *ctx, JSModuleDef *m)
{
return JS_SetModuleExportList(ctx, m, mylib_funcs,
sizeof(mylib_funcs) / sizeof(JSCFunctionListEntry));
}
JSModuleDef *js_init_module_mylib(JSContext *ctx, const char *name)
{
JSModuleDef *m = JS_NewCModule(ctx, name, js_mylib_init);
JS_AddModuleExportList(ctx, m, mylib_funcs,
sizeof(mylib_funcs) / sizeof(JSCFunctionListEntry));
return m;
}
在 JS 端即可:
import * as mylib from 'mylib';
console.log(mylib.version, mylib.sum(3,4));
4.3 从 JS 调用 C 函数并返回对象
static JSValue js_make_player(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
JSValue obj = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, "Alice"));
JS_SetPropertyStr(ctx, obj, "hp", JS_NewInt32(ctx, 100));
return obj;
}
let p = make_player();
console.log(p.name, p.hp);
4.4 从 C 调用 JS 函数
获取函数引用
JSValue global = JS_GetGlobalObject(ctx);
JSValue func = JS_GetPropertyStr(ctx, global, "onEvent");
JSValue arg = JS_NewString(ctx, "test");
JS_Call(ctx, func, JS_UNDEFINED, 1, &arg);
JS_FreeValue(ctx, arg);
JS_FreeValue(ctx, func);
JS_FreeValue(ctx, global);
绑定回调对象
可以将 C 指针作为 opaque 绑定在 JS 对象中,用于回调时恢复 C 对象上下文。
4.5 JS <-> C 结构体映射(opaque)
QuickJS 提供 JS_SetOpaque() 与 JS_GetOpaque() 用于在对象上保存 C 指针。
typedef struct {
int id;
char name[32];
} Player;
static JSClassID player_class_id;
static void player_finalizer(JSRuntime *rt, JSValue val)
{
Player *p = JS_GetOpaque(val, player_class_id);
free(p);
}
static JSClassDef player_class = {
"Player",
.finalizer = player_finalizer,
};
void register_player_class(JSContext *ctx)
{
JS_NewClassID(&player_class_id);
JS_NewClass(JS_GetRuntime(ctx), player_class_id, &player_class);
JSValue proto = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, proto, NULL, 0);
JS_SetClassProto(ctx, player_class_id, proto);
}
创建对象:
Player *p = malloc(sizeof(Player));
p->id = 1;
strcpy(p->name, "Hero");
JSValue obj = JS_NewObjectClass(ctx, player_class_id);
JS_SetOpaque(obj, p);
从 JS 调用:
console.log(player.name); // "Hero"
4.6 处理 C 层异常
在 C 层可返回异常对象:
return JS_ThrowTypeError(ctx, "Invalid argument");
JS 层将自动捕获为异常。
4.7 在 JS 模块中注册全局方法
JSValue global = JS_GetGlobalObject(ctx);
JS_SetPropertyStr(ctx, global, "print",
JS_NewCFunction(ctx, js_print, "print", 1));
示例实现:
static JSValue js_print(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
for (int i=0; i<argc; i++) {
const char *str = JS_ToCString(ctx, argv[i]);
printf("%s ", str);
JS_FreeCString(ctx, str);
}
printf("\n");
return JS_UNDEFINED;
}
4.8 在 C++ 项目中封装 QuickJS
你可以封装一个简单的 RAII 类:
class QuickJSContext {
public:
QuickJSContext() {
rt = JS_NewRuntime();
ctx = JS_NewContext(rt);
}
~QuickJSContext() {
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
void eval(const std::string& code) {
JS_Eval(ctx, code.c_str(), code.size(), "<eval>", JS_EVAL_TYPE_GLOBAL);
}
private:
JSRuntime* rt;
JSContext* ctx;
};
4.9 通过 C 调用异步 Promise
QuickJS 的 async 函数返回 Promise 对象,可通过 C 手动执行任务:
JSValue promise = JS_Eval(ctx, "async function f(){return 42;} f()", -1, "<eval>", 0);
while (JS_IsJobPending(rt)) {
JS_ExecutePendingJob(rt, &ctx);
}
获取结果:
int64_t result;
JS_ToInt64(ctx, &result, promise);
4.10 复杂结构交互实例:注册事件系统
static JSValue js_emit(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
const char *event = JS_ToCString(ctx, argv[0]);
const char *msg = JS_ToCString(ctx, argv[1]);
printf("[Event] %s -> %s\n", event, msg);
JS_FreeCString(ctx, event);
JS_FreeCString(ctx, msg);
return JS_UNDEFINED;
}
void register_event_system(JSContext *ctx)
{
JSValue global = JS_GetGlobalObject(ctx);
JS_SetPropertyStr(ctx, global, "emit",
JS_NewCFunction(ctx, js_emit, "emit", 2));
JS_FreeValue(ctx, global);
}
使用:
emit("player.join", "Alice");
4.11 嵌入式最佳实践
| 项 | 建议 |
|---|---|
| Context | 每个独立任务/租户使用独立 Context |
| 资源回收 | 所有 JSValue 必须释放 |
| 错误处理 | 统一封装 JS_IsException |
| 异步任务 | 手动执行 JobQueue |
| 沙箱化 | 禁止导入 os、std 模块 |
| 性能 | 尽量减少跨语言调用 |
第 5 章:模块系统与字节码编译器 qjsc
“QuickJS 的模块系统既是 ECMAScript 标准的一部分,也是嵌入式系统实现脚本复用、热更新与沙箱的关键。”
5.1 QuickJS 模块体系概述
QuickJS 支持完整 ECMAScript Module(ESM)语义:
import / export语法;- 模块依赖解析;
- 模块缓存与懒加载;
- 动态导入(
import()); - 支持 C 扩展模块(
JS_NewCModule); - 可重定义模块加载器函数。
模块加载流程(简化)
sequenceDiagram
participant A as JSContext
participant B as ModuleLoader
participant C as JSRuntime
A->>B: resolve(import_path)
B-->>A: 返回源码或字节码
A->>C: instantiate module
C-->>A: 执行并缓存结果
5.2 JS 层模块示例
// math.js
export function add(a, b) { return a + b; }
export const PI = 3.14159;
// main.js
import { add, PI } from './math.js';
console.log(add(2, 3) * PI);
使用 CLI:
qjs main.js
QuickJS 的 import 语义完全与浏览器 / Node.js 一致,
但解析逻辑由宿主提供的 module_loader_func 决定。
5.3 自定义模块加载器(C 层)
默认情况下,QuickJS 在嵌入模式下不会自动解析 import。
你必须注册自己的模块加载器:
static JSModuleDef *module_loader(JSContext *ctx,
const char *module_name, void *opaque)
{
// 读取模块源码
char *buf = load_file(module_name);
JSValue func_val = JS_Eval(ctx, buf, strlen(buf),
module_name, JS_EVAL_TYPE_MODULE);
free(buf);
return JS_VALUE_GET_PTR(func_val);
}
void setup_loader(JSRuntime *rt)
{
JS_SetModuleLoaderFunc(rt, NULL, module_loader, NULL);
}
这样,你可以控制模块查找路径、缓存策略与权限。
5.4 模块缓存与热更新机制
QuickJS 会将已加载模块缓存在 Runtime 内部。 再次导入同名模块时直接返回缓存对象。
import * as math from './math.js';
console.log(math.PI);
在 C 层,可清理模块缓存:
JS_ClearModuleRegistry(rt);
实现热更新策略:
- 清理缓存;
- 重新加载模块;
- 重新执行导入。
5.5 C 模块与原生扩展
你可以直接在 C 中定义一个模块:
static JSValue js_hello(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
printf("Hello from C!\n");
return JS_UNDEFINED;
}
static const JSCFunctionListEntry mylib_funcs[] = {
JS_CFUNC_DEF("hello", 0, js_hello),
};
static int js_mylib_init(JSContext *ctx, JSModuleDef *m)
{
return JS_SetModuleExportList(ctx, m, mylib_funcs, 1);
}
JSModuleDef *js_init_module_mylib(JSContext *ctx, const char *name)
{
JSModuleDef *m = JS_NewCModule(ctx, name, js_mylib_init);
JS_AddModuleExportList(ctx, m, mylib_funcs, 1);
return m;
}
JavaScript 调用:
import * as mylib from 'mylib';
mylib.hello();
5.6 动态导入(异步 import)
QuickJS 支持动态导入:
async function load() {
const math = await import('./math.js');
console.log(math.add(2,3));
}
load();
C 层循环:
while (JS_IsJobPending(rt)) {
JS_ExecutePendingJob(rt, &ctx);
}
这允许在 SaaS 或插件系统中实现 动态脚本加载与懒执行。
5.7 字节码编译器 qjsc 基础
qjsc 是 QuickJS 的字节码编译器,可将 JS 源码转换为:
- 可执行文件;
- C 源码;
- 字节码二进制文件。
命令行用法
| 命令 | 功能 |
|---|---|
qjsc -o out.c input.js |
编译为 C 源码 |
qjsc -o app input.js |
直接生成可执行 |
qjsc -e |
启用 ESM 模式 |
qjsc -m |
强制模块输出 |
qjsc -fbignum |
启用 BigInt 支持 |
qjsc -D |
添加 debug info |
示例:
qjsc -o hello.c hello.js
gcc -O2 hello.c -lquickjs -lm -o hello
./hello
或:
qjsc -o hello hello.js
./hello
5.8 内嵌多模块编译
可以编译多个 JS 文件并自动合并为 C 源文件:
qjsc -o bundle.c main.js math.js utils.js
嵌入后,运行时可直接加载内嵌模块。
5.9 字节码文件加载
QuickJS 支持加载 .c 或 .bin 形式的预编译脚本:
extern const uint8_t qjsc_test[];
extern const uint32_t qjsc_test_size;
JSValue obj = JS_ReadObject(ctx, qjsc_test, qjsc_test_size, JS_READ_OBJ_BYTECODE);
JSValue val = JS_EvalFunction(ctx, obj);
这适合 资源受限环境,无需运行时解析源码。
5.10 模块系统嵌入架构示意
flowchart LR
A["Host App (C)"] --> B["QuickJS Runtime"]
B --> C["Context"]
C --> D["Module Loader (C)"]
D --> E["JS Module Registry"]
E --> F["Compiled Bytecode / C Module"]
5.11 实战示例:嵌入式脚本插件系统
void load_plugin(JSContext *ctx, const char *path) {
FILE *fp = fopen(path, "r");
fseek(fp, 0, SEEK_END);
size_t len = ftell(fp);
fseek(fp, 0, SEEK_SET);
char *buf = malloc(len+1);
fread(buf, 1, len, fp);
buf[len] = '\0';
JSValue v = JS_Eval(ctx, buf, len, path, JS_EVAL_TYPE_MODULE);
free(buf);
if (JS_IsException(v)) { JSValue e = JS_GetException(ctx); ... }
}
5.12 模块系统总结
| 特性 | 说明 |
|---|---|
| 模块语法 | 完整 ES Module |
| C 模块扩展 | JS_NewCModule |
| 模块缓存 | Runtime 内部维护 |
| 动态导入 | 支持 Promise 模式 |
| 自定义加载 | JS_SetModuleLoaderFunc |
| 字节码编译 | qjsc 支持生成 .c / 可执行 |
第 6 章:内存管理与 GC 调优
“GC 是 QuickJS 的心跳,它让脚本永不泄露,也可能让性能止步。”
6.1 QuickJS 的内存管理模型
QuickJS 采用 引用计数 + 标记清除 GC 结合机制:
flowchart TB
A["JSValue"] --> B["引用计数 (RC)"]
A --> C["GC 标记"]
C --> D["JS_RunGC 触发清理"]
- 引用计数:即时释放对象;
- 标记清除:周期性扫描全图。
6.2 内存分配器与可插拔机制
所有内存操作都通过 JSMallocFunctions 抽象:
typedef struct JSMallocState {
size_t malloc_count;
size_t malloc_size;
size_t malloc_limit;
void *opaque; /* user opaque */
} JSMallocState;
typedef struct JSMallocFunctions {
void *(*js_malloc)(JSMallocState *s, size_t size);
void (*js_free)(JSMallocState *s, void *ptr);
void *(*js_realloc)(JSMallocState *s, void *ptr, size_t size);
size_t (*js_malloc_usable_size)(const void *ptr);
} JSMallocFunctions;
注册:
// JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque);
JSRuntime *JS_NewRuntime2(&my_alloc_funcs, NULL);
可接入:
- jemalloc;
- 内存池;
- 自定义追踪系统(用于统计分配总量)。
6.3 垃圾回收触发条件
QuickJS 通过以下逻辑触发 GC:
- 每次分配后更新计数;
- 当
allocated_bytes > malloc_limit时触发; - 调用
JS_RunGC(rt)强制触发。
你可修改阈值:
JS_SetMemoryLimit(rt, 32 * 1024 * 1024); // 32MB
6.4 手动控制 GC
| 函数 | 功能 |
|---|---|
JS_RunGC(rt) |
立即触发 GC |
JS_MarkValue(ctx, val, mark_func) |
手动标记对象 |
JS_IsLiveObject() |
检查对象状态 |
JS_GCStats(rt) |
获取统计信息 |
6.5 内存分配监控
QuickJS 提供 js_trace_malloc 调试功能:
void *js_trace_malloc(void *opaque, size_t size)
{
printf("[alloc] %zu bytes\n", size);
return malloc(size);
}
配合 JS_NewRuntime2() 使用。
6.6 内存分布示意
| 区域 | 内容 |
|---|---|
| Heap | 对象、数组、字符串 |
| Atom Table | 全局字符串池 |
| Module Cache | 已加载模块 |
| Function Cache | 字节码函数 |
| JobQueue | 异步任务 |
| Context Stack | 当前执行栈 |
6.7 内存泄漏防御
原则 1:所有 JSValue 必须释放
JS_FreeValue(ctx, value);
原则 2:所有 C 分配的指针必须注册 Finalizer
JS_SetOpaque(obj, p);
JS_NewClass(rt, id, &(JSClassDef){.finalizer = my_free});
原则 3:Context 必须在 Runtime 之前释放
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
6.8 常见 GC 问题排查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 内存不断上升 | 未释放 JSValue | 检查局部变量 |
| 对象回收异常 | JS_SetOpaque 未注册 finalizer | 添加 class finalizer |
| 崩溃 | 释放顺序错误 | Context → Runtime 顺序 |
| 跨 Context 对象失效 | 不可混用 Context 对象 | 重新 clone |
6.9 内存调优建议
| 场景 | 策略 |
|---|---|
| 嵌入式 | 禁用 BigNum、Unicode |
| 高并发 | 多 Runtime 分片 |
| SaaS 多租户 | 每租户一个 Context |
| 长时间运行 | 定期 JS_RunGC |
| 性能关键 | 使用字节码模式 (qjsc) |
6.10 GC 性能测量
示例:监控 GC 延迟
double start = now();
JS_RunGC(rt);
printf("GC took %.3f ms\n", now() - start);
结果通常在:
- 小型脚本:0.1–0.5 ms;
- 大型脚本:2–5 ms;
- 极端情况:10ms+。
6.11 Finalizer 的使用
static void player_finalizer(JSRuntime *rt, JSValue val)
{
Player *p = JS_GetOpaque(val, player_class_id);
if (p) free(p);
}
GC 扫描到无引用对象时自动触发。 适合关闭文件、socket、释放 native 内存等场景。
6.12 多 Runtime 模式
QuickJS 不支持线程安全的并发访问。 若需多线程,应创建多个 Runtime。
void *thread(void *arg) {
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
JS_Eval(ctx, "doWork()", -1, "<eval>", 0);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
不同 Runtime 之间互不干扰,可并行执行。
6.13 内存调试工具链
QuickJS 自带调试选项:
make DEBUG=1
输出 GC/alloc 信息。
也可配合 valgrind 或 asan 检查泄漏:
valgrind ./qjs test.js
6.14 嵌入式内存策略
在嵌入式设备上建议:
- 限制字符串池大小;
- 禁用 BigFloat;
- 使用
-Os编译; - 控制堆上限;
- 每帧调用
JS_RunGC()。
6.15 小结
| 项 | QuickJS 特点 |
|---|---|
| GC 模型 | 引用计数 + 标记清除 |
| 可控性 | 高(手动/自动均可) |
| 可扩展性 | 支持自定义 allocator |
| 优化方向 | 减少跨 Context 引用 |
| 嵌入特性 | 轻量、可裁剪 |
| 安全性 | Finalizer 管理原生资源 |
第 7 章:QuickJS 多语言绑定与跨语言集成实践
“QuickJS 的真正力量,不仅在于它能嵌入 C 程序,还在于它能成为 Go、Rust、Python、C++ 等生态的脚本桥梁。”
7.1 为什么要多语言绑定
虽然 QuickJS 的官方 API 是 C 语言接口,但在现代系统中, 很多业务逻辑与主框架并非用 C 开发,例如:
- Go:用于 SaaS、服务端逻辑;
- Rust:用于高性能、安全型系统;
- Python:用于 AI 工具或脚本自动化;
- C++:用于游戏引擎、桌面应用。
为了在这些语言中使用 QuickJS,需要通过 FFI(Foreign Function Interface) 或 binding 层 实现调用。
7.2 QuickJS 的 FFI 原理
QuickJS 的 FFI 逻辑通常遵循三层结构:
flowchart TB
A["主语言(Go/Rust/Python)"] --> B["FFI 绑定层(CGo/Rust FFI/PyBind11)"]
B --> C["QuickJS C API (libquickjs.a)"]
C --> D["JS Runtime + Context"]
这样即可从高层语言调用 QuickJS 运行 JS 脚本,或在 JS 中调用主语言函数。
7.3 Go 与 QuickJS 集成
7.3.1 绑定方案概述
Go 调用 QuickJS 通常有三种方案:
| 方案 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| CGo 调用官方 C API | 直接封装 QuickJS | 性能高、稳定 | 编译复杂、跨平台困难 |
现有封装库 github.com/quickjs-go/quickjs |
使用现成绑定 | 快速上手 | 功能略少 |
| 通过插件 (c-shared) 模式 | 编译为 .so | 与 Go 模块解耦 | 通信开销较大 |
7.3.2 Go 示例:执行 JS 代码
package main
/*
#cgo CFLAGS: -I./quickjs
#cgo LDFLAGS: -L./quickjs -lquickjs -lm
#include "quickjs.h"
*/
import "C"
import "unsafe"
func main() {
rt := C.JS_NewRuntime()
ctx := C.JS_NewContext(rt)
code := C.CString(`let x = 5 * 10; x;`)
defer C.free(unsafe.Pointer(code))
res := C.JS_Eval(ctx, code, C.strlen(code), C.CString("<eval>"), C.JS_EVAL_TYPE_GLOBAL)
var val C.int32_t
C.JS_ToInt32(ctx, &val, res)
println("Result:", int(val))
C.JS_FreeValue(ctx, res)
C.JS_FreeContext(ctx)
C.JS_FreeRuntime(rt)
}
输出:
Result: 50
7.3.3 Go 层包装接口示例
type QuickJS struct {
rt *C.JSRuntime
ctx *C.JSContext
}
func NewQuickJS() *QuickJS {
return &QuickJS{
rt: C.JS_NewRuntime(),
ctx: C.JS_NewContext(C.JS_NewRuntime()),
}
}
func (q *QuickJS) Eval(code string) string {
cCode := C.CString(code)
defer C.free(unsafe.Pointer(cCode))
res := C.JS_Eval(q.ctx, cCode, C.strlen(cCode), C.CString("<go>"), C.JS_EVAL_TYPE_GLOBAL)
cStr := C.JS_ToCString(q.ctx, res)
defer C.JS_FreeCString(q.ctx, cStr)
return C.GoString(cStr)
}
7.4 Rust 与 QuickJS 集成
Rust 生态下已有多个成熟绑定项目:
| 库 | 地址 | 特点 |
|---|---|---|
rquickjs |
https://github.com/theduke/rquickjs | 高层封装、支持 async、安全抽象 |
quickjs-rs |
https://github.com/quickjs-rs/quickjs-rs | 轻量绑定、接近 C API |
boa |
Rust 实现 JS 引擎 | 非 QuickJS,但理念相似 |
7.4.1 使用 rquickjs 执行脚本
use rquickjs::{Runtime, Context, Value};
fn main() -> rquickjs::Result<()> {
let rt = Runtime::new()?;
let ctx = Context::full(&rt)?;
ctx.eval::<Value, _>("let x = 6 * 7; x")?;
let result: i32 = ctx.eval("x")?;
println!("Result = {}", result);
Ok(())
}
输出:
Result = 42
7.4.2 注册 Rust 函数到 JS
use rquickjs::{Context, Runtime, function::Func};
fn greet(name: String) -> String {
format!("Hello, {}", name)
}
fn main() -> rquickjs::Result<()> {
let rt = Runtime::new()?;
let ctx = Context::full(&rt)?;
ctx.globals().set("greet", Func::from(greet))?;
ctx.eval::<(), _>("console.log(greet('World'))")?;
Ok(())
}
输出:
Hello, World
7.4.3 Rust 异步任务集成
rquickjs 允许直接使用 async:
ctx.with(|ctx| async move {
let code = "await Promise.resolve(123)";
let result: i32 = ctx.eval(code).await?;
println!("Async result: {}", result);
Ok(())
});
7.5 Python 与 QuickJS 集成
Python 下有现成封装库:
| 库 | 功能 |
|---|---|
pyquickjs |
调用 QuickJS C API |
quickjs(pypi) |
内置轻量封装 |
7.5.1 安装与使用
pip install quickjs
示例:
import quickjs
ctx = quickjs.Context()
result = ctx.eval("2 ** 8")
print(result)
输出:
256
7.5.2 Python 调用 JS 函数
ctx = quickjs.Context()
ctx.add_callable("py_print", lambda msg: print(f"[JS->Python] {msg}"))
ctx.eval("""
py_print('Hello from JS')
""")
7.5.3 JS 调用 Python 模块(扩展)
通过 add_callable 可将 Python 函数注册进 JS 全局对象,实现双向通信。
7.6 多语言绑定的统一设计建议
| 原则 | 说明 |
|---|---|
| 保持宿主语言对象生命周期独立 | QuickJS 不自动管理主语言资源 |
| 所有 JSValue 必须显式释放 | 避免内存泄漏 |
| 跨线程隔离 Runtime | 不共享 Context |
| 统一接口层 | 为多语言封装一套中间层 C API |
7.7 Go/Rust 混合架构图
flowchart TB
A["Go 服务层"] --> B["QuickJS Wrapper (CGo)"]
B --> C["libquickjs.a"]
C --> D["JS Context"]
D --> E["User Scripts"]
A --> F["Rust 模块"]
F --> B
7.8 多语言绑定性能评估
| 场景 | QuickJS 原生 | Go 绑定 | Rust 绑定 | Python 绑定 |
|---|---|---|---|---|
| 单次脚本执行 | 1.0x | 1.3x | 1.1x | 1.5x |
| 函数回调 | 1.0x | 1.2x | 1.05x | 1.6x |
| 跨语言通信 | — | 较快 | 快 | 较慢 |
| 内存占用 | 低 | 中 | 中 | 高 |
7.9 小结
- QuickJS 具备高度可移植性,可在几乎所有语言生态中使用;
- Go/Rust 是最常见宿主;
- 通过 FFI 可在游戏引擎、云函数、边缘服务中嵌入执行逻辑。
第 8 章:QuickJS 在嵌入式与 IoT 场景中的应用
“在内存只有 8MB 的设备上,运行完整 ES2023 JavaScript,不再是梦想。”
8.1 为什么 QuickJS 适合嵌入式
| 特点 | 优势说明 |
|---|---|
| 纯 C 实现 | 无第三方依赖,方便移植 |
| 体积小 | 几百 KB 即可运行 |
| 可裁剪 | 模块化结构,可禁用 BigNum、Unicode |
| 快速启动 | 100ms 内启动 |
| 内存可控 | 可设定上限 |
| 安全沙箱 | 可完全封闭 OS 调用 |
这些特性让 QuickJS 在 IoT、边缘计算、嵌入式终端中表现出色。
8.2 嵌入式系统中的典型架构
flowchart LR
A["设备固件 (C)"] --> B["QuickJS Runtime"]
B --> C["配置脚本 / 控制逻辑"]
C --> D["传感器 / 通信驱动"]
QuickJS 作为“脚本层”,解耦了固件逻辑与可变控制。
8.3 构建极简 QuickJS 内核
最小构建配置:
gcc -Os quickjs.c cutils.c libregexp.c -DCONFIG_NO_BIGNUM -o mini-qjs
体积仅约 350 KB。
禁用项:
- BigFloat / Decimal;
- Unicode;
- OS / std 模块。
8.4 在 OpenWrt 上运行 QuickJS
1️⃣ 安装交叉编译工具:
opkg install gcc make
2️⃣ 编译:
make CROSS_PREFIX=mipsel-openwrt-linux- CC=mipsel-openwrt-linux-gcc
3️⃣ 上传 qjs 至 /usr/bin
执行:
qjs -e "print('IoT Ready!')"
8.5 在 ESP32 / STM32 上集成 QuickJS
QuickJS 可裁剪为 “Micro QuickJS”:
- 编译时使用
-DCONFIG_STACK_CHECK -DCONFIG_NO_BIGNUM; - 限制栈大小 256KB;
- 关闭文件系统支持;
- 使用
malloc替换 RTOS 内存池。
示例:控制设备 LED
function blink(times) {
for (let i = 0; i < times; i++) {
gpio.write(LED_PIN, 1);
sleep(500);
gpio.write(LED_PIN, 0);
sleep(500);
}
}
blink(3);
嵌入式系统中通过注册 gpio.write C 函数即可。
8.6 在路由器 / 边缘节点中的使用
QuickJS 常用于:
- 动态规则(ACL、防火墙脚本);
- 数据采集规则引擎;
- 设备远程控制逻辑。
示例:
if (packet.src == "192.168.1.10") drop();
8.7 性能与功耗对比(IoT 实测)
| 项目 | QuickJS | Lua | Python (MicroPython) |
|---|---|---|---|
| 二进制体积 | ~400 KB | ~300 KB | ~600 KB |
| 内存占用 | 2–5 MB | 1–2 MB | 4–8 MB |
| 启动时间 | 80–150 ms | 50–100 ms | 200–400 ms |
| 功耗影响 | 低 | 极低 | 高 |
| 可读性 | JS 标准 | Lua 特有 | Python 风格 |
QuickJS 在兼顾可读性与标准化方面更优。
8.8 嵌入式沙箱与权限控制
在 IoT 场景,必须禁止危险模块:
JS_SetModuleLoaderFunc(rt, NULL, safe_loader, NULL);
JS_SetMemoryLimit(rt, 8 * 1024 * 1024);
JS_SetMaxStackSize(rt, 1 << 17);
只暴露安全 API,例如:
register_function(ctx, "readTemp", js_read_temp);
register_function(ctx, "setRelay", js_set_relay);
8.9 热更新与远程脚本执行
QuickJS 可从服务器下载并执行脚本:
download_js("/tmp/update.js");
JS_EvalFile(ctx, "/tmp/update.js", JS_EVAL_TYPE_GLOBAL);
通过签名校验与沙箱机制防止恶意脚本攻击。
8.10 案例:工业控制脚本引擎
结构:
main.c
├── quickjs/
├── scripts/
│ ├── rules.js
│ └── sensors.js
└── build.sh
运行逻辑:
- 初始化硬件;
- 启动 QuickJS;
- 执行 JS 控制脚本;
- 定期运行 GC;
- 接收远程脚本更新。
8.11 案例:智能家居设备逻辑
if (motion.detected() && !light.on()) {
light.on();
setTimeout(() => light.off(), 30000);
}
这种脚本可由用户在线配置,设备端执行。 QuickJS 提供了高可维护性与灵活性。
8.12 嵌入式优化清单
| 优化项 | 建议 |
|---|---|
| 编译优化 | -Os -ffunction-sections -fdata-sections |
| 禁用模块 | CONFIG_NO_BIGNUM、CONFIG_NO_STD_MODULE |
| 减少内存 | 降低堆限制、关闭 Unicode |
| 启动优化 | 预加载字节码 |
| 热更新 | 使用签名校验 |
| 稳定性 | 定期 GC、异常捕获 |
8.13 QuickJS 与 Lua 在 IoT 场景的对比
| 指标 | QuickJS | Lua |
|---|---|---|
| 语法 | ES 标准 | Lua 特有 |
| 可读性 | 高 | 中 |
| 生态 | 前端丰富 | 游戏嵌入成熟 |
| 模块化 | ESM | require |
| 异步 | Promise | coroutine |
| 工业可维护性 | 更优 | 稳定 |
| 教育易用性 | 强 | 强 |
| 热更新 | 支持 | 支持 |
8.14 嵌入式架构拓扑示意
flowchart TB
A["设备主控 (MCU)"] --> B["QuickJS VM"]
B --> C["本地 JS 脚本"]
B --> D["云端脚本下发"]
B --> E["设备驱动层"]
E --> F["传感器/执行器"]
D --> B
8.15 小结
- QuickJS 可在 32 位、64 位平台高效运行;
- 在 IoT、路由器、智能设备中已被广泛采用;
- 通过裁剪与沙箱化,可实现安全、稳定、低功耗脚本系统;
- 与 Lua 相比,QuickJS 拥有更高通用性与可扩展性。
第 9 章:QuickJS 在 SaaS 与工具系统中的应用实践
“QuickJS 是 SaaS 系统中最轻量的‘内嵌智能层’,它让你的用户编写逻辑,而你保持可控。”
9.1 SaaS 场景下的脚本化需求
现代 SaaS 系统广泛采用“可编程逻辑”以增强可定制性:
| 需求场景 | 说明 |
|---|---|
| 表单系统 | 用户自定义计算逻辑(如税率、积分) |
| 工作流引擎 | 条件判断与动态任务分支 |
| 报表工具 | 用户自定义聚合与转换 |
| 游戏或广告系统 | 策略引擎与事件脚本 |
| AI 应用 | Prompt 动态拼装与执行控制 |
QuickJS 以“纯 C 实现 + 高兼容性 + 沙箱控制”成为 SaaS 系统内最常用的可控脚本执行引擎。
9.2 QuickJS 在云端的运行模型
SaaS 集成架构示意
flowchart LR
A["Web/Client"] --> B["API Server"]
B --> C["Script Engine Layer (QuickJS)"]
C --> D["Business Logic Hooks"]
D --> E["DB / Cache / Message Bus"]
SaaS 系统中通常:
- 每个租户或规则实例创建独立
JSContext; JSRuntime共用;- 执行完成后清理上下文;
- 通过 C 层注册安全 API 与资源限制。
9.3 示例:表单计算引擎
// 用户配置脚本
function calc(data) {
if (data.type === "vip") {
return data.price * 0.9;
}
return data.price;
}
嵌入运行(Go / C):
JSValue fn = JS_Eval(ctx, script, strlen(script), "<form>", JS_EVAL_TYPE_GLOBAL);
JSValue arg = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, arg, "price", JS_NewFloat64(ctx, 100.0));
JS_SetPropertyStr(ctx, arg, "type", JS_NewString(ctx, "vip"));
JSValue result = JS_Call(ctx, fn, JS_UNDEFINED, 1, &arg);
double val; JS_ToFloat64(ctx, &val, result);
printf("Result: %.2f\n", val);
输出:
Result: 90.00
9.4 示例:策略与风控规则引擎
function shouldBlock(user) {
if (user.age < 18 || user.region === "blacklist") return true;
if (user.score < 50) return true;
return false;
}
宿主系统:
if (JS_EvalFunction(ctx, rule_func, arg) == JS_TRUE)
block_request(user);
这种模式非常适合“动态风控 / 广告投放 / 游戏 AI 行为脚本”。
9.5 QuickJS + Redis + HTTP 桥接
QuickJS 可结合 Redis 与 HTTP 形成规则集执行框架。
flowchart TB
A["HTTP 事件"] --> B["QuickJS Runtime"]
B --> C["脚本规则执行"]
C --> D["Redis 存储"]
C --> E["HTTP 回调"]
注册 C 函数:
register_function(ctx, "httpPost", js_http_post);
register_function(ctx, "redisSet", js_redis_set);
用户脚本:
redisSet("user:1", "active");
httpPost("/notify", { id: 1 });
9.6 低代码与自动化平台中的 QuickJS
QuickJS 可以作为“逻辑执行节点”,支持可视化编排系统执行用户逻辑:
graph LR
A["拖拽式节点编辑器"] --> B["JS 脚本节点"]
B --> C["QuickJS Sandbox"]
C --> D["执行结果 / 输出"]
代码示例:
function onNode(input) {
return input.x + input.y;
}
在 C 层注册:
JS_Eval(ctx, script, strlen(script), "<node>", JS_EVAL_TYPE_GLOBAL);
9.7 QuickJS 与 WebAssembly (WASM) 集成
QuickJS 可以与 WASM 模块混合运行,实现跨语言逻辑层。
flowchart TB
A["QuickJS Runtime"] --> B["WASM 模块调用"]
B --> C["C 函数导出"]
A --> D["JS 逻辑控制"]
这为 SaaS 提供了:
- JS 的灵活性;
- WASM 的性能;
- C 层的安全沙箱。
9.8 QuickJS 在 AI 与 Prompt 平台中的应用
例如在 AI 工具链中动态生成请求参数:
function buildPrompt(user, input) {
return `${user.name} said: ${input.toUpperCase()}`;
}
QuickJS 可在云端执行用户定义脚本,安全拼装参数,防止注入风险。
9.9 多租户隔离方案
| 级别 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| Context 隔离 | 每租户一个 Context | 快速、轻量 | 内存共享有限 |
| Runtime 隔离 | 每租户独立 Runtime | 安全、强隔离 | 内存消耗高 |
| 进程隔离 | 每租户独立进程 | 最高安全性 | 启动慢 |
推荐模式:
SaaS 小规模:
多 Context; 高安全系统:多 Runtime; 云原生方案:容器级隔离。
9.10 SaaS 性能与稳定性优化策略
| 优化点 | 建议 |
|---|---|
| 脚本编译缓存 | JS_ReadObject / JS_WriteObject |
| JobQueue 循环 | 统一事件循环驱动 |
| Context 复用 | 避免频繁创建销毁 |
| GC 调度 | 周期性回收 |
| 异常隔离 | 每 Context 捕获异常 |
| 指令限流 | 自定义 instruction_limit |
| 沙箱注入 | 禁止导入 os、std 模块 |
9.11 SaaS 集成架构(详细)
flowchart TB
A["HTTP Gateway"]
B["Script Dispatcher"]
C["QuickJS Sandbox Pool"]
D["Result Aggregator"]
E["DB / Cache"]
A --> B --> C --> D --> E
C -->|GC| C
B -->|Load Balance| C
9.12 QuickJS 的业务可扩展模式
- 插件化架构:每个插件一个 JS 模块;
- 热更新机制:通过字节码更新逻辑;
- 审计系统:记录每次脚本执行日志;
- 安全策略:沙箱封闭环境;
- 统一配置中心:存储脚本版本与依赖。
9.13 小结
QuickJS 在 SaaS 场景中可实现:
- 动态逻辑;
- 用户可编程;
- 沙箱安全;
- 热更新与日志追踪;
- 高度可扩展的执行层。
第 10 章:性能优化与安全沙箱设计
10.1 性能调优总览
QuickJS 的性能取决于以下因素:
| 因素 | 影响 | 优化方向 |
|---|---|---|
| 字节码编译 | 启动速度 | 使用 qjsc 预编译 |
| 跨语言调用 | 函数开销 | 合并调用逻辑 |
| GC 周期 | 内存抖动 | 调整频率 |
| 字符串池 | 内存利用 | 复用 Atom |
| Context 数量 | 并行能力 | 控制池大小 |
10.2 脚本预编译与缓存
JSValue obj = JS_ReadObject(ctx, buffer, len, JS_READ_OBJ_BYTECODE);
JSValue val = JS_EvalFunction(ctx, obj);
将脚本编译为字节码后持久化存储,大幅减少启动开销。
10.3 GC 调度优化
建议采用:
- 定时触发(如每 10 秒一次);
- 内存阈值触发;
- 手动触发关键节点。
示例:
if (used_memory > 20*1024*1024) JS_RunGC(rt);
10.4 JobQueue 调度优化
为异步任务建立统一执行循环:
while (JS_IsJobPending(rt)) {
JS_ExecutePendingJob(rt, &ctx);
}
可集成到 epoll 或主循环中,提升 async 任务吞吐。
10.5 跨语言通信优化
- 减少函数调用频率;
- 批量传输数据(如数组或 JSON);
- 使用 C 层缓冲区复用。
示例:
JSValue json = JS_Eval(ctx, "JSON.stringify(obj)", -1, "<eval>", 0);
10.6 沙箱安全体系
QuickJS 本身不会访问系统资源,但默认模块(如 os, std)存在安全隐患。
禁用标准模块
#define CONFIG_NO_OS_MODULE
#define CONFIG_NO_STD_MODULE
封闭模块加载器
JS_SetModuleLoaderFunc(rt, NULL, safe_loader, NULL);
限制内存与栈
JS_SetMemoryLimit(rt, 32 * 1024 * 1024);
JS_SetMaxStackSize(rt, 1 << 18);
禁止无限循环
JS_SetInterruptHandler(rt, interrupt_cb, NULL);
示例:
static int interrupt_cb(JSRuntime *rt, void *opaque) {
return should_stop() ? 1 : 0;
}
10.7 沙箱隔离层架构
flowchart TB
A["Host Process"]
B["QuickJS Runtime"]
C["Context 1"]
D["Context 2"]
E["API Proxy Layer"]
F["Safe C Modules"]
A --> B --> C
B --> D
C --> E --> F
10.8 安全白名单策略
| 层级 | 控制目标 | 实现方式 |
|---|---|---|
| 模块层 | 禁止导入 | 自定义 loader |
| 函数层 | 仅允许安全 API | 注册白名单 |
| 数据层 | 禁止访问敏感数据 | 参数过滤 |
| 执行层 | 防止死循环 | 中断回调 |
| 内存层 | 限制堆栈 | 运行时约束 |
10.9 审计与日志系统
QuickJS 可通过:
- 记录每次执行脚本;
- 统计执行时间;
- 记录异常堆栈;
- 输出 GC 统计。
printf("[QJS] Script took %.2fms\n", exec_time);
或:
try {
runTask();
} catch (e) {
log("error", e.stack);
}
10.10 防御策略总结
| 风险 | 解决方式 |
|---|---|
| 无限循环 | 中断回调 |
| 内存暴涨 | 内存上限 |
| 非法模块 | 禁止导入 |
| 文件访问 | 移除 os 模块 |
| 恶意脚本 | 签名校验 |
| 数据泄露 | 沙箱 API 白名单 |
| 调用阻塞 | JobQueue 管理 |
10.11 性能与安全平衡
QuickJS 的设计理念:
“默认安全,按需开放。”
建议策略:
- 默认禁用所有外部模块;
- 按需注册 API;
- 对脚本执行设置时间/内存/栈限制;
- 所有异常记录与上报。
10.12 监控与指标体系
| 指标 | 描述 |
|---|---|
ctx_count |
当前上下文数量 |
mem_usage |
运行时内存占用 |
gc_time |
每次 GC 延迟 |
exec_time |
脚本执行时间 |
job_pending |
异步任务数量 |
可导出至 Prometheus / Grafana。
10.13 工业级部署架构
flowchart LR
A["Load Balancer"]
B["Worker Pool"]
C["QuickJS Runtime Pool"]
D["Context Manager"]
E["Sandbox Monitor"]
F["Persistent Storage"]
A --> B --> C --> D --> F
D --> E
E --> C
特点:
- 可水平扩展;
- 可监控;
- 可快速回收;
- 安全隔离。
10.14 未来趋势:QuickJS + WASM + AI
结合 WebAssembly 与 AI 推理框架,QuickJS 正逐渐演变为:
- 云端轻量执行层;
- 边缘智能脚本层;
- 低功耗可控逻辑引擎。
应用场景包括:
- AI 规则解释器;
- 云函数运行时;
- 智能控制脚本;
- 自动化工作流引擎。
10.15 小结
| 维度 | 优化要点 |
|---|---|
| 性能 | 预编译、Context 复用、GC 调度 |
| 安全 | 沙箱隔离、白名单、资源限制 |
| 可维护性 | 模块化、可观测、热更新 |
| 可扩展性 | 多语言绑定、容器化运行 |
| 未来方向 | 与 WASM / AI 融合 |
全书总结
QuickJS 的价值在于:嵌入容易;性能高效;标准兼容;安全可控;可跨语言生态集成。
它既是 轻量引擎,也是 脚本操作系统的基石。
从 IoT 到 SaaS,从游戏到云函数,QuickJS 已成为连接 嵌入式世界与云端智能 的关键桥梁。