diff --git a/components/common/cpp/include/ftl/handle.hpp b/components/common/cpp/include/ftl/handle.hpp new file mode 100644 index 0000000000000000000000000000000000000000..0fe697e8435e5d33548cd6159025f6964c8b5fe4 --- /dev/null +++ b/components/common/cpp/include/ftl/handle.hpp @@ -0,0 +1,100 @@ +#ifndef _FTL_HANDLE_HPP_ +#define _FTL_HANDLE_HPP_ + +#include <ftl/threads.hpp> +#include <functional> +#include <unordered_map> + +namespace ftl { + +struct Handle; +struct BaseHandler { + virtual void remove(const Handle &)=0; + + inline Handle make_handle(BaseHandler*, int); + + protected: + MUTEX mutex_; + int id_=0; +}; + +/** + * A `Handle` is used to manage registered callbacks, allowing them to be + * removed safely whenever the `Handle` instance is destroyed. + */ +struct Handle { + friend struct BaseHandler; + + /** + * Cancel the timer job. If currently executing it will block and wait for + * the job to complete. + */ + inline void cancel() { if (handler_) handler_->remove(*this); handler_ = nullptr; } + + inline int id() const { return id_; } + + Handle() : handler_(nullptr), id_(0) {} + + Handle(const Handle &)=delete; + Handle &operator=(const Handle &)=delete; + + inline Handle(Handle &&h) : handler_(nullptr) { + if (handler_) handler_->remove(*this); + handler_ = h.handler_; + h.handler_ = nullptr; + id_ = h.id_; + } + + inline Handle &operator=(Handle &&h) { + if (handler_) handler_->remove(*this); + handler_ = h.handler_; + h.handler_ = nullptr; + id_ = h.id_; + return *this; + } + + inline ~Handle() { if (handler_) handler_->remove(*this); } + + private: + BaseHandler *handler_; + int id_; + + Handle(BaseHandler *h, int id) : handler_(h), id_(id) {} +}; + +template <typename ...ARGS> +struct Handler : BaseHandler { + Handle on(const std::function<bool(ARGS...)> &f) { + std::unique_lock<std::mutex> lk(mutex_); + int id = id_++; + callbacks_[id] = f; + return make_handle(this, id); + } + + void trigger(ARGS ...args) { + std::unique_lock<std::mutex> lk(mutex_); + try { + for (auto &f : callbacks_) { + f.second(std::forward<ARGS...>(args...)); + } + } catch (const std::exception &e) { + LOG(ERROR) << "Exception in callback: " << e.what(); + } + } + + void remove(const Handle &h) override { + std::unique_lock<std::mutex> lk(mutex_); + callbacks_.erase(h.id()); + } + + private: + std::unordered_map<int, std::function<bool(ARGS...)>> callbacks_; +}; + +} + +ftl::Handle ftl::BaseHandler::make_handle(BaseHandler *h, int id) { + return ftl::Handle(h, id); +} + +#endif \ No newline at end of file diff --git a/components/common/cpp/test/CMakeLists.txt b/components/common/cpp/test/CMakeLists.txt index 9112234bfadbd4de3be225dce71937a38ed1f62a..9e86ae5b1b6d66c4ed63e901ed480b6238fc7d0b 100644 --- a/components/common/cpp/test/CMakeLists.txt +++ b/components/common/cpp/test/CMakeLists.txt @@ -28,6 +28,15 @@ target_include_directories(timer_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../inc target_link_libraries(timer_unit ftlcommon Threads::Threads ${OS_LIBS}) +### Handle Unit ################################################################ +add_executable(handle_unit + $<TARGET_OBJECTS:CatchTest> + ./handle_unit.cpp +) +target_include_directories(handle_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include") +target_link_libraries(handle_unit ftlcommon + Threads::Threads ${OS_LIBS}) + ### URI ######################################################################## add_executable(msgpack_unit $<TARGET_OBJECTS:CatchTest> diff --git a/components/common/cpp/test/handle_unit.cpp b/components/common/cpp/test/handle_unit.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1c1c27d61b55af2b26bb9a37f0827e2e2fe98a7d --- /dev/null +++ b/components/common/cpp/test/handle_unit.cpp @@ -0,0 +1,81 @@ +#include "catch.hpp" +#define LOGURU_REPLACE_GLOG 1 +#include <loguru.hpp> +#include <ftl/handle.hpp> + +using ftl::Handler; +using ftl::Handle; + +TEST_CASE( "Handle release on cancel" ) { + Handler<int> handler; + + int calls = 0; + + auto h = handler.on([&calls](int i) { + calls += i; + return true; + }); + + handler.trigger(5); + REQUIRE(calls == 5); + h.cancel(); + handler.trigger(5); + REQUIRE(calls == 5); +} + +TEST_CASE( "Handle multiple triggers" ) { + Handler<int> handler; + + int calls = 0; + + auto h = handler.on([&calls](int i) { + calls += i; + return true; + }); + + handler.trigger(5); + REQUIRE(calls == 5); + handler.trigger(5); + REQUIRE(calls == 10); +} + +TEST_CASE( "Handle release on destruct" ) { + Handler<int> handler; + + int calls = 0; + + { + auto h = handler.on([&calls](int i) { + calls += i; + return true; + }); + + handler.trigger(5); + REQUIRE(calls == 5); + } + + handler.trigger(5); + REQUIRE(calls == 5); +} + +TEST_CASE( "Handle moving" ) { + SECTION("old handle cannot cancel") { + Handler<int> handler; + + int calls = 0; + + auto h = handler.on([&calls](int i) { + calls += i; + return true; + }); + + handler.trigger(5); + REQUIRE(calls == 5); + + auto h2 = std::move(h); + h.cancel(); + + handler.trigger(5); + REQUIRE(calls == 10); + } +} \ No newline at end of file