From af4fbc1f8a99a3c5370cb6db45435e67f9ce92d2 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Sat, 18 Jun 2022 11:57:10 -0400 Subject: DRTVWR-564: WIP: Add LazyEventAPI and tests. Tests don't yet pass. LazyEventAPI is a registrar that implicitly instantiates some particular LLEventAPI subclass on demand: that is, when LLEventPumps::obtain() tries to find an LLEventPump by the registered name. This leverages the new LLEventPumps::registerPumpFactory() machinery. Fix registerPumpFactory() to adapt the passed PumpFactory to accept TypeFactory parameters (two of which it ignores). Supplement it with unregisterPumpFactory() to support LazyEventAPI instances with lifespans shorter than the process -- which may be mostly test programs, but still a hole worth closing. Similarly, add unregisterTypeFactory(). A LazyEventAPI subclass takes over responsibility for specifying the LLEventAPI's name, desc, field, plus whatever add() calls will be needed to register the LLEventAPI's operations. This is so we can (later) enhance LLLeapListener to consult LazyEventAPI instances for not-yet-instantiated LLEventAPI metadata, as well as enumerating existing LLEventAPI instances. The trickiest part of this is capturing calls to the various LLEventDispatcher::add() overloads in such a way that, when the LLEventAPI subclass is eventually instantiated, we can replay them in the new instance. LLEventAPI acquires a new protected constructor specifically for use by a subclass registered by a companion LazyEventAPI. It accepts a const reference to LazyEventAPIParams, intended to be opaque to the LLEventAPI subclass; the subclass must declare a constructor that accepts and forwards the parameter block to the new LLEventAPI constructor. The implementation delegates to the existing LLEventAPI constructor, plus it runs deferred add() calls. LLDispatchListener now derives from LLEventStream instead of containing it as a data member. The reason is that if LLEventPumps::obtain() implicitly instantiates it, LLEventPumps's destructor will try to destroy it by deleting the LLEventPump*. If the LLEventPump returned by the factory function is a data member of an outer class, that won't work so well. But if LLDispatchListener (and by implication, LLEventAPI and any subclass) is derived from LLEventPump, then the virtual destructor will Do The Right Thing. Change LLDispatchListener to *not* allow tweaking the LLEventPump name. Since the overwhelming use case for LLDispatchListener is LLEventAPI, accepting but silently renaming an LLEventAPI subclass would ensure nobody could reach it. Change LLEventDispatcher's use of std::enable_if to control the set of add() overloads available for the intended use cases. Apparently this formulation is just as functional at the method declaration point, while avoiding the need to restate the whole enable_if expression at the method definition point. Add lazyeventapi_test.cpp to exercise. --- indra/llcommon/lazyeventapi.h | 204 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 indra/llcommon/lazyeventapi.h (limited to 'indra/llcommon/lazyeventapi.h') diff --git a/indra/llcommon/lazyeventapi.h b/indra/llcommon/lazyeventapi.h new file mode 100644 index 0000000000..2e947899dc --- /dev/null +++ b/indra/llcommon/lazyeventapi.h @@ -0,0 +1,204 @@ +/** + * @file lazyeventapi.h + * @author Nat Goodspeed + * @date 2022-06-16 + * @brief Declaring a static module-scope LazyEventAPI registers a specific + * LLEventAPI for future on-demand instantiation. + * + * $LicenseInfo:firstyear=2022&license=viewerlgpl$ + * Copyright (c) 2022, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LAZYEVENTAPI_H) +#define LL_LAZYEVENTAPI_H + +#include "apply.h" +#include "lleventapi.h" +#include "llinstancetracker.h" +#include +#include +#include +#include // std::pair +#include + +namespace LL +{ + /** + * Bundle params we want to pass to LLEventAPI's protected constructor. We + * package them this way so a subclass constructor can simply forward an + * opaque reference to the LLEventAPI constructor. + */ + // This is a class instead of a plain struct mostly so when we forward- + // declare it we don't have to remember the distinction. + class LazyEventAPIParams + { + public: + // package the parameters used by the normal LLEventAPI constructor + std::string name, desc, field; + // bundle LLEventAPI::add() calls collected by LazyEventAPI::add(), so + // the special LLEventAPI constructor we engage can "play back" those + // add() calls + boost::signals2::signal init; + }; + + // The tricky part is: can we capture a sequence of add() calls in the + // LazyEventAPI subclass constructor and then, in effect, replay those + // add() calls on instantiation of the registered LLEventAPI subclass? so + // we don't have to duplicate the add() calls in both constructors? + + // Derive a subclass from LazyEventAPI. Its constructor must pass + // LazyEventAPI's constructor the name, desc, field params. Moreover the + // constructor body must call add(name, desc, *args) for any of the + // LLEventDispatcher add() methods, referencing the LLEventAPI subclass + // methods. + + // LazyEventAPI will store the name, desc, field params for the overall + // LLEventAPI. It will support a single generic add() call accepting name, + // desc, parameter pack. + + // It will hold a std::vector> for each operation. + // It will make all these strings available to LLLeapListener. + + // Maybe what we want is to store a vector of callables (a + // boost::signals2!) and populate it with lambdas, each of which accepts + // LLEventAPI* and calls the relevant add() method by forwarding exactly + // the name, desc and parameter pack. Then, on constructing the target + // LLEventAPI, we just fire the signal, passing the new instance pointer. + + /** + * LazyEventAPIBase implements most of the functionality of LazyEventAPI + * (q.v.), but we need the LazyEventAPI template subclass so we can accept + * the specific LLEventAPI subclass type. + */ + // No LLInstanceTracker key: we don't need to find a specific instance, + // LLLeapListener just needs to be able to enumerate all instances. + class LazyEventAPIBase: public LLInstanceTracker + { + public: + LazyEventAPIBase(const std::string& name, const std::string& desc, + const std::string& field); + virtual ~LazyEventAPIBase(); + + // Do not copy or move: once constructed, LazyEventAPIBase must stay + // put: we bind its instance pointer into a callback. + LazyEventAPIBase(const LazyEventAPIBase&) = delete; + LazyEventAPIBase(LazyEventAPIBase&&) = delete; + LazyEventAPIBase& operator=(const LazyEventAPIBase&) = delete; + LazyEventAPIBase& operator=(LazyEventAPIBase&&) = delete; + + // actually instantiate the companion LLEventAPI subclass + virtual LLEventPump* construct(const std::string& name) = 0; + + // capture add() calls we want to play back on LLEventAPI construction + template + void add(const std::string& name, const std::string& desc, ARGS&&... rest) + { + // capture the metadata separately + mOperations.push_back(std::make_pair(name, desc)); + // Use connect_extended() so the lambda is passed its own + // connection. + // We can't bind an unexpanded parameter pack into a lambda -- + // shame really. Instead, capture it as a std::tuple and then, in + // the lambda, use apply() to convert back to function args. + mParams.init.connect_extended( + [name, desc, rest = std::make_tuple(std::forward(rest)...)] + (const boost::signals2::connection& conn, LLEventAPI* instance) + { + // we only need this connection once + conn.disconnect(); + // Our add() method distinguishes name and desc because we + // capture them separately. But now, because apply() + // expects a tuple specifying ALL the arguments, expand to + // a tuple including add_trampoline() arguments: instance, + // name, desc, rest. + // apply() can't accept a template per se; it needs a + // particular specialization. + apply(&LazyEventAPIBase::add_trampoline, + std::tuple_cat(std::make_tuple(instance, name, desc), + rest)); + }); + } + + // metadata that might be queried by LLLeapListener + std::vector> mOperations; + // Params with which to instantiate the companion LLEventAPI subclass + LazyEventAPIParams mParams; + + private: + // Passing an overloaded function to any function that accepts an + // arbitrary callable is a PITB because you have to specify the + // correct overload. What we want is for the compiler to select the + // correct overload, based on the carefully-wrought enable_ifs in + // LLEventDispatcher. This (one and only) add_trampoline() method + // exists solely to pass to LL::apply(). Once add_trampoline() is + // called with the expanded arguments, we hope the compiler will Do + // The Right Thing in selecting the correct LLEventAPI::add() + // overload. + template + static + void add_trampoline(LLEventAPI* instance, ARGS&&... args) + { + instance->add(std::forward(args)...); + } + + bool mRegistered; + }; + + /** + * LazyEventAPI provides a way to register a particular LLEventAPI to be + * instantiated on demand, that is, when its name is passed to + * LLEventPumps::obtain(). + * + * Derive your listener from LLEventAPI as usual, with its various + * operation methods, but code your constructor to accept + * (const LL::LazyEventAPIParams& params) + * and forward that reference to (the protected) + * LLEventAPI(const LL::LazyEventAPIParams&) constructor. + * + * Then derive your listener registrar from + * LazyEventAPI. The constructor should + * look very like a traditional LLEventAPI constructor: + * + * * pass (name, desc [, field]) to LazyEventAPI's constructor + * * in the body, make a series of add() calls referencing your LLEventAPI + * subclass methods. + * + * You may use any LLEventAPI::add() methods, that is, any + * LLEventDispatcher::add() methods. But the target methods you pass to + * add() must belong to your LLEventAPI subclass, not the LazyEventAPI + * subclass. + * + * Declare a static instance of your LazyEventAPI listener registrar + * class. When it's constructed at static initialization time, it will + * register your LLEventAPI subclass with LLEventPumps. It will also + * collect metadata for the LLEventAPI and its operations to provide to + * LLLeapListener's introspection queries. + * + * When someone later calls LLEventPumps::obtain() to post an event to + * your LLEventAPI subclass, obtain() will instantiate it using + * LazyEventAPI's name, desc, field and add() calls. + */ + template + class LazyEventAPI: public LazyEventAPIBase + { + public: + // for subclass constructor to reference handler methods + using listener = EVENTAPI; + + LazyEventAPI(const std::string& name, const std::string& desc, + const std::string& field="op"): + // Forward ctor params to LazyEventAPIBase + LazyEventAPIBase(name, desc, field) + {} + + LLEventPump* construct(const std::string& /*name*/) override + { + // base class has carefully assembled LazyEventAPIParams embedded + // in this instance, just pass to LLEventAPI subclass constructor + return new EVENTAPI(mParams); + } + }; +} // namespace LL + +#endif /* ! defined(LL_LAZYEVENTAPI_H) */ -- cgit v1.2.3 From fa3a67f56b15d81bfd22f744314a7d9aa35bf90e Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 21 Jun 2022 15:50:08 -0400 Subject: DRTVWR-564: Remove implementation notes from before implementation. --- indra/llcommon/lazyeventapi.h | 24 ------------------------ 1 file changed, 24 deletions(-) (limited to 'indra/llcommon/lazyeventapi.h') diff --git a/indra/llcommon/lazyeventapi.h b/indra/llcommon/lazyeventapi.h index 2e947899dc..7267a3e4ec 100644 --- a/indra/llcommon/lazyeventapi.h +++ b/indra/llcommon/lazyeventapi.h @@ -42,30 +42,6 @@ namespace LL boost::signals2::signal init; }; - // The tricky part is: can we capture a sequence of add() calls in the - // LazyEventAPI subclass constructor and then, in effect, replay those - // add() calls on instantiation of the registered LLEventAPI subclass? so - // we don't have to duplicate the add() calls in both constructors? - - // Derive a subclass from LazyEventAPI. Its constructor must pass - // LazyEventAPI's constructor the name, desc, field params. Moreover the - // constructor body must call add(name, desc, *args) for any of the - // LLEventDispatcher add() methods, referencing the LLEventAPI subclass - // methods. - - // LazyEventAPI will store the name, desc, field params for the overall - // LLEventAPI. It will support a single generic add() call accepting name, - // desc, parameter pack. - - // It will hold a std::vector> for each operation. - // It will make all these strings available to LLLeapListener. - - // Maybe what we want is to store a vector of callables (a - // boost::signals2!) and populate it with lambdas, each of which accepts - // LLEventAPI* and calls the relevant add() method by forwarding exactly - // the name, desc and parameter pack. Then, on constructing the target - // LLEventAPI, we just fire the signal, passing the new instance pointer. - /** * LazyEventAPIBase implements most of the functionality of LazyEventAPI * (q.v.), but we need the LazyEventAPI template subclass so we can accept -- cgit v1.2.3 From 6b53036f7499a4e42813378009050eaf02c0b69d Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 22 Jun 2022 10:51:11 -0400 Subject: DRTVWR-564: Allow LLLeapListener to report LazyEventAPIs too. One important factor in the design of LazyEventAPI was the desire to allow LLLeapListener to query metadata for an LLEventAPI even if it hasn't yet been instantiated by LazyEventAPI. That's why LazyEventAPI requires the same metadata required by a classic LLEventAPI. Instead of just publicly exposing its data members, give LazyEventAPI a query API mimicking LLEventAPI / LLEventDispatcher. Protect data members and private methods. Adapt lazyeventapi_test.cpp accordingly. Extend LLLeapListener::getAPIs() and getAPI() to look through LazyEventAPIBase instances after first checking existing LLEventAPI instances. Because the query API for LazyEventAPIBase mimics LLEventAPI's, extract getAPI()'s actual metadata reporting to a new internal template function reportAPI(). While we're touching LLLeapListener, we no longer need BOOST_FOREACH(). --- indra/llcommon/lazyeventapi.h | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) (limited to 'indra/llcommon/lazyeventapi.h') diff --git a/indra/llcommon/lazyeventapi.h b/indra/llcommon/lazyeventapi.h index 7267a3e4ec..a815b119f0 100644 --- a/indra/llcommon/lazyeventapi.h +++ b/indra/llcommon/lazyeventapi.h @@ -63,9 +63,6 @@ namespace LL LazyEventAPIBase& operator=(const LazyEventAPIBase&) = delete; LazyEventAPIBase& operator=(LazyEventAPIBase&&) = delete; - // actually instantiate the companion LLEventAPI subclass - virtual LLEventPump* construct(const std::string& name) = 0; - // capture add() calls we want to play back on LLEventAPI construction template void add(const std::string& name, const std::string& desc, ARGS&&... rest) @@ -96,12 +93,40 @@ namespace LL }); } + // The following queries mimic the LLEventAPI / LLEventDispatcher + // query API. + + // Get the string name of the subject LLEventAPI + std::string getName() const { return mParams.name; } + // Get the documentation string + std::string getDesc() const { return mParams.desc; } + // Retrieve the LLSD key we use for dispatching + std::string getDispatchKey() const { return mParams.field; } + + // operations + using NameDesc = std::pair; + + private: // metadata that might be queried by LLLeapListener - std::vector> mOperations; + std::vector mOperations; + + public: + using const_iterator = decltype(mOperations)::const_iterator; + const_iterator begin() const { return mOperations.begin(); } + const_iterator end() const { return mOperations.end(); } + LLSD getMetadata(const std::string& name) const; + + protected: // Params with which to instantiate the companion LLEventAPI subclass LazyEventAPIParams mParams; private: + // true if we successfully registered our LLEventAPI on construction + bool mRegistered; + + // actually instantiate the companion LLEventAPI subclass + virtual LLEventPump* construct(const std::string& name) = 0; + // Passing an overloaded function to any function that accepts an // arbitrary callable is a PITB because you have to specify the // correct overload. What we want is for the compiler to select the @@ -117,8 +142,6 @@ namespace LL { instance->add(std::forward(args)...); } - - bool mRegistered; }; /** @@ -168,6 +191,7 @@ namespace LL LazyEventAPIBase(name, desc, field) {} + private: LLEventPump* construct(const std::string& /*name*/) override { // base class has carefully assembled LazyEventAPIParams embedded -- cgit v1.2.3 From 35e8d44e17bb53b01ca8c73d3302d08220f5373c Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 27 Jul 2023 08:52:10 -0400 Subject: DRTVWR-587: Tweak LazyEventAPIBase::add() to mollify clang. Both the previous version and this compile and run successfully with Xcode 14.3.1, but our older TeamCity compiler chokes -- so we must iterate remotely, sigh. --- indra/llcommon/lazyeventapi.h | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) (limited to 'indra/llcommon/lazyeventapi.h') diff --git a/indra/llcommon/lazyeventapi.h b/indra/llcommon/lazyeventapi.h index a815b119f0..e2dec8f35b 100644 --- a/indra/llcommon/lazyeventapi.h +++ b/indra/llcommon/lazyeventapi.h @@ -71,25 +71,22 @@ namespace LL mOperations.push_back(std::make_pair(name, desc)); // Use connect_extended() so the lambda is passed its own // connection. + // We can't bind an unexpanded parameter pack into a lambda -- - // shame really. Instead, capture it as a std::tuple and then, in - // the lambda, use apply() to convert back to function args. + // shame really. Instead, capture all our args as a std::tuple and + // then, in the lambda, use apply() to pass to add_trampoline(). mParams.init.connect_extended( - [name, desc, rest = std::make_tuple(std::forward(rest)...)] + [args = std::make_tuple(name, desc, std::forward(rest)...)] (const boost::signals2::connection& conn, LLEventAPI* instance) { // we only need this connection once conn.disconnect(); - // Our add() method distinguishes name and desc because we - // capture them separately. But now, because apply() - // expects a tuple specifying ALL the arguments, expand to - // a tuple including add_trampoline() arguments: instance, - // name, desc, rest. + // apply() expects a tuple specifying ALL the arguments, + // so prepend instance. // apply() can't accept a template per se; it needs a // particular specialization. apply(&LazyEventAPIBase::add_trampoline, - std::tuple_cat(std::make_tuple(instance, name, desc), - rest)); + std::tuple_cat(std::make_tuple(instance), args)); }); } -- cgit v1.2.3 From c406fa7ae97441d1d6e0ea6727c42c8f978fabed Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 27 Jul 2023 09:49:54 -0400 Subject: DRTVWR-587: Try again to address older clang difficulties. --- indra/llcommon/lazyeventapi.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'indra/llcommon/lazyeventapi.h') diff --git a/indra/llcommon/lazyeventapi.h b/indra/llcommon/lazyeventapi.h index e2dec8f35b..8bedb6fe18 100644 --- a/indra/llcommon/lazyeventapi.h +++ b/indra/llcommon/lazyeventapi.h @@ -83,10 +83,11 @@ namespace LL conn.disconnect(); // apply() expects a tuple specifying ALL the arguments, // so prepend instance. + std::tuple full_args{ std::tuple_cat(std::make_tuple(instance), args) }; // apply() can't accept a template per se; it needs a // particular specialization. apply(&LazyEventAPIBase::add_trampoline, - std::tuple_cat(std::make_tuple(instance), args)); + full_args); }); } -- cgit v1.2.3 From edab599edaf67d37a3a2e954371128676a8cf9cc Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 27 Jul 2023 10:31:55 -0400 Subject: DRTVWR-587: Revert "Try again to address older clang difficulties." That wasn't the issue. This is a compiler bug: https://github.com/llvm/llvm-project/issues/41999 https://stackoverflow.com/q/57080425 https://bugs.llvm.org/show_bug.cgi?id=42654 This reverts commit c406fa7ae97441d1d6e0ea6727c42c8f978fabed. --- indra/llcommon/lazyeventapi.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'indra/llcommon/lazyeventapi.h') diff --git a/indra/llcommon/lazyeventapi.h b/indra/llcommon/lazyeventapi.h index 8bedb6fe18..e2dec8f35b 100644 --- a/indra/llcommon/lazyeventapi.h +++ b/indra/llcommon/lazyeventapi.h @@ -83,11 +83,10 @@ namespace LL conn.disconnect(); // apply() expects a tuple specifying ALL the arguments, // so prepend instance. - std::tuple full_args{ std::tuple_cat(std::make_tuple(instance), args) }; // apply() can't accept a template per se; it needs a // particular specialization. apply(&LazyEventAPIBase::add_trampoline, - full_args); + std::tuple_cat(std::make_tuple(instance), args)); }); } -- cgit v1.2.3 From 5000f8bd123a87ffbc321a7e3fed61221c9a4da1 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 27 Jul 2023 10:50:46 -0400 Subject: DRTVWR-587: Try to work around clang bug. --- indra/llcommon/lazyeventapi.h | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'indra/llcommon/lazyeventapi.h') diff --git a/indra/llcommon/lazyeventapi.h b/indra/llcommon/lazyeventapi.h index e2dec8f35b..cc566e35af 100644 --- a/indra/llcommon/lazyeventapi.h +++ b/indra/llcommon/lazyeventapi.h @@ -72,21 +72,24 @@ namespace LL // Use connect_extended() so the lambda is passed its own // connection. + // apply() can't accept a template per se; it needs a particular + // specialization. Specialize out here to work around a clang bug: + // https://github.com/llvm/llvm-project/issues/41999 + auto func{ &LazyEventAPIBase::add_trampoline + }; + // We can't bind an unexpanded parameter pack into a lambda -- // shame really. Instead, capture all our args as a std::tuple and // then, in the lambda, use apply() to pass to add_trampoline(). mParams.init.connect_extended( - [args = std::make_tuple(name, desc, std::forward(rest)...)] + [func, args = std::make_tuple(name, desc, std::forward(rest)...)] (const boost::signals2::connection& conn, LLEventAPI* instance) { // we only need this connection once conn.disconnect(); // apply() expects a tuple specifying ALL the arguments, // so prepend instance. - // apply() can't accept a template per se; it needs a - // particular specialization. - apply(&LazyEventAPIBase::add_trampoline, - std::tuple_cat(std::make_tuple(instance), args)); + apply(func, std::tuple_cat(std::make_tuple(instance), args)); }); } -- cgit v1.2.3 From 2159da95e0acc15daa7ab18bc17f1d5f60be71cc Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 27 Jul 2023 11:11:53 -0400 Subject: DRTVWR-587: Try harder to work around clang bug. --- indra/llcommon/lazyeventapi.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'indra/llcommon/lazyeventapi.h') diff --git a/indra/llcommon/lazyeventapi.h b/indra/llcommon/lazyeventapi.h index cc566e35af..e36831270b 100644 --- a/indra/llcommon/lazyeventapi.h +++ b/indra/llcommon/lazyeventapi.h @@ -77,12 +77,13 @@ namespace LL // https://github.com/llvm/llvm-project/issues/41999 auto func{ &LazyEventAPIBase::add_trampoline }; - // We can't bind an unexpanded parameter pack into a lambda -- // shame really. Instead, capture all our args as a std::tuple and // then, in the lambda, use apply() to pass to add_trampoline(). + auto args{ std::make_tuple(name, desc, std::forward(rest)...) }; + mParams.init.connect_extended( - [func, args = std::make_tuple(name, desc, std::forward(rest)...)] + [func, args] (const boost::signals2::connection& conn, LLEventAPI* instance) { // we only need this connection once -- cgit v1.2.3