在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中的:

拷贝到自己工程

相关推荐
用户805533698031 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner1 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz6 天前
QML Hello World 入门示例
qt
xcyxiner9 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner10 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner10 天前
DicomViewer (添加模型类)3
qt
xcyxiner11 天前
DicomViewer (目录调整) 2
qt
xcyxiner11 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00613 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术13 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript