diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index 9cdd5665dc8..284f911fa88 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -21,8 +21,9 @@ #include "stringize.h" #include // std::uncaught_exceptions() #include // std::shared_ptr -#include +#include #include +#include #include // std::pair class LuaListener; @@ -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() -template -int lua_emplace_call_gc(lua_State* L); -// this will be the function called by the new userdata's metatable's __gc() -template -int lua_emplace_gc(lua_State* L); -// name by which we'll store the new userdata's metatable in the Registry -template -std::string lua_emplace_metaname(const std::string& Tname = LLError::Log::classname()); +// If we start engaging lua_emplace() from more than one thread, type_tags +// will need locking. +std::unordered_map type_tags; + +// find or create a new Luau userdata "tag" for type T +template +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(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(object)) to make ~LuaState() call ~T(). - * - * We wrap the userdata object as std::optional 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(L, T constructor args...); @@ -226,178 +232,45 @@ std::string lua_emplace_metaname(const std::string& Tname = LLError::Log::classn template void lua_emplace(lua_State* L, ARGS&&... args) { - using optT = std::optional; - luaL_checkstack(L, 5, nullptr); - auto ptr = lua_newuserdata(L, sizeof(optT)); + luaL_checkstack(L, 1, nullptr); + int tag{ type_tag() }; + 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(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)...); - // 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. - auto Tname{ LLError::Log::classname() }; - auto metaname{ lua_emplace_metaname(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, gcname.c_str()); - // stack is userdata, metatable, lua_emplace_gc - 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. - - // 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, weak_table) - auto callgcname{ stringize("lua_emplace_call_gc<", Tname, ">") }; - lua_pushcclosure(L, lua_emplace_call_gc, 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, weak_table{userdata})); -// retrieves bound userdata to pass to lua_emplace_gc() -template -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(). Its one and only - // entry should be the lua_emplace() 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() - return lua_emplace_gc(L); -} - -// set as metatable(userdata).__gc to be called by the garbage collector -template -int lua_emplace_gc(lua_State* L) -{ - using optT = std::optional; - // 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(ptr)->reset(); - // pop the userdata - lua_pop(L, 1); - return 0; -} - -template -std::string lua_emplace_metaname(const std::string& Tname) -{ - return stringize("lua_emplace_", Tname, "_meta"); + new (ptr) T(std::forward(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() -- that is, the userdata contains a non-empty - * std::optional -- return a pointer to the contained T instance. Otherwise - * (index is not a full userdata; userdata is not of type std::optional; - * std::optional is empty) return nullptr. + * lua_emplace(), return a pointer to the contained T instance. Otherwise + * (index is not a full userdata; userdata is not of type T) return nullptr. */ template T* lua_toclass(lua_State* L, int index) { - using optT = std::optional; - // recreate the name lua_emplace() uses for its metatable - auto metaname{ lua_emplace_metaname() }; // 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(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()) }; + // 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(ptr); } /*****************************************************************************