diff --git a/xo-callback/.github/workflows/main.yml b/xo-callback/.github/workflows/main.yml new file mode 100644 index 00000000..38eee8cf --- /dev/null +++ b/xo-callback/.github/workflows/main.yml @@ -0,0 +1,98 @@ +name: build xo-callback + xo dependencies + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + build: + # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. + # You can convert this to a matrix build if you need cross-platform coverage. + # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix + runs-on: ubuntu-latest + + steps: + - name: checkout source + uses: actions/checkout@v3 + + - name: Install catch2 + # install catch2. see [[https://stackoverflow.com/questions/57982945/how-to-apt-get-install-in-a-github-actions-workflow]] + run: sudo apt-get install -y catch2 + + # ---------------------------------------------------------------- + + - name: Clone xo-cmake + uses: actions/checkout@v3 + with: + repository: Rconybea/xo-cmake + path: repo/xo-cmake + + - name: Configure xo-cmake + run: cmake -B ${{github.workspace}}/build_xo-cmake -DCMAKE_INSTALL_PREFIX=${{github.workspace}}/local repo/xo-cmake + + - name: Build xo-cmake (trivial) + run: cmake --build ${{github.workspace}}/build_xo-cmake --config ${{env.BUILD_TYPE}} + + - name: Install xo-cmake + run: cmake --install ${{github.workspace}}/build_xo-cmake + + # ---------------------------------------------------------------- + + - name: Clone indentlog + uses: actions/checkout@v3 + with: + repository: Rconybea/indentlog + path: repo/indentlog + + - name: Configure indentlog + # configure cmake for indentlog in dedicated build directory. + run: cmake -B ${{github.workspace}}/build_indentlog -DCMAKE_MODULE_PATH=${{github.workspace}}/local/share/cmake -DCMAKE_INSTALL_PREFIX=${{github.workspace}}/local repo/indentlog + + - name: Build indentlog + run: cmake --build ${{github.workspace}}/build_indentlog --config ${{env.BUILD_TYPE}} + + - name: Install indentlog + # install into ${{github.workspace}}/local + run: cmake --install ${{github.workspace}}/build_indentlog + + # ---------------------------------------------------------------- + + - name: Clone refcnt + uses: actions/checkout@v3 + with: + repository: Rconybea/refcnt + path: repo/refcnt + + - name: Configure refcnt + # configure cmake for refcnt in dedicated build directory. + run: cmake -B ${{github.workspace}}/build_refcnt -DCMAKE_MODULE_PATH=${{github.workspace}}/local/share/cmake -DCMAKE_PREFIX_PATH=${{github.workspace}}/local -DCMAKE_INSTALL_PREFIX=${{github.workspace}}/local repo/refcnt + + - name: Build refcnt + run: cmake --build ${{github.workspace}}/build_refcnt --config ${{env.BUILD_TYPE}} + + - name: Install refcnt + # install into ${{github.workspace}}/local + run: cmake --install ${{github.workspace}}/build_refcnt + + # ---------------------------------------------------------------- + + - name: Configure self (callback) + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: cmake -B ${{github.workspace}}/build_callback -DCMAKE_MODULE_PATH=${{github.workspace}}/local/share/cmake -DCMAKE_PREFIX_PATH=${{github.workspace}}/local -DCMAKE_INSTALL_PREFIX=${{github.workspace}}/local -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} + + - name: Build self (callback) + # Build your program with the given configuration + run: cmake --build ${{github.workspace}}/build_callback --config ${{env.BUILD_TYPE}} + + - name: Test self (callback) + working-directory: ${{github.workspace}}/build_callback + # Execute tests defined by the CMake configuration. + # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail + run: ctest -C ${{env.BUILD_TYPE}} diff --git a/xo-callback/.gitignore b/xo-callback/.gitignore new file mode 100644 index 00000000..132925c0 --- /dev/null +++ b/xo-callback/.gitignore @@ -0,0 +1,6 @@ +# symlink to path/to/build/compile_commands.json should be manual +compile_commands.json +# lsp keeps state here +.cache +# typical build directories +.build* diff --git a/xo-callback/CMakeLists.txt b/xo-callback/CMakeLists.txt new file mode 100644 index 00000000..17d73eb3 --- /dev/null +++ b/xo-callback/CMakeLists.txt @@ -0,0 +1,31 @@ +# callback/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project(callback VERSION 0.1) + +include(GNUInstallDirs) +include(cmake/xo-bootstrap-macros.cmake) + +xo_cxx_toplevel_options3() + +# ---------------------------------------------------------------- +# common 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}) + +# ---------------------------------------------------------------- +# sources + +add_subdirectory(src/callback) +#add_subdirectory(utest) + +# ---------------------------------------------------------------- +# provide find_package() support + +xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +# end CMakeLists.txt diff --git a/xo-callback/README.md b/xo-callback/README.md new file mode 100644 index 00000000..53cc5849 --- /dev/null +++ b/xo-callback/README.md @@ -0,0 +1,50 @@ +# callback-set with reentrant invocation + +Reentrant: +1. A callback can modify parent callback-set (for example to remove itself), + even while being invoked. +2. Any such re-entrant operations are deferred until callback invocation completes. + +## Getting Started + +### build + install dependencies + +- [github/Rconybea/refcnt](https://github.com/Rconybea/refcnt) + +### build + install + +``` +$ cd xo-callback +$ mkdir build +$ cd build +$ INSTALL_PREFIX=/usr/local # or wherever you prefer, e.g. ~/local +$ cmake \ + -DCMAKE_MODULE_PATH=${INSTALL_PREFIX}/share/cmake \ + -DCMAKE_PREFIX_PATH=${INSTALL_PREFIX} \ + -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} .. +$ make +$ make install +``` +(also see .github/workflows/main.yml) + +### build for unit test coverage + +``` +$ cd xo-callback +$ mkdir build-ccov +$ cd build-ccov +$ cmake \ + -DCMAKE_MODULE_PATH=${INSTALL_PREFIX}/share/cmake \ + -DCMAKE_PREFIX_PATH=${INSTALL_PREFIX} \ + -DCODE_COVERAGE=ON \ + -DCMAKE_BUILD_TYPE=Debug .. +``` + +### LSP (language server) support + +LSP looks for compile commands in the root of the source tree; +cmake creates them in the root of its build directory. +``` +$ cd xo-callback +$ ln -s build/compile_commands.json +``` diff --git a/xo-callback/cmake/callbackConfig.cmake.in b/xo-callback/cmake/callbackConfig.cmake.in new file mode 100644 index 00000000..f7176f38 --- /dev/null +++ b/xo-callback/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/xo-callback/cmake/xo-bootstrap-macros.cmake b/xo-callback/cmake/xo-bootstrap-macros.cmake new file mode 100644 index 00000000..aba31169 --- /dev/null +++ b/xo-callback/cmake/xo-bootstrap-macros.cmake @@ -0,0 +1,35 @@ +# ---------------------------------------------------------------- +# for example: +# $ PREFIX=/usr/local # for example +# $ cmake -DCMAKE_MODULE_PATH=prefix -DCMAKE_INSTALL_PREFIX=$PREFIX -B .build +# +# will get +# CMAKE_MODULE_PATH +# from xo-cmake-config --cmake-module-path +# +# and expect .cmake macros in +# CMAKE_MODULE_PATH/xo_macros/xo_cxx.cmake +# ---------------------------------------------------------------- + +find_program(XO_CMAKE_CONFIG_EXECUTABLE NAMES xo-cmake-config REQUIRED) + +if ("${XO_CMAKE_CONFIG_EXECUTABLE}" STREQUAL "XO_CMAKE_CONFIG_EXECUTABLE-NOT_FOUND") + message(FATAL "could not find xo-cmake-config executable") +endif() + +message(STATUS "XO_CMAKE_CONFIG_EXECUTABLE=${XO_CMAKE_CONFIG_EXECUTABLE}") + +if (NOT XO_SUBMODULE_BUILD) + if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix)) + # default to typical install location for xo-project-macros + execute_process(COMMAND ${XO_CMAKE_CONFIG_EXECUTABLE} --cmake-module-path OUTPUT_VARIABLE CMAKE_MODULE_PATH) + message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}") + endif() +endif() + +# needs to have been installed somewhere on CMAKE_MODULE_PATH, +# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX) +# +include(xo_macros/xo_cxx) + +xo_cxx_bootstrap_message() diff --git a/xo-callback/include/xo/callback/CallbackSet.hpp b/xo-callback/include/xo/callback/CallbackSet.hpp new file mode 100644 index 00000000..cfc3047b --- /dev/null +++ b/xo-callback/include/xo/callback/CallbackSet.hpp @@ -0,0 +1,318 @@ +/* @file CallbackSet.hpp */ + +#pragma once + +//#include "indentlog/scope.hpp" +//#include "indentlog/print/tag.hpp" +#include +#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); + * + * Tag so xo-callback can be header-only + */ + template + class CallbackIdImpl { + public: + CallbackIdImpl() = default; + explicit CallbackIdImpl(uint32_t id) : id_{id} {} + + /* generate a globally-unique id (not threadsafe) */ + static CallbackIdImpl generate() { + static CallbackIdImpl s_last_id; + + s_last_id = CallbackIdImpl(s_last_id.id() + 1); + + return s_last_id; + } /*generate*/ + + uint32_t id() const { return id_; } + + private: + uint32_t id_ = 0; + }; /*CallbackIdImpl*/ + + template + inline bool operator==(CallbackIdImpl lhs, CallbackIdImpl rhs) { return lhs.id() == rhs.id(); } + template + inline bool operator!=(CallbackIdImpl lhs, CallbackIdImpl rhs) { return lhs.id() != rhs.id(); } + + using CallbackId = CallbackIdImpl; + + /* 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; + /* operate on callback with this id */ + CallbackId id_; + /* callback function to add/remove */ + 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::element_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: + * - Fnptr::element_type + * - Fnptr::get() -> Fnptr::element_type const * + * - 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 (with cb=cb_recd.fn_) + * 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*/ + + 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 NotifyCallbackSet's 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/xo-callback/src/callback/CMakeLists.txt b/xo-callback/src/callback/CMakeLists.txt new file mode 100644 index 00000000..cfeaf08c --- /dev/null +++ b/xo-callback/src/callback/CMakeLists.txt @@ -0,0 +1,14 @@ +# callback/CMakeLists.txt + +set(SELF_LIB callback) +#set(SELF_SRCS CallbackSet.cpp) + +xo_add_headeronly_library(${SELF_LIB}) +xo_install_library4(${SELF_LIB} ${PROJECT_NAME}Targets) + +# ---------------------------------------------------------------- +# external dependencies: + +#xo_dependency(${SELF_LIB} refcnt) + +# end CMakeLists.txt diff --git a/xo-callback/src/callback/CallbackSet.cpp b/xo-callback/src/callback/CallbackSet.cpp new file mode 100644 index 00000000..1a889f3a --- /dev/null +++ b/xo-callback/src/callback/CallbackSet.cpp @@ -0,0 +1,13 @@ +/* file CallbackSet.cpp + * + * author: Roland Conybeare, Sep 2022 + */ + +#include "CallbackSet.hpp" + +namespace xo { + namespace fn { + } /*namespace fn*/ +} /*namespace xo*/ + +/* end CallbackSet.cpp */