From 0bd7e3be7780a35738e68da7a2e59cbc7d1c7770 Mon Sep 17 00:00:00 2001 From: Rob Knopf Date: Sat, 17 Oct 2026 17:08:46 -0400 Subject: [PATCH 1/2] Cppia debugger: read a function's variables from the right frame base With HXCPP_STACK_SCRIPTABLE, the debugger reads a cppia function's `this` and variables relative to its hx::StackFrame, taking the frame base to be `inStack - stackSize`. That isn't where the frame starts. At a call, ctx->frame is set to the stack pointer before `this` and the arguments are pushed (pushArgs / AutoStack), and the function's StackLayout already counts the `this` slot and the arguments, so runFunction's `pointer += stackSize` allocates the whole layout again on top of them. The StackFrame is then pushed at frame + sizeof(hx::Object *) + argBytes + stackSize so `inStack - stackSize` lands on the first argument (or the first local, with no arguments), not on `this`. Inspecting variables in a cppia function with arguments read an Int argument as `this` and crashed; locals read from the wrong slots. scriptableBase() computes the base from the StackFrame by subtracting the layout size, the `this` slot and the arguments' bytes, and getScriptableVariables, getScriptableValue and setScriptableValue use it. The closure path (CppiaClosure::doRun) writes `this` to ctx->frame and lays out the same, so it's covered too. --- src/hx/cppia/CppiaFunction.cpp | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/hx/cppia/CppiaFunction.cpp b/src/hx/cppia/CppiaFunction.cpp index 005977b36..8618afde3 100644 --- a/src/hx/cppia/CppiaFunction.cpp +++ b/src/hx/cppia/CppiaFunction.cpp @@ -201,11 +201,30 @@ CppiaExpr *ScriptCallable::link(CppiaModule &inModule) } #ifdef HXCPP_STACK_SCRIPTABLE + +// Stack layout for CPPIA functions (both vtable dispatch and CppiaClosure paths): +// ctx->frame (= base) = P +// P+0: 'this' pointer (pushed by host or written by doRun) +// P+8: arg0 (stackPos=8) +// P+8+argBytes: arg1... locals (stackPos=8+argBytes...) +// P+8+argBytes+stackSize: StackFrame <- inStack +// +// So: base = inStack - stackSize - sizeof(Object*) - argBytes = ctx->frame +// Variables are at base + var->stackPos. +static unsigned char *scriptableBase(unsigned char *inStack, int stackSize, + const std::vector &args) +{ + int argBytes = 0; + for (int i = 0; i < (int)args.size(); i++) + argBytes += sTypeSize[args[i].argType]; + return inStack - stackSize - sizeof(hx::Object *) - argBytes; +} + void ScriptCallable::getScriptableVariables(unsigned char *inStack, Array outNames) { - unsigned char *frame = inStack - stackSize; + unsigned char *base = scriptableBase(inStack, stackSize, args); - hx::Object *thizz = *(hx::Object **)frame; + hx::Object *thizz = *(hx::Object **)base; if (thizz) outNames->push(HX_CSTRING("this")); @@ -219,10 +238,10 @@ void ScriptCallable::getScriptableVariables(unsigned char *inStack, Arraysecond; if ( var->module->strings[ var->nameId]==inName) { - outValue = var->getInFrame(frame); + outValue = var->getInFrame(base); return true; } } @@ -243,13 +262,13 @@ bool ScriptCallable::getScriptableValue(unsigned char *inStack, String inName, : bool ScriptCallable::setScriptableValue(unsigned char *inStack, String inName, ::Dynamic inValue) { - unsigned char *frame = inStack - stackSize; + unsigned char *base = scriptableBase(inStack, stackSize, args); for(CppiaStackVarMap::iterator i=varMap.begin();i!=varMap.end();++i) { CppiaStackVar *var = i->second; if ( var->module->strings[ var->nameId]==inName) { - var->setInFrame(frame,inValue); + var->setInFrame(base,inValue); return true; } } From be3fb7d84719d28222994a383579da7bd70d89f5 Mon Sep 17 00:00:00 2001 From: Rob Knopf Date: Wed, 21 Oct 2026 14:03:14 -0400 Subject: [PATCH 2/2] Cppia debugger: jitted frames keep their base; zero a jitted frame's locals and debugger lists The previous commit's base is right for interpreted functions only. Jitted code lays its frame out differently: it pushes the StackFrame at its frame pointer + stackSize, so inStack - stackSize was right there, and the interpreter's base reads the wrong slots of a jitted method (and crashed in testing). The base is now chosen by whether the function is compiled. Two more problems show up under the JIT with the debugger: the prologue set the StackFrame's position but not its variables and catchables lists, which the debugger walks, and it never zeroed the frame's locals, so reading one not yet assigned (a String, say) read stale data and could crash. Debugger builds now clear both, as the interpreter and the CppiaStackFrame constructor do. This follows openfl/libhxcpp@57f3042 by Chris Speciale, which found the JIT cases. test/regression/CppiaDebuggerFrames reads, through cpp.vm.Debugger, the arguments and locals of methods (mixed Int, Float, String and object arguments, defaults, a call through Reflect.callMethod) and of a static function, and sets two of them, in the interpreter and the JIT. Before this branch: the interpreter read zeros, and the jitted static function crashed. With only the previous commit: the jitted method crashed. Co-authored-by: Chris Speciale <26172437+dimensionscape@users.noreply.github.com> --- src/hx/cppia/Cppia.h | 3 + src/hx/cppia/CppiaFunction.cpp | 63 +++++++++++++++---- .../regression/CppiaDebuggerFrames/Inspect.hx | 28 +++++++++ test/regression/CppiaDebuggerFrames/Main.hx | 16 +++++ test/regression/CppiaDebuggerFrames/Script.hx | 46 ++++++++++++++ .../regression/CppiaDebuggerFrames/build.hxml | 8 +++ .../regression/CppiaDebuggerFrames/stdout.txt | 12 ++++ 7 files changed, 163 insertions(+), 13 deletions(-) create mode 100644 test/regression/CppiaDebuggerFrames/Inspect.hx create mode 100644 test/regression/CppiaDebuggerFrames/Main.hx create mode 100644 test/regression/CppiaDebuggerFrames/Script.hx create mode 100644 test/regression/CppiaDebuggerFrames/build.hxml create mode 100644 test/regression/CppiaDebuggerFrames/stdout.txt diff --git a/src/hx/cppia/Cppia.h b/src/hx/cppia/Cppia.h index 42085ed56..468681fac 100644 --- a/src/hx/cppia/Cppia.h +++ b/src/hx/cppia/Cppia.h @@ -304,6 +304,9 @@ struct ScriptCallable : public CppiaDynamicExpr #ifdef CPPIA_JIT void compile(); void genDefaults(CppiaCompiler *compiler); + #ifdef HXCPP_DEBUGGER + void genZeroLocals(CppiaCompiler *compiler); + #endif void genArgs(CppiaCompiler *compiler, CppiaExpr *inThis, Expressions &inArgs, const JitVal &inThisVal); void genCode(CppiaCompiler *compiler,const JitVal &inDest=JitVal(),ExprType type=etNull) HXCPP_OVERRIDE; #endif diff --git a/src/hx/cppia/CppiaFunction.cpp b/src/hx/cppia/CppiaFunction.cpp index 8618afde3..3865ff07e 100644 --- a/src/hx/cppia/CppiaFunction.cpp +++ b/src/hx/cppia/CppiaFunction.cpp @@ -202,18 +202,18 @@ CppiaExpr *ScriptCallable::link(CppiaModule &inModule) #ifdef HXCPP_STACK_SCRIPTABLE -// Stack layout for CPPIA functions (both vtable dispatch and CppiaClosure paths): -// ctx->frame (= base) = P -// P+0: 'this' pointer (pushed by host or written by doRun) -// P+8: arg0 (stackPos=8) -// P+8+argBytes: arg1... locals (stackPos=8+argBytes...) -// P+8+argBytes+stackSize: StackFrame <- inStack -// -// So: base = inStack - stackSize - sizeof(Object*) - argBytes = ctx->frame -// Variables are at base + var->stackPos. +// The base holds 'this', then the arguments and locals at their stackPos +// (stackSize covers all of them). Interpreted (both vtable dispatch and +// CppiaClosure paths), 'this' and the arguments are pushed at ctx->frame, and +// runFunction then allocates the whole layout again before the StackFrame: +// base = ctx->frame; StackFrame (inStack) = base + sizeof(Object*) + argBytes + stackSize +// Jitted code pushes its StackFrame right after the layout: +// StackFrame (inStack) = base + stackSize static unsigned char *scriptableBase(unsigned char *inStack, int stackSize, - const std::vector &args) + const std::vector &args, bool inCompiled) { + if (inCompiled) + return inStack - stackSize; int argBytes = 0; for (int i = 0; i < (int)args.size(); i++) argBytes += sTypeSize[args[i].argType]; @@ -222,7 +222,7 @@ static unsigned char *scriptableBase(unsigned char *inStack, int stackSize, void ScriptCallable::getScriptableVariables(unsigned char *inStack, Array outNames) { - unsigned char *base = scriptableBase(inStack, stackSize, args); + unsigned char *base = scriptableBase(inStack, stackSize, args, compiled!=0); hx::Object *thizz = *(hx::Object **)base; if (thizz) @@ -238,7 +238,7 @@ void ScriptCallable::getScriptableVariables(unsigned char *inStack, Arraysecond; @@ -383,6 +383,27 @@ void ScriptCallable::genArgs(CppiaCompiler *compiler, CppiaExpr *inThis, Express } +#ifdef HXCPP_DEBUGGER +static void SLJIT_CALL zeroLocals(unsigned char *inLocals, void *inSize) +{ + memset(inLocals, 0, (size_t)inSize); +} + +// The interpreter zeroes a frame's locals; jitted code writes each before +// reading it, but the debugger can read any of them at any time +void ScriptCallable::genZeroLocals(CppiaCompiler *compiler) +{ + int argEnd = sizeof(void *); + for(int i=0;i argEnd) + { + compiler->add(sJitArg1.as(jtPointer), sJitFrame.as(jtPointer), argEnd ); + compiler->callNative( (void *)zeroLocals, sJitArg1.as(jtPointer), JitVal((void *)(size_t)(stackSize - argEnd)) ); + } +} +#endif + void ScriptCallable::genDefaults(CppiaCompiler *compiler) { if (hasDefaults) @@ -665,10 +686,18 @@ void ScriptCallable::compile() CppiaCompiler *compiler = CppiaCompiler::create(size); // First pass calculates size... + #ifdef HXCPP_DEBUGGER + genZeroLocals(compiler); + #endif genDefaults(compiler); #ifdef HXCPP_STACK_TRACE compiler->move(sJitFrame.star(jtPointer)+(offsetof(StackFrame,position) + stackSize), (void *)&position ); + #if defined(HXCPP_STACK_LINE) && defined(HXCPP_DEBUGGER) + // As the StackFrame constructor does - the debugger walks these lists + compiler->move(sJitFrame.star(jtPointer)+(offsetof(StackFrame,variables) + stackSize), (void *)0 ); + compiler->move(sJitFrame.star(jtPointer)+(offsetof(StackFrame,catchables) + stackSize), (void *)0 ); + #endif compiler->add(sJitArg1.as(jtPointer), sJitFrame.as(jtPointer), stackSize ); compiler->callNative( (void *)pushFrame, sJitCtx.as(jtPointer), sJitArg1.as(jtPointer) ); @@ -683,10 +712,18 @@ void ScriptCallable::compile() compiler->beginGeneration(1); // Second pass does the job + #ifdef HXCPP_DEBUGGER + genZeroLocals(compiler); + #endif genDefaults(compiler); #ifdef HXCPP_STACK_TRACE compiler->move(sJitFrame.star(jtPointer)+(offsetof(StackFrame,position) + stackSize), (void *)&position ); + #if defined(HXCPP_STACK_LINE) && defined(HXCPP_DEBUGGER) + // As the StackFrame constructor does - the debugger walks these lists + compiler->move(sJitFrame.star(jtPointer)+(offsetof(StackFrame,variables) + stackSize), (void *)0 ); + compiler->move(sJitFrame.star(jtPointer)+(offsetof(StackFrame,catchables) + stackSize), (void *)0 ); + #endif compiler->add(sJitArg1.as(jtPointer), sJitFrame.as(jtPointer), stackSize ); compiler->callNative( (void *)pushFrame, sJitCtx.as(jtPointer), sJitArg1.as(jtPointer) ); #endif diff --git a/test/regression/CppiaDebuggerFrames/Inspect.hx b/test/regression/CppiaDebuggerFrames/Inspect.hx new file mode 100644 index 000000000..6a94f0137 --- /dev/null +++ b/test/regression/CppiaDebuggerFrames/Inspect.hx @@ -0,0 +1,28 @@ +import cpp.vm.Debugger; + +class Inspect { + static function frame(name:String):Int { + var thread = Debugger.getCurrentThreadNumber(); + var stack = Debugger.getThreadInfo(thread, true).stack; + for (i in 0...stack.length) + if (stack[i].functionName == name) + return i; + return -1; + } + + public static function caller(name:String):String { + var thread = Debugger.getCurrentThreadNumber(); + var i = frame(name); + if (i < 0) + return "no frame for " + name; + var names = Debugger.getStackVariables(thread, i, true); + names.sort(Reflect.compare); + var values = [for (n in names) n == "this" ? "this=" + Std.string(Debugger.getStackVariableValue(thread, i, n, true) != null) + : n + "=" + Std.string(Debugger.getStackVariableValue(thread, i, n, true))]; + return values.join(" "); + } + + public static function set(name:String, variable:String, value:Dynamic) { + Debugger.setStackVariableValue(Debugger.getCurrentThreadNumber(), frame(name), variable, value, true); + } +} diff --git a/test/regression/CppiaDebuggerFrames/Main.hx b/test/regression/CppiaDebuggerFrames/Main.hx new file mode 100644 index 000000000..79f6a6c60 --- /dev/null +++ b/test/regression/CppiaDebuggerFrames/Main.hx @@ -0,0 +1,16 @@ +// hx::EnableJit, which Host.enableJit calls +@:cppInclude("hx/Scriptable.h") +class Main { + static function main() { + Inspect.caller(""); + var script = haxe.io.Path.join([haxe.io.Path.directory(Sys.programPath()), "script.cppia"]); + for (jit in [false, true]) { + cpp.cppia.Host.enableJit(jit); + try { + cpp.cppia.Host.runFile(script); + } catch (e:Dynamic) { + Sys.println("error: " + e); + } + } + } +} diff --git a/test/regression/CppiaDebuggerFrames/Script.hx b/test/regression/CppiaDebuggerFrames/Script.hx new file mode 100644 index 000000000..001c2480e --- /dev/null +++ b/test/regression/CppiaDebuggerFrames/Script.hx @@ -0,0 +1,46 @@ +class Script { + var label:String; + + function new() + label = "probe"; + + function probe(a:Int, b:String):String { + var c = a * 2; + var seen = Inspect.caller("probe"); + return seen + (c > 0 ? "" : ""); + } + + function mixed(i:Int, f:Float, s:String, j:Int, o:Array):String { + var k = i + j; + var seen = Inspect.caller("mixed"); + return seen + (k > 0 && f > 0 && s != null && o != null ? "" : ""); + } + + function defaults(a:Int, b:Int = 7, ?c:String):String { + var seen = Inspect.caller("defaults"); + return seen + (a + b > 0 && c == null ? "" : ""); + } + + function setter(n:Int):String { + var m = n; + Inspect.set("setter", "m", 99); + Inspect.set("setter", "n", 5); + return "m=" + m + " n=" + n; + } + + static function staticProbe(x:Float):String { + var y = x + 1; + var seen = Inspect.caller("staticProbe"); + return seen + (y > 0 ? "" : ""); + } + + static function main() { + var s = new Script(); + Sys.println("method: " + s.probe(21, "two")); + Sys.println("mixed: " + s.mixed(1, 2.5, "s", 3, [4])); + Sys.println("defaults: " + s.defaults(1)); + Sys.println("dynamic: " + Reflect.callMethod(s, Reflect.field(s, "mixed"), [5, 0.5, "d", 6, []])); + Sys.println("setter: " + s.setter(1)); + Sys.println("static: " + staticProbe(1.5)); + } +} diff --git a/test/regression/CppiaDebuggerFrames/build.hxml b/test/regression/CppiaDebuggerFrames/build.hxml new file mode 100644 index 000000000..587f63357 --- /dev/null +++ b/test/regression/CppiaDebuggerFrames/build.hxml @@ -0,0 +1,8 @@ +--main Script +-cppia bin/script.cppia +--next +--main Main +-cpp bin +-D scriptable +-D HXCPP_DEBUGGER +--dce no diff --git a/test/regression/CppiaDebuggerFrames/stdout.txt b/test/regression/CppiaDebuggerFrames/stdout.txt new file mode 100644 index 000000000..1e2aff7ea --- /dev/null +++ b/test/regression/CppiaDebuggerFrames/stdout.txt @@ -0,0 +1,12 @@ +method: a=21 b=two c=42 seen=null this=true +mixed: f=2.5 i=1 j=3 k=4 o=[4] s=s seen=null this=true +defaults: a=1 b=7 c=null seen=null this=true +dynamic: f=0.5 i=5 j=6 k=11 o=[] s=d seen=null this=true +setter: m=99 n=5 +static: seen=null x=1.5 y=2.5 +method: a=21 b=two c=42 seen=null this=true +mixed: f=2.5 i=1 j=3 k=4 o=[4] s=s seen=null this=true +defaults: a=1 b=7 c=null seen=null this=true +dynamic: f=0.5 i=5 j=6 k=11 o=[] s=d seen=null this=true +setter: m=99 n=5 +static: seen=null x=1.5 y=2.5