在Qt中使用QuickJS

1.背景

目前的一个Qt项目需要用到脚本功能,在qt自带的QJSEngine和QScripteEngine中,我们选择了QScripteEngine,因为我们需要脚本的调试功能。QJSEngine虽然支持的功能特性比较新,但是没有debug功能,所以没选用。

但是在使用了QScriptEngine一段时间后,发现其在多线程运行时有问题:在多(子)线程中申请、析构资源时,有概率导致软件崩溃(一般会定位到wtf::fast-free位置)。

因此,我们需要选用新的脚本引擎。

使用新的第三方的脚本引擎的话,他们会持续不断维护、更新,而不像QScriptEngine那样早就不再维护;而且支持的特性也比较新。百利而无一害。

经过对比QuickJS、Duktape(作者好久不维护了)、JerryScript(不支持多线程)、tiny-js(支持的特性比较少) 、escargot(依赖的第三方库太多,不好编译)等能用在C/C++中集成的脚本引擎后,选择了QuickJS,虽然其没有直接支持debug,但是可以稍微修改一下源码。

github上的开源js引擎对比可以在此处看到:
https://zoo.js.org/

2.quickJS源码下载

注意,我们使用的是 quickjs-ng ,而不是原版的quickjs,原版的对windows支持不好 。

3.将引擎文件添加进工程

下载后,我们把源码解压到我们工程的一个目录中。

然后在pro文件中添加进来,添加部分文件即可

bash 复制代码
# 注意,此处使用的是 quickjs-ng https://github.com/quickjs-ng/quickjs
# 而不是原版的,原版的对windows支持不好 https://github.com/bellard/quickjs
SOURCES += \
    quickjs/cutils.c \
    quickjs/dtoa.c \
    quickjs/libregexp.c \
    quickjs/libunicode.c \
    quickjs/quickjs.c


HEADERS += \
    quickjs/cutils.h \
    quickjs/dtoa.h \
    quickjs/libregexp-opcode.h \
    quickjs/libregexp.h \
    quickjs/libunicode-table.h \
    quickjs/libunicode.h \
    quickjs/quickjs.h

4.使用

此时便可以在工程中使用了

cpp 复制代码
#include <QDebug>
#include <QDateTime>
#include <QtConcurrentRun>
#include <iostream>
#include <string>

#include "quickjs/quickjs.h"

// 自定义 C++ 函数,暴露给 JavaScript
static JSValue js_print_message(JSContext *ctx, JSValueConst this_val,
                                int argc, JSValueConst *argv)
{
    if (argc < 1)
        return JS_UNDEFINED;

    const char *str = JS_ToCString(ctx, argv[0]);
    if (!str)
        return JS_EXCEPTION;

    std::cout << "C++ Print: " << str << std::endl;
    JS_FreeCString(ctx, str);

    return JS_UNDEFINED;
}

// 返回一个对象给 JavaScript
static JSValue js_get_info(JSContext *ctx, JSValueConst this_val,
                           int argc, JSValueConst *argv)
{
    JSValue obj = JS_NewObject(ctx);
    JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, "QuickJS"));
    JS_SetPropertyStr(ctx, obj, "version", JS_NewString(ctx, "2024"));
    JS_SetPropertyStr(ctx, obj, "language", JS_NewString(ctx, "C++"));
    return obj;
}


int jsTest()
{
    JSRuntime *rt;
    JSContext *ctx;

    // 同一运行时内不支持多线程,因此每个线程都需要自行创建一个运行时
    // 1. 创建 JavaScript 运行时
    rt = JS_NewRuntime();
    if (!rt) {
        std::cerr << "Failed to create runtime" << std::endl;
        return 1;
    }

    // 2. 创建 JavaScript 上下文
    ctx = JS_NewContext(rt);
    if (!ctx) {
        std::cerr << "Failed to create context" << std::endl;
        JS_FreeRuntime(rt);
        return 1;
    }

    // // 将堆栈深度限制提高到20层
    // Error.stackTraceLimit = 20;
    const char *initCode = "Error.stackTraceLimit = 20;";
    JS_Eval(ctx, initCode, strlen(initCode), "", JS_EVAL_TYPE_GLOBAL);
  
    // 3. 注册 C++ 函数到全局对象
    JSValue global_obj = JS_GetGlobalObject(ctx);
    JS_SetPropertyStr(ctx, global_obj, "printMessage",
                      JS_NewCFunction(ctx, js_print_message, "printMessage", 1));
    JS_SetPropertyStr(ctx, global_obj, "getInfo",
                      JS_NewCFunction(ctx, js_get_info, "getInfo", 0));
    JS_FreeValue(ctx, global_obj);

    // 4. 执行 JavaScript 代码
    const char *code = R"(
        // 调用 C++ 函数
        printMessage("Hello from JavaScript!");

        // 获取 C++ 返回的对象
        let info = getInfo();
        printMessage("Name: " + info.name);
        printMessage("Version: " + info.version);

        // 定义 JavaScript 函数
        function calculate(a, b) {
            return a + b;
        }

        // 执行计算
        let result = calculate(10, 20);
        printMessage("Result: " + result);

        // 返回结果
        result;
    )";

    JSValue val = JS_Eval(ctx, code, strlen(code), "myScript.js", JS_EVAL_TYPE_GLOBAL);

    // 5. 处理执行结果
    if (JS_IsException(val)) {
        JSValue exception = JS_GetException(ctx);
// 1. 获取 "stack" 对应的原子(atom),这是高效查找属性的键
        JSAtom atom_stack = JS_NewAtom(ctx, "stack");

        // 2. 从异常对象中获取 stack 属性的值
        JSValue stack_val = JS_GetProperty(ctx, exception, atom_stack);

        // 3. 检查并转换堆栈信息为C字符串
        if (!JS_IsUndefined(stack_val)) {
            const char* stack_str = JS_ToCString(ctx, stack_val);
            if (stack_str) {
                // 4. 打印错误和堆栈
                fprintf(stderr, "Exception occurred:\n%s\n", stack_str);
                JS_FreeCString(ctx, stack_str); // 释放C字符串
            }
        } else {
            // 如果 stack 属性不存在,打印一个提示
            fprintf(stderr, "Exception occurred, but no stack trace is available.\n");
        }

        // 5. 释放所有创建的 JS 值
        JS_FreeValue(ctx, stack_val);
        JS_FreeAtom(ctx, atom_stack); // 释放原子


        const char *error = JS_ToCString(ctx, exception);
        std::cerr << "JavaScript Error: " << error << std::endl;
        JS_FreeCString(ctx, error);
        JS_FreeValue(ctx, exception);
    } else {
        // 获取返回值
        if (JS_IsNumber(val)) {
            int32_t result;
            JS_ToInt32(ctx, &result, val);
            std::cout << "JavaScript returned: " << result << std::endl;
        }
    }
    JS_FreeValue(ctx, val);

    // // 6. 调用 JavaScript 函数
    // const char *func_code = "function multiply(a, b) { return a * b; }";
    // JS_Eval(ctx, func_code, strlen(func_code), "<func>", JS_EVAL_TYPE_GLOBAL);

    // global_obj = JS_GetGlobalObject(ctx);
    // JSValue multiply_func = JS_GetPropertyStr(ctx, global_obj, "multiply");

    // if (JS_IsFunction(ctx, multiply_func)) {
    //     JSValue args[2] = {
    //         JS_NewInt32(ctx, 5),
    //         JS_NewInt32(ctx, 6)
    //     };
    //     JSValue result = JS_Call(ctx, multiply_func, JS_UNDEFINED, 2, args);

    //     if (!JS_IsException(result)) {
    //         int32_t value;
    //         JS_ToInt32(ctx, &value, result);
    //         std::cout << "multiply(5, 6) = " << value << std::endl;
    //     }

    //     JS_FreeValue(ctx, result);
    //     JS_FreeValue(ctx, args[0]);
    //     JS_FreeValue(ctx, args[1]);
    // }

    // JS_FreeValue(ctx, multiply_func);
    // JS_FreeValue(ctx, global_obj);

    // 7. 清理资源
    JS_FreeContext(ctx);
    JS_FreeRuntime(rt);

    return 0;
}

5.关于debug

经过GitHub copilot的分析,可以在字节码运行函数JS_CallInternal中的for循环处添加如下代码。

cpp 复制代码
        int col_num = 0;
        int line_num = -1;
        const char *filename = NULL;
        const char *funcname = NULL;

        if (b) {
            uint32_t pc_index = (uint32_t)(pc - b->byte_code_buf - 1);
            line_num = find_line_num(ctx, b, pc_index, &col_num);
            filename = b->filename ? JS_AtomToCString(ctx, b->filename) : NULL;
            funcname = b->func_name ? JS_AtomToCString(ctx, b->func_name) : NULL;
            /* 现在 filename, funcname, line_num, col_num 可用(line_num/col_num 从 1 开始) */
            if (filename) {
                fprintf(stderr, "at %s %s:%d:%d\n", funcname, filename, line_num, col_num);
                JS_FreeCString(ctx, filename);
            } else {
                fprintf(stderr, "at <unknown>:%d:%d\n", line_num, col_num);
            }
        }

从打印出来的信息可以得知,此处利用必要的函数就可以监听脚本的每一次运行。从而实现类似QScriptEngineAgent的所有功能。而利用这些功能,就可以轻松实现调试。(functionEntry/functionExit可以在前面的基础上配合pc/opCode来得到)

假如想要匹配操作码,得把quickjs.c中的这一部分拷贝到我们自己的工程文件中。

quickjs.c中的:

拷贝到自己工程

相关推荐
β添砖java1 小时前
python第一阶段第六章python数据容器
开发语言·python
YouEmbedded1 小时前
解码C++基础——从C到C++
开发语言·从c到c++
yong99901 小时前
基于多普勒连续波雷达原理的MATLAB测速程序
开发语言·matlab
o***36931 小时前
python爬虫——爬取全年天气数据并做可视化分析
开发语言·爬虫·python
枫叶丹41 小时前
【Qt开发】Qt窗口(三) -> QStatusBar状态栏
c语言·开发语言·数据库·c++·qt·microsoft
亮子AI1 小时前
【JavaScript】修改数组的正确方法
开发语言·javascript·ecmascript
浮尘笔记2 小时前
Go语言中如何实现线程安全的map
开发语言·后端·安全·golang
时尚IT男2 小时前
Python 魔术方法详解:掌握面向对象编程的精髓
开发语言·python