2024-01-20

QuickJS 初探

深入探讨 QuickJS 轻量级 JavaScript 引擎的特性和使用方法,包括基本使用、核心特性和实际应用场景。

QuickJSJavaScriptC/C++嵌入式

QuickJS 是一个轻量级的 JavaScript 引擎,由 Fabrice Bellard 开发。本文将带你初探 QuickJS 的特性和使用方法。

什么是 QuickJS

QuickJS 是一个小型且可嵌入的 JavaScript 引擎,由 Fabrice Bellard(FFmpeg 和 QEMU 的创始人)开发,支持 ES2020 标准。它具有以下特点:

  • 轻量级:核心库只有几百 KB,相比 V8 引擎的几十 MB 要小得多
  • 快速:启动速度快,冷启动时间通常在毫秒级别
  • 可嵌入:易于集成到 C/C++ 项目中,API 设计简洁
  • 标准兼容:支持最新的 JavaScript 特性,包括 ES6 模块、async/await 等
  • 内存友好:内存占用低,适合资源受限的环境

基本使用

安装 QuickJS

bash
# 克隆仓库
git clone https://github.com/bellard/quickjs.git
cd quickjs

# 编译
make

简单的 JavaScript 执行

c
#include "quickjs.h"

int main() {
    JSRuntime *rt = JS_NewRuntime();
    JSContext *ctx = JS_NewContext(rt);
    
    // 执行 JavaScript 代码
    const char *script = "console.log('Hello, QuickJS!');";
    JSValue result = JS_Eval(ctx, script, strlen(script), "<input>", JS_EVAL_TYPE_GLOBAL);
    
    JS_FreeValue(ctx, result);
    JS_FreeContext(ctx);
    JS_FreeRuntime(rt);
    return 0;
}

核心特性

1. 模块系统

QuickJS 支持 ES6 模块:

javascript
// math.js
export function add(a, b) {
    return a + b;
}

export const PI = 3.14159;
javascript
// main.js
import { add, PI } from './math.js';
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159

2. 异步支持

QuickJS 支持 Promise 和 async/await:

javascript
async function fetchData() {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
}

fetchData().then(data => {
    console.log('Data:', data);
}).catch(error => {
    console.error('Error:', error);
});

3. 与 C 的互操作

QuickJS 提供了丰富的 C API 用于与 JavaScript 交互:

c
// 注册 C 函数到 JavaScript
JSValue js_add(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    int a, b;
    if (JS_ToInt32(ctx, &a, argv[0]) || JS_ToInt32(ctx, &b, argv[1]))
        return JS_EXCEPTION;
    return JS_NewInt32(ctx, a + b);
}

// 在 JavaScript 中使用
JS_SetPropertyStr(ctx, global_obj, "add", JS_NewCFunction(ctx, js_add, "add", 2));

QuickJS vs V8:为什么选择 QuickJS

性能对比

特性 QuickJS V8
二进制大小 ~600KB ~20MB+
内存占用 1-10MB 50-200MB+
启动时间 <10ms 100-500ms
适用场景 嵌入式、移动端 服务器、桌面应用

QuickJS 的优势

  1. 极小的内存占用:QuickJS 运行时通常只需要几 MB 内存,而 V8 需要几十 MB
  2. 快速启动:冷启动时间在毫秒级别,适合需要频繁启动的场景
  3. 更好的资源控制:可以精确控制内存使用和垃圾回收
  4. 简化的 API:C API 更加简洁,学习成本低

V8 的优势

  1. 更高的执行性能:在长时间运行的场景下,V8 的 JIT 编译能提供更好的性能
  2. 更丰富的生态系统:Node.js 生态系统的支持
  3. 更好的调试工具:Chrome DevTools 集成

实际应用场景

1. 移动端应用

滴滴 Hummer

滴滴的 Hummer 跨平台框架使用 QuickJS 作为 JavaScript 引擎:

javascript
// Hummer 中的 JavaScript 代码示例
import { View, Text, Button } from '@hummer/hummer-core';

class MyComponent extends View {
    constructor() {
        super();
        this.text = new Text();
        this.text.text = 'Hello Hummer!';
        this.appendChild(this.text);
    }
}

Hummer 选择 QuickJS 的原因:

  • 包体积优化:相比 V8,QuickJS 能显著减少 App 包大小
  • 内存效率:在移动设备上内存占用更少
  • 快速启动:提升 App 启动速度

阿里 Weex 2.0

Weex 2.0 也采用了 QuickJS 引擎:

javascript
// Weex 2.0 组件示例
export default {
    data() {
        return {
            message: 'Hello Weex 2.0!'
        }
    },
    methods: {
        handleClick() {
            this.message = 'QuickJS is awesome!';
        }
    }
}

2. 嵌入式设备

QuickJS 的轻量级特性使其非常适合嵌入式应用:

c
// 在资源受限的设备上运行 JavaScript
void run_script_on_device(const char *script) {
    JSRuntime *rt = JS_NewRuntime();
    JSContext *ctx = JS_NewContext(rt);
    
    // 设置内存限制
    JS_SetMemoryLimit(rt, 1024 * 1024); // 1MB
    
    JSValue result = JS_Eval(ctx, script, strlen(script), "<device>", JS_EVAL_TYPE_GLOBAL);
    JS_FreeValue(ctx, result);
    JS_FreeContext(ctx);
    JS_FreeRuntime(rt);
}

3. 配置文件处理

使用 QuickJS 处理 JSON 配置文件:

javascript
// config.js
const config = {
    server: {
        host: 'localhost',
        port: 8080
    },
    database: {
        url: 'mongodb://localhost:27017',
        name: 'myapp'
    }
};

export default config;

4. 游戏脚本引擎

QuickJS 也被用于游戏中的脚本系统:

c
// 游戏脚本引擎示例
typedef struct {
    JSRuntime *rt;
    JSContext *ctx;
    JSValue game_objects;
} GameScriptEngine;

void init_game_script_engine(GameScriptEngine *engine) {
    engine->rt = JS_NewRuntime();
    engine->ctx = JS_NewContext(engine->rt);
    
    // 注册游戏 API
    register_game_api(engine->ctx);
    
    // 加载游戏脚本
    load_game_scripts(engine);
}

性能优化

1. 内存管理

c
// 及时释放 JavaScript 值
JSValue value = JS_NewString(ctx, "Hello");
// 使用完毕后释放
JS_FreeValue(ctx, value);

2. 预编译脚本

c
// 预编译脚本以提高性能
JSValue script = JS_Eval(ctx, source_code, strlen(source_code), "<script>", JS_EVAL_TYPE_MODULE);
// 保存编译后的脚本供重复使用

最佳实践和注意事项

1. 内存管理最佳实践

c
// 正确的内存管理方式
void safe_js_execution() {
    JSRuntime *rt = JS_NewRuntime();
    JSContext *ctx = JS_NewContext(rt);
    
    // 设置内存限制
    JS_SetMemoryLimit(rt, 5 * 1024 * 1024); // 5MB
    
    // 执行脚本
    JSValue result = JS_Eval(ctx, script, strlen(script), "<script>", JS_EVAL_TYPE_GLOBAL);
    
    // 检查执行结果
    if (JS_IsException(result)) {
        JSValue exception = JS_GetException(ctx);
        const char *error = JS_ToCString(ctx, &exception);
        printf("Error: %s\n", error);
        JS_FreeCString(ctx, error);
        JS_FreeValue(ctx, exception);
    }
    
    // 清理资源
    JS_FreeValue(ctx, result);
    JS_FreeContext(ctx);
    JS_FreeRuntime(rt);
}

2. 错误处理

c
// 完善的错误处理机制
JSValue execute_with_error_handling(JSContext *ctx, const char *script) {
    JSValue result = JS_Eval(ctx, script, strlen(script), "<script>", JS_EVAL_TYPE_GLOBAL);
    
    if (JS_IsException(result)) {
        JSValue exception = JS_GetException(ctx);
        JSValue stack = JS_GetPropertyStr(ctx, exception, "stack");
        
        const char *error_msg = JS_ToCString(ctx, &exception);
        const char *stack_trace = JS_ToCString(ctx, &stack);
        
        printf("JavaScript Error: %s\n", error_msg);
        printf("Stack Trace: %s\n", stack_trace);
        
        JS_FreeCString(ctx, error_msg);
        JS_FreeCString(ctx, stack_trace);
        JS_FreeValue(ctx, stack);
        JS_FreeValue(ctx, exception);
    }
    
    return result;
}

3. 性能优化技巧

c
// 预编译和缓存脚本
typedef struct {
    JSValue compiled_script;
    char *script_hash;
} CachedScript;

CachedScript *cache_script(JSContext *ctx, const char *script) {
    CachedScript *cached = malloc(sizeof(CachedScript));
    
    // 编译脚本
    cached->compiled_script = JS_Eval(ctx, script, strlen(script), "<script>", JS_EVAL_TYPE_MODULE);
    
    // 生成脚本哈希(简化示例)
    cached->script_hash = strdup(script);
    
    return cached;
}

QuickJS 事件循环机制

1. 事件循环基础

QuickJS 的事件循环机制相对简单,主要基于 Promise 和微任务队列:

javascript
// QuickJS 中的事件循环示例
console.log('1. 同步代码开始');

Promise.resolve().then(() => {
    console.log('3. 微任务 1');
});

setTimeout(() => {
    console.log('4. 宏任务');
}, 0);

Promise.resolve().then(() => {
    console.log('3. 微任务 2');
});

console.log('2. 同步代码结束');

// 输出顺序:
// 1. 同步代码开始
// 2. 同步代码结束
// 3. 微任务 1
// 3. 微任务 2
// 4. 宏任务

2. 自定义事件循环

QuickJS 允许你实现自定义的事件循环:

c
#include "quickjs.h"
#include <sys/epoll.h>
#include <unistd.h>

typedef struct {
    int epoll_fd;
    JSContext *ctx;
    int running;
} EventLoop;

// 事件循环结构
EventLoop* create_event_loop(JSContext *ctx) {
    EventLoop *loop = malloc(sizeof(EventLoop));
    loop->epoll_fd = epoll_create1(0);
    loop->ctx = ctx;
    loop->running = 1;
    return loop;
}

// 运行事件循环
void run_event_loop(EventLoop *loop) {
    struct epoll_event events[10];
    
    while (loop->running) {
        // 处理微任务队列
        JSContext *ctx = loop->ctx;
        JSValue pending = JS_GetPendingException(ctx);
        if (!JS_IsUndefined(pending)) {
            // 处理异常
            JS_FreeValue(ctx, pending);
        }
        
        // 处理 I/O 事件
        int nfds = epoll_wait(loop->epoll_fd, events, 10, 100);
        for (int i = 0; i < nfds; i++) {
            // 处理事件
            handle_io_event(events[i]);
        }
        
        // 执行微任务
        execute_microtasks(ctx);
    }
}

// 执行微任务
void execute_microtasks(JSContext *ctx) {
    // QuickJS 内部会自动处理 Promise 微任务
    // 这里可以添加自定义的微任务处理逻辑
}

3. 异步操作处理

c
// 注册异步操作到事件循环
typedef struct {
    int fd;
    JSValue callback;
    EventLoop *loop;
} AsyncOperation;

// 创建异步读取操作
JSValue create_async_read(JSContext *ctx, int fd, JSValue callback) {
    AsyncOperation *op = malloc(sizeof(AsyncOperation));
    op->fd = fd;
    op->callback = JS_DupValue(ctx, callback);
    
    // 注册到事件循环
    register_async_operation(ctx, op);
    
    return JS_UNDEFINED;
}

// 异步操作完成后的回调
void async_read_complete(AsyncOperation *op, const char *data) {
    JSContext *ctx = op->loop->ctx;
    
    // 调用 JavaScript 回调函数
    JSValue args[1];
    args[0] = JS_NewString(ctx, data);
    
    JSValue result = JS_Call(ctx, op->callback, JS_UNDEFINED, 1, args);
    JS_FreeValue(ctx, result);
    JS_FreeValue(ctx, args[0]);
}

QuickJS 垃圾回收机制

1. 垃圾回收器类型

QuickJS 使用标记-清除(Mark-and-Sweep)垃圾回收算法:

c
// 垃圾回收器配置
void configure_gc(JSRuntime *rt) {
    // 设置内存限制
    JS_SetMemoryLimit(rt, 10 * 1024 * 1024); // 10MB
    
    // 设置垃圾回收阈值
    JS_SetGCThreshold(rt, 1024 * 1024); // 1MB
    
    // 启用增量垃圾回收
    JS_SetGCObjectCountLimit(rt, 10000);
}

2. 手动垃圾回收控制

c
// 手动触发垃圾回收
void manual_gc(JSRuntime *rt) {
    // 强制垃圾回收
    JS_RunGC(rt);
    
    // 获取内存使用情况
    size_t memory_usage = JS_GetMemoryUsage(rt);
    printf("Memory usage: %zu bytes\n", memory_usage);
}

// 监控垃圾回收
void monitor_gc(JSRuntime *rt) {
    // 设置垃圾回收回调
    JS_SetGCCallback(rt, gc_callback, NULL);
}

// 垃圾回收回调函数
void gc_callback(JSRuntime *rt, JS_MarkFunc *mark_func, void *opaque) {
    printf("Garbage collection triggered\n");
    
    // 可以在这里添加自定义的清理逻辑
    cleanup_custom_resources();
}

3. 内存泄漏检测

c
// 内存泄漏检测工具
typedef struct {
    JSValue *tracked_values;
    size_t count;
    size_t capacity;
} MemoryTracker;

MemoryTracker* create_memory_tracker() {
    MemoryTracker *tracker = malloc(sizeof(MemoryTracker));
    tracker->capacity = 1000;
    tracker->count = 0;
    tracker->tracked_values = malloc(sizeof(JSValue) * tracker->capacity);
    return tracker;
}

// 跟踪 JavaScript 值
void track_value(MemoryTracker *tracker, JSContext *ctx, JSValue value) {
    if (tracker->count >= tracker->capacity) {
        tracker->capacity *= 2;
        tracker->tracked_values = realloc(tracker->tracked_values, 
                                        sizeof(JSValue) * tracker->capacity);
    }
    
    tracker->tracked_values[tracker->count++] = JS_DupValue(ctx, value);
}

// 检查内存泄漏
void check_memory_leaks(MemoryTracker *tracker, JSContext *ctx) {
    printf("Tracking %zu JavaScript values\n", tracker->count);
    
    for (size_t i = 0; i < tracker->count; i++) {
        JSValue value = tracker->tracked_values[i];
        
        // 检查值是否仍然有效
        if (!JS_IsUndefined(value) && !JS_IsNull(value)) {
            printf("Potential memory leak detected at index %zu\n", i);
        }
    }
}

4. 优化垃圾回收性能

c
// 优化垃圾回收的代码模式
void optimized_js_usage(JSContext *ctx) {
    // 1. 及时释放不再使用的值
    JSValue temp_value = JS_NewString(ctx, "temporary");
    // 使用完毕后立即释放
    JS_FreeValue(ctx, temp_value);
    
    // 2. 避免创建大量临时对象
    for (int i = 0; i < 1000; i++) {
        // 不好的做法:每次都创建新对象
        // JSValue obj = JS_NewObject(ctx);
        
        // 好的做法:重用对象
        static JSValue reusable_obj = JS_UNDEFINED;
        if (JS_IsUndefined(reusable_obj)) {
            reusable_obj = JS_NewObject(ctx);
        }
        // 使用 reusable_obj
    }
    
    // 3. 使用对象池
    ObjectPool *pool = create_object_pool(ctx);
    JSValue obj = get_object_from_pool(pool);
    // 使用对象
    return_object_to_pool(pool, obj);
}

// 对象池实现
typedef struct {
    JSValue *objects;
    size_t count;
    size_t capacity;
} ObjectPool;

ObjectPool* create_object_pool(JSContext *ctx) {
    ObjectPool *pool = malloc(sizeof(ObjectPool));
    pool->capacity = 100;
    pool->count = 0;
    pool->objects = malloc(sizeof(JSValue) * pool->capacity);
    return pool;
}

JSValue get_object_from_pool(ObjectPool *pool) {
    if (pool->count > 0) {
        return pool->objects[--pool->count];
    }
    return JS_NewObject(NULL); // 创建新对象
}

void return_object_to_pool(ObjectPool *pool, JSValue obj) {
    if (pool->count < pool->capacity) {
        pool->objects[pool->count++] = obj;
    } else {
        JS_FreeValue(NULL, obj); // 释放对象
    }
}

总结

QuickJS 作为一个现代化的 JavaScript 引擎,在保持轻量级的同时提供了强大的功能。它特别适合需要嵌入 JavaScript 运行时的 C/C++ 项目,以及资源受限的嵌入式应用。

选择 QuickJS 的场景

  • 移动端应用:需要优化包大小和内存占用
  • 嵌入式设备:资源受限的环境
  • 脚本引擎:需要快速启动和频繁执行
  • 配置文件处理:轻量级的 JSON/JavaScript 处理

不适用 QuickJS 的场景

  • 长时间运行的服务器应用:V8 的 JIT 编译性能更好
  • 需要复杂调试工具的场景:V8 的调试工具更丰富
  • 依赖 Node.js 生态的项目:V8 生态系统更完善

通过本文的介绍,你应该对 QuickJS 有了深入的了解。从滴滴 Hummer 到阿里 Weex 2.0,QuickJS 已经在多个知名项目中证明了其价值。接下来可以尝试在实际项目中应用它,探索更多高级特性。