Skip to content

Commit

Permalink
Make lua_emplace<T>() use Luau userdata tags with destructors.
Browse files Browse the repository at this point in the history
It turns out that Luau does not honor PUC-Rio Lua's __gc metafunction, so
despite elaborate measures, the previous lua_emplace<T>() implementation would
not have destroyed the contained C++ T object when the resulting userdata
object was garbage-collected.

Moreover, using LL.atexit() as the mechanism to destroy lua_emplace<T>()
userdata objects (e.g. LuaListener) would have been slightly fragile because
we also want to use LL.atexit() to make the final fiber.run() call, when
appropriate. Introducing an order dependency between fiber.run() and the
LuaListener destructor would not be robust.

Both of those problems are addressed by leveraging one of Luau's extensions
over PUC-Rio Lua. A Luau userdata object can have an int tag; and a tag can
have an associated C++ destructor function. When any userdata object bearing
that tag is garbage-collected, Luau will call that destructor; and Luau's
lua_close() function destroys all userdata objects.

The resulting lua_emplace<T>() and lua_toclass<T>() code is far simpler.

It only remains to generate a distinct int tag value for each different C++
type passed to the lua_emplace<T>() template.
unordered_map<std::type_index, int> addresses that need.
  • Loading branch information
nat-goodspeed committed Jun 27, 2024
1 parent b347ad5 commit 6ee98f4
Showing 1 changed file with 48 additions and 175 deletions.
223 changes: 48 additions & 175 deletions indra/llcommon/lua_function.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
#include "stringize.h"
#include <exception> // std::uncaught_exceptions()
#include <memory> // std::shared_ptr
#include <optional>
#include <typeindex>
#include <typeinfo>
#include <unordered_map>
#include <utility> // std::pair

class LuaListener;
Expand Down Expand Up @@ -196,28 +197,33 @@ int name##_luasub::call(lua_State* L)
*****************************************************************************/
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>());
// If we start engaging lua_emplace<T>() from more than one thread, type_tags
// will need locking.
std::unordered_map<std::type_index, int> type_tags;

// find or create a new Luau userdata "tag" for type T
template <typename T>
int type_tag()
{
// The first time we encounter a given type T, assign a new distinct tag
// value based on the number of previously-created tags. But avoid tag 0,
// which is evidently the default for userdata objects created without
// explicit tags. Don't try to destroy a nonexistent T object in a random
// userdata object!
auto [entry, created] = type_tags.emplace(std::type_index(typeid(T)), int(type_tags.size()+1));
// Luau only permits up to LUA_UTAG_LIMIT distinct userdata tags (ca. 128)
llassert(entry->second < LUA_UTAG_LIMIT);
return entry->second;
}

} // 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. Also call
* LL.atexit(lua_emplace_call_gc<T>(object)) to make ~LuaState() call ~T().
*
* We wrap the userdata object as std::optional<T> so we can explicitly
* destroy the contained T, and detect that we've done so.
* containing a newly-constructed C++ object T(args...). The userdata has a
* Luau destructor guaranteeing that the new T instance is destroyed when the
* userdata is garbage-collected, no later than when the LuaState is
* destroyed.
*
* Usage:
* lua_emplace<T>(L, T constructor args...);
Expand All @@ -226,178 +232,45 @@ std::string lua_emplace_metaname(const std::string& Tname = LLError::Log::classn
template <class T, typename... ARGS>
void lua_emplace(lua_State* L, ARGS&&... args)
{
using optT = std::optional<T>;
luaL_checkstack(L, 5, nullptr);
auto ptr = lua_newuserdata(L, sizeof(optT));
luaL_checkstack(L, 1, nullptr);
int tag{ type_tag<T>() };
if (! lua_getuserdatadtor(L, tag))
{
// We haven't yet told THIS lua_State the destructor to use for this tag.
lua_setuserdatadtor(
L, tag,
[](lua_State*, void* ptr)
{
// destroy the contained T instance
static_cast<T*>(ptr)->~T();
});
}
auto ptr = lua_newuserdatatagged(L, sizeof(T), tag);
// 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);
llassert((uintptr_t(ptr) % alignof(T)) == 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 destroyed 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
// ditch LL
lua_replace(L, -2);
// stack contains userdata, LL.atexit

// We have a bit of a problem here. We want to allow the garbage collector
// to collect the userdata if it must; but we also want to register a
// cleanup function to destroy the value if (usual case) it has NOT been
// garbage-collected. The problem is that if we bind into atexit()'s queue
// a strong reference to the userdata, we ensure that the garbage
// collector cannot collect it, making our metatable with __gc function
// completely moot. And we must assume that lua_pushcclosure() binds a
// strong reference to each value passed as a closure.

// The solution is to use one more indirection: create a weak table whose
// sole entry is the userdata. If all other references to the new userdata
// are forgotten, so the only remaining reference is the weak table, the
// userdata can be collected. Then we can bind that weak table as the
// closure value for our cleanup function.
// The new weak table will have at most 1 array value, 0 other keys.
lua_createtable(L, 1, 0);
// stack contains userdata, LL.atexit, weak_table
if (luaL_newmetatable(L, "weak_values"))
{
// stack contains userdata, LL.atexit, weak_table, weak_values
// just created "weak_values" metatable: populate it
// Registry.weak_values = {__mode="v"}
lua_pushliteral(L, "v");
// stack contains userdata, LL.atexit, weak_table, weak_values, "v"
lua_setfield(L, -2, "__mode");
}
// stack contains userdata, LL.atexit, weak_table, weak_values
// setmetatable(weak_table, weak_values)
lua_setmetatable(L, -2);
// stack contains userdata, LL.atexit, weak_table
lua_pushinteger(L, 1);
// stack contains userdata, LL.atexit, weak_table, 1
// duplicate userdata
lua_pushvalue(L, -4);
// stack contains userdata, LL.atexit, weak_table, 1, userdata
// weak_table[1] = userdata
lua_settable(L, -3);
// stack contains userdata, LL.atexit, weak_table

// push a closure binding (lua_emplace_call_gc<T>, weak_table)
auto callgcname{ stringize("lua_emplace_call_gc<", Tname, ">") };
lua_pushcclosure(L, lua_emplace_call_gc<T>, callgcname.c_str(), 1);
// stack contains userdata, LL.atexit, closure
// Call LL.atexit(closure)
lua_call(L, 1, 0);
// stack contains userdata -- return that
}

namespace {

// passed to LL.atexit(closure(lua_emplace_call_gc<T>, weak_table{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, 2, nullptr);
// retrieve the first (only) bound upvalue and push to stack top
lua_pushvalue(L, lua_upvalueindex(1));
// This is the weak_table bound by lua_emplace<T>(). Its one and only
// entry should be the lua_emplace<T>() userdata -- unless userdata has
// been garbage collected. Retrieve weak_table[1].
lua_pushinteger(L, 1);
// stack contains weak_table, 1
lua_gettable(L, -2);
// stack contains weak_table, weak_table[1]
// If our userdata was garbage-collected, there is no weak_table[1],
// and we just retrieved nil.
if (lua_isnil(L, -1))
{
lua_pop(L, 2);
return 0;
}
// stack contains weak_table, userdata
// ditch weak_table
lua_replace(L, -2);
// pass userdata to lua_emplace_gc<T>()
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");
new (ptr) T(std::forward<ARGS>(args)...);
// stack is now initialized userdata containing our T instance -- return
// that
}

} // 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.
* lua_emplace<T>(), return a pointer to the contained T instance. Otherwise
* (index is not a full userdata; userdata is not of type T) return nullptr.
*/
template <class T>
T* lua_toclass(lua_State* L, int index)
{
using optT = std::optional<T>;
// recreate the name lua_emplace<T>() uses for its metatable
auto metaname{ lua_emplace_metaname<T>() };
// get void* pointer to userdata (if that's what it is)
void* ptr{ luaL_checkudata(L, index, metaname.c_str()) };
if (! ptr)
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(static_cast<optT*>(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();
void* ptr{ lua_touserdatatagged(L, index, type_tag<T>()) };
// Derive the T* from ptr. If in future lua_emplace() must manually
// align our T* within the Lua-provided void*, adjust accordingly.
return static_cast<T*>(ptr);
}

/*****************************************************************************
Expand Down

0 comments on commit 6ee98f4

Please sign in to comment.