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/CMakeLists.txt | 4 + indra/llcommon/lua_function.cpp | 575 +++++++++++++++++++++++++++++ indra/llcommon/lua_function.h | 193 ++++++++++ indra/llcommon/lualistener.cpp | 103 ++++++ indra/llcommon/lualistener.h | 77 ++++ indra/newview/llluamanager.cpp | 798 +--------------------------------------- 6 files changed, 958 insertions(+), 792 deletions(-) create mode 100644 indra/llcommon/lua_function.cpp create mode 100644 indra/llcommon/lua_function.h create mode 100644 indra/llcommon/lualistener.cpp create mode 100644 indra/llcommon/lualistener.h diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 5f4ed2fffa..990d661e38 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -107,6 +107,8 @@ set(llcommon_SOURCE_FILES lluriparser.cpp lluuid.cpp llworkerthread.cpp + lua_function.cpp + lualistener.cpp hbxxh.cpp u64.cpp threadpool.cpp @@ -249,6 +251,8 @@ set(llcommon_HEADER_FILES llwin32headers.h llwin32headerslean.h llworkerthread.h + lua_function.h + lualistener.h hbxxh.h lockstatic.h stdtypes.h 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; +} diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h new file mode 100644 index 0000000000..e2bd58e80a --- /dev/null +++ b/indra/llcommon/lua_function.h @@ -0,0 +1,193 @@ +/** + * @file lua_function.h + * @author Nat Goodspeed + * @date 2024-02-05 + * @brief Definitions useful for coding a new Luau entry point into C++ + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LUA_FUNCTION_H) +#define LL_LUA_FUNCTION_H + +#include "luau/luacode.h" +#include "luau/lua.h" +#include "luau/luaconf.h" +#include "luau/lualib.h" +#include "stringize.h" + +#define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n))) +#define lua_rawlen lua_objlen + +namespace lluau +{ + // luau defines luaL_error() as void, but we want to use the Lua idiom of + // 'return error(...)'. Wrap luaL_error() in an int function. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wformat-security" + template + int error(lua_State* L, const char* format, Args&&... args) + { + luaL_error(L, format, std::forward(args)...); + return 0; + } +#pragma clang diagnostic pop + + // luau removed lua_dostring(), but since we perform the equivalent luau + // sequence in multiple places, encapsulate it. desc and text are strings + // rather than string_views because dostring() needs pointers to nul- + // terminated char arrays. + int dostring(lua_State* L, const std::string& desc, const std::string& text); +} // namespace lluau + +std::string lua_tostdstring(lua_State* L, int index); +void lua_pushstdstring(lua_State* L, const std::string& str); +LLSD lua_tollsd(lua_State* L, int index); +void lua_pushllsd(lua_State* L, const LLSD& data); + +/** + * RAII class to manage the lifespan of a lua_State + */ +class LuaState +{ +public: + typedef std::function script_finished_fn; + + LuaState(const std::string_view& desc, script_finished_fn cb); + + LuaState(const LuaState&) = delete; + LuaState& operator=(const LuaState&) = delete; + + ~LuaState(); + + bool checkLua(int r); + + operator lua_State*() const { return mState; } + +private: + std::string mDesc; + script_finished_fn mCallback; + lua_State* mState; + std::string mError; +}; + +/** + * LuaPopper is an RAII struct whose role is to pop some number of entries + * from the Lua stack if the calling function exits early. + */ +struct LuaPopper +{ + LuaPopper(lua_State* L, int count): + mState(L), + mCount(count) + {} + + LuaPopper(const LuaPopper&) = delete; + LuaPopper& operator=(const LuaPopper&) = delete; + + ~LuaPopper(); + + void disarm() { set(0); } + void set(int count) { mCount = count; } + + lua_State* mState; + int mCount; +}; + +/** + * LuaFunction is a base class containing a static registry of its static + * subclass call() methods. call() is NOT virtual: instead, each subclass + * constructor passes a pointer to its distinct call() method to the base- + * class constructor, along with a name by which to register that method. + * + * The init() method walks the registry and registers each such name with the + * passed lua_State. + */ +class LuaFunction +{ +public: + LuaFunction(const std::string_view& name, lua_CFunction function); + + static void init(lua_State* L); + + static lua_CFunction get(const std::string& key); + +private: + using Registry = std::map; + static Registry& getRegistry(); +}; + +/** + * lua_function(name) is a macro to facilitate defining C++ functions + * available to Lua. It defines a subclass of LuaFunction and declares a + * static instance of that subclass, thereby forcing the compiler to call its + * constructor at module initialization time. The constructor passes the + * stringized instance name to its LuaFunction base-class constructor, along + * with a pointer to the static subclass call() method. It then emits the + * call() method definition header, to be followed by a method body enclosed + * in curly braces as usual. + */ +#define lua_function(name) \ +static struct name##_ : public LuaFunction \ +{ \ + name##_(): LuaFunction(#name, &call) {} \ + static int call(lua_State* L); \ +} name; \ +int name##_::call(lua_State* L) +// { +// ... supply method body here, referencing 'L' ... +// } + +// Usage: std::cout << lua_what(L, stackindex) << ...; +// Reports on the Lua value found at the passed stackindex. +// If cast to std::string, returns the corresponding string value. +class lua_what +{ +public: + lua_what(lua_State* state, int idx): + L(state), + index(idx) + {} + + friend std::ostream& operator<<(std::ostream& out, const lua_what& self); + + operator std::string() const { return stringize(*this); } + +private: + lua_State* L; + int index; +}; + +// Usage: std::cout << lua_stack(L) << ...; +// Reports on the contents of the Lua stack. +// If cast to std::string, returns the corresponding string value. +class lua_stack +{ +public: + lua_stack(lua_State* state): + L(state) + {} + + friend std::ostream& operator<<(std::ostream& out, const lua_stack& self); + + operator std::string() const { return stringize(*this); } + +private: + lua_State* L; +}; + +// log exit from any block declaring an instance of DebugExit, regardless of +// how control leaves that block +struct DebugExit +{ + DebugExit(const std::string& name): mName(name) {} + DebugExit(const DebugExit&) = delete; + DebugExit& operator=(const DebugExit&) = delete; + ~DebugExit(); + + std::string mName; +}; + +#endif /* ! defined(LL_LUA_FUNCTION_H) */ diff --git a/indra/llcommon/lualistener.cpp b/indra/llcommon/lualistener.cpp new file mode 100644 index 0000000000..0fa03ffb3b --- /dev/null +++ b/indra/llcommon/lualistener.cpp @@ -0,0 +1,103 @@ +/** + * @file lualistener.cpp + * @author Nat Goodspeed + * @date 2024-02-06 + * @brief Implementation for lualistener. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "lualistener.h" +// STL headers +// std headers +#include // std::rand() +#include // std::memcpy() +// external library headers +#include "luau/lua.h" +// other Linden headers +#include "llerror.h" +#include "llleaplistener.h" +#include "lua_function.h" + +LuaListener::LuaListener(lua_State* L): + super(getUniqueKey()), + mListener( + new LLLeapListener( + [L](LLEventPump& pump, const std::string& listener) + { return connect(L, pump, listener); })) +{ + mReplyConnection = connect(L, mReplyPump, "LuaListener"); +} + +LuaListener::~LuaListener() +{ + LL_DEBUGS("Lua") << "~LuaListener('" << mReplyPump.getName() << "')" << LL_ENDL; +} + +int LuaListener::getUniqueKey() +{ + // Find a random key that does NOT already correspond to a LuaListener + // instance. Passing a duplicate key to LLInstanceTracker would do Bad + // Things. + int key; + do + { + key = std::rand(); + } while (LuaListener::getInstance(key)); + // This is theoretically racy, if we were instantiating new + // LuaListeners on multiple threads. Don't. + return key; +} + +LLBoundListener LuaListener::connect(lua_State* L, LLEventPump& pump, const std::string& listener) +{ + return pump.listen( + listener, + [L, pumpname=pump.getName()](const LLSD& data) + { return call_lua(L, pumpname, data); }); +} + +bool LuaListener::call_lua(lua_State* L, const std::string& pump, const LLSD& data) +{ + LL_INFOS("Lua") << "LuaListener::call_lua('" << pump << "', " << data << ")" << LL_ENDL; + if (! lua_checkstack(L, 3)) + { + LL_WARNS("Lua") << "Cannot extend Lua stack to call listen_events() callback" + << LL_ENDL; + return false; + } + // push the registered Lua callback function stored in our registry as + // "event.function" + lua_getfield(L, LUA_REGISTRYINDEX, "event.function"); + llassert(lua_isfunction(L, -1)); + // pass pump name + lua_pushstdstring(L, pump); + // then the data blob + lua_pushllsd(L, data); + // call the registered Lua listener function; allow it to return bool; + // no message handler + auto status = lua_pcall(L, 2, 1, 0); + bool result{ false }; + if (status != LUA_OK) + { + LL_WARNS("Lua") << "Error in listen_events() callback: " + << lua_tostdstring(L, -1) << LL_ENDL; + } + else + { + result = lua_toboolean(L, -1); + } + // discard either the error message or the bool return value + lua_pop(L, 1); + return result; +} + +std::string LuaListener::getCommandName() const +{ + return mListener->getPumpName(); +} diff --git a/indra/llcommon/lualistener.h b/indra/llcommon/lualistener.h new file mode 100644 index 0000000000..550f029158 --- /dev/null +++ b/indra/llcommon/lualistener.h @@ -0,0 +1,77 @@ +/** + * @file lualistener.h + * @author Nat Goodspeed + * @date 2024-02-06 + * @brief Define LuaListener class + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LUALISTENER_H) +#define LL_LUALISTENER_H + +#include "llevents.h" +#include "llinstancetracker.h" +#include "lluuid.h" +#include // std::unique_ptr + +#ifdef LL_TEST +#include "lleventfilter.h" +#endif + +class lua_State; +class LLLeapListener; + +/** + * LuaListener is based on LLLeap. It serves an analogous function. + * + * Each LuaListener instance has an int key, generated randomly to + * inconvenience malicious Lua scripts wanting to mess with others. The idea + * is that a given lua_State stores in its Registry: + * - "event.listener": the int key of the corresponding LuaListener, if any + * - "event.function": the Lua function to be called with incoming events + * The original thought was that LuaListener would itself store the Lua + * function -- but surprisingly, there is no C/C++ type in the API that stores + * a Lua function. + * + * (We considered storing in "event.listener" the LuaListener pointer itself + * as a light userdata, but the problem would be if Lua code overwrote that. + * We want to prevent any Lua script from crashing the viewer, intentionally + * or otherwise. Safer to use a key lookup.) + * + * Like LLLeap, each LuaListener instance also has an associated + * LLLeapListener to respond to LLEventPump management commands. + */ +class LuaListener: public LLInstanceTracker +{ + using super = LLInstanceTracker; +public: + LuaListener(lua_State* L); + + LuaListener(const LuaListener&) = delete; + LuaListener& operator=(const LuaListener&) = delete; + + ~LuaListener(); + + std::string getReplyName() const { return mReplyPump.getName(); } + std::string getCommandName() const; + +private: + static int getUniqueKey(); + + static LLBoundListener connect(lua_State* L, LLEventPump& pump, const std::string& listener); + + static bool call_lua(lua_State* L, const std::string& pump, const LLSD& data); + +#ifndef LL_TEST + LLEventStream mReplyPump{ LLUUID::generateNewID().asString() }; +#else + LLEventLogProxyFor mReplyPump{ "luapump", false }; +#endif + LLTempBoundListener mReplyConnection; + std::unique_ptr mListener; +}; + +#endif /* ! defined(LL_LUALISTENER_H) */ diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index 3e3ce45cb0..596d8cab56 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -28,14 +28,10 @@ #include "llviewerprecompiledheaders.h" #include "llluamanager.h" -#include "hexdump.h" #include "llerror.h" #include "lleventcoro.h" -#include "lleventfilter.h" -#include "llevents.h" -#include "llinstancetracker.h" -#include "llleaplistener.h" -#include "lluuid.h" +#include "lua_function.h" +#include "lualistener.h" #include "stringize.h" // skip all these link dependencies for integration testing @@ -55,230 +51,10 @@ extern LLUIListener sUIListener; #include "luau/luaconf.h" #include "luau/lualib.h" -#define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n))) -#define lua_rawlen lua_objlen - -#include -#include // std::rand() -#include // std::memcpy() -#include -#include // std::unique_ptr #include #include #include -std::string lua_tostdstring(lua_State* L, int index); -void lua_pushstdstring(lua_State* L, const std::string& str); -LLSD lua_tollsd(lua_State* L, int index); -void lua_pushllsd(lua_State* L, const LLSD& data); - -/** - * LuaListener is based on LLLeap. It serves an analogous function. - * - * Each LuaListener instance has an int key, generated randomly to - * inconvenience malicious Lua scripts wanting to mess with others. The idea - * is that a given lua_State stores in its Registry: - * - "event.listener": the int key of the corresponding LuaListener, if any - * - "event.function": the Lua function to be called with incoming events - * The original thought was that LuaListener would itself store the Lua - * function -- but surprisingly, there is no C/C++ type in the API that stores - * a Lua function. - * - * (We considered storing in "event.listener" the LuaListener pointer itself - * as a light userdata, but the problem would be if Lua code overwrote that. - * We want to prevent any Lua script from crashing the viewer, intentionally - * or otherwise. Safer to use a key lookup.) - * - * Like LLLeap, each LuaListener instance also has an associated - * LLLeapListener to respond to LLEventPump management commands. - */ -class LuaListener: public LLInstanceTracker -{ - using super = LLInstanceTracker; -public: - LuaListener(lua_State* L): - super(getUniqueKey()), - mListener( - new LLLeapListener( - [L](LLEventPump& pump, const std::string& listener) - { return connect(L, pump, listener); })) - { - mReplyConnection = connect(L, mReplyPump, "LuaListener"); - } - - LuaListener(const LuaListener&) = delete; - LuaListener& operator=(const LuaListener&) = delete; - - ~LuaListener() - { - LL_DEBUGS("Lua") << "~LuaListener('" << mReplyPump.getName() << "')" << LL_ENDL; - } - - std::string getReplyName() const { return mReplyPump.getName(); } - std::string getCommandName() const { return mListener->getPumpName(); } - -private: - static int getUniqueKey() - { - // Find a random key that does NOT already correspond to a LuaListener - // instance. Passing a duplicate key to LLInstanceTracker would do Bad - // Things. - int key; - do - { - key = std::rand(); - } while (LuaListener::getInstance(key)); - // This is theoretically racy, if we were instantiating new - // LuaListeners on multiple threads. Don't. - return key; - } - - static LLBoundListener connect(lua_State* L, LLEventPump& pump, const std::string& listener) - { - return pump.listen( - listener, - [L, pumpname=pump.getName()](const LLSD& data) - { return call_lua(L, pumpname, data); }); - } - - static bool call_lua(lua_State* L, const std::string& pump, const LLSD& data) - { - LL_INFOS("Lua") << "LuaListener::call_lua('" << pump << "', " << data << ")" << LL_ENDL; - if (! lua_checkstack(L, 3)) - { - LL_WARNS("Lua") << "Cannot extend Lua stack to call listen_events() callback" - << LL_ENDL; - return false; - } - // push the registered Lua callback function stored in our registry as - // "event.function" - lua_getfield(L, LUA_REGISTRYINDEX, "event.function"); - llassert(lua_isfunction(L, -1)); - // pass pump name - lua_pushstdstring(L, pump); - // then the data blob - lua_pushllsd(L, data); - // call the registered Lua listener function; allow it to return bool; - // no message handler - auto status = lua_pcall(L, 2, 1, 0); - bool result{ false }; - if (status != LUA_OK) - { - LL_WARNS("Lua") << "Error in listen_events() callback: " - << lua_tostdstring(L, -1) << LL_ENDL; - } - else - { - result = lua_toboolean(L, -1); - } - // discard either the error message or the bool return value - lua_pop(L, 1); - return result; - } - -#ifndef LL_TEST - LLEventStream mReplyPump{ LLUUID::generateNewID().asString() }; -#else - LLEventLogProxyFor mReplyPump{ "luapump", false }; -#endif - LLTempBoundListener mReplyConnection; - std::unique_ptr mListener; -}; - -/** - * LuaPopper is an RAII struct whose role is to pop some number of entries - * from the Lua stack if the calling function exits early. - */ -struct LuaPopper -{ - LuaPopper(lua_State* L, int count): - mState(L), - mCount(count) - {} - - LuaPopper(const LuaPopper&) = delete; - LuaPopper& operator=(const LuaPopper&) = delete; - - ~LuaPopper() - { - if (mCount) - { - lua_pop(mState, mCount); - } - } - - void disarm() { set(0); } - void set(int count) { mCount = count; } - - lua_State* mState; - int mCount; -}; - -/** - * LuaFunction is a base class containing a static registry of its static - * subclass call() methods. call() is NOT virtual: instead, each subclass - * constructor passes a pointer to its distinct call() method to the base- - * class constructor, along with a name by which to register that method. - * - * The init() method walks the registry and registers each such name with the - * passed lua_State. - */ -class LuaFunction -{ -public: - LuaFunction(const std::string_view& name, lua_CFunction function) - { - getRegistry().emplace(name, function); - } - - static void init(lua_State* L) - { - for (const auto& pair: getRegistry()) - { - lua_register(L, pair.first.c_str(), pair.second); - } - } - - static lua_CFunction 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; - } - -private: - using Registry = std::map; - static Registry& getRegistry() - { - // use a function-local static to ensure it's initialized - static Registry registry; - return registry; - } -}; - -/** - * lua_function(name) is a macro to facilitate defining C++ functions - * available to Lua. It defines a subclass of LuaFunction and declares a - * static instance of that subclass, thereby forcing the compiler to call its - * constructor at module initialization time. The constructor passes the - * stringized instance name to its LuaFunction base-class constructor, along - * with a pointer to the static subclass call() method. It then emits the - * call() method definition header, to be followed by a method body enclosed - * in curly braces as usual. - */ -#define lua_function(name) \ -static struct name##_ : public LuaFunction \ -{ \ - name##_(): LuaFunction(#name, &call) {} \ - static int call(lua_State* L); \ -} name; \ -int name##_::call(lua_State* L) -// { -// ... supply method body here, referencing 'L' ... -// } - /* // This function consumes ALL Lua stack arguments and returns concatenated // message string @@ -484,80 +260,6 @@ lua_function(await_event) return 1; } -/** - * RAII class to manage the lifespan of a lua_State - */ -class LuaState -{ -public: - LuaState(const std::string_view& desc, LLLUAmanager::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(const LuaState&) = delete; - LuaState& operator=(const LuaState&) = delete; - - ~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 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; - } - - operator lua_State*() const { return mState; } - -private: - std::string mDesc; - LLLUAmanager::script_finished_fn mCallback; - lua_State* mState; - std::string mError; -}; - void LLLUAmanager::runScriptFile(const std::string& filename, script_finished_fn cb) { std::string desc{ stringize("runScriptFile('", filename, "')") }; @@ -580,16 +282,9 @@ void LLLUAmanager::runScriptFile(const std::string& filename, script_finished_fn if (in_file.is_open()) { - std::string text((std::istreambuf_iterator(in_file)), std::istreambuf_iterator()); - - size_t bytecodeSize = 0; - char *bytecode = luau_compile(text.c_str(), text.length(), NULL, &bytecodeSize); - L.checkLua(luau_load(L, desc.c_str(), bytecode, bytecodeSize, 0)); - free(bytecode); - - L.checkLua(lua_pcall(L, 0, 0, 0)); - - in_file.close(); + std::string text{std::istreambuf_iterator(in_file), + std::istreambuf_iterator()}; + L.checkLua(lluau::dostring(L, desc, text)); } else { @@ -613,12 +308,7 @@ void LLLUAmanager::runScriptLine(const std::string& cmd, script_finished_fn cb) LLCoros::instance().launch(desc, [desc, cmd, cb]() { LuaState L(desc, cb); - - size_t bytecodeSize = 0; - char *bytecode = luau_compile(cmd.c_str(), cmd.length(), NULL, &bytecodeSize); - L.checkLua(luau_load(L, desc.c_str(), bytecode, bytecodeSize, 0)); - free(bytecode); - L.checkLua(lua_pcall(L, 0, 0, 0)); + L.checkLua(lluau::dostring(L, desc, cmd)); }); } @@ -642,479 +332,3 @@ void LLLUAmanager::runScriptOnLogin() runScriptFile(filename); #endif // ! LL_TEST } - -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()); -} - -// Usage: std::cout << lua_what(L, stackindex) << ...; -// Reports on the Lua value found at the passed stackindex. -// If cast to std::string, returns the corresponding string value. -class lua_what -{ -public: - lua_what(lua_State* state, int idx): - L(state), - index(idx) - {} - - friend 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; - } - - operator std::string() const { return stringize(*this); } - -private: - lua_State* L; - int index; -}; - -// Usage: std::cout << lua_stack(L) << ...; -// Reports on the contents of the Lua stack. -// If cast to std::string, returns the corresponding string value. -class lua_stack -{ -public: - lua_stack(lua_State* state): - L(state) - {} - - friend 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; - } - - operator std::string() const { return stringize(*this); } - -private: - lua_State* L; -}; - -// log exit from any block declaring an instance of DebugExit, regardless of -// how control leaves that block -struct DebugExit -{ - DebugExit(const std::string& name): mName(name) {} - DebugExit(const DebugExit&) = delete; - DebugExit& operator=(const DebugExit&) = delete; - ~DebugExit() - { - LL_DEBUGS("Lua") << "exit " << mName << LL_ENDL; - } - - std::string mName; -}; - -// 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 - luaL_error(L, "Expected integer array key, got %f instead", lua_tonumber(L, -2)); - return 0; - } - if (intkey < 1) - { - luaL_error(L, "array key %d out of bounds", int(intkey)); - return 0; - } - - keys.push_back(LLSD::Integer(intkey)); - break; - } - - case LUA_TSTRING: - // break out strings specially to report the value - luaL_error(L, "Cannot convert string array key '%s' to LLSD", lua_tostring(L, -2)); - return 0; - - default: - luaL_error(L, "Cannot convert %s array key to LLSD", lua_typename(L, arraykeytype)); - return 0; - } - - // 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) - { - luaL_error(L, "Conversion from Lua to LLSD array limited to %d entries", int(array_max)); - return 0; - } - // 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. - luaL_error(L, "Gaps in Lua table too large for conversion to LLSD array"); - return 0; - } - 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) - { - luaL_error(L, "Cannot convert %s map key to LLSD", lua_typename(L, mapkeytype)); - return 0; - } - - 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 - luaL_error(L, "Cannot convert %s table key to LLSD", lua_typename(L, firstkeytype)); - return 0; - } - } - - 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. - luaL_error(L, "Cannot convert type %s to LLSD", luaL_typename(L, index)); - return 0; - } -} - -// 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; - } - } -} -- 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 ++++++++++++++-- indra/llcommon/lua_function.h | 30 ++++++++----- indra/newview/llluamanager.cpp | 93 +++++++++++++++++++++++++++++++++-------- indra/newview/llluamanager.h | 30 +++++++++++-- 4 files changed, 156 insertions(+), 34 deletions(-) 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() { diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index e2bd58e80a..ea9f2ebdf8 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -17,6 +17,7 @@ #include "luau/luaconf.h" #include "luau/lualib.h" #include "stringize.h" +#include // std::pair #define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n))) #define lua_rawlen lua_objlen @@ -55,19 +56,28 @@ class LuaState public: typedef std::function script_finished_fn; - LuaState(const std::string_view& desc, script_finished_fn cb); + LuaState(script_finished_fn cb={}); LuaState(const LuaState&) = delete; LuaState& operator=(const LuaState&) = delete; ~LuaState(); - bool checkLua(int r); + bool checkLua(const std::string& desc, int r); + + // expr() is for when we want to capture any results left on the stack + // by a Lua expression, possibly including multiple return values. + // int < 0 means error, and LLSD::asString() is the error message. + // int == 0 with LLSD::isUndefined() means the Lua expression returned no + // results. + // int == 1 means the Lua expression returned one result. + // int > 1 with LLSD::isArray() means the Lua expression returned + // multiple results, represented as the entries of the array. + std::pair expr(const std::string& desc, const std::string& text); operator lua_State*() const { return mState; } private: - std::string mDesc; script_finished_fn mCallback; lua_State* mState; std::string mError; @@ -129,13 +139,13 @@ private: * call() method definition header, to be followed by a method body enclosed * in curly braces as usual. */ -#define lua_function(name) \ -static struct name##_ : public LuaFunction \ -{ \ - name##_(): LuaFunction(#name, &call) {} \ - static int call(lua_State* L); \ -} name; \ -int name##_::call(lua_State* L) +#define lua_function(name) \ +static struct name##_luadecl : public LuaFunction \ +{ \ + name##_luadecl(): LuaFunction(#name, &call) {} \ + static int call(lua_State* L); \ +} name##_luadef; \ +int name##_luadecl::call(lua_State* L) // { // ... supply method body here, referencing 'L' ... // } diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index 596d8cab56..44c7a6ba3b 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -28,6 +28,7 @@ #include "llviewerprecompiledheaders.h" #include "llluamanager.h" +#include "llcoros.h" #include "llerror.h" #include "lleventcoro.h" #include "lua_function.h" @@ -55,6 +56,14 @@ extern LLUIListener sUIListener; #include #include +lua_function(sleep) +{ + F32 seconds = lua_tonumber(L, -1); + lua_pop(L, 1); + llcoro::suspendUntilTimeout(seconds); + return 0; +}; + /* // This function consumes ALL Lua stack arguments and returns concatenated // message string @@ -262,21 +271,34 @@ lua_function(await_event) void LLLUAmanager::runScriptFile(const std::string& filename, script_finished_fn cb) { - std::string desc{ stringize("runScriptFile('", filename, "')") }; - LLCoros::instance().launch(desc, [desc, filename, cb]() - { - LuaState L(desc, cb); + // A script_finished_fn is used to initialize the LuaState. + // It will be called when the LuaState is destroyed. + LuaState L(cb); + runScriptFile(L, filename); +} - auto LUA_sleep_func = [](lua_State *L) - { - F32 seconds = lua_tonumber(L, -1); - lua_pop(L, 1); - llcoro::suspendUntilTimeout(seconds); - return 0; - }; +void LLLUAmanager::runScriptFile(const std::string& filename, script_result_fn cb) +{ + LuaState L; + // A script_result_fn will be called when LuaState::expr() completes. + runScriptFile(L, filename, cb); +} - lua_register(L, "sleep", LUA_sleep_func); +std::pair LLLUAmanager::waitScriptFile(LuaState& L, const std::string& filename) +{ + LLCoros::Promise> promise; + auto future{ LLCoros::getFuture(promise) }; + runScriptFile(L, filename, + [&promise](int count, LLSD result) + { promise.set_value({ count, result }); }); + return future.get(); +} +void LLLUAmanager::runScriptFile(LuaState& L, const std::string& filename, script_result_fn cb) +{ + std::string desc{ stringize("runScriptFile('", filename, "')") }; + LLCoros::instance().launch(desc, [&L, desc, filename, cb]() + { llifstream in_file; in_file.open(filename.c_str()); @@ -284,16 +306,50 @@ void LLLUAmanager::runScriptFile(const std::string& filename, script_finished_fn { std::string text{std::istreambuf_iterator(in_file), std::istreambuf_iterator()}; - L.checkLua(lluau::dostring(L, desc, text)); + auto [count, result] = L.expr(desc, text); + if (cb) + { + cb(count, result); + } } else { - LL_WARNS("Lua") << "unable to open script file '" << filename << "'" << LL_ENDL; + auto msg{ stringize("unable to open script file '", filename, "'") }; + LL_WARNS("Lua") << msg << LL_ENDL; + if (cb) + { + cb(-1, msg); + } } }); } void LLLUAmanager::runScriptLine(const std::string& cmd, script_finished_fn cb) +{ + // A script_finished_fn is used to initialize the LuaState. + // It will be called when the LuaState is destroyed. + LuaState L(cb); + runScriptLine(L, cmd); +} + +void LLLUAmanager::runScriptLine(const std::string& cmd, script_result_fn cb) +{ + LuaState L; + // A script_result_fn will be called when LuaState::expr() completes. + runScriptLine(L, cmd, cb); +} + +std::pair LLLUAmanager::waitScriptLine(LuaState& L, const std::string& cmd) +{ + LLCoros::Promise> promise; + auto future{ LLCoros::getFuture(promise) }; + runScriptLine(L, cmd, + [&promise](int count, LLSD result) + { promise.set_value({ count, result }); }); + return future.get(); +} + +void LLLUAmanager::runScriptLine(LuaState& L, const std::string& cmd, script_result_fn cb) { // find a suitable abbreviation for the cmd string std::string_view shortcmd{ cmd }; @@ -305,10 +361,13 @@ void LLLUAmanager::runScriptLine(const std::string& cmd, script_finished_fn cb) shortcmd = stringize(shortcmd.substr(0, shortlen), "..."); std::string desc{ stringize("runScriptLine('", shortcmd, "')") }; - LLCoros::instance().launch(desc, [desc, cmd, cb]() + LLCoros::instance().launch(desc, [&L, desc, cmd, cb]() { - LuaState L(desc, cb); - L.checkLua(lluau::dostring(L, desc, cmd)); + auto [count, result] = L.expr(desc, cmd); + if (cb) + { + cb(count, result); + } }); } diff --git a/indra/newview/llluamanager.h b/indra/newview/llluamanager.h index 08d9876ce2..bed12e358a 100644 --- a/indra/newview/llluamanager.h +++ b/indra/newview/llluamanager.h @@ -27,19 +27,43 @@ #ifndef LL_LLLUAMANAGER_H #define LL_LLLUAMANAGER_H +#include "llsd.h" #include #include +#include // std::pair + +class LuaState; class LLLUAmanager { public: + // Pass a callback with this signature to obtain the error message, if + // any, from running a script or source string. Empty msg means success. typedef std::function script_finished_fn; + // Pass a callback with this signature to obtain the result, if any, of + // running a script or source string. + // count < 0 means error, and result.asString() is the error message. + // count == 0 with result.isUndefined() means the script returned no results. + // count == 1 means the script returned one result. + // count > 1 with result.isArray() means the script returned multiple + // results, represented as the entries of the result array. + typedef std::function script_result_fn; + + static void runScriptFile(const std::string &filename, script_finished_fn cb = {}); + static void runScriptFile(const std::string &filename, script_result_fn cb); + static void runScriptFile(LuaState& L, const std::string &filename, script_result_fn cb = {}); + // Run a Lua script file, and pause the calling coroutine until it completes. + // The return value is the (count, result) pair described above. + static std::pair waitScriptFile(LuaState& L, const std::string& filename); - static void runScriptFile(const std::string &filename, script_finished_fn cb = script_finished_fn()); - static void runScriptLine(const std::string &cmd, script_finished_fn cb = script_finished_fn()); + static void runScriptLine(const std::string &cmd, script_finished_fn cb = {}); + static void runScriptLine(const std::string &cmd, script_result_fn cb); + static void runScriptLine(LuaState& L, const std::string &cmd, script_result_fn cb = {}); + // Run a Lua expression string, and pause the calling coroutine until it completes. + // The return value is the (count, result) pair described above. + static std::pair waitScriptLine(LuaState& L, const std::string& cmd); static void runScriptOnLogin(); }; - #endif -- cgit v1.2.3 From c9ae127ce1482ab2bc9c0968c97a76414e798d23 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 7 Feb 2024 21:56:42 -0500 Subject: Fix up a few longstanding missing #includes. --- indra/llcommon/llformat.h | 2 ++ indra/llcommon/llrefcount.h | 1 + indra/llcommon/llstring.h | 1 + 3 files changed, 4 insertions(+) diff --git a/indra/llcommon/llformat.h b/indra/llcommon/llformat.h index fb8e7cd045..97ea3b7b78 100644 --- a/indra/llcommon/llformat.h +++ b/indra/llcommon/llformat.h @@ -28,6 +28,8 @@ #ifndef LL_LLFORMAT_H #define LL_LLFORMAT_H +#include "llpreprocessor.h" + // Use as follows: // LL_INFOS() << llformat("Test:%d (%.2f %.2f)", idx, x, y) << LL_ENDL; // diff --git a/indra/llcommon/llrefcount.h b/indra/llcommon/llrefcount.h index 2080da1565..5f7e668c7f 100644 --- a/indra/llcommon/llrefcount.h +++ b/indra/llcommon/llrefcount.h @@ -29,6 +29,7 @@ #include #include #include "llatomic.h" +#include "llerror.h" class LLMutex; diff --git a/indra/llcommon/llstring.h b/indra/llcommon/llstring.h index 1fd6cac14a..b43093fbfc 100644 --- a/indra/llcommon/llstring.h +++ b/indra/llcommon/llstring.h @@ -38,6 +38,7 @@ #include #include #include "llformat.h" +#include "stdtypes.h" #if LL_LINUX #include -- 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(-) 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 4ecfd706bce5c45556eaf2833f89fd9f2f5b07a9 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 7 Feb 2024 22:24:52 -0500 Subject: Add LLLUAmanager::startScriptFile(), startScriptLine() functions. Break out for LLLUAmanager consumers the promise/future semantics of waitScriptFile() and waitScriptLine(). startScriptMumble() uses runScriptMumble() to launch a coroutine to run the specified Lua script or chunk, providing an internal adapter callback to set a promise on completion. It then returns to its caller the future obtained from that promise. This allows a caller to call startScriptMumble(), run in parallel with the Lua coroutine for a while and then call get() on the returned future to wait for results. waitScriptMumble() is then trivially implemented using startScriptMumble(). Fix runScriptLine()'s logic to abbreviate the passed Lua chunk for use as the description. We were erroneously assigning back through a string_view of the parameter, which overwrote a temporary string in the argument list. With Lua 5.4, listen_events() tried to discover the main "thread" (Lua coroutine) associated with the current lua_State so we could call async callbacks on that thread. Luau doesn't seem to provide that feature, so run callbacks on whichever thread calls listen_events(). Reinstate original multi-argument lua_print_msg(), tweaked to avoid the Lua 5.4 lua_rotate() function, which is missing from Luau. --- indra/newview/llluamanager.cpp | 107 +++++++++++++++++++++-------------------- indra/newview/llluamanager.h | 25 ++++++++-- 2 files changed, 76 insertions(+), 56 deletions(-) diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index 44c7a6ba3b..fac66003a1 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -64,13 +64,13 @@ lua_function(sleep) return 0; }; -/* // This function consumes ALL Lua stack arguments and returns concatenated // message string -std::string lua_print_msg_args(lua_State* L, const std::string_view& level) +std::string lua_print_msg(lua_State* L, const std::string_view& level) { - // On top of existing Lua arguments, push 'where' info - luaL_checkstack(L, 1, nullptr); + // On top of existing Lua arguments, we're going to push tostring() and + // duplicate each existing stack entry so we can stringize each one. + luaL_checkstack(L, 2, nullptr); luaL_where(L, 1); // start with the 'where' info at the top of the stack std::ostringstream out; @@ -79,7 +79,7 @@ std::string lua_print_msg_args(lua_State* L, const std::string_view& level) const char* sep = ""; // 'where' info ends with ": " // now iterate over arbitrary args, calling Lua tostring() on each and // concatenating with separators - for (int p = 1; p <= lua_gettop(L); ++p) + for (int p = 1, top = lua_gettop(L); p <= top; ++p) { out << sep; sep = " "; @@ -87,15 +87,15 @@ std::string lua_print_msg_args(lua_State* L, const std::string_view& level) // lua_tostring()! lua_getglobal(L, "tostring"); // Now the stack is arguments 1 .. N, plus tostring(). - // Rotate downwards, producing stack args 2 .. N, tostring(), arg1. - lua_rotate(L, 1, -1); - // pop tostring() and arg1, pushing tostring(arg1) + // Push a copy of the argument at index p. + lua_pushvalue(L, p); + // pop tostring() and arg-p, pushing tostring(arg-p) // (ignore potential error code from lua_pcall() because, if there was // an error, we expect the stack top to be an error message -- which // we'll print) lua_pcall(L, 1, 1, 0); - // stack now holds args 2 .. N, tostring(arg1) out << lua_tostring(L, -1); + lua_pop(L, 1); } // pop everything lua_settop(L, 0); @@ -105,20 +105,6 @@ std::string lua_print_msg_args(lua_State* L, const std::string_view& level) LLEventPumps::instance().obtain("lua output").post(stringize(level, ": ", msg)); return msg; } -*/ - -std::string lua_print_msg(lua_State *L, const std::string_view &level) -{ - lua_getglobal(L, "tostring"); - - lua_pushvalue(L, -1); /* function to be called */ - lua_pushvalue(L, 1); /* value to print */ - lua_call(L, 1, 1); - std::string msg = lua_tostring(L, -1); - - LLEventPumps::instance().obtain("lua output").post(stringize(level, ": ", msg)); - return msg; -} lua_function(print_debug) { @@ -187,19 +173,24 @@ lua_function(listen_events) } luaL_checkstack(L, 2, nullptr); +/*==========================================================================*| // Get the lua_State* for the main thread of this state, in case we were // called from a coroutine thread. We're going to make callbacks into Lua // code, and we want to do it on the main thread rather than a (possibly // suspended) coroutine thread. // Registry table is at pseudo-index LUA_REGISTRYINDEX // Main thread is at registry key LUA_RIDX_MAINTHREAD - auto regtype {lua_rawgeti(L, LUA_REGISTRYINDEX, 1 /*LUA_RIDX_MAINTHREAD*/)}; + auto regtype {lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD)}; // Not finding the main thread at the documented place isn't a user error, // it's a Problem llassert_always(regtype == LUA_TTHREAD); lua_State* mainthread{ lua_tothread(L, -1) }; // pop the main thread lua_pop(L, 1); +|*==========================================================================*/ + // Luau is based on Lua 5.1, and Lua 5.1 apparently provides no way to get + // back to the main thread from a coroutine thread? + lua_State* mainthread{ L }; luaL_checkstack(mainthread, 1, nullptr); LuaListener::ptr_t listener; @@ -284,14 +275,21 @@ void LLLUAmanager::runScriptFile(const std::string& filename, script_result_fn c runScriptFile(L, filename, cb); } -std::pair LLLUAmanager::waitScriptFile(LuaState& L, const std::string& filename) +LLCoros::Future> +LLLUAmanager::startScriptFile(LuaState& L, const std::string& filename) { - LLCoros::Promise> promise; - auto future{ LLCoros::getFuture(promise) }; + // Despite returning from startScriptFile(), we need this Promise to + // remain alive until the callback has fired. + auto promise{ std::make_shared>>() }; runScriptFile(L, filename, - [&promise](int count, LLSD result) - { promise.set_value({ count, result }); }); - return future.get(); + [promise](int count, LLSD result) + { promise->set_value({ count, result }); }); + return LLCoros::getFuture(*promise); +} + +std::pair LLLUAmanager::waitScriptFile(LuaState& L, const std::string& filename) +{ + return startScriptFile(L, filename).get(); } void LLLUAmanager::runScriptFile(LuaState& L, const std::string& filename, script_result_fn cb) @@ -324,46 +322,53 @@ void LLLUAmanager::runScriptFile(LuaState& L, const std::string& filename, scrip }); } -void LLLUAmanager::runScriptLine(const std::string& cmd, script_finished_fn cb) +void LLLUAmanager::runScriptLine(const std::string& chunk, script_finished_fn cb) { // A script_finished_fn is used to initialize the LuaState. // It will be called when the LuaState is destroyed. LuaState L(cb); - runScriptLine(L, cmd); + runScriptLine(L, chunk); } -void LLLUAmanager::runScriptLine(const std::string& cmd, script_result_fn cb) +void LLLUAmanager::runScriptLine(const std::string& chunk, script_result_fn cb) { LuaState L; // A script_result_fn will be called when LuaState::expr() completes. - runScriptLine(L, cmd, cb); + runScriptLine(L, chunk, cb); +} + +LLCoros::Future> +LLLUAmanager::startScriptLine(LuaState& L, const std::string& chunk) +{ + // Despite returning from startScriptLine(), we need this Promise to + // remain alive until the callback has fired. + auto promise{ std::make_shared>>() }; + runScriptLine(L, chunk, + [promise](int count, LLSD result) + { promise->set_value({ count, result }); }); + return LLCoros::getFuture(*promise); } -std::pair LLLUAmanager::waitScriptLine(LuaState& L, const std::string& cmd) +std::pair LLLUAmanager::waitScriptLine(LuaState& L, const std::string& chunk) { - LLCoros::Promise> promise; - auto future{ LLCoros::getFuture(promise) }; - runScriptLine(L, cmd, - [&promise](int count, LLSD result) - { promise.set_value({ count, result }); }); - return future.get(); + return startScriptLine(L, chunk).get(); } -void LLLUAmanager::runScriptLine(LuaState& L, const std::string& cmd, script_result_fn cb) +void LLLUAmanager::runScriptLine(LuaState& L, const std::string& chunk, script_result_fn cb) { - // find a suitable abbreviation for the cmd string - std::string_view shortcmd{ cmd }; + // find a suitable abbreviation for the chunk string + std::string shortchunk{ chunk }; const size_t shortlen = 40; - std::string::size_type eol = shortcmd.find_first_of("\r\n"); + std::string::size_type eol = shortchunk.find_first_of("\r\n"); if (eol != std::string::npos) - shortcmd = shortcmd.substr(0, eol); - if (shortcmd.length() > shortlen) - shortcmd = stringize(shortcmd.substr(0, shortlen), "..."); + shortchunk = shortchunk.substr(0, eol); + if (shortchunk.length() > shortlen) + shortchunk = stringize(shortchunk.substr(0, shortlen), "..."); - std::string desc{ stringize("runScriptLine('", shortcmd, "')") }; - LLCoros::instance().launch(desc, [&L, desc, cmd, cb]() + std::string desc{ stringize("runScriptLine('", shortchunk, "')") }; + LLCoros::instance().launch(desc, [&L, desc, chunk, cb]() { - auto [count, result] = L.expr(desc, cmd); + auto [count, result] = L.expr(desc, chunk); if (cb) { cb(count, result); diff --git a/indra/newview/llluamanager.h b/indra/newview/llluamanager.h index bed12e358a..12284cd0e4 100644 --- a/indra/newview/llluamanager.h +++ b/indra/newview/llluamanager.h @@ -27,6 +27,7 @@ #ifndef LL_LLLUAMANAGER_H #define LL_LLLUAMANAGER_H +#include "llcoros.h" #include "llsd.h" #include #include @@ -52,16 +53,30 @@ public: static void runScriptFile(const std::string &filename, script_finished_fn cb = {}); static void runScriptFile(const std::string &filename, script_result_fn cb); static void runScriptFile(LuaState& L, const std::string &filename, script_result_fn cb = {}); + // Start running a Lua script file, returning an LLCoros::Future whose + // get() method will pause the calling coroutine until it can deliver the + // (count, result) pair described above. Between startScriptFile() and + // Future::get(), the caller and the Lua script coroutine will run + // concurrently. + static LLCoros::Future> + startScriptFile(LuaState& L, const std::string& filename); // Run a Lua script file, and pause the calling coroutine until it completes. // The return value is the (count, result) pair described above. static std::pair waitScriptFile(LuaState& L, const std::string& filename); - static void runScriptLine(const std::string &cmd, script_finished_fn cb = {}); - static void runScriptLine(const std::string &cmd, script_result_fn cb); - static void runScriptLine(LuaState& L, const std::string &cmd, script_result_fn cb = {}); - // Run a Lua expression string, and pause the calling coroutine until it completes. + static void runScriptLine(const std::string &chunk, script_finished_fn cb = {}); + static void runScriptLine(const std::string &chunk, script_result_fn cb); + static void runScriptLine(LuaState& L, const std::string &chunk, script_result_fn cb = {}); + // Start running a Lua chunk, returning an LLCoros::Future whose + // get() method will pause the calling coroutine until it can deliver the + // (count, result) pair described above. Between startScriptLine() and + // Future::get(), the caller and the Lua script coroutine will run + // concurrently. + static LLCoros::Future> + startScriptLine(LuaState& L, const std::string& chunk); + // Run a Lua chunk, and pause the calling coroutine until it completes. // The return value is the (count, result) pair described above. - static std::pair waitScriptLine(LuaState& L, const std::string& cmd); + static std::pair waitScriptLine(LuaState& L, const std::string& chunk); static void runScriptOnLogin(); }; -- cgit v1.2.3 From 0c14c8a372a5d4639de3365f69e566962493fe78 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 7 Feb 2024 22:50:03 -0500 Subject: Fix tests broken by switching from Lua 5.4 to Luau. Add a new test<1>() that tests returning values from a Lua chunk using LLLUAmanager::waitScriptLine(). This exercises lua_tollsd() without yet involving LLEventPump machinery. For that purpose, extract from test<2>() the sequence of (description, expression, LLSD expected) triples into a static C array. The new test<1>() returns each such expression as a result; test<2>() posts each such expression to a test LLEventPump. test<2>() now uses waitScriptLine() instead of pumping the coroutine scheduler a few times and hoping. The pump-and-hope tactic worked before, but no longer does. waitScriptLine() is more robust anyway. Move the former test<1>() to test<3>() because it exercises still more machinery, specifically listen_events() and await_event(). Because this test involves a handshake with C++ code, use startScriptLine() to launch the Lua coroutine while providing a definite way to wait for completion later. Again, startScriptLine() followed by get() on the returned future is more robust than the previous pump-and-hope code. Similarly, the former test<3>(), now renamed test<4>(), uses startScriptLine() and Future::get() instead of pump-and-hope. --- indra/newview/tests/llluamanager_test.cpp | 143 ++++++++++++++++-------------- 1 file changed, 78 insertions(+), 65 deletions(-) diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp index 98a2726af7..1f25fd5f3b 100644 --- a/indra/newview/tests/llluamanager_test.cpp +++ b/indra/newview/tests/llluamanager_test.cpp @@ -27,6 +27,7 @@ #include "llsdutil.h" #include "lluri.h" #include "lluuid.h" +#include "lua_function.h" #include "stringize.h" class LLTestApp : public LLApp @@ -64,14 +65,80 @@ namespace tut typedef llluamanager_group::object object; llluamanager_group llluamanagergrp("llluamanager"); + static struct LuaExpr + { + std::string desc, expr; + LLSD expect; + } lua_expressions[] = { + { "nil", "nil", LLSD() }, + { "true", "true", true }, + { "false", "false", false }, + { "int", "17", 17 }, + { "real", "3.14", 3.14 }, + { "string", "'string'", "string" }, + // can't synthesize Lua userdata in Lua code: that can only be + // constructed by a C function + { "empty table", "{}", LLSD() }, + { "nested empty table", "{ 1, 2, 3, {}, 5 }", + llsd::array(1, 2, 3, LLSD(), 5) }, + { "nested non-empty table", "{ 1, 2, 3, {a=0, b=1}, 5 }", + llsd::array(1, 2, 3, llsd::map("a", 0, "b", 1), 5) }, + }; + template<> template<> void object::test<1>() + { + set_test_name("test Lua results"); + LuaState L; + for (auto& luax : lua_expressions) + { + auto [count, result] = + LLLUAmanager::waitScriptLine(L, "return " + luax.expr); + auto desc{ stringize("waitScriptLine(", luax.desc, ")") }; + ensure_equals(desc + ".count", count, 1); + ensure_equals(desc + ".result", result, luax.expect); + } + } + + void from_lua(const std::string& desc, const std::string_view& construct, const LLSD& expect) + { + LLSD fromlua; + LLEventStream replypump("testpump"); + LLTempBoundListener conn( + replypump.listen("llluamanager_test", + listener([&fromlua](const LLSD& data){ fromlua = data; }))); + const std::string lua(stringize( + "-- test LLSD synthesized by Lua\n", + "data = ", construct, "\n" + "post_on('testpump', data)\n" + )); + LuaState L; + auto [count, result] = LLLUAmanager::waitScriptLine(L, lua); + // We woke up again ourselves because the coroutine running Lua has + // finished. But our Lua chunk didn't actually return anything, so we + // expect count to be 0 and result to be undefined. + ensure_equals(desc + " count", count, 0); + ensure_equals(desc, fromlua, expect); + } + + template<> template<> + void object::test<2>() + { + set_test_name("LLSD from post_on()"); + for (auto& luax : lua_expressions) + { + from_lua(luax.desc, luax.expr, luax.expect); + } + } + + template<> template<> + void object::test<3>() { set_test_name("test post_on(), listen_events(), await_event()"); StringVec posts; LLEventStream replypump("testpump"); LLTempBoundListener conn( - replypump.listen("test<1>", + replypump.listen("test<3>", listener([&posts](const LLSD& data) { posts.push_back(data.asString()); }))); const std::string lua( @@ -88,7 +155,11 @@ namespace tut "await_event(replypump)\n" "post_on('testpump', 'exit')\n" ); - LLLUAmanager::runScriptLine(lua); + LuaState L; + // It's important to let the startScriptLine() coroutine run + // concurrently with ours until we've had a chance to post() our + // reply. + auto future = LLLUAmanager::startScriptLine(L, lua); StringVec expected{ "entry", "listen_events()", @@ -97,10 +168,6 @@ namespace tut "message", "exit" }; - for (int i = 0; i < 10 && posts.size() <= 2 && posts[2].empty(); ++i) - { - llcoro::suspend(); - } expected[2] = posts.at(2); LL_DEBUGS() << "Found pumpname '" << expected[2] << "'" << LL_ENDL; LLEventPump& luapump{ LLEventPumps::instance().obtain(expected[2]) }; @@ -108,57 +175,10 @@ namespace tut << LLError::Log::classname(luapump) << "': post('" << expected[4] << "')" << LL_ENDL; luapump.post(expected[4]); - llcoro::suspend(); + auto [count, result] = future.get(); ensure_equals("post_on() sequence", posts, expected); } - void from_lua(const std::string& desc, const std::string_view& construct, const LLSD& expect) - { - LLSD fromlua; - LLEventStream replypump("testpump"); - LLTempBoundListener conn( - replypump.listen("llluamanager_test", - listener([&fromlua](const LLSD& data){ fromlua = data; }))); - const std::string lua(stringize( - "-- test LLSD synthesized by Lua\n", - // we expect the caller's Lua snippet to construct a Lua object - // called 'data' - construct, "\n" - "post_on('testpump', data)\n" - )); - LLLUAmanager::runScriptLine(lua); - // At this point LLLUAmanager::runScriptLine() has launched a new C++ - // coroutine to run the passed Lua snippet, but that coroutine hasn't - // yet had a chance to run. Poke the coroutine scheduler until the Lua - // script has sent its data. - for (int i = 0; i < 10 && fromlua.isUndefined(); ++i) - { - llcoro::suspend(); - } - // We woke up again ourselves because the coroutine running Lua has - // finished. - ensure_equals(desc, fromlua, expect); - } - - template<> template<> - void object::test<2>() - { - set_test_name("LLSD from Lua"); - from_lua("nil", "data = nil", LLSD()); - from_lua("true", "data = true", true); - from_lua("false", "data = false", false); - from_lua("int", "data = 17", 17); - from_lua("real", "data = 3.14", 3.14); - from_lua("string", "data = 'string'", "string"); - // can't synthesize Lua userdata in Lua code: that can only be - // constructed by a C function - from_lua("empty table", "data = {}", LLSD()); - from_lua("nested empty table", "data = { 1, 2, 3, {}, 5 }", - llsd::array(1, 2, 3, LLSD(), 5)); - from_lua("nested non-empty table", "data = { 1, 2, 3, {a=0, b=1}, 5 }", - llsd::array(1, 2, 3, llsd::map("a", 0, "b", 1), 5)); - } - void round_trip(const std::string& desc, const LLSD& send, const LLSD& expect) { LLSD reply; @@ -176,15 +196,8 @@ namespace tut "post_on('testpump', replypump)\n" "await_event(replypump)\n" ); - LLLUAmanager::runScriptLine(lua); - // At this point LLLUAmanager::runScriptLine() has launched a new C++ - // coroutine to run the passed Lua snippet, but that coroutine hasn't - // yet had a chance to run. Poke the coroutine scheduler until the Lua - // script has sent its reply pump name. - for (int i = 0; i < 10 && reply.isUndefined(); ++i) - { - llcoro::suspend(); - } + LuaState L; + auto future = LLLUAmanager::startScriptLine(L, lua); // We woke up again ourselves because the coroutine running Lua has // reached the await_event() call, which suspends the calling C++ // coroutine (including the Lua code running on it) until we post @@ -194,7 +207,7 @@ namespace tut LLEventPumps::instance().post(luapump, send); // The C++ coroutine running the Lua script is now ready to run. Run // it so it will echo the LLSD back to us. - llcoro::suspend(); + auto [count, result] = future.get(); ensure_equals(desc, reply, expect); } @@ -219,7 +232,7 @@ namespace tut }; template<> template<> - void object::test<3>() + void object::test<4>() { set_test_name("LLSD round trip"); LLSD::Binary binary{ 3, 1, 4, 1, 5, 9, 2, 6, 5 }; -- cgit v1.2.3 From f0abd59ce4b1604e87497b65d74cdb4f2f11bf72 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 8 Feb 2024 11:28:25 -0500 Subject: Another missing #include. --- indra/llcommon/llexception.h | 1 + 1 file changed, 1 insertion(+) diff --git a/indra/llcommon/llexception.h b/indra/llcommon/llexception.h index 375bea4a57..9e322db86d 100644 --- a/indra/llcommon/llexception.h +++ b/indra/llcommon/llexception.h @@ -12,6 +12,7 @@ #if ! defined(LL_LLEXCEPTION_H) #define LL_LLEXCEPTION_H +#include "stdtypes.h" #include #include #include -- cgit v1.2.3 From 43be64733ca667f2b117b48d9a808d0e3cf2aebc Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 8 Feb 2024 11:29:22 -0500 Subject: Updated llgltfmaterial_test.cpp sizeof check. --- indra/llprimitive/tests/llgltfmaterial_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/llprimitive/tests/llgltfmaterial_test.cpp b/indra/llprimitive/tests/llgltfmaterial_test.cpp index 88b6fae3a7..823a36ec3a 100644 --- a/indra/llprimitive/tests/llgltfmaterial_test.cpp +++ b/indra/llprimitive/tests/llgltfmaterial_test.cpp @@ -143,7 +143,7 @@ namespace tut #if LL_WINDOWS // If any fields are added/changed, these tests should be updated (consider also updating ASSET_VERSION in LLGLTFMaterial) // This test result will vary between compilers, so only test a single platform - ensure_equals("fields supported for GLTF (sizeof check)", sizeof(LLGLTFMaterial), 216); + ensure_equals("fields supported for GLTF (sizeof check)", sizeof(LLGLTFMaterial), 232); #endif #endif ensure_equals("LLGLTFMaterial texture info count", (U32)LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT, 4); -- cgit v1.2.3 From ef9a7813522c247763ba53bf3e3c55943e385745 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 8 Feb 2024 11:33:59 -0500 Subject: Defend lluau::error() from platform-specific diagnostics. macOS clang produces fatal warnings when trying to pass a const char* parameter to luaL_error() (-Wformat-security). Temporarily suppressing that requires #pragma clang directives which, in turn, produce fatal warnings in VS. Moreover, VS recognizes that luaL_error() never returns, and so diagnoses the following return statement as unreachable code. But that return statement is the whole reason for lluau::error()'s existence... --- indra/llcommon/lua_function.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index ea9f2ebdf8..82cd91984a 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -26,15 +26,21 @@ namespace lluau { // luau defines luaL_error() as void, but we want to use the Lua idiom of // 'return error(...)'. Wrap luaL_error() in an int function. +#if __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wformat-security" +#endif // __clang__ template int error(lua_State* L, const char* format, Args&&... args) { luaL_error(L, format, std::forward(args)...); +#ifndef LL_MSVC return 0; +#endif } +#if __clang__ #pragma clang diagnostic pop +#endif // __clang__ // luau removed lua_dostring(), but since we perform the equivalent luau // sequence in multiple places, encapsulate it. desc and text are strings -- cgit v1.2.3 From 72e680cfae03aa86659b6c2165e16ce46487097b Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 8 Feb 2024 12:02:15 -0500 Subject: Align forward declaration of lua_State with official declaration. --- indra/llcommon/lualistener.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/llcommon/lualistener.h b/indra/llcommon/lualistener.h index 550f029158..df88d55dd5 100644 --- a/indra/llcommon/lualistener.h +++ b/indra/llcommon/lualistener.h @@ -21,7 +21,7 @@ #include "lleventfilter.h" #endif -class lua_State; +struct lua_State; class LLLeapListener; /** -- 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 +++++++----- indra/llcommon/lua_function.h | 21 +++++++++++---------- indra/newview/llluamanager.cpp | 24 +++++++++++++++--------- 3 files changed, 33 insertions(+), 24 deletions(-) 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() diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index 82cd91984a..26c399cdd1 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -124,19 +124,20 @@ struct LuaPopper class LuaFunction { public: - LuaFunction(const std::string_view& name, lua_CFunction function); + LuaFunction(const std::string_view& name, lua_CFunction function, + const std::string_view& helptext); static void init(lua_State* L); static lua_CFunction get(const std::string& key); private: - using Registry = std::map; + using Registry = std::map>; static Registry& getRegistry(); }; /** - * lua_function(name) is a macro to facilitate defining C++ functions + * lua_function(name, helptext) is a macro to facilitate defining C++ functions * available to Lua. It defines a subclass of LuaFunction and declares a * static instance of that subclass, thereby forcing the compiler to call its * constructor at module initialization time. The constructor passes the @@ -145,13 +146,13 @@ private: * call() method definition header, to be followed by a method body enclosed * in curly braces as usual. */ -#define lua_function(name) \ -static struct name##_luadecl : public LuaFunction \ -{ \ - name##_luadecl(): LuaFunction(#name, &call) {} \ - static int call(lua_State* L); \ -} name##_luadef; \ -int name##_luadecl::call(lua_State* L) +#define lua_function(name, helptext) \ +static struct name##_luasub : public LuaFunction \ +{ \ + name##_luasub(): LuaFunction(#name, &call, helptext) {} \ + static int call(lua_State* L); \ +} name##_luadecl; \ +int name##_luasub::call(lua_State* L) // { // ... supply method body here, referencing 'L' ... // } diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index fac66003a1..c55c44e671 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -56,7 +56,7 @@ extern LLUIListener sUIListener; #include #include -lua_function(sleep) +lua_function(sleep, "sleep(seconds): pause the running coroutine") { F32 seconds = lua_tonumber(L, -1); lua_pop(L, 1); @@ -106,20 +106,20 @@ std::string lua_print_msg(lua_State* L, const std::string_view& level) return msg; } -lua_function(print_debug) +lua_function(print_debug, "print_debug(args...): DEBUG level logging") { LL_DEBUGS("Lua") << lua_print_msg(L, "DEBUG") << LL_ENDL; return 0; } // also used for print(); see LuaState constructor -lua_function(print_info) +lua_function(print_info, "print_info(args...): INFO level logging") { LL_INFOS("Lua") << lua_print_msg(L, "INFO") << LL_ENDL; return 0; } -lua_function(print_warning) +lua_function(print_warning, "print_warning(args...): WARNING level logging") { LL_WARNS("Lua") << lua_print_msg(L, "WARN") << LL_ENDL; return 0; @@ -127,7 +127,9 @@ lua_function(print_warning) #ifndef LL_TEST -lua_function(run_ui_command) +lua_function(run_ui_command, + "run_ui_command(name [, parameter]): " + "call specified UI command with specified parameter") { int top = lua_gettop(L); std::string func_name; @@ -154,7 +156,7 @@ lua_function(run_ui_command) } #endif // ! LL_TEST -lua_function(post_on) +lua_function(post_on, "post_on(pumpname, data): post specified data to specified LLEventPump") { std::string pumpname{ lua_tostdstring(L, 1) }; LLSD data{ lua_tollsd(L, 2) }; @@ -164,7 +166,10 @@ lua_function(post_on) return 0; } -lua_function(listen_events) +lua_function(listen_events, + "listen_events(callback): call callback(pumpname, data) with events received\n" + "on this Lua chunk's replypump.\n" + "Returns replypump, commandpump: names of LLEventPumps specific to this chunk.") { if (! lua_isfunction(L, 1)) { @@ -237,9 +242,10 @@ lua_function(listen_events) return 2; } -lua_function(await_event) +lua_function(await_event, + "await_event(pumpname [, timeout [, value to return if timeout (default nil)]]):\n" + "pause the running Lua chunk until the next event on the named LLEventPump") { - // await_event(pumpname [, timeout [, value to return if timeout (default nil)]]) auto pumpname{ lua_tostdstring(L, 1) }; LLSD result; if (lua_gettop(L) > 1) -- cgit v1.2.3