强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 集成

扩展阅读