From 831130b1d75a2242727c9e0dbc6c99af0872a503 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=9F=AD=E8=8F=9C=E9=92=9F?= <312780179@qq.com>
Date: Thu, 21 May 2026 21:25:06 +0800
Subject: [PATCH 1/4] Add debugger interface
---
api-test.c | 126 ++++++++++++++++++++++++++++
quickjs-opcode.h | 2 +
quickjs.c | 212 +++++++++++++++++++++++++++++++++++++++++++++++
quickjs.h | 56 +++++++++++++
4 files changed, 396 insertions(+)
diff --git a/api-test.c b/api-test.c
index 7aa7dc8a6..ee80995f0 100644
--- a/api-test.c
+++ b/api-test.c
@@ -1013,6 +1013,131 @@ static void get_uint8array(void)
JS_FreeRuntime(rt);
}
+static struct {
+ int call_count;
+ int last_line;
+ int last_col;
+ char last_filename[256];
+ char last_funcname[256];
+ int stack_depth;
+ int max_local_count;
+ int abort_at; /* abort (return -1) on this call, 0 = never */
+} trace_state;
+
+static int debug_trace_cb(JSContext *ctx,
+ const char *filename,
+ const char *funcname,
+ int line,
+ int col,
+ void *opaque)
+{
+ trace_state.call_count++;
+ trace_state.last_line = line;
+ trace_state.last_col = col;
+ snprintf(trace_state.last_filename, sizeof(trace_state.last_filename),
+ "%s", filename);
+ snprintf(trace_state.last_funcname, sizeof(trace_state.last_funcname),
+ "%s", funcname);
+ trace_state.stack_depth = JS_GetStackDepth(ctx);
+ int count = 0;
+ JSDebugLocalVar *vars = NULL;
+ assert(JS_GetLocalVariablesAtLevel(ctx, 0, &vars, &count) == 0);
+ if (count > trace_state.max_local_count)
+ trace_state.max_local_count = count;
+ if (vars)
+ JS_FreeLocalVariables(ctx, vars, count);
+ if (trace_state.abort_at > 0 &&
+ trace_state.call_count >= trace_state.abort_at)
+ return -1;
+ return 0;
+}
+
+static void debug_trace(void)
+{
+ JSRuntime *rt = JS_NewRuntime();
+ JSContext *ctx = JS_NewContext(rt);
+
+ memset(&trace_state, 0, sizeof(trace_state));
+ {
+ JSValue ret = eval(ctx, "1+2");
+ assert(!JS_IsException(ret));
+ JS_FreeValue(ctx, ret);
+ assert(trace_state.call_count == 0);
+ }
+
+ JS_SetDebugTraceHandler(ctx, debug_trace_cb, NULL);
+ memset(&trace_state, 0, sizeof(trace_state));
+ {
+ JSValue ret = eval(ctx, "var x = 1; x + 2");
+ assert(!JS_IsException(ret));
+ JS_FreeValue(ctx, ret);
+ assert(trace_state.call_count > 0);
+ assert(!strcmp(trace_state.last_filename, ""));
+ }
+
+ {
+ JSDebugLocalVar *vars = NULL;
+ int count = -1;
+ assert(JS_GetLocalVariablesAtLevel(ctx, 0, &vars, &count) == 0);
+ assert(vars == NULL);
+ assert(count == 0);
+ }
+
+ memset(&trace_state, 0, sizeof(trace_state));
+ {
+ static const char code[] =
+ "function outer() {\n"
+ " function inner() {\n"
+ " return 42;\n"
+ " }\n"
+ " return inner();\n"
+ "}\n"
+ "outer();\n";
+ JSValue ret = eval(ctx, code);
+ assert(!JS_IsException(ret));
+ JS_FreeValue(ctx, ret);
+ assert(trace_state.call_count > 0);
+ assert(trace_state.stack_depth >= 1);
+ }
+
+ memset(&trace_state, 0, sizeof(trace_state));
+ {
+ static const char code[] =
+ "function f(a, b) {\n"
+ " var c = a + b;\n"
+ " return c;\n"
+ "}\n"
+ "f(10, 20);\n";
+ JSValue ret = eval(ctx, code);
+ assert(!JS_IsException(ret));
+ JS_FreeValue(ctx, ret);
+ assert(trace_state.call_count > 0);
+ assert(trace_state.max_local_count >= 2);
+ }
+
+ memset(&trace_state, 0, sizeof(trace_state));
+ trace_state.abort_at = 1;
+ {
+ JSValue ret = eval(ctx, "1+2; 3+4");
+ assert(JS_IsException(ret));
+ JS_FreeValue(ctx, ret);
+ JSValue exc = JS_GetException(ctx);
+ JS_FreeValue(ctx, exc);
+ }
+
+ JS_SetDebugTraceHandler(ctx, NULL, NULL);
+ memset(&trace_state, 0, sizeof(trace_state));
+ {
+ JSValue ret = eval(ctx, "1+2");
+ assert(!JS_IsException(ret));
+ JS_FreeValue(ctx, ret);
+ assert(trace_state.call_count == 0);
+ }
+
+ JS_FreeContext(ctx);
+ JS_FreeRuntime(rt);
+}
+
static void new_symbol(void)
{
JSRuntime *rt = new_runtime();
@@ -1089,6 +1214,7 @@ int main(void)
slice_string_tocstring();
immutable_array_buffer();
get_uint8array();
+ debug_trace();
new_symbol();
return 0;
}
diff --git a/quickjs-opcode.h b/quickjs-opcode.h
index ec2a5ad91..a454f836a 100644
--- a/quickjs-opcode.h
+++ b/quickjs-opcode.h
@@ -372,6 +372,8 @@ DEF( is_null, 1, 1, 1, none)
DEF(typeof_is_undefined, 1, 1, 1, none)
DEF( typeof_is_function, 1, 1, 1, none)
+DEF( debug, 1, 0, 0, none)
+
#undef DEF
#undef def
#endif /* DEF */
diff --git a/quickjs.c b/quickjs.c
index 38a724fbb..c46e5445a 100644
--- a/quickjs.c
+++ b/quickjs.c
@@ -539,6 +539,9 @@ struct JSContext {
const char *input, size_t input_len,
const char *filename, int line, int flags, int scope_idx);
void *user_opaque;
+
+ JSDebugTraceFunc *debug_trace;
+ void *debug_trace_opaque;
};
typedef union JSFloat64Union {
@@ -1390,6 +1393,7 @@ static void js_async_function_resolve_mark(JSRuntime *rt, JSValueConst val,
static JSValue JS_EvalInternal(JSContext *ctx, JSValueConst this_obj,
const char *input, size_t input_len,
const char *filename, int line, int flags, int scope_idx);
+static const char *JS_AtomGetStr(JSContext *ctx, char *buf, int buf_size, JSAtom atom);
static void js_free_module_def(JSContext *ctx, JSModuleDef *m);
static void js_mark_module_def(JSRuntime *rt, JSModuleDef *m,
JS_MarkFunc *mark_func);
@@ -2593,6 +2597,141 @@ JSValue JS_GetFunctionProto(JSContext *ctx)
return js_dup(ctx->function_proto);
}
+void JS_SetDebugTraceHandler(JSContext *ctx, JSDebugTraceFunc *cb, void *opaque)
+{
+ ctx->debug_trace = cb;
+ ctx->debug_trace_opaque = opaque;
+}
+
+static JSStackFrame *js_get_stack_frame_at_level(JSContext *ctx, int level)
+{
+ JSRuntime *rt = ctx->rt;
+ JSStackFrame *sf = rt->current_stack_frame;
+ int current_level = 0;
+
+ while (sf != NULL && current_level < level) {
+ sf = sf->prev_frame;
+ current_level++;
+ }
+ return sf;
+}
+
+int JS_GetStackDepth(JSContext *ctx)
+{
+ JSRuntime *rt = ctx->rt;
+ JSStackFrame *sf = rt->current_stack_frame;
+ int depth = 0;
+
+ while (sf != NULL) {
+ depth++;
+ sf = sf->prev_frame;
+ }
+ return depth;
+}
+
+int JS_GetLocalVariablesAtLevel(JSContext *ctx, int level,
+ JSDebugLocalVar **pvars, int *pcount)
+{
+ if (pvars)
+ *pvars = NULL;
+ if (pcount)
+ *pcount = 0;
+ if (!pvars) {
+ JS_ThrowTypeError(ctx, "pvars must not be NULL");
+ return -1;
+ }
+
+ JSStackFrame *sf = js_get_stack_frame_at_level(ctx, level);
+ if (sf == NULL)
+ return 0;
+
+ JSValue func = sf->cur_func;
+ if (JS_VALUE_GET_TAG(func) != JS_TAG_OBJECT)
+ return 0;
+
+ JSObject *p = JS_VALUE_GET_OBJ(func);
+ if (p->class_id != JS_CLASS_BYTECODE_FUNCTION)
+ return 0;
+
+ JSFunctionBytecode *b = p->u.func.function_bytecode;
+ int total_vars = b->arg_count + b->var_count;
+
+ if (total_vars == 0)
+ return 0;
+
+ JSDebugLocalVar *vars = js_malloc(ctx, sizeof(JSDebugLocalVar) * total_vars);
+ if (!vars)
+ return -1;
+
+ int idx = 0;
+
+#define APPEND_VAR(vd_, value_, is_arg_) \
+ do { \
+ JSAtom name_ = (vd_)->var_name; \
+ const char *name_str_; \
+ if (name_ != JS_ATOM_NULL) { \
+ char tmp_[32]; \
+ JS_AtomGetStr(ctx, tmp_, sizeof(tmp_), name_); \
+ if (tmp_[0] == '<') \
+ break; \
+ } \
+ name_str_ = JS_AtomToCString(ctx, name_); \
+ if (unlikely(!name_str_)) \
+ goto fail; \
+ vars[idx].name = name_str_; \
+ /* Do not expose the internal TDZ sentinel to C callers. */ \
+ if (JS_VALUE_GET_TAG(value_) == JS_TAG_UNINITIALIZED) \
+ vars[idx].value = JS_UNDEFINED; \
+ else \
+ vars[idx].value = js_dup(value_); \
+ vars[idx].is_arg = (is_arg_); \
+ vars[idx].scope_level = (vd_)->scope_level; \
+ idx++; \
+ } while (0)
+
+ for (int i = 0; i < b->arg_count; i++) {
+ JSVarDef *vd = &b->vardefs[i];
+ APPEND_VAR(vd, sf->arg_buf[i], true);
+ }
+
+ for (int i = 0; i < b->var_count; i++) {
+ JSVarDef *vd = &b->vardefs[b->arg_count + i];
+ APPEND_VAR(vd, sf->var_buf[i], false);
+ }
+
+#undef APPEND_VAR
+
+ if (idx == 0) {
+ js_free(ctx, vars);
+ return 0;
+ }
+
+ if (pvars)
+ *pvars = vars;
+ if (pcount)
+ *pcount = idx;
+ return 0;
+
+fail:
+ for (int i = 0; i < idx; i++) {
+ JS_FreeCString(ctx, vars[i].name);
+ JS_FreeValue(ctx, vars[i].value);
+ }
+ js_free(ctx, vars);
+ return -1;
+}
+
+void JS_FreeLocalVariables(JSContext *ctx, JSDebugLocalVar *vars, int count)
+{
+ if (!vars)
+ return;
+ for (int i = 0; i < count; i++) {
+ JS_FreeCString(ctx, vars[i].name);
+ JS_FreeValue(ctx, vars[i].value);
+ }
+ js_free(ctx, vars);
+}
+
typedef enum JSFreeModuleEnum {
JS_FREE_MODULE_ALL,
JS_FREE_MODULE_NOT_RESOLVED,
@@ -17687,6 +17826,44 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
JSValue *call_argv;
SWITCH(pc) {
+ CASE(OP_debug):
+ if (unlikely(ctx->debug_trace)) {
+ int col_num = 0;
+ int line_num = -1;
+ uint32_t pc_index = (uint32_t)(pc - b->byte_code_buf - 1);
+ line_num = find_line_num(ctx, b, pc_index, &col_num);
+
+ /* Use JS_AtomToCString to get the full filename / funcname
+ without the 63-byte truncation that a stack buffer would
+ impose. The pointers are only valid for the duration of
+ the callback. */
+ const char *filename = JS_AtomToCString(ctx, b->filename);
+ if (unlikely(!filename)) {
+ /* OOM: a pending exception has been raised */
+ goto exception;
+ }
+ const char *funcname = JS_AtomToCString(ctx, b->func_name);
+ if (unlikely(!funcname)) {
+ JS_FreeCString(ctx, filename);
+ goto exception;
+ }
+ int ret = ctx->debug_trace(ctx, filename, funcname,
+ line_num, col_num,
+ ctx->debug_trace_opaque);
+ JS_FreeCString(ctx, filename);
+ JS_FreeCString(ctx, funcname);
+
+ if (ret != 0 || JS_HasException(ctx)) {
+ /* If the callback indicated failure but did not raise
+ an exception itself, synthesize a default one so the
+ caller never observes JS_UNINITIALIZED via
+ JS_GetException(). */
+ if (ret != 0 && !JS_HasException(ctx))
+ JS_ThrowInternalError(ctx, "aborted by debugger");
+ goto exception;
+ }
+ }
+ BREAK;
CASE(OP_push_i32):
*sp++ = js_int32(get_u32(pc));
pc += 4;
@@ -23410,6 +23587,20 @@ static void emit_source_loc(JSParseState *s)
emit_source_loc_at(s, s->token.line_num, s->token.col_num);
}
+static void emit_debug(JSParseState *s)
+{
+ if (unlikely(s->ctx->debug_trace))
+ dbuf_putc(&s->cur_func->byte_code, OP_debug);
+}
+
+static void emit_source_loc_debug(JSParseState *s)
+{
+ if (unlikely(s->ctx->debug_trace)) {
+ emit_source_loc(s);
+ emit_debug(s);
+ }
+}
+
static void emit_op(JSParseState *s, uint8_t val)
{
JSFunctionDef *fd = s->cur_func;
@@ -28774,6 +28965,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s,
goto fail;
break;
case TOK_RETURN:
+ emit_source_loc_debug(s);
if (s->cur_func->is_eval) {
js_parse_error(s, "return not in a function");
goto fail;
@@ -28802,6 +28994,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s,
goto fail;
}
emit_source_loc(s);
+ emit_debug(s);
if (js_parse_expr(s))
goto fail;
emit_op(s, OP_throw);
@@ -28825,6 +29018,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s,
goto fail;
}
s->cur_func->has_await = true;
+ emit_source_loc_debug(s);
if (next_token(s)) /* skip 'using' */
goto fail;
if (js_parse_var(s, PF_IN_ACCEPTED | PF_AWAIT_USING, TOK_USING, /*export_flag*/false))
@@ -28847,6 +29041,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s,
}
/* fall thru */
case TOK_VAR:
+ emit_source_loc_debug(s);
if (next_token(s))
goto fail;
if (js_parse_var(s, PF_IN_ACCEPTED, tok, /*export_flag*/false))
@@ -28857,6 +29052,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s,
case TOK_IF:
{
int label1, label2, mask;
+ emit_source_loc_debug(s);
if (next_token(s))
goto fail;
/* create a new scope for `let f;if(1) function f(){}` */
@@ -28967,6 +29163,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s,
int source_line_num, source_col_num;
bool is_async;
+ emit_source_loc_debug(s);
source_line_num = s->token.line_num;
source_col_num = s->token.col_num;
if (next_token(s))
@@ -29202,6 +29399,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s,
int default_label_pos;
BlockEnv break_entry;
+ emit_source_loc_debug(s);
if (next_token(s))
goto fail;
@@ -29553,6 +29751,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s,
js_parse_error(s, "using declaration is not allowed at the top level of a script");
goto fail;
}
+ emit_source_loc_debug(s);
if (next_token(s))
goto fail;
if (js_parse_var(s, PF_IN_ACCEPTED, TOK_USING, /*export_flag*/false))
@@ -29606,6 +29805,7 @@ static __exception int js_parse_statement_or_decl(JSParseState *s,
default:
hasexpr:
emit_source_loc(s);
+ emit_debug(s);
if (js_parse_expr(s))
goto fail;
if (s->cur_func->eval_ret_idx >= 0) {
@@ -33969,6 +34169,8 @@ static bool code_match(CodeContext *s, int pos, ...)
line_num = get_u32(tab + pos + 1);
col_num = get_u32(tab + pos + 5);
pos = pos_next;
+ } else if (op == OP_debug) {
+ pos = pos_next;
} else {
break;
}
@@ -34256,6 +34458,9 @@ static int get_label_pos(JSFunctionDef *s, int label)
case OP_source_loc:
pos += 9;
continue;
+ case OP_debug:
+ pos += 1;
+ continue;
case OP_label:
pos += 5;
continue;
@@ -35017,6 +35222,13 @@ static __exception int resolve_labels(JSContext *ctx, JSFunctionDef *s)
col_num = get_u32(bc_buf + pos + 5);
break;
+ case OP_debug:
+ /* record pc2line so the debugger can resolve the source
+ location when OP_debug is hit at runtime */
+ add_pc2line_info(s, bc_out.size, line_num, col_num);
+ dbuf_putc(&bc_out, OP_debug);
+ break;
+
case OP_label:
{
label = get_u32(bc_buf + pos + 1);
diff --git a/quickjs.h b/quickjs.h
index b9ed27560..498ee1a2b 100644
--- a/quickjs.h
+++ b/quickjs.h
@@ -523,6 +523,62 @@ JS_EXTERN void JS_SetClassProto(JSContext *ctx, JSClassID class_id, JSValue obj)
JS_EXTERN JSValue JS_GetClassProto(JSContext *ctx, JSClassID class_id);
JS_EXTERN JSValue JS_GetFunctionProto(JSContext *ctx);
+/* Debug callback - invoked when the interpreter hits an OP_debug opcode.
+ Return 0 to continue execution. Return non-zero to abort execution at
+ this point: the engine will jump to the exception handler. The
+ callback may itself call JS_Throw* to provide a specific exception;
+ if the callback returns non-zero without having raised one, the engine
+ will synthesize a default InternalError("aborted by debugger"). If
+ the callback raises an exception via JS_Throw* but returns 0, the
+ engine still treats it as a request to abort.
+
+ The filename / funcname pointers passed to the callback are only valid
+ for the duration of the callback invocation; do not store them.
+
+ OP_debug opcodes are only emitted at statement boundaries when a debug
+ trace handler is registered at parse time. Therefore only code that
+ is parsed (e.g. by JS_Eval / JS_Compile) AFTER JS_SetDebugTraceHandler
+ has been called will be instrumented; previously compiled bytecode
+ will not invoke the callback. In practice, install the handler before
+ evaluating any application code. */
+typedef int JSDebugTraceFunc(JSContext *ctx,
+ const char *filename,
+ const char *funcname,
+ int line,
+ int col,
+ void *opaque);
+
+/* Set (or clear) the debug trace handler on a context. Pass NULL to
+ disable. Works with any context, including those created with
+ JS_NewContextRaw. See JSDebugTraceFunc above for the parse-time
+ instrumentation contract. */
+JS_EXTERN void JS_SetDebugTraceHandler(JSContext *ctx,
+ JSDebugTraceFunc *cb,
+ void *opaque);
+
+/* Debug API: Get local variables in stack frames */
+typedef struct JSDebugLocalVar {
+ const char *name;
+ JSValue value;
+ bool is_arg;
+ int scope_level;
+} JSDebugLocalVar;
+
+/* Get the call stack depth (0 when no frames are active). */
+JS_EXTERN int JS_GetStackDepth(JSContext *ctx);
+
+/* Get local variables at a specific stack level (0 = current frame, 1 = caller, etc.).
+ On success, *pvars receives an allocated array of JSDebugLocalVar entries
+ that must be freed with JS_FreeLocalVariables(), and *pcount receives the
+ entry count. If no variables are available, *pvars is set to NULL and
+ *pcount is set to 0. Returns -1 on exception. */
+JS_EXTERN int JS_GetLocalVariablesAtLevel(JSContext *ctx, int level,
+ JSDebugLocalVar **pvars,
+ int *pcount);
+
+/* Free local variables array returned by JS_GetLocalVariablesAtLevel */
+JS_EXTERN void JS_FreeLocalVariables(JSContext *ctx, JSDebugLocalVar *vars, int count);
+
/* the following functions are used to select the intrinsic object to
save memory */
JS_EXTERN JSContext *JS_NewContextRaw(JSRuntime *rt);
From 80733f94ace92cf5dc4ab972ac61790facab8729 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=9F=AD=E8=8F=9C=E9=92=9F?= <312780179@qq.com>
Date: Fri, 22 May 2026 10:00:02 +0800
Subject: [PATCH 2/4] refactor: change filename and funcname parameters to
JSAtom in debug trace functions
suggestion form @jprendes at https://github.com/quickjs-ng/quickjs/pull/1421#discussion_r3282209707
---
api-test.c | 20 +++++++++++---------
quickjs.c | 22 +++++-----------------
quickjs.h | 15 ++++++++++++---
3 files changed, 28 insertions(+), 29 deletions(-)
diff --git a/api-test.c b/api-test.c
index ee80995f0..300f02f83 100644
--- a/api-test.c
+++ b/api-test.c
@@ -1017,16 +1017,16 @@ static struct {
int call_count;
int last_line;
int last_col;
- char last_filename[256];
- char last_funcname[256];
+ JSAtom last_filename;
+ JSAtom last_funcname;
int stack_depth;
int max_local_count;
int abort_at; /* abort (return -1) on this call, 0 = never */
} trace_state;
static int debug_trace_cb(JSContext *ctx,
- const char *filename,
- const char *funcname,
+ JSAtom filename,
+ JSAtom funcname,
int line,
int col,
void *opaque)
@@ -1034,10 +1034,8 @@ static int debug_trace_cb(JSContext *ctx,
trace_state.call_count++;
trace_state.last_line = line;
trace_state.last_col = col;
- snprintf(trace_state.last_filename, sizeof(trace_state.last_filename),
- "%s", filename);
- snprintf(trace_state.last_funcname, sizeof(trace_state.last_funcname),
- "%s", funcname);
+ trace_state.last_filename = filename;
+ trace_state.last_funcname = funcname;
trace_state.stack_depth = JS_GetStackDepth(ctx);
int count = 0;
JSDebugLocalVar *vars = NULL;
@@ -1072,7 +1070,11 @@ static void debug_trace(void)
assert(!JS_IsException(ret));
JS_FreeValue(ctx, ret);
assert(trace_state.call_count > 0);
- assert(!strcmp(trace_state.last_filename, ""));
+ {
+ const char *fn = JS_AtomToCString(ctx, trace_state.last_filename);
+ assert(fn && !strcmp(fn, ""));
+ JS_FreeCString(ctx, fn);
+ }
}
{
diff --git a/quickjs.c b/quickjs.c
index c46e5445a..008856bc7 100644
--- a/quickjs.c
+++ b/quickjs.c
@@ -17833,25 +17833,13 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
uint32_t pc_index = (uint32_t)(pc - b->byte_code_buf - 1);
line_num = find_line_num(ctx, b, pc_index, &col_num);
- /* Use JS_AtomToCString to get the full filename / funcname
- without the 63-byte truncation that a stack buffer would
- impose. The pointers are only valid for the duration of
- the callback. */
- const char *filename = JS_AtomToCString(ctx, b->filename);
- if (unlikely(!filename)) {
- /* OOM: a pending exception has been raised */
- goto exception;
- }
- const char *funcname = JS_AtomToCString(ctx, b->func_name);
- if (unlikely(!funcname)) {
- JS_FreeCString(ctx, filename);
- goto exception;
- }
- int ret = ctx->debug_trace(ctx, filename, funcname,
+ /* Pass the JSAtom values directly — no heap allocation.
+ The atoms are valid for the lifetime of the bytecode
+ object, which outlives the callback. The embedder must
+ call JS_DupAtom() if it needs to retain them. */
+ int ret = ctx->debug_trace(ctx, b->filename, b->func_name,
line_num, col_num,
ctx->debug_trace_opaque);
- JS_FreeCString(ctx, filename);
- JS_FreeCString(ctx, funcname);
if (ret != 0 || JS_HasException(ctx)) {
/* If the callback indicated failure but did not raise
diff --git a/quickjs.h b/quickjs.h
index 498ee1a2b..55b3276a4 100644
--- a/quickjs.h
+++ b/quickjs.h
@@ -540,10 +540,19 @@ JS_EXTERN JSValue JS_GetFunctionProto(JSContext *ctx);
is parsed (e.g. by JS_Eval / JS_Compile) AFTER JS_SetDebugTraceHandler
has been called will be instrumented; previously compiled bytecode
will not invoke the callback. In practice, install the handler before
- evaluating any application code. */
+ evaluating any application code.
+
+ 'filename' and 'funcname' are JSAtom values identifying the source file
+ and enclosing function name. They are valid only for the duration of
+ the callback; call JS_DupAtom() if you need to retain them. Either
+ may be JS_ATOM_NULL (0) for anonymous functions or eval code. Use
+ JS_AtomToCString() / JS_AtomToString() to convert to a C string or
+ JSValue when needed. Accepting JSAtom avoids a heap allocation on
+ every instrumented statement when the embedder only needs to compare
+ against a known set of breakpoint locations. */
typedef int JSDebugTraceFunc(JSContext *ctx,
- const char *filename,
- const char *funcname,
+ JSAtom filename,
+ JSAtom funcname,
int line,
int col,
void *opaque);
From db61e3c2b2876f49fe645053e308d939ac309682 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=9F=AD=E8=8F=9C=E9=92=9F?= <312780179@qq.com>
Date: Fri, 22 May 2026 10:04:48 +0800
Subject: [PATCH 3/4] refactor: remove outdated comments regarding filename and
funcname pointers in debug trace handler
---
quickjs.h | 3 ---
1 file changed, 3 deletions(-)
diff --git a/quickjs.h b/quickjs.h
index 55b3276a4..0bec35988 100644
--- a/quickjs.h
+++ b/quickjs.h
@@ -532,9 +532,6 @@ JS_EXTERN JSValue JS_GetFunctionProto(JSContext *ctx);
the callback raises an exception via JS_Throw* but returns 0, the
engine still treats it as a request to abort.
- The filename / funcname pointers passed to the callback are only valid
- for the duration of the callback invocation; do not store them.
-
OP_debug opcodes are only emitted at statement boundaries when a debug
trace handler is registered at parse time. Therefore only code that
is parsed (e.g. by JS_Eval / JS_Compile) AFTER JS_SetDebugTraceHandler
From c64d4894db5f1f97d9999da11ea34f6ef135b5ad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=9F=AD=E8=8F=9C=E9=92=9F?= <312780179@qq.com>
Date: Fri, 22 May 2026 10:20:33 +0800
Subject: [PATCH 4/4] refactor: change last_filename and last_funcname from
JSAtom to char arrays in debug trace
Update api-test.c to match the new signature, converting to string inside
the callback while the atoms are still guaranteed to be valid.
---
api-test.c | 27 ++++++++++++++++++---------
1 file changed, 18 insertions(+), 9 deletions(-)
diff --git a/api-test.c b/api-test.c
index 300f02f83..403cfec49 100644
--- a/api-test.c
+++ b/api-test.c
@@ -1017,8 +1017,8 @@ static struct {
int call_count;
int last_line;
int last_col;
- JSAtom last_filename;
- JSAtom last_funcname;
+ char last_filename[256];
+ char last_funcname[256];
int stack_depth;
int max_local_count;
int abort_at; /* abort (return -1) on this call, 0 = never */
@@ -1034,8 +1034,21 @@ static int debug_trace_cb(JSContext *ctx,
trace_state.call_count++;
trace_state.last_line = line;
trace_state.last_col = col;
- trace_state.last_filename = filename;
- trace_state.last_funcname = funcname;
+ /* Convert while the atom is still valid (within callback lifetime).
+ Embedders who only need to compare against known breakpoint atoms
+ can skip this conversion entirely. */
+ const char *fn = JS_AtomToCString(ctx, filename);
+ if (fn) {
+ snprintf(trace_state.last_filename, sizeof(trace_state.last_filename),
+ "%s", fn);
+ JS_FreeCString(ctx, fn);
+ }
+ const char *fnn = JS_AtomToCString(ctx, funcname);
+ if (fnn) {
+ snprintf(trace_state.last_funcname, sizeof(trace_state.last_funcname),
+ "%s", fnn);
+ JS_FreeCString(ctx, fnn);
+ }
trace_state.stack_depth = JS_GetStackDepth(ctx);
int count = 0;
JSDebugLocalVar *vars = NULL;
@@ -1070,11 +1083,7 @@ static void debug_trace(void)
assert(!JS_IsException(ret));
JS_FreeValue(ctx, ret);
assert(trace_state.call_count > 0);
- {
- const char *fn = JS_AtomToCString(ctx, trace_state.last_filename);
- assert(fn && !strcmp(fn, ""));
- JS_FreeCString(ctx, fn);
- }
+ assert(!strcmp(trace_state.last_filename, ""));
}
{