diff options
author | nat-goodspeed <nat@lindenlab.com> | 2024-02-08 14:45:43 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-08 14:45:43 -0500 |
commit | 24b6059620698744c035474db93975ebac28e45b (patch) | |
tree | 5437fa6948a8a111b77b8bcf726629d419cdcf1d | |
parent | eb0c19b2746890eb4dbfd6eac045699d0e5842dd (diff) | |
parent | 4e9794bd06660baea6d16779f6e354ca99e11b8a (diff) |
Merge pull request #21 from secondlife/lua_function
Break out lua_function.h to enable decentralizing Lua entry points to C++.
-rw-r--r-- | indra/llcommon/CMakeLists.txt | 4 | ||||
-rw-r--r-- | indra/llcommon/llexception.h | 1 | ||||
-rw-r--r-- | indra/llcommon/llformat.h | 2 | ||||
-rw-r--r-- | indra/llcommon/llrefcount.h | 1 | ||||
-rw-r--r-- | indra/llcommon/llstring.h | 1 | ||||
-rw-r--r-- | indra/llcommon/lua_function.cpp | 613 | ||||
-rw-r--r-- | indra/llcommon/lua_function.h | 210 | ||||
-rw-r--r-- | indra/llcommon/lualistener.cpp | 103 | ||||
-rw-r--r-- | indra/llcommon/lualistener.h | 77 | ||||
-rw-r--r-- | indra/llprimitive/tests/llgltfmaterial_test.cpp | 2 | ||||
-rw-r--r-- | indra/newview/llluamanager.cpp | 966 | ||||
-rw-r--r-- | indra/newview/llluamanager.h | 45 | ||||
-rw-r--r-- | indra/newview/tests/llluamanager_test.cpp | 143 |
13 files changed, 1258 insertions, 910 deletions
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/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 <stdexcept> #include <boost/exception/exception.hpp> #include <boost/throw_exception.hpp> 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 <boost/noncopyable.hpp> #include <boost/intrusive_ptr.hpp> #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 <vector> #include <map> #include "llformat.h" +#include "stdtypes.h" #if LL_LINUX #include <wctype.h> diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp new file mode 100644 index 0000000000..07e0c1fac2 --- /dev/null +++ b/indra/llcommon/lua_function.cpp @@ -0,0 +1,613 @@ +/** + * @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 <algorithm> +#include <map> +#include <memory> // 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<char[], freer> 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 + + // 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) +{ + 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: + { + // 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 numval; + } + } + + 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<LLSD::Integer> 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(script_finished_fn cb): + 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(const std::string& desc, int r) +{ + if (r != LUA_OK) + { + mError = lua_tostring(mState, -1); + lua_pop(mState, 1); + + LL_WARNS() << desc << ": " << mError << LL_ENDL; + return false; + } + return true; +} + +std::pair<int, LLSD> 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<int, LLSD> 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() +{ + if (mCount) + { + lua_pop(mState, mCount); + } +} + +LuaFunction::LuaFunction(const std::string_view& name, lua_CFunction function, + const std::string_view& helptext) +{ + getRegistry().emplace(name, Registry::mapped_type{ function, helptext }); +} + +void LuaFunction::init(lua_State* L) +{ + for (const auto& [name, pair]: getRegistry()) + { + const auto& [funcptr, helptext] = pair; + lua_register(L, name.c_str(), funcptr); + } +} + +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.first; +} + +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..26c399cdd1 --- /dev/null +++ b/indra/llcommon/lua_function.h @@ -0,0 +1,210 @@ +/** + * @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" +#include <utility> // std::pair + +#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. +#if __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wformat-security" +#endif // __clang__ + template<typename... Args> + int error(lua_State* L, const char* format, Args&&... args) + { + luaL_error(L, format, std::forward<Args>(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 + // 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<void(std::string msg)> script_finished_fn; + + LuaState(script_finished_fn cb={}); + + LuaState(const LuaState&) = delete; + LuaState& operator=(const LuaState&) = delete; + + ~LuaState(); + + 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<int, LLSD> expr(const std::string& desc, const std::string& text); + + operator lua_State*() const { return mState; } + +private: + 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, + const std::string_view& helptext); + + static void init(lua_State* L); + + static lua_CFunction get(const std::string& key); + +private: + using Registry = std::map<std::string, std::pair<lua_CFunction, std::string>>; + static Registry& getRegistry(); +}; + +/** + * 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 + * 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, 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' ... +// } + +// 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 <cstdlib> // std::rand() +#include <cstring> // 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..df88d55dd5 --- /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 <memory> // std::unique_ptr + +#ifdef LL_TEST +#include "lleventfilter.h" +#endif + +struct 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<LuaListener, int> +{ + using super = LLInstanceTracker<LuaListener, int>; +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<LLEventStream> mReplyPump{ "luapump", false }; +#endif + LLTempBoundListener mReplyConnection; + std::unique_ptr<LLLeapListener> mListener; +}; + +#endif /* ! defined(LL_LUALISTENER_H) */ 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); diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index 3e3ce45cb0..c55c44e671 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -28,14 +28,11 @@ #include "llviewerprecompiledheaders.h" #include "llluamanager.h" -#include "hexdump.h" +#include "llcoros.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,237 +52,25 @@ 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 <algorithm> -#include <cstdlib> // std::rand() -#include <cstring> // std::memcpy() -#include <map> -#include <memory> // std::unique_ptr #include <sstream> #include <string_view> #include <vector> -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<LuaListener, int> -{ - using super = LLInstanceTracker<LuaListener, int>; -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<LLEventStream> mReplyPump{ "luapump", false }; -#endif - LLTempBoundListener mReplyConnection; - std::unique_ptr<LLLeapListener> 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 +lua_function(sleep, "sleep(seconds): pause the running coroutine") { - 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<std::string, lua_CFunction>; - static Registry& getRegistry() - { - // use a function-local static to ensure it's initialized - static Registry registry; - return registry; - } + F32 seconds = lua_tonumber(L, -1); + lua_pop(L, 1); + llcoro::suspendUntilTimeout(seconds); + return 0; }; -/** - * 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 -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; @@ -294,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 = " "; @@ -302,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); @@ -320,35 +105,21 @@ 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) +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; @@ -356,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; @@ -383,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) }; @@ -393,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)) { @@ -402,19 +178,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; @@ -461,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) @@ -484,141 +266,119 @@ lua_function(await_event) return 1; } -/** - * RAII class to manage the lifespan of a lua_State - */ -class LuaState +void LLLUAmanager::runScriptFile(const std::string& filename, script_finished_fn cb) { -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); + // 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); +} - LL_WARNS() << mDesc << ": " << mError << LL_ENDL; - return false; - } - return true; - } +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); +} - operator lua_State*() const { return mState; } +LLCoros::Future<std::pair<int, LLSD>> +LLLUAmanager::startScriptFile(LuaState& L, const std::string& filename) +{ + // Despite returning from startScriptFile(), we need this Promise to + // remain alive until the callback has fired. + auto promise{ std::make_shared<LLCoros::Promise<std::pair<int, LLSD>>>() }; + runScriptFile(L, filename, + [promise](int count, LLSD result) + { promise->set_value({ count, result }); }); + return LLCoros::getFuture(*promise); +} -private: - std::string mDesc; - LLLUAmanager::script_finished_fn mCallback; - lua_State* mState; - std::string mError; -}; +std::pair<int, LLSD> LLLUAmanager::waitScriptFile(LuaState& L, const std::string& filename) +{ + return startScriptFile(L, filename).get(); +} -void LLLUAmanager::runScriptFile(const std::string& filename, script_finished_fn cb) +void LLLUAmanager::runScriptFile(LuaState& L, const std::string& filename, script_result_fn cb) { std::string desc{ stringize("runScriptFile('", filename, "')") }; - LLCoros::instance().launch(desc, [desc, filename, cb]() + LLCoros::instance().launch(desc, [&L, desc, filename, cb]() { - LuaState L(desc, cb); - - auto LUA_sleep_func = [](lua_State *L) - { - F32 seconds = lua_tonumber(L, -1); - lua_pop(L, 1); - llcoro::suspendUntilTimeout(seconds); - return 0; - }; - - lua_register(L, "sleep", LUA_sleep_func); - llifstream in_file; in_file.open(filename.c_str()); if (in_file.is_open()) { - std::string text((std::istreambuf_iterator<char>(in_file)), std::istreambuf_iterator<char>()); - - 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<char>(in_file), + std::istreambuf_iterator<char>()}; + 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) +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, chunk); +} + +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, chunk, cb); +} + +LLCoros::Future<std::pair<int, LLSD>> +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<LLCoros::Promise<std::pair<int, LLSD>>>() }; + runScriptLine(L, chunk, + [promise](int count, LLSD result) + { promise->set_value({ count, result }); }); + return LLCoros::getFuture(*promise); +} + +std::pair<int, LLSD> LLLUAmanager::waitScriptLine(LuaState& L, const std::string& chunk) { - // find a suitable abbreviation for the cmd string - std::string_view shortcmd{ cmd }; + return startScriptLine(L, chunk).get(); +} + +void LLLUAmanager::runScriptLine(LuaState& L, const std::string& chunk, script_result_fn cb) +{ + // 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, [desc, cmd, cb]() + std::string desc{ stringize("runScriptLine('", shortchunk, "')") }; + LLCoros::instance().launch(desc, [&L, desc, chunk, 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)); + auto [count, result] = L.expr(desc, chunk); + if (cb) + { + cb(count, result); + } }); } @@ -642,479 +402,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<LLSD::Integer> 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; - } - } -} diff --git a/indra/newview/llluamanager.h b/indra/newview/llluamanager.h index 08d9876ce2..12284cd0e4 100644 --- a/indra/newview/llluamanager.h +++ b/indra/newview/llluamanager.h @@ -27,19 +27,58 @@ #ifndef LL_LLLUAMANAGER_H #define LL_LLLUAMANAGER_H +#include "llcoros.h" +#include "llsd.h" #include <functional> #include <string> +#include <utility> // 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<void(std::string msg)> 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<void(int count, const LLSD& result)> 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 = {}); + // 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<std::pair<int, LLSD>> + 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<int, LLSD> 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 &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<std::pair<int, LLSD>> + 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<int, LLSD> waitScriptLine(LuaState& L, const std::string& chunk); static void runScriptOnLogin(); }; - #endif 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 }; |