From ab9cb6fcd96c1c29650d844b5fd76e2ebbf5f2df Mon Sep 17 00:00:00 2001
From: Nat Goodspeed <nat@lindenlab.com>
Date: Fri, 14 Jun 2024 20:43:09 -0400
Subject: 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.
---
 indra/llcommon/lua_function.cpp |  76 +++++++++++++++-
 indra/llcommon/lua_function.h   | 186 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 261 insertions(+), 1 deletion(-)

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;
@@ -685,6 +716,49 @@ LuaListener::ptr_t LuaState::obtainListener(lua_State* L)
     return listener;
 }
 
+/*****************************************************************************
+*   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
 *****************************************************************************/
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
-- 
cgit v1.2.3