diff options
author | Nat Goodspeed <nat@lindenlab.com> | 2024-09-05 14:37:25 -0400 |
---|---|---|
committer | Nat Goodspeed <nat@lindenlab.com> | 2024-09-05 14:37:25 -0400 |
commit | ff2d79906ccef217194d5d9ec9d7025db03592a8 (patch) | |
tree | 83d5db1c173636bb77ebb33e860fac77ab5d79e8 | |
parent | 25a86618002a397d1d8dabf2ec1f093489b2f816 (diff) | |
parent | 18d81e20f0b0044c16615953d7b69d7fb34d3449 (diff) |
Merge branch 'release/luau-scripting' into lua-merge-dev
-rw-r--r-- | indra/llcommon/CMakeLists.txt | 3 | ||||
-rw-r--r-- | indra/llcommon/llinstancetracker.h | 16 | ||||
-rw-r--r-- | indra/llcommon/llinttracker.h | 43 | ||||
-rw-r--r-- | indra/llcommon/lua_function.cpp | 572 | ||||
-rw-r--r-- | indra/llcommon/lua_function.h | 18 | ||||
-rw-r--r-- | indra/llcommon/resultset.cpp | 96 | ||||
-rw-r--r-- | indra/llcommon/resultset.h | 61 | ||||
-rw-r--r-- | indra/llcommon/stringize.h | 12 | ||||
-rw-r--r-- | indra/llui/llfloaterreglistener.cpp | 20 | ||||
-rw-r--r-- | indra/newview/llinventorylistener.cpp | 188 | ||||
-rw-r--r-- | indra/newview/llinventorylistener.h | 9 | ||||
-rw-r--r-- | indra/newview/scripts/lua/require/LLInventory.lua | 41 | ||||
-rw-r--r-- | indra/newview/scripts/lua/require/UI.lua | 5 | ||||
-rw-r--r-- | indra/newview/scripts/lua/require/result_view.lua | 98 | ||||
-rw-r--r-- | indra/newview/scripts/lua/test_LLInventory.lua | 15 | ||||
-rw-r--r-- | indra/newview/scripts/lua/test_inv_resultset.lua | 18 | ||||
-rw-r--r-- | indra/newview/scripts/lua/test_result_view.lua | 55 | ||||
-rw-r--r-- | indra/newview/scripts/lua/test_setdtor.lua | 91 | ||||
-rw-r--r-- | indra/newview/tests/llluamanager_test.cpp | 4 |
19 files changed, 1257 insertions, 108 deletions
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 6ff2916c52..f47136f781 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -107,6 +107,7 @@ set(llcommon_SOURCE_FILES lockstatic.cpp lua_function.cpp lualistener.cpp + resultset.cpp threadpool.cpp throttle.cpp u64.cpp @@ -182,6 +183,7 @@ set(llcommon_HEADER_FILES llinitparam.h llinstancetracker.h llinstancetrackersubclass.h + llinttracker.h llkeybind.h llkeythrottle.h llleap.h @@ -254,6 +256,7 @@ set(llcommon_HEADER_FILES lockstatic.h lua_function.h lualistener.h + resultset.h stdtypes.h stringize.h tempset.h diff --git a/indra/llcommon/llinstancetracker.h b/indra/llcommon/llinstancetracker.h index de71a5fd68..03418e9bad 100644 --- a/indra/llcommon/llinstancetracker.h +++ b/indra/llcommon/llinstancetracker.h @@ -263,19 +263,19 @@ public: virtual const KEY& getKey() const { return mInstanceKey; } /// for use ONLY for an object we're sure resides on the heap! - static bool destruct(const KEY& key) + static bool erase(const KEY& key) { - return destruct(getInstance(key)); + return erase(getInstance(key)); } /// for use ONLY for an object we're sure resides on the heap! - static bool destruct(const weak_t& ptr) + static bool erase(const weak_t& ptr) { - return destruct(ptr.lock()); + return erase(ptr.lock()); } /// for use ONLY for an object we're sure resides on the heap! - static bool destruct(const ptr_t& ptr) + static bool erase(const ptr_t& ptr) { if (! ptr) { @@ -482,13 +482,13 @@ public: using key_snapshot_of = instance_snapshot_of<SUBCLASS>; /// for use ONLY for an object we're sure resides on the heap! - static bool destruct(const weak_t& ptr) + static bool erase(const weak_t& ptr) { - return destruct(ptr.lock()); + return erase(ptr.lock()); } /// for use ONLY for an object we're sure resides on the heap! - static bool destruct(const ptr_t& ptr) + static bool erase(const ptr_t& ptr) { if (! ptr) { diff --git a/indra/llcommon/llinttracker.h b/indra/llcommon/llinttracker.h new file mode 100644 index 0000000000..fd6d24d0fd --- /dev/null +++ b/indra/llcommon/llinttracker.h @@ -0,0 +1,43 @@ +/** + * @file llinttracker.h + * @author Nat Goodspeed + * @date 2024-08-30 + * @brief LLIntTracker isa LLInstanceTracker with generated int keys. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLINTTRACKER_H) +#define LL_LLINTTRACKER_H + +#include "llinstancetracker.h" + +template <typename T> +class LLIntTracker: public LLInstanceTracker<T, int> +{ + using super = LLInstanceTracker<T, int>; +public: + LLIntTracker(): + super(getUniqueKey()) + {} + +private: + static int getUniqueKey() + { + // Find a random key that does NOT already correspond to an instance. + // Passing a duplicate key to LLInstanceTracker would do Bad Things. + int key; + do + { + key = std::rand(); + } while (super::getInstance(key)); + // This could be racy, if we were instantiating new LLIntTracker<T>s + // on multiple threads. If we need that, have to lock between checking + // getInstance() and constructing the new super. + return key; + } +}; + +#endif /* ! defined(LL_LLINTTRACKER_H) */ diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index c0090dd395..f7876e4aaf 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -26,6 +26,7 @@ // other Linden headers #include "fsyspath.h" #include "hexdump.h" +#include "llcoros.h" #include "lleventcoro.h" #include "llsd.h" #include "llsdutil.h" @@ -64,10 +65,26 @@ int dostring(lua_State* L, const std::string& desc, const std::string& text) if (r != LUA_OK) return r; + // Push debug.traceback() onto the stack as lua_pcall()'s error + // handler function. On error, lua_pcall() calls the specified error + // handler function with the original error message; the message + // returned by the error handler is then returned by lua_pcall(). + // Luau's debug.traceback() is called with a message to prepend to the + // returned traceback string. Almost as if they'd been designed to + // work together... + lua_getglobal(L, "debug"); + lua_getfield(L, -1, "traceback"); + // ditch "debug" + lua_remove(L, -2); + // stack: compiled chunk, debug.traceback() + lua_insert(L, -2); + // stack: debug.traceback(), compiled chunk + LuaRemover cleanup(L, -2); + // It's important to pass LUA_MULTRET as the expected number of return // values: if we pass any fixed number, we discard any returned values // beyond that number. - return lua_pcall(L, 0, LUA_MULTRET, 0); + return lua_pcall(L, 0, LUA_MULTRET, -2); } int loadstring(lua_State *L, const std::string &desc, const std::string &text) @@ -100,6 +117,34 @@ fsyspath source_path(lua_State* L) } // namespace lluau /***************************************************************************** +* lua_destroyuserdata(), lua_destroybounduserdata() (see lua_emplace<T>()) +*****************************************************************************/ +int lua_destroyuserdata(lua_State* L) +{ + // stack: lua_emplace() userdata to be destroyed + if (int tag; + lua_isuserdata(L, -1) && + (tag = lua_userdatatag(L, -1)) != 0) + { + auto dtor = lua_getuserdatadtor(L, tag); + // detach this userdata from the destructor with tag 'tag' + lua_setuserdatatag(L, -1, 0); + // now run the real destructor + dtor(L, lua_touserdata(L, -1)); + } + lua_settop(L, 0); + return 0; +} + +int lua_destroybounduserdata(lua_State *L) +{ + // called with no arguments -- push bound upvalue + lluau_checkstack(L, 1); + lua_pushvalue(L, lua_upvalueindex(1)); + return lua_destroyuserdata(L); +} + +/***************************************************************************** * Lua <=> C++ conversions *****************************************************************************/ std::string lua_tostdstring(lua_State* L, int index) @@ -467,6 +512,20 @@ namespace using LuaStateMap = std::unordered_map<lua_State*, LuaState*>; static LuaStateMap sLuaStateMap; +// replace table-at-index[name] with passed func, +// binding the original table-at-index[name] as func's upvalue +void replace_entry(lua_State* L, int index, + const std::string& name, lua_CFunction func); + +// replacement next() function that understands setdtor() proxy args +int lua_proxydrill(lua_State* L); +// replacement pairs() function that supports __iter() metamethod +int lua_metapairs(lua_State* L); +// replacement ipairs() function that supports __index() metamethod +int lua_metaipairs(lua_State* L); +// helper for lua_metaipairs() (actual generator function) +int lua_metaipair(lua_State* L); + } // anonymous namespace LuaState::LuaState(script_finished_fn cb): @@ -484,8 +543,143 @@ LuaState::LuaState(script_finished_fn cb): lua_register(mState, "print", LuaFunction::get("print_info")); // We don't want to have to prefix require(). lua_register(mState, "require", LuaFunction::get("require")); + + // Replace certain key global functions so they understand our + // LL.setdtor() proxy objects. + // (We could also do this for selected library functions as well, + // e.g. the table, string, math libraries... let's see if needed.) + replace_entry(mState, LUA_GLOBALSINDEX, "next", lua_proxydrill); + // Replacing pairs() with lua_metapairs() makes global pairs() honor + // objects with __iter() metamethods. + replace_entry(mState, LUA_GLOBALSINDEX, "pairs", lua_metapairs); + // Replacing ipairs() with lua_metaipairs() makes global ipairs() honor + // objects with __index() metamethods -- as long as the object in question + // has no array entries (int keys) of its own. (If it does, object[i] will + // retrieve table[i] instead of calling __index(table, i).) + replace_entry(mState, LUA_GLOBALSINDEX, "ipairs", lua_metaipairs); +} + +namespace +{ + +void replace_entry(lua_State* L, int index, + const std::string& name, lua_CFunction func) +{ + index = lua_absindex(L, index); + lua_checkdelta(L); + // push the function's name string twice + lua_pushlstring(L, name.data(), name.length()); + lua_pushvalue(L, -1); + // stack: name, name + // look up the existing table entry + lua_rawget(L, index); + // stack: name, original function + // bind original function as the upvalue for func() + lua_pushcclosure(L, func, (name + "()").c_str(), 1); + // stack: name, func-with-bound-original + // table[name] = func-with-bound-original + lua_rawset(L, index); +} + +int lua_metapairs(lua_State* L) +{ +// LuaLog debug(L, "lua_metapairs()"); + // pairs(obj): object is at index 1 + // How many args were we passed? + int args = lua_gettop(L); + // stack: obj, ... + if (luaL_getmetafield(L, 1, "__iter")) + { + // stack: obj, ..., getmetatable(obj).__iter + } + else + { + // Push the original pairs() function, captured as our upvalue. + lua_pushvalue(L, lua_upvalueindex(1)); + // stack: obj, ..., original pairs() + } + lua_insert(L, 1); + // stack: (__iter() or pairs()), obj, ... + // call whichever function(obj, ...) (args args, up to 3 return values) + lua_call(L, args, LUA_MULTRET); + // return as many values as the selected function returned + return lua_gettop(L); +} + +int lua_metaipairs(lua_State* L) +{ +// LuaLog debug(L, "lua_metaipairs()"); + // ipairs(obj): object is at index 1 + // How many args were we passed? + int args = lua_gettop(L); + // stack: obj, ... + if (luaL_getmetafield(L, 1, "__index")) + { + // stack: obj, ..., getmetatable(obj).__index + // discard __index and everything but obj: + // we don't want to call __index(), just check its presence + lua_settop(L, 1); + // stack: obj + lua_pushcfunction(L, lua_metaipair, "lua_metaipair"); + // stack: obj, lua_metaipair + lua_insert(L, 1); + // stack: lua_metaipair, obj + // push explicit 0 so lua_metaipair need not special-case nil + lua_pushinteger(L, 0); + // stack: lua_metaipair, obj, 0 + return 3; + } + else // no __index() metamethod + { + // Although our lua_metaipair() function demonstrably works whether or + // not our object has an __index() metamethod, the code below assumes + // that the Lua engine may have a more efficient implementation for + // built-in ipairs() than our lua_metaipair(). + // Push the original ipairs() function, captured as our upvalue. + lua_pushvalue(L, lua_upvalueindex(1)); + // stack: obj, ..., original ipairs() + // Shift the stack so the original function is first. + lua_insert(L, 1); + // stack: original ipairs(), obj, ... + // Call original ipairs() with all original args, no error checking. + // Don't truncate however many values that function returns. + lua_call(L, args, LUA_MULTRET); + // Return as many values as the original function returned. + return lua_gettop(L); + } +} + +int lua_metaipair(lua_State* L) +{ +// LuaLog debug(L, "lua_metaipair()"); + // called with (obj, previous-index) + // increment previous-index for this call + lua_Integer i = luaL_optinteger(L, 2, 0) + 1; + lua_pop(L, 1); + // stack: obj + lua_pushinteger(L, i); + // stack: obj, i + lua_pushvalue(L, -1); + // stack: obj, i, i + lua_insert(L, 1); + // stack: i, obj, i + lua_gettable(L, -2); + // stack: i, obj, obj[i] (honoring __index()) + lua_remove(L, -2); + // stack: i, obj[i] + if (! lua_isnil(L, -1)) + { + // great, obj[i] isn't nil: return (i, obj[i]) + return 2; + } + // obj[i] is nil. ipairs() is documented to stop at the first hole, + // regardless of #obj. Clear the stack, i.e. return nil. + lua_settop(L, 0); + return 0; } +} // anonymous namespace + LuaState::~LuaState() { // We're just about to destroy this lua_State mState. Did this Lua chunk @@ -502,7 +696,8 @@ LuaState::~LuaState() // That's important because we walk the atexit table backwards, to // destroy last the things we created (passed to LL.atexit()) first. int len(lua_objlen(mState, -1)); - LL_DEBUGS("Lua") << "Registry.atexit is a table with " << len << " entries" << LL_ENDL; + LL_DEBUGS("Lua") << LLCoros::getName() << ": Registry.atexit is a table with " + << len << " entries" << LL_ENDL; // Push debug.traceback() onto the stack as lua_pcall()'s error // handler function. On error, lua_pcall() calls the specified error @@ -527,15 +722,17 @@ LuaState::~LuaState() // Use lua_pcall() because errors in any one atexit() function // shouldn't cancel the rest of them. Pass debug.traceback() as // the error handler function. - LL_DEBUGS("Lua") << "Calling atexit(" << i << ")" << LL_ENDL; + LL_DEBUGS("Lua") << LLCoros::getName() + << ": calling atexit(" << i << ")" << LL_ENDL; if (lua_pcall(mState, 0, 0, -2) != LUA_OK) { auto error{ lua_tostdstring(mState, -1) }; - LL_WARNS("Lua") << "atexit(" << i << ") error: " << error << LL_ENDL; + LL_WARNS("Lua") << LLCoros::getName() + << ": atexit(" << i << ") error: " << error << LL_ENDL; // pop error message lua_pop(mState, 1); } - LL_DEBUGS("Lua") << "atexit(" << i << ") done" << LL_ENDL; + LL_DEBUGS("Lua") << LLCoros::getName() << ": atexit(" << i << ") done" << LL_ENDL; // lua_pcall() has already popped atexit[i]: // stack contains atexit, debug.traceback() } @@ -664,10 +861,10 @@ LuaListener& LuaState::obtainListener(lua_State* L) // At this point, one way or the other, the stack top should be (a Lua // userdata containing) our LuaListener. LuaListener* listener{ lua_toclass<LuaListener>(L, -1) }; - // userdata objects created by lua_emplace<T>() are bound on the atexit() - // queue, and are thus never garbage collected: they're destroyed only - // when ~LuaState() walks that queue. That's why we dare pop the userdata - // value off the stack while still depending on a pointer into its data. + // Since our LuaListener instance is stored in the Registry, it won't be + // garbage collected: it will be destroyed only when lua_close() clears + // out the Registry. That's why we dare pop the userdata value off the + // stack while still depending on a pointer into its data. lua_pop(L, 1); return *listener; } @@ -851,8 +1048,8 @@ lua_function(check_stop, "check_stop(): ensure that a Lua script responds to vie * help() *****************************************************************************/ lua_function(help, - "help(): list viewer's Lua functions\n" - "help(function): show help string for specific function") + "LL.help(): list viewer's Lua functions\n" + "LL.help(function): show help string for specific function") { auto& luapump{ LLEventPumps::instance().obtain("lua output") }; const auto& [registry, lookup]{ LuaFunction::getRState() }; @@ -911,8 +1108,8 @@ lua_function(help, *****************************************************************************/ lua_function( leaphelp, - "leaphelp(): list viewer's LEAP APIs\n" - "leaphelp(api): show help for specific api string name") + "LL.leaphelp(): list viewer's LEAP APIs\n" + "LL.leaphelp(api): show help for specific api string name") { LLSD request; int top{ lua_gettop(L) }; @@ -975,6 +1172,327 @@ lua_function( } /***************************************************************************** +* setdtor +*****************************************************************************/ +namespace { + +// proxy userdata object returned by setdtor() +struct setdtor_refs +{ + lua_State* L; + std::string desc; + // You can't directly store a Lua object in a C++ object, but you can + // create a Lua "reference" by storing the object in the Lua Registry and + // capturing its Registry index. + int objref; + int dtorref; + + setdtor_refs(lua_State* L, const std::string& desc, int objref, int dtorref): + L(L), + desc(desc), + objref(objref), + dtorref(dtorref) + {} + setdtor_refs(const setdtor_refs&) = delete; + setdtor_refs& operator=(const setdtor_refs&) = delete; + ~setdtor_refs(); + + static void push_metatable(lua_State* L); + static std::string binop(const std::string& name, const std::string& op); + static int meta__index(lua_State* L); +}; + +} // anonymous namespace + +lua_function( + setdtor, + "setdtor(desc, obj, dtorfunc) => proxy object referencing obj and dtorfunc.\n" + "When the returned proxy object is garbage-collected, or when the script\n" + "ends, call dtorfunc(obj). String desc is logged in the error message, if any.\n" + "Use the returned proxy object (or proxy._target) like obj.\n" + "obj won't be destroyed as long as the proxy exists; it's the proxy object's\n" + "lifespan that determines when dtorfunc(obj) will be called.") +{ + if (lua_gettop(L) != 3) + { + return lluau::error(L, "setdtor(desc, obj, dtor) requires exactly 3 arguments"); + } + // called with (desc, obj, dtor), returns proxy object + lua_checkdelta(L, -2); +// lluau_checkstack(L, 0); // might get up to 3 stack entries + auto desc{ lua_tostdstring(L, 1) }; + // Get Lua "references" for each of the object and the dtor function. + int objref = lua_ref(L, 2); + int dtorref = lua_ref(L, 3); + // Having captured each of our parameters, discard them. + lua_settop(L, 0); + // Push our setdtor_refs userdata. Not only do we want to push it on L's + // stack, but setdtor_refs's constructor itself requires L. + lua_emplace<setdtor_refs>(L, L, desc, objref, dtorref); + // stack: proxy (i.e. setdtor_refs userdata) + // have to set its metatable + lua_getfield(L, LUA_REGISTRYINDEX, "setdtor_meta"); + // stack: proxy, setdtor_meta (which might be nil) + if (lua_isnil(L, -1)) + { + // discard nil + lua_pop(L, 1); + // compile and push our forwarding metatable + setdtor_refs::push_metatable(L); + // stack: proxy, metatable + // duplicate metatable to save it + lua_pushvalue(L, -1); + // stack: proxy, metatable, metable + // save metatable for future calls + lua_setfield(L, LUA_REGISTRYINDEX, "setdtor_meta"); + // stack: proxy, metatable + } + // stack: proxy, metatable + lua_setmetatable(L, -2); + // stack: proxy + // Because ~setdtor_refs() necessarily uses the Lua stack, the Registry et + // al., we can't let a setdtor_refs instance be destroyed by lua_close(): + // the Lua environment will already be partially shut down. To destroy + // this new setdtor_refs instance BEFORE lua_close(), bind it with + // lua_destroybounduserdata() and register it with LL.atexit(). + // push (the entry point for) LL.atexit() + lua_pushcfunction(L, atexit_luasub::call, "LL.atexit()"); + // stack: proxy, atexit() + lua_pushvalue(L, -2); + // stack: proxy, atexit(), proxy + int tag = lua_userdatatag(L, -1); + // We don't have a lookup table to get from an int Lua userdata tag to the + // corresponding C++ typeinfo name string. We'll introduce one if we need + // it for debugging. But for this particular call, we happen to know it's + // always a setdtor_refs object. + lua_pushcclosure(L, lua_destroybounduserdata, + stringize("lua_destroybounduserdata<", tag, ">()").c_str(), + 1); + // stack: proxy, atexit(), lua_destroybounduserdata + // call atexit(): one argument, no results, let error propagate + lua_call(L, 1, 0); + // stack: proxy + return 1; +} + +namespace { + +void setdtor_refs::push_metatable(lua_State* L) +{ + lua_checkdelta(L, 1); + lluau_checkstack(L, 1); + // Ideally we want a metatable that forwards every operation on our + // setdtor_refs userdata proxy object to the original object. But the + // published C API doesn't include (e.g.) arithmetic operations on Lua + // objects, so in fact it's easier to express the desired metatable in Lua + // than in C++. We could make setdtor() depend on an external Lua module, + // but it seems less fragile to embed the Lua source code right here. + static const std::string setdtor_meta = stringize(R"-( + -- This metatable literal doesn't define __index() because that's + -- implemented in C++. We cannot, in Lua, peek into the setdtor_refs + -- userdata object to obtain objref, nor can we fetch Registry[objref]. + -- So our C++ __index() metamethod recognizes access to '_target' as a + -- reference to Registry[objref]. + -- The rest are defined per https://www.lua.org/manual/5.1/manual.html#2.8. + -- Luau supports destructors instead of __gc metamethod -- we rely on that! + -- We don't set __mode because our proxy is not a table. Real references + -- are stored in the wrapped table, so ITS __mode is what counts. + -- Initial definition of meta omits binary metamethods so they can bind the + -- metatable itself, as explained for binop() below. + local meta = { + __unm = function(arg) + return -arg._target + end, + __len = function(arg) + return #arg._target + end, + -- Comparison metamethods __eq(), __lt() and __le() are only called + -- when both operands have the same metamethod. For our purposes, that + -- means both operands are setdtor_refs userdata objects. + __eq = function(lhs, rhs) + return (lhs._target == rhs._target) + end, + __lt = function(lhs, rhs) + return (lhs._target < rhs._target) + end, + __le = function(lhs, rhs) + return (lhs._target <= rhs._target) + end, + __newindex = function(t, key, value) + assert(key ~= '_target', + "Don't try to replace a setdtor() proxy's _target") + t._target[key] = value + end, + __call = function(func, ...) + return func._target(...) + end, + __tostring = function(arg) + -- don't fret about arg._target's __tostring metamethod, + -- if any, because built-in tostring() deals with that + return tostring(arg._target) + end, + __iter = function(arg) + local iter = (getmetatable(arg._target) or {}).__iter + if iter then + return iter(arg._target) + else + return next, arg._target + end + end + } +)-", + binop("add", "+"), + binop("sub", "-"), + binop("mul", "*"), + binop("div", "/"), + binop("idiv", "//"), + binop("mod", "%"), + binop("pow", "^"), + binop("concat", ".."), +R"-( + return meta +)-"); + // only needed for debugging binop() +// LL_DEBUGS("Lua") << setdtor_meta << LL_ENDL; + + if (lluau::dostring(L, LL_PRETTY_FUNCTION, setdtor_meta) != LUA_OK) + { + // stack: error message string + lua_error(L); + } + llassert(lua_gettop(L) > 0); + llassert(lua_type(L, -1) == LUA_TTABLE); + // stack: Lua metatable compiled from setdtor_meta source + // Inject our C++ __index metamethod. + lua_rawsetfield(L, -1, "__index"sv, &setdtor_refs::meta__index); +} + +// In the definition of setdtor_meta above, binary arithmethic and +// concatenation metamethods are a little funny in that we don't know a +// priori which operand is the userdata with our metatable: the metamethod +// can be invoked either way. So every such metamethod must check, which +// leads to lots of redundancy. Hence this helper function. Call it a Lua +// macro. +std::string setdtor_refs::binop(const std::string& name, const std::string& op) +{ + return stringize( + " meta.__", name, " = function(lhs, rhs)\n" + " if getmetatable(lhs) == meta then\n" + " return lhs._target ", op, " rhs\n" + " else\n" + " return lhs ", op, " rhs._target\n" + " end\n" + " end\n"); +} + +// setdtor_refs __index() metamethod +int setdtor_refs::meta__index(lua_State* L) +{ + // called with (setdtor_refs userdata, key), returns retrieved object + lua_checkdelta(L, -1); + lluau_checkstack(L, 2); + // stack: proxy, key + // get ptr to the C++ struct data + auto ptr = lua_toclass<setdtor_refs>(L, -2); + // meta__index() should NEVER be called with anything but setdtor_refs! + llassert(ptr); + // push the wrapped object + lua_getref(L, ptr->objref); + // stack: proxy, key, _target + // replace userdata with _target + lua_replace(L, -3); + // stack: _target, key + // Duplicate key because lua_tostring() converts number to string: + // if the key is (e.g.) 1, don't try to retrieve _target["1"]! + lua_pushvalue(L, -1); + // stack: _target, key, key + // recognize the special _target field + if (lua_tostdstring(L, -1) == "_target") + { + // okay, ditch both copies of "_target" string key + lua_pop(L, 2); + // stack: _target + } + else // any key but _target + { + // ditch stringized key + lua_pop(L, 1); + // stack: _target, key + // replace key with _target[key], invoking metamethod if any + lua_gettable(L, -2); + // stack: _target, _target[key] + // discard _target + lua_remove(L, -2); + // stack: _target[key] + } + return 1; +} + +// replacement for global next(): +// its lua_upvalueindex(1) is the original function it's replacing +int lua_proxydrill(lua_State* L) +{ + // Accept however many arguments the original function normally accepts. + // If our first arg is a userdata, check if it's a setdtor_refs proxy. + // Drill through as many levels of proxy wrapper as needed. + while (const setdtor_refs* ptr = lua_toclass<setdtor_refs>(L, 1)) + { + // push original object + lua_getref(L, ptr->objref); + // replace first argument with that + lua_replace(L, 1); + } + // We've reached a first argument that's not a setdtor() proxy. + // How many arguments were we passed, anyway? + int args = lua_gettop(L); + // Push the original function, captured as our upvalue. + lua_pushvalue(L, lua_upvalueindex(1)); + // Shift the stack so the original function is first. + lua_insert(L, 1); + // Call the original function with all original args, no error checking. + // Don't truncate however many values that function returns. + lua_call(L, args, LUA_MULTRET); + // Return as many values as the original function returned. + return lua_gettop(L); +} + +// When Lua destroys a setdtor_refs userdata object, either from garbage +// collection or from LL.atexit(lua_destroybounduserdata), it's time to keep +// its promise to call the specified Lua destructor function with the +// specified Lua object. Of course we must also delete the captured +// "references" to both objects. +setdtor_refs::~setdtor_refs() +{ + lua_checkdelta(L); + lluau_checkstack(L, 2); + // push Registry[dtorref] + lua_getref(L, dtorref); + // push Registry[objref] + lua_getref(L, objref); + // free Registry[dtorref] + lua_unref(L, dtorref); + // free Registry[objref] + lua_unref(L, objref); + // call dtor(obj): one arg, no result, no error function + int rc = lua_pcall(L, 1, 0, 0); + if (rc != LUA_OK) + { + // TODO: we don't really want to propagate the error here. + // If this setdtor_refs instance is being destroyed by + // LL.atexit(), we want to continue cleanup. If it's being + // garbage-collected, the call is completely unpredictable from + // the consuming script's point of view. But what to do about this + // error?? For now, just log it. + LL_WARNS("Lua") << LLCoros::getName() + << ": setdtor(" << std::quoted(desc) << ") error: " + << lua_tostring(L, -1) << LL_ENDL; + lua_pop(L, 1); + } +} + +} // anonymous namespace + +/***************************************************************************** * lua_what *****************************************************************************/ std::ostream& operator<<(std::ostream& out, const lua_what& self) @@ -1024,6 +1542,31 @@ std::ostream& operator<<(std::ostream& out, const lua_what& self) out << lua_touserdata(self.L, self.index); break; + case LUA_TFUNCTION: + { + // Try for the function's name, at the cost of a few more stack + // entries. + lua_checkdelta(self.L); + lluau_checkstack(self.L, 3); + lua_getglobal(self.L, "debug"); + // stack: ..., debug + lua_getfield(self.L, -1, "info"); + // stack: ..., debug, debug.info + lua_remove(self.L, -2); + // stack: ..., debug.info + lua_pushvalue(self.L, self.index); + // stack: ..., debug.info, this function + lua_pushstring(self.L, "n"); + // stack: ..., debug.info, this function, "n" + // 2 arguments, 1 return value (or error message), no error handler + lua_pcall(self.L, 2, 1, 0); + // stack: ..., function name (or error) from debug.info() + out << "function " << lua_tostdstring(self.L, -1); + lua_pop(self.L, 1); + // stack: ... + break; + } + default: // anything else, don't bother trying to report value, just type out << lua_typename(self.L, lua_type(self.L, self.index)); @@ -1066,7 +1609,8 @@ LuaStackDelta::~LuaStackDelta() // instance to keep its contract wrt the Lua data stack. if (std::uncaught_exceptions() == 0 && mDepth + mDelta != depth) { - LL_ERRS("Lua") << mWhere << ": Lua stack went from " << mDepth << " to " << depth; + LL_ERRS("Lua") << LLCoros::getName() << ": " << mWhere + << ": Lua stack went from " << mDepth << " to " << depth; if (mDelta) { LL_CONT << ", rather than expected " << (mDepth + mDelta) << " (" << mDelta << ")"; diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index b9d640f84f..e28656c03b 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -339,6 +339,7 @@ auto lua_getfieldv(lua_State* L, int index, const char* k) template <typename T> auto lua_setfieldv(lua_State* L, int index, const char* k, const T& value) { + index = lua_absindex(L, index); lua_checkdelta(L); lluau_checkstack(L, 1); lua_push(L, value); @@ -349,6 +350,7 @@ auto lua_setfieldv(lua_State* L, int index, const char* k, const T& value) template <typename T> auto lua_rawgetfield(lua_State* L, int index, const std::string_view& k) { + index = lua_absindex(L, index); lua_checkdelta(L); lluau_checkstack(L, 1); lua_pushlstring(L, k.data(), k.length()); @@ -361,6 +363,7 @@ auto lua_rawgetfield(lua_State* L, int index, const std::string_view& k) template <typename T> void lua_rawsetfield(lua_State* L, int index, const std::string_view& k, const T& value) { + index = lua_absindex(L, index); lua_checkdelta(L); lluau_checkstack(L, 2); lua_pushlstring(L, k.data(), k.length()); @@ -459,7 +462,7 @@ DistinctInt TypeTag<T>::value; * containing a newly-constructed C++ object T(args...). The userdata has a * Luau destructor guaranteeing that the new T instance is destroyed when the * userdata is garbage-collected, no later than when the LuaState is - * destroyed. + * destroyed. It may be destroyed explicitly by calling lua_destroyuserdata(). * * Usage: * lua_emplace<T>(L, T constructor args...); @@ -511,6 +514,19 @@ T* lua_toclass(lua_State* L, int index) return static_cast<T*>(ptr); } +/** + * Call lua_destroyuserdata() with the doomed userdata on the stack top. + * It must have been created by lua_emplace(). + */ +int lua_destroyuserdata(lua_State* L); + +/** + * Call lua_pushcclosure(L, lua_destroybounduserdata, 1) with the target + * userdata on the stack top. When the resulting C closure is called with no + * arguments, the bound userdata is destroyed by lua_destroyuserdata(). + */ +int lua_destroybounduserdata(lua_State *L); + /***************************************************************************** * lua_what() *****************************************************************************/ diff --git a/indra/llcommon/resultset.cpp b/indra/llcommon/resultset.cpp new file mode 100644 index 0000000000..4d7b00eabd --- /dev/null +++ b/indra/llcommon/resultset.cpp @@ -0,0 +1,96 @@ +/** + * @file resultset.cpp + * @author Nat Goodspeed + * @date 2024-09-03 + * @brief Implementation for resultset. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "resultset.h" +// STL headers +// std headers +// external library headers +// other Linden headers +#include "llerror.h" +#include "llsdutil.h" + +namespace LL +{ + +LLSD ResultSet::getKeyLength() const +{ + return llsd::array(getKey(), getLength()); +} + +std::pair<LLSD, int> ResultSet::getSliceStart(int index, int count) const +{ + // only call getLength() once + auto length = getLength(); + // Adjust bounds [start, end) to overlap the actual result set from + // [0, getLength()). Permit negative index; e.g. with a result set + // containing 5 entries, getSlice(-2, 5) will adjust start to 0 and + // end to 3. + int start = llclamp(index, 0, length); + int end = llclamp(index + count, 0, length); + LLSD result{ LLSD::emptyArray() }; + // beware of count <= 0, or an [index, count) range that doesn't even + // overlap [0, length) at all + if (end > start) + { + // Right away expand the result array to the size we'll need. + // (end - start) is that size; (end - start - 1) is the index of the + // last entry in result. + result[end - start - 1] = LLSD(); + for (int i = 0; (start + i) < end; ++i) + { + // For this to be a slice, set result[0] = getSingle(start), etc. + result[i] = getSingle(start + i); + } + } + return { result, start }; +} + +LLSD ResultSet::getSlice(int index, int count) const +{ + return getSliceStart(index, count).first; +} + +/*==========================================================================*| +LLSD ResultSet::getSingle(int index) const +{ + if (0 <= index && index < getLength()) + { + return getSingle_(index); + } + else + { + return {}; + } +} +|*==========================================================================*/ + +ResultSet::ResultSet(const std::string& name): + mName(name) +{ + LL_DEBUGS("Lua") << *this << LL_ENDL; +} + +ResultSet::~ResultSet() +{ + // We want to be able to observe that the consuming script eventually + // destroys each of these ResultSets. + LL_DEBUGS("Lua") << "~" << *this << LL_ENDL; +} + +} // namespace LL + +std::ostream& operator<<(std::ostream& out, const LL::ResultSet& self) +{ + return out << "ResultSet(" << self.mName << ", " << self.getKey() << ")"; +} diff --git a/indra/llcommon/resultset.h b/indra/llcommon/resultset.h new file mode 100644 index 0000000000..90d52b6fe4 --- /dev/null +++ b/indra/llcommon/resultset.h @@ -0,0 +1,61 @@ +/** + * @file resultset.h + * @author Nat Goodspeed + * @date 2024-09-03 + * @brief ResultSet is an abstract base class to allow scripted access to + * potentially large collections representable as LLSD arrays. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_RESULTSET_H) +#define LL_RESULTSET_H + +#include "llinttracker.h" +#include "llsd.h" +#include <iosfwd> // std::ostream +#include <utility> // std::pair + +namespace LL +{ + +// This abstract base class defines an interface by which a large collection +// of items representable as an LLSD array can be retrieved in slices. It isa +// LLIntTracker so we can pass its unique int key to a consuming script via +// LLSD. +struct ResultSet: public LLIntTracker<ResultSet> +{ + // Get the length of the result set. Indexes are 0-relative. + virtual int getLength() const = 0; + // Get conventional LLSD { key, length } pair. + LLSD getKeyLength() const; + // Retrieve LLSD corresponding to a single entry from the result set, + // once we're sure the index is valid. + virtual LLSD getSingle(int index) const = 0; + // Retrieve LLSD corresponding to a "slice" of the result set: a + // contiguous sub-array starting at index. The returned LLSD array might + // be shorter than count entries if count > MAX_ITEM_LIMIT, or if the + // specified slice contains the end of the result set. + LLSD getSlice(int index, int count) const; + // Like getSlice(), but also return adjusted start position. + std::pair<LLSD, int> getSliceStart(int index, int count) const; +/*==========================================================================*| + // Retrieve LLSD corresponding to a single entry from the result set, + // with index validation. + LLSD getSingle(int index) const; +|*==========================================================================*/ + + /*---------------- the rest is solely for debug logging ----------------*/ + std::string mName; + + ResultSet(const std::string& name); + virtual ~ResultSet(); +}; + +} // namespace LL + +std::ostream& operator<<(std::ostream& out, const LL::ResultSet& self); + +#endif /* ! defined(LL_RESULTSET_H) */ diff --git a/indra/llcommon/stringize.h b/indra/llcommon/stringize.h index e712b6cc28..2730728637 100644 --- a/indra/llcommon/stringize.h +++ b/indra/llcommon/stringize.h @@ -190,16 +190,4 @@ void destringize_f(std::basic_string<CHARTYPE> const & str, Functor const & f) f(in); } -/** - * DESTRINGIZE(str, item1 >> item2 >> item3 ...) effectively expands to the - * following: - * @code - * std::istringstream in(str); - * in >> item1 >> item2 >> item3 ... ; - * @endcode - */ -#define DESTRINGIZE(STR, EXPRESSION) (destringize_f((STR), [&](auto& in){in >> EXPRESSION;})) -// legacy name, just use DESTRINGIZE() going forward -#define DEWSTRINGIZE(STR, EXPRESSION) DESTRINGIZE(STR, EXPRESSION) - #endif /* ! defined(LL_STRINGIZE_H) */ diff --git a/indra/llui/llfloaterreglistener.cpp b/indra/llui/llfloaterreglistener.cpp index 065246ac9b..6e5f048c27 100644 --- a/indra/llui/llfloaterreglistener.cpp +++ b/indra/llui/llfloaterreglistener.cpp @@ -38,6 +38,7 @@ #include "llfloater.h" #include "llbutton.h" #include "llluafloater.h" +#include "resultset.h" LLFloaterRegListener::LLFloaterRegListener(): LLEventAPI("LLFloaterReg", @@ -82,9 +83,9 @@ LLFloaterRegListener::LLFloaterRegListener(): &LLFloaterRegListener::getLuaFloaterEvents); add("getFloaterNames", - "Return the table of all registered floaters", + "Return result set key [\"floaters\"] for names of all registered floaters", &LLFloaterRegListener::getFloaterNames, - llsd::map("reply", LLSD())); + llsd::map("reply", LLSD::String())); } void LLFloaterRegListener::getBuildMap(const LLSD& event) const @@ -126,10 +127,22 @@ void LLFloaterRegListener::instanceVisible(const LLSD& event) const event); } +struct NameResultSet: public LL::ResultSet +{ + NameResultSet(): + LL::ResultSet("floaters"), + mNames(LLFloaterReg::getFloaterNames()) + {} + LLSD mNames; + + int getLength() const override { return narrow(mNames.size()); } + LLSD getSingle(int index) const override { return mNames[index]; } +}; void LLFloaterRegListener::getFloaterNames(const LLSD &event) const { - Response response(llsd::map("floaters", LLFloaterReg::getFloaterNames()), event); + auto nameresult = new NameResultSet; + sendReply(llsd::map("floaters", nameresult->getKeyLength()), event); } void LLFloaterRegListener::clickButton(const LLSD& event) const @@ -178,4 +191,3 @@ void LLFloaterRegListener::getLuaFloaterEvents(const LLSD &event) const { Response response(llsd::map("events", LLLuaFloater::getEventsData()), event); } - diff --git a/indra/newview/llinventorylistener.cpp b/indra/newview/llinventorylistener.cpp index 753ad3ddeb..79726c3e0b 100644 --- a/indra/newview/llinventorylistener.cpp +++ b/indra/newview/llinventorylistener.cpp @@ -31,7 +31,9 @@ #include "llinventoryfunctions.h" #include "lltransutil.h" #include "llwearableitemslist.h" +#include "resultset.h" #include "stringize.h" +#include <algorithm> // std::min() constexpr S32 MAX_ITEM_LIMIT = 100; @@ -41,7 +43,7 @@ LLInventoryListener::LLInventoryListener() { add("getItemsInfo", "Return information about items or folders defined in [\"item_ids\"]:\n" - "reply will contain [\"items\"] and [\"categories\"] tables accordingly", + "reply will contain [\"items\"] and [\"categories\"] result set keys", &LLInventoryListener::getItemsInfo, llsd::map("item_ids", LLSD(), "reply", LLSD())); @@ -61,78 +63,115 @@ LLInventoryListener::LLInventoryListener() &LLInventoryListener::getBasicFolderID, llsd::map("ft_name", LLSD(), "reply", LLSD())); - add("getDirectDescendents", - "Return the direct descendents(both items and folders) of the [\"folder_id\"]", - &LLInventoryListener::getDirectDescendents, + add("getDirectDescendants", + "Return result set keys [\"categories\"] and [\"items\"] for the direct\n" + "descendants of the [\"folder_id\"]", + &LLInventoryListener::getDirectDescendants, llsd::map("folder_id", LLSD(), "reply", LLSD())); - add("collectDescendentsIf", - "Return the descendents(both items and folders) of the [\"folder_id\"], if it passes specified filters:\n" + add("collectDescendantsIf", + "Return result set keys [\"categories\"] and [\"items\"] for the descendants\n" + "of the [\"folder_id\"], if it passes specified filters:\n" "[\"name\"] is a substring of object's name,\n" "[\"desc\"] is a substring of object's description,\n" "asset [\"type\"] corresponds to the string name of the object's asset type\n" - "[\"limit\"] sets item count limit in reply, maximum and default is 100\n" + "[\"limit\"] sets item count limit in result set (default unlimited)\n" "[\"filter_links\"]: EXCLUDE_LINKS - don't show links, ONLY_LINKS - only show links, INCLUDE_LINKS - show links too (default)", - &LLInventoryListener::collectDescendentsIf, + &LLInventoryListener::collectDescendantsIf, llsd::map("folder_id", LLSD(), "reply", LLSD())); - } - -void add_item_info(LLEventAPI::Response& response, LLViewerInventoryItem* item) -{ - response["items"].insert(item->getUUID().asString(), - llsd::map("name", item->getName(), - "parent_id", item->getParentUUID(), - "desc", item->getDescription(), - "inv_type", LLInventoryType::lookup(item->getInventoryType()), - "asset_type", LLAssetType::lookup(item->getType()), - "creation_date", (S32) item->getCreationDate(), - "asset_id", item->getAssetUUID(), - "is_link", item->getIsLinkType(), - "linked_id", item->getLinkedUUID())); +/*==========================================================================*| + add("getSingle", + "Return LLSD [\"single\"] for a single folder or item from the specified\n" + "[\"result\"] key at the specified 0-relative [\"index\"].", + &LLInventoryListener::getSingle, + llsd::map("result", LLSD::Integer(), "index", LLSD::Integer(), + "reply", LLSD::String())); +|*==========================================================================*/ + + add("getSlice", + stringize( + "Return an LLSD array [\"slice\"] from the specified [\"result\"] key\n" + "starting at 0-relative [\"index\"] with (up to) [\"count\"] entries.\n" + "count is limited to ", MAX_ITEM_LIMIT, " (default and max)."), + &LLInventoryListener::getSlice, + llsd::map("result", LLSD::Integer(), "index", LLSD::Integer(), + "reply", LLSD::String())); + + add("closeResult", + "Release resources associated with specified [\"result\"] key,\n" + "or keys if [\"result\"] is an array.", + &LLInventoryListener::closeResult, + llsd::map("result", LLSD())); } -void add_cat_info(LLEventAPI::Response &response, LLViewerInventoryCategory *cat) +// This struct captures (possibly large) category results from +// getDirectDescendants() and collectDescendantsIf(). +struct CatResultSet: public LL::ResultSet { - response["categories"].insert(cat->getUUID().asString(), - llsd::map("name", cat->getName(), - "parent_id", cat->getParentUUID(), - "type", LLFolderType::lookup(cat->getPreferredType()))); -} + CatResultSet(): LL::ResultSet("categories") {} + LLInventoryModel::cat_array_t mCategories; -void add_objects_info(LLEventAPI::Response& response, LLInventoryModel::cat_array_t cat_array, LLInventoryModel::item_array_t item_array) -{ - for (auto &p : item_array) + int getLength() const override { return narrow(mCategories.size()); } + LLSD getSingle(int index) const override { - add_item_info(response, p); + auto cat = mCategories[index]; + return llsd::map("name", cat->getName(), + "parent_id", cat->getParentUUID(), + "type", LLFolderType::lookup(cat->getPreferredType())); } - for (auto &p : cat_array) +}; + +// This struct captures (possibly large) item results from +// getDirectDescendants() and collectDescendantsIf(). +struct ItemResultSet: public LL::ResultSet +{ + ItemResultSet(): LL::ResultSet("items") {} + LLInventoryModel::item_array_t mItems; + + int getLength() const override { return narrow(mItems.size()); } + LLSD getSingle(int index) const override { - add_cat_info(response, p); + auto item = mItems[index]; + return llsd::map("name", item->getName(), + "parent_id", item->getParentUUID(), + "desc", item->getDescription(), + "inv_type", LLInventoryType::lookup(item->getInventoryType()), + "asset_type", LLAssetType::lookup(item->getType()), + "creation_date", LLSD::Integer(item->getCreationDate()), + "asset_id", item->getAssetUUID(), + "is_link", item->getIsLinkType(), + "linked_id", item->getLinkedUUID()); } -} +}; void LLInventoryListener::getItemsInfo(LLSD const &data) { Response response(LLSD(), data); + auto catresult = new CatResultSet; + auto itemresult = new ItemResultSet; + uuid_vec_t ids = LLSDParam<uuid_vec_t>(data["item_ids"]); for (auto &it : ids) { LLViewerInventoryItem* item = gInventory.getItem(it); if (item) { - add_item_info(response, item); + itemresult->mItems.push_back(item); } else { LLViewerInventoryCategory *cat = gInventory.getCategory(it); if (cat) { - add_cat_info(response, cat); + catresult->mCategories.push_back(cat); } } } + // Each of categories and items is a { result set key, total length } pair. + response["categories"] = catresult->getKeyLength(); + response["items"] = itemresult->getKeyLength(); } void LLInventoryListener::getFolderTypeNames(LLSD const &data) @@ -151,14 +190,21 @@ void LLInventoryListener::getBasicFolderID(LLSD const &data) } -void LLInventoryListener::getDirectDescendents(LLSD const &data) +void LLInventoryListener::getDirectDescendants(LLSD const &data) { Response response(LLSD(), data); LLInventoryModel::cat_array_t* cats; LLInventoryModel::item_array_t* items; gInventory.getDirectDescendentsOf(data["folder_id"], cats, items); - add_objects_info(response, *cats, *items); + auto catresult = new CatResultSet; + auto itemresult = new ItemResultSet; + + catresult->mCategories = *cats; + itemresult->mItems = *items; + + response["categories"] = catresult->getKeyLength(); + response["items"] = itemresult->getKeyLength(); } struct LLFilteredCollector : public LLInventoryCollectFunctor @@ -173,7 +219,11 @@ struct LLFilteredCollector : public LLInventoryCollectFunctor LLFilteredCollector(LLSD const &data); virtual ~LLFilteredCollector() {} virtual bool operator()(LLInventoryCategory *cat, LLInventoryItem *item) override; - virtual bool exceedsLimit() override { return (mItemLimit <= mItemCount); }; + virtual bool exceedsLimit() override + { + // mItemLimit == 0 means unlimited + return (mItemLimit && mItemLimit <= mItemCount); + } protected: bool checkagainstType(LLInventoryCategory *cat, LLInventoryItem *item); @@ -189,7 +239,7 @@ struct LLFilteredCollector : public LLInventoryCollectFunctor S32 mItemCount; }; -void LLInventoryListener::collectDescendentsIf(LLSD const &data) +void LLInventoryListener::collectDescendantsIf(LLSD const &data) { Response response(LLSD(), data); LLUUID folder_id(data["folder_id"].asUUID()); @@ -198,20 +248,64 @@ void LLInventoryListener::collectDescendentsIf(LLSD const &data) { return response.error(stringize("Folder ", std::quoted(data["folder_id"].asString()), " was not found")); } - LLInventoryModel::cat_array_t cat_array; - LLInventoryModel::item_array_t item_array; + auto catresult = new CatResultSet; + auto itemresult = new ItemResultSet; LLFilteredCollector collector = LLFilteredCollector(data); - gInventory.collectDescendentsIf(folder_id, cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH, collector); + // Populate results directly into the catresult and itemresult arrays. + // TODO: sprinkle count-based coroutine yields into the real + // collectDescendentsIf() method so it doesn't steal too many cycles. + gInventory.collectDescendentsIf( + folder_id, + catresult->mCategories, + itemresult->mItems, + LLInventoryModel::EXCLUDE_TRASH, + collector); + + response["categories"] = catresult->getKeyLength(); + response["items"] = itemresult->getKeyLength(); +} + +/*==========================================================================*| +void LLInventoryListener::getSingle(LLSD const& data) +{ + auto result = LL::ResultSet::getInstance(data["result"]); + sendReply(llsd::map("single", result->getSingle(data["index"])), data); +} +|*==========================================================================*/ + +void LLInventoryListener::getSlice(LLSD const& data) +{ + auto result = LL::ResultSet::getInstance(data["result"]); + int count = data.has("count")? data["count"].asInteger() : MAX_ITEM_LIMIT; + LL_DEBUGS("Lua") << *result << ".getSlice(" << data["index"].asInteger() + << ", " << count << ')' << LL_ENDL; + auto pair{ result->getSliceStart(data["index"], std::min(count, MAX_ITEM_LIMIT)) }; + sendReply(llsd::map("slice", pair.first, "start", pair.second), data); +} - add_objects_info(response, cat_array, item_array); +void LLInventoryListener::closeResult(LLSD const& data) +{ + LLSD results = data["result"]; + if (results.isInteger()) + { + results = llsd::array(results); + } + for (const auto& result : llsd::inArray(results)) + { + auto ptr = LL::ResultSet::getInstance(result); + if (ptr) + { + delete ptr.get(); + } + } } LLFilteredCollector::LLFilteredCollector(LLSD const &data) : mType(LLAssetType::EType::AT_UNKNOWN), mLinkFilter(INCLUDE_LINKS), - mItemLimit(MAX_ITEM_LIMIT), + mItemLimit(0), mItemCount(0) { @@ -235,7 +329,7 @@ LLFilteredCollector::LLFilteredCollector(LLSD const &data) : } if (data["limit"].isInteger()) { - mItemLimit = llclamp(data["limit"].asInteger(), 1, MAX_ITEM_LIMIT); + mItemLimit = std::max(data["limit"].asInteger(), 1); } } diff --git a/indra/newview/llinventorylistener.h b/indra/newview/llinventorylistener.h index 5cbac2ca32..a05385f2c8 100644 --- a/indra/newview/llinventorylistener.h +++ b/indra/newview/llinventorylistener.h @@ -40,8 +40,13 @@ private: void getFolderTypeNames(LLSD const &data); void getAssetTypeNames(LLSD const &data); void getBasicFolderID(LLSD const &data); - void getDirectDescendents(LLSD const &data); - void collectDescendentsIf(LLSD const &data); + void getDirectDescendants(LLSD const &data); + void collectDescendantsIf(LLSD const &data); +/*==========================================================================*| + void getSingle(LLSD const& data); +|*==========================================================================*/ + void getSlice(LLSD const& data); + void closeResult(LLSD const& data); }; #endif // LL_LLINVENTORYLISTENER_H diff --git a/indra/newview/scripts/lua/require/LLInventory.lua b/indra/newview/scripts/lua/require/LLInventory.lua index dd1b910250..2c80a8602b 100644 --- a/indra/newview/scripts/lua/require/LLInventory.lua +++ b/indra/newview/scripts/lua/require/LLInventory.lua @@ -1,12 +1,28 @@ local leap = require 'leap' local mapargs = require 'mapargs' +local result_view = require 'result_view' + +local function result(keys) + -- capture result_view() instances for both categories and items + local result_table = { + categories=result_view(keys.categories), + items=result_view(keys.items), + -- call result_table:close() to release result sets before garbage + -- collection or script completion + close = function(self) + result_view.close(keys.categories[1], keys.items[1]) + end + } + -- When the result_table is destroyed, close its result_views. + return LL.setdtor('LLInventory result', result_table, result_table.close) +end local LLInventory = {} -- Get the items/folders info by provided IDs, -- reply will contain "items" and "categories" tables accordingly function LLInventory.getItemsInfo(item_ids) - return leap.request('LLInventory', {op = 'getItemsInfo', item_ids=item_ids}) + return result(leap.request('LLInventory', {op = 'getItemsInfo', item_ids=item_ids})) end -- Get the table of folder type names, which can be later used to get the ID of the basic folders @@ -19,30 +35,33 @@ function LLInventory.getBasicFolderID(ft_name) return leap.request('LLInventory', {op = 'getBasicFolderID', ft_name=ft_name}).id end --- Get the table of asset type names, which can be later used to get the specific items via LLInventory.collectDescendentsIf(...) +-- Get the table of asset type names, which can be later used to get the specific items via LLInventory.collectDescendantsIf(...) function LLInventory.getAssetTypeNames() return leap.request('LLInventory', {op = 'getAssetTypeNames'}).names end --- Get the direct descendents of the 'folder_id' provided, +-- Get the direct descendants of the 'folder_id' provided, -- reply will contain "items" and "categories" tables accordingly -function LLInventory.getDirectDescendents(folder_id) - return leap.request('LLInventory', {op = 'getDirectDescendents', folder_id=folder_id}) +function LLInventory.getDirectDescendants(folder_id) + return result(leap.request('LLInventory', {op = 'getDirectDescendants', folder_id=folder_id})) end +-- backwards compatibility +LLInventory.getDirectDescendents = LLInventory.getDirectDescendants --- Get the descendents of the 'folder_id' provided, which pass specified filters +-- Get the descendants of the 'folder_id' provided, which pass specified filters -- reply will contain "items" and "categories" tables accordingly --- LLInventory.collectDescendentsIf{ folder_id -- parent folder ID +-- LLInventory.collectDescendantsIf{ folder_id -- parent folder ID -- [, name] -- name (substring) -- [, desc] -- description (substring) -- [, type] -- asset type -- [, limit] -- item count limit in reply, maximum and default is 100 -- [, filter_links]} -- EXCLUDE_LINKS - don't show links, ONLY_LINKS - only show links, INCLUDE_LINKS - show links too (default) -function LLInventory.collectDescendentsIf(...) +function LLInventory.collectDescendantsIf(...) local args = mapargs('folder_id,name,desc,type,filter_links,limit', ...) - args.op = 'collectDescendentsIf' - return leap.request('LLInventory', args) + args.op = 'collectDescendantsIf' + return result(leap.request('LLInventory', args)) end - +-- backwards compatibility +LLInventory.collectDescendentsIf = LLInventory.collectDescendantsIf return LLInventory diff --git a/indra/newview/scripts/lua/require/UI.lua b/indra/newview/scripts/lua/require/UI.lua index bbcae3514a..73a76fa6b8 100644 --- a/indra/newview/scripts/lua/require/UI.lua +++ b/indra/newview/scripts/lua/require/UI.lua @@ -2,6 +2,7 @@ local leap = require 'leap' local mapargs = require 'mapargs' +local result_view = require 'result_view' local Timer = (require 'timers').Timer local util = require 'util' @@ -234,7 +235,9 @@ function UI.closeAllFloaters() end function UI.getFloaterNames() - return leap.request("LLFloaterReg", {op = "getFloaterNames"}).floaters + local key_length = leap.request("LLFloaterReg", {op = "getFloaterNames"}).floaters + local view = result_view(key_length) + return LL.setdtor('registered floater names', view, view.close) end return UI diff --git a/indra/newview/scripts/lua/require/result_view.lua b/indra/newview/scripts/lua/require/result_view.lua new file mode 100644 index 0000000000..5301d7838c --- /dev/null +++ b/indra/newview/scripts/lua/require/result_view.lua @@ -0,0 +1,98 @@ +local leap = require 'leap' + +-- metatable for every result_view() table +local mt = { + __len = function(self) + return self.length + end, + __index = function(self, i) + -- right away, convert to 0-relative indexing + i -= 1 + -- can we find this index within the current slice? + local reli = i - self.start + if 0 <= reli and reli < #self.slice then + -- Lua 1-relative indexing + return self.slice[reli + 1] + end + -- is this index outside the overall result set? + if not (0 <= i and i < self.length) then + return nil + end + -- fetch a new slice starting at i, using provided fetch() + local start + self.slice, start = self.fetch(self.key, i) + -- It's possible that caller-provided fetch() function forgot + -- to return the adjusted start index of the new slice. In + -- Lua, 0 tests as true, so if fetch() returned (slice, 0), + -- we'll duly reset self.start to 0. Otherwise, assume the + -- requested index was not adjusted: that the returned slice + -- really does start at i. + self.start = start or i + -- Hopefully this slice contains the desired i. + -- Back to 1-relative indexing. + return self.slice[i - self.start + 1] + end, + -- We purposely avoid putting any array entries (int keys) into + -- our table so that access to any int key will always call our + -- __index() metamethod. Moreover, we want any table iteration to + -- call __index(table, i) however many times; we do NOT want it to + -- retrieve key, length, start, slice. + -- So turn 'for k, v in result' into 'for k, v in ipairs(result)'. + __iter = ipairs, + -- This result set provides read-only access. + -- We do not support pushing updates to individual items back to + -- C++; for the intended use cases, that makes no sense. + __newindex = function(self, i, value) + error("result_view is a read-only data structure", 2) + end +} + +-- result_view(key_length, fetch) returns a table which stores only a slice +-- of a result set plus some control values, yet presents read-only virtual +-- access to the entire result set. +-- key_length: {result set key, total result set length} +-- fetch: function(key, start) that returns (slice, adjusted start) +local result_view = setmetatable( + { + -- generic fetch() function + fetch = function(key, start) + local fetched = leap.request( + 'LLInventory', + {op='getSlice', result=key, index=start}) + return fetched.slice, fetched.start + end, + -- generic close() function accepting variadic result-set keys + close = function(...) + local keys = table.pack(...) + -- table.pack() produces a table with an array entry for every + -- parameter, PLUS an 'n' key with the count. Unfortunately that + -- 'n' key bollixes our conversion to LLSD, which requires either + -- all int keys (for an array) or all string keys (for a map). + keys.n = nil + leap.send('LLInventory', {op='closeResult', result=keys}) + end + }, + { + -- result_view(key_length, fetch) calls this + __call = function(class, key_length, fetch) + return setmetatable( + { + key=key_length[1], + length=key_length[2], + -- C++ result sets use 0-based indexing, so internally we do too + start=0, + -- start with a dummy array with length 0 + slice={}, + -- if caller didn't pass fetch() function, use generic + fetch=fetch or class.fetch, + -- returned view:close() will close result set with passed key + close=function(self) class.close(key_length[1]) end + }, + -- use our special metatable + mt + ) + end + } +) + +return result_view diff --git a/indra/newview/scripts/lua/test_LLInventory.lua b/indra/newview/scripts/lua/test_LLInventory.lua index 107b0791d4..de57484bcd 100644 --- a/indra/newview/scripts/lua/test_LLInventory.lua +++ b/indra/newview/scripts/lua/test_LLInventory.lua @@ -5,7 +5,9 @@ LLInventory = require 'LLInventory' my_landmarks_id = LLInventory.getBasicFolderID('landmark') -- Get 3 landmarks from the 'My Landmarks' folder (you can see all folder types via LLInventory.getAssetTypeNames()) landmarks = LLInventory.collectDescendentsIf{folder_id=my_landmarks_id, type="landmark", limit=3} -print(inspect(landmarks)) +for _, landmark in pairs(landmarks.items) do + print(landmark.name) +end -- Get 'Calling Cards' folder id calling_cards_id = LLInventory.getBasicFolderID('callcard') @@ -13,9 +15,10 @@ calling_cards_id = LLInventory.getBasicFolderID('callcard') calling_cards = LLInventory.getDirectDescendents(calling_cards_id).items -- Print a random calling card name from 'Calling Cards' folder -local card_names = {} -for _, value in pairs(calling_cards) do - table.insert(card_names, value.name) -end +-- (because getDirectDescendents().items is a Lua result set, selecting +-- a random entry only fetches one slice containing that entry) math.randomseed(os.time()) -print("Random calling card: " .. inspect(card_names[math.random(#card_names)])) +for i = 1, 5 do + pick = math.random(#calling_cards) + print(`Random calling card (#{pick} of {#calling_cards}): {calling_cards[pick].name}`) +end diff --git a/indra/newview/scripts/lua/test_inv_resultset.lua b/indra/newview/scripts/lua/test_inv_resultset.lua new file mode 100644 index 0000000000..c31cfe3c67 --- /dev/null +++ b/indra/newview/scripts/lua/test_inv_resultset.lua @@ -0,0 +1,18 @@ +local LLInventory = require 'LLInventory' +local inspect = require 'inspect' + +print('basic folders:') +print(inspect(LLInventory.getFolderTypeNames())) + +local folder = LLInventory.getBasicFolderID('my_otfts') +print(`folder = {folder}`) +local result = LLInventory.getDirectDescendants(folder) +print(`type(result) = {type(result)}`) +print(#result.categories, 'categories:') +for i, cat in pairs(result.categories) do + print(`{i}: {cat.name}`) +end +print(#result.items, 'items') +for i, item in pairs(result.items) do + print(`{i}: {item.name}`) +end diff --git a/indra/newview/scripts/lua/test_result_view.lua b/indra/newview/scripts/lua/test_result_view.lua new file mode 100644 index 0000000000..304633a472 --- /dev/null +++ b/indra/newview/scripts/lua/test_result_view.lua @@ -0,0 +1,55 @@ +-- Verify the functionality of result_view. +result_view = require 'result_view' + +print('alphabet') +alphabet = "abcdefghijklmnopqrstuvwxyz" +assert(#alphabet == 26) +alphabits = string.split(alphabet, '') + +print('function slice()') +function slice(t, index, count) + return table.move(t, index, index + count - 1, 1, {}) +end + +print('verify slice()') +-- verify that slice() does what we expect +assert(table.concat(slice(alphabits, 4, 3)) == "def") +assert(table.concat(slice(alphabits, 14, 3)) == "nop") +assert(table.concat(slice(alphabits, 25, 3)) == "yz") + +print('function fetch()') +function fetch(key, index) + -- fetch function is defined to be 0-relative: fix for Lua data + -- constrain view of alphabits to slices of at most 3 elements + return slice(alphabits, index+1, 3), index +end + +print('result_view()') +-- for test purposes, key is irrelevant, so just 'key' +view = result_view({'key', #alphabits}, fetch) + +print('function check_iter()') +function check_iter(...) + result = {} + for k, v in ... do + table.insert(result, v) + end + assert(table.concat(result) == alphabet) +end + +print('check_iter(pairs(view))') +check_iter(pairs(view)) +print('check_iter(ipairs(view))') +check_iter(ipairs(view)) +print('check_iter(view)') +check_iter(view) + +print('raw index access') +assert(view[5] == 'e') +assert(view[10] == 'j') +assert(view[15] == 'o') +assert(view[20] == 't') +assert(view[25] == 'y') + +print('Success!') + diff --git a/indra/newview/scripts/lua/test_setdtor.lua b/indra/newview/scripts/lua/test_setdtor.lua new file mode 100644 index 0000000000..ec5cd47e93 --- /dev/null +++ b/indra/newview/scripts/lua/test_setdtor.lua @@ -0,0 +1,91 @@ +inspect = require 'inspect' + +print('initial setdtor') +bye = LL.setdtor('initial setdtor', 'Goodbye world!', print) + +print('arithmetic') +n = LL.setdtor('arithmetic', 11, print) +print("n =", n) +print("n._target =", n._target) +print(pcall(function() n._target = 12 end)) +print("getmetatable(n) =", inspect(getmetatable(n))) +print("-n =", -n) +for i = 10, 12 do + -- Comparison metamethods are only called if both operands have the same + -- metamethod. + tempi = LL.setdtor('tempi', i, function(n) print('temp', i) end) + print(`n < {i}`, n < tempi) + print(`n <= {i}`, n <= tempi) + print(`n == {i}`, n == tempi) + print(`n ~= {i}`, n ~= tempi) + print(`n >= {i}`, n >= tempi) + print(`n > {i}`, n > tempi) +end +i = 2 +print(`n + {i} =`, n + i) +print(`{i} + n =`, i + n) +print(`n - {i} =`, n - i) +print(`{i} - n =`, i - n) +print(`n * {i} =`, n * i) +print(`{i} * n =`, i * n) +print(`n / {i} =`, n / i) +print(`{i} / n =`, i / n) +print(`n // {i} =`, n // i) +print(`{i} // n =`, i // n) +print(`n % {i} =`, n % i) +print(`{i} % n =`, i % n) +print(`n ^ {i} =`, n ^ i) +print(`{i} ^ n =`, i ^ n) + +print('string') +s = LL.setdtor('string', 'hello', print) +print('s =', s) +print('#s =', #s) +print('s .. " world" =', s .. " world") +print('"world " .. s =', "world " .. s) + +print('table') +t = LL.setdtor('table', {'[1]', '[2]', abc='.abc', def='.def'}, + function(t) print(inspect(t)) end) +print('t =', inspect(t)) +print('t._target =', inspect(t._target)) +print('#t =', #t) +print('next(t) =', next(t)) +print('next(t, 1) =', next(t, 1)) +print('t[2] =', t[2]) +print('t.def =', t.def) +t[1] = 'new [1]' +print('t[1] =', t[1]) +print('for k, v in pairs(t) do') +for k, v in pairs(t) do + print(`{k}: {v}`) +end +print('for k, v in ipairs(t) do') +for k, v in ipairs(t) do + print(`{k}: {v}`) +end +print('for k, v in t do') +for k, v in t do + print(`{k}: {v}`) +end +-- and now for something completely different +setmetatable( + t._target, + { + __iter = function(arg) + return next, {'alternate', '__iter'} + end + } +) +print('for k, v in t with __iter() metamethod do') +for k, v in t do + print(`{k}: {v}`) +end + +print('function') +f = LL.setdtor('function', function(a, b) return (a .. b) end, print) +print('f =', f) +print('f._target =', f._target) +print('f("Hello", " world") =', f("Hello", " world")) + +print('cleanup') diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp index 8ce5c357e0..8d1333815b 100644 --- a/indra/newview/tests/llluamanager_test.cpp +++ b/indra/newview/tests/llluamanager_test.cpp @@ -463,8 +463,8 @@ namespace tut // but now we have to give the startScriptLine() coroutine a chance to run auto [count, result] = future.get(); ensure_equals("killed Lua script terminated normally", count, -1); - ensure_equals("unexpected killed Lua script error", - result.asString(), "viewer is stopping"); + ensure_contains("unexpected killed Lua script error", + result.asString(), "viewer is stopping"); } template<> template<> |