From 444377f9db0e5443433468cc13904bed5b343d40 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Tue, 3 Apr 2012 13:13:44 +0400 Subject: [PATCH 1/5] Finish documenting the DFHack core lua api existing so far. --- LUA_API.rst | 181 ++++++++++++++++++++++++++++++++++++++++++- library/LuaTools.cpp | 25 +++++- 2 files changed, 200 insertions(+), 6 deletions(-) diff --git a/LUA_API.rst b/LUA_API.rst index 12b25bac8..fd05e5448 100644 --- a/LUA_API.rst +++ b/LUA_API.rst @@ -127,10 +127,15 @@ They implement the following features: Valid fields of the structure may be accessed by subscript. - In case of inheritance, *superclass* fields have precedence + Primitive typed fields, i.e. numbers & strings, are converted + to/from matching lua values. The value of a pointer is a reference + to the target, or nil/NULL. Complex types are represented by + a reference to the field within the structure; unless recursive + lua table assignment is used, such fields can only be read. + + **NOTE:** In case of inheritance, *superclass* fields have precedence over the subclass, but fields shadowed in this way can still be accessed as ``ref['subclasstype.field']``. - This shadowing order is necessary because vtable-based classes are automatically exposed in their exact type, and the reverse rule would make access to superclass fields unreliable. @@ -138,8 +143,8 @@ They implement the following features: * ``ref._field(field)`` Returns a reference to a valid field. That is, unlike regular - subscript, it returns a pointer reference even for primitive - typed fields. + subscript, it returns a reference to the field within the structure + even for primitive typed fields and pointers. * ``ref:vmethod(args...)`` @@ -297,3 +302,171 @@ The ``df`` table itself contains the following functions and values: * ``df.is_instance(type,obj)`` Equivalent to the method, but also allows a reference as proxy for its type. + +Recursive table assignment +========================== + +Recursive assignment is invoked when a lua table is assigned +to a C++ object or field, i.e. one of: + +* ``ref:assign{...}`` +* ``ref.field = {...}`` + +The general mode of operation is that all fields of the table +are assigned to the fields of the target structure, roughly +emulating the following code:: + + function rec_assign(ref,table) + for key,value in pairs(table) do + ref[key] = value + end + end + +Since assigning a table to a field using = invokes the same +process, it is recursive. + +There are however some variations to this process depending +on the type of the field being assigned to: + +1. If the table contains an ``assign`` field, it is + applied first, using the ``ref:assign(value)`` method. + It is never assigned as a usual field. + +2. When a table is assigned to a non-NULL pointer field + using the ``ref.field = {...}`` syntax, it is applied + to the target of the pointer instead. + + If the pointer is NULL, the table is checked for a ``new`` field: + + a. If it is *nil* or *false*, assignment fails with an error. + + b. If it is *true*, the pointer is initialized with a newly + allocated object of the declared target type of the pointer. + + c. Otherwise, ``table.new`` must be a named type, or an + object of a type compatible with the pointer. The pointer + is initialized with the result of calling ``table.new:new()``. + + After this auto-vivification process, assignment proceeds + as if the pointer wasn't NULL. + + Obviously, the ``new`` field inside the table is always skipped + during the actual per-field assignment processing. + +3. If the target of the assignment is a container, a separate + rule set is used: + + a. If the table contains neither ``assign`` nor ``resize`` + fields, it is interpreted as an ordinary *1-based* lua + array. The container is resized to the #-size of the + table, and elements are assigned in numeric order:: + + ref:resize(#table); + for i=1,#table do ref[i-1] = table[i] end + + b. Otherwise, ``resize`` must be *true*, *false*, or + an explicit number. If it is not false, the container + is resized. After that the usual struct-like 'pairs' + assignment is performed. + + In case ``resize`` is *true*, the size is computed + by scanning the table for the largest numeric key. + + This means that in order to reassign only one element of + a container using this system, it is necessary to use:: + + { resize=false, [idx]=value } + +Since nil inside a table is indistinguishable from missing key, +it is necessary to use ``df.NULL`` as a null pointer value. + +This system is intended as a way to define a nested object +tree using pure lua data structures, and then materialize it in +C++ memory in one go. Note that if pointer auto-vivification +is used, an error in the middle of the recursive walk would +not destroy any objects allocated in this way, so the user +should be prepared to catch the error and do the necessary +cleanup. + +================ +DFHack utilities +================ + +DFHack utility functions are placed in the ``dfhack`` global tree. + +Currently it defines the following features: + +* ``dfhack.print(args...)`` + + Output tab-separated args as standard lua print would do, + but without a newline. + +* ``print(args...)``, ``dfhack.println(args...)`` + + A replacement of the standard library print function that + works with DFHack output infrastructure. + +* ``dfhack.printerr(args...)`` + + Same as println; intended for errors. Uses red color and logs to stderr.log. + +* ``dfhack.interpreter([prompt[,env[,history_filename]]])`` + + Starts an interactive lua interpreter, using the specified prompt + string, global environment and command-line history file. + + If the interactive console is not accessible, returns *nil, error*. + +* ``dfhack.with_suspend(f[,args...])`` + + Calls ``f`` with arguments after grabbing the DF core suspend lock. + Suspending is necessary for accessing a consistent state of DF memory. + + Returned values and errors are propagated through after releasing + the lock. It is safe to nest suspends. + + Every thread is allowed only one suspend per DF frame, so it is best + to group operations together in one big critical section. A plugin + can choose to run all lua code inside a C++-side suspend lock. + +Persistent configuration storage +================================ + +This api is intended for storing configuration options in the world itself. +It probably should be restricted to data that is world-dependent. + +Entries are identified by a string ``key``, but it is also possible to manage +multiple entries with the same key; their identity is determined by ``entry_id``. +Every entry has a mutable string ``value``, and an array of 7 mutable ``ints``. + +* ``dfhack.persistent.get(key)``, ``entry:get()`` + + Retrieves a persistent config record with the given string key, + or refreshes an already retrieved entry. If there are multiple + entries with the same key, it is undefined which one is retrieved + by the first version of the call. + + Returns entry, or *nil* if not found. + +* ``dfhack.persistent.delete(key)``, ``entry:delete()`` + + Removes an existing entry. Returns *true* if succeeded. + +* ``dfhack.persistent.get_all(key[,match_prefix])`` + + Retrieves all entries with the same key, or starting with key..'/'. + Calling ``get_all('',true)`` will match all entries. + + If none found, returns nil; otherwise returns an array of entries. + +* ``dfhack.persistent.save({key=str1, ...}[,new])``, ``entry:save([new])`` + + Saves changes in an entry, or creates a new one. Passing true as + new forces creation of a new entry even if one already exists; + otherwise the existing one is simply updated. + Returns *entry, did_create_new* + +Since the data is hidden in data structures owned by the DF world, +and automatically stored in the save game, these save and retrieval +functions can just copy values in memory without doing any actual I/O. +However, currently every entry has a 180+-byte dead-weight overhead. diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index f81396f95..1477b16d5 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -240,6 +240,8 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, // Make a proxy global environment. lua_newtable(state); + int base = lua_gettop(state); + lua_newtable(state); if (env) lua_pushvalue(state, env); @@ -249,7 +251,6 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, lua_setmetatable(state, -2); // Main interactive loop - int base = lua_gettop(state); int vcnt = 1; string curline; string prompt_str = "[" + string(prompt) + "]# "; @@ -324,8 +325,20 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, static int lua_dfhack_interpreter(lua_State *state) { color_ostream *pstream = Lua::GetOutput(state); + if (!pstream) - luaL_error(state, "Cannot use dfhack.interpreter() without output."); + { + lua_pushnil(state); + lua_pushstring(state, "no output stream"); + return 2; + } + + if (!pstream->is_console()) + { + lua_pushnil(state); + lua_pushstring(state, "not an interactive console"); + return 2; + } int argc = lua_gettop(state); @@ -459,6 +472,8 @@ static PersistentDataItem get_persistent(lua_State *state) static int dfhack_persistent_get(lua_State *state) { + CoreSuspender suspend; + auto ref = get_persistent(state); return read_persistent(state, ref, !lua_istable(state, 1)); @@ -466,6 +481,8 @@ static int dfhack_persistent_get(lua_State *state) static int dfhack_persistent_delete(lua_State *state) { + CoreSuspender suspend; + auto ref = get_persistent(state); bool ok = Core::getInstance().getWorld()->DeletePersistentData(ref); @@ -476,6 +493,8 @@ static int dfhack_persistent_delete(lua_State *state) static int dfhack_persistent_get_all(lua_State *state) { + CoreSuspender suspend; + const char *str = luaL_checkstring(state, 1); bool prefix = (lua_gettop(state)>=2 ? lua_toboolean(state,2) : false); @@ -501,6 +520,8 @@ static int dfhack_persistent_get_all(lua_State *state) static int dfhack_persistent_save(lua_State *state) { + CoreSuspender suspend; + lua_settop(state, 2); luaL_checktype(state, 1, LUA_TTABLE); bool add = lua_toboolean(state, 2); From 42a9b0a59271f80d512dde78407d3a07681b3421 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Tue, 3 Apr 2012 13:29:59 +0400 Subject: [PATCH 2/5] Make Core::Suspend safe in plugin_onupdate by pretending to hold the lock. It is in essence true that OnUpdate owns the suspend lock, so expose it officially to the recursive suspend lock mechanics. --- library/Core.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/library/Core.cpp b/library/Core.cpp index ffc174721..b3c91034f 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -868,6 +868,15 @@ int Core::Update() color_ostream_proxy out(con); + // Pretend this thread has suspended the core in the usual way + { + lock_guard lock(d->AccessMutex); + + assert(d->df_suspend_depth == 0); + d->df_suspend_thread = this_thread::get_id(); + d->df_suspend_depth = 1000; + } + // detect if the game was loaded or unloaded in the meantime void *new_wdata = NULL; void *new_mapdata = NULL; @@ -922,6 +931,14 @@ int Core::Update() // notify all the plugins that a game tick is finished plug_mgr->OnUpdate(out); + // Release the fake suspend lock + { + lock_guard lock(d->AccessMutex); + + assert(d->df_suspend_depth == 1000); + d->df_suspend_depth = 0; + } + out << std::flush; // wake waiting tools From 2d4af4ac3e42ae1140fecc90b3beb02d0dd84f4a Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Tue, 3 Apr 2012 20:02:01 +0400 Subject: [PATCH 3/5] A few more utility functions for lua. --- LUA_API.rst | 4 ++ library/LuaTools.cpp | 99 +++++++++++++++++++++++++++++++------- library/include/LuaTools.h | 23 +++++++++ library/lua/dfhack.lua | 2 + 4 files changed, 110 insertions(+), 18 deletions(-) diff --git a/LUA_API.rst b/LUA_API.rst index fd05e5448..6a169e009 100644 --- a/LUA_API.rst +++ b/LUA_API.rst @@ -410,6 +410,10 @@ Currently it defines the following features: Same as println; intended for errors. Uses red color and logs to stderr.log. +* ``safecall(f[,args...])``, ``dfhack.safecall(f[,args...])`` + + Just like pcall, but prints the error with traceback using printerr. + * ``dfhack.interpreter([prompt[,env[,history_filename]]])`` Starts an interactive lua interpreter, using the specified prompt diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 1477b16d5..caeed3a36 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -141,6 +141,49 @@ static int traceback (lua_State *L) { return 1; } +static int finish_dfhack_safecall (lua_State *L, bool success) +{ + if (!lua_checkstack(L, 1)) + { + lua_settop(L, 0); /* create space for return values */ + lua_pushboolean(L, 0); + lua_pushstring(L, "stack overflow in dfhack.safecall()"); + success = false; + } + else + { + lua_pushboolean(L, success); + lua_replace(L, 1); /* put first result in first slot */ + } + + if (!success) + { + const char *msg = lua_tostring(L, -1); + if (!msg) msg = "In dfhack.safecall: error message is not a string."; + if (color_ostream *out = Lua::GetOutput(L)) + out->printerr("%s\n", msg); + else + Core::printerr("%s\n", msg); + } + + return lua_gettop(L); +} + +static int safecall_cont (lua_State *L) +{ + int status = lua_getctx(L, NULL); + return finish_dfhack_safecall(L, (status == LUA_YIELD)); +} + +static int lua_dfhack_safecall (lua_State *L) +{ + luaL_checkany(L, 1); + lua_pushcfunction(L, traceback); + lua_insert(L, 1); + int status = lua_pcallk(L, lua_gettop(L) - 2, LUA_MULTRET, 1, 0, safecall_cont); + return finish_dfhack_safecall(L, (status == LUA_OK)); +} + static void report_error(color_ostream &out, lua_State *L) { const char *msg = lua_tostring(L, -1); @@ -189,24 +232,48 @@ bool DFHack::Lua::Require(color_ostream &out, lua_State *state, return true; } -static bool load_with_env(color_ostream &out, lua_State *state, const std::string &code, int eidx) +bool DFHack::Lua::AssignDFObject(color_ostream &out, lua_State *state, + type_identity *type, void *target, int val_index, bool perr) { - if (luaL_loadbuffer(state, code.data(), code.size(), "=(interactive)") != LUA_OK) + val_index = lua_absindex(state, val_index); + lua_getfield(state, LUA_REGISTRYINDEX, DFHACK_ASSIGN_NAME); + PushDFObject(state, type, target); + lua_pushvalue(state, val_index); + return Lua::SafeCall(out, state, 2, 0, perr); +} + +bool DFHack::Lua::SafeCallString(color_ostream &out, lua_State *state, const std::string &code, + int nargs, int nres, bool perr, + const char *debug_tag, int env_idx) +{ + if (!debug_tag) + debug_tag = code.c_str(); + if (env_idx) + env_idx = lua_absindex(state, env_idx); + + int base = lua_gettop(state); + + // Parse the code + if (luaL_loadbuffer(state, code.data(), code.size(), debug_tag) != LUA_OK) { - report_error(out, state); + if (perr) + report_error(out, state); + return false; } // Replace _ENV - lua_pushvalue(state, eidx); - - if (!lua_setupvalue(state, -2, 1)) + if (env_idx) { - out.printerr("No _ENV upvalue.\n"); - return false; + lua_pushvalue(state, env_idx); + lua_setupvalue(state, -2, 1); + assert(lua_gettop(state) == base+1); } - return true; + if (nargs > 0) + lua_insert(state, -1-nargs); + + return Lua::SafeCall(out, state, nargs, nres, perr); } bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, @@ -273,9 +340,7 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, { curline = "return " + curline.substr(1); - if (!load_with_env(out, state, curline, base)) - continue; - if (!SafeCall(out, state, 0, LUA_MULTRET)) + if (!Lua::SafeCallString(out, state, curline, 0, LUA_MULTRET, true, "=(interactive)", base)) continue; int numret = lua_gettop(state) - base; @@ -309,9 +374,7 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, } else { - if (!load_with_env(out, state, curline, base)) - continue; - if (!SafeCall(out, state, 0, 0)) + if (!Lua::SafeCallString(out, state, curline, 0, LUA_MULTRET, true, "=(interactive)", base)) continue; } } @@ -352,8 +415,7 @@ static int lua_dfhack_interpreter(lua_State *state) static int lua_dfhack_with_suspend(lua_State *L) { - int ctx; - int rv = lua_getctx(L, &ctx); + int rv = lua_getctx(L, NULL); // Non-resume entry point: if (rv == LUA_OK) @@ -385,7 +447,8 @@ static const luaL_Reg dfhack_funcs[] = { { "print", lua_dfhack_print }, { "println", lua_dfhack_println }, { "printerr", lua_dfhack_printerr }, - { "traceback", traceback }, + { "safecall", lua_dfhack_safecall }, + { "onerror", traceback }, { "interpreter", lua_dfhack_interpreter }, { "with_suspend", lua_dfhack_with_suspend }, { NULL, NULL } diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 6f95e7f6e..7e3ed1683 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -79,6 +79,13 @@ namespace DFHack { namespace Lua { */ DFHACK_EXPORT void *GetDFObject(lua_State *state, type_identity *type, int val_index, bool exact_type = false); + /** + * Assign the value at val_index to the target of given identity using df.assign(). + * Return behavior is of SafeCall below. + */ + DFHACK_EXPORT bool AssignDFObject(color_ostream &out, lua_State *state, + type_identity *type, void *target, int val_index, bool perr = true); + /** * Push the pointer onto the stack as a wrapped DF object of a specific type. */ @@ -95,12 +102,28 @@ namespace DFHack { namespace Lua { return (T*)GetDFObject(state, df::identity_traits::get(), val_index, exact_type); } + /** + * Assign the value at val_index to the target using df.assign(). + */ + template + bool AssignDFObject(color_ostream &out, lua_State *state, T *target, int val_index, bool perr = true) { + return AssignDFObject(out, state, df::identity_traits::get(), target, val_index, perr); + } + /** * Invoke lua function via pcall. Returns true if success. * If an error is signalled, and perr is true, it is printed and popped from the stack. */ DFHACK_EXPORT bool SafeCall(color_ostream &out, lua_State *state, int nargs, int nres, bool perr = true); + /** + * Parse code from string with debug_tag and env_idx, then call it using SafeCall. + * In case of error, it is either left on the stack, or printed like SafeCall does. + */ + DFHACK_EXPORT bool SafeCallString(color_ostream &out, lua_State *state, const std::string &code, + int nargs, int nres, bool perr = true, + const char *debug_tag = NULL, int env_idx = 0); + /** * Returns the ostream passed to SafeCall. */ diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index b7052b2ea..9435b393c 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -1,6 +1,8 @@ -- Common startup file for all dfhack plugins with lua support -- The global dfhack table is already created by C++ init code. +safecall = dfhack.safecall + function mkmodule(module,env) local pkg = package.loaded[module] if pkg == nil then From 81fb57a8536fed5957f9a8c5e4cecbc71746951f Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Wed, 4 Apr 2012 10:40:33 +0400 Subject: [PATCH 4/5] Add color output and input prompt support to core lua api. --- LUA_API.rst | 17 ++++++++- library/LuaTools.cpp | 90 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/LUA_API.rst b/LUA_API.rst index 6a169e009..30b1c638b 100644 --- a/LUA_API.rst +++ b/LUA_API.rst @@ -410,9 +410,18 @@ Currently it defines the following features: Same as println; intended for errors. Uses red color and logs to stderr.log. -* ``safecall(f[,args...])``, ``dfhack.safecall(f[,args...])`` +* ``dfhack.color([color])`` - Just like pcall, but prints the error with traceback using printerr. + Sets the current output color. If color is *nil* or *-1*, resets to default. + +* ``dfhack.is_interactive()`` + + Checks if the thread can access the interactive console and returns *true* or *false*. + +* ``dfhack.lineedit([prompt[,history_filename]])`` + + If the thread owns the interactive console, shows a prompt + and returns the entered string. Otherwise returns *nil, error*. * ``dfhack.interpreter([prompt[,env[,history_filename]]])`` @@ -421,6 +430,10 @@ Currently it defines the following features: If the interactive console is not accessible, returns *nil, error*. +* ``safecall(f[,args...])``, ``dfhack.safecall(f[,args...])`` + + Just like pcall, but prints the error with traceback using printerr. + * ``dfhack.with_suspend(f[,args...])`` Calls ``f`` with arguments after grabbing the DF core suspend lock. diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index caeed3a36..a2b672034 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -76,6 +76,27 @@ static void set_dfhack_output(lua_State *L, color_ostream *p) lua_rawsetp(L, LUA_REGISTRYINDEX, &DFHACK_OSTREAM_TOKEN); } +static Console *get_console(lua_State *state) +{ + color_ostream *pstream = Lua::GetOutput(state); + + if (!pstream) + { + lua_pushnil(state); + lua_pushstring(state, "no output stream"); + return NULL; + } + + if (!pstream->is_console()) + { + lua_pushnil(state); + lua_pushstring(state, "not an interactive console"); + return NULL; + } + + return static_cast(pstream); +} + static std::string lua_print_fmt(lua_State *L) { /* Copied from lua source to fully replicate builtin print */ @@ -130,6 +151,56 @@ static int lua_dfhack_printerr(lua_State *S) return 0; } +static int lua_dfhack_color(lua_State *S) +{ + int cv = luaL_optint(S, 1, -1); + + if (cv < -1 || cv > color_ostream::COLOR_MAX) + luaL_argerror(S, 1, "invalid color value"); + + color_ostream *out = Lua::GetOutput(S); + if (out) + out->color(color_ostream::color_value(cv)); + return 0; +} + +static int lua_dfhack_is_interactive(lua_State *S) +{ + lua_pushboolean(S, get_console(S) != NULL); + return 1; +} + +static int lua_dfhack_lineedit(lua_State *S) +{ + const char *prompt = luaL_optstring(S, 1, ">> "); + const char *hfile = luaL_optstring(S, 2, NULL); + + Console *pstream = get_console(S); + if (!pstream) + return 2; + + DFHack::CommandHistory hist; + if (hfile) + hist.load(hfile); + + std::string ret; + int rv = pstream->lineedit(prompt, ret, hist); + + if (rv < 0) + { + lua_pushnil(S); + lua_pushstring(S, "input error"); + return 2; + } + else + { + if (hfile) + hist.save(hfile); + lua_pushlstring(S, ret.data(), ret.size()); + return 1; + } +} + static int traceback (lua_State *L) { const char *msg = lua_tostring(L, 1); if (msg) @@ -387,21 +458,9 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, static int lua_dfhack_interpreter(lua_State *state) { - color_ostream *pstream = Lua::GetOutput(state); - + Console *pstream = get_console(state); if (!pstream) - { - lua_pushnil(state); - lua_pushstring(state, "no output stream"); - return 2; - } - - if (!pstream->is_console()) - { - lua_pushnil(state); - lua_pushstring(state, "not an interactive console"); return 2; - } int argc = lua_gettop(state); @@ -447,9 +506,12 @@ static const luaL_Reg dfhack_funcs[] = { { "print", lua_dfhack_print }, { "println", lua_dfhack_println }, { "printerr", lua_dfhack_printerr }, + { "color", lua_dfhack_color }, + { "is_interactive", lua_dfhack_is_interactive }, + { "lineedit", lua_dfhack_lineedit }, + { "interpreter", lua_dfhack_interpreter }, { "safecall", lua_dfhack_safecall }, { "onerror", traceback }, - { "interpreter", lua_dfhack_interpreter }, { "with_suspend", lua_dfhack_with_suspend }, { NULL, NULL } }; From 7efbd798ce30eb598763bfceeb5afc9e5ddffda6 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Wed, 4 Apr 2012 13:34:07 +0400 Subject: [PATCH 5/5] Upgrade lua errors to structures when attaching stack trace. This allows detecting when it's re-thrown and avoiding attaching the stack twice, and so on. Would also be useful if debugging is added. --- LUA_API.rst | 12 ++- library/LuaTools.cpp | 193 +++++++++++++++++++++++++++++++++-------- library/lua/dfhack.lua | 4 + 3 files changed, 171 insertions(+), 38 deletions(-) diff --git a/LUA_API.rst b/LUA_API.rst index 30b1c638b..bf6c81ec0 100644 --- a/LUA_API.rst +++ b/LUA_API.rst @@ -430,9 +430,19 @@ Currently it defines the following features: If the interactive console is not accessible, returns *nil, error*. +* ``dfhack.pcall(f[,args...])`` + + Invokes f via xpcall, using an error function that attaches + a stack trace to the error. The same function is used by SafeCall + in C++, and dfhack.safecall. + + The returned error is a table with separate ``message`` and + ``stacktrace`` string fields; it implements ``__tostring``. + * ``safecall(f[,args...])``, ``dfhack.safecall(f[,args...])`` - Just like pcall, but prints the error with traceback using printerr. + Just like pcall, but also prints the error using printerr before + returning. Intended as a convenience function. * ``dfhack.with_suspend(f[,args...])`` diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index a2b672034..5a50d0dd2 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -141,13 +141,18 @@ static int lua_dfhack_println(lua_State *S) return 0; } -static int lua_dfhack_printerr(lua_State *S) +static void dfhack_printerr(lua_State *S, const std::string &str) { - std::string str = lua_print_fmt(S); if (color_ostream *out = Lua::GetOutput(S)) out->printerr("%s\n", str.c_str()); else Core::printerr("%s\n", str.c_str()); +} + +static int lua_dfhack_printerr(lua_State *S) +{ + std::string str = lua_print_fmt(S); + dfhack_printerr(S, str); return 0; } @@ -201,20 +206,135 @@ static int lua_dfhack_lineedit(lua_State *S) } } -static int traceback (lua_State *L) { - const char *msg = lua_tostring(L, 1); - if (msg) - luaL_traceback(L, L, msg, 1); - else if (!lua_isnoneornil(L, 1)) { /* is there an error object? */ - if (!luaL_callmeta(L, 1, "__tostring")) /* try its 'tostring' metamethod */ - lua_pushliteral(L, "(no error message)"); +static int DFHACK_EXCEPTION_META_TOKEN = 0; + +static void error_tostring(lua_State *L) +{ + lua_getglobal(L, "tostring"); + lua_pushvalue(L, -2); + bool ok = lua_pcall(L, 1, 1, 0) == LUA_OK; + + const char *msg = lua_tostring(L, -1); + if (!msg) + { + msg = "tostring didn't return a string"; + ok = false; + } + + if (!ok) + { + lua_pushfstring(L, "(invalid error: %s)", msg); + lua_remove(L, -2); + } +} + +static void report_error(lua_State *L, color_ostream *out = NULL) +{ + lua_dup(L); + error_tostring(L); + + const char *msg = lua_tostring(L, -1); + assert(msg); + + if (out) + out->printerr("%s\n", msg); + else + dfhack_printerr(L, msg); + + lua_pop(L, 1); +} + +static bool convert_to_exception(lua_State *L) +{ + int base = lua_gettop(L); + + bool force_unknown = false; + + if (lua_istable(L, base) && lua_getmetatable(L, base)) + { + lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_EXCEPTION_META_TOKEN); + bool is_exception = lua_rawequal(L, -1, -2); + lua_settop(L, base); + + // If it is an exception, return as is + if (is_exception) + return false; + + force_unknown = true; } + + if (!lua_istable(L, base) || force_unknown) + { + lua_newtable(L); + lua_swap(L); + + if (lua_isstring(L, -1)) + lua_setfield(L, base, "message"); + else + { + error_tostring(L); + lua_setfield(L, base, "message"); + lua_setfield(L, base, "object"); + } + } + else + { + lua_getfield(L, base, "message"); + + if (!lua_isstring(L, -1)) + { + error_tostring(L); + lua_setfield(L, base, "message"); + } + + lua_settop(L, base); + } + + lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_EXCEPTION_META_TOKEN); + lua_setmetatable(L, base); + return true; +} + +static int dfhack_onerror(lua_State *L) +{ + luaL_checkany(L, 1); + lua_settop(L, 1); + + bool changed = convert_to_exception(L); + if (!changed) + return 1; + + luaL_traceback(L, L, NULL, 1); + lua_setfield(L, 1, "stacktrace"); + + return 1; +} + +static int dfhack_exception_tostring(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + + int base = lua_gettop(L); + + lua_getfield(L, 1, "message"); + if (!lua_isstring(L, -1)) + { + lua_pop(L, 1); + lua_pushstring(L, "(error message is not a string)"); + } + + lua_pushstring(L, "\n"); + lua_getfield(L, 1, "stacktrace"); + if (!lua_isstring(L, -1)) + lua_pop(L, 2); + + lua_concat(L, lua_gettop(L) - base); return 1; } static int finish_dfhack_safecall (lua_State *L, bool success) { - if (!lua_checkstack(L, 1)) + if (!lua_checkstack(L, 2)) { lua_settop(L, 0); /* create space for return values */ lua_pushboolean(L, 0); @@ -228,14 +348,7 @@ static int finish_dfhack_safecall (lua_State *L, bool success) } if (!success) - { - const char *msg = lua_tostring(L, -1); - if (!msg) msg = "In dfhack.safecall: error message is not a string."; - if (color_ostream *out = Lua::GetOutput(L)) - out->printerr("%s\n", msg); - else - Core::printerr("%s\n", msg); - } + report_error(L); return lua_gettop(L); } @@ -249,22 +362,12 @@ static int safecall_cont (lua_State *L) static int lua_dfhack_safecall (lua_State *L) { luaL_checkany(L, 1); - lua_pushcfunction(L, traceback); + lua_pushcfunction(L, dfhack_onerror); lua_insert(L, 1); int status = lua_pcallk(L, lua_gettop(L) - 2, LUA_MULTRET, 1, 0, safecall_cont); return finish_dfhack_safecall(L, (status == LUA_OK)); } -static void report_error(color_ostream &out, lua_State *L) -{ - const char *msg = lua_tostring(L, -1); - if (msg) - out.printerr("%s\n", msg); - else - out.printerr("In Lua::SafeCall: error message is not a string.\n", msg); - lua_pop(L, 1); -} - bool DFHack::Lua::SafeCall(color_ostream &out, lua_State *L, int nargs, int nres, bool perr) { int base = lua_gettop(L) - nargs; @@ -272,17 +375,20 @@ bool DFHack::Lua::SafeCall(color_ostream &out, lua_State *L, int nargs, int nres color_ostream *cur_out = Lua::GetOutput(L); set_dfhack_output(L, &out); - lua_pushcfunction(L, traceback); + lua_pushcfunction(L, dfhack_onerror); lua_insert(L, base); bool ok = lua_pcall(L, nargs, nres, base) == LUA_OK; + if (!ok && perr) + { + report_error(L, &out); + lua_pop(L, 1); + } + lua_remove(L, base); set_dfhack_output(L, cur_out); - if (!ok && perr) - report_error(out, L); - return ok; } @@ -328,7 +434,10 @@ bool DFHack::Lua::SafeCallString(color_ostream &out, lua_State *state, const std if (luaL_loadbuffer(state, code.data(), code.size(), debug_tag) != LUA_OK) { if (perr) - report_error(out, state); + { + report_error(state, &out); + lua_pop(state, 1); + } return false; } @@ -485,7 +594,7 @@ static int lua_dfhack_with_suspend(lua_State *L) Core::getInstance().Suspend(); - lua_pushcfunction(L, traceback); + lua_pushcfunction(L, dfhack_onerror); lua_insert(L, 1); rv = lua_pcallk(L, nargs-1, LUA_MULTRET, 1, 0, lua_dfhack_with_suspend); @@ -511,7 +620,7 @@ static const luaL_Reg dfhack_funcs[] = { { "lineedit", lua_dfhack_lineedit }, { "interpreter", lua_dfhack_interpreter }, { "safecall", lua_dfhack_safecall }, - { "onerror", traceback }, + { "onerror", dfhack_onerror }, { "with_suspend", lua_dfhack_with_suspend }, { NULL, NULL } }; @@ -743,8 +852,18 @@ lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state) lua_pushcfunction(state, lua_dfhack_println); lua_setglobal(state, "print"); - // Create and initialize the dfhack global + // Create the dfhack global + lua_newtable(state); + + // Create the metatable for exceptions lua_newtable(state); + lua_pushcfunction(state, dfhack_exception_tostring); + lua_setfield(state, -2, "__tostring"); + lua_dup(state); + lua_rawsetp(state, LUA_REGISTRYINDEX, &DFHACK_EXCEPTION_META_TOKEN); + lua_setfield(state, -2, "exception"); + + // Initialize the dfhack global luaL_setfuncs(state, dfhack_funcs, 0); OpenPersistent(state); diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 9435b393c..67652ff15 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -3,6 +3,10 @@ safecall = dfhack.safecall +function dfhack.pcall(f, ...) + return xpcall(f, dfhack.onerror, ...) +end + function mkmodule(module,env) local pkg = package.loaded[module] if pkg == nil then