From f664c2ea26fb63f162f3d988b6d00f1483be5d45 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 6 Feb 2024 14:49:45 -0500 Subject: Break out lua_function.h,.cpp and lualistener.h,.cpp. The intention is to decentralize Luau entry points into our C++ code, permitting a given entry point to be added to the .cpp file that already deals with that class or functional area. Continuing to add every such entry point to llluamanager.cpp doesn't scale well. Extract LuaListener class from llluamanager.cpp to its own header and .cpp file. Extract from llluamanager into lua_function.h (and .cpp) declarations useful for adding a lua_function Luau entry point, e.g.: lua_register() lua_rawlen() lua_tostdstring() lua_pushstdstring() lua_tollsd() lua_pushllsd() LuaPopper lua_function() and LuaFunction class LuaState lua_what lua_stack DebugExit --- indra/llcommon/lua_function.cpp | 575 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 575 insertions(+) create mode 100644 indra/llcommon/lua_function.cpp (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp new file mode 100644 index 0000000000..f6db4eafe1 --- /dev/null +++ b/indra/llcommon/lua_function.cpp @@ -0,0 +1,575 @@ +/** + * @file lua_function.cpp + * @author Nat Goodspeed + * @date 2024-02-05 + * @brief Implementation for lua_function. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "lua_function.h" +// STL headers +// std headers +#include +#include +#include // std::unique_ptr +// external library headers +// other Linden headers +#include "hexdump.h" +#include "llsd.h" +#include "llsdutil.h" +#include "lualistener.h" + +namespace +{ + // can't specify free function free() as a unique_ptr deleter + struct freer + { + void operator()(void* ptr){ free(ptr); } + }; +} // anonymous namespace + +int lluau::dostring(lua_State* L, const std::string& desc, const std::string& text) +{ + { + size_t bytecodeSize = 0; + // The char* returned by luau_compile() must be freed by calling free(). + // Use unique_ptr so the memory will be freed even if luau_load() throws. + std::unique_ptr bytecode{ + luau_compile(text.data(), text.length(), nullptr, &bytecodeSize)}; + auto r = luau_load(L, desc.data(), bytecode.get(), bytecodeSize, 0); + if (r != LUA_OK) + return r; + } // free bytecode + + return lua_pcall(L, 0, 0, 0); +} + +std::string lua_tostdstring(lua_State* L, int index) +{ + size_t len; + const char* strval{ lua_tolstring(L, index, &len) }; + return { strval, len }; +} + +void lua_pushstdstring(lua_State* L, const std::string& str) +{ + luaL_checkstack(L, 1, nullptr); + lua_pushlstring(L, str.c_str(), str.length()); +} + +// By analogy with existing lua_tomumble() functions, return an LLSD object +// corresponding to the Lua object at stack index 'index' in state L. +// This function assumes that a Lua caller is fully aware that they're trying +// to call a viewer function. In other words, the caller must specifically +// construct Lua data convertible to LLSD. +// +// For proper error handling, we REQUIRE that the Lua runtime be compiled as +// C++ so errors are raised as C++ exceptions rather than as longjmp() calls: +// http://www.lua.org/manual/5.4/manual.html#4.4 +// "Internally, Lua uses the C longjmp facility to handle errors. (Lua will +// use exceptions if you compile it as C++; search for LUAI_THROW in the +// source code for details.)" +// Some blocks within this function construct temporary C++ objects in the +// expectation that these objects will be properly destroyed even if code +// reached by that block raises a Lua error. +LLSD lua_tollsd(lua_State* L, int index) +{ + LL_DEBUGS("Lua") << "lua_tollsd(" << index << ") of " << lua_gettop(L) << " stack entries: " + << lua_what(L, index) << LL_ENDL; + DebugExit log_exit("lua_tollsd()"); + switch (lua_type(L, index)) + { + case LUA_TNONE: + // Should LUA_TNONE be an error instead of returning isUndefined()? + case LUA_TNIL: + return {}; + + case LUA_TBOOLEAN: + return bool(lua_toboolean(L, index)); + + case LUA_TNUMBER: + { + // check if integer truncation leaves the number intact + int isint; + lua_Integer intval{ lua_tointegerx(L, index, &isint) }; + if (isint) + { + return LLSD::Integer(intval); + } + else + { + return lua_tonumber(L, index); + } + } + + case LUA_TSTRING: + return lua_tostdstring(L, index); + + case LUA_TUSERDATA: + { + LLSD::Binary binary(lua_rawlen(L, index)); + std::memcpy(binary.data(), lua_touserdata(L, index), binary.size()); + return binary; + } + + case LUA_TTABLE: + { + // A Lua table correctly constructed to convert to LLSD will have + // either consecutive integer keys starting at 1, which we represent + // as an LLSD array (with Lua key 1 at C++ index 0), or will have + // all string keys. + // + // In the belief that Lua table traversal skips "holes," that is, it + // doesn't report any key/value pair whose value is nil, we allow a + // table with integer keys >= 1 but with "holes." This produces an + // LLSD array with isUndefined() entries at unspecified keys. There + // would be no other way for a Lua caller to construct an + // isUndefined() LLSD array entry. However, to guard against crazy int + // keys, we forbid gaps larger than a certain size: crazy int keys + // could result in a crazy large contiguous LLSD array. + // + // Possible looseness could include: + // - A mix of integer and string keys could produce an LLSD map in + // which the integer keys are converted to string. (Key conversion + // must be performed in C++, not Lua, to avoid confusing + // lua_next().) + // - However, since in Lua t[0] and t["0"] are distinct table entries, + // do not consider converting numeric string keys to int to return + // an LLSD array. + // But until we get more experience with actual Lua scripts in + // practice, let's say that any deviation is a Lua coding error. + // An important property of the strict definition above is that most + // conforming data blobs can make a round trip across the language + // boundary and still compare equal. A non-conforming data blob would + // lose that property. + // Known exceptions to round trip identity: + // - Empty LLSD map and empty LLSD array convert to empty Lua table. + // But empty Lua table converts to isUndefined() LLSD object. + // - LLSD::Real with integer value returns as LLSD::Integer. + // - LLSD::UUID, LLSD::Date and LLSD::URI all convert to Lua string, + // and so return as LLSD::String. + // - Lua does not store any table key whose value is nil. An LLSD + // array with isUndefined() entries produces a Lua table with + // "holes" in the int key sequence; this converts back to an LLSD + // array containing corresponding isUndefined() entries -- except + // when one or more of the final entries isUndefined(). These are + // simply dropped, producing a shorter LLSD array than the original. + // - For the same reason, any keys in an LLSD map whose value + // isUndefined() are simply discarded in the converted Lua table. + // This converts back to an LLSD map lacking those keys. + // - If it's important to preserve the original length of an LLSD + // array whose final entries are undefined, or the full set of keys + // for an LLSD map some of whose values are undefined, store an + // LLSD::emptyArray() or emptyMap() instead. These will be + // represented in Lua as empty table, which should convert back to + // undefined LLSD. Naturally, though, those won't survive a second + // round trip. + + // This is the most important of the luaL_checkstack() calls because a + // deeply nested Lua structure will enter this case at each level, and + // we'll need another 2 stack slots to traverse each nested table. + luaL_checkstack(L, 2, nullptr); + // BEFORE we push nil to initialize the lua_next() traversal, convert + // 'index' to absolute! Our caller might have passed a relative index; + // we do, below: lua_tollsd(L, -1). If 'index' is -1, then when we + // push nil, what we find at index -1 is nil, not the table! + index = lua_absindex(L, index); + LL_DEBUGS("Lua") << "checking for empty table" << LL_ENDL; + lua_pushnil(L); // first key + LL_DEBUGS("Lua") << lua_stack(L) << LL_ENDL; + if (! lua_next(L, index)) + { + // it's a table, but the table is empty -- no idea if it should be + // modeled as empty array or empty map -- return isUndefined(), + // which can be consumed as either + LL_DEBUGS("Lua") << "empty table" << LL_ENDL; + return {}; + } + // key is at stack index -2, value at index -1 + // from here until lua_next() returns 0, have to lua_pop(2) if we + // return early + LuaPopper popper(L, 2); + // Remember the type of the first key + auto firstkeytype{ lua_type(L, -2) }; + LL_DEBUGS("Lua") << "table not empty, first key type " << lua_typename(L, firstkeytype) + << LL_ENDL; + switch (firstkeytype) + { + case LUA_TNUMBER: + { + // First Lua key is a number: try to convert table to LLSD array. + // This is tricky because we don't know in advance the size of the + // array. The Lua reference manual says that lua_rawlen() is the + // same as the length operator '#'; but the length operator states + // that it might stop at any "hole" in the subject table. + // Moreover, the Lua next() function (and presumably lua_next()) + // traverses a table in unspecified order, even for numeric keys + // (emphasized in the doc). + // Make a preliminary pass over the whole table to validate and to + // collect keys. + std::vector keys; + // Try to determine the length of the table. If the length + // operator is truthful, avoid allocations while we grow the keys + // vector. Even if it's not, we can still grow the vector, albeit + // a little less efficiently. + keys.reserve(lua_objlen(L, index)); + do + { + auto arraykeytype{ lua_type(L, -2) }; + switch (arraykeytype) + { + case LUA_TNUMBER: + { + int isint; + lua_Integer intkey{ lua_tointegerx(L, -2, &isint) }; + if (! isint) + { + // key isn't an integer - this doesn't fit our LLSD + // array constraints + return lluau::error(L, "Expected integer array key, got %f instead", + lua_tonumber(L, -2)); + } + if (intkey < 1) + { + return lluau::error(L, "array key %d out of bounds", int(intkey)); + } + + keys.push_back(LLSD::Integer(intkey)); + break; + } + + case LUA_TSTRING: + // break out strings specially to report the value + return lluau::error(L, "Cannot convert string array key '%s' to LLSD", + lua_tostring(L, -2)); + + default: + return lluau::error(L, "Cannot convert %s array key to LLSD", + lua_typename(L, arraykeytype)); + } + + // remove value, keep key for next iteration + lua_pop(L, 1); + } while (lua_next(L, index) != 0); + popper.disarm(); + // Table keys are all integers: are they reasonable integers? + // Arbitrary max: may bite us, but more likely to protect us + size_t array_max{ 10000 }; + if (keys.size() > array_max) + { + return lluau::error(L, "Conversion from Lua to LLSD array limited to %d entries", + int(array_max)); + } + // We know the smallest key is >= 1. Check the largest. We also + // know the vector is NOT empty, else we wouldn't have gotten here. + std::sort(keys.begin(), keys.end()); + LLSD::Integer highkey = *keys.rbegin(); + if ((highkey - LLSD::Integer(keys.size())) > 100) + { + // Looks like we've gone beyond intentional array gaps into + // crazy key territory. + return lluau::error(L, "Gaps in Lua table too large for conversion to LLSD array"); + } + LL_DEBUGS("Lua") << "collected " << keys.size() << " keys, max " << highkey << LL_ENDL; + // right away expand the result array to the size we'll need + LLSD result{ LLSD::emptyArray() }; + result[highkey - 1] = LLSD(); + // Traverse the table again, and this time populate result array. + lua_pushnil(L); // first key + while (lua_next(L, index)) + { + // key at stack index -2, value at index -1 + // We've already validated lua_tointegerx() for each key. + auto key{ lua_tointeger(L, -2) }; + LL_DEBUGS("Lua") << "key " << key << ':' << LL_ENDL; + // Don't forget to subtract 1 from Lua key for LLSD subscript! + result[LLSD::Integer(key) - 1] = lua_tollsd(L, -1); + // remove value, keep key for next iteration + lua_pop(L, 1); + } + return result; + } + + case LUA_TSTRING: + { + // First Lua key is a string: try to convert table to LLSD map + LLSD result{ LLSD::emptyMap() }; + do + { + auto mapkeytype{ lua_type(L, -2) }; + if (mapkeytype != LUA_TSTRING) + { + return lluau::error(L, "Cannot convert %s map key to LLSD", + lua_typename(L, mapkeytype)); + } + + auto key{ lua_tostdstring(L, -2) }; + LL_DEBUGS("Lua") << "map key " << std::quoted(key) << ':' << LL_ENDL; + result[key] = lua_tollsd(L, -1); + // remove value, keep key for next iteration + lua_pop(L, 1); + } while (lua_next(L, index) != 0); + popper.disarm(); + return result; + } + + default: + // First Lua key isn't number or string: sorry + return lluau::error(L, "Cannot convert %s table key to LLSD", + lua_typename(L, firstkeytype)); + } + } + + default: + // Other Lua entities (e.g. function, C function, light userdata, + // thread, userdata) are not convertible to LLSD, indicating a coding + // error in the caller. + return lluau::error(L, "Cannot convert type %s to LLSD", luaL_typename(L, index)); + } +} + +// By analogy with existing lua_pushmumble() functions, push onto state L's +// stack a Lua object corresponding to the passed LLSD object. +void lua_pushllsd(lua_State* L, const LLSD& data) +{ + // might need 2 slots for array or map + luaL_checkstack(L, 2, nullptr); + switch (data.type()) + { + case LLSD::TypeUndefined: + lua_pushnil(L); + break; + + case LLSD::TypeBoolean: + lua_pushboolean(L, data.asBoolean()); + break; + + case LLSD::TypeInteger: + lua_pushinteger(L, data.asInteger()); + break; + + case LLSD::TypeReal: + lua_pushnumber(L, data.asReal()); + break; + + case LLSD::TypeBinary: + { + auto binary{ data.asBinary() }; + std::memcpy(lua_newuserdata(L, binary.size()), + binary.data(), binary.size()); + break; + } + + case LLSD::TypeMap: + { + // push a new table with space for our non-array keys + lua_createtable(L, 0, data.size()); + for (const auto& pair: llsd::inMap(data)) + { + // push value -- so now table is at -2, value at -1 + lua_pushllsd(L, pair.second); + // pop value, assign to table[key] + lua_setfield(L, -2, pair.first.c_str()); + } + break; + } + + case LLSD::TypeArray: + { + // push a new table with space for array entries + lua_createtable(L, data.size(), 0); + lua_Integer key{ 0 }; + for (const auto& item: llsd::inArray(data)) + { + // push new array value: table at -2, value at -1 + lua_pushllsd(L, item); + // pop value, assign table[key] = value + lua_rawseti(L, -2, ++key); + } + break; + } + + case LLSD::TypeString: + case LLSD::TypeUUID: + case LLSD::TypeDate: + case LLSD::TypeURI: + default: + { + lua_pushstdstring(L, data.asString()); + break; + } + } +} + +LuaState::LuaState(const std::string_view& desc, script_finished_fn cb): + mDesc(desc), + mCallback(cb), + mState(luaL_newstate()) +{ + luaL_openlibs(mState); + LuaFunction::init(mState); + // Try to make print() write to our log. + lua_register(mState, "print", LuaFunction::get("print_info")); +} + +LuaState::~LuaState() +{ + // Did somebody call listen_events() on this LuaState? + // That is, is there a LuaListener key in its registry? + auto keytype{ lua_getfield(mState, LUA_REGISTRYINDEX, "event.listener") }; + if (keytype == LUA_TNUMBER) + { + // We do have a LuaListener. Retrieve it. + int isint; + auto listener{ LuaListener::getInstance(lua_tointegerx(mState, -1, &isint)) }; + // pop the int "event.listener" key + lua_pop(mState, 1); + // if we got a LuaListener instance, destroy it + // (if (! isint), lua_tointegerx() returned 0, but key 0 might + // validly designate someone ELSE's LuaListener) + if (isint && listener) + { + auto lptr{ listener.get() }; + listener.reset(); + delete lptr; + } + } + + lua_close(mState); + + if (mCallback) + { + // mError potentially set by previous checkLua() call(s) + mCallback(mError); + } +} + +bool LuaState::checkLua(int r) +{ + if (r != LUA_OK) + { + mError = lua_tostring(mState, -1); + lua_pop(mState, 1); + + LL_WARNS() << mDesc << ": " << mError << LL_ENDL; + return false; + } + return true; +} + + +LuaPopper::~LuaPopper() +{ + if (mCount) + { + lua_pop(mState, mCount); + } +} + +LuaFunction::LuaFunction(const std::string_view& name, lua_CFunction function) +{ + getRegistry().emplace(name, function); +} + +void LuaFunction::init(lua_State* L) +{ + for (const auto& pair: getRegistry()) + { + lua_register(L, pair.first.c_str(), pair.second); + } +} + +lua_CFunction LuaFunction::get(const std::string& key) +{ + // use find() instead of subscripting to avoid creating an entry for + // unknown key + const auto& registry{ getRegistry() }; + auto found{ registry.find(key) }; + return (found == registry.end())? nullptr : found->second; +} + +LuaFunction::Registry& LuaFunction::getRegistry() +{ + // use a function-local static to ensure it's initialized + static Registry registry; + return registry; +} + + +std::ostream& operator<<(std::ostream& out, const lua_what& self) +{ + switch (lua_type(self.L, self.index)) + { + case LUA_TNONE: + // distinguish acceptable but non-valid index + out << "none"; + break; + + case LUA_TNIL: + out << "nil"; + break; + + case LUA_TBOOLEAN: + { + auto oldflags { out.flags() }; + out << std::boolalpha << lua_toboolean(self.L, self.index); + out.flags(oldflags); + break; + } + + case LUA_TNUMBER: + out << lua_tonumber(self.L, self.index); + break; + + case LUA_TSTRING: + out << std::quoted(lua_tostdstring(self.L, self.index)); + break; + + case LUA_TUSERDATA: + { + const S32 maxlen = 20; + S32 binlen{ lua_rawlen(self.L, self.index) }; + LLSD::Binary binary(std::min(maxlen, binlen)); + std::memcpy(binary.data(), lua_touserdata(self.L, self.index), binary.size()); + out << LL::hexdump(binary); + if (binlen > maxlen) + { + out << "...(" << (binlen - maxlen) << " more)"; + } + break; + } + + case LUA_TLIGHTUSERDATA: + out << lua_touserdata(self.L, self.index); + break; + + default: + // anything else, don't bother trying to report value, just type + out << lua_typename(self.L, lua_type(self.L, self.index)); + break; + } + return out; +} + +std::ostream& operator<<(std::ostream& out, const lua_stack& self) +{ + const char* sep = "stack: ["; + for (int index = 1; index <= lua_gettop(self.L); ++index) + { + out << sep << lua_what(self.L, index); + sep = ", "; + } + out << ']'; + return out; +} + +DebugExit::~DebugExit() +{ + LL_DEBUGS("Lua") << "exit " << mName << LL_ENDL; +} -- cgit v1.2.3 From 5b0404961e35ecca46148b90f2c27aed1bad607f Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 7 Feb 2024 12:50:26 -0500 Subject: Add machinery to capture result of running a Lua script or snippet. Add LuaState::expr() that evaluates a Lua snippet and reports back any result (or error) left on the stack. Add LLLUAmanager::runScriptFile() and runScriptLine() overloads that accept a callback with an (int count, LLSD result) signature. The count disambiguates (error, no result, one result, array of results). Also add overloads that accept an existing LuaState instance. Also add waitScriptFile() and waitScriptLine() methods that pause the calling coroutine until the Lua script completes, and return its results. Instead of giving LuaState a description to use for all subsequent checkLua() calls, remove description from its constructor and data members. Move to expr() and checkLua() parameters: we want a description specific to each operation, rather than for the LuaState as a whole. This prepares for persistent LuaState instances. For now, the existing script_finished_fn semantics remain: the callback will be called only when the LuaState is destroyed. This may need to change as we migrate towards longer-lasting LuaState instances. Make lua_function(name) macro append suffixes to the name for both the LuaFunction subclass declaration and the instance declaration. This allows publishing a lua_function() name such as sleep(), which already has a different C++ declaration. Move the Lua sleep() entry point to a standalone lua_function(sleep), instead of a lambda in the body of runScriptFile(). --- indra/llcommon/lua_function.cpp | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index f6db4eafe1..5466e0058e 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -407,8 +407,7 @@ void lua_pushllsd(lua_State* L, const LLSD& data) } } -LuaState::LuaState(const std::string_view& desc, script_finished_fn cb): - mDesc(desc), +LuaState::LuaState(script_finished_fn cb): mCallback(cb), mState(luaL_newstate()) { @@ -450,19 +449,49 @@ LuaState::~LuaState() } } -bool LuaState::checkLua(int r) +bool LuaState::checkLua(const std::string& desc, int r) { if (r != LUA_OK) { mError = lua_tostring(mState, -1); lua_pop(mState, 1); - LL_WARNS() << mDesc << ": " << mError << LL_ENDL; + LL_WARNS() << desc << ": " << mError << LL_ENDL; return false; } return true; } +std::pair LuaState::expr(const std::string& desc, const std::string& text) +{ + if (! checkLua(desc, lluau::dostring(mState, desc, text))) + return { -1, mError }; + + // here we believe there was no error -- did the Lua fragment leave + // anything on the stack? + std::pair result{ lua_gettop(mState), {} }; + if (! result.first) + return result; + + // aha, at least one entry on the stack! + if (result.first == 1) + { + result.second = lua_tollsd(mState, 1); + // pop the result we claimed + lua_settop(mState, 0); + return result; + } + + // multiple entries on the stack + for (int index = 1; index <= result.first; ++index) + { + result.second.append(lua_tollsd(mState, index)); + } + // pop everything + lua_settop(mState, 0); + return result; +} + LuaPopper::~LuaPopper() { -- cgit v1.2.3 From 382ec7b2269546605da7ba0a44654deaadf3ff3c Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 7 Feb 2024 22:03:06 -0500 Subject: Fix lluau::dostring() for return values. We were calling lua_pcall() in such a way as to discard any values returned by the Lua chunk. Work around Luau's broken lua_tointegerx(), which unlike vanilla Lua's does not report whether the value at the specified index is or is not an integer. --- indra/llcommon/lua_function.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 5466e0058e..956630ed3c 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -47,7 +47,10 @@ int lluau::dostring(lua_State* L, const std::string& desc, const std::string& te return r; } // free bytecode - return lua_pcall(L, 0, 0, 0); + // It's important to pass LUA_MULTRET as the expected number of return + // values: if we pass any fixed number, we discard any returned values + // beyond that number. + return lua_pcall(L, 0, LUA_MULTRET, 0); } std::string lua_tostdstring(lua_State* L, int index) @@ -95,16 +98,20 @@ LLSD lua_tollsd(lua_State* L, int index) case LUA_TNUMBER: { - // check if integer truncation leaves the number intact - int isint; - lua_Integer intval{ lua_tointegerx(L, index, &isint) }; - if (isint) + // Vanilla Lua supports lua_tointegerx(), which tells the caller + // whether the number at the specified stack index is or is not an + // integer. Apparently the function exists but does not work right in + // Luau: it reports even non-integer numbers as integers. + // Instead, check if integer truncation leaves the number intact. + lua_Number numval{ lua_tonumber(L, index) }; + lua_Integer intval{ narrow(numval) }; + if (lua_Number(intval) == numval) { return LLSD::Integer(intval); } else { - return lua_tonumber(L, index); + return numval; } } -- cgit v1.2.3 From 4e9794bd06660baea6d16779f6e354ca99e11b8a Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 8 Feb 2024 13:59:19 -0500 Subject: Add required helptext parameter to lua_function() macro. Extend the LuaFunction::Registry map to store helptext as well as the function pointer. Add help text to every existing lua_function() invocation. --- indra/llcommon/lua_function.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 956630ed3c..07e0c1fac2 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -508,16 +508,18 @@ LuaPopper::~LuaPopper() } } -LuaFunction::LuaFunction(const std::string_view& name, lua_CFunction function) +LuaFunction::LuaFunction(const std::string_view& name, lua_CFunction function, + const std::string_view& helptext) { - getRegistry().emplace(name, function); + getRegistry().emplace(name, Registry::mapped_type{ function, helptext }); } void LuaFunction::init(lua_State* L) { - for (const auto& pair: getRegistry()) + for (const auto& [name, pair]: getRegistry()) { - lua_register(L, pair.first.c_str(), pair.second); + const auto& [funcptr, helptext] = pair; + lua_register(L, name.c_str(), funcptr); } } @@ -527,7 +529,7 @@ lua_CFunction LuaFunction::get(const std::string& key) // unknown key const auto& registry{ getRegistry() }; auto found{ registry.find(key) }; - return (found == registry.end())? nullptr : found->second; + return (found == registry.end())? nullptr : found->second.first; } LuaFunction::Registry& LuaFunction::getRegistry() -- cgit v1.2.3 From b4583fac09657cb64ed02e82e12ce69c4ace225d Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 12 Feb 2024 17:10:42 -0500 Subject: WIP: Changes towards supporting Lua console help text. --- indra/llcommon/lua_function.cpp | 65 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 07e0c1fac2..455e853e8f 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -25,6 +25,9 @@ #include "llsdutil.h" #include "lualistener.h" +/***************************************************************************** +* luau namespace +*****************************************************************************/ namespace { // can't specify free function free() as a unique_ptr deleter @@ -53,6 +56,9 @@ int lluau::dostring(lua_State* L, const std::string& desc, const std::string& te return lua_pcall(L, 0, LUA_MULTRET, 0); } +/***************************************************************************** +* Lua <=> C++ conversions +*****************************************************************************/ std::string lua_tostdstring(lua_State* L, int index) { size_t len; @@ -414,6 +420,9 @@ void lua_pushllsd(lua_State* L, const LLSD& data) } } +/***************************************************************************** +* LuaState class +*****************************************************************************/ LuaState::LuaState(script_finished_fn cb): mCallback(cb), mState(luaL_newstate()) @@ -499,7 +508,9 @@ std::pair LuaState::expr(const std::string& desc, const std::string& return result; } - +/***************************************************************************** +* LuaPopper class +*****************************************************************************/ LuaPopper::~LuaPopper() { if (mCount) @@ -508,6 +519,9 @@ LuaPopper::~LuaPopper() } } +/***************************************************************************** +* LuaFunction class +*****************************************************************************/ LuaFunction::LuaFunction(const std::string_view& name, lua_CFunction function, const std::string_view& helptext) { @@ -539,7 +553,9 @@ LuaFunction::Registry& LuaFunction::getRegistry() return registry; } - +/***************************************************************************** +* lua_what +*****************************************************************************/ std::ostream& operator<<(std::ostream& out, const lua_what& self) { switch (lua_type(self.L, self.index)) @@ -595,6 +611,9 @@ std::ostream& operator<<(std::ostream& out, const lua_what& self) return out; } +/***************************************************************************** +* lua_stack +*****************************************************************************/ std::ostream& operator<<(std::ostream& out, const lua_stack& self) { const char* sep = "stack: ["; @@ -607,7 +626,49 @@ std::ostream& operator<<(std::ostream& out, const lua_stack& self) return out; } +/***************************************************************************** +* DebugExit +*****************************************************************************/ DebugExit::~DebugExit() { LL_DEBUGS("Lua") << "exit " << mName << LL_ENDL; } + +/***************************************************************************** +* help() +*****************************************************************************/ +lua_function(help, + "help(): list viewer's Lua functions\n" + "help(function): show help string for specific function") +{ + auto& luapump{ LLEventPumps::instance().obtain("lua output") }; + const auto& registered{ LuaFunction::getRegistered() }; + if (! lua_gettop(L)) + { + // no arguments passed: list all lua_functions + for (const auto& [name, pair] : registered) + { + const auto& [fptr, helptext] = pair; + luapump.post(helptext); + } + } + else + { + // arguments passed: list each of the specified lua_functions + for (int idx = 1, top = lua_gettop(L); idx <= top; ++idx) + { + auto arg{ lua_tostdstring(L, idx) }; + if (auto found = registered.find(arg); found != registered.end()) + { + luapump.post(found->second.second); + } + else + { + luapump.post(arg + ": NOT FOUND"); + } + } + // pop all arguments + lua_settop(L, 0); + } + return 0; // void return +} -- cgit v1.2.3 From 3044a6e62e628fdb3c4b8832fd23e566216d7bb9 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 13 Feb 2024 12:24:13 -0500 Subject: Add help() function to Lua "builtins." help() with no argument lists all our viewer builtins. help(function, function, ...) shows help text for each named function. Each argument can be either a string or the function in question (e.g. help(help)). To support Lua-related text containing line breaks, make LLTextEditor:: pasteTextWithLinebreaks() a public template method. Change the existing implementation, which specifically accepts (const LLWString&), into its LLWString specialization. The generic template passes llconvert(arg) to that specialization, the one real implementation. Make LLFloaterLUADebug methods call pasteTextWithLinebreaks() instead of insertText(), which ignores newline characters. To allow help() to accept an actual function as well as a string name, add a lookup-by-function-pointer map to LuaFunction. (A Lua function does not store a name.) Make the constructor store an entry in the new lookup map as well as in the original registry map. Change LuaFunction::getRegistry() and getRegistered() to getState() and getRState(), respectively. Each returns a std::pair, but the first binds non-const references while the second binds const references. --- indra/llcommon/lua_function.cpp | 113 ++++++++++++++++++++++++---------------- 1 file changed, 68 insertions(+), 45 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 455e853e8f..467ca04449 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -525,12 +525,15 @@ LuaPopper::~LuaPopper() LuaFunction::LuaFunction(const std::string_view& name, lua_CFunction function, const std::string_view& helptext) { - getRegistry().emplace(name, Registry::mapped_type{ function, helptext }); + const auto& [registry, lookup] = getState(); + registry.emplace(name, Registry::mapped_type{ function, helptext }); + lookup.emplace(function, name); } void LuaFunction::init(lua_State* L) { - for (const auto& [name, pair]: getRegistry()) + const auto& [registry, lookup] = getRState(); + for (const auto& [name, pair]: registry) { const auto& [funcptr, helptext] = pair; lua_register(L, name.c_str(), funcptr); @@ -541,16 +544,75 @@ lua_CFunction LuaFunction::get(const std::string& key) { // use find() instead of subscripting to avoid creating an entry for // unknown key - const auto& registry{ getRegistry() }; + const auto& [registry, lookup] = getState(); auto found{ registry.find(key) }; return (found == registry.end())? nullptr : found->second.first; } -LuaFunction::Registry& LuaFunction::getRegistry() +std::pair LuaFunction::getState() { - // use a function-local static to ensure it's initialized + // use function-local statics to ensure they're initialized static Registry registry; - return registry; + static Lookup lookup; + return { registry, lookup }; +} + +/***************************************************************************** +* help() +*****************************************************************************/ +lua_function(help, + "help(): list viewer's Lua functions\n" + "help(function): show help string for specific function") +{ + auto& luapump{ LLEventPumps::instance().obtain("lua output") }; + const auto& [registry, lookup]{ LuaFunction::getRState() }; + if (! lua_gettop(L)) + { + // no arguments passed: list all lua_functions + for (const auto& [name, pair] : registry) + { + const auto& [fptr, helptext] = pair; + luapump.post(helptext); + } + } + else + { + // arguments passed: list each of the specified lua_functions + for (int idx = 1, top = lua_gettop(L); idx <= top; ++idx) + { + std::string arg{ stringize("") }; + if (lua_type(L, idx) == LUA_TSTRING) + { + arg = lua_tostdstring(L, idx); + } + else if (lua_type(L, idx) == LUA_TFUNCTION) + { + // Caller passed the actual function instead of its string + // name. A Lua function is an anonymous callable object; it + // has a name only by assigment. You can't ask Lua for a + // function's name, which is why our constructor maintains a + // reverse Lookup map. + auto function{ lua_tocfunction(L, idx) }; + if (auto found = lookup.find(function); found != lookup.end()) + { + // okay, pass found name to lookup below + arg = found->second; + } + } + + if (auto found = registry.find(arg); found != registry.end()) + { + luapump.post(found->second.second); + } + else + { + luapump.post(arg + ": NOT FOUND"); + } + } + // pop all arguments + lua_settop(L, 0); + } + return 0; // void return } /***************************************************************************** @@ -633,42 +695,3 @@ DebugExit::~DebugExit() { LL_DEBUGS("Lua") << "exit " << mName << LL_ENDL; } - -/***************************************************************************** -* help() -*****************************************************************************/ -lua_function(help, - "help(): list viewer's Lua functions\n" - "help(function): show help string for specific function") -{ - auto& luapump{ LLEventPumps::instance().obtain("lua output") }; - const auto& registered{ LuaFunction::getRegistered() }; - if (! lua_gettop(L)) - { - // no arguments passed: list all lua_functions - for (const auto& [name, pair] : registered) - { - const auto& [fptr, helptext] = pair; - luapump.post(helptext); - } - } - else - { - // arguments passed: list each of the specified lua_functions - for (int idx = 1, top = lua_gettop(L); idx <= top; ++idx) - { - auto arg{ lua_tostdstring(L, idx) }; - if (auto found = registered.find(arg); found != registered.end()) - { - luapump.post(found->second.second); - } - else - { - luapump.post(arg + ": NOT FOUND"); - } - } - // pop all arguments - lua_settop(L, 0); - } - return 0; // void return -} -- cgit v1.2.3 From d583fb8badd8060c0f74bcb6e99bb6e7d08a67d0 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 13 Feb 2024 17:34:00 -0500 Subject: Add leaphelp() Lua builtin function for help on LEAP operations. leaphelp() (no argument) shows a list of all LEAP APIs. leaphelp(API) shows further help for a specific API. Both forms query LuaListener's LeapListener and report its responses. In future we might reimplement leaphelp() as a Lua function. Add LuaState::getListener() method, which checks whether there's a LuaListener associated with this LuaState and returns a pointer if so. Add LuaState::obtainListener() method, which finds or creates a LuaListener for this LuaState and returns its pointer. Both the above use logic migrated from the Lua listen_events() entry point, which now calls obtainListener() instead. --- indra/llcommon/lua_function.cpp | 130 +++++++++++++++++++++++++++++++++++----- 1 file changed, 114 insertions(+), 16 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 467ca04449..13210fe5a6 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -21,6 +21,7 @@ // external library headers // other Linden headers #include "hexdump.h" +#include "lleventcoro.h" #include "llsd.h" #include "llsdutil.h" #include "lualistener.h" @@ -435,25 +436,15 @@ LuaState::LuaState(script_finished_fn cb): LuaState::~LuaState() { - // Did somebody call listen_events() on this LuaState? + // Did somebody call obtainListener() on this LuaState? // That is, is there a LuaListener key in its registry? - auto keytype{ lua_getfield(mState, LUA_REGISTRYINDEX, "event.listener") }; - if (keytype == LUA_TNUMBER) + auto listener{ getListener() }; + if (listener) { - // We do have a LuaListener. Retrieve it. - int isint; - auto listener{ LuaListener::getInstance(lua_tointegerx(mState, -1, &isint)) }; - // pop the int "event.listener" key - lua_pop(mState, 1); // if we got a LuaListener instance, destroy it - // (if (! isint), lua_tointegerx() returned 0, but key 0 might - // validly designate someone ELSE's LuaListener) - if (isint && listener) - { - auto lptr{ listener.get() }; - listener.reset(); - delete lptr; - } + auto lptr{ listener.get() }; + listener.reset(); + delete lptr; } lua_close(mState); @@ -508,6 +499,45 @@ std::pair LuaState::expr(const std::string& desc, const std::string& return result; } +LuaListener::ptr_t LuaState::getListener(lua_State* L) +{ + // have to use one more stack slot + luaL_checkstack(L, 1, nullptr); + LuaListener::ptr_t listener; + // Does this lua_State already have a LuaListener stored in the registry? + auto keytype{ lua_getfield(L, LUA_REGISTRYINDEX, "event.listener") }; + llassert(keytype == LUA_TNIL || keytype == LUA_TNUMBER); + if (keytype == LUA_TNUMBER) + { + // We do already have a LuaListener. Retrieve it. + int isint; + listener = LuaListener::getInstance(lua_tointegerx(L, -1, &isint)); + // Nobody should have destroyed this LuaListener instance! + llassert(isint && listener); + } + // pop the int "event.listener" key + lua_pop(L, 1); + return listener; +} + +LuaListener::ptr_t LuaState::obtainListener(lua_State* L) +{ + auto listener{ getListener(L) }; + if (! listener) + { + // have to use one more stack slot + luaL_checkstack(L, 1, nullptr); + // instantiate a new LuaListener, binding the L state -- but use a + // no-op deleter: we do NOT want this ptr_t to manage the lifespan of + // this new LuaListener! + listener.reset(new LuaListener(L), [](LuaListener*){}); + // set its key in the field where we'll look for it later + lua_pushinteger(L, listener->getKey()); + lua_setfield(L, LUA_REGISTRYINDEX, "event.listener"); + } + return listener; +} + /***************************************************************************** * LuaPopper class *****************************************************************************/ @@ -615,6 +645,74 @@ lua_function(help, return 0; // void return } +/***************************************************************************** +* leaphelp() +*****************************************************************************/ +lua_function( + leaphelp, + "leaphelp(): list viewer's LEAP APIs\n" + "leaphelp(api): show help for specific api string name") +{ + LLSD request; + int top{ lua_gettop(L) }; + if (top) + { + request = llsd::map("op", "getAPI", "api", lua_tostdstring(L, 1)); + } + else + { + request = llsd::map("op", "getAPIs"); + } + // pop all args + lua_settop(L, 0); + + auto& outpump{ LLEventPumps::instance().obtain("lua output") }; + auto listener{ LuaState::obtainListener(L) }; + LLEventStream replyPump("leaphelp", true); + // ask the LuaListener's LeapListener and suspend calling coroutine until reply + auto reply{ llcoro::postAndSuspend(request, listener->getCommandName(), replyPump, "reply") }; + reply.erase("reqid"); + + if (auto error = reply["error"]; error.isString()) + { + outpump.post(error.asString()); + return 0; + } + + if (top) + { + // caller wants a specific API + outpump.post(stringize(reply["name"].asString(), ":\n", reply["desc"].asString())); + for (const auto& opmap : llsd::inArray(reply["ops"])) + { + std::ostringstream reqstr; + auto req{ opmap["required"] }; + if (req.isArray()) + { + const char* sep = " (requires "; + for (const auto& [reqkey, reqval] : llsd::inMap(req)) + { + reqstr << sep << reqkey; + sep = ", "; + } + reqstr << ")"; + } + outpump.post(stringize("---- ", reply["key"].asString(), " == '", + opmap["name"].asString(), "'", reqstr.str(), ":\n", + opmap["desc"].asString())); + } + } + else + { + // caller wants a list of APIs + for (const auto& [name, data] : llsd::inMap(reply)) + { + outpump.post(stringize("==== ", name, ":\n", data["desc"].asString())); + } + } + return 0; // void return +} + /***************************************************************************** * lua_what *****************************************************************************/ -- cgit v1.2.3 From b305fb6411939bf6afbe2ecf2c1bdf765c9247a9 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Tue, 20 Feb 2024 14:25:56 +0200 Subject: Initial require implementation --- indra/llcommon/lua_function.cpp | 9 --------- 1 file changed, 9 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 07e0c1fac2..17d81e8fc7 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -25,15 +25,6 @@ #include "llsdutil.h" #include "lualistener.h" -namespace -{ - // can't specify free function free() as a unique_ptr deleter - struct freer - { - void operator()(void* ptr){ free(ptr); } - }; -} // anonymous namespace - int lluau::dostring(lua_State* L, const std::string& desc, const std::string& text) { { -- cgit v1.2.3 From 32bf9c7b7a9d2b2428b052d74389ec48ccc427cf Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Wed, 21 Feb 2024 17:11:33 +0200 Subject: Add the option to use clean lua_State in "Lua debug" floater --- indra/llcommon/lua_function.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 17d81e8fc7..e9b4bf0b89 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -406,9 +406,14 @@ void lua_pushllsd(lua_State* L, const LLSD& data) } LuaState::LuaState(script_finished_fn cb): - mCallback(cb), - mState(luaL_newstate()) + mCallback(cb) { + initLuaState(); +} + +void LuaState::initLuaState() +{ + mState = luaL_newstate(); luaL_openlibs(mState); LuaFunction::init(mState); // Try to make print() write to our log. -- cgit v1.2.3 From d7e411d8fb355b7e4198c3521144e3fee2b8e62c Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 23 Feb 2024 09:56:16 -0500 Subject: Ditch DebugExit: we already have Debug (in debug.h) --- indra/llcommon/lua_function.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 13210fe5a6..906a3f379c 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -16,6 +16,7 @@ // STL headers // std headers #include +#include // std::quoted #include #include // std::unique_ptr // external library headers @@ -92,7 +93,6 @@ LLSD lua_tollsd(lua_State* L, int index) { LL_DEBUGS("Lua") << "lua_tollsd(" << index << ") of " << lua_gettop(L) << " stack entries: " << lua_what(L, index) << LL_ENDL; - DebugExit log_exit("lua_tollsd()"); switch (lua_type(L, index)) { case LUA_TNONE: @@ -785,11 +785,3 @@ std::ostream& operator<<(std::ostream& out, const lua_stack& self) out << ']'; return out; } - -/***************************************************************************** -* DebugExit -*****************************************************************************/ -DebugExit::~DebugExit() -{ - LL_DEBUGS("Lua") << "exit " << mName << LL_ENDL; -} -- cgit v1.2.3 From 49785357e07f6309e2504b56829d9916f75168b2 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Fri, 23 Feb 2024 16:57:00 +0200 Subject: require() code clean-up --- indra/llcommon/lua_function.cpp | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index e9b4bf0b89..41bb7bac12 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -25,18 +25,20 @@ #include "llsdutil.h" #include "lualistener.h" -int lluau::dostring(lua_State* L, const std::string& desc, const std::string& text) +namespace { + // can't specify free function free() as a unique_ptr deleter + struct freer { - size_t bytecodeSize = 0; - // The char* returned by luau_compile() must be freed by calling free(). - // Use unique_ptr so the memory will be freed even if luau_load() throws. - std::unique_ptr bytecode{ - luau_compile(text.data(), text.length(), nullptr, &bytecodeSize)}; - auto r = luau_load(L, desc.data(), bytecode.get(), bytecodeSize, 0); - if (r != LUA_OK) - return r; - } // free bytecode + void operator()(void* ptr){ free(ptr); } + }; +} // anonymous namespace + +int lluau::dostring(lua_State* L, const std::string& desc, const std::string& text) +{ + auto r = loadstring(L, desc, text); + if (r != LUA_OK) + return r; // It's important to pass LUA_MULTRET as the expected number of return // values: if we pass any fixed number, we discard any returned values @@ -44,6 +46,16 @@ int lluau::dostring(lua_State* L, const std::string& desc, const std::string& te return lua_pcall(L, 0, LUA_MULTRET, 0); } +int lluau::loadstring(lua_State *L, const std::string &desc, const std::string &text) +{ + size_t bytecodeSize = 0; + // The char* returned by luau_compile() must be freed by calling free(). + // Use unique_ptr so the memory will be freed even if luau_load() throws. + std::unique_ptr bytecode{ + luau_compile(text.data(), text.length(), nullptr, &bytecodeSize)}; + return luau_load(L, desc.data(), bytecode.get(), bytecodeSize, 0); +} + std::string lua_tostdstring(lua_State* L, int index) { size_t len; @@ -406,13 +418,18 @@ void lua_pushllsd(lua_State* L, const LLSD& data) } LuaState::LuaState(script_finished_fn cb): - mCallback(cb) + mCallback(cb), + mState(nullptr) { initLuaState(); } void LuaState::initLuaState() { + if (mState) + { + lua_close(mState); + } mState = luaL_newstate(); luaL_openlibs(mState); LuaFunction::init(mState); -- cgit v1.2.3 From 9dae3e96ee6b0cb4139f368488da85f9961d1d4f Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 29 Feb 2024 12:09:59 -0500 Subject: Refactor require() to make it easier to reason about Lua stack usage. Push throwing Lua errors down into LLRequireResolver::findModule() and findModuleImpl() so their callers don't have to handle the error case. That eliminates finishrequire(). require() itself now only retrieves (and pops) the passed module name and calls LLRequireResolver::resolveRequire() to do the actual work. resolveRequire() is now void. It only instantiates LLRequireResolver and calls its findModule(). findModule() is now also void. It's guaranteed to either push the loaded Lua module or throw a Lua error. In particular, when findPathImpl() cannot find the specified module, findModule() throws an error. That replaces ModuleStatus::NotFound. Since std::filesystem::path::append() aka operator/() detects when its right operand is absolute and, in that case, discards the left operand, we no longer need resolveAndStoreDefaultPaths(): we can just invoke that operation inline. When findModule() pushes _MODULES on the Lua stack, it uses LuaRemover (below) to ensure that _MODULES is removed again no matter how findModules() exits. findModuleImpl() now accepts the candidate pathname as its argument. That eliminates mAbsolutePath. findModuleImpl() now returns only bool: true means the module was found and loaded and pushed on the Lua stack, false means not found and nothing was pushed; no return means an error was reported. Push running a newly found module's source file down into findModuleImpl(). That eliminates the distinction between Cached and FileRead, which obviates ModuleStatus: a bool return means either "previously cached" or "we read it, compiled it, loaded it and ran it." That also eliminates the need to store the module's textual content in mSourceCode. Similarly, once loading the module succeeds, findModuleImpl() caches it in _MODULES right away. That eliminates ResolvedRequire since we need not pass the full pathname of the found module (or its contents) back up through the call chain. Move require() code that runs the new module into private runModule() method, called by findModuleImpl() in the not-cached case. runModule() is the only remaining method that can push either a string error message or the desired module, because of its funny stack manipulations. That means the check for a string error message on the stack top can move down to findModuleImpl(). Add LuaRemover class to ensure that on exit from some particular C++ block, the specified Lua stack entry will definitely be removed. This is different from LuaPopper in that it engages lua_remove() rather than lua_pop(). Also ditch obsolete await_event() Lua entry point. --- indra/llcommon/lua_function.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index a5f1f582d9..78abb8ba7e 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -789,7 +789,8 @@ std::ostream& operator<<(std::ostream& out, const lua_what& self) *****************************************************************************/ std::ostream& operator<<(std::ostream& out, const lua_stack& self) { - const char* sep = "stack: ["; + out << "stack: ["; + const char* sep = ""; for (int index = 1; index <= lua_gettop(self.L); ++index) { out << sep << lua_what(self.L, index); -- cgit v1.2.3 From b4bef56b20fbeaaea60b20bd84d1569ed76cf29b Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 6 Mar 2024 09:54:02 -0500 Subject: Defend LuaState::expr() against lua_tollsd() errors. This is an unusual use case in which lua_tollsd() is called by C++ code without the Lua runtime farther up the call stack. --- indra/llcommon/lua_function.cpp | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 78abb8ba7e..b5de5099ba 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -16,9 +16,11 @@ // STL headers // std headers #include +#include #include // std::quoted #include #include // std::unique_ptr +#include // external library headers // other Linden headers #include "hexdump.h" @@ -26,6 +28,7 @@ #include "llsd.h" #include "llsdutil.h" #include "lualistener.h" +#include "stringize.h" /***************************************************************************** * luau namespace @@ -496,16 +499,45 @@ std::pair LuaState::expr(const std::string& desc, const std::string& // aha, at least one entry on the stack! if (result.first == 1) { - result.second = lua_tollsd(mState, 1); + // Don't forget that lua_tollsd() can throw Lua errors. + try + { + result.second = lua_tollsd(mState, 1); + } + catch (const std::exception& error) + { + // lua_tollsd() is designed to be called from a lua_function(), + // that is, from a C++ function called by Lua. In case of error, + // it throws a Lua error to be caught by the Lua runtime. expr() + // is a peculiar use case in which our C++ code is calling + // lua_tollsd() after return from the Lua runtime. We must catch + // the exception thrown for a Lua error, else it will propagate + // out to the main coroutine and terminate the viewer -- but since + // we instead of the Lua runtime catch it, our lua_State retains + // its internal error status. Any subsequent lua_pcall() calls + // with this lua_State will report error regardless of whether the + // chunk runs successfully. Get a new lua_State(). + initLuaState(); + return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) }; + } // pop the result we claimed lua_settop(mState, 0); return result; } // multiple entries on the stack - for (int index = 1; index <= result.first; ++index) + try + { + for (int index = 1; index <= result.first; ++index) + { + result.second.append(lua_tollsd(mState, index)); + } + } + catch (const std::exception& error) { - result.second.append(lua_tollsd(mState, index)); + // see above comments regarding lua_State's error status + initLuaState(); + return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) }; } // pop everything lua_settop(mState, 0); -- cgit v1.2.3 From 0566af988790e95414ed18cd82206710094d8fae Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 21 Mar 2024 23:56:46 +0900 Subject: WIP: Add fiber.lua module and use in leap.lua and WaitQueue.lua. fiber.lua goes beyond coro.lua in that it distinguishes ready suspended coroutines from waiting suspended coroutines, and presents a rudimentary scheduler in fiber.yield(). yield() can determine that when all coroutines are waiting, it's time to retrieve the next incoming event from the viewer. Moreover, it can detect when all coroutines have completed and exit without being explicitly told. fiber.launch() associates a name with each fiber for debugging purposes. fiber.get_name() retrieves the name of the specified fiber, or the running fiber. fiber.status() is like coroutine.status(), but can return 'ready' or 'waiting' instead of 'suspended'. fiber.yield() leaves the calling fiber ready, but lets other ready fibers run. fiber.wait() suspends the calling fiber and lets other ready fibers run. fiber.wake(), called from some other coroutine, returns the passed fiber to ready status for a future call to fiber.yield(). fiber.run() drives the scheduler to run all fibers to completion. If, on completion of the subject Lua script, LuaState::expr() detects that the script loaded fiber.lua, it calls fiber.run() to finish running any dangling fibers. This lets a script make calls to fiber.launch() and then just fall off the end, leaving the implicit fiber.run() call to run them all. fiber.lua is designed to allow the main thread, as well as explicitly launched coroutines, to make leap.request() calls. This part still needs debugging. The leap.lua module now configures a fiber.set_idle() function that honors leap.done(), but calls get_event_next() and dispatches the next incoming event. leap.request() and generate() now leave the reqid stamp in the response. This lets a caller handle subsequent events with the same reqid, e.g. for LLLuaFloater. Remove leap.process(): it has been superseded by fiber.run(). Remove leap.WaitFor:iterate(): unfortunately that would run afoul of the Luau bug that prevents suspending the calling coroutine within a generic 'for' iterator function. Make leap.lua use weak tables to track WaitFor objects. Make WaitQueue:Dequeue() call fiber.wait() to suspend its caller when the queue is empty, and Enqueue() call fiber.wake() to set it ready again when a new item is pushed. Make llluamanager_test.cpp's leap test script use the fiber module to launch coroutines, instead of the coro module. Fix a bug in which its drain() function was inadvertently setting and testing the global 'item' variable instead of one local to the function. Since some other modules had the same bug, it was getting confused. Also add printf.lua, providing a printf() function. printf() is short for print(string.format()), but it can also print tables: anything not a number or string is formatted using the inspect() function. Clean up some LL_DEBUGS() output left over from debugging lua_tollsd(). --- indra/llcommon/lua_function.cpp | 128 ++++++++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 45 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index b5de5099ba..441e17dafd 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -17,6 +17,7 @@ // std headers #include #include +#include #include // std::quoted #include #include // std::unique_ptr @@ -97,8 +98,6 @@ void lua_pushstdstring(lua_State* L, const std::string& str) // reached by that block raises a Lua error. LLSD lua_tollsd(lua_State* L, int index) { - LL_DEBUGS("Lua") << "lua_tollsd(" << index << ") of " << lua_gettop(L) << " stack entries: " - << lua_what(L, index) << LL_ENDL; switch (lua_type(L, index)) { case LUA_TNONE: @@ -200,15 +199,12 @@ LLSD lua_tollsd(lua_State* L, int index) // we do, below: lua_tollsd(L, -1). If 'index' is -1, then when we // push nil, what we find at index -1 is nil, not the table! index = lua_absindex(L, index); - LL_DEBUGS("Lua") << "checking for empty table" << LL_ENDL; lua_pushnil(L); // first key - LL_DEBUGS("Lua") << lua_stack(L) << LL_ENDL; if (! lua_next(L, index)) { // it's a table, but the table is empty -- no idea if it should be // modeled as empty array or empty map -- return isUndefined(), // which can be consumed as either - LL_DEBUGS("Lua") << "empty table" << LL_ENDL; return {}; } // key is at stack index -2, value at index -1 @@ -217,8 +213,6 @@ LLSD lua_tollsd(lua_State* L, int index) LuaPopper popper(L, 2); // Remember the type of the first key auto firstkeytype{ lua_type(L, -2) }; - LL_DEBUGS("Lua") << "table not empty, first key type " << lua_typename(L, firstkeytype) - << LL_ENDL; switch (firstkeytype) { case LUA_TNUMBER: @@ -296,7 +290,6 @@ LLSD lua_tollsd(lua_State* L, int index) // crazy key territory. return lluau::error(L, "Gaps in Lua table too large for conversion to LLSD array"); } - LL_DEBUGS("Lua") << "collected " << keys.size() << " keys, max " << highkey << LL_ENDL; // right away expand the result array to the size we'll need LLSD result{ LLSD::emptyArray() }; result[highkey - 1] = LLSD(); @@ -307,7 +300,6 @@ LLSD lua_tollsd(lua_State* L, int index) // key at stack index -2, value at index -1 // We've already validated lua_tointegerx() for each key. auto key{ lua_tointeger(L, -2) }; - LL_DEBUGS("Lua") << "key " << key << ':' << LL_ENDL; // Don't forget to subtract 1 from Lua key for LLSD subscript! result[LLSD::Integer(key) - 1] = lua_tollsd(L, -1); // remove value, keep key for next iteration @@ -330,7 +322,6 @@ LLSD lua_tollsd(lua_State* L, int index) } auto key{ lua_tostdstring(L, -2) }; - LL_DEBUGS("Lua") << "map key " << std::quoted(key) << ':' << LL_ENDL; result[key] = lua_tollsd(L, -1); // remove value, keep key for next iteration lua_pop(L, 1); @@ -493,53 +484,100 @@ std::pair LuaState::expr(const std::string& desc, const std::string& // here we believe there was no error -- did the Lua fragment leave // anything on the stack? std::pair result{ lua_gettop(mState), {} }; - if (! result.first) - return result; - - // aha, at least one entry on the stack! - if (result.first == 1) + if (result.first) { - // Don't forget that lua_tollsd() can throw Lua errors. - try + // aha, at least one entry on the stack! + if (result.first == 1) { - result.second = lua_tollsd(mState, 1); + // Don't forget that lua_tollsd() can throw Lua errors. + try + { + result.second = lua_tollsd(mState, 1); + } + catch (const std::exception& error) + { + // lua_tollsd() is designed to be called from a lua_function(), + // that is, from a C++ function called by Lua. In case of error, + // it throws a Lua error to be caught by the Lua runtime. expr() + // is a peculiar use case in which our C++ code is calling + // lua_tollsd() after return from the Lua runtime. We must catch + // the exception thrown for a Lua error, else it will propagate + // out to the main coroutine and terminate the viewer -- but since + // we instead of the Lua runtime catch it, our lua_State retains + // its internal error status. Any subsequent lua_pcall() calls + // with this lua_State will report error regardless of whether the + // chunk runs successfully. Get a new lua_State(). + initLuaState(); + return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) }; + } } - catch (const std::exception& error) + else { - // lua_tollsd() is designed to be called from a lua_function(), - // that is, from a C++ function called by Lua. In case of error, - // it throws a Lua error to be caught by the Lua runtime. expr() - // is a peculiar use case in which our C++ code is calling - // lua_tollsd() after return from the Lua runtime. We must catch - // the exception thrown for a Lua error, else it will propagate - // out to the main coroutine and terminate the viewer -- but since - // we instead of the Lua runtime catch it, our lua_State retains - // its internal error status. Any subsequent lua_pcall() calls - // with this lua_State will report error regardless of whether the - // chunk runs successfully. Get a new lua_State(). - initLuaState(); - return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) }; + // multiple entries on the stack + try + { + for (int index = 1; index <= result.first; ++index) + { + result.second.append(lua_tollsd(mState, index)); + } + } + catch (const std::exception& error) + { + // see above comments regarding lua_State's error status + initLuaState(); + return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) }; + } } - // pop the result we claimed - lua_settop(mState, 0); - return result; } + // pop everything + lua_settop(mState, 0); - // multiple entries on the stack - try - { - for (int index = 1; index <= result.first; ++index) + // If we ran a script that loaded the fiber module, finish up with a call + // to fiber.run(). That allows a script to kick off some number of fibers, + // do some work on the main thread and then fall off the end of the script + // without explicitly appending a call to fiber.run(). run() ensures the + // rest of the fibers run to completion (or error). + luaL_checkstack(mState, 4, nullptr); + // Push _MODULES table on stack + luaL_findtable(mState, LUA_REGISTRYINDEX, "_MODULES", 1); + int index = lua_gettop(mState); + bool found = false; + // Did this chunk already require('fiber')? To find out, we must search + // the _MODULES table, because our require() implementation uses the + // pathname of the module file as the key. Push nil key to start. + lua_pushnil(mState); + while (lua_next(mState, index) != 0) + { + // key is at index -2, value at index -1 + // "While traversing a table, do not call lua_tolstring directly on a + // key, unless you know that the key is actually a string. Recall that + // lua_tolstring changes the value at the given index; this confuses + // the next call to lua_next." + // https://www.lua.org/manual/5.1/manual.html#lua_next + if (lua_type(mState, -2) == LUA_TSTRING && + std::filesystem::path(lua_tostdstring(mState, -2)).stem() == "fiber") { - result.second.append(lua_tollsd(mState, index)); + found = true; + break; } + // pop value so key is at top for lua_next() + lua_pop(mState, 1); } - catch (const std::exception& error) + if (found) { - // see above comments regarding lua_State's error status - initLuaState(); - return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) }; + // okay, index -1 is a table loaded from a file 'fiber.xxx' -- + // does it have a function named 'run'? + auto run_type{ lua_getfield(mState, -1, "run") }; + if (run_type == LUA_TFUNCTION) + { + // there's a fiber.run() function sitting on the top of the stack + // -- call it with no arguments, discarding anything it returns + LL_DEBUGS("Lua") << "Calling fiber.run()" << LL_ENDL; + if (! checkLua(desc, lua_pcall(mState, 0, 0, 0))) + return { -1, mError }; + } } - // pop everything + // pop everything again lua_settop(mState, 0); return result; } -- cgit v1.2.3 From ac4fa418e3a7402f9d9122c726d2fbfc4b8767b2 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 25 Mar 2024 16:29:17 -0400 Subject: Add LL. prefix to viewer entry points, fix existing references. --- indra/llcommon/lua_function.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 441e17dafd..962e9ee2fa 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -439,6 +439,8 @@ void LuaState::initLuaState() LuaFunction::init(mState); // Try to make print() write to our log. lua_register(mState, "print", LuaFunction::get("print_info")); + // We don't want to have to prefix require(). + lua_register(mState, "require", LuaFunction::get("require")); } LuaState::~LuaState() @@ -646,11 +648,20 @@ LuaFunction::LuaFunction(const std::string_view& name, lua_CFunction function, void LuaFunction::init(lua_State* L) { const auto& [registry, lookup] = getRState(); + luaL_checkstack(L, 2, nullptr); + // create LL table -- + // it happens that we know exactly how many non-array members we want + lua_createtable(L, 0, int(narrow(lookup.size()))); + int idx = lua_gettop(L); for (const auto& [name, pair]: registry) { const auto& [funcptr, helptext] = pair; - lua_register(L, name.c_str(), funcptr); + // store funcptr in LL table with saved name + lua_pushcfunction(L, funcptr, name.c_str()); + lua_setfield(L, idx, name.c_str()); } + // store LL in new lua_State's globals + lua_setglobal(L, "LL"); } lua_CFunction LuaFunction::get(const std::string& key) -- cgit v1.2.3 From 98e6356aed0c757f16267cc2ae921f9c90a249fe Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 25 Mar 2024 16:57:28 -0400 Subject: Add LL.check_stop() entry point and call it in fiber scheduler(). fiber.lua's scheduler() is greedy, in the sense that it wants to run every ready Lua fiber before retrieving the next incoming event from the viewer (and possibly blocking for some real time before it becomes available). But check for viewer shutdown before resuming any suspended-but-ready Lua fiber. --- indra/llcommon/lua_function.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 962e9ee2fa..9f0abd5674 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -681,6 +681,15 @@ std::pair LuaFunction::getState() return { registry, lookup }; } +/***************************************************************************** +* check_stop() +*****************************************************************************/ +lua_function(check_stop, "ensure that a Lua script responds to viewer shutdown") +{ + LLCoros::checkStop(); + return 0; +} + /***************************************************************************** * help() *****************************************************************************/ -- cgit v1.2.3 From db6732401af13c3e283c7a0a33fc9c379a8798a6 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 28 Mar 2024 07:06:54 -0400 Subject: Move our lua_register(), lua_rawlen() from lua_function.h to .cpp. --- indra/llcommon/lua_function.cpp | 3 +++ 1 file changed, 3 insertions(+) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 9f0abd5674..e731408c7d 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -31,6 +31,9 @@ #include "lualistener.h" #include "stringize.h" +#define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n))) +#define lua_rawlen lua_objlen + /***************************************************************************** * luau namespace *****************************************************************************/ -- cgit v1.2.3 From 1d1a278712298b91342b687d1b15b107ad51b4ba Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 2 Apr 2024 11:16:29 -0400 Subject: Add LL.source_path(), source_dir() Lua entry points. This helps a Lua script log its own identity, or find associated files relative to its location in the filesystem. Add more comprehensive logging around the start and end of a given Lua script, or its "p.s." fiber.run() call. --- indra/llcommon/lua_function.cpp | 45 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index e731408c7d..96282df554 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -68,6 +68,15 @@ int lluau::loadstring(lua_State *L, const std::string &desc, const std::string & return luau_load(L, desc.data(), bytecode.get(), bytecodeSize, 0); } +std::filesystem::path lluau::source_path(lua_State* L) +{ + //Luau lua_Debug and lua_getinfo() are different compared to default Lua: + //see https://github.com/luau-lang/luau/blob/80928acb92d1e4b6db16bada6d21b1fb6fa66265/VM/include/lua.h + lua_Debug ar; + lua_getinfo(L, 1, "s", &ar); + return ar.source; +} + /***************************************************************************** * Lua <=> C++ conversions *****************************************************************************/ @@ -484,11 +493,15 @@ bool LuaState::checkLua(const std::string& desc, int r) std::pair LuaState::expr(const std::string& desc, const std::string& text) { if (! checkLua(desc, lluau::dostring(mState, desc, text))) + { + LL_WARNS("Lua") << desc << " error: " << mError << LL_ENDL; return { -1, mError }; + } // here we believe there was no error -- did the Lua fragment leave // anything on the stack? std::pair result{ lua_gettop(mState), {} }; + LL_INFOS("Lua") << desc << " done, " << result.first << " results." << LL_ENDL; if (result.first) { // aha, at least one entry on the stack! @@ -501,6 +514,7 @@ std::pair LuaState::expr(const std::string& desc, const std::string& } catch (const std::exception& error) { + LL_WARNS("Lua") << desc << " error converting result: " << error.what() << LL_ENDL; // lua_tollsd() is designed to be called from a lua_function(), // that is, from a C++ function called by Lua. In case of error, // it throws a Lua error to be caught by the Lua runtime. expr() @@ -519,15 +533,18 @@ std::pair LuaState::expr(const std::string& desc, const std::string& else { // multiple entries on the stack + int index; try { - for (int index = 1; index <= result.first; ++index) + for (index = 1; index <= result.first; ++index) { result.second.append(lua_tollsd(mState, index)); } } catch (const std::exception& error) { + LL_WARNS("Lua") << desc << " error converting result " << index << ": " + << error.what() << LL_ENDL; // see above comments regarding lua_State's error status initLuaState(); return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) }; @@ -577,9 +594,13 @@ std::pair LuaState::expr(const std::string& desc, const std::string& { // there's a fiber.run() function sitting on the top of the stack // -- call it with no arguments, discarding anything it returns - LL_DEBUGS("Lua") << "Calling fiber.run()" << LL_ENDL; + LL_INFOS("Lua") << desc << " p.s. fiber.run()" << LL_ENDL; if (! checkLua(desc, lua_pcall(mState, 0, 0, 0))) + { + LL_WARNS("Lua") << desc << " p.s. fiber.run() error: " << mError << LL_ENDL; return { -1, mError }; + } + LL_INFOS("Lua") << desc << " p.s. done." << LL_ENDL; } } // pop everything again @@ -684,6 +705,26 @@ std::pair LuaFunction::getState() return { registry, lookup }; } +/***************************************************************************** +* source_path() +*****************************************************************************/ +lua_function(source_path, "return the source path of the running Lua script") +{ + luaL_checkstack(L, 1, nullptr); + lua_pushstdstring(L, lluau::source_path(L)); + return 1; +} + +/***************************************************************************** +* source_dir() +*****************************************************************************/ +lua_function(source_dir, "return the source directory of the running Lua script") +{ + luaL_checkstack(L, 1, nullptr); + lua_pushstdstring(L, lluau::source_path(L).parent_path()); + return 1; +} + /***************************************************************************** * check_stop() *****************************************************************************/ -- cgit v1.2.3 From 7049485ebd0b997a097c12e094425d58db56e043 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 2 Apr 2024 13:25:08 -0400 Subject: Fix std::filesystem::path - to - std::string conversions on Windows. On Windows, std::filesystem::path::value_type is wchar_t, not char -- so path::string_type is std::wstring, not std::string. So while Posix path instances implicitly convert to string, Windows path instances do not. Add explicit u8string() calls. Also add LL.abspath() Lua entry point to further facilitate finding a resource file relative to the calling Lua script. Use abspath() for both test_luafloater_demo.lua and test_luafloater_gesture_list.lua. --- indra/llcommon/lua_function.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 96282df554..cd83f40e85 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -711,7 +711,7 @@ std::pair LuaFunction::getState() lua_function(source_path, "return the source path of the running Lua script") { luaL_checkstack(L, 1, nullptr); - lua_pushstdstring(L, lluau::source_path(L)); + lua_pushstdstring(L, lluau::source_path(L).u8string()); return 1; } @@ -721,7 +721,19 @@ lua_function(source_path, "return the source path of the running Lua script") lua_function(source_dir, "return the source directory of the running Lua script") { luaL_checkstack(L, 1, nullptr); - lua_pushstdstring(L, lluau::source_path(L).parent_path()); + lua_pushstdstring(L, lluau::source_path(L).parent_path().u8string()); + return 1; +} + +/***************************************************************************** +* abspath() +*****************************************************************************/ +lua_function(abspath, + "for given filesystem path relative to running script, return absolute path") +{ + auto path{ lua_tostdstring(L, 1) }; + lua_pop(L, 1); + lua_pushstdstring(L, (lluau::source_path(L).parent_path() / path).u8string()); return 1; } -- cgit v1.2.3 From e399b02e3306a249cb161f07cac578d3f2617bab Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 3 Apr 2024 12:31:43 -0400 Subject: Introduce fsyspath subclass of std::filesystem::path. Our std::strings are UTF-8 encoded, so conversion from std::string to std::filesystem::path must use UTF-8 decoding. The native Windows std::filesystem::path constructor and assignment operator accepting std::string use "native narrow encoding," which mangles path strings containing UTF-8 encoded non-ASCII characters. fsyspath's std::string constructor and assignment operator explicitly engage std::filesystem::u8path() to handle encoding. u8path() is deprecated in C++20, but once we adapt fsyspath's conversion to C++20 conventions, consuming code need not be modified. --- indra/llcommon/lua_function.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index cd83f40e85..7a5668f384 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -17,13 +17,13 @@ // std headers #include #include -#include #include // std::quoted #include #include // std::unique_ptr #include // external library headers // other Linden headers +#include "fsyspath.h" #include "hexdump.h" #include "lleventcoro.h" #include "llsd.h" @@ -68,7 +68,7 @@ int lluau::loadstring(lua_State *L, const std::string &desc, const std::string & return luau_load(L, desc.data(), bytecode.get(), bytecodeSize, 0); } -std::filesystem::path lluau::source_path(lua_State* L) +fsyspath lluau::source_path(lua_State* L) { //Luau lua_Debug and lua_getinfo() are different compared to default Lua: //see https://github.com/luau-lang/luau/blob/80928acb92d1e4b6db16bada6d21b1fb6fa66265/VM/include/lua.h @@ -577,7 +577,7 @@ std::pair LuaState::expr(const std::string& desc, const std::string& // the next call to lua_next." // https://www.lua.org/manual/5.1/manual.html#lua_next if (lua_type(mState, -2) == LUA_TSTRING && - std::filesystem::path(lua_tostdstring(mState, -2)).stem() == "fiber") + fsyspath(lua_tostdstring(mState, -2)).stem() == "fiber") { found = true; break; -- cgit v1.2.3 From 81d7fae644b759dd7b5620c7469b2941743dced8 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 3 Apr 2024 15:52:46 -0400 Subject: Introduce LLInstanceTracker::destroy() methods; use in ~LuaState(). --- indra/llcommon/lua_function.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 7a5668f384..c86faf1ae2 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -286,7 +286,7 @@ LLSD lua_tollsd(lua_State* L, int index) popper.disarm(); // Table keys are all integers: are they reasonable integers? // Arbitrary max: may bite us, but more likely to protect us - size_t array_max{ 10000 }; + const size_t array_max{ 10000 }; if (keys.size() > array_max) { return lluau::error(L, "Conversion from Lua to LLSD array limited to %d entries", @@ -459,14 +459,7 @@ LuaState::~LuaState() { // Did somebody call obtainListener() on this LuaState? // That is, is there a LuaListener key in its registry? - auto listener{ getListener() }; - if (listener) - { - // if we got a LuaListener instance, destroy it - auto lptr{ listener.get() }; - listener.reset(); - delete lptr; - } + LuaListener::destroy(getListener()); lua_close(mState); -- cgit v1.2.3 From e02ea3ddee87021a4b6fa0de874e2d6d71da65f9 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 3 Apr 2024 16:32:21 -0400 Subject: LLInstanceTracker::destruct() instead of destroy(). Avoid ambiguity with LLFloater::destroy(). --- indra/llcommon/lua_function.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index c86faf1ae2..332a08a444 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -459,7 +459,7 @@ LuaState::~LuaState() { // Did somebody call obtainListener() on this LuaState? // That is, is there a LuaListener key in its registry? - LuaListener::destroy(getListener()); + LuaListener::destruct(getListener()); lua_close(mState); -- cgit v1.2.3 From fa821576c010eca2cacb0fab25dd240de4062b31 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 18 Apr 2024 16:34:32 -0400 Subject: Move {set,check}_interrupts_counter() to lluau namespace. Use in LuaState::expr() so we can catch a runaway in-memory Lua chunk as well as a script read from a file. --- indra/llcommon/lua_function.cpp | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 332a08a444..08bc65e0c5 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -31,6 +31,9 @@ #include "lualistener.h" #include "stringize.h" +const S32 INTERRUPTS_MAX_LIMIT = 20000; +const S32 INTERRUPTS_SUSPEND_LIMIT = 100; + #define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n))) #define lua_rawlen lua_objlen @@ -77,6 +80,35 @@ fsyspath lluau::source_path(lua_State* L) return ar.source; } +void lluau::set_interrupts_counter(lua_State *L, S32 counter) +{ + luaL_checkstack(L, 2, nullptr); + lua_pushstring(L, "_INTERRUPTS"); + lua_pushinteger(L, counter); + lua_rawset(L, LUA_REGISTRYINDEX); +} + +void lluau::check_interrupts_counter(lua_State* L) +{ + luaL_checkstack(L, 1, nullptr); + lua_pushstring(L, "_INTERRUPTS"); + lua_rawget(L, LUA_REGISTRYINDEX); + S32 counter = lua_tointeger(L, -1); + lua_pop(L, 1); + + lluau::set_interrupts_counter(L, ++counter); + if (counter > INTERRUPTS_MAX_LIMIT) + { + lluau::error(L, "Possible infinite loop, terminated."); + } + else if (counter % INTERRUPTS_SUSPEND_LIMIT == 0) + { + LL_DEBUGS("Lua") << LLCoros::getName() << " suspending at " << counter << " interrupts" + << LL_ENDL; + llcoro::suspend(); + } +} + /***************************************************************************** * Lua <=> C++ conversions *****************************************************************************/ @@ -485,6 +517,18 @@ bool LuaState::checkLua(const std::string& desc, int r) std::pair LuaState::expr(const std::string& desc, const std::string& text) { + lluau::set_interrupts_counter(mState, 0); + + lua_callbacks(mState)->interrupt = [](lua_State *L, int gc) + { + // skip if we're interrupting only for garbage collection + if (gc >= 0) + return; + + LLCoros::checkStop(); + lluau::check_interrupts_counter(L); + }; + if (! checkLua(desc, lluau::dostring(mState, desc, text))) { LL_WARNS("Lua") << desc << " error: " << mError << LL_ENDL; -- cgit v1.2.3 From 128514da9e1b24e8d817ec90b53dea9506f31101 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 12 Jun 2024 16:51:33 -0400 Subject: LuaState::expr() has log messages for ending, add for starting. It's helpful to see when expr() is actually going to start running a particular Lua chunk. We already report not only when it's done, but also if/when we start and finish a p.s. fiber.run() call. --- indra/llcommon/lua_function.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 08bc65e0c5..7c80f65ff9 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -529,6 +529,7 @@ std::pair LuaState::expr(const std::string& desc, const std::string& lluau::check_interrupts_counter(L); }; + LL_INFOS("Lua") << desc << " run" << LL_ENDL; if (! checkLua(desc, lluau::dostring(mState, desc, text))) { LL_WARNS("Lua") << desc << " error: " << mError << LL_ENDL; -- cgit v1.2.3 From ab9cb6fcd96c1c29650d844b5fd76e2ebbf5f2df Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 14 Jun 2024 20:43:09 -0400 Subject: Introduce LL.atexit(), internal lua_emplace(), lua_toclass(). 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(ctor args...) pushes a Lua userdata object containing a newly-constructed T instance -- actually a std::optional to avoid double destruction. lua_emplace() 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() also uses LL.atexit() to ensure that ~T() will run when the LuaState is destroyed. The companion to lua_emplace() is lua_toclass(), which returns a non-nullptr T* if the referenced index is in fact a userdata created by lua_emplace() 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 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') 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 *****************************************************************************/ -- cgit v1.2.3 From 5b6a5c757deaba3c2b361eb49f2e61630fe3eb47 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 17 Jun 2024 11:18:09 -0400 Subject: Store script's LuaListener in userdata in lua_State's Registry. Instead of deriving LuaListener from LLInstanceTracker with an int key, generating a unique int key and storing that key in the Registry, use new lua_emplace() to store the LuaListener directly in a Lua userdata object in the Lua Registry. Because lua_emplace() uses LL.atexit() to guarantee that ~LuaState will destroy the T object, we no longer need ~LuaState() to make a special call specifically to destroy the LuaListener, if any. So we no longer need LuaState::getListener() separate from obtainListener(). Since LuaListener is no longer an LLInstanceTracker subclass, make LuaState::obtainListener() return LuaListener& rather than LuaListener::ptr_t. --- indra/llcommon/lua_function.cpp | 61 +++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 39 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index edd49feed9..cd1a0cd562 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -520,10 +520,6 @@ LuaState::~LuaState() // 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()); - lua_close(mState); if (mCallback) @@ -677,43 +673,30 @@ std::pair LuaState::expr(const std::string& desc, const std::string& return result; } -LuaListener::ptr_t LuaState::getListener(lua_State* L) +LuaListener& LuaState::obtainListener(lua_State* L) { - // have to use one more stack slot - luaL_checkstack(L, 1, nullptr); - LuaListener::ptr_t listener; - // Does this lua_State already have a LuaListener stored in the registry? - auto keytype{ lua_getfield(L, LUA_REGISTRYINDEX, "event.listener") }; - llassert(keytype == LUA_TNIL || keytype == LUA_TNUMBER); - if (keytype == LUA_TNUMBER) + luaL_checkstack(L, 2, nullptr); + lua_getfield(L, LUA_REGISTRYINDEX, "LuaListener"); + // compare lua_type() because lua_isuserdata() also accepts light userdata + if (lua_type(L, -1) != LUA_TUSERDATA) { - // We do already have a LuaListener. Retrieve it. - int isint; - listener = LuaListener::getInstance(lua_tointegerx(L, -1, &isint)); - // Nobody should have destroyed this LuaListener instance! - llassert(isint && listener); + llassert(lua_type(L, -1) == LUA_TNIL); + lua_pop(L, 1); + // push a userdata containing new LuaListener, binding L + lua_emplace(L, L); + // duplicate the top stack entry so we can store one copy + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, "LuaListener"); } - // pop the int "event.listener" key + // At this point, one way or the other, the stack top should be (a Lua + // userdata containing) our LuaListener. + LuaListener* listener{ lua_toclass(L, -1) }; + // userdata objects created by lua_emplace() are bound on the atexit() + // queue, and are thus never garbage collected: they're destroyed only + // when ~LuaState() walks that queue. That's why we dare pop the userdata + // value off the stack while still depending on a pointer into its data. lua_pop(L, 1); - return listener; -} - -LuaListener::ptr_t LuaState::obtainListener(lua_State* L) -{ - auto listener{ getListener(L) }; - if (! listener) - { - // have to use one more stack slot - luaL_checkstack(L, 1, nullptr); - // instantiate a new LuaListener, binding the L state -- but use a - // no-op deleter: we do NOT want this ptr_t to manage the lifespan of - // this new LuaListener! - listener.reset(new LuaListener(L), [](LuaListener*){}); - // set its key in the field where we'll look for it later - lua_pushinteger(L, listener->getKey()); - lua_setfield(L, LUA_REGISTRYINDEX, "event.listener"); - } - return listener; + return *listener; } /***************************************************************************** @@ -938,10 +921,10 @@ lua_function( lua_settop(L, 0); auto& outpump{ LLEventPumps::instance().obtain("lua output") }; - auto listener{ LuaState::obtainListener(L) }; + auto& listener{ LuaState::obtainListener(L) }; LLEventStream replyPump("leaphelp", true); // ask the LuaListener's LeapListener and suspend calling coroutine until reply - auto reply{ llcoro::postAndSuspend(request, listener->getCommandName(), replyPump, "reply") }; + auto reply{ llcoro::postAndSuspend(request, listener.getCommandName(), replyPump, "reply") }; reply.erase("reqid"); if (auto error = reply["error"]; error.isString()) -- cgit v1.2.3 From aff78224a026bbf17e6ac4818228c0e1814c4226 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 18 Jun 2024 09:11:29 -0400 Subject: Make lluau::source_path() report top-level script path. source_path() previously reported the path of the module containing the current (lowest-level) Lua function. The effect was that the Floater.lua module would always try to look up the XUI file relative to scripts/lua/require. It makes more intuitive sense to make source_path() return the path containing the top-level script, so that a script engaging the Floater.lua module looks for the XUI file relative to the script. --- indra/llcommon/lua_function.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index cd1a0cd562..3e3934c9c1 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -75,8 +75,11 @@ fsyspath lluau::source_path(lua_State* L) { //Luau lua_Debug and lua_getinfo() are different compared to default Lua: //see https://github.com/luau-lang/luau/blob/80928acb92d1e4b6db16bada6d21b1fb6fa66265/VM/include/lua.h + // In particular: + // passing level=1 gets you info about the deepest function call + // passing level=lua_stackdepth() gets you info about the topmost script lua_Debug ar; - lua_getinfo(L, 1, "s", &ar); + lua_getinfo(L, lua_stackdepth(L), "s", &ar); return ar.source; } -- cgit v1.2.3 From 5cc4b42a3001be120e22f745460dbb76d8d8d018 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 18 Jun 2024 13:11:07 -0400 Subject: Make ~LuaState() walk Registry.atexit table backwards so cleanup happens in reverse order, as is conventional. Streamline LL.atexit() function: luaL_newmetatable() performs all the find-or-create named Registry table logic. --- indra/llcommon/lua_function.cpp | 67 ++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 37 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 3e3934c9c1..7894c7b96a 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -496,17 +496,24 @@ LuaState::~LuaState() // 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"] + luaL_checkstack(mState, 2, nullptr); + // look up Registry.atexit lua_getfield(mState, LUA_REGISTRYINDEX, "atexit"); - // stack contains Registry["atexit"] + // stack contains Registry.atexit if (lua_istable(mState, -1)) { - lua_pushnil(mState); // first key - while (lua_next(mState, -2)) + // We happen to know that Registry.atexit is built by appending array + // entries using table.insert(). That's important because it means + // there are no holes, and therefore lua_objlen() should be correct. + // That's important because we walk the atexit table backwards, to + // destroy last the things we created (passed to LL.atexit()) first. + for (int i(lua_objlen(mState, -1)); i >= 1; --i) { - // stack contains Registry["atexit"], key, value - // Call value(), no args, no return values. + lua_pushinteger(mState, i); + // stack contains Registry.atexit, i + lua_gettable(mState, -2); + // stack contains Registry.atexit, atexit[i] + // Call atexit[i](), 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) @@ -516,11 +523,10 @@ LuaState::~LuaState() // 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. + // lua_pcall() has already popped atexit[i]: stack contains atexit } } - // pop Registry["atexit"] (either table or nil) + // pop Registry.atexit (either table or nil) lua_pop(mState, 1); lua_close(mState); @@ -710,37 +716,24 @@ 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" + // 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) + // stack contains function, table, table.insert + // ditch table + lua_replace(L, -2); + // stack contains function, table.insert + // find or create the "atexit" table in the Registry + luaL_newmetatable(L, "atexit"); + // stack contains function, table.insert, Registry.atexit + // we were called with a Lua function to append to that Registry.atexit + // table -- push function + lua_pushvalue(L, 1); // or -3 + // stack contains function, table.insert, Registry.atexit, function + // call table.insert(Registry.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 + // stack contains function -- pop everything lua_settop(L, 0); return 0; } -- cgit v1.2.3 From e26727e03bb02d26b3f0d83f748dbd8eb88e4940 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 18 Jun 2024 13:13:39 -0400 Subject: Remove special-case ~LuaState() code to call fiber.run(). Instead, make fiber.lua call LL.atexit(fiber.run) to schedule that final run() call at ~LuaState() time using the generic mechanism. Append an explicit fiber.run() call to a specific test in llluamanager_test.cpp because the test code wants to interact with multiple Lua fibers *before* we destroy the LuaState. --- indra/llcommon/lua_function.cpp | 52 ----------------------------------------- 1 file changed, 52 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 7894c7b96a..7c5a939d8a 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -627,58 +627,6 @@ std::pair LuaState::expr(const std::string& desc, const std::string& } // pop everything lua_settop(mState, 0); - - // If we ran a script that loaded the fiber module, finish up with a call - // to fiber.run(). That allows a script to kick off some number of fibers, - // do some work on the main thread and then fall off the end of the script - // without explicitly appending a call to fiber.run(). run() ensures the - // rest of the fibers run to completion (or error). - luaL_checkstack(mState, 4, nullptr); - // Push _MODULES table on stack - luaL_findtable(mState, LUA_REGISTRYINDEX, "_MODULES", 1); - int index = lua_gettop(mState); - bool found = false; - // Did this chunk already require('fiber')? To find out, we must search - // the _MODULES table, because our require() implementation uses the - // pathname of the module file as the key. Push nil key to start. - lua_pushnil(mState); - while (lua_next(mState, index) != 0) - { - // key is at index -2, value at index -1 - // "While traversing a table, do not call lua_tolstring directly on a - // key, unless you know that the key is actually a string. Recall that - // lua_tolstring changes the value at the given index; this confuses - // the next call to lua_next." - // https://www.lua.org/manual/5.1/manual.html#lua_next - if (lua_type(mState, -2) == LUA_TSTRING && - fsyspath(lua_tostdstring(mState, -2)).stem() == "fiber") - { - found = true; - break; - } - // pop value so key is at top for lua_next() - lua_pop(mState, 1); - } - if (found) - { - // okay, index -1 is a table loaded from a file 'fiber.xxx' -- - // does it have a function named 'run'? - auto run_type{ lua_getfield(mState, -1, "run") }; - if (run_type == LUA_TFUNCTION) - { - // there's a fiber.run() function sitting on the top of the stack - // -- call it with no arguments, discarding anything it returns - LL_INFOS("Lua") << desc << " p.s. fiber.run()" << LL_ENDL; - if (! checkLua(desc, lua_pcall(mState, 0, 0, 0))) - { - LL_WARNS("Lua") << desc << " p.s. fiber.run() error: " << mError << LL_ENDL; - return { -1, mError }; - } - LL_INFOS("Lua") << desc << " p.s. done." << LL_ENDL; - } - } - // pop everything again - lua_settop(mState, 0); return result; } -- cgit v1.2.3 From b7a70eb7ee3451dae596ddd691666eb83ac9350c Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 18 Jun 2024 18:09:16 -0400 Subject: Initialize lua_Debug lluau::source_path() passes to lua_getinfo(). On Mac it doesn't seem to matter, but on Windows, leaving it uninitialized can produce garbage results and even crash the coroutine. This seems strange, since we've been assuming lua_getinfo() treats its lua_Debug* as output-only. --- indra/llcommon/lua_function.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 3e3934c9c1..283dfa3c94 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -78,7 +78,7 @@ fsyspath lluau::source_path(lua_State* L) // In particular: // passing level=1 gets you info about the deepest function call // passing level=lua_stackdepth() gets you info about the topmost script - lua_Debug ar; + lua_Debug ar{}; lua_getinfo(L, lua_stackdepth(L), "s", &ar); return ar.source; } -- cgit v1.2.3 From 2739154eaa04877b0d19d1dfc56fc1679aa6bb98 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 19 Jun 2024 08:56:51 -0400 Subject: Try harder to keep Luau's lua_getinfo() from crashing. --- indra/llcommon/lua_function.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 7e99480201..e76bd55dbb 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -78,8 +78,13 @@ fsyspath lluau::source_path(lua_State* L) // In particular: // passing level=1 gets you info about the deepest function call // passing level=lua_stackdepth() gets you info about the topmost script + // Empirically, lua_getinfo(level > 1) behaves strangely (including + // crashing the program) unless you iterate from 1 to desired level. lua_Debug ar{}; - lua_getinfo(L, lua_stackdepth(L), "s", &ar); + for (int i(0), depth(lua_stackdepth(L)); i <= depth; ++i) + { + lua_getinfo(L, i, "s", &ar); + } return ar.source; } -- cgit v1.2.3 From ef596c44fce4a1059b8d79aff1c73db5ce628169 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 19 Jun 2024 09:43:27 -0400 Subject: Improve LL.help() function. The help string for each lua_function() must restate the function name and its arguments. The help string is all that's shown; unless it restates the function name, LL.help() output lists terse explanations for functions whose names are not shown. Make help() prepend "LL." to help output, because these functions must be accessed via the "builtin" LL table instead of directly populating the global Lua namespace. Similarly, before string name lookup, remove "LL." prefix if specified. --- indra/llcommon/lua_function.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index e76bd55dbb..255385b8c4 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -28,6 +28,7 @@ #include "lleventcoro.h" #include "llsd.h" #include "llsdutil.h" +#include "llstring.h" #include "lualistener.h" #include "stringize.h" @@ -716,7 +717,8 @@ LuaListener& LuaState::obtainListener(lua_State* L) /***************************************************************************** * atexit() *****************************************************************************/ -lua_function(atexit, "register a Lua function to be called at script termination") +lua_function(atexit, "atexit(function): " + "register Lua function to be called at script termination") { luaL_checkstack(L, 4, nullptr); // look up the global name "table" @@ -804,7 +806,7 @@ std::pair LuaFunction::getState() /***************************************************************************** * source_path() *****************************************************************************/ -lua_function(source_path, "return the source path of the running Lua script") +lua_function(source_path, "source_path(): return the source path of the running Lua script") { luaL_checkstack(L, 1, nullptr); lua_pushstdstring(L, lluau::source_path(L).u8string()); @@ -814,7 +816,7 @@ lua_function(source_path, "return the source path of the running Lua script") /***************************************************************************** * source_dir() *****************************************************************************/ -lua_function(source_dir, "return the source directory of the running Lua script") +lua_function(source_dir, "source_dir(): return the source directory of the running Lua script") { luaL_checkstack(L, 1, nullptr); lua_pushstdstring(L, lluau::source_path(L).parent_path().u8string()); @@ -824,7 +826,7 @@ lua_function(source_dir, "return the source directory of the running Lua script" /***************************************************************************** * abspath() *****************************************************************************/ -lua_function(abspath, +lua_function(abspath, "abspath(path): " "for given filesystem path relative to running script, return absolute path") { auto path{ lua_tostdstring(L, 1) }; @@ -836,7 +838,7 @@ lua_function(abspath, /***************************************************************************** * check_stop() *****************************************************************************/ -lua_function(check_stop, "ensure that a Lua script responds to viewer shutdown") +lua_function(check_stop, "check_stop(): ensure that a Lua script responds to viewer shutdown") { LLCoros::checkStop(); return 0; @@ -857,7 +859,7 @@ lua_function(help, for (const auto& [name, pair] : registry) { const auto& [fptr, helptext] = pair; - luapump.post(helptext); + luapump.post("LL." + helptext); } } else @@ -869,6 +871,7 @@ lua_function(help, if (lua_type(L, idx) == LUA_TSTRING) { arg = lua_tostdstring(L, idx); + LLStringUtil::removePrefix(arg, "LL."); } else if (lua_type(L, idx) == LUA_TFUNCTION) { @@ -887,7 +890,7 @@ lua_function(help, if (auto found = registry.find(arg); found != registry.end()) { - luapump.post(found->second.second); + luapump.post("LL." + found->second.second); } else { -- cgit v1.2.3 From 0cc7436be1f57299384c5acad5d32e13f2f4d1cf Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 27 Jun 2024 19:47:00 -0400 Subject: Introduce TypeTag template whose int value differs for each T. This replaces type_tag(), which searched and possibly extended the type_tags unordered_map at runtime. If we called lua_emplace() from different threads, that would require locking type_tags. In contrast, the compiler must instantiate a distinct TypeTag for every distinct T passed to lua_emplace(), so each gets a distinct value at static initialization time. No locking is required; no lookup; no allocations. Add a test to llluamanager_test.cpp to verify that each distinct T passed to lua_emplace() gets its own TypeTag::value, and that each gets its own destructor -- but that different lua_emplace() calls with the same T share the same TypeTag::value and the same destructor. --- indra/llcommon/lua_function.cpp | 2 ++ 1 file changed, 2 insertions(+) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 255385b8c4..2d08de68c5 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -38,6 +38,8 @@ const S32 INTERRUPTS_SUSPEND_LIMIT = 100; #define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n))) #define lua_rawlen lua_objlen +int DistinctInt::mValues{0}; + /***************************************************************************** * luau namespace *****************************************************************************/ -- cgit v1.2.3 From 8c94ff566a4f9076607d1b988f3eb7ad7e200bd9 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 10 Jul 2024 15:14:13 -0400 Subject: Remove ability to reuse a LuaState between LLLUAmanager functions. Remove LLLUAmanager::mumbleScriptLine() LuaState& parameters. Make startScriptLine(), waitScriptLine() and runScriptLine() exactly parallel to startScriptFile(), waitScriptFile() and runScriptFile(). That means that runScriptLine()'s C++ coroutine instantiates and destroys its own LuaState, which means that LL.atexit() functions will run on the Lua-specific C++ coroutine rather than (say) the viewer's main coroutine. Introduce LLLUAmanager::script_result typedef for std::pair and use in method returns. Remove LuaState::initLuaState(); move its logic back into the constructor. Remove initLuaState() calls in the expr() error cases: they're moot now that we won't get subsequent expr() calls on the same LuaState instance. Remove LLFloaterLUADebug "Use clean lua_State" checkbox and the cleanLuaState() method. Remove mState member. Remove explicit LuaState declarations from LLLUAmanager tests. Adapt one test for implicit LuaState: it was directly calling LuaState::obtainListener() to discover the LuaListener's reply-pump name. But since that test also captures two leap.request() calls from the Lua script, it can just look at the "reply" key in either of those requests. --- indra/llcommon/lua_function.cpp | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 2d08de68c5..4acb09d564 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -478,19 +478,11 @@ void lua_pushllsd(lua_State* L, const LLSD& data) *****************************************************************************/ LuaState::LuaState(script_finished_fn cb): mCallback(cb), - mState(nullptr) + mState(luaL_newstate()) { - initLuaState(); -} - -void LuaState::initLuaState() -{ - if (mState) - { - lua_close(mState); - } - mState = luaL_newstate(); luaL_openlibs(mState); + // publish to this new lua_State all the LL entry points we defined using + // the lua_function() macro LuaFunction::init(mState); // Try to make print() write to our log. lua_register(mState, "print", LuaFunction::get("print_info")); @@ -607,8 +599,7 @@ std::pair LuaState::expr(const std::string& desc, const std::string& // we instead of the Lua runtime catch it, our lua_State retains // its internal error status. Any subsequent lua_pcall() calls // with this lua_State will report error regardless of whether the - // chunk runs successfully. Get a new lua_State(). - initLuaState(); + // chunk runs successfully. return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) }; } } @@ -628,7 +619,6 @@ std::pair LuaState::expr(const std::string& desc, const std::string& LL_WARNS("Lua") << desc << " error converting result " << index << ": " << error.what() << LL_ENDL; // see above comments regarding lua_State's error status - initLuaState(); return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) }; } } -- cgit v1.2.3 From 3214c7bd7e77fdd458d64ec101a8a67287b59ffa Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 1 Aug 2024 16:09:11 -0400 Subject: Add lua_push(), lua_to(), lua_[gs]etfieldv(), lua_raw[gs]etfield(). Leverage C++ overloads to allow use of generic function names disambiguated by argument type. This allows using templates for certain common operation sequences. --- indra/llcommon/lua_function.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 42bba80ed5..b173d17ede 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -32,6 +32,8 @@ #include "lualistener.h" #include "stringize.h" +using namespace std::literals; // e.g. std::string_view literals: "this"sv + const S32 INTERRUPTS_MAX_LIMIT = 20000; const S32 INTERRUPTS_SUSPEND_LIMIT = 100; @@ -41,7 +43,7 @@ const S32 INTERRUPTS_SUSPEND_LIMIT = 100; int DistinctInt::mValues{0}; /***************************************************************************** -* luau namespace +* lluau namespace *****************************************************************************/ namespace { @@ -93,21 +95,14 @@ fsyspath lluau::source_path(lua_State* L) void lluau::set_interrupts_counter(lua_State *L, S32 counter) { - luaL_checkstack(L, 2, nullptr); - lua_pushstring(L, "_INTERRUPTS"); - lua_pushinteger(L, counter); - lua_rawset(L, LUA_REGISTRYINDEX); + lua_rawsetfield(L, LUA_REGISTRYINDEX, "_INTERRUPTS"sv, lua_Integer(counter)); } void lluau::check_interrupts_counter(lua_State* L) { - luaL_checkstack(L, 1, nullptr); - lua_pushstring(L, "_INTERRUPTS"); - lua_rawget(L, LUA_REGISTRYINDEX); - S32 counter = lua_tointeger(L, -1); - lua_pop(L, 1); + auto counter = lua_rawgetfield(L, LUA_REGISTRYINDEX, "_INTERRUPTS"sv); - lluau::set_interrupts_counter(L, ++counter); + set_interrupts_counter(L, ++counter); if (counter > INTERRUPTS_MAX_LIMIT) { lluau::error(L, "Possible infinite loop, terminated."); -- cgit v1.2.3 From b3fb23ee0c6d33f5eba3502328ffb0011b5f25fb Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 15 Aug 2024 09:49:34 -0400 Subject: Introduce lluau_checkstack(L, n); use instead of luaL_checkstack(). luaL_checkstack() accepts a third parameter which is included in the stack overflow error message. We've been passing nullptr, leading to messages of the form "stack overflow ((null))". lluau_checkstack() implicitly passes __FUNCTION__, so we can distinguish which underlying luaL_checkstack() call encountered the stack overflow condition. Also, when calling each atexit() function, pass Luau's debug.traceback() function as the lua_pcall() error handler. This should help diagnose errors in atexit() functions. --- indra/llcommon/lua_function.cpp | 57 ++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 21 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index b173d17ede..defadea358 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -127,7 +127,7 @@ std::string lua_tostdstring(lua_State* L, int index) void lua_pushstdstring(lua_State* L, const std::string& str) { - luaL_checkstack(L, 1, nullptr); + lluau_checkstack(L, 1); lua_pushlstring(L, str.c_str(), str.length()); } @@ -240,10 +240,10 @@ LLSD lua_tollsd(lua_State* L, int index) // undefined LLSD. Naturally, though, those won't survive a second // round trip. - // This is the most important of the luaL_checkstack() calls because a + // This is the most important of the lluau_checkstack() calls because a // deeply nested Lua structure will enter this case at each level, and // we'll need another 2 stack slots to traverse each nested table. - luaL_checkstack(L, 2, nullptr); + lluau_checkstack(L, 2); // BEFORE we push nil to initialize the lua_next() traversal, convert // 'index' to absolute! Our caller might have passed a relative index; // we do, below: lua_tollsd(L, -1). If 'index' is -1, then when we @@ -400,7 +400,7 @@ LLSD lua_tollsd(lua_State* L, int index) void lua_pushllsd(lua_State* L, const LLSD& data) { // might need 2 slots for array or map - luaL_checkstack(L, 2, nullptr); + lluau_checkstack(L, 2); switch (data.type()) { case LLSD::TypeUndefined: @@ -487,39 +487,54 @@ LuaState::LuaState(script_finished_fn cb): 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, 2, nullptr); + // We're just about to destroy this lua_State mState. Did this Lua chunk + // register any atexit() functions? + lluau_checkstack(mState, 3); // look up Registry.atexit lua_getfield(mState, LUA_REGISTRYINDEX, "atexit"); // stack contains Registry.atexit if (lua_istable(mState, -1)) { + // Push debug.traceback() onto the stack as lua_pcall()'s error + // handler function. On error, lua_pcall() calls the specified error + // handler function with the original error message; the message + // returned by the error handler is then returned by lua_pcall(). + // Luau's debug.traceback() is called with a message to prepend to the + // returned traceback string. Almost as if they'd been designed to + // work together... + lua_getglobal(mState, "debug"); + lua_getfield(mState, -1, "traceback"); + // ditch "debug" + lua_remove(mState, -2); + // stack now contains atexit, debug.traceback() + // We happen to know that Registry.atexit is built by appending array // entries using table.insert(). That's important because it means // there are no holes, and therefore lua_objlen() should be correct. // That's important because we walk the atexit table backwards, to // destroy last the things we created (passed to LL.atexit()) first. - for (int i(lua_objlen(mState, -1)); i >= 1; --i) + for (int i(lua_objlen(mState, -2)); i >= 1; --i) { lua_pushinteger(mState, i); - // stack contains Registry.atexit, i - lua_gettable(mState, -2); - // stack contains Registry.atexit, atexit[i] + // stack contains Registry.atexit, debug.traceback(), i + lua_gettable(mState, -3); + // stack contains Registry.atexit, debug.traceback(), atexit[i] // Call atexit[i](), 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) + // shouldn't cancel the rest of them. Pass debug.traceback() as + // the error handler function. + if (lua_pcall(mState, 0, 0, -2) != LUA_OK) { auto error{ lua_tostdstring(mState, -1) }; LL_WARNS("Lua") << "atexit() function error: " << error << LL_ENDL; // pop error message lua_pop(mState, 1); } - // lua_pcall() has already popped atexit[i]: stack contains atexit + // lua_pcall() has already popped atexit[i]: + // stack contains atexit, debug.traceback() } + // pop debug.traceback() + lua_pop(mState, 1); } // pop Registry.atexit (either table or nil) lua_pop(mState, 1); @@ -625,7 +640,7 @@ std::pair LuaState::expr(const std::string& desc, const std::string& LuaListener& LuaState::obtainListener(lua_State* L) { - luaL_checkstack(L, 2, nullptr); + lluau_checkstack(L, 2); lua_getfield(L, LUA_REGISTRYINDEX, "LuaListener"); // compare lua_type() because lua_isuserdata() also accepts light userdata if (lua_type(L, -1) != LUA_TUSERDATA) @@ -655,7 +670,7 @@ LuaListener& LuaState::obtainListener(lua_State* L) lua_function(atexit, "atexit(function): " "register Lua function to be called at script termination") { - luaL_checkstack(L, 4, nullptr); + lluau_checkstack(L, 4); // look up the global name "table" lua_getglobal(L, "table"); // stack contains function, table @@ -705,7 +720,7 @@ LuaFunction::LuaFunction(const std::string_view& name, lua_CFunction function, void LuaFunction::init(lua_State* L) { const auto& [registry, lookup] = getRState(); - luaL_checkstack(L, 2, nullptr); + lluau_checkstack(L, 2); // create LL table -- // it happens that we know exactly how many non-array members we want lua_createtable(L, 0, int(narrow(lookup.size()))); @@ -743,7 +758,7 @@ std::pair LuaFunction::getState() *****************************************************************************/ lua_function(source_path, "source_path(): return the source path of the running Lua script") { - luaL_checkstack(L, 1, nullptr); + lluau_checkstack(L, 1); lua_pushstdstring(L, lluau::source_path(L).u8string()); return 1; } @@ -753,7 +768,7 @@ lua_function(source_path, "source_path(): return the source path of the running *****************************************************************************/ lua_function(source_dir, "source_dir(): return the source directory of the running Lua script") { - luaL_checkstack(L, 1, nullptr); + lluau_checkstack(L, 1); lua_pushstdstring(L, lluau::source_path(L).parent_path().u8string()); return 1; } -- cgit v1.2.3 From ab0f7ff14cd80b89524ba95eb5a39e2d6df55b26 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Thu, 15 Aug 2024 23:29:54 +0300 Subject: First batch of Inventory api; raise interrupts limit --- indra/llcommon/lua_function.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index b173d17ede..59aa2a0ba7 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -34,7 +34,7 @@ using namespace std::literals; // e.g. std::string_view literals: "this"sv -const S32 INTERRUPTS_MAX_LIMIT = 20000; +const S32 INTERRUPTS_MAX_LIMIT = 100000; const S32 INTERRUPTS_SUSPEND_LIMIT = 100; #define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n))) -- cgit v1.2.3 From 376c890c095fbc59b83402255cc1036c411150b9 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 20 Aug 2024 21:12:25 -0400 Subject: Fix for #2237: intermittent Lua data stack overflow. Use a static unordered_map to allow a function receiving (lua_State* L) to look up the LuaState instance managing that lua_State. We've thought about this from time to time already. LuaState's constructor creates the map entry; its destructor removes it; the new static getParent(lua_State* L) method performs the lookup. Migrate lluau::set_interrupts_counter() and check_interrupts_counter() into LuaState member functions. Add a new mInterrupts counter for them. Importantly, LuaState::check_interrupts_counter(), which is indirectly called by a lua_callbacks().interrupt function, no longer performs any Lua stack operations. Empirically, it seems the Lua engine is capable of interrupting itself at a moment when re-entry confuses it. Change previous lluau::set_interrupts_counter(L, 0) calls to LuaState::getParent(L).set_interrupts_counter(0). Also add LuaStackDelta class, and a lua_checkdelta() helper macro, to verify that the Lua data stack depth on exit from a block differs from the depth on entry by exactly the expected amount. Sprinkle lua_checkdelta() macros in likely places. --- indra/llcommon/lua_function.cpp | 126 +++++++++++++++++++++++++++++++--------- 1 file changed, 99 insertions(+), 27 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index defadea358..ad77a1e040 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -21,6 +21,7 @@ #include #include // std::unique_ptr #include +#include // external library headers // other Linden headers #include "fsyspath.h" @@ -54,7 +55,10 @@ namespace }; } // anonymous namespace -int lluau::dostring(lua_State* L, const std::string& desc, const std::string& text) +namespace lluau +{ + +int dostring(lua_State* L, const std::string& desc, const std::string& text) { auto r = loadstring(L, desc, text); if (r != LUA_OK) @@ -66,7 +70,7 @@ int lluau::dostring(lua_State* L, const std::string& desc, const std::string& te return lua_pcall(L, 0, LUA_MULTRET, 0); } -int lluau::loadstring(lua_State *L, const std::string &desc, const std::string &text) +int loadstring(lua_State *L, const std::string &desc, const std::string &text) { size_t bytecodeSize = 0; // The char* returned by luau_compile() must be freed by calling free(). @@ -76,7 +80,7 @@ int lluau::loadstring(lua_State *L, const std::string &desc, const std::string & return luau_load(L, desc.data(), bytecode.get(), bytecodeSize, 0); } -fsyspath lluau::source_path(lua_State* L) +fsyspath source_path(lua_State* L) { //Luau lua_Debug and lua_getinfo() are different compared to default Lua: //see https://github.com/luau-lang/luau/blob/80928acb92d1e4b6db16bada6d21b1fb6fa66265/VM/include/lua.h @@ -93,33 +97,14 @@ fsyspath lluau::source_path(lua_State* L) return ar.source; } -void lluau::set_interrupts_counter(lua_State *L, S32 counter) -{ - lua_rawsetfield(L, LUA_REGISTRYINDEX, "_INTERRUPTS"sv, lua_Integer(counter)); -} - -void lluau::check_interrupts_counter(lua_State* L) -{ - auto counter = lua_rawgetfield(L, LUA_REGISTRYINDEX, "_INTERRUPTS"sv); - - set_interrupts_counter(L, ++counter); - if (counter > INTERRUPTS_MAX_LIMIT) - { - lluau::error(L, "Possible infinite loop, terminated."); - } - else if (counter % INTERRUPTS_SUSPEND_LIMIT == 0) - { - LL_DEBUGS("Lua") << LLCoros::getName() << " suspending at " << counter << " interrupts" - << LL_ENDL; - llcoro::suspend(); - } -} +} // namespace lluau /***************************************************************************** * Lua <=> C++ conversions *****************************************************************************/ std::string lua_tostdstring(lua_State* L, int index) { + lua_checkdelta(L); size_t len; const char* strval{ lua_tolstring(L, index, &len) }; return { strval, len }; @@ -127,6 +112,7 @@ std::string lua_tostdstring(lua_State* L, int index) void lua_pushstdstring(lua_State* L, const std::string& str) { + lua_checkdelta(L, 1); lluau_checkstack(L, 1); lua_pushlstring(L, str.c_str(), str.length()); } @@ -148,6 +134,7 @@ void lua_pushstdstring(lua_State* L, const std::string& str) // reached by that block raises a Lua error. LLSD lua_tollsd(lua_State* L, int index) { + lua_checkdelta(L); switch (lua_type(L, index)) { case LUA_TNONE: @@ -399,6 +386,7 @@ LLSD lua_tollsd(lua_State* L, int index) // stack a Lua object corresponding to the passed LLSD object. void lua_pushllsd(lua_State* L, const LLSD& data) { + lua_checkdelta(L, 1); // might need 2 slots for array or map lluau_checkstack(L, 2); switch (data.type()) @@ -471,10 +459,23 @@ void lua_pushllsd(lua_State* L, const LLSD& data) /***************************************************************************** * LuaState class *****************************************************************************/ +namespace +{ + +// If we find we're running Lua scripts from more than one thread, sLuaStateMap +// should be thread_local. Until then, avoid the overhead. +using LuaStateMap = std::unordered_map; +static LuaStateMap sLuaStateMap; + +} // anonymous namespace + LuaState::LuaState(script_finished_fn cb): mCallback(cb), mState(luaL_newstate()) { + // Ensure that we can always find this LuaState instance, given the + // lua_State we just created or any of its coroutines. + sLuaStateMap.emplace(mState, this); luaL_openlibs(mState); // publish to this new lua_State all the LL entry points we defined using // the lua_function() macro @@ -545,7 +546,9 @@ LuaState::~LuaState() { // mError potentially set by previous checkLua() call(s) mCallback(mError); - } + } + // with the demise of this LuaState, remove sLuaStateMap entry + sLuaStateMap.erase(mState); } bool LuaState::checkLua(const std::string& desc, int r) @@ -563,7 +566,7 @@ bool LuaState::checkLua(const std::string& desc, int r) std::pair LuaState::expr(const std::string& desc, const std::string& text) { - lluau::set_interrupts_counter(mState, 0); + set_interrupts_counter(0); lua_callbacks(mState)->interrupt = [](lua_State *L, int gc) { @@ -572,7 +575,7 @@ std::pair LuaState::expr(const std::string& desc, const std::string& return; LLCoros::checkStop(); - lluau::check_interrupts_counter(L); + LuaState::getParent(L).check_interrupts_counter(); }; LL_INFOS("Lua") << desc << " run" << LL_ENDL; @@ -664,12 +667,53 @@ LuaListener& LuaState::obtainListener(lua_State* L) return *listener; } +LuaState& LuaState::getParent(lua_State* L) +{ + // Look up the LuaState instance associated with the *script*, not the + // specific Lua *coroutine*. In other words, first find this lua_State's + // main thread. + auto found{ sLuaStateMap.find(lua_mainthread(L)) }; + // Our constructor creates the map entry, our destructor deletes it. As + // long as the LuaState exists, we should be able to find it. And we + // SHOULD only be talking to a lua_State managed by a LuaState instance. + llassert(found != sLuaStateMap.end()); + return *found->second; +} + +void LuaState::set_interrupts_counter(S32 counter) +{ + mInterrupts = counter; +} + +void LuaState::check_interrupts_counter() +{ + // The official way to manage data associated with a lua_State is to store + // it *as* Lua data within the lua_State. But this method is called by the + // Lua engine via lua_callbacks(L)->interrupt, and empirically we've hit + // mysterious Lua data stack overflows trying to use stack-based Lua data + // access functions in that situation. It seems the Lua engine is capable + // of interrupting itself at a moment when re-entry is not valid. So only + // touch data in this LuaState. + ++mInterrupts; + if (mInterrupts > INTERRUPTS_MAX_LIMIT) + { + lluau::error(mState, "Possible infinite loop, terminated."); + } + else if (mInterrupts % INTERRUPTS_SUSPEND_LIMIT == 0) + { + LL_DEBUGS("Lua") << LLCoros::getName() << " suspending at " << mInterrupts + << " interrupts" << LL_ENDL; + llcoro::suspend(); + } +} + /***************************************************************************** * atexit() *****************************************************************************/ lua_function(atexit, "atexit(function): " "register Lua function to be called at script termination") { + lua_checkdelta(L, -1); lluau_checkstack(L, 4); // look up the global name "table" lua_getglobal(L, "table"); @@ -758,6 +802,7 @@ std::pair LuaFunction::getState() *****************************************************************************/ lua_function(source_path, "source_path(): return the source path of the running Lua script") { + lua_checkdelta(L, 1); lluau_checkstack(L, 1); lua_pushstdstring(L, lluau::source_path(L).u8string()); return 1; @@ -768,6 +813,7 @@ lua_function(source_path, "source_path(): return the source path of the running *****************************************************************************/ lua_function(source_dir, "source_dir(): return the source directory of the running Lua script") { + lua_checkdelta(L, 1); lluau_checkstack(L, 1); lua_pushstdstring(L, lluau::source_path(L).parent_path().u8string()); return 1; @@ -779,6 +825,7 @@ lua_function(source_dir, "source_dir(): return the source directory of the runni lua_function(abspath, "abspath(path): " "for given filesystem path relative to running script, return absolute path") { + lua_checkdelta(L); auto path{ lua_tostdstring(L, 1) }; lua_pop(L, 1); lua_pushstdstring(L, (lluau::source_path(L).parent_path() / path).u8string()); @@ -790,6 +837,7 @@ lua_function(abspath, "abspath(path): " *****************************************************************************/ lua_function(check_stop, "check_stop(): ensure that a Lua script responds to viewer shutdown") { + lua_checkdelta(L); LLCoros::checkStop(); return 0; } @@ -994,3 +1042,27 @@ std::ostream& operator<<(std::ostream& out, const lua_stack& self) out << ']'; return out; } + +/***************************************************************************** +* LuaStackDelta +*****************************************************************************/ +LuaStackDelta::LuaStackDelta(lua_State* L, const std::string& where, int delta): + L(L), + mWhere(where), + mDepth(lua_gettop(L)), + mDelta(delta) +{} + +LuaStackDelta::~LuaStackDelta() +{ + auto depth{ lua_gettop(L) }; + if (mDepth + mDelta != depth) + { + LL_ERRS("Lua") << mWhere << ": Lua stack went from " << mDepth << " to " << depth; + if (mDelta) + { + LL_CONT << ", rather than expected " << (mDepth + mDelta) << " (" << mDelta << ")"; + } + LL_ENDL; + } +} -- cgit v1.2.3 From f4b650b100c120ed99208545864b0a7f36ce058d Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 21 Aug 2024 09:12:52 -0400 Subject: Suppress ~LuaStackDelta() verification during stack unwinding. Otherwise, an exception raised in the block containing a LuaStackDelta instance -- that might be caught -- would result in an LL_ERRS() crash. We can't expect a block exited via exception to keep its contract wrt the Lua data stack. --- indra/llcommon/lua_function.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index ad77a1e040..1e9bbdd651 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -1056,7 +1056,10 @@ LuaStackDelta::LuaStackDelta(lua_State* L, const std::string& where, int delta): LuaStackDelta::~LuaStackDelta() { auto depth{ lua_gettop(L) }; - if (mDepth + mDelta != depth) + // If we're unwinding the stack due to an exception, then of course we + // can't expect the logic in the block containing this LuaStackDelta + // instance to keep its contract wrt the Lua data stack. + if (std::uncaught_exceptions() == 0 && mDepth + mDelta != depth) { LL_ERRS("Lua") << mWhere << ": Lua stack went from " << mDepth << " to " << depth; if (mDelta) -- cgit v1.2.3 From f2abd050bce3c4132f12d962fc6436d1f06666bd Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 21 Aug 2024 09:29:26 -0400 Subject: Improve diagnostic output for Lua atexit() functions. --- indra/llcommon/lua_function.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 1e9bbdd651..452bc24b16 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -496,6 +496,14 @@ LuaState::~LuaState() // stack contains Registry.atexit if (lua_istable(mState, -1)) { + // We happen to know that Registry.atexit is built by appending array + // entries using table.insert(). That's important because it means + // there are no holes, and therefore lua_objlen() should be correct. + // That's important because we walk the atexit table backwards, to + // destroy last the things we created (passed to LL.atexit()) first. + int len(lua_objlen(mState, -1)); + LL_DEBUGS("Lua") << "Registry.atexit is a table with " << len << " entries" << LL_ENDL; + // Push debug.traceback() onto the stack as lua_pcall()'s error // handler function. On error, lua_pcall() calls the specified error // handler function with the original error message; the message @@ -509,12 +517,7 @@ LuaState::~LuaState() lua_remove(mState, -2); // stack now contains atexit, debug.traceback() - // We happen to know that Registry.atexit is built by appending array - // entries using table.insert(). That's important because it means - // there are no holes, and therefore lua_objlen() should be correct. - // That's important because we walk the atexit table backwards, to - // destroy last the things we created (passed to LL.atexit()) first. - for (int i(lua_objlen(mState, -2)); i >= 1; --i) + for (int i(len); i >= 1; --i) { lua_pushinteger(mState, i); // stack contains Registry.atexit, debug.traceback(), i @@ -524,13 +527,15 @@ LuaState::~LuaState() // Use lua_pcall() because errors in any one atexit() function // shouldn't cancel the rest of them. Pass debug.traceback() as // the error handler function. + LL_DEBUGS("Lua") << "Calling atexit(" << i << ")" << LL_ENDL; if (lua_pcall(mState, 0, 0, -2) != LUA_OK) { auto error{ lua_tostdstring(mState, -1) }; - LL_WARNS("Lua") << "atexit() function error: " << error << LL_ENDL; + LL_WARNS("Lua") << "atexit(" << i << ") error: " << error << LL_ENDL; // pop error message lua_pop(mState, 1); } + LL_DEBUGS("Lua") << "atexit(" << i << ") done" << LL_ENDL; // lua_pcall() has already popped atexit[i]: // stack contains atexit, debug.traceback() } -- cgit v1.2.3 From 14c8fc3768d978205bf17ffc1905c2772afbd434 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 28 Aug 2024 16:47:38 -0400 Subject: Add `LL.setdtor()` function to add a "destructor" to any Lua object. `setdtor('description', object, function)` returns a proxy userdata object referencing object and function. When the proxy is garbage-collected, or at the end of the script, its destructor calls `function(object)`. The original object may be retrieved as `proxy._target`, e.g. to pass it to the `table` library. The proxy also has a metatable with metamethods supporting arithmetic operations, string concatenation, length and table indexing. For other operations, retrieve `proxy._target`. (But don't assign to `proxy._target`. It will appear to work, in that subsequent references to `proxy._target` will retrieve the replacement object -- however, the destructor will still call `function(original object)`.) Fix bugs in `lua_setfieldv()`, `lua_rawgetfield()` and `lua_rawsetfield()`. Add C++ functions `lua_destroyuserdata()` to explicitly destroy a `lua_emplace()` userdata object, plus `lua_destroybounduserdata()`. The latter can bind such a userdata object as an upvalue to pass to `LL.atexit()`. Make `LL.help()` and `LL.leaphelp()` help text include the `LL.` prefix. --- indra/llcommon/lua_function.cpp | 325 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 317 insertions(+), 8 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 880bc209f6..12cff89fbd 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -99,6 +99,34 @@ fsyspath source_path(lua_State* L) } // namespace lluau +/***************************************************************************** +* lua_destroyuserdata(), lua_destroybounduserdata() (see lua_emplace()) +*****************************************************************************/ +int lua_destroyuserdata(lua_State* L) +{ + // stack: lua_emplace() userdata to be destroyed + if (int tag; + lua_isuserdata(L, -1) && + (tag = lua_userdatatag(L, -1)) != 0) + { + auto dtor = lua_getuserdatadtor(L, tag); + // detach this userdata from the destructor with tag 'tag' + lua_setuserdatatag(L, -1, 0); + // now run the real destructor + dtor(L, lua_touserdata(L, -1)); + } + lua_settop(L, 0); + return 0; +} + +int lua_destroybounduserdata(lua_State *L) +{ + // called with no arguments -- push bound upvalue + lluau_checkstack(L, 1); + lua_pushvalue(L, lua_upvalueindex(1)); + return lua_destroyuserdata(L); +} + /***************************************************************************** * Lua <=> C++ conversions *****************************************************************************/ @@ -664,10 +692,10 @@ LuaListener& LuaState::obtainListener(lua_State* L) // At this point, one way or the other, the stack top should be (a Lua // userdata containing) our LuaListener. LuaListener* listener{ lua_toclass(L, -1) }; - // userdata objects created by lua_emplace() are bound on the atexit() - // queue, and are thus never garbage collected: they're destroyed only - // when ~LuaState() walks that queue. That's why we dare pop the userdata - // value off the stack while still depending on a pointer into its data. + // Since our LuaListener instance is stored in the Registry, it won't be + // garbage collected: it will be destroyed only when lua_close() clears + // out the Registry. That's why we dare pop the userdata value off the + // stack while still depending on a pointer into its data. lua_pop(L, 1); return *listener; } @@ -851,8 +879,8 @@ lua_function(check_stop, "check_stop(): ensure that a Lua script responds to vie * help() *****************************************************************************/ lua_function(help, - "help(): list viewer's Lua functions\n" - "help(function): show help string for specific function") + "LL.help(): list viewer's Lua functions\n" + "LL.help(function): show help string for specific function") { auto& luapump{ LLEventPumps::instance().obtain("lua output") }; const auto& [registry, lookup]{ LuaFunction::getRState() }; @@ -911,8 +939,8 @@ lua_function(help, *****************************************************************************/ lua_function( leaphelp, - "leaphelp(): list viewer's LEAP APIs\n" - "leaphelp(api): show help for specific api string name") + "LL.leaphelp(): list viewer's LEAP APIs\n" + "LL.leaphelp(api): show help for specific api string name") { LLSD request; int top{ lua_gettop(L) }; @@ -974,6 +1002,287 @@ lua_function( return 0; // void return } +/***************************************************************************** +* setdtor +*****************************************************************************/ +namespace { + +// proxy userdata object returned by setdtor() +struct setdtor_refs +{ + lua_State* L; + std::string desc; + // You can't directly store a Lua object in a C++ object, but you can + // create a Lua "reference" by storing the object in the Lua Registry and + // capturing its Registry index. + int objref; + int dtorref; + + setdtor_refs(lua_State* L, const std::string& desc, int objref, int dtorref): + L(L), + desc(desc), + objref(objref), + dtorref(dtorref) + {} + setdtor_refs(const setdtor_refs&) = delete; + setdtor_refs& operator=(const setdtor_refs&) = delete; + ~setdtor_refs(); + + static void push_metatable(lua_State* L); + static std::string binop(const std::string& name, const std::string& op); + static int meta__index(lua_State* L); +}; + +} // anonymous namespace + +lua_function( + setdtor, + "setdtor(desc, obj, dtorfunc) => proxy object referencing obj and dtorfunc.\n" + "When the returned proxy object is garbage-collected, or when the script\n" + "ends, call dtorfunc(obj). String desc is logged in the error message, if any.\n" + "Use the returned proxy object (or proxy._target) like obj.\n" + "obj won't be destroyed as long as the proxy exists; it's the proxy object's\n" + "lifespan that determines when dtorfunc(obj) will be called.") +{ + if (lua_gettop(L) != 3) + { + return lluau::error(L, "setdtor(desc, obj, dtor) requires exactly 3 arguments"); + } + // called with (desc, obj, dtor), returns proxy object + lua_checkdelta(L, -2); + lluau_checkstack(L, 3); // might get up to 6 stack entries + auto desc{ lua_tostdstring(L, 1) }; + // Get Lua "references" for each of the object and the dtor function. + int objref = lua_ref(L, 2); + int dtorref = lua_ref(L, 3); + // Having captured each of our parameters, discard them. + lua_settop(L, 0); + // Push our setdtor_refs userdata. Not only do we want to push it on L's + // stack, but setdtor_refs's constructor itself requires L. + lua_emplace(L, L, desc, objref, dtorref); + // stack: proxy (i.e. setdtor_refs userdata) + // have to set its metatable + lua_getfield(L, LUA_REGISTRYINDEX, "setdtor_meta"); + // stack: proxy, setdtor_meta (which might be nil) + if (lua_isnil(L, -1)) + { + // discard nil + lua_pop(L, 1); + // compile and push our forwarding metatable + setdtor_refs::push_metatable(L); + // stack: proxy, metatable + // duplicate metatable to save it + lua_pushvalue(L, -1); + // stack: proxy, metatable, metable + // save metatable for future calls + lua_setfield(L, LUA_REGISTRYINDEX, "setdtor_meta"); + // stack: proxy, metatable + } + // stack: proxy, metatable + lua_setmetatable(L, -2); + // stack: proxy + // Because ~setdtor_refs() necessarily uses the Lua stack, the Registry et + // al., we can't let a setdtor_refs instance be destroyed by lua_close(): + // the Lua environment will already be partially shut down. To destroy + // this new setdtor_refs instance BEFORE lua_close(), bind it with + // lua_destroybounduserdata() and register it with LL.atexit(). + // push (the entry point for) LL.atexit() + lua_pushcfunction(L, atexit_luasub::call, "LL.atexit()"); + // stack: proxy, atexit() + lua_pushvalue(L, -2); + // stack: proxy, atexit(), proxy + int tag = lua_userdatatag(L, -1); + // We don't have a lookup table to get from an int Lua userdata tag to the + // corresponding C++ typeinfo name string. We'll introduce one if we need + // it for debugging. But for this particular call, we happen to know it's + // always a setdtor_refs object. + lua_pushcclosure(L, lua_destroybounduserdata, + stringize("lua_destroybounduserdata<", tag, ">()").c_str(), + 1); + // stack: proxy, atexit(), lua_destroybounduserdata + // call atexit(): one argument, no results, let error propagate + lua_call(L, 1, 0); + // stack: proxy + return 1; +} + +namespace { + +void setdtor_refs::push_metatable(lua_State* L) +{ + lua_checkdelta(L, 1); + lluau_checkstack(L, 1); + // Ideally we want a metatable that forwards every operation on our + // setdtor_refs userdata proxy object to the original object. But the + // published C API doesn't include (e.g.) arithmetic operations on Lua + // objects, so in fact it's easier to express the desired metatable in Lua + // than in C++. We could make setdtor() depend on an external Lua module, + // but it seems less fragile to embed the Lua source code right here. + static const std::string setdtor_meta = stringize(R"-( + -- This metatable literal doesn't define __index() because that's + -- implemented in C++. We cannot, in Lua, peek into the setdtor_refs + -- userdata object to obtain objref, nor can we fetch Registry[objref]. + -- So our C++ __index() metamethod recognizes access to '_target' as a + -- reference to Registry[objref]. + -- The rest are defined per https://www.lua.org/manual/5.1/manual.html#2.8. + -- Luau supports destructors instead of __gc metamethod -- we rely on that! + -- We don't set __mode because our proxy is not a table. Real references + -- are stored in the wrapped table, so ITS __mode is what counts. + -- Initial definition of meta omits binary metamethods so they can bind the + -- metatable itself, as explained for binop() below. + local meta = { + __unm = function(arg) + return -arg._target + end, + __len = function(arg) + return #arg._target + end, + -- Comparison metamethods __eq(), __lt() and __le() are only called + -- when both operands have the same metamethod. For our purposes, that + -- means both operands are setdtor_refs userdata objects. + __eq = function(lhs, rhs) + return (lhs._target == rhs._target) + end, + __lt = function(lhs, rhs) + return (lhs._target < rhs._target) + end, + __le = function(lhs, rhs) + return (lhs._target <= rhs._target) + end, + __newindex = function(t, key, value) + t._target[key] = value + end, + __call = function(func, ...) + return func._target(...) + end, + __tostring = function(arg) + -- don't fret about arg._target's __tostring metamethod, + -- if any, because built-in tostring() deals with that + return tostring(arg._target) + end + } +)-", + binop("add", "+"), + binop("sub", "-"), + binop("mul", "*"), + binop("div", "/"), + binop("mod", "%"), + binop("pow", "^"), + binop("concat", ".."), +R"-( + return meta +)-"); + // only needed for debugging binop() +// LL_DEBUGS("Lua") << setdtor_meta << LL_ENDL; + + if (lluau::dostring(L, LL_PRETTY_FUNCTION, setdtor_meta) != LUA_OK) + { + // stack: error message string + lua_error(L); + } + llassert(lua_gettop(L) > 0); + llassert(lua_type(L, -1) == LUA_TTABLE); + // stack: Lua metatable compiled from setdtor_meta source + // Inject our C++ __index metamethod. + lua_rawsetfield(L, -1, "__index"sv, &setdtor_refs::meta__index); +} + +// In the definition of setdtor_meta above, binary arithmethic and +// concatenation metamethods are a little funny in that we don't know a +// priori which operand is the userdata with our metatable: the metamethod +// can be invoked either way. So every such metamethod must check, which +// leads to lots of redundancy. Hence this helper function. Call it a Lua +// macro. +std::string setdtor_refs::binop(const std::string& name, const std::string& op) +{ + return stringize( + " meta.__", name, " = function(lhs, rhs)\n" + " if getmetatable(lhs) == meta then\n" + " return lhs._target ", op, " rhs\n" + " else\n" + " return lhs ", op, " rhs._target\n" + " end\n" + " end\n"); +} + +// setdtor_refs __index() metamethod +int setdtor_refs::meta__index(lua_State* L) +{ + // called with (setdtor_refs userdata, key), returns retrieved object + lua_checkdelta(L, -1); + lluau_checkstack(L, 2); + // stack: proxy, key + // get ptr to the C++ struct data + auto ptr = lua_toclass(L, -2); + // meta__index() should NEVER be called with anything but setdtor_refs! + llassert(ptr); + // push the wrapped object + lua_getref(L, ptr->objref); + // stack: proxy, key, _target + // replace userdata with _target + lua_replace(L, -3); + // stack: _target, key + // Duplicate key because lua_tostring() converts number to string: + // if the key is (e.g.) 1, don't try to retrieve _target["1"]! + lua_pushvalue(L, -1); + // stack: _target, key, key + // recognize the special _target field + if (lua_tostdstring(L, -1) == "_target") + { + // okay, ditch both copies of "_target" string key + lua_pop(L, 2); + // stack: _target + } + else // any key but _target + { + // ditch stringized key + lua_pop(L, 1); + // stack: _target, key + // replace key with _target[key], invoking metamethod if any + lua_gettable(L, -2); + // stack: _target, _target[key] + // discard _target + lua_remove(L, -2); + // stack: _target[key] + } + return 1; +} + +// When Lua destroys a setdtor_refs userdata object, either from garbage +// collection or from LL.atexit(lua_destroybounduserdata), it's time to keep +// its promise to call the specified Lua destructor function with the +// specified Lua object. Of course we must also delete the captured +// "references" to both objects. +setdtor_refs::~setdtor_refs() +{ + lua_checkdelta(L); + lluau_checkstack(L, 2); + // push Registry[dtorref] + lua_getref(L, dtorref); + // push Registry[objref] + lua_getref(L, objref); + // free Registry[dtorref] + lua_unref(L, dtorref); + // free Registry[objref] + lua_unref(L, objref); + // call dtor(obj): one arg, no result, no error function + int rc = lua_pcall(L, 1, 0, 0); + if (rc != LUA_OK) + { + // TODO: we don't really want to propagate the error here. + // If this setdtor_refs instance is being destroyed by + // LL.atexit(), we want to continue cleanup. If it's being + // garbage-collected, the call is completely unpredictable from + // the consuming script's point of view. But what to do about this + // error?? For now, just log it. + LL_WARNS("Lua") << "setdtor(" << std::quoted(desc) << ") error: " + << lua_tostring(L, -1) << LL_ENDL; + lua_pop(L, 1); + } +} + +} // anonymous namespace + /***************************************************************************** * lua_what *****************************************************************************/ -- cgit v1.2.3 From 364ea79ab3a4d48e0d10fbeabb9b8e88f226baac Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 28 Aug 2024 19:34:05 -0400 Subject: Prevent erroneous assignment to LL.setdtor() proxy._target field. Trim redundant output from test_setdtor.lua. --- indra/llcommon/lua_function.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 12cff89fbd..f61cf3fe10 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -1050,7 +1050,7 @@ lua_function( } // called with (desc, obj, dtor), returns proxy object lua_checkdelta(L, -2); - lluau_checkstack(L, 3); // might get up to 6 stack entries +// lluau_checkstack(L, 0); // might get up to 3 stack entries auto desc{ lua_tostdstring(L, 1) }; // Get Lua "references" for each of the object and the dtor function. int objref = lua_ref(L, 2); @@ -1150,6 +1150,8 @@ void setdtor_refs::push_metatable(lua_State* L) return (lhs._target <= rhs._target) end, __newindex = function(t, key, value) + assert(key ~= '_target', + "Don't try to replace a setdtor() proxy's _target") t._target[key] = value end, __call = function(func, ...) -- cgit v1.2.3 From 03d7f2b84daf9ab991de6cad7d6149abda1ef716 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 28 Aug 2024 21:16:56 -0400 Subject: Ditch trailing spaces. --- indra/llcommon/lua_function.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 880bc209f6..ffb90032d2 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -3,7 +3,7 @@ * @author Nat Goodspeed * @date 2024-02-05 * @brief Implementation for lua_function. - * + * * $LicenseInfo:firstyear=2024&license=viewerlgpl$ * Copyright (c) 2024, Linden Research, Inc. * $/LicenseInfo$ @@ -700,11 +700,11 @@ void LuaState::check_interrupts_counter() // of interrupting itself at a moment when re-entry is not valid. So only // touch data in this LuaState. ++mInterrupts; - if (mInterrupts > INTERRUPTS_MAX_LIMIT) + if (mInterrupts > INTERRUPTS_MAX_LIMIT) { lluau::error(mState, "Possible infinite loop, terminated."); } - else if (mInterrupts % INTERRUPTS_SUSPEND_LIMIT == 0) + else if (mInterrupts % INTERRUPTS_SUSPEND_LIMIT == 0) { LL_DEBUGS("Lua") << LLCoros::getName() << " suspending at " << mInterrupts << " interrupts" << LL_ENDL; -- cgit v1.2.3 From a098a3d42bf862e0e3789e21f6b8f3f0e71d60d0 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 29 Aug 2024 08:27:43 -0400 Subject: Add Lua script name to log messages. --- indra/llcommon/lua_function.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index f61cf3fe10..be39a7d095 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -26,6 +26,7 @@ // other Linden headers #include "fsyspath.h" #include "hexdump.h" +#include "llcoros.h" #include "lleventcoro.h" #include "llsd.h" #include "llsdutil.h" @@ -530,7 +531,8 @@ LuaState::~LuaState() // That's important because we walk the atexit table backwards, to // destroy last the things we created (passed to LL.atexit()) first. int len(lua_objlen(mState, -1)); - LL_DEBUGS("Lua") << "Registry.atexit is a table with " << len << " entries" << LL_ENDL; + LL_DEBUGS("Lua") << LLCoros::getName() << ": Registry.atexit is a table with " + << len << " entries" << LL_ENDL; // Push debug.traceback() onto the stack as lua_pcall()'s error // handler function. On error, lua_pcall() calls the specified error @@ -555,15 +557,17 @@ LuaState::~LuaState() // Use lua_pcall() because errors in any one atexit() function // shouldn't cancel the rest of them. Pass debug.traceback() as // the error handler function. - LL_DEBUGS("Lua") << "Calling atexit(" << i << ")" << LL_ENDL; + LL_DEBUGS("Lua") << LLCoros::getName() + << ": calling atexit(" << i << ")" << LL_ENDL; if (lua_pcall(mState, 0, 0, -2) != LUA_OK) { auto error{ lua_tostdstring(mState, -1) }; - LL_WARNS("Lua") << "atexit(" << i << ") error: " << error << LL_ENDL; + LL_WARNS("Lua") << LLCoros::getName() + << ": atexit(" << i << ") error: " << error << LL_ENDL; // pop error message lua_pop(mState, 1); } - LL_DEBUGS("Lua") << "atexit(" << i << ") done" << LL_ENDL; + LL_DEBUGS("Lua") << LLCoros::getName() << ": atexit(" << i << ") done" << LL_ENDL; // lua_pcall() has already popped atexit[i]: // stack contains atexit, debug.traceback() } @@ -1277,7 +1281,8 @@ setdtor_refs::~setdtor_refs() // garbage-collected, the call is completely unpredictable from // the consuming script's point of view. But what to do about this // error?? For now, just log it. - LL_WARNS("Lua") << "setdtor(" << std::quoted(desc) << ") error: " + LL_WARNS("Lua") << LLCoros::getName() + << ": setdtor(" << std::quoted(desc) << ") error: " << lua_tostring(L, -1) << LL_ENDL; lua_pop(L, 1); } @@ -1377,7 +1382,8 @@ LuaStackDelta::~LuaStackDelta() // instance to keep its contract wrt the Lua data stack. if (std::uncaught_exceptions() == 0 && mDepth + mDelta != depth) { - LL_ERRS("Lua") << mWhere << ": Lua stack went from " << mDepth << " to " << depth; + LL_ERRS("Lua") << LLCoros::getName() << ": " << mWhere + << ": Lua stack went from " << mDepth << " to " << depth; if (mDelta) { LL_CONT << ", rather than expected " << (mDepth + mDelta) << " (" << mDelta << ")"; -- cgit v1.2.3 From 2d5cf36be6e0e367efec2bfa01378146269f33db Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 29 Aug 2024 21:36:39 -0400 Subject: Support next(), pairs(), ipairs() for LL.setdtor() table proxies. Replace the global next(), pairs() and ipairs() functions with a C++ function that drills down through layers of setdtor() proxy objects and then forwards the updated arguments to the original global function. Add a Luau __iter() metamethod to setdtor() proxy objects that, like other proxy metamethods, drills down to the underlying _target object. __iter() recognizes the case of a _target table which itself has a __iter() metamethod. Also add __idiv() metamethod to support integer division. Add tests for proxy // division, next(proxy), next(proxy, key), pairs(proxy), ipairs(proxy) and 'for k, v in proxy'. Also test the case where the table wrapped in the proxy has an __iter() metamethod of its own. --- indra/llcommon/lua_function.cpp | 62 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index be39a7d095..850dff1a33 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -496,6 +496,9 @@ namespace using LuaStateMap = std::unordered_map; static LuaStateMap sLuaStateMap; +// replacement next(), pairs(), ipairs() that understand setdtor() proxy args +int lua_proxydrill(lua_State* L); + } // anonymous namespace LuaState::LuaState(script_finished_fn cb): @@ -513,6 +516,28 @@ LuaState::LuaState(script_finished_fn cb): lua_register(mState, "print", LuaFunction::get("print_info")); // We don't want to have to prefix require(). lua_register(mState, "require", LuaFunction::get("require")); + + // Replace certain key global functions so they understand our + // LL.setdtor() proxy objects. + // (We could also do this for selected library functions as well, + // e.g. the table, string, math libraries... let's see if needed.) + for (const auto& func : { "next", "pairs", "ipairs" }) + { + // push the function's name string twice + lua_pushstring(mState, func); + lua_pushvalue(mState, -1); + // stack: name, name + // look up the existing global + lua_rawget(mState, LUA_GLOBALSINDEX); + // stack: name, global function + // bind original function as the upvalue for lua_proxydrill() + lua_pushcclosure(mState, lua_proxydrill, + stringize("lua_proxydrill(", func, ')').c_str(), + 1); + // stack: name, lua_proxydrill(func) + // global name = lua_proxydrill(func) + lua_rawset(mState, LUA_GLOBALSINDEX); + } } LuaState::~LuaState() @@ -1165,6 +1190,14 @@ void setdtor_refs::push_metatable(lua_State* L) -- don't fret about arg._target's __tostring metamethod, -- if any, because built-in tostring() deals with that return tostring(arg._target) + end, + __iter = function(arg) + local iter = (getmetatable(arg._target) or {}).__iter + if iter then + return iter(arg._target) + else + return next, arg._target + end end } )-", @@ -1172,6 +1205,7 @@ void setdtor_refs::push_metatable(lua_State* L) binop("sub", "-"), binop("mul", "*"), binop("div", "/"), + binop("idiv", "//"), binop("mod", "%"), binop("pow", "^"), binop("concat", ".."), @@ -1254,6 +1288,34 @@ int setdtor_refs::meta__index(lua_State* L) return 1; } +// replacement for global next(), pairs(), ipairs(): +// its lua_upvalueindex(1) is the original function it's replacing +int lua_proxydrill(lua_State* L) +{ + // Accept however many arguments the original function normally accepts. + // If our first arg is a userdata, check if it's a setdtor_refs proxy. + // Drill through as many levels of proxy wrapper as needed. + while (const setdtor_refs* ptr = lua_toclass(L, 1)) + { + // push original object + lua_getref(L, ptr->objref); + // replace first argument with that + lua_replace(L, 1); + } + // We've reached a first argument that's not a setdtor() proxy. + // How many arguments were we passed, anyway? + int args = lua_gettop(L); + // Push the original function, captured as our upvalue. + lua_pushvalue(L, lua_upvalueindex(1)); + // Shift the stack so the original function is first. + lua_insert(L, 1); + // Call the original function with all original args, no error checking. + // Don't truncate however many values that function returns. + lua_call(L, args, LUA_MULTRET); + // Return as many values as the original function returned. + return lua_gettop(L); +} + // When Lua destroys a setdtor_refs userdata object, either from garbage // collection or from LL.atexit(lua_destroybounduserdata), it's time to keep // its promise to call the specified Lua destructor function with the -- cgit v1.2.3 From 15db5010a0330bcb2ca2d0e4125ac3374f22b9cf Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Sat, 31 Aug 2024 12:37:57 -0400 Subject: Make global pairs(), ipairs() honor metamethods. Specifically, make pairs(obj) honor obj's __iter() metamethod if any. Make ipairs(obj) honor obj's __index() metamethod, if any. Given the semantics of the __index() metamethod, though, this only works for a proxy table if the proxy has no array entries (int keys) of its own. --- indra/llcommon/lua_function.cpp | 132 +++++++++++++++++++++++++++++++++++----- 1 file changed, 116 insertions(+), 16 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 850dff1a33..e4758d25a4 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -496,8 +496,19 @@ namespace using LuaStateMap = std::unordered_map; static LuaStateMap sLuaStateMap; -// replacement next(), pairs(), ipairs() that understand setdtor() proxy args +// replace table-at-index[name] with passed func, +// binding the original table-at-index[name] as func's upvalue +void replace_entry(lua_State* L, int index, + const std::string& name, lua_CFunction func); + +// replacement next() function that understands setdtor() proxy args int lua_proxydrill(lua_State* L); +// replacement pairs() function that supports __iter() metamethod +int lua_metapairs(lua_State* L); +// replacement ipairs() function that supports __index() metamethod +int lua_metaipairs(lua_State* L); +// helper for lua_metaipairs() (actual generator function) +int lua_metaipair(lua_State* L); } // anonymous namespace @@ -521,25 +532,114 @@ LuaState::LuaState(script_finished_fn cb): // LL.setdtor() proxy objects. // (We could also do this for selected library functions as well, // e.g. the table, string, math libraries... let's see if needed.) - for (const auto& func : { "next", "pairs", "ipairs" }) + replace_entry(mState, LUA_GLOBALSINDEX, "next", lua_proxydrill); + // Replacing pairs() with lua_metapairs() makes global pairs() honor + // objects with __iter() metamethods. + replace_entry(mState, LUA_GLOBALSINDEX, "pairs", lua_metapairs); + // Replacing ipairs() with lua_metaipairs() makes global ipairs() honor + // objects with __index() metamethods -- as long as the object in question + // has no array entries (int keys) of its own. (If it does, object[i] will + // retrieve table[i] instead of calling __index(table, i).) + replace_entry(mState, LUA_GLOBALSINDEX, "ipairs", lua_metaipairs); +} + +namespace +{ + +void replace_entry(lua_State* L, int index, + const std::string& name, lua_CFunction func) +{ + index = lua_absindex(L, index); + lua_checkdelta(L); + // push the function's name string twice + lua_pushlstring(L, name.data(), name.length()); + lua_pushvalue(L, -1); + // stack: name, name + // look up the existing table entry + lua_rawget(L, index); + // stack: name, original function + // bind original function as the upvalue for func() + lua_pushcclosure(L, func, (name + "()").c_str(), 1); + // stack: name, func-with-bound-original + // table[name] = func-with-bound-original + lua_rawset(L, index); +} + +int lua_metapairs(lua_State* L) +{ + // pairs(obj): object is at index 1 + // discard any erroneous surplus parameters + lua_settop(L, 1); + // stack: obj + if (luaL_getmetafield(L, 1, "__iter")) + { + // stack: obj, getmetatable(obj).__iter + lua_insert(L, 1); + // stack: __iter, obj + // We don't use the even nicer shorthand luaL_callmeta() because + // luaL_callmeta() only permits the metamethod to return a single + // value, and __iter() returns up to 3. Use lua_call() instead. + lua_call(L, 1, LUA_MULTRET); + // return as many values as __iter(obj) returned + return lua_gettop(L); + } + // otherwise, just return (next, obj) + lluau_checkstack(L, 1); + // stack: obj + lua_getglobal(L, "next"); + // stack: obj, next + lua_insert(L, 1); + // stack: next, obj + return 2; +} + +int lua_metaipairs(lua_State* L) +{ + lua_checkdelta(L, 2); + // ipairs(obj): object is at index 1 + // discard any erroneous surplus parameters + lua_settop(L, 1); + // stack: obj + lua_pushcfunction(L, lua_metaipair, "lua_metaipair"); + // stack: obj, lua_metaipair + lua_insert(L, 1); + // stack: lua_metaipair, obj + // push explicit 0 so lua_metaipair need not special-case nil + lua_pushinteger(L, 0); + // stack: lua_metaipair, obj, 0 + return 3; +} + +int lua_metaipair(lua_State* L) +{ + // called with (obj, previous-index) + // increment previous-index for this call + lua_Integer i = luaL_checkinteger(L, 2) + 1; + lua_pop(L, 1); + // stack: obj + lua_pushinteger(L, i); + // stack: obj, i + lua_pushvalue(L, -1); + // stack: obj, i, i + lua_insert(L, 1); + // stack: i, obj, i + lua_gettable(L, -2); + // stack: i, obj, obj[i] + lua_remove(L, -2); + // stack: i, obj[i] + if (! lua_isnil(L, -1)) { - // push the function's name string twice - lua_pushstring(mState, func); - lua_pushvalue(mState, -1); - // stack: name, name - // look up the existing global - lua_rawget(mState, LUA_GLOBALSINDEX); - // stack: name, global function - // bind original function as the upvalue for lua_proxydrill() - lua_pushcclosure(mState, lua_proxydrill, - stringize("lua_proxydrill(", func, ')').c_str(), - 1); - // stack: name, lua_proxydrill(func) - // global name = lua_proxydrill(func) - lua_rawset(mState, LUA_GLOBALSINDEX); + // great, obj[i] isn't nil: return (i, obj[i]) + return 2; } + // obj[i] is nil. ipairs() is documented to stop at the first hole, + // regardless of #obj. Clear the stack, i.e. return nil. + lua_settop(L, 0); + return 0; } +} // anonymous namespace + LuaState::~LuaState() { // We're just about to destroy this lua_State mState. Did this Lua chunk -- cgit v1.2.3 From 8c18cfd22583e981f93734ec0aa6ee0ead3f26c5 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 2 Sep 2024 15:26:34 -0400 Subject: Make `pairs()`, `ipairs()` forward to original funcs if no metamethods. That is, our replacement `pairs()` forwards the call to built-in `pairs()` when the passed object has no `__iter()` metamethod. Similarly, our replacement `ipairs()` forwards to built-in `ipairs()` when the passed object has no `__index()` metamethod. This allows for the possibility that the built-in `pairs()` and `ipairs()` functions engage more efficient implementations than the obvious ones. --- indra/llcommon/lua_function.cpp | 87 +++++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 33 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index e4758d25a4..da88f57a5b 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -568,46 +568,67 @@ void replace_entry(lua_State* L, int index, int lua_metapairs(lua_State* L) { // pairs(obj): object is at index 1 - // discard any erroneous surplus parameters - lua_settop(L, 1); - // stack: obj + // How many args were we passed? + int args = lua_gettop(L); + // stack: obj, ... if (luaL_getmetafield(L, 1, "__iter")) { - // stack: obj, getmetatable(obj).__iter - lua_insert(L, 1); - // stack: __iter, obj - // We don't use the even nicer shorthand luaL_callmeta() because - // luaL_callmeta() only permits the metamethod to return a single - // value, and __iter() returns up to 3. Use lua_call() instead. - lua_call(L, 1, LUA_MULTRET); - // return as many values as __iter(obj) returned - return lua_gettop(L); + // stack: obj, ..., getmetatable(obj).__iter + } + else + { + // Push the original pairs() function, captured as our upvalue. + lua_pushvalue(L, lua_upvalueindex(1)); + // stack: obj, ..., original pairs() } - // otherwise, just return (next, obj) - lluau_checkstack(L, 1); - // stack: obj - lua_getglobal(L, "next"); - // stack: obj, next lua_insert(L, 1); - // stack: next, obj - return 2; + // stack: (__iter() or pairs()), obj, ... + // call whichever function(obj, ...) (args args, up to 3 return values) + lua_call(L, args, LUA_MULTRET); + // return as many values as the selected function returned + return lua_gettop(L); } int lua_metaipairs(lua_State* L) { - lua_checkdelta(L, 2); // ipairs(obj): object is at index 1 - // discard any erroneous surplus parameters - lua_settop(L, 1); - // stack: obj - lua_pushcfunction(L, lua_metaipair, "lua_metaipair"); - // stack: obj, lua_metaipair - lua_insert(L, 1); - // stack: lua_metaipair, obj - // push explicit 0 so lua_metaipair need not special-case nil - lua_pushinteger(L, 0); - // stack: lua_metaipair, obj, 0 - return 3; + // How many args were we passed? + int args = lua_gettop(L); + // stack: obj, ... + if (luaL_getmetafield(L, 1, "__index")) + { + // stack: obj, ..., getmetatable(obj).__index + // discard __index and everything but obj: + // we don't want to call __index(), just check its presence + lua_settop(L, 1); + // stack: obj + lua_pushcfunction(L, lua_metaipair, "lua_metaipair"); + // stack: obj, lua_metaipair + lua_insert(L, 1); + // stack: lua_metaipair, obj + // push explicit 0 so lua_metaipair need not special-case nil + lua_pushinteger(L, 0); + // stack: lua_metaipair, obj, 0 + return 3; + } + else // no __index() metamethod + { + // Although our lua_metaipair() function demonstrably works whether or + // not our object has an __index() metamethod, the code below assumes + // that the Lua engine may have a more efficient implementation for + // built-in ipairs() than our lua_metaipair(). + // Push the original ipairs() function, captured as our upvalue. + lua_pushvalue(L, lua_upvalueindex(1)); + // stack: obj, ..., original ipairs() + // Shift the stack so the original function is first. + lua_insert(L, 1); + // stack: original ipairs(), obj, ... + // Call original ipairs() with all original args, no error checking. + // Don't truncate however many values that function returns. + lua_call(L, args, LUA_MULTRET); + // Return as many values as the original function returned. + return lua_gettop(L); + } } int lua_metaipair(lua_State* L) @@ -624,7 +645,7 @@ int lua_metaipair(lua_State* L) lua_insert(L, 1); // stack: i, obj, i lua_gettable(L, -2); - // stack: i, obj, obj[i] + // stack: i, obj, obj[i] (honoring __index()) lua_remove(L, -2); // stack: i, obj[i] if (! lua_isnil(L, -1)) @@ -1388,7 +1409,7 @@ int setdtor_refs::meta__index(lua_State* L) return 1; } -// replacement for global next(), pairs(), ipairs(): +// replacement for global next(): // its lua_upvalueindex(1) is the original function it's replacing int lua_proxydrill(lua_State* L) { -- cgit v1.2.3 From 9dc916bfcafd43890be20623d359be82e84f73ac Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 3 Sep 2024 12:28:25 -0400 Subject: In lua_what() and lua_stack(), try to report a function's name. --- indra/llcommon/lua_function.cpp | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index da88f57a5b..67ca29c689 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -567,6 +567,7 @@ void replace_entry(lua_State* L, int index, int lua_metapairs(lua_State* L) { +// LuaLog debug(L, "lua_metapairs()"); // pairs(obj): object is at index 1 // How many args were we passed? int args = lua_gettop(L); @@ -591,6 +592,7 @@ int lua_metapairs(lua_State* L) int lua_metaipairs(lua_State* L) { +// LuaLog debug(L, "lua_metaipairs()"); // ipairs(obj): object is at index 1 // How many args were we passed? int args = lua_gettop(L); @@ -633,9 +635,10 @@ int lua_metaipairs(lua_State* L) int lua_metaipair(lua_State* L) { +// LuaLog debug(L, "lua_metaipair()"); // called with (obj, previous-index) // increment previous-index for this call - lua_Integer i = luaL_checkinteger(L, 2) + 1; + lua_Integer i = luaL_optinteger(L, 2, 0) + 1; lua_pop(L, 1); // stack: obj lua_pushinteger(L, i); @@ -1523,6 +1526,31 @@ std::ostream& operator<<(std::ostream& out, const lua_what& self) out << lua_touserdata(self.L, self.index); break; + case LUA_TFUNCTION: + { + // Try for the function's name, at the cost of a few more stack + // entries. + lua_checkdelta(self.L); + lluau_checkstack(self.L, 3); + lua_getglobal(self.L, "debug"); + // stack: ..., debug + lua_getfield(self.L, -1, "info"); + // stack: ..., debug, debug.info + lua_remove(self.L, -2); + // stack: ..., debug.info + lua_pushvalue(self.L, self.index); + // stack: ..., debug.info, this function + lua_pushstring(self.L, "n"); + // stack: ..., debug.info, this function, "n" + // 2 arguments, 1 return value (or error message), no error handler + lua_pcall(self.L, 2, 1, 0); + // stack: ..., function name (or error) from debug.info() + out << "function " << lua_tostdstring(self.L, -1); + lua_pop(self.L, 1); + // stack: ... + break; + } + default: // anything else, don't bother trying to report value, just type out << lua_typename(self.L, lua_type(self.L, self.index)); -- cgit v1.2.3 From bf2b2eb01ca8680914d17dda713d9365e2ecc3eb Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 3 Sep 2024 16:26:03 -0400 Subject: Add Lua traceback to errors from calling lluau::expr(). That includes scripts run by LLLUAmanager::runScriptFile(), runScriptLine() et al. --- indra/llcommon/lua_function.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 67ca29c689..c3d336bfcb 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -65,10 +65,26 @@ int dostring(lua_State* L, const std::string& desc, const std::string& text) if (r != LUA_OK) return r; + // Push debug.traceback() onto the stack as lua_pcall()'s error + // handler function. On error, lua_pcall() calls the specified error + // handler function with the original error message; the message + // returned by the error handler is then returned by lua_pcall(). + // Luau's debug.traceback() is called with a message to prepend to the + // returned traceback string. Almost as if they'd been designed to + // work together... + lua_getglobal(L, "debug"); + lua_getfield(L, -1, "traceback"); + // ditch "debug" + lua_remove(L, -2); + // stack: compiled chunk, debug.traceback() + lua_insert(L, -2); + // stack: debug.traceback(), compiled chunk + LuaRemover cleanup(L, -2); + // It's important to pass LUA_MULTRET as the expected number of return // values: if we pass any fixed number, we discard any returned values // beyond that number. - return lua_pcall(L, 0, LUA_MULTRET, 0); + return lua_pcall(L, 0, LUA_MULTRET, -2); } int loadstring(lua_State *L, const std::string &desc, const std::string &text) -- cgit v1.2.3 From 5319d314206c7c1c21b2bbd3a661c0c520373dcc Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 4 Sep 2024 16:24:42 -0400 Subject: Windows build fixes --- indra/llcommon/lua_function.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index ffb90032d2..c0090dd395 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -418,7 +418,7 @@ void lua_pushllsd(lua_State* L, const LLSD& data) case LLSD::TypeMap: { // push a new table with space for our non-array keys - lua_createtable(L, 0, data.size()); + lua_createtable(L, 0, narrow(data.size())); for (const auto& pair: llsd::inMap(data)) { // push value -- so now table is at -2, value at -1 @@ -432,7 +432,7 @@ void lua_pushllsd(lua_State* L, const LLSD& data) case LLSD::TypeArray: { // push a new table with space for array entries - lua_createtable(L, data.size(), 0); + lua_createtable(L, narrow(data.size()), 0); lua_Integer key{ 0 }; for (const auto& item: llsd::inArray(data)) { -- cgit v1.2.3 From 782a898efadadf2747cc3310749f34a8dde8dd60 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 6 Sep 2024 15:11:15 -0400 Subject: Introduce LuaFeature debug setting, default off. Make central Lua engine functionality conditional on that flag. --- indra/llcommon/lua_function.cpp | 160 +++++++++++++++++++++++++--------------- 1 file changed, 102 insertions(+), 58 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index f7876e4aaf..eefb1e62cf 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -24,6 +24,7 @@ #include // external library headers // other Linden headers +#include "commoncontrol.h" #include "fsyspath.h" #include "hexdump.h" #include "llcoros.h" @@ -529,9 +530,35 @@ int lua_metaipair(lua_State* L); } // anonymous namespace LuaState::LuaState(script_finished_fn cb): - mCallback(cb), - mState(luaL_newstate()) + mCallback(cb) { + /*---------------------------- feature flag ----------------------------*/ + try + { + mFeature = LL::CommonControl::get("Global", "LuaFeature").asBoolean(); + } + catch (const LL::CommonControl::NoListener&) + { + // If this program doesn't have an LLViewerControlListener, + // it's probably a test program; go ahead. + mFeature = true; + } + catch (const LL::CommonControl::ParamError&) + { + // We found LLViewerControlListener, but its settings do not include + // "LuaFeature". Hmm, fishy: that feature flag was introduced at the + // same time as this code. + mFeature = false; + } + // None of the rest of this is necessary if we're not going to run anything. + if (! mFeature) + { + mError = "Lua feature disabled"; + return; + } + /*---------------------------- feature flag ----------------------------*/ + + mState = luaL_newstate(); // Ensure that we can always find this LuaState instance, given the // lua_State we just created or any of its coroutines. sLuaStateMap.emplace(mState, this); @@ -682,75 +709,81 @@ int lua_metaipair(lua_State* L) LuaState::~LuaState() { - // We're just about to destroy this lua_State mState. Did this Lua chunk - // register any atexit() functions? - lluau_checkstack(mState, 3); - // look up Registry.atexit - lua_getfield(mState, LUA_REGISTRYINDEX, "atexit"); - // stack contains Registry.atexit - if (lua_istable(mState, -1)) + /*---------------------------- feature flag ----------------------------*/ + if (mFeature) + /*---------------------------- feature flag ----------------------------*/ { - // We happen to know that Registry.atexit is built by appending array - // entries using table.insert(). That's important because it means - // there are no holes, and therefore lua_objlen() should be correct. - // That's important because we walk the atexit table backwards, to - // destroy last the things we created (passed to LL.atexit()) first. - int len(lua_objlen(mState, -1)); - LL_DEBUGS("Lua") << LLCoros::getName() << ": Registry.atexit is a table with " - << len << " entries" << LL_ENDL; - - // Push debug.traceback() onto the stack as lua_pcall()'s error - // handler function. On error, lua_pcall() calls the specified error - // handler function with the original error message; the message - // returned by the error handler is then returned by lua_pcall(). - // Luau's debug.traceback() is called with a message to prepend to the - // returned traceback string. Almost as if they'd been designed to - // work together... - lua_getglobal(mState, "debug"); - lua_getfield(mState, -1, "traceback"); - // ditch "debug" - lua_remove(mState, -2); - // stack now contains atexit, debug.traceback() - - for (int i(len); i >= 1; --i) + // We're just about to destroy this lua_State mState. Did this Lua chunk + // register any atexit() functions? + lluau_checkstack(mState, 3); + // look up Registry.atexit + lua_getfield(mState, LUA_REGISTRYINDEX, "atexit"); + // stack contains Registry.atexit + if (lua_istable(mState, -1)) { - lua_pushinteger(mState, i); - // stack contains Registry.atexit, debug.traceback(), i - lua_gettable(mState, -3); - // stack contains Registry.atexit, debug.traceback(), atexit[i] - // Call atexit[i](), no args, no return values. - // Use lua_pcall() because errors in any one atexit() function - // shouldn't cancel the rest of them. Pass debug.traceback() as - // the error handler function. - LL_DEBUGS("Lua") << LLCoros::getName() - << ": calling atexit(" << i << ")" << LL_ENDL; - if (lua_pcall(mState, 0, 0, -2) != LUA_OK) + // We happen to know that Registry.atexit is built by appending array + // entries using table.insert(). That's important because it means + // there are no holes, and therefore lua_objlen() should be correct. + // That's important because we walk the atexit table backwards, to + // destroy last the things we created (passed to LL.atexit()) first. + int len(lua_objlen(mState, -1)); + LL_DEBUGS("Lua") << LLCoros::getName() << ": Registry.atexit is a table with " + << len << " entries" << LL_ENDL; + + // Push debug.traceback() onto the stack as lua_pcall()'s error + // handler function. On error, lua_pcall() calls the specified error + // handler function with the original error message; the message + // returned by the error handler is then returned by lua_pcall(). + // Luau's debug.traceback() is called with a message to prepend to the + // returned traceback string. Almost as if they'd been designed to + // work together... + lua_getglobal(mState, "debug"); + lua_getfield(mState, -1, "traceback"); + // ditch "debug" + lua_remove(mState, -2); + // stack now contains atexit, debug.traceback() + + for (int i(len); i >= 1; --i) { - auto error{ lua_tostdstring(mState, -1) }; - LL_WARNS("Lua") << LLCoros::getName() - << ": atexit(" << i << ") error: " << error << LL_ENDL; - // pop error message - lua_pop(mState, 1); + lua_pushinteger(mState, i); + // stack contains Registry.atexit, debug.traceback(), i + lua_gettable(mState, -3); + // stack contains Registry.atexit, debug.traceback(), atexit[i] + // Call atexit[i](), no args, no return values. + // Use lua_pcall() because errors in any one atexit() function + // shouldn't cancel the rest of them. Pass debug.traceback() as + // the error handler function. + LL_DEBUGS("Lua") << LLCoros::getName() + << ": calling atexit(" << i << ")" << LL_ENDL; + if (lua_pcall(mState, 0, 0, -2) != LUA_OK) + { + auto error{ lua_tostdstring(mState, -1) }; + LL_WARNS("Lua") << LLCoros::getName() + << ": atexit(" << i << ") error: " << error << LL_ENDL; + // pop error message + lua_pop(mState, 1); + } + LL_DEBUGS("Lua") << LLCoros::getName() << ": atexit(" << i << ") done" << LL_ENDL; + // lua_pcall() has already popped atexit[i]: + // stack contains atexit, debug.traceback() } - LL_DEBUGS("Lua") << LLCoros::getName() << ": atexit(" << i << ") done" << LL_ENDL; - // lua_pcall() has already popped atexit[i]: - // stack contains atexit, debug.traceback() + // pop debug.traceback() + lua_pop(mState, 1); } - // pop debug.traceback() + // pop Registry.atexit (either table or nil) lua_pop(mState, 1); - } - // pop Registry.atexit (either table or nil) - lua_pop(mState, 1); - lua_close(mState); + // with the demise of this LuaState, remove sLuaStateMap entry + sLuaStateMap.erase(mState); + + lua_close(mState); + } if (mCallback) { // mError potentially set by previous checkLua() call(s) mCallback(mError); } - // with the demise of this LuaState, remove sLuaStateMap entry - sLuaStateMap.erase(mState); } bool LuaState::checkLua(const std::string& desc, int r) @@ -768,6 +801,14 @@ bool LuaState::checkLua(const std::string& desc, int r) std::pair LuaState::expr(const std::string& desc, const std::string& text) { + /*---------------------------- feature flag ----------------------------*/ + if (! mFeature) + { + // fake an error + return { -1, stringize("Not running ", desc) }; + } + /*---------------------------- feature flag ----------------------------*/ + set_interrupts_counter(0); lua_callbacks(mState)->interrupt = [](lua_State *L, int gc) @@ -843,6 +884,9 @@ std::pair LuaState::expr(const std::string& desc, const std::string& return result; } +// We think we don't need mFeature tests in the rest of these LuaState methods +// because, if expr() isn't running code, nobody should be calling any of them. + LuaListener& LuaState::obtainListener(lua_State* L) { lluau_checkstack(L, 2); -- cgit v1.2.3 From 26efc7e376ef52284a6281f36cf45eb03bc13507 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 10 Sep 2024 15:25:07 -0400 Subject: Pass std::string_view by value, not by const reference. Consensus seems to be that (a) string_view is, in effect, already a reference, (b) it's small enough to make pass-by-value reasonable and (c) the optimizer can reason about values way better than it can about references. --- indra/llcommon/lua_function.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index eefb1e62cf..380e650360 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -999,8 +999,8 @@ LuaPopper::~LuaPopper() /***************************************************************************** * LuaFunction class *****************************************************************************/ -LuaFunction::LuaFunction(const std::string_view& name, lua_CFunction function, - const std::string_view& helptext) +LuaFunction::LuaFunction(std::string_view name, lua_CFunction function, + std::string_view helptext) { const auto& [registry, lookup] = getState(); registry.emplace(name, Registry::mapped_type{ function, helptext }); -- cgit v1.2.3 From 034d13bcd77c3cbba00da1ef6c3c59d22f4a689e Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 12 Sep 2024 10:39:26 -0400 Subject: Disable happy-path destructor semantics when unwinding C++ stack. If the C++ runtime is already handling an exception, don't try to launch more Lua operations. --- indra/llcommon/lua_function.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 380e650360..2557fd0cc9 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -709,6 +709,11 @@ int lua_metaipair(lua_State* L) LuaState::~LuaState() { + // If we're unwinding the stack due to an exception, don't bother trying + // to call any callbacks -- either Lua or C++. + if (std::uncaught_exceptions() != 0) + return; + /*---------------------------- feature flag ----------------------------*/ if (mFeature) /*---------------------------- feature flag ----------------------------*/ @@ -990,7 +995,8 @@ lua_function(atexit, "atexit(function): " *****************************************************************************/ LuaPopper::~LuaPopper() { - if (mCount) + // If we're unwinding the C++ stack due to an exception, don't pop! + if (std::uncaught_exceptions() == 0 && mCount) { lua_pop(mState, mCount); } -- cgit v1.2.3 From 1154e02fdded191147284997707a3b18ee3b43fd Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 16 Sep 2024 13:53:11 -0400 Subject: WIP: edits in support of Lua script args --- indra/llcommon/lua_function.cpp | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 2557fd0cc9..014ff3c4c4 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -60,7 +60,8 @@ namespace namespace lluau { -int dostring(lua_State* L, const std::string& desc, const std::string& text) +int dostring(lua_State* L, const std::string& desc, const std::string& text, + const std::vector& args) { auto r = loadstring(L, desc, text); if (r != LUA_OK) @@ -80,12 +81,22 @@ int dostring(lua_State* L, const std::string& desc, const std::string& text) // stack: compiled chunk, debug.traceback() lua_insert(L, -2); // stack: debug.traceback(), compiled chunk - LuaRemover cleanup(L, -2); + // capture absolute index of debug.traceback() + int traceback = lua_absindex(L, -2); + // remove it from stack on exit + LuaRemover cleanup(L, traceback); + + // push any args passed -- all strings -- script must handle any desired + // conversions + for (const auto& arg : args) + { + lua_pushstdstring(L, arg); + } // It's important to pass LUA_MULTRET as the expected number of return // values: if we pass any fixed number, we discard any returned values // beyond that number. - return lua_pcall(L, 0, LUA_MULTRET, -2); + return lua_pcall(L, int(args.size()), LUA_MULTRET, traceback); } int loadstring(lua_State *L, const std::string &desc, const std::string &text) @@ -804,7 +815,13 @@ bool LuaState::checkLua(const std::string& desc, int r) return true; } -std::pair LuaState::expr(const std::string& desc, const std::string& text) +std::pair LuaState::expr(const std::string& desc, const ScriptCommand& command) +{ + return expr(desc, , command.args); +} + +std::pair LuaState::expr(const std::string& desc, const std::string& text, + const std::vector& args) { /*---------------------------- feature flag ----------------------------*/ if (! mFeature) @@ -827,7 +844,7 @@ std::pair LuaState::expr(const std::string& desc, const std::string& }; LL_INFOS("Lua") << desc << " run" << LL_ENDL; - if (! checkLua(desc, lluau::dostring(mState, desc, text))) + if (! checkLua(desc, lluau::dostring(mState, desc, text, args))) { LL_WARNS("Lua") << desc << " error: " << mError << LL_ENDL; return { -1, mError }; @@ -1049,6 +1066,18 @@ std::pair LuaFunction::getState() return { registry, lookup }; } +/***************************************************************************** +* LuaCommand +*****************************************************************************/ +LuaCommand::LuaCommand(const std::string& command) +{ +} + +bool LuaCommand::found() const +{ + return ; +} + /***************************************************************************** * source_path() *****************************************************************************/ -- cgit v1.2.3 From 6d29beb91b019e1995cdb7c4aaf7a043de4bf053 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 20 Sep 2024 15:13:43 -0400 Subject: Add ability to pass command-line arguments to a Lua script. Introduce `ScriptCommand` class that parses a command line into a script name and optional args, using bash-like quoting and escaping. `ScriptCommand` searches for a file with that script name on a passed list of directories; the directories may be specified relative to a particular base directory. `ScriptCommand` supports the special case of a script name containing unescaped spaces. It guarantees that either the returned script file exists, or its `error()` string is non-empty. Replace `LLLeap::create()` logic, from which `ScriptCommand` was partly derived, with a `ScriptCommand` instance. Make `LLLUAmanager::runScriptFile()` use a `ScriptCommand` instance to parse the passed command line. Subsume `LLAppViewer::init()` script-path-searching logic for `--luafile` into `ScriptCommand`. In fact that lambda now simply calls `LLLUAmanager::runScriptFile()`. Make `lluau::dostring()` accept an optional vector of script argument strings. Following PUC-Rio Lua convention, pass these arguments into a Lua script as the predefined global `arg`, and also as the script's `...` argument. `LuaState::expr()` also accepts and passes through script argument strings. Change the log tag for the Lua script interruption message: if we want it, we can still enable it, but we don't necessarily want it along with all other "Lua" DEBUG messages. Remove `LuaState::script_finished_fn`, which isn't used any more. Also remove the corresponding `LLLUAmanager::script_finished_fn`. This allows us to simplify `~LuaState()` slightly, as well as the parameter signatures for `LLLUAmanager::runScriptFile()` and `runScriptLine()`. --- indra/llcommon/lua_function.cpp | 199 ++++++++++++++++++++++------------------ 1 file changed, 111 insertions(+), 88 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 014ff3c4c4..a9f88f3170 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -63,6 +63,8 @@ namespace lluau int dostring(lua_State* L, const std::string& desc, const std::string& text, const std::vector& args) { + // debug.traceback() + compiled chunk + args table + args... + slop + lluau_checkstack(L, 1 + 1 + 1 + int(args.size()) + 2); auto r = loadstring(L, desc, text); if (r != LUA_OK) return r; @@ -86,12 +88,56 @@ int dostring(lua_State* L, const std::string& desc, const std::string& text, // remove it from stack on exit LuaRemover cleanup(L, traceback); - // push any args passed -- all strings -- script must handle any desired - // conversions + // Originally we just pushed 'args' to the Lua stack before entering the + // chunk. But that's awkward for the chunk: it must reference those + // arguments as '...', using any of a number of tactics to move them to + // named variables. This doesn't work as a Lua chunk expecting arguments: + // function(a, b, c) + // -- ... + // end + // because that code only *defines* a function: the function's body isn't + // entered by executing the chunk. + // + // Per https://www.lua.org/manual/5.1/manual.html#6 Lua Stand-alone, we + // now also create a global table called 'arg' whose [0] is the script + // name, ['n'] is the number of additional arguments and [1] through + // [['n']] are the additional arguments. We diverge from that spec in not + // creating any negative indices. + // + // Since the spec notes that the chunk can also reference args using + // '...', we also leave them on the stack. + + // stack: debug.traceback(), compiled chunk + // create arg table pre-sized to hold the args array, plus [0] and ['n'] + lua_createtable(L, narrow(args.size()), 2); + // stack: debug.traceback(), compiled chunk, arg table + int argi = lua_absindex(L, -1); + lua_Integer i = 0; + // store desc (e.g. script name) as arg[0] + lua_pushinteger(L, i); + lua_pushstdstring(L, desc); + lua_rawset(L, argi); // rawset() pops key and value + // store args.size() as arg.n + lua_pushinteger(L, narrow(args.size())); + lua_setfield(L, argi, "n"); // setfield() pops value for (const auto& arg : args) { + // push each arg in order lua_pushstdstring(L, arg); + // push index + lua_pushinteger(L, ++i); + // duplicate arg[i] to store in arg table + lua_pushvalue(L, -2); + // stack: ..., arg[i], i, arg[i] + lua_rawset(L, argi); + // leave ..., arg[i] on stack } + // stack: debug.traceback(), compiled chunk, arg, arg[1], arg[2], ... + // duplicate the arg table to store it + lua_pushvalue(L, argi); + lua_setglobal(L, "arg"); + lua_remove(L, argi); + // stack: debug.traceback(), compiled chunk, arg[1], arg[2], ... // It's important to pass LUA_MULTRET as the expected number of return // values: if we pass any fixed number, we discard any returned values @@ -101,6 +147,7 @@ int dostring(lua_State* L, const std::string& desc, const std::string& text, int loadstring(lua_State *L, const std::string &desc, const std::string &text) { + lluau_checkstack(L, 1); size_t bytecodeSize = 0; // The char* returned by luau_compile() must be freed by calling free(). // Use unique_ptr so the memory will be freed even if luau_load() throws. @@ -540,8 +587,7 @@ int lua_metaipair(lua_State* L); } // anonymous namespace -LuaState::LuaState(script_finished_fn cb): - mCallback(cb) +LuaState::LuaState() { /*---------------------------- feature flag ----------------------------*/ try @@ -726,80 +772,74 @@ LuaState::~LuaState() return; /*---------------------------- feature flag ----------------------------*/ - if (mFeature) + if (! mFeature) + return; /*---------------------------- feature flag ----------------------------*/ + + // We're just about to destroy this lua_State mState. Did this Lua chunk + // register any atexit() functions? + lluau_checkstack(mState, 3); + // look up Registry.atexit + lua_getfield(mState, LUA_REGISTRYINDEX, "atexit"); + // stack contains Registry.atexit + if (lua_istable(mState, -1)) { - // We're just about to destroy this lua_State mState. Did this Lua chunk - // register any atexit() functions? - lluau_checkstack(mState, 3); - // look up Registry.atexit - lua_getfield(mState, LUA_REGISTRYINDEX, "atexit"); - // stack contains Registry.atexit - if (lua_istable(mState, -1)) + // We happen to know that Registry.atexit is built by appending array + // entries using table.insert(). That's important because it means + // there are no holes, and therefore lua_objlen() should be correct. + // That's important because we walk the atexit table backwards, to + // destroy last the things we created (passed to LL.atexit()) first. + int len(lua_objlen(mState, -1)); + LL_DEBUGS("Lua") << LLCoros::getName() << ": Registry.atexit is a table with " + << len << " entries" << LL_ENDL; + + // Push debug.traceback() onto the stack as lua_pcall()'s error + // handler function. On error, lua_pcall() calls the specified error + // handler function with the original error message; the message + // returned by the error handler is then returned by lua_pcall(). + // Luau's debug.traceback() is called with a message to prepend to the + // returned traceback string. Almost as if they'd been designed to + // work together... + lua_getglobal(mState, "debug"); + lua_getfield(mState, -1, "traceback"); + // ditch "debug" + lua_remove(mState, -2); + // stack now contains atexit, debug.traceback() + + for (int i(len); i >= 1; --i) { - // We happen to know that Registry.atexit is built by appending array - // entries using table.insert(). That's important because it means - // there are no holes, and therefore lua_objlen() should be correct. - // That's important because we walk the atexit table backwards, to - // destroy last the things we created (passed to LL.atexit()) first. - int len(lua_objlen(mState, -1)); - LL_DEBUGS("Lua") << LLCoros::getName() << ": Registry.atexit is a table with " - << len << " entries" << LL_ENDL; - - // Push debug.traceback() onto the stack as lua_pcall()'s error - // handler function. On error, lua_pcall() calls the specified error - // handler function with the original error message; the message - // returned by the error handler is then returned by lua_pcall(). - // Luau's debug.traceback() is called with a message to prepend to the - // returned traceback string. Almost as if they'd been designed to - // work together... - lua_getglobal(mState, "debug"); - lua_getfield(mState, -1, "traceback"); - // ditch "debug" - lua_remove(mState, -2); - // stack now contains atexit, debug.traceback() - - for (int i(len); i >= 1; --i) + lua_pushinteger(mState, i); + // stack contains Registry.atexit, debug.traceback(), i + lua_gettable(mState, -3); + // stack contains Registry.atexit, debug.traceback(), atexit[i] + // Call atexit[i](), no args, no return values. + // Use lua_pcall() because errors in any one atexit() function + // shouldn't cancel the rest of them. Pass debug.traceback() as + // the error handler function. + LL_DEBUGS("Lua") << LLCoros::getName() + << ": calling atexit(" << i << ")" << LL_ENDL; + if (lua_pcall(mState, 0, 0, -2) != LUA_OK) { - lua_pushinteger(mState, i); - // stack contains Registry.atexit, debug.traceback(), i - lua_gettable(mState, -3); - // stack contains Registry.atexit, debug.traceback(), atexit[i] - // Call atexit[i](), no args, no return values. - // Use lua_pcall() because errors in any one atexit() function - // shouldn't cancel the rest of them. Pass debug.traceback() as - // the error handler function. - LL_DEBUGS("Lua") << LLCoros::getName() - << ": calling atexit(" << i << ")" << LL_ENDL; - if (lua_pcall(mState, 0, 0, -2) != LUA_OK) - { - auto error{ lua_tostdstring(mState, -1) }; - LL_WARNS("Lua") << LLCoros::getName() - << ": atexit(" << i << ") error: " << error << LL_ENDL; - // pop error message - lua_pop(mState, 1); - } - LL_DEBUGS("Lua") << LLCoros::getName() << ": atexit(" << i << ") done" << LL_ENDL; - // lua_pcall() has already popped atexit[i]: - // stack contains atexit, debug.traceback() + auto error{ lua_tostdstring(mState, -1) }; + LL_WARNS("Lua") << LLCoros::getName() + << ": atexit(" << i << ") error: " << error << LL_ENDL; + // pop error message + lua_pop(mState, 1); } - // pop debug.traceback() - lua_pop(mState, 1); + LL_DEBUGS("Lua") << LLCoros::getName() << ": atexit(" << i << ") done" << LL_ENDL; + // lua_pcall() has already popped atexit[i]: + // stack contains atexit, debug.traceback() } - // pop Registry.atexit (either table or nil) + // pop debug.traceback() lua_pop(mState, 1); - - // with the demise of this LuaState, remove sLuaStateMap entry - sLuaStateMap.erase(mState); - - lua_close(mState); } + // pop Registry.atexit (either table or nil) + lua_pop(mState, 1); - if (mCallback) - { - // mError potentially set by previous checkLua() call(s) - mCallback(mError); - } + // with the demise of this LuaState, remove sLuaStateMap entry + sLuaStateMap.erase(mState); + + lua_close(mState); } bool LuaState::checkLua(const std::string& desc, int r) @@ -815,11 +855,6 @@ bool LuaState::checkLua(const std::string& desc, int r) return true; } -std::pair LuaState::expr(const std::string& desc, const ScriptCommand& command) -{ - return expr(desc, , command.args); -} - std::pair LuaState::expr(const std::string& desc, const std::string& text, const std::vector& args) { @@ -969,8 +1004,8 @@ void LuaState::check_interrupts_counter() } else if (mInterrupts % INTERRUPTS_SUSPEND_LIMIT == 0) { - LL_DEBUGS("Lua") << LLCoros::getName() << " suspending at " << mInterrupts - << " interrupts" << LL_ENDL; + LL_DEBUGS("Lua.suspend") << LLCoros::getName() << " suspending at " + << mInterrupts << " interrupts" << LL_ENDL; llcoro::suspend(); } } @@ -1066,18 +1101,6 @@ std::pair LuaFunction::getState() return { registry, lookup }; } -/***************************************************************************** -* LuaCommand -*****************************************************************************/ -LuaCommand::LuaCommand(const std::string& command) -{ -} - -bool LuaCommand::found() const -{ - return ; -} - /***************************************************************************** * source_path() *****************************************************************************/ -- cgit v1.2.3 From 34af8cc936eba7f058bf02819f736f1547c39525 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Thu, 26 Sep 2024 22:39:02 +0300 Subject: get rid of extra LL in help text --- indra/llcommon/lua_function.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indra/llcommon/lua_function.cpp') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index a9f88f3170..33666964f7 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -1150,7 +1150,7 @@ lua_function(check_stop, "check_stop(): ensure that a Lua script responds to vie * help() *****************************************************************************/ lua_function(help, - "LL.help(): list viewer's Lua functions\n" + "help(): list viewer's Lua functions\n" "LL.help(function): show help string for specific function") { auto& luapump{ LLEventPumps::instance().obtain("lua output") }; @@ -1210,7 +1210,7 @@ lua_function(help, *****************************************************************************/ lua_function( leaphelp, - "LL.leaphelp(): list viewer's LEAP APIs\n" + "leaphelp(): list viewer's LEAP APIs\n" "LL.leaphelp(api): show help for specific api string name") { LLSD request; -- cgit v1.2.3