/* * * Copyright (c) 2024 Project CHIP Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include namespace chip { namespace Callback { namespace detail { // Internal helper functions template void TaggedDequeueGroup(Cancelable * cancelable); void EnqueueWithGroup(Cancelable * cancelable, Cancelable *& group, Cancelable * anchor, void (*cancelFn)(Cancelable *)); void LinkGroup(Cancelable * prev, Cancelable * next); inline Cancelable * ClearCancelable(Cancelable * cancelable); } // namespace detail /** * A GroupedCallbackList manages a list of Callback objects (see CHIPCallback.h). * The state of the list is maintained using the prev/next pointers of each Callback. * Unlike a normal linked list where entries are managed individually, this class * manages a number of related callbacks as a group, with the callback function types * given as template parameters. * * For example, a `GroupedCallbackList` manages groups of a * `Callback` and a `Callback`. * * Groups of callbacks are enqueued and dequeued (or cancelled) as a unit. * Within a group each callback is optional (i.e. can be null), however attempting * to enqueue a group where all callbacks are null has no effect. */ template class GroupedCallbackList : protected Cancelable { public: GroupedCallbackList() = default; ~GroupedCallbackList() { Clear(); } GroupedCallbackList(GroupedCallbackList const &) = delete; GroupedCallbackList & operator=(GroupedCallbackList const &) = delete; bool IsEmpty() { return mNext == this; } /** * Enqueues the specified group of callbacks, any of which may be null. */ void Enqueue(Callback *... callback) { Enqueue(std::index_sequence_for{}, callback...); } /** * If the list is non-empty, populates the reference arguments with the first * group of callbacks and returns true. Returns false if the list is empty. */ bool Peek(Callback *&... callback) const { return Peek(std::index_sequence_for{}, callback...); } /** * Like Peek(), but additionally removes the first group of callbacks from the list. */ bool Take(Callback *&... callback) { VerifyOrReturnValue(Peek(callback...), false); mNext->Cancel(); return true; } /** * Moves all elements of the source list into this list, leaving the source list empty. */ void EnqueueTakeAll(GroupedCallbackList & source) { VerifyOrReturn(!source.IsEmpty() && this != &source); detail::LinkGroup(mPrev, source.mNext); source.mPrev->mNext = this; mPrev = source.mPrev; source.mPrev = source.mNext = &source; } void Clear() { Cancelable * next = mNext; while (next != this) { next = detail::ClearCancelable(next); } mPrev = mNext = this; } private: /* * The grouped list structure is similar to a normal doubly linked list, * with the list object itself (via inheriting from Cancelable) acting as * an external "anchor" node that is both the head and tail of the list. * * However we have the additional requirement of representing node grouping. * Due to the requirement so support sparse groups (one or more callbacks may * not be present in a particular group) we cannot rely on a fixed group size. * This problem is solved by having the "prev" pointer for all nodes in a group * point to the node before the group, as illustrated below: * * |Anchor| |Grp 1| |====== Group 2 ======| * _______________________________________________ * / \ * \ +---+ +---+ +---+ +---+ +---+ / * ->|###|----->| |----->| |-->| |-->| |-- * |###| | | | | | | | | * --|###|<-----| |<-----| | -| | -| |<- * / +---+ +---+ \ +---+ / +---+ / +---+ \ * | \_______/ / | * | \_____________/ | * \_______________________________________________/ * * This allows the start of a group to be reached from any group member via * ->prev->next. Nodes in a group can be enumerated by via the "next" chain, * inspecting the "prev" pointers to detect the end of the group. The price * for encoding grouping in this way is that upon removal of a group we have * to update not just the "prev" pointer of the following node, but of all * nodes in the following group. * * When retrieving a (sparse) group from the list, we also need to be able * to tell which callbacks are present: In a grouped list with types (A, B) * both (a, nullptr) and (nullptr, b) are by necessity represented by only * a single node in the list. To be able to recover this information, we use * distinct trampolines that tag the "cancel" function pointer stored in each * node with the index of the callback type within the argument type tuple. */ template void Enqueue(std::index_sequence, Callback *... callback) { Cancelable * group = nullptr; ( [&] { VerifyOrReturn(callback != nullptr); detail::EnqueueWithGroup(callback->Cancel(), group, this, &detail::TaggedDequeueGroup); }(), ...); } template bool Peek(std::index_sequence, Callback *&... callback) const { Cancelable * cancelable = mNext; VerifyOrReturnValue(cancelable != this, false); Cancelable * groupPrev = cancelable->mPrev; ( [&] { if (cancelable->mPrev == groupPrev && cancelable->mCancel == &detail::TaggedDequeueGroup) { callback = CallbackmCall)>::FromCancelable(cancelable); cancelable = cancelable->mNext; } else { callback = nullptr; } }(), ...); return true; } }; namespace detail { // Inserts `cancelable` before `anchor`, either starting a new `group` // (populating the passed pointer if it is null) or adding to it. inline void EnqueueWithGroup(Cancelable * cancelable, Cancelable *& group, Cancelable * anchor, void (*cancelFn)(Cancelable *)) { cancelable->mCancel = cancelFn; cancelable->mNext = anchor; if (!group) { group = cancelable; cancelable->mPrev = anchor->mPrev; } else { cancelable->mPrev = group->mPrev; } anchor->mPrev->mNext = cancelable; anchor->mPrev = cancelable; } // Establish prev/next links between `prev` and the group starting at `cancelable`. inline void LinkGroup(Cancelable * prev, Cancelable * cancelable) { prev->mNext = cancelable; Cancelable * groupPrev = cancelable->mPrev; do { cancelable->mPrev = prev; cancelable = cancelable->mNext; } while (cancelable->mPrev == groupPrev); } // Clears the state of a cancelable and returns the following one. // Does NOT touch the state of adjacent nodes. inline Cancelable * ClearCancelable(Cancelable * cancelable) { auto * next = cancelable->mNext; cancelable->mPrev = cancelable->mNext = cancelable; cancelable->mCancel = nullptr; return next; } // Dequeues `cancelable` and all otehr nodes in the same group. inline void DequeueGroup(Cancelable * cancelable) { Cancelable * prev = cancelable->mPrev; Cancelable * next = prev->mNext; do { next = ClearCancelable(next); } while (next->mPrev == prev); LinkGroup(prev, next); } template void TaggedDequeueGroup(Cancelable * cancelable) { (void) Index; // not used, we only care that instantiations have unique addresses DequeueGroup(cancelable); } } // namespace detail } // namespace Callback } // namespace chip