QuickJS 嵌入式 JavaScript 引擎完全教程 / 10 - 最佳实践
最佳实践
10.1 沙箱安全
原则:最小权限
// sandbox_checklist.c — 沙箱安全最佳实践清单
// ✅ 1. 不注册 std/os 模块
// js_init_module_std(ctx, "std"); // ← 不要
// js_init_module_os(ctx, "os"); // ← 不要
// ✅ 2. 移除所有危险的全局 API
static void lock_down_globals(JSContext *ctx) {
JSValue global = JS_GetGlobalObject(ctx);
// 移除直接代码执行能力
JS_DeletePropertyStr(ctx, global, "eval", 0);
JS_DeletePropertyStr(ctx, global, "Function", 0);
// 移除脚本加载
JS_DeletePropertyStr(ctx, global, "__loadScript", 0);
JS_DeletePropertyStr(ctx, global, "print", 0);
JS_DeletePropertyStr(ctx, global, "scriptArgs", 0);
// 移除 Worker(防止创建新的执行上下文)
JS_DeletePropertyStr(ctx, global, "Worker", 0);
JS_FreeValue(ctx, global);
}
// ✅ 3. 设置资源上限
void set_resource_limits(JSRuntime *rt) {
// 内存限制(根据场景调整)
JS_SetMemoryLimit(rt, 16 * 1024 * 1024); // 16MB
// 栈大小限制
JS_SetMaxStackSize(rt, 256 * 1024); // 256KB
}
// ✅ 4. 设置执行超时
void set_execution_timeout(JSRuntime *rt, double seconds) {
typedef struct {
clock_t start;
double limit;
} TimeoutCtx;
static TimeoutCtx ctx;
ctx.start = clock();
ctx.limit = seconds;
JS_SetInterruptHandler(rt, [](JSRuntime *rt, void *opaque) {
TimeoutCtx *tc = (TimeoutCtx *)opaque;
double elapsed = (double)(clock() - tc->start) / CLOCKS_PER_SEC;
return elapsed > tc->limit ? 1 : 0;
}, &ctx);
}
// ✅ 5. 验证输入(永远不要直接执行用户代码)
JSValue safe_execute(JSContext *ctx, const char *user_code) {
// 检查代码长度
if (strlen(user_code) > 10000) {
return JS_ThrowInternalError(ctx, "Code too long");
}
// 检查危险模式
const char *dangerous[] = {
"import", "__proto__", "constructor",
"prototype", "arguments", "caller",
NULL
};
for (const char **d = dangerous; *d; d++) {
if (strstr(user_code, *d)) {
return JS_ThrowSecurityError(ctx,
"Forbidden keyword: %s", *d);
}
}
// 执行代码
return JS_Eval(ctx, user_code, strlen(user_code),
"<sandbox>", 0);
}
沙箱安全等级
| 等级 | 场景 | 措施 |
|---|
| L1 - 基础 | 内部可信脚本 | 仅设置内存/栈限制 |
| L2 - 标准 | 第三方插件 | 移除 std/os,设置超时 |
| L3 - 严格 | 不受信任的用户代码 | 全部移除,输入验证,审计日志 |
| L4 - 最高 | 金融/安全领域 | 多进程隔离 + seccomp + 独立用户 |
10.2 嵌入式应用
资源受限环境的配置
// embedded_config.c — 嵌入式设备的 QuickJS 配置
#include "quickjs-libc.h"
// 针对不同硬件的推荐配置
typedef struct {
size_t ram_total; // 设备总 RAM
size_t js_memory_limit; // JS 内存限制
size_t js_stack_size; // JS 栈大小
double js_timeout; // 执行超时(秒)
} EmbeddedConfig;
static const EmbeddedConfig configs[] = {
// STM32F4 (192KB RAM)
{ 192 * 1024, 32 * 1024, 8 * 1024, 0.5 },
// ESP32 (520KB RAM)
{ 520 * 1024, 128 * 1024, 32 * 1024, 2.0 },
// Raspberry Pi Pico (264KB RAM)
{ 264 * 1024, 64 * 1024, 16 * 1024, 1.0 },
// Raspberry Pi Zero (512MB RAM)
{ 512 * 1024 * 1024, 16 * 1024 * 1024, 256 * 1024, 10.0 },
// Linux 嵌入式网关 (256MB RAM)
{ 256 * 1024 * 1024, 32 * 1024 * 1024, 512 * 1024, 5.0 },
};
const EmbeddedConfig* get_config_for_device(size_t ram) {
for (int i = 0; i < sizeof(configs)/sizeof(configs[0]); i++) {
if (ram <= configs[i].ram_total * 2) {
return &configs[i];
}
}
return &configs[sizeof(configs)/sizeof(configs[0]) - 1];
}
预编译字节码部署
#!/bin/bash
# deploy_embedded.sh — 嵌入式设备的字节码部署流程
# 1. 在开发机上编译字节码
./qjsc -c -o script_bytecode.h \
-m config.js \
-m sensor.js \
-m logic.js \
main.js
# 2. 编译为静态链接的可执行文件
arm-none-eabi-gcc -O2 -static \
-I/path/to/quickjs \
main.c \
quickjs.c quickjs-libc.c cutils.c \
libregexp.c libunicode.c \
-lm -o firmware_app
# 3. 烧录到设备
# flash firmware_app
嵌入式初始化模式
// embedded_init.c — 嵌入式设备的初始化模式
#include "quickjs-libc.h"
// 预编译的配置字节码
// #include "config_bytecode.h"
typedef struct {
JSRuntime *rt;
JSContext *ctx;
JSValue main_func;
} JSEngine;
// 初始化 JS 引擎(设备启动时调用一次)
JSEngine* js_engine_init(void) {
JSEngine *engine = malloc(sizeof(JSEngine));
engine->rt = JS_NewRuntime();
JS_SetMemoryLimit(engine->rt, 64 * 1024); // 64KB
JS_SetMaxStackSize(engine->rt, 8 * 1024); // 8KB
engine->ctx = JS_NewContext(engine->rt);
// 注册硬件抽象层 API
register_hal_api(engine->ctx);
// 从字节码加载配置脚本
/*
JSValue obj = JS_ReadObject(engine->ctx,
config_bytecode, config_bytecode_size,
JS_READ_OBJ_BYTECODE);
JS_EvalFunction(engine->ctx, obj);
*/
// 缓存主处理函数
JSValue global = JS_GetGlobalObject(engine->ctx);
engine->main_func = JS_GetPropertyStr(engine->ctx,
global, "processData");
JS_FreeValue(engine->ctx, global);
return engine;
}
// 处理传感器数据(循环调用)
int js_engine_process(JSEngine *engine, float *data, int len) {
// 创建 JS 数组
JSValue arr = JS_NewArray(engine->ctx);
for (int i = 0; i < len; i++) {
JS_SetPropertyUint32(engine->ctx, arr, i,
JS_NewFloat64(engine->ctx, data[i]));
}
// 调用 JS 处理函数
JSValue result = JS_Call(engine->ctx, engine->main_func,
JS_UNDEFINED, 1, &arr);
int ret = 0;
if (!JS_IsException(result)) {
JS_ToInt32(engine->ctx, &ret, result);
}
JS_FreeValue(engine->ctx, result);
JS_FreeValue(engine->ctx, arr);
return ret;
}
// 关闭引擎(设备关机时调用)
void js_engine_cleanup(JSEngine *engine) {
JS_FreeValue(engine->ctx, engine->main_func);
JS_FreeContext(engine->ctx);
JS_FreeRuntime(engine->rt);
free(engine);
}
10.3 脚本化架构
插件系统设计
// plugin_system.c — 基于 QuickJS 的插件系统
#include "quickjs-libc.h"
#include <stdio.h>
#include <string.h>
#include <dirent.h>
typedef struct Plugin {
char name[64];
JSValue module;
JSValue on_init;
JSValue on_event;
JSValue on_cleanup;
} Plugin;
typedef struct PluginSystem {
JSRuntime *rt;
JSContext *ctx;
Plugin plugins[32];
int plugin_count;
} PluginSystem;
PluginSystem* plugin_system_create(void) {
PluginSystem *ps = calloc(1, sizeof(PluginSystem));
ps->rt = JS_NewRuntime();
JS_SetMemoryLimit(ps->rt, 32 * 1024 * 1024);
ps->ctx = JS_NewContext(ps->rt);
// 注册插件 API
register_plugin_api(ps->ctx);
return ps;
}
int plugin_system_load(PluginSystem *ps, const char *path) {
// 加载插件模块
JSValue result = JS_Eval(ps->ctx,
"import * as plugin from \"$PATH\";",
100, path, JS_EVAL_TYPE_MODULE);
if (JS_IsException(result)) {
// 记录错误,继续加载其他插件
JSValue ex = JS_GetException(ps->ctx);
const char *msg = JS_ToCString(ps->ctx, ex);
fprintf(stderr, "Failed to load %s: %s\n", path, msg);
JS_FreeCString(ps->ctx, msg);
JS_FreeValue(ps->ctx, ex);
JS_FreeValue(ps->ctx, result);
return -1;
}
Plugin *p = &ps->plugins[ps->plugin_count++];
strncpy(p->name, path, sizeof(p->name) - 1);
p->module = result;
// 获取生命周期钩子
p->on_init = JS_GetPropertyStr(ps->ctx, result, "onInit");
p->on_event = JS_GetPropertyStr(ps->ctx, result, "onEvent");
p->on_cleanup = JS_GetPropertyStr(ps->ctx, result, "onCleanup");
return 0;
}
void plugin_system_init_all(PluginSystem *ps) {
for (int i = 0; i < ps->plugin_count; i++) {
Plugin *p = &ps->plugins[i];
if (JS_IsFunction(ps->ctx, p->on_init)) {
JSValue result = JS_Call(ps->ctx, p->on_init,
p->module, 0, NULL);
JS_FreeValue(ps->ctx, result);
}
}
}
void plugin_system_emit(PluginSystem *ps, const char *event,
JSValue data) {
JSValue event_name = JS_NewString(ps->ctx, event);
for (int i = 0; i < ps->plugin_count; i++) {
Plugin *p = &ps->plugins[i];
if (JS_IsFunction(ps->ctx, p->on_event)) {
JSValue argv[] = { event_name, data };
JSValue result = JS_Call(ps->ctx, p->on_event,
p->module, 2, argv);
JS_FreeValue(ps->ctx, result);
}
}
JS_FreeValue(ps->ctx, event_name);
}
配置管理
// config_manager.js — 可脚本化的配置管理
export default class ConfigManager {
#config = {};
#watchers = new Map();
load(source) {
// 支持 JS 配置(比 JSON 更强大)
const config = eval(`(${source})`);
Object.assign(this.#config, config);
this.#notify("*", config);
}
get(path, defaultValue) {
const keys = path.split(".");
let value = this.#config;
for (const key of keys) {
if (value == null) return defaultValue;
value = value[key];
}
return value ?? defaultValue;
}
set(path, value) {
const keys = path.split(".");
let target = this.#config;
for (let i = 0; i < keys.length - 1; i++) {
target[keys[i]] ??= {};
target = target[keys[i]];
}
target[keys[keys.length - 1]] = value;
this.#notify(path, value);
}
watch(path, callback) {
(this.#watchers.get(path) ?? this.#watchers.set(path, []).get(path))
.push(callback);
}
#notify(path, value) {
for (const [watchPath, callbacks] of this.#watchers) {
if (path === "*" || path.startsWith(watchPath)) {
callbacks.forEach(cb => cb(value, path));
}
}
}
}
10.4 游戏脚本
游戏脚本引擎架构
// game_script_engine.c — 游戏脚本引擎
#include "quickjs-libc.h"
typedef struct {
JSRuntime *rt;
JSContext *ctx;
JSValue update_func;
JSValue render_func;
double delta_time;
} GameScriptEngine;
// 暴露游戏 API 给脚本
static JSValue api_get_entity_position(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv) {
int32_t entity_id;
JS_ToInt32(ctx, &entity_id, argv[0]);
// 从游戏引擎获取实体位置
// Vec3 pos = game_engine_get_position(entity_id);
JSValue pos = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, pos, "x", JS_NewFloat64(ctx, 0.0));
JS_SetPropertyStr(ctx, pos, "y", JS_NewFloat64(ctx, 0.0));
JS_SetPropertyStr(ctx, pos, "z", JS_NewFloat64(ctx, 0.0));
return pos;
}
static JSValue api_set_entity_velocity(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv) {
int32_t entity_id;
JS_ToInt32(ctx, &entity_id, argv[0]);
double vx, vy, vz;
JSValue vel = argv[1];
JS_ToFloat64(ctx, &vx, JS_GetPropertyStr(ctx, vel, "x"));
JS_ToFloat64(ctx, &vy, JS_GetPropertyStr(ctx, vel, "y"));
JS_ToFloat64(ctx, &vz, JS_GetPropertyStr(ctx, vel, "z"));
// game_engine_set_velocity(entity_id, vx, vy, vz);
return JS_UNDEFINED;
}
static JSValue api_play_sound(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv) {
const char *sound = JS_ToCString(ctx, argv[0]);
double volume = 1.0;
if (argc > 1) JS_ToFloat64(ctx, &volume, argv[1]);
// audio_engine_play(sound, volume);
JS_FreeCString(ctx, sound);
return JS_UNDEFINED;
}
static JSValue api_spawn_particle(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv) {
// ...
return JS_UNDEFINED;
}
void game_script_init(GameScriptEngine *engine) {
engine->rt = JS_NewRuntime();
JS_SetMemoryLimit(engine->rt, 16 * 1024 * 1024);
engine->ctx = JS_NewContext(engine->rt);
// 注册游戏 API
JSValue global = JS_GetGlobalObject(engine->ctx);
JSValue game = JS_NewObject(engine->ctx);
JS_SetPropertyStr(engine->ctx, game, "getEntityPosition",
JS_NewCFunction(engine->ctx, api_get_entity_position,
"getEntityPosition", 1));
JS_SetPropertyStr(engine->ctx, game, "setEntityVelocity",
JS_NewCFunction(engine->ctx, api_set_entity_velocity,
"setEntityVelocity", 2));
JS_SetPropertyStr(engine->ctx, game, "playSound",
JS_NewCFunction(engine->ctx, api_play_sound,
"playSound", 2));
JS_SetPropertyStr(engine->ctx, game, "spawnParticle",
JS_NewCFunction(engine->ctx, api_spawn_particle,
"spawnParticle", 3));
JS_SetPropertyStr(engine->ctx, global, "Game", game);
JS_FreeValue(engine->ctx, global);
}
void game_script_load(GameScriptEngine *engine, const char *script_path) {
// 加载并编译脚本
JSValue result = JS_Eval(engine->ctx,
"import { update, render } from \"$SCRIPT\";",
100, script_path, JS_EVAL_TYPE_MODULE);
if (!JS_IsException(result)) {
JSValue mod = result;
engine->update_func = JS_GetPropertyStr(engine->ctx, mod, "update");
engine->render_func = JS_GetPropertyStr(engine->ctx, mod, "render");
}
JS_FreeValue(engine->ctx, result);
}
void game_script_update(GameScriptEngine *engine, double dt) {
engine->delta_time = dt;
JSValue dt_val = JS_NewFloat64(engine->ctx, dt);
JSValue result = JS_Call(engine->ctx, engine->update_func,
JS_UNDEFINED, 1, &dt_val);
if (JS_IsException(result)) {
JSValue ex = JS_GetException(engine->ctx);
const char *msg = JS_ToCString(engine->ctx, ex);
fprintf(stderr, "Script error: %s\n", msg);
JS_FreeCString(engine->ctx, msg);
JS_FreeValue(engine->ctx, ex);
}
JS_FreeValue(engine->ctx, result);
JS_FreeValue(engine->ctx, dt_val);
}
NPC 脚本示例
// npc_guard.js — 游戏 NPC 守卫行为脚本
import { AI } from "./ai_utils.js";
const STATE = {
IDLE: "idle",
PATROL: "patrol",
ALERT: "alert",
CHASE: "chase",
ATTACK: "attack",
RETURN: "return"
};
const config = {
visionRange: 15.0,
attackRange: 2.0,
patrolSpeed: 2.0,
chaseSpeed: 4.0,
alertDuration: 3.0,
patrolPoints: [
{ x: 10, y: 0, z: 10 },
{ x: 10, y: 0, z: -10 },
{ x: -10, y: 0, z: -10 },
{ x: -10, y: 0, z: 10 },
]
};
let state = STATE.IDLE;
let patrolIndex = 0;
let alertTimer = 0;
let lastKnownPlayerPos = null;
export function update(dt) {
const myPos = Game.getEntityPosition(this.entityId);
const playerPos = Game.getEntityPosition(0); // player = entity 0
const distToPlayer = AI.distance(myPos, playerPos);
switch (state) {
case STATE.IDLE:
if (distToPlayer < config.visionRange) {
if (AI.canSee(myPos, playerPos)) {
state = STATE.CHASE;
lastKnownPlayerPos = { ...playerPos };
Game.playSound("alert.ogg");
}
}
break;
case STATE.PATROL:
const target = config.patrolPoints[patrolIndex];
AI.moveToward(this.entityId, target, config.patrolSpeed, dt);
if (AI.distance(myPos, target) < 1.0) {
patrolIndex = (patrolIndex + 1) % config.patrolPoints.length;
}
if (distToPlayer < config.visionRange && AI.canSee(myPos, playerPos)) {
state = STATE.CHASE;
lastKnownPlayerPos = { ...playerPos };
Game.playSound("alert.ogg");
}
break;
case STATE.CHASE:
if (distToPlayer < config.attackRange) {
state = STATE.ATTACK;
} else if (distToPlayer > config.visionRange * 1.5) {
state = STATE.ALERT;
alertTimer = config.alertDuration;
} else {
AI.moveToward(this.entityId, playerPos, config.chaseSpeed, dt);
lastKnownPlayerPos = { ...playerPos };
}
break;
case STATE.ATTACK:
if (distToPlayer > config.attackRange * 1.2) {
state = STATE.CHASE;
} else {
AI.attack(this.entityId, 0); // attack player
}
break;
case STATE.ALERT:
alertTimer -= dt;
AI.lookAt(this.entityId, lastKnownPlayerPos);
if (distToPlayer < config.visionRange && AI.canSee(myPos, playerPos)) {
state = STATE.CHASE;
} else if (alertTimer <= 0) {
state = STATE.RETURN;
}
break;
case STATE.RETURN:
const home = config.patrolPoints[0];
AI.moveToward(this.entityId, home, config.patrolSpeed, dt);
if (AI.distance(myPos, home) < 1.0) {
state = STATE.PATROL;
}
break;
}
}
10.5 IoT 应用
IoT 网关脚本运行器
// iot_gateway.c — IoT 网关的 QuickJS 脚本引擎
#include "quickjs-libc.h"
#include <stdio.h>
#include <time.h>
// 传感器数据结构
typedef struct {
char device_id[32];
char sensor_type[16]; // "temperature", "humidity", "pressure"
double value;
uint64_t timestamp;
} SensorReading;
// IoT 规则引擎
typedef struct {
JSRuntime *rt;
JSContext *ctx;
JSValue process_func;
JSValue alert_func;
} IoTRuleEngine;
// 注册 IoT API
static JSValue api_send_alert(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv) {
const char *device = JS_ToCString(ctx, argv[0]);
const char *message = JS_ToCString(ctx, argv[1]);
double severity = 1.0;
if (argc > 2) JS_ToFloat64(ctx, &severity, argv[2]);
// 发送告警到监控系统
printf("[ALERT] Device: %s, Message: %s, Severity: %.1f\n",
device, message, severity);
// mqtt_publish("alerts/device", alert_json);
JS_FreeCString(ctx, device);
JS_FreeCString(ctx, message);
return JS_UNDEFINED;
}
static JSValue api_store_data(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv) {
const char *key = JS_ToCString(ctx, argv[0]);
double value;
JS_ToFloat64(ctx, &value, argv[1]);
// 存储到时序数据库
// influxdb_write(key, value, timestamp);
JS_FreeCString(ctx, key);
return JS_UNDEFINED;
}
static JSValue api_read_config(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv) {
const char *key = JS_ToCString(ctx, argv[0]);
// 从配置系统读取
JSValue result;
if (strcmp(key, "threshold.temp.high") == 0) {
result = JS_NewFloat64(ctx, 35.0);
} else if (strcmp(key, "threshold.temp.low") == 0) {
result = JS_NewFloat64(ctx, 5.0);
} else {
result = JS_UNDEFINED;
}
JS_FreeCString(ctx, key);
return result;
}
// 处理传感器数据
int iot_process_reading(IoTRuleEngine *engine,
const SensorReading *reading) {
JSValue obj = JS_NewObject(engine->ctx);
JS_SetPropertyStr(engine->ctx, obj, "deviceId",
JS_NewString(engine->ctx, reading->device_id));
JS_SetPropertyStr(engine->ctx, obj, "sensorType",
JS_NewString(engine->ctx, reading->sensor_type));
JS_SetPropertyStr(engine->ctx, obj, "value",
JS_NewFloat64(engine->ctx, reading->value));
JS_SetPropertyStr(engine->ctx, obj, "timestamp",
JS_NewInt64(engine->ctx, reading->timestamp));
JSValue result = JS_Call(engine->ctx, engine->process_func,
JS_UNDEFINED, 1, &obj);
int ret = 0;
if (!JS_IsException(result)) {
JS_ToInt32(engine->ctx, &ret, result);
}
JS_FreeValue(engine->ctx, result);
JS_FreeValue(engine->ctx, obj);
return ret;
}
IoT 规则脚本示例
// rules/temperature_monitor.js — 温度监控规则
export function process(reading) {
if (reading.sensorType !== "temperature") return 0;
const highThreshold = IoT.readConfig("threshold.temp.high") ?? 35.0;
const lowThreshold = IoT.readConfig("threshold.temp.low") ?? 5.0;
// 存储数据
IoT.storeData(`sensor.${reading.deviceId}.temperature`, reading.value);
// 检查告警条件
if (reading.value > highThreshold) {
IoT.sendAlert(reading.deviceId,
`High temperature: ${reading.value.toFixed(1)}°C`, 2.0);
// 连续高温检测
const recentReadings = IoT.getRecent(reading.deviceId, 5);
const allHigh = recentReadings.every(r => r.value > highThreshold);
if (allHigh) {
IoT.sendAlert(reading.deviceId,
"CRITICAL: Sustained high temperature!", 3.0);
IoT.activateCooling(reading.deviceId);
}
}
if (reading.value < lowThreshold) {
IoT.sendAlert(reading.deviceId,
`Low temperature: ${reading.value.toFixed(1)}°C`, 1.5);
}
return 1; // 处理完成
}
10.6 错误处理最佳实践
分层错误处理
// error_handling_best_practices.c
#include "quickjs-libc.h"
#include <stdio.h>
#include <setjmp.h>
typedef enum {
ERR_NONE = 0,
ERR_SYNTAX,
ERR_RUNTIME,
ERR_TIMEOUT,
ERR_MEMORY,
ERR_INTERNAL
} ErrorCode;
typedef struct {
ErrorCode code;
char message[256];
char stack[1024];
} ErrorInfo;
// 全面的错误捕获
ErrorInfo eval_and_catch(JSContext *ctx, const char *code,
const char *filename) {
ErrorInfo info = { ERR_NONE, "", "" };
JSValue result = JS_Eval(ctx, code, strlen(code), filename, 0);
if (JS_IsException(result)) {
JSValue ex = JS_GetException(ctx);
// 获取错误消息
JSValue message = JS_GetPropertyStr(ctx, ex, "message");
const char *msg = JS_ToCString(ctx, message);
strncpy(info.message, msg, sizeof(info.message) - 1);
JS_FreeCString(ctx, msg);
JS_FreeValue(ctx, message);
// 获取调用栈
JSValue stack = JS_GetPropertyStr(ctx, ex, "stack");
if (!JS_IsUndefined(stack)) {
const char *stk = JS_ToCString(ctx, stack);
strncpy(info.stack, stk, sizeof(info.stack) - 1);
JS_FreeCString(ctx, stk);
}
JS_FreeValue(ctx, stack);
// 判断错误类型
if (JS_IsSyntaxError(ctx, ex)) {
info.code = ERR_SYNTAX;
} else if (strstr(info.message, "out of memory")) {
info.code = ERR_MEMORY;
} else if (strstr(info.message, "stack overflow")) {
info.code = ERR_MEMORY;
} else {
info.code = ERR_RUNTIME;
}
JS_FreeValue(ctx, ex);
}
JS_FreeValue(ctx, result);
return info;
}
10.7 测试策略
单元测试框架
// test_framework.js — 轻量级测试框架
let tests = [];
let passed = 0;
let failed = 0;
export function test(name, fn) {
tests.push({ name, fn });
}
export function assert(condition, message) {
if (!condition) {
throw new Error(message || "Assertion failed");
}
}
export function assertEqual(actual, expected, message) {
if (actual !== expected) {
throw new Error(
message || `Expected ${expected}, got ${actual}`
);
}
}
export function assertDeepEqual(actual, expected, message) {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(
message ||
`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
);
}
}
export function run() {
console.log(`Running ${tests.length} tests...\n`);
for (const { name, fn } of tests) {
try {
fn();
console.log(` ✓ ${name}`);
passed++;
} catch (e) {
console.error(` ✗ ${name}`);
console.error(` ${e.message}`);
failed++;
}
}
console.log(`\nResults: ${passed} passed, ${failed} failed`);
return failed === 0;
}
// test_math.js — 使用测试框架
import { test, assertEqual, run } from "./test_framework.js";
import { add, multiply, factorial } from "./math.js";
test("add: basic addition", () => {
assertEqual(add(2, 3), 5);
assertEqual(add(-1, 1), 0);
assertEqual(add(0, 0), 0);
});
test("multiply: basic multiplication", () => {
assertEqual(multiply(3, 4), 12);
assertEqual(multiply(0, 100), 0);
assertEqual(multiply(-2, 3), -6);
});
test("factorial: compute factorial", () => {
assertEqual(factorial(0), 1);
assertEqual(factorial(1), 1);
assertEqual(factorial(5), 120);
assertEqual(factorial(10), 3628800);
});
const success = run();
std.exit(success ? 0 : 1);
10.8 调试技巧
调试信息收集
// debug_helpers.c — 调试辅助工具
#include "quickjs-libc.h"
#include <stdio.h>
// 获取 JS 值的详细类型信息
const char* js_debug_type(JSContext *ctx, JSValue val) {
if (JS_IsNull(val)) return "null";
if (JS_IsUndefined(val)) return "undefined";
if (JS_IsBool(val)) return JS_ToBool(ctx, val) ? "true" : "false";
if (JS_IsNumber(val)) {
static char buf[64];
double d;
JS_ToFloat64(ctx, &d, val);
snprintf(buf, sizeof(buf), "number(%g)", d);
return buf;
}
if (JS_IsString(val)) {
const char *s = JS_ToCString(ctx, val);
static char buf[256];
snprintf(buf, sizeof(buf), "string(\"%.200s\")", s);
JS_FreeCString(ctx, s);
return buf;
}
if (JS_IsObject(val)) {
if (JS_IsFunction(ctx, val)) return "function";
if (JS_IsArray(ctx, val)) return "array";
if (JS_IsError(ctx, val)) return "error";
return "object";
}
return "unknown";
}
// JS 值转字符串(调试用)
char* js_debug_value(JSContext *ctx, JSValue val) {
JSValue str = JS_JSONStringify(ctx, val, JS_NULL,
JS_NewInt32(ctx, 2));
const char *s = JS_ToCString(ctx, str);
char *result = strdup(s);
JS_FreeCString(ctx, s);
JS_FreeValue(ctx, str);
return result;
}
10.9 常见陷阱与解决方案
| 陷阱 | 症状 | 解决方案 |
|---|
| 忘记释放 JSValue | 内存持续增长 | 每个 JS_Get*/JS_New* 都对应一个 JS_FreeValue |
| 双重释放 | 程序崩溃 | 确保每个值只释放一次,使用 JS_DupValue 增加引用 |
| 使用已释放的值 | 段错误 | 释放后置 NULL,使用前检查 |
| 模块未注册 | import 失败 | 确保在 eval 前调用 JS_NewCModule |
| 栈溢出 | 执行中断 | 增加栈大小或限制递归深度 |
| 超时未生效 | 死循环卡死 | 确保设置 JS_SetInterruptHandler |
| GC 不及时 | 内存压力大 | 手动调用 JS_RunGC |
| 线程安全 | 数据竞争 | 每个线程使用独立的 Runtime |
| 字节码版本不匹配 | 加载失败 | 使用相同版本的 qjsc 和运行时 |
| C 字符串未释放 | 内存泄漏 | JS_ToCString 必须配对 JS_FreeCString |
10.10 项目结构建议
my-quickjs-project/
├── CMakeLists.txt # 或 Makefile
├── README.md
├── src/
│ ├── main.c # 应用入口
│ ├── sandbox.c # 沙箱配置
│ ├── api_*.c # 原生 API 实现
│ └── modules/ # C 模块
│ ├── native_math.c
│ └── native_io.c
├── scripts/
│ ├── main.js # JS 入口
│ ├── utils.js # 工具库
│ └── plugins/ # 插件目录
├── tests/
│ ├── test_c/ # C 单元测试
│ └── test_js/ # JS 单元测试
│ ├── test_utils.js
│ └── run_all.js
├── bytecode/ # 预编译字节码
├── docker/
│ ├── Dockerfile
│ └── Dockerfile.arm64
└── docs/
└── architecture.md
10.11 本章小结
| 要点 | 说明 |
|---|
| 安全沙箱 | 最小权限原则,移除危险 API,输入验证 |
| 嵌入式 | 预编译字节码,合理内存限制,HAL API |
| 插件系统 | JS 模块 + C API 注册,生命周期管理 |
| 游戏脚本 | 暴露游戏 API,NPC 行为脚本,热重载 |
| IoT 应用 | 规则引擎,传感器数据处理,告警系统 |
| 错误处理 | 分层捕获,详细错误信息,审计日志 |
| 测试 | 轻量测试框架,CI/CD 集成 |
扩展阅读