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

拷贝到自己工程
