commit 487f516a3fd513e92fa75a338e13994626d7090f Author: Roland Conybeare Date: Tue Oct 10 12:32:34 2023 -0400 initial implementation diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..ada7743d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,48 @@ +# callback/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project(callback VERSION 0.1) +enable_language(CXX) + +# common XO cmake macros (see proj/xo-cmake) +include(xo_macros/xo_cxx) +include(xo_macros/code-coverage) + +# ---------------------------------------------------------------- +# unit test setup + +enable_testing() +# activate code coverage for all executables + libraries (when configured with -DCODE_COVERAGE=ON) +add_code_coverage() +# 1. assuming that /nix/store/ prefixes .hpp files belonging to gcc, catch2 etc. +# we're not interested in code coverage for these sources. +# 2. exclude the utest/ subdir, we don't need coverage on the unit tests themselves; +# rather, want coverage on the code that the unit tests exercise. +# +# NOTE: this seems to work only with the 'ccov-all' target. In particular, doesn't seem to do anything with the 'ccov' target +# +add_code_coverage_all_targets(EXCLUDE /nix/store/* ${PROJECT_SOURCE_DIR}/utest/* ${PROJECT_BINARY_DIR}/local/* ${PROJECT_SOURCE_DIR}/repo/*) + +# ---------------------------------------------------------------- +# c++ settings + +# PROJECT_CXX_FLAGS: bespoke for this project - usually empty +set(PROJECT_CXX_FLAGS "") +#set(PROJECT_CXX_FLAGS "-fconcepts-diagnostics-depth=2") +add_definitions(${PROJECT_CXX_FLAGS}) + +xo_toplevel_compile_options() + +# ---------------------------------------------------------------- +# sources + +add_subdirectory(src/callback) +#add_subdirectory(utest) + +# ---------------------------------------------------------------- +# provide find_package() support to customers + +xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +# end CMakeLists.txt diff --git a/cmake/callbackConfig.cmake.in b/cmake/callbackConfig.cmake.in new file mode 100644 index 00000000..f7176f38 --- /dev/null +++ b/cmake/callbackConfig.cmake.in @@ -0,0 +1,6 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(refcnt) +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +check_required_components("@PROJECT_NAME@") diff --git a/include/xo/callback/CallbackSet.hpp b/include/xo/callback/CallbackSet.hpp new file mode 100644 index 00000000..2abb2c04 --- /dev/null +++ b/include/xo/callback/CallbackSet.hpp @@ -0,0 +1,312 @@ +/* @file CallbackSet.hpp */ + +#pragma once + +#include "xo/refcnt/Refcounted.hpp" +//#include "indentlog/scope.hpp" +//#include "indentlog/print/tag.hpp" +#include +#include + +namespace xo { + namespace fn { + /* identifies a particular callback in a CallbackSet (see below). + * an unique id is created: + * CallbackSetImpl cbset = ...; + * CallbackId cb_id = cbset.add_callback(..); + * + * can use id to remove callback later: + * cbset.remove_callback(cb_id); + */ + class CallbackId { + public: + CallbackId() = default; + explicit CallbackId(uint32_t id) : id_{id} {} + + /* generate a globally-unique id (not threadsafe) */ + static CallbackId generate(); + + uint32_t id() const { return id_; } + + private: + uint32_t id_ = 0; + }; /*CallbackId*/ + + inline bool operator==(CallbackId lhs, CallbackId rhs) { return lhs.id() == rhs.id(); } + inline bool operator!=(CallbackId lhs, CallbackId rhs) { return lhs.id() != rhs.id(); } + + /* queue add/remove callback instructions encountered during callback + * execution, to avoid invalidating vector iterator. + * + */ + template + struct ReentrantCbsetCmd { + enum CbsetCmdEnum { AddCallback, RemoveCallback }; + + ReentrantCbsetCmd() = default; + ReentrantCbsetCmd(CbsetCmdEnum cmd, CallbackId id, Fn const & fn) + : cmd_{cmd}, id_{id}, fn_{fn} {} + + static ReentrantCbsetCmd add(CallbackId id, Fn const & fn) { + return ReentrantCbsetCmd{AddCallback, id, fn}; + } /*add*/ + + static ReentrantCbsetCmd remove(CallbackId id) { + return ReentrantCbsetCmd{RemoveCallback, id, Fn()}; + } /*remove*/ + + bool is_add() const { return cmd_ == AddCallback; } + bool is_remove() const { return cmd_ == RemoveCallback; } + CallbackId id() const { return id_; } + Fn const & fn() const { return fn_; } + + private: + /* AddCallback: deferred CallbackSet::add_callback(.fn) + * RemoveCallback: deferred CallbackSet::remove_callback(.fn) + */ + CbsetCmdEnum cmd_ = AddCallback; + CallbackId id_; + Fn fn_; + }; /*ReentrantCbsetCmd*/ + + /* record for remembering a single callback. + * callbacks are given unique ids so they can be removed later + */ + template + struct CbRecd { + CbRecd(CallbackId id, Fn const & fn) : id_{id}, fn_{fn} {} + + CallbackId id_; + Fn fn_; + }; /*CbRecd*/ + + /* If Fnptr is a type such that this works: + * Fnptr fn = ...; + * using Fn = Fnptr::destination_type; + * Fn * native_fn = fn.get(); + * (native_fn->*member_fn)(args ...); + * + * then + * CallbackSet cbset = ...; + * cbset.invoke(&Fn::member_fn, args...) + * + * calls + * (cb->*member_fn)(args...) + * + * for each callback cb in this set. + * + * In addition, calls hook methods: + * cb->notify_add_callback() + * cb->notify_remove_callback() + * when adding/removing callback. + * + * Require: + * - can invoke (Fnptr->*member_fn)(...) + * + * implementation is reentrant: running callbacks can safely make + * add/remove calls on the cbset that invoked them. + * + * not threadsafe. + */ + template + class CallbackSetImpl { + public: + using callback_type = typename Fn::element_type; + //using scope = xo::scope; + + public: + CallbackSetImpl() = default; + + /* support for range iterators */ + typename std::vector>::const_iterator begin() const { return cb_v_.begin(); } + typename std::vector>::const_iterator end() const { return cb_v_.end(); } + + /* invoke callbacks registered with this callback set */ + template + void invoke(void (callback_type::* member_fn)(Sn... args), Tn&&... args) { + this->cb_running_ = true; + + try { + for(CbRecd const & cb_recd : this->cb_v_) { + callback_type * native_cb = cb_recd.fn_.get(); + + /* clang11 doesn't like + * cb->*member_fn + * when cb-> is overloaded + */ + (native_cb->*member_fn)(args...); + } + + this->make_deferred_changes(); + } catch(...) { + this->make_deferred_changes(); + throw; + } + } /*operator()*/ + + /* call fn(cb) for each callback present in this set */ + void visit_callbacks(std::function fn) const { + CallbackSetImpl * self = const_cast(this); + + self->cb_running_ = true; + + try { + for(Fn const & cb : this->cb_v_) + fn(cb); + + this->make_deferred_changes(); + } catch(...) { + this->make_deferred_changes(); + throw; + } + } /*visit_callbacks*/ + + /* add callback target_fn to this callback set. + * reentrant + */ + CallbackId add_callback(Fn const & target_fn) { + CallbackId id = CallbackId::generate(); + + if(this->cb_running_) { + /* defer until callback execution completes */ + this->reentrant_cmd_v_.push_back(ReentrantCbsetCmd::add(id, target_fn)); + } else { +#ifdef NOT_USING + constexpr bool c_debug_enabled_flag = false; + scope lscope(reflect::type_name(), + "::add_callback", c_debug_enabled_flag); + + if (c_debug_enabled_flag) { + lscope.log("before appending .cb_v[]", + xo::xtag("target_fn", (void*)target_fn.get()), + xo::xtag("target_fn.refcnt", + target_fn->reference_counter())); + } +#endif + + this->cb_v_.push_back(CbRecd(id, target_fn)); + +#ifdef NOT_USING + if (c_debug_enabled_flag) { + lscope.log("after appending .cb_v[]", + xo::xtag("target_fn", (void *)target_fn.get()), + xo::xtag("target_fn.refcnt", + target_fn->reference_counter())); + } +#endif + } + + return id; + } /*add_callback*/ + + void remove_callback(CallbackId id) { + if(this->cb_running_) { + /* defer until callback execution completes */ + this->reentrant_cmd_v_.push_back(ReentrantCbsetCmd::remove(id)); + } else { + this->remove_callback_impl(id); + } + + } /*remove_callback*/ + +#ifdef NOT_USING + /* remove callback target_fn from this callback set. + * noop if callback is not present + */ + void remove_callback(Fn const & target_fn) { + if(this->cb_running_) { + /* defer until callback execution completes */ + this->reentrant_cmd_v_.push_back(ReentrantCbsetCmd::remove(target_fn)); + } else { + this->remove_callback_impl(target_fn); + } + } /*remove_callback*/ +#endif + + private: + /* apply deferred changes to .cb_v[] */ + void make_deferred_changes() { + this->cb_running_ = false; + + std::vector> cmd_v; + std::swap(cmd_v, this->reentrant_cmd_v_); + + for(ReentrantCbsetCmd const & cmd : cmd_v) { + if(cmd.is_add()) { + this->cb_v_.push_back(CbRecd(cmd.id(), cmd.fn())); + + cmd.fn()->notify_add_callback(); + } else if(cmd.is_remove()) { + this->remove_callback_impl(cmd.id()); + } + } + } /*make_deferred_changes*/ + + void remove_callback_impl(CallbackId target_id) { + for (auto ix = this->cb_v_.begin(); ix != this->cb_v_.end(); ++ix) { + if (ix->id_ == target_id) { + Fn target_fn = ix->fn_; + + this->cb_v_.erase(ix); + + target_fn->notify_remove_callback(); + break; + } + } + } /*remove_callback_impl*/ + +#ifdef NOT_USING + void remove_callback_impl(Fn const & target_fn) { + auto ix = std::find(this->cb_v_.begin(), this->cb_v_.end(), target_fn); + + if(ix != this->cb_v_.end()) + this->cb_v_.erase(ix); + + target_fn->notify_remove_callback(); + } /*remove_callback_impl*/ +#endif + + private: + bool cb_running_ = false; + /* collection of callback functions */ + std::vector> cb_v_; + /* when a callback registered with *this, while running, + * attempts to add/remove a callback to/from this set + * (including removing itself), + * must defer until all callbacks have executed. + * remember deferred instructions here. + */ + std::vector> reentrant_cmd_v_; + }; /*CallbackSetImpl*/ + + template + using RpCallbackSet = CallbackSetImpl>; + + /* like RpCallbackSet, + * but also provides overload(s) for operator()(..) + */ + template + class NotifyCallbackSet : public RpCallbackSet { + public: + NotifyCallbackSet(MemberFn fn) + : privileged_member_fn_{fn} {} + + template + void operator()(Tn&&... args) { + this->invoke(this->privileged_member_fn_, args...); + } /*operator()*/ + + private: + /* implements operator()(...) */ + MemberFn privileged_member_fn_; + }; /*NotifyCallbackSet*/ + + template + inline NotifyCallbackSet + make_notify_cbset(Sret (NativeFn::* member_fn)(Sn...)) { + return NotifyCallbackSet(member_fn); + } /*make_notify_cbset*/ + } /*namespace fn*/ +} /*namespace xo*/ + +/* end CallbackSet.hpp */ diff --git a/src/callback/CMakeLists.txt b/src/callback/CMakeLists.txt new file mode 100644 index 00000000..58dc3de5 --- /dev/null +++ b/src/callback/CMakeLists.txt @@ -0,0 +1,14 @@ +# callback/CMakeLists.txt + +set(SELF_LIB callback) +set(SELF_SRCS CallbackSet.cpp) + +# reminder: can't be header-only library, because depends on non-header-only refcnt +xo_add_shared_library(${SELF_LIB} ${PROJECT_VERSION} 1 ${SELF_SRCS}) + +# ---------------------------------------------------------------- +# external dependencies: + +xo_dependency(${SELF_LIB} refcnt) + +# end CMakeLists.txt diff --git a/src/callback/CallbackSet.cpp b/src/callback/CallbackSet.cpp new file mode 100644 index 00000000..881f57f8 --- /dev/null +++ b/src/callback/CallbackSet.cpp @@ -0,0 +1,22 @@ +/* file CallbackSet.cpp + * + * author: Roland Conybeare, Sep 2022 + */ + +#include "CallbackSet.hpp" + +namespace xo { + namespace fn { + CallbackId + CallbackId::generate() + { + static CallbackId s_last_id; + + s_last_id = CallbackId(s_last_id.id() + 1); + + return s_last_id; + } /*generate*/ + } /*namespace fn*/ +} /*namespace xo*/ + +/* end CallbackSet.cpp */