-
Notifications
You must be signed in to change notification settings - Fork 5.3k
wip: introduce safe callbacks and use in rewriting init manager #6245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| licenses(["notice"]) # Apache 2 | ||
|
|
||
| load( | ||
| "//bazel:envoy_build_system.bzl", | ||
| "envoy_cc_library", | ||
| "envoy_package", | ||
| ) | ||
|
|
||
| envoy_package() | ||
|
|
||
| envoy_cc_library( | ||
| name = "callback", | ||
| hdrs = ["callback.h"], | ||
| ) | ||
|
|
||
| envoy_cc_library( | ||
| name = "manager", | ||
| hdrs = ["manager.h"], | ||
| deps = ["//source/common/callback"], | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| #pragma once | ||
|
|
||
| #include <functional> | ||
| #include <memory> | ||
|
|
||
| namespace Envoy { | ||
| namespace Common { | ||
|
|
||
| /** | ||
| * The Callback::Caller and Callback::Receiver classes address a memory safety issue with callbacks | ||
| * in C++. Typically, an "event consumer" (a.k.a. handler, listener, observer) might register | ||
| * interest with an "event producer" (a.k.a. manager, subject) either by implementing an OO-style | ||
| * callback interface like: | ||
| * | ||
| * struct EventConsumer { | ||
| * virtual void onEvent(const Event& e) = 0; | ||
| * }; | ||
| * | ||
| * class MyEventConsumer : public EventConsumer { | ||
| * public: | ||
| * MyEventConsumer(EventProducer& producer) { producer.attach(this); } | ||
| * void onEvent(const Event& e) override { ... handle event ... } | ||
| * }; | ||
| * | ||
| * class EventProducer { | ||
| * public: | ||
| * void attach(EventConsumer* consumer) { consumer_ = consumer; } | ||
| * void invoke() { consumer_.onEvent(... some event ...); } | ||
| * private: | ||
| * EventConsumer* consumer_; | ||
| * }; | ||
| * | ||
| * ... or by passing a functional-style callback like: | ||
| * | ||
| * class MyEventConsumer { | ||
| * public: | ||
| * MyEventConsumer(EventProducer& producer) { | ||
| * producer.attach([this]() { ... handle event ... }); | ||
| * } | ||
| * }; | ||
| * | ||
| * class EventProducer { | ||
| * public: | ||
| * void attach(std::function<void(const Event&)> callback) { callback_ = callback; } | ||
| * void invoke() { callback_(... some event ...); } | ||
| * private: | ||
| * std::function<void(const Event&)> callback_; | ||
| * }; | ||
| * | ||
| * | ||
| * These approaches are equivalent, and they both have the same issue: the event producer | ||
| * references the event consumer, either directly or via the lambda's captures, but doesn't manage | ||
| * its lifetime. If the event consumer is destroyed before the event producer calls it, this is a | ||
| * use-after-free. | ||
| * | ||
| * In some cases, it's straightforward enough to implement "cancelation" by allowing an event | ||
| * consumer to be detached from any event producers it is currently attached to, but that requires | ||
| * holding references to all such event producers. That may be impractical or, again, unsafe in the | ||
| * case where the event consumer outlives its producers. | ||
| * | ||
| * Caller and Receiver provide some additional safety. A Receiver owns a callback function, and | ||
| * can produce Callers which function as weak references to it. The receiver's callback function | ||
| * can only be invoked via its callers. If the receiver is destroyed, invoking its callers has | ||
| * no effect, so none of the callback's captures can be unsafely dereferenced. | ||
| * | ||
| * When implementing this pattern, an event consumer would own a Receiver and an event producer | ||
| * would own the corresponding Caller. For example: | ||
| * | ||
| * using EventCaller = Callback::Caller<const Event&>; | ||
| * using EventReceiver = Callback::Receiver<const Event&>; | ||
| * | ||
| * class MyEventConsumer { | ||
| * public: | ||
| * MyEventConsumer(EventProducer& producer) { | ||
| * producer.attach(receiver_.caller()); | ||
| * } | ||
| * private: | ||
| * EventReceiver receiver_([this]() { ... handle event ... }); | ||
| * }; | ||
| * | ||
| * class EventProducer { | ||
| * public: | ||
| * void attach(EventCaller caller) { caller_ = caller; } | ||
| * void invoke() { caller_(... some event ... ); } | ||
| * private: | ||
| * EventCaller caller_; | ||
| * }; | ||
| */ | ||
| namespace Callback { | ||
|
|
||
| // Forward-declaration for Caller's friend declaration. | ||
| template <typename... Args> class Receiver; | ||
|
|
||
| /** | ||
| * Caller: simple wrapper for a weak_ptr to a callback function. Copyable and movable. | ||
| */ | ||
| template <typename... Args> class Caller { | ||
| public: | ||
| /** | ||
| * Default constructor for default / value initialization. | ||
htuch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| */ | ||
| Caller() = default; | ||
|
|
||
| /** | ||
| * Implicit conversion to bool, to test whether the corresponding Receiver is still available. | ||
| * @return true if the corresponding Receiver is still available, false otherwise. | ||
| */ | ||
| operator bool() const { return !fn_.expired(); } | ||
|
|
||
| /** | ||
| * Reset this caller to not reference a Receiver. | ||
| */ | ||
| void reset() { fn_.reset(); } | ||
|
|
||
| /** | ||
| * Invoke the corresponding Receiver's callback, if it is still available. If the receiver has | ||
| * been destroyed or reset, this has no effect. | ||
| * @param args the arguments, if any, to pass to the receiver's callback function. | ||
| */ | ||
| void operator()(Args... args) const { | ||
| auto locked_fn(fn_.lock()); | ||
| if (locked_fn) { | ||
| (*locked_fn)(args...); | ||
| } | ||
| } | ||
|
|
||
| private: | ||
| /** | ||
| * Can only be constructed by a Receiver | ||
| */ | ||
| friend Receiver<Args...>; | ||
| Caller(std::weak_ptr<std::function<void(Args...)>> fn) : fn_(std::move(fn)) {} | ||
|
|
||
| std::weak_ptr<std::function<void(Args...)>> fn_; | ||
| }; | ||
|
|
||
| /** | ||
| * Receiver: simple wrapper for a shared_ptr to a callback function. Copyable and movable, but | ||
| * typically should be owned uniquely by the owner of any pointers and references captured by its | ||
| * handler function. For example, if `this` is captured by the handler function, `this` should | ||
| * probably also own the Receiver. | ||
| */ | ||
| template <typename... Args> class Receiver { | ||
| public: | ||
| /** | ||
| * Default constructor for default / value initialization. | ||
| */ | ||
| Receiver() = default; | ||
htuch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Construct a receiver to own a callback function. | ||
| */ | ||
| Receiver(std::function<void(Args...)> fn) | ||
| : fn_(std::make_shared<std::function<void(Args...)>>(std::move(fn))) {} | ||
|
|
||
| /** | ||
| * @return a new caller for this receiver. | ||
| */ | ||
| Caller<Args...> caller() const { | ||
| return Caller<Args...>(std::weak_ptr<std::function<void(Args...)>>(fn_)); | ||
| } | ||
|
|
||
| /** | ||
| * Reset this receiver, such that any callers previously created will not be able to invoke it. | ||
| */ | ||
| void reset() { fn_.reset(); } | ||
|
|
||
| /** | ||
| * Explicit conversion to bool, to test whether the receiver contains a callback function. | ||
| * @return true if the corresponding Receiver contains a callback, false otherwise. | ||
| */ | ||
| explicit operator bool() const { return static_cast<bool>(fn_); } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: prefer |
||
|
|
||
| private: | ||
| std::shared_ptr<std::function<void(Args...)>> fn_; | ||
| }; | ||
|
|
||
| } // namespace Callback | ||
| } // namespace Common | ||
| } // namespace Envoy | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| #pragma once | ||
|
|
||
| #include <list> | ||
|
|
||
| #include "common/callback/callback.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Common { | ||
| namespace Callback { | ||
|
|
||
| /** | ||
| * Callback::Manager is a fairly typical implementation of the "Observer" pattern | ||
| * (https://en.wikipedia.org/wiki/Observer_pattern), made safe by using Callback::Caller. Any number | ||
| * of Callers may be added to a Manager. Resetting or destroying a Caller's corresponding Receiver | ||
| * will remove it from the Manager's list when it is invoked. | ||
| * | ||
| * Manager is actually a type alias (below) to this class template, ManagerT, which is parameterized | ||
| * on an additional type C, representing the caller type. This will typically be Caller<Args...>, | ||
| * but can be anything that behaves like a function. See examples in manager_test.cc (where the | ||
| * caller type is a mock), and Init::ManagerImpl (where the caller does some extra logging). | ||
| */ | ||
| template <typename C, typename... Args> class ManagerT { | ||
| public: | ||
| /** | ||
| * Adds a Caller to the callback manager, such that its corresponding Receiver will be invoked | ||
| * by subsequent calls to call() as long as it remains available. | ||
| * @param caller the caller to add to the callback manager | ||
| */ | ||
| ManagerT& add(C caller) { | ||
| callers_.push_back(std::move(caller)); | ||
| return *this; | ||
| } | ||
|
|
||
| /** | ||
| * Invokes all callers previously provided to add(). Any corresponding receivers that are no | ||
| * longer available after invocation (which may have happened as a side-effect of invoking them) | ||
| * will be removed from the callback manager. | ||
| * @param args the arguments, if any, to pass to all callers. | ||
| */ | ||
| void operator()(Args... args) { | ||
htuch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| for (auto it = callers_.begin(); it != callers_.end(); /* incremented below */) { | ||
| // First, invoke the caller whether or not it references an available receiver. | ||
| const auto& caller = *it; | ||
| caller(args...); | ||
|
|
||
| // The caller may reference an unavailable receiver, either because the receiver was already | ||
| // unavailable beforehand, or because it was reset or destroyed as a side-effect of invoking | ||
| // it. If the receiver is unavailable, remove it so we don't try to call it again. | ||
| if (caller) { | ||
| ++it; | ||
| } else { | ||
| it = callers_.erase(it); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private: | ||
| std::list<C> callers_; | ||
| }; | ||
|
|
||
| template <typename... Args> using Manager = ManagerT<Caller<Args...>, Args...>; | ||
|
|
||
| } // namespace Callback | ||
| } // namespace Common | ||
| } // namespace Envoy | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.