Lua Backtraces

Author: thothonegan
Tags: development lua

Lua is a great language for embedding into other projects. It's small, fast, and a lot of tools support it. One of the gotchas though is making sure you can debug the lua scripts nicely. Since its embedded, if you have a lot of custom stuff, you can only really test the scripts live, and thus can't use a lua debugger. And of course your C++ debugger will just show you the internals of lua, which aren't that useful. So here's something helpful!

Quick Overview of calling Lua

To use lua in your program, you create a state, tell it to run/resume a lua function, then deal with any errors that come back. So for example to run as a coroutine (which i highly suggest for reentrant purposes), it looks something like:

void runLuaFunction (lua_State* rootState, const char* functionName)
     lua_State* threadState = lua_newthread (rootState);      // +1 [thread]
     int threadRef = luaL_ref (rootState, LUA_REGISTRYINDEX); // -1 [turned into ref]
     lua_getglobal (threadState, functionName); // +1 [function from global table]
     int err = lua_resume (threadState, NULL, 0); // -1 [function to call, no args], +? for return values, and/or +? for debug stack if it errors
     if (err != LUA_OK && err != LUA_YIELD)
         // if an error occurs, the message is on top of the stack
         console.error() << "Error occurred : "
           << lua_tostring (threadState, -1) << WolfConsole::EndLine;
         // what more can we do? see below.
     luaL_unref (rootState, LUA_REGISTRYINDEX, threadRef); // can now GC the thread

This gets you the error message from either lua itself (invalid call, etc) or the script (via error). This is good enough for a lot of things, but as soon as you have scripts start calling other scripts via require, and an error three levels deep, you need more.

Adding Tracing

So the next part is to add the ability to walk the stack back, and output each call that occurred. As long as lua's debug support is still there, this is relatively easy [albeit verbose].

// write a trace
int level = 1; // starting level of the trace
lua_Debug ar; // this is where our debug information will be stored
memset (&ar, 0, sizeof(ar)); // clear out ar
console.error() << "stack traceback: " << WolfConsole::EndLine;

// while we still have another frame in the stack, fetch it into 'ar' and increase the level we're at.
while (lua_getstack (threadState, level++, &ar))
    // lua_getstack just gets enough to reference the stack, but none of the useful fields are filled in.
    // So we use lua_getinfo to pull more information about the frame and pull
    // it into 'ar'. Valid letters are:
    // - n : fills in the 'name' and 'namewhat'.
    // - S : fills in the'source', 'short_src', 'linedefined', 'lastlinedefined', and 'what'.
    // - l : fills in the 'currentline'.
    // - u : fills in the 'nups'
    // - f : will also push the function object onto the lua stack
    // - L : will also push a table onto the stack containing valid lines in the function.
    // - t : will fill in tail call information.
    // In this case we just want enough to output what file/line/etc we're at, so
    // we do S for source, l for currentline, n for names, and t for tail call.
    lua_getinfo (threadState, "Slnt", &ar);
    // now we can output the information!
    console.error() << "- " << ar.short_src << ":" << ar.currentline
        << " in " <<
        << (ar.istailcall ? " (... tail calls...)" : "")
        << WolfConsole::EndLine;

Now if lua ever errors, we get a nice log of the entire callstack. Such as.

[E] [State::p_resumeLuaState] Error while resuming lua
[E] Endless.tek/Scripts/Endless/Tournament.lua:44: attempt to call a nil value (field 'gmeState')
[E] stack traceback: 
[E] - Endless.tek/World/Crossroads/Global/CrossroadsExt.lua:34 in mapInitFinished
[E] - Endless.tek/World/Crossroads/Global/MapScript.lua:108 in mapInitFinished

Do this as early in your project as possible - no reason to waste time figuring something out that the program could have told you.