diff options
author | Nat Goodspeed <nat@lindenlab.com> | 2024-06-14 20:43:09 -0400 |
---|---|---|
committer | Nat Goodspeed <nat@lindenlab.com> | 2024-06-14 20:43:09 -0400 |
commit | ab9cb6fcd96c1c29650d844b5fd76e2ebbf5f2df (patch) | |
tree | d03d7e8f69be2f938608440177fced681b935635 /indra | |
parent | f7137765438f149cbae6f3b18da45dce75a25336 (diff) |
Introduce LL.atexit(), internal lua_emplace<T>(), lua_toclass<T>().
Publish new LL.atexit() function that accepts a Lua function (or C++ closure)
and saves it (in Registry["atexit"] table) to call later.
Make ~LuaState() walk the Registry["atexit"] table, if it exists, calling each
function appended to that table.
(Consider using that mechanism to clean up a LuaListener, if one was
instantiated. Possibly also use for p.s. leap.run()? But that's run after
every expr() call, instead of only at ~LuaState() time. Pragmatically, though,
the distinction only matters for a LUA Debug Console LUA string with "clean
lua_State" unchecked.)
For use by future lua_function() entry points, lua_emplace<T>(ctor args...)
pushes a Lua userdata object containing a newly-constructed T instance --
actually a std::optional<T> to avoid double destruction. lua_emplace<T>() is
specifically intended to be usable even for T with a nontrivial destructor: it
gives the userdata a metatable with a __gc function that destroys the
contained T instance when the userdata is garbage collected. But since garbage
collection doesn't guarantee to clean up global variables with __gc methods,
lua_emplace<T>() also uses LL.atexit() to ensure that ~T() will run when the
LuaState is destroyed.
The companion to lua_emplace<T>() is lua_toclass<T>(), which returns a
non-nullptr T* if the referenced index is in fact a userdata created by
lua_emplace<T>() for the same T, that has not yet been destroyed. This lets
C++ code access a T previously embedded in Lua userdata.
Diffstat (limited to 'indra')
-rw-r--r-- | indra/llcommon/lua_function.cpp | 76 | ||||
-rw-r--r-- | indra/llcommon/lua_function.h | 186 |
2 files changed, 261 insertions, 1 deletions
diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 7c80f65ff9..edd49feed9 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -489,6 +489,37 @@ void LuaState::initLuaState() LuaState::~LuaState() { + // We're just about to destroy this lua_State mState. lua_close() doesn't + // implicitly garbage-collect everything, so (for instance) any lingering + // objects with __gc metadata methods aren't cleaned up. This is why we + // provide atexit(). + luaL_checkstack(mState, 3, nullptr); + // look up Registry["atexit"] + lua_getfield(mState, LUA_REGISTRYINDEX, "atexit"); + // stack contains Registry["atexit"] + if (lua_istable(mState, -1)) + { + lua_pushnil(mState); // first key + while (lua_next(mState, -2)) + { + // stack contains Registry["atexit"], key, value + // Call value(), no args, no return values. + // Use lua_pcall() because errors in any one atexit() function + // shouldn't cancel the rest of them. + if (lua_pcall(mState, 0, 0, 0) != LUA_OK) + { + auto error{ lua_tostdstring(mState, -1) }; + LL_WARNS("Lua") << "atexit() function error: " << error << LL_ENDL; + // pop error message + lua_pop(mState, 1); + } + // Normally we would pop value, keeping the key for the next + // iteration. But lua_pcall() has already popped the value. + } + } + // pop Registry["atexit"] (either table or nil) + lua_pop(mState, 1); + // Did somebody call obtainListener() on this LuaState? // That is, is there a LuaListener key in its registry? LuaListener::destruct(getListener()); @@ -509,7 +540,7 @@ bool LuaState::checkLua(const std::string& desc, int r) mError = lua_tostring(mState, -1); lua_pop(mState, 1); - LL_WARNS() << desc << ": " << mError << LL_ENDL; + LL_WARNS("Lua") << desc << ": " << mError << LL_ENDL; return false; } return true; @@ -686,6 +717,49 @@ LuaListener::ptr_t LuaState::obtainListener(lua_State* L) } /***************************************************************************** +* atexit() +*****************************************************************************/ +lua_function(atexit, "register a Lua function to be called at script termination") +{ + luaL_checkstack(L, 4, nullptr); + // look up the global name "table" + lua_getglobal(L, "table"); + // stack contains function, "table" + // look up table.insert + lua_getfield(L, -1, "insert"); + // stack contains function, "table", "insert" + // look up the "atexit" table in the Registry + lua_getfield(L, LUA_REGISTRYINDEX, "atexit"); + // stack contains function, "table", "insert", Registry["atexit"] + if (! lua_istable(L, -1)) + { + llassert(lua_isnil(L, -1)); + // stack contains function, "table", "insert", nil + lua_pop(L, 1); + // make a new, empty table + lua_newtable(L); + // stack contains function, "table", "insert", {} + // duplicate the table reference on the stack + lua_pushvalue(L, -1); + // stack contains function, "table", "insert", {}, {} + // store the new empty "atexit" table to the Registry, leaving a + // reference on the stack + lua_setfield(L, LUA_REGISTRYINDEX, "atexit"); + } + // stack contains function, "table", "insert", Registry["atexit"] + // we were called with a Lua function to append to that Registry["atexit"] + // table -- push that function + lua_pushvalue(L, 1); // or -4 + // stack contains function, "table", "insert", Registry["atexit"], function + // call table.insert(atexit, function) + // don't use pcall(): if there's an error, let it propagate + lua_call(L, 2, 0); + // stack contains function, "table" -- pop everything + lua_settop(L, 0); + return 0; +} + +/***************************************************************************** * LuaPopper class *****************************************************************************/ LuaPopper::~LuaPopper() diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index e7013f92c6..5bbcbc441f 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -17,13 +17,19 @@ #include "luau/luaconf.h" #include "luau/lualib.h" #include "fsyspath.h" +#include "llerror.h" #include "stringize.h" #include <exception> // std::uncaught_exceptions() #include <memory> // std::shared_ptr +#include <optional> +#include <typeinfo> #include <utility> // std::pair class LuaListener; +/***************************************************************************** +* lluau namespace utility functions +*****************************************************************************/ namespace lluau { // luau defines luaL_error() as void, but we want to use the Lua idiom of @@ -62,6 +68,9 @@ void lua_pushstdstring(lua_State* L, const std::string& str); LLSD lua_tollsd(lua_State* L, int index); void lua_pushllsd(lua_State* L, const LLSD& data); +/***************************************************************************** +* LuaState +*****************************************************************************/ /** * RAII class to manage the lifespan of a lua_State */ @@ -110,6 +119,9 @@ private: std::string mError; }; +/***************************************************************************** +* LuaPopper +*****************************************************************************/ /** * LuaPopper is an RAII struct whose role is to pop some number of entries * from the Lua stack if the calling function exits early. @@ -133,6 +145,9 @@ struct LuaPopper int mCount; }; +/***************************************************************************** +* lua_function (and helper class LuaFunction) +*****************************************************************************/ /** * LuaFunction is a base class containing a static registry of its static * subclass call() methods. call() is NOT virtual: instead, each subclass @@ -182,6 +197,171 @@ int name##_luasub::call(lua_State* L) // ... supply method body here, referencing 'L' ... // } +/***************************************************************************** +* lua_emplace<T>(), lua_toclass<T>() +*****************************************************************************/ +namespace { + +// this closure function retrieves its bound argument to pass to +// lua_emplace_gc<T>() +template <class T> +int lua_emplace_call_gc(lua_State* L); +// this will be the function called by the new userdata's metatable's __gc() +template <class T> +int lua_emplace_gc(lua_State* L); +// name by which we'll store the new userdata's metatable in the Registry +template <class T> +std::string lua_emplace_metaname(const std::string& Tname = LLError::Log::classname<T>()); + +} // anonymous namespace + +/** + * On the stack belonging to the passed lua_State, push a Lua userdata object + * with a newly-constructed C++ object std::optional<T>(args...). The new + * userdata has a metadata table with a __gc() function to ensure that when + * the userdata instance is garbage-collected, ~T() is called. + * + * We wrap the userdata object as std::optional<T> so we can explicitly + * destroy the contained T, and detect that we've done so. + * + * Usage: + * lua_emplace<T>(L, T constructor args...); + */ +template <class T, typename... ARGS> +void lua_emplace(lua_State* L, ARGS&&... args) +{ + using optT = std::optional<T>; + luaL_checkstack(L, 3, nullptr); + auto ptr = lua_newuserdata(L, sizeof(optT)); + // stack is uninitialized userdata + // For now, assume (but verify) that lua_newuserdata() returns a + // conservatively-aligned ptr. If that turns out not to be the case, we + // might have to discard the new userdata, overallocate its successor and + // perform manual alignment -- but only if we must. + llassert((uintptr_t(ptr) % alignof(optT)) == 0); + // Construct our T there using placement new + new (ptr) optT(std::in_place, std::forward<ARGS>(args)...); + // stack is now initialized userdata containing our T instance + + // Find or create the metatable shared by all userdata instances holding + // C++ type T. We want it to be shared across instances, but it must be + // type-specific because its __gc field is lua_emplace_gc<T>. + auto Tname{ LLError::Log::classname<T>() }; + auto metaname{ lua_emplace_metaname<T>(Tname) }; + if (luaL_newmetatable(L, metaname.c_str())) + { + // just created it: populate it + auto gcname{ stringize("lua_emplace_gc<", Tname, ">") }; + lua_pushcfunction(L, lua_emplace_gc<T>, gcname.c_str()); + // stack is userdata, metatable, lua_emplace_gc<T> + lua_setfield(L, -2, "__gc"); + } + // stack is userdata, metatable + lua_setmetatable(L, -2); + // Stack is now userdata, initialized with T(args), + // with metatable.__gc pointing to lua_emplace_gc<T>. + // But wait, there's more! Use our atexit() function to ensure that this + // C++ object is eventually cleaned up even if the garbage collector never + // gets around to it. + lua_getglobal(L, "LL"); + // stack contains userdata, LL + lua_getfield(L, -1, "atexit"); + // stack contains userdata, LL, LL.atexit + // duplicate userdata + lua_pushvalue(L, -3); + // stack contains userdata, LL, LL.atexit, userdata + // push a closure binding (lua_emplace_call_gc<T>, userdata) + auto callgcname{ stringize("lua_emplace_call_gc<", Tname, ">") }; + lua_pushcclosure(L, lua_emplace_call_gc<T>, callgcname.c_str(), 1); + // stack contains userdata, LL, LL.atexit, closure + // Call LL.atexit(closure) + lua_call(L, 1, 0); + // stack contains userdata, LL + lua_pop(L, 1); + // stack contains userdata -- return that +} + +namespace { + +// passed to LL.atexit(closure(lua_emplace_call_gc<T>, userdata)); +// retrieves bound userdata to pass to lua_emplace_gc<T>() +template <class T> +int lua_emplace_call_gc(lua_State* L) +{ + luaL_checkstack(L, 1, nullptr); + // retrieve the first (only) bound upvalue and push to stack top as the + // argument for lua_emplace_gc<T>() + lua_pushvalue(L, lua_upvalueindex(1)); + return lua_emplace_gc<T>(L); +} + +// set as metatable(userdata).__gc to be called by the garbage collector +template <class T> +int lua_emplace_gc(lua_State* L) +{ + using optT = std::optional<T>; + // We're called with userdata on the stack holding an instance of type T. + auto ptr = lua_touserdata(L, -1); + llassert(ptr); + // Destroy the T object contained in optT at the void* address ptr. If + // in future lua_emplace() must manually align our optT* within the + // Lua-provided void*, derive optT* from ptr. + static_cast<optT*>(ptr)->reset(); + // pop the userdata + lua_pop(L, 1); + return 0; +} + +template <class T> +std::string lua_emplace_metaname(const std::string& Tname) +{ + return stringize("lua_emplace_", Tname, "_meta"); +} + +} // anonymous namespace + +/** + * If the value at the passed acceptable index is a full userdata created by + * lua_emplace<T>() -- that is, the userdata contains a non-empty + * std::optional<T> -- return a pointer to the contained T instance. Otherwise + * (index is not a full userdata; userdata is not of type std::optional<T>; + * std::optional<T> is empty) return nullptr. + */ +template <class T> +T* lua_toclass(lua_State* L, int index) +{ + using optT = std::optional<T>; + luaL_checkstack(L, 2, nullptr); + // get void* pointer to userdata (if that's what it is) + auto ptr{ lua_touserdata(L, index) }; + if (! ptr) + return nullptr; + // push the metatable for this userdata, if any + if (! lua_getmetatable(L, index)) + return nullptr; + // now push the metatable created by lua_emplace<T>() + auto metaname{ lua_emplace_metaname<T>() }; + luaL_getmetatable(L, metaname.c_str()); + auto equal{ lua_equal(L, -1, -2) }; + // Having compared the userdata's metatable with the one set by + // lua_emplace<T>(), we no longer need either metatable on the stack. + lua_pop(L, 2); + if (! equal) + return nullptr; + // Derive the optT* from ptr. If in future lua_emplace() must manually + // align our optT* within the Lua-provided void*, adjust accordingly. + optT* tptr(ptr); + // make sure our optT isn't empty + if (! *tptr) + return nullptr; + // looks like we still have a non-empty optT: return the *address* of the + // value() reference + return &tptr->value(); +} + +/***************************************************************************** +* lua_what() +*****************************************************************************/ // Usage: std::cout << lua_what(L, stackindex) << ...; // Reports on the Lua value found at the passed stackindex. // If cast to std::string, returns the corresponding string value. @@ -202,6 +382,9 @@ private: int index; }; +/***************************************************************************** +* lua_stack() +*****************************************************************************/ // Usage: std::cout << lua_stack(L) << ...; // Reports on the contents of the Lua stack. // If cast to std::string, returns the corresponding string value. @@ -220,6 +403,9 @@ private: lua_State* L; }; +/***************************************************************************** +* LuaLog +*****************************************************************************/ // adapted from indra/test/debug.h // can't generalize Debug::operator() target because it's a variadic template class LuaLog |