feat(baselib): debugstack takes a coroutine

debugstack([thread,] [start [, count1 [, count2]]]) returns the stack
of a coroutine when one is passed first, with start defaulting to 0
(the coroutine's innermost level), as 4.3.4's Script_debugstack does.

1.12.1's debugstack (0x703760) has no thread argument. A thread fails
its lua_isnumber and lua_isstring checks, so it returns the thread
itself, and msg .. debugstack(co) raises "attempt to concatenate a
thread value" in the coroutine error paths of WeakAuras, Questie's
ThreadLib, MRT and Zygor.

The engine function still builds the text, so its line format and its
12/10-level head and tail windows are unchanged, which keeps addons
that parse debugstack() for their folder working. It reads levels
through six calls on the state it runs on: lua_getstack (0x6FBAA0) at
0x70383F, 0x70386E, 0x703896, 0x7038AD and 0x703998, and
lua_getinfo (0x6FBC70) at 0x7038CD. Those calls are redirected to
read from the state the new global names for the call: the thread, or
the calling state. The engine function has no callers of its own, so
the global is its only way in and sets that state for every call; an
error part way cannot leave a thread behind for the next call.

The global removes the thread argument, inserts 0 as start when none
is given, and calls the engine function directly in its own frame, so
levels without a thread count exactly as before.
This commit is contained in:
Brues
2026-11-03 15:33:30 -06:00
parent 05fa5845b7
commit 12a81f7f89
3 changed files with 127 additions and 0 deletions
+13
View File
@@ -678,6 +678,7 @@ build instructions.
- [`difftime(time2 [, time1])`](#difftimetime2--time1)
- [`xpcall(f, msgh [, arg1, ...])`](#xpcallf-msgh--arg1-)
- [`GetCallstackHeight()` / `GetErrorCallstackHeight()` / `SetErrorCallstackHeight([height])`](#getcallstackheight--geterrorcallstackheight--seterrorcallstackheightheight)
- [`debugstack(thread [, start [, count1 [, count2]]])`](#debugstackthread--start--count1--count2)
- [`debuglocals([level [, skipFunctionsAndUserdata]])`](#debuglocalslevel--skipfunctionsanduserdata)
- [`collectgarbage(opt [, arg])`](#collectgarbageopt--arg)
- [`table.wipe(t)` / `wipe(t)`](#tablewipet--wipet)
@@ -16137,6 +16138,18 @@ end
Works in the world and on the login screen.
### `debugstack(thread [, start [, count1 [, count2]]])`
With a coroutine as the first argument, `debugstack` returns the call stack
of that coroutine. `start` is 0 by default, which is the innermost level of
the coroutine. Without a coroutine, `debugstack` works as before.
```lua
local co = coroutine.create(function() coroutine.yield() end)
coroutine.resume(co)
print(debugstack(co))
```
### `debuglocals([level [, skipFunctionsAndUserdata]])`
Returns the local variables and the upvalues of a call level as one string.
+12
View File
@@ -8809,6 +8809,18 @@ enum Offsets {
// name ("" for a C closure), or null for no such upvalue or a value that
// is not a function. (0x006F45B0 is lua_pushupvalues.)
LUA_GET_UPVALUE = 0x006F4660,
// `debugstack([start [, count1 [, count2]]])` — the engine's C function
// (base-function table 0x811E28); no engine callers. It reads the stack
// of the state it is called on through five lua_getstack calls and one
// lua_getinfo("Snl"), and pushes its text onto that same state.
// BaseLib::DebugStack points those six calls at a thread's state.
FUN_SCRIPT_DEBUGSTACK = 0x00703760,
CALL_DEBUGSTACK_GETSTACK_FIRST = 0x0070383F,
CALL_DEBUGSTACK_GETSTACK_TAIL_PROBE = 0x0070386E,
CALL_DEBUGSTACK_GETSTACK_TAIL_START = 0x00703896,
CALL_DEBUGSTACK_GETSTACK_TAIL_SKIP = 0x007038AD,
CALL_DEBUGSTACK_GETSTACK_NEXT = 0x00703998,
CALL_DEBUGSTACK_GETINFO = 0x007038CD,
// `luaO_chunkid(out, source, bufflen)` — `void __fastcall(char *out /*ecx*/,
// const char *source /*edx*/, int bufflen /*stack*/)`, RET 4: the name a
// chunk has in error messages. `=name` is copied as is, `@file` is the
+102
View File
@@ -0,0 +1,102 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `debugstack([thread,] [start [, count1 [, count2]]])` — the stack of a
// coroutine, as 4.3.4's Script_debugstack takes it: with a thread first,
// the levels are that thread's and `start` defaults to 0 (the thread's
// innermost level) instead of 1.
//
// 1.12's debugstack (FUN_SCRIPT_DEBUGSTACK) has no thread argument, and a
// thread falls through its argument checks so that it returns the thread
// itself: `msg .. debugstack(co)` raises "attempt to concatenate a thread
// value" in the coroutine error paths of WeakAuras, Questie's ThreadLib,
// MRT and Zygor.
//
// The engine's function still does the work, so its text and its head and
// tail windows stay exactly as they are. It reads levels through six calls
// (five lua_getstack, one lua_getinfo), and those calls are redirected to
// read from the state Script_debugstack names for the call: the thread, or
// the calling state when there is none. Script_debugstack replaces the
// global and is the engine function's only way in, so the state is set for
// every call; an error part way cannot leave a thread behind for the next.
//
// Registered on both Lua states, like the base-library helpers.
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
#include <initializer_list>
namespace BaseLib::DebugStack {
namespace {
namespace Lua = Game::Lua;
// The state whose levels the engine's debugstack reads, or null for its own.
void *g_levelsOf = nullptr;
int __fastcall GetStack(void *L, int level, void *ar) {
return Lua::GetStack(g_levelsOf ? g_levelsOf : L, level, ar);
}
int __fastcall GetInfo(void *L, const char *what, void *ar) {
return Lua::GetInfo(g_levelsOf ? g_levelsOf : L, what, ar);
}
int __fastcall Script_debugstack(void *L) {
const auto engineDebugStack =
reinterpret_cast<Lua::CFunction>(Offsets::FUN_SCRIPT_DEBUGSTACK);
g_levelsOf = nullptr;
if (Lua::Type(L, 1) == Lua::TYPE_THREAD) {
g_levelsOf = Lua::ToThread(L, 1);
Lua::Remove(L, 1);
if (!Lua::IsNumber(L, 1)) { // start defaults to 0 for a thread
Lua::PushNumber(L, 0);
Lua::Insert(L, 1);
}
}
const int results = engineDebugStack(L);
g_levelsOf = nullptr;
return results;
}
void Apply() {
const auto getStack = reinterpret_cast<const void *>(&GetStack);
for (const uintptr_t site :
{Offsets::CALL_DEBUGSTACK_GETSTACK_FIRST, Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_PROBE,
Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_START, Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_SKIP,
Offsets::CALL_DEBUGSTACK_GETSTACK_NEXT})
Common::RedirectCall(site, Offsets::LUA_GET_STACK, getStack);
Common::RedirectCall(Offsets::CALL_DEBUGSTACK_GETINFO, Offsets::LUA_GET_INFO,
reinterpret_cast<const void *>(&GetInfo));
}
void RegisterInGame() {
Apply();
Lua::RegisterGlobalFunction("debugstack", &Script_debugstack);
}
void RegisterGlue() {
Apply();
Lua::RegisterGlueFunction("debugstack", &Script_debugstack);
}
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
} // namespace
} // namespace BaseLib::DebugStack