summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNat Goodspeed <nat@lindenlab.com>2024-09-05 14:37:25 -0400
committerNat Goodspeed <nat@lindenlab.com>2024-09-05 14:37:25 -0400
commitff2d79906ccef217194d5d9ec9d7025db03592a8 (patch)
tree83d5db1c173636bb77ebb33e860fac77ab5d79e8
parent25a86618002a397d1d8dabf2ec1f093489b2f816 (diff)
parent18d81e20f0b0044c16615953d7b69d7fb34d3449 (diff)
Merge branch 'release/luau-scripting' into lua-merge-dev
-rw-r--r--indra/llcommon/CMakeLists.txt3
-rw-r--r--indra/llcommon/llinstancetracker.h16
-rw-r--r--indra/llcommon/llinttracker.h43
-rw-r--r--indra/llcommon/lua_function.cpp572
-rw-r--r--indra/llcommon/lua_function.h18
-rw-r--r--indra/llcommon/resultset.cpp96
-rw-r--r--indra/llcommon/resultset.h61
-rw-r--r--indra/llcommon/stringize.h12
-rw-r--r--indra/llui/llfloaterreglistener.cpp20
-rw-r--r--indra/newview/llinventorylistener.cpp188
-rw-r--r--indra/newview/llinventorylistener.h9
-rw-r--r--indra/newview/scripts/lua/require/LLInventory.lua41
-rw-r--r--indra/newview/scripts/lua/require/UI.lua5
-rw-r--r--indra/newview/scripts/lua/require/result_view.lua98
-rw-r--r--indra/newview/scripts/lua/test_LLInventory.lua15
-rw-r--r--indra/newview/scripts/lua/test_inv_resultset.lua18
-rw-r--r--indra/newview/scripts/lua/test_result_view.lua55
-rw-r--r--indra/newview/scripts/lua/test_setdtor.lua91
-rw-r--r--indra/newview/tests/llluamanager_test.cpp4
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<>