diff --git a/xo-subsys/.github/workflows/cmake-single-platform.yml b/xo-subsys/.github/workflows/cmake-single-platform.yml new file mode 100644 index 00000000..f654ea9c --- /dev/null +++ b/xo-subsys/.github/workflows/cmake-single-platform.yml @@ -0,0 +1,57 @@ +# This starter workflow is for a CMake project running on a single platform. There is a different starter workflow if you need cross-platform coverage. +# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-multi-platform.yml +name: CMake on a single platform + +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: + - uses: actions/checkout@v3 + + # ---------------------------------------------------------------- + + - 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: Configure self (subsys) + # 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 -DCMAKE_MODULE_PATH=${{github.workspace}}/local/share/cmake -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} + + - name: Build self (subsys) + # Build your program with the given configuration + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + + - name: Test self (subsys) + working-directory: ${{github.workspace}}/build + # 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-subsys/.gitignore b/xo-subsys/.gitignore new file mode 100644 index 00000000..8e2ef56b --- /dev/null +++ b/xo-subsys/.gitignore @@ -0,0 +1,8 @@ +# emacs workspace config +.projectile +# symlink to ${mybuilddirectory}/compile_commands.json for LSP +compile_commands.json +# LSP keeps state here +.cache +# typical build directories +.build* diff --git a/xo-subsys/CMakeLists.txt b/xo-subsys/CMakeLists.txt new file mode 100644 index 00000000..b4150081 --- /dev/null +++ b/xo-subsys/CMakeLists.txt @@ -0,0 +1,38 @@ +# xo-subsys/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project(subsys VERSION 0.1) + +include(GNUInstallDirs) +include(cmake/xo-bootstrap-macros.cmake) + +xo_cxx_toplevel_options2() + +# ---------------------------------------------------------------- +# cmake -DCMAKE_BUILD_TYPE=coverage +xo_toplevel_coverage_config2() + +# ---------------------------------------------------------------- +# cmake -DCMAKE_BUILD_TYPE=debug +xo_toplevel_debug_config2() + + +#set(XO_PROJECT_NAME subsys) + +# ---------------------------------------------------------------- +# installing header-only library + +set(SELF_LIB subsys) +xo_add_headeronly_library(${SELF_LIB}) +xo_install_library4(${SELF_LIB} ${PROJECT_NAME}Targets) +#add_library(subsys INTERFACE) +#xo_include_headeronly_options2(subsys) +#xo_install_library2(subsys) + +# ---------------------------------------------------------------- +# provide find_package() support + +xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +# end CMakeLists.txt diff --git a/xo-subsys/README.md b/xo-subsys/README.md new file mode 100644 index 00000000..29b31f1a --- /dev/null +++ b/xo-subsys/README.md @@ -0,0 +1,102 @@ +# plugin initialization support + +subsys is a small header-only library providing support for plugin initialization + +## Features + +- provide application control of initialization order across c++ libraries +- circumvents the 'static order initialization fiasco' +- ensure initialization code runs exactly once if subsystem is linked +- enforce initialization order constraints +- defend against static linker stripping essential initialization code +- designed to work cleanly for libraries integrating into existing executable like python, java runtime, .. +- initialization state browseable at runtime + +## Getting Started + +### build + install `indentlog` dependency + +see [github/rconybea/indentlog](https://github.com/Rconybea/indentlog) + +### copy `subsys` repository locally +``` +$ git clone git@github.com:rconybea/subsys.git +$ ls -d subsys +subsys +``` + +### build + install +``` +$ cd subsys +$ mkdir build +$ cd build +$ INSTALL_PREFIX=/usr/local # or wherever you prefer +$ cmake -DCMAKE_MODULE_PATH=${INSTALL_PREFIX}/share/cmake -DCMAKE_PREFIX_PATH=${INSTALL_PREFIX} -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} .. +$ make +$ make install +``` + +`CMAKE_PREFIX_PATH` should point to prefix where `indentlog` is installed + +alternatively, if you're a nix user: +``` +$ git clone git@github.com:rconybea/xo-nix.git +$ ls -d xo-nix +xo-nix +$ cd xo-nix +$ nix-build -A subsys +``` + +### build for unit test coverage +``` +$ cd subsys +$ mkdir ccov +$ cd ccov +$ cmake -DCMAKE_PREFIX_PATH=${INSTALL_PREFIX} -DCODE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug .. +``` + +## Examples + +### 1 +``` +// initialization code in .hpp for a subsystem foo, that relies on related subsystem bar + +#include "subsys/Subsystem.hpp" + +enum S_foo_tag {}; /* tag to represent initialization of subsystem foo */ + +template<> +struct InitSubsys { + static void init() { + // plugin initialization for subsystem foo + } + + static InitEvidence require() { + InitEvidence retval; + + // demand initialization of dependent subsystem bar, + // before initialization subsystem foo + // + retval ^= InitSubsys::require(); + + // initialization of this subsystem foo + retval ^= Subsystem::provide("foo", &init); + + return retval; + } +}; +``` + +``` +// in application code that relies on foo (perhaps along with other subsystems), +// for example in a pybind11 module: +// +PYBIND11_MODULE(pyfoo, m) { + // include foo in initialization set + InitSubsys::require(); + // ensure foo + dependencies are initialized + Subsystem::initialize_all(); + + ... +} +``` diff --git a/xo-subsys/cmake/subsysConfig.cmake.in b/xo-subsys/cmake/subsysConfig.cmake.in new file mode 100644 index 00000000..9c15f36a --- /dev/null +++ b/xo-subsys/cmake/subsysConfig.cmake.in @@ -0,0 +1,4 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +check_required_components("@PROJECT_NAME@") diff --git a/xo-subsys/cmake/xo-bootstrap-macros.cmake b/xo-subsys/cmake/xo-bootstrap-macros.cmake new file mode 100644 index 00000000..aba31169 --- /dev/null +++ b/xo-subsys/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-subsys/include/xo/subsys/Subsystem.hpp b/xo-subsys/include/xo/subsys/Subsystem.hpp new file mode 100644 index 00000000..3cc6012a --- /dev/null +++ b/xo-subsys/include/xo/subsys/Subsystem.hpp @@ -0,0 +1,310 @@ +/* file Subsystem.hpp + * + * author: Roland Conybeare, Aug 2022 + */ + +#pragma once + +#include "xo/indentlog/scope.hpp" +#include +#include +#include +#include +#include + +/* e.g. XO_SUBSYSTEM_TAG(simulator) => xo::S_simulator_tag */ +#define XO_SUBSYSTEM_TAG(subsys_name) xo::S_ ## subsys_name ## _tag + +/* e.g. XO_SUBSYSTEM_REQUIRE(simulator) => + * xo::InitSubsys::require() + */ +#define XO_SUBSYSTEM_REQUIRE(subsys_name) xo::InitSubsys::require(); + +/* e.g. XO_SUBSYSTEM_PROVIDE(simulator, &init) => + * xo::Subsystem::provide("simulator", &init) + */ +#define XO_SUBSYSTEM_PROVIDE(subsys_name, init_addr) xo::Subsystem::provide(STRINGIFY(subsys_name), init_addr) + +//#define VERIFY_SUBSYSTEM(tag) Subsystem::verify_present(STRINGIFY(tag)) + +namespace xo { + using xo::tostr; + + /* evidence that one or more subsystems have been initialized. + * Used to prevent static linker stripping must-run initialization code + */ + class InitEvidence { + public: + InitEvidence() = default; + InitEvidence(std::uint64_t x) : evidence_{x} {} + + std::uint64_t evidence() const { return evidence_; } + + InitEvidence operator^=(InitEvidence x) { + this->evidence_ ^= x.evidence_; + + return *this; + } /*operator^=*/ + + InitEvidence operator^(InitEvidence x) { + return InitEvidence(this->evidence_ ^ x.evidence_); + } + + private: + /* we don't care about the specific value computed here, + * purpose is to be sufficiently impenentrable to compiler such + * that static linker can't optimize it away + */ + std::uint64_t evidence_ = 0; + }; /*InitEvidence*/ + + inline std::ostream & + operator<<(std::ostream & os, InitEvidence x) { + os << ""; + return os; + } /*operator<<*/ + + /* Goals: + * 1. provide for code that must run once (and only once) + * to initialize subsystems + * 2. in executable, want to run such code after main() starts + * instead of relying on static initializers; + * that way init behavior can be parameterized based on + * program arguments + * + * Use + * // subsystem foo + * + * enum Foo_tag {}; + * + * // guarantees that if anything gets initialized, then + * // foo_init() is included + * // + * template<> + * struct InitSubsys { + * static void foo_init() { ... } + * + * static InitEvidence require() { + * return Subsystem::require("foo", &foo_init); + * } + * }; + * + * .. register other subsystems .. + * + * Subsystem::initialize_all(); // foo_init() has been called once + * + * If subsystem bar depends on supporting subsystem {foo, quux}, then write: + * + * enum Bar_tag {}; + * + * template<> + * struct InitSubsys { + * static void bar_init() { ... } + * + * static InitEvidence require() { + * InitEvidence retval; + * + * retval ^= InitSubsys::require(); + * retval ^= InitSubsys::require(); + * + * retval ^= Subsystem::require("bar", &bar_init); + * } + * }; + * + * If using subsystems from a shared library (so no access to cmdline args etc): + * e.g. in pyfoo.cpp: + * + * InitEvidence s_pyfoo_init = InitSubsys::require(); + * or + * InitEvidence s_pyfoo_init = (InitSubsys::require() + * ^ InitSubsys::require()); + * + * Note: Tag argument here no relation of BuildTag in SubsystemImpl below + */ + template + struct InitSubsys {}; + + /* BuildTag: placeholder; insisting on header-only library */ + template + class SubsystemImpl { + public: + SubsystemImpl() = default; + SubsystemImpl(bool require_flag, + std::string_view subsys_name, + std::function init_fn) + : require_flag_{require_flag}, + subsys_name_{subsys_name}, + init_fn_{init_fn} {} + + /* establish an empty Subsystem record for subsys_name. + * record is _not_ linked into s_subsys_l! + * idempotent. + */ + template + static SubsystemImpl * establish() { + static SubsystemImpl s_subsys; + + return &s_subsys; + } /*establish*/ + + template + static bool verify_present(std::string subsys_tag) { + SubsystemImpl * subsys = establish(); + + if (!subsys->require_flag()) { + throw std::runtime_error(tostr("subsystem not present." + "(missing InitSubsys<", subsys_tag, ">::require() ?)")); + return false; + } + + return true; + } /*verify_present*/ + + /* provide (once only) initialization code for a subsystem with tag SubsystemTag. + * ideally this would be called just once for a particular tag; + * if called multiple times, calls after the first are no-ops. + */ + template + static InitEvidence provide(std::string_view subsys_name, + std::function init_fn) { + SubsystemImpl * subsys = establish(); + + provide_aux(subsys_name, init_fn, subsys); + + return InitEvidence(reinterpret_cast(subsys)); + } /*provide*/ + + /* throw exception if there's anything left for .initialize_all() to do, + * or subsystems have been added since last call to .initialize_all() + * Can use this to remind application author to call SubsystemImpl::initialize_all() + */ + static bool verify_all_initialized(); + + /* 1. initialize all subsystems: promise that for every preceding call + * to .require(), the corresponding initialization function has been + * run exactly once. + * 2. harmless to call this multiple times -- will not call any init_fn more than once + * 3. can interleave .initialize_all() with .require() as desired + */ + static InitEvidence initialize_all(); + + bool require_flag() const { return require_flag_; } + bool init_flag() const { return init_flag_; } + std::string_view subsys_name() const { return subsys_name_; } + + InitEvidence initialize(); + + private: + /* helper for .provide() */ + static void provide_aux(std::string_view subsys_name, + std::function init_fn, + SubsystemImpl * p_subsys); + + private: + /* set to true iff .s_subsys_l has been extended since last call to .initialize_all() */ + static bool s_dirty_flag; + /* one member for each unique call to .require() */ + static std::list s_subsys_l; + + private: + /* set to true on 1st call to .require() */ + bool require_flag_ = false; + /* set to true when .init_fn() invoked */ + bool init_flag_ = false; + /* unique subsystem name */ + std::string_view subsys_name_; + /* call this function once (at most) to initialize this subsystem */ + std::function init_fn_; + }; /*SubsystemImpl*/ + + template + bool + SubsystemImpl::s_dirty_flag = false; + + template + std::list *> + SubsystemImpl::s_subsys_l; + + template + void + SubsystemImpl::provide_aux(std::string_view subsys_name, + std::function init_fn, + SubsystemImpl * p_subsys) + { + if (!p_subsys->require_flag()) { + /* 1st call to .provide() for this SubsystemTag */ + + using xo::scope; + using xo::xtag; + + scope log(XO_ENTER0(chatty), + xtag("subsys", subsys_name), + xtag("address", p_subsys)); + + *p_subsys = SubsystemImpl(true /*require_flag*/, + subsys_name, + init_fn); + + s_dirty_flag = true; + s_subsys_l.push_back(p_subsys); + } + } /*provide_aux*/ + + template + bool + SubsystemImpl::verify_all_initialized() + { + if (s_dirty_flag) { + scope log(XO_ENTER0(error), "required subsystems NOT initialized!?"); + + for (SubsystemImpl * subsys : s_subsys_l) { + if (!subsys->init_flag()) { + log && log("missing InitSubsyssubsys_name(), "_tag>::require()"); + } + } + + throw std::runtime_error("Subsystem::verify_initialized:" + " Subsystem::initialize_all() never called, or out-of-date"); + return false; + } + + return true; + } /*verify_all_initialized*/ + + template + InitEvidence + SubsystemImpl::initialize_all() { + scope log(XO_ENTER0(chatty)); + + InitEvidence retval; + + if (s_dirty_flag) { + for (SubsystemImpl * subsys : s_subsys_l) { + log && log("init", xtag("subsys", subsys->subsys_name())); + + retval ^= subsys->initialize(); + } + } + + s_dirty_flag = false; + + return retval; + } /*initialize_all*/ + + template + InitEvidence + SubsystemImpl::initialize() + { + if (!init_flag_) { + init_flag_ = true; + + init_fn_(); + } + + return InitEvidence(reinterpret_cast(this)); + } /*initialize*/ + + using Subsystem = SubsystemImpl; +} /*namespace xo*/ + +/* end Subsystem.hpp */