diff --git a/xo-refcnt/.github/workflows/cmake-single-platform.yml b/xo-refcnt/.github/workflows/cmake-single-platform.yml new file mode 100644 index 00000000..527d5655 --- /dev/null +++ b/xo-refcnt/.github/workflows/cmake-single-platform.yml @@ -0,0 +1,80 @@ +# 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: 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: Configure self (refcnt) + # 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_refcnt -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 refcnt + # Build your program with the given configuration + run: cmake --build ${{github.workspace}}/build_refcnt --config ${{env.BUILD_TYPE}} + + - name: Test refcnt + working-directory: ${{github.workspace}}/build_refcnt + # 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-refcnt/.gitignore b/xo-refcnt/.gitignore new file mode 100644 index 00000000..39d62bcb --- /dev/null +++ b/xo-refcnt/.gitignore @@ -0,0 +1,9 @@ +# emacs workspace config +.projectile +# symlink to ${mybuilddirectory}/compile_commands.json for LSP +compile_commands.json +# LSP keeps state here +.cache +# typical build directories +.build* +ccov diff --git a/xo-refcnt/CMakeLists.txt b/xo-refcnt/CMakeLists.txt new file mode 100644 index 00000000..4ef7c6c4 --- /dev/null +++ b/xo-refcnt/CMakeLists.txt @@ -0,0 +1,31 @@ +# refcnt/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project(refcnt VERSION 0.1) + +include(GNUInstallDirs) +include(cmake/xo-bootstrap-macros.cmake) + +xo_cxx_toplevel_options3() + +# ---------------------------------------------------------------- +# c++ settings + +set(XO_PROJECT_NAME refcnt) # is this used? +set(PROJECT_CXX_FLAGS "") +#set(PROJECT_CXX_FLAGS "-fconcepts-diagnostics-depth=2") # gcc-only! + +add_definitions(${PROJECT_CXX_FLAGS}) + +# ---------------------------------------------------------------- + +add_subdirectory(src) +add_subdirectory(utest) + +# ---------------------------------------------------------------- +# cmake export + +xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +# end CMakeLists.txt diff --git a/xo-refcnt/README.md b/xo-refcnt/README.md new file mode 100644 index 00000000..e17529d9 --- /dev/null +++ b/xo-refcnt/README.md @@ -0,0 +1,104 @@ +# intrusive reference counting + +Refcnt is a small shared library supplying intrusive reference counting. + +## Features + +- base class `ref::Refcounted`. + Application classes opt-in to reference counting by inheriting this class. +- common base simplifies connecting to common-base-object applications such as python, java etc. + +## Getting Started + +### build + install `xo-cmake` dependency (cmake macros) + +see [github/Rconybea/xo-cmake](https://github.com/Rconybea/xo-cmake) + +Installs a few cmake ingredients, along with a build assistant for XO projects such as this one. + +### build + install XO deps +``` +$ xo-build --clone --configure --build --install xo-indentlog +``` + +### copy `refcnt` repository locally +``` +$ xo-build --clone xo-refcnt +``` + +or equivalently +``` +$ git clone git@github.com:Rconybea/refcnt.git xo-refcnt +``` + +### build + install +``` +$ xo-build --configure --build --install xo-refcnt +``` + +or equivalently: +``` +$ mkdir xo-refcnt/.build +$ cmake -DCMAKE_INSTALL_PREFIX=${PREFIX} -S xo-refcnt -B xo-refcnt/.build +$ cmake --build xo-refcnt/.build +$ cmake --install xo-refcnt/.build +``` + +`CMAKE_PREFIX_PATH` should point to the prefix where `xo-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 xo-refcnt +``` + +### build for unit test coverage +``` +$ cmake -DCMAKE_BUILD_TYPE=coverage -DCMAKE_PREFIX_PATH=${PREFIX} -S xo-refcnt -B xo-refcnt/.build-ccov +$ cmake --build xo-refcnt/.build-ccov +``` + +### LSP support +``` +$ cd xo-refcnt +$ ln -s build/compile_commands.json # lsp will look for compile_commands.json in the root of the source tree +``` + +## Examples + +### 1 +``` +#include "xo/refcnt/Refcounted.hpp" + +using xo::ref::Refcounted; + +struct MyObject : public Refcounted { + static rp make() { return new MyObject(); } + +private: + // intrusively-reference-counted objects should only be heap-allocated + MyObject() { ... } +}; + +int main() { + // create reference-counted instance + auto x = MyObject::make(); + auto y = x; + // x,y refer to the same instance. + x = nullptr; + // y holds last reference + y = nullptr; + // MyObject has been deleted +} +``` + +### 2 + +To log reference-counting activity + +``` +xo::ref::intrusive_ptr_set_debug(true); +``` diff --git a/xo-refcnt/cmake/refcntConfig.cmake.in b/xo-refcnt/cmake/refcntConfig.cmake.in new file mode 100644 index 00000000..5b38fa74 --- /dev/null +++ b/xo-refcnt/cmake/refcntConfig.cmake.in @@ -0,0 +1,6 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(indentlog) +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +check_required_components("@PROJECT_NAME@") diff --git a/xo-refcnt/cmake/xo-bootstrap-macros.cmake b/xo-refcnt/cmake/xo-bootstrap-macros.cmake new file mode 100644 index 00000000..aba31169 --- /dev/null +++ b/xo-refcnt/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-refcnt/include/xo/cxxutil/demangle.hpp b/xo-refcnt/include/xo/cxxutil/demangle.hpp new file mode 100644 index 00000000..e2184f5d --- /dev/null +++ b/xo-refcnt/include/xo/cxxutil/demangle.hpp @@ -0,0 +1,92 @@ +/* @file demangle.hpp */ + +#pragma once + +#include +#include +#include // std::array +#include // std::index_sequence + +namespace xo { + namespace reflect { + + template + constexpr auto + substring_as_array(std::string_view str, + std::index_sequence indexes) + { + //return std::array{str[Idxs]..., '\n'}; + return std::array{str[Idxs]...}; + } /*substring_as_array*/ + + template constexpr auto type_name_array() { +#if defined(__clang__) + constexpr auto prefix = std::string_view{"[T = "}; + constexpr auto suffix = std::string_view{"]"}; + constexpr auto function = std::string_view{__PRETTY_FUNCTION__}; +#elif defined(__GNUC__) + constexpr auto prefix = std::string_view{"with T = "}; + constexpr auto suffix = std::string_view{"]"}; + constexpr auto function = std::string_view{__PRETTY_FUNCTION__}; +#elif defined(_MSC_VER) + constexpr auto prefix = std::string_view{"type_name_array<"}; + constexpr auto suffix = std::string_view{">(void)"}; + constexpr auto function = std::string_view{__FUNCSIG__}; +#else +# error type_name_array: Unsupported compiler +#endif + + constexpr auto start = function.find(prefix) + prefix.size(); + constexpr auto end = function.rfind(suffix); + + //static_assert(start < end); + + constexpr auto name = function.substr(start, (end - start)); + + constexpr auto ixseq = std::make_index_sequence{}; + + return substring_as_array(name, ixseq); + } /*type_name_array*/ + + template + struct type_name_holder { + static inline constexpr auto value = type_name_array(); + }; + + template + constexpr auto type_name() -> std::string_view + { + constexpr auto& value = type_name_holder::value; + return std::string_view{value.data(), value.size()}; + } + +#ifdef NOT_IN_USE + template + struct join + { + // Join all strings into a single std::array of chars + static constexpr auto impl() noexcept + { + constexpr std::size_t len = (Strs.size() + ... + 0); + std::array arr{}; + auto append = [i = 0, &arr](auto const& s) mutable { + for (auto c : s) arr[i++] = c; + }; + (append(Strs), ...); + arr[len] = 0; + return arr; + } + // Give the joined string static storage + static constexpr auto arr = impl(); + // View as a std::string_view + static constexpr std::string_view value {arr.data(), arr.size() - 1}; + }; + + // Helper to get the value out + template + static constexpr auto join_v = join::value; +#endif + } /*namespace reflect*/ +} /*namespace xo*/ + +/* end demangle.hpp */ diff --git a/xo-refcnt/include/xo/refcnt/Displayable.hpp b/xo-refcnt/include/xo/refcnt/Displayable.hpp new file mode 100644 index 00000000..5a184667 --- /dev/null +++ b/xo-refcnt/include/xo/refcnt/Displayable.hpp @@ -0,0 +1,29 @@ +/* @file Displayable.hpp */ + +#pragma once + +#include "Refcounted.hpp" + +namespace xo { + namespace ref { + class Displayable : public Refcount { + public: + /* write some kind of human-readable representation on stream */ + virtual void display(std::ostream & os) const = 0; + std::string display_string() const; + }; /*Displayable*/ + + /* see also + * operator<<(std::ostream &, intrusive_ptr const &) + * in [Refcounted.hpp] + */ + inline std::ostream & + operator<<(std::ostream &os, Displayable const & x) { + x.display(os); + return os; + } /*operator<<*/ + + } /*namespace ref*/ +} /*namespace xo*/ + +/* end Displayable.hpp */ diff --git a/xo-refcnt/include/xo/refcnt/Refcounted.hpp b/xo-refcnt/include/xo/refcnt/Refcounted.hpp new file mode 100644 index 00000000..bab9f1ba --- /dev/null +++ b/xo-refcnt/include/xo/refcnt/Refcounted.hpp @@ -0,0 +1,351 @@ +/* @file Refcounted.hpp */ + +#pragma once + +#include "xo/indentlog/scope.hpp" +#include "xo/cxxutil/demangle.hpp" + +//#include +#include +#include + +namespace xo { + namespace ref { + template + class intrusive_ptr; + } + + template + using rp = ref::intrusive_ptr; + + namespace ref { + class Refcount; + + template + class Borrow; + + /* originally used boost::instrusive_ptr<>. + * ran into a bug. probably mine, but implemented + * refcounting inline for debugging + */ + template + class intrusive_ptr { + public: + using element_type = T; + + public: + intrusive_ptr() : ptr_(nullptr) {} + intrusive_ptr(T * x) : ptr_(x) { +#ifdef XO_INTRUSIVE_PTR_ENABLE_LOGGING + intrusive_ptr_log_ctor(sc_self_type, this, x); +#endif + intrusive_ptr_add_ref(ptr_); + } /*ctor*/ + + /* NOTE: need exactly this form for copy-constructor + * clang11 will not recognize template form below as + * supplying copy ctor, and default version is broken for + * instrusive_ptr. + */ + intrusive_ptr(intrusive_ptr const & x) : ptr_(x.get()) { +#ifdef XO_INTRUSIVE_PTR_ENABLE_LOGGING + intrusive_ptr_log_cctor(sc_self_type, this, x.get()); +#endif + intrusive_ptr_add_ref(ptr_); + } /*cctor*/ + + /* create from instrusive pointer to some related type S */ + template + intrusive_ptr(intrusive_ptr const & x) : ptr_(x.get()) { +#ifdef XO_INTRUSIVE_PTR_ENABLE_LOGGING + intrusive_ptr_log_cctor(sc_self_type, this, x.get()); +#endif + intrusive_ptr_add_ref(ptr_); + } /*cctor*/ + + /* move ctor -- in this case don't need to update refcount */ + intrusive_ptr(intrusive_ptr && x) : ptr_{std::move(x.ptr_)} { +#ifdef XO_INTRUSIVE_PTR_ENABLE_LOGGING + intrusive_ptr_log_mctor(sc_self_type, this, ptr_); +#endif + /* since we're moving from x, need to make sure x dtor + * doesn't decrement refcount + */ + x.ptr_ = nullptr; + } + + /* aliasing ctor. see ctor (8) here: + * [[https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr]] + * and this dicsussion: + * [[https://stackoverflow.com/questions/49178231/pybind11-multiple-inheritance-with-custom-holder-type-fails-to-cast-to-base-type/73131206#73131206]] + */ + template + intrusive_ptr(intrusive_ptr const & /*x*/, element_type * y) : ptr_{y} { + if (std::is_same()) { +#ifdef XO_INTRUSIVE_PTR_ENABLE_LOGGING + intrusive_ptr_log_actor(sc_self_type, this, y); +#endif + intrusive_ptr_add_ref(ptr_); + ; /* trivial aliasing, proceed */ + } else { + using xo::xtag; + throw std::runtime_error(tostr("attempt to use aliasing ctor with", + xtag("Y", reflect::type_name()), + xtag("T", reflect::type_name()))); + } + } /*ctor*/ + + ~intrusive_ptr() { + T * x = this->ptr_; + +#ifdef XO_INTRUSIVE_PTR_ENABLE_LOGGING + intrusive_ptr_log_dtor(sc_self_type, this, x); +#endif + + this->ptr_ = nullptr; + + intrusive_ptr_release(x); + } /*dtor*/ + + static bool compare(intrusive_ptr const & x, + intrusive_ptr const & y) { + return ptrdiff_t(x.get() - y.get()); + } + + Borrow borrow() const; + + T * get() const { return ptr_; } + + T * operator->() const { return ptr_; } + + operator bool() const { return ptr_ != nullptr; } + + intrusive_ptr & operator=(intrusive_ptr const & rhs) { + T * x = rhs.get(); + +#ifdef XO_INTRUSIVE_PTR_ENABLE_LOGGING + intrusive_ptr_log_assign(sc_self_type, this, x); +#endif + + T * old = this->ptr_; + this->ptr_ = x; + + intrusive_ptr_add_ref(x); + intrusive_ptr_release(old); + + return *this; + } /*operator=*/ + + intrusive_ptr & operator=(intrusive_ptr && rhs) { +#ifdef XO_INTRUSIVE_PTR_ENABLE_LOGGING + intrusive_ptr_log_massign(sc_self_type, this, rhs.get()); +#endif + + std::swap(this->ptr_, rhs.ptr_); + + /* dtor on rhs will decrement refcount on old value of this->ptr_ + * don't increment for new value, since refcount just transfers from rhs to *this + */ + + return *this; + } /*operator=*/ + + private: + static constexpr std::string_view sc_self_type = xo::reflect::type_name>(); + + private: + T * ptr_ = nullptr; + }; /*intrusive_ptr*/ + + template + inline bool operator==(intrusive_ptr const & x, + intrusive_ptr const & y) { return intrusive_ptr::compare(x, y) == 0; } + + class Refcount { + public: + Refcount() : reference_counter_(0) {} + /* WARNING: virtual dtor here is essential, + * since it's what allows us to invoke delete on a Refcount*, + * for an object of some derived class type T. Otherwise clang + * will use different addresses for Refcount-part and T-part of + * such instance, which means pointer given to delete will not be + * the same as pointer returned from new + */ + virtual ~Refcount() = default; + + uint32_t reference_counter() const { return reference_counter_.load(); } + + private: + friend uint32_t intrusive_ptr_refcount(Refcount *); + friend void intrusive_ptr_add_ref(Refcount *); + friend void intrusive_ptr_release(Refcount *); + + private: + std::atomic reference_counter_; + }; /*Refcount*/ + + inline uint32_t + intrusive_ptr_refcount(Refcount * x) { + /* reporting accurately for diagnostics */ + if (x) + return x->reference_counter_.load(); + else + return 0; + } /*intrusive_ptr_refcount*/ + +#ifdef XO_INTRUSIVE_PTR_ENABLE_LOGGING + extern void intrusive_ptr_set_debug(bool x); + extern void intrusive_ptr_log_ctor(std::string_view const & self_type, + void * this_ptr, + Refcount * x); + /* here actor short for 'aliasing ctor' */ + extern void intrusive_ptr_log_actor(std::string_view const & self_type, + void * this_ptr, + Refcount * x); + extern void intrusive_ptr_log_cctor(std::string_view const & self_type, + void * this_ptr, + Refcount * x); + extern void intrusive_ptr_log_mctor(std::string_view const & self_type, + void *this_ptr, + Refcount * x); + extern void intrusive_ptr_log_dtor(std::string_view const & self_type, + void * this_ptr, + Refcount * x); + extern void intrusive_ptr_log_assign(std::string_view const & self_type, + void * this_ptr, + Refcount * x); + extern void intrusive_ptr_log_massign(std::string_view const & self_type, + void *this_ptr, + Refcount * x); +#endif + extern void intrusive_ptr_add_ref(Refcount * x); + extern void intrusive_ptr_release(Refcount * x); + + template + inline std::ostream & + operator<<(std::ostream & os, intrusive_ptr const & x) { + if(x.get()) { + os << *(x.get()); + } else { + os << "() << ">"; + } + return os; + } /*operator<<*/ + + /** Wrap a (presumably non-reference-counted) class T so that it has a refcount. + **/ + template + class RefcountWrapper : public Refcount, public T { + public: + template + RefcountWrapper(Args... args) : Refcount(), T(std::forward(args...)) {} + }; + + /* borrow a reference-counted pointer to pass down the stack + * 1. borrowed pointer intended to replace: + * a. code like + * foo(rp x), + * passing rp by value requires increment/decrement pair, + * which is superfluous given that caller holds reference throughout + * b. code like + * foo(rp const & x) + * passing rp by reference requires double-indirection in called + * code + * 2. borrowed pointer does not check/maintain reference count. + * it should never be stored in a struct; intended strictly + * to be passed down stack + * 3. just the same, want to be able to copy the borrowed pointer, + * to avoid double-indirection + * 4. also can promote borrowed pointer to full reference-counted + * whenever desired + */ + template + class Borrow { + public: + Borrow() = default; + + template + Borrow(rp const & x) : ptr_(x.get()) {} + + template + Borrow(S * x) : ptr_(x) {} + + Borrow(Borrow const & x) = default; + + /* convert from another borrow, if it has compatible pointer type */ + template + Borrow(Borrow const & x) : ptr_(x.get()) {} + + /* dynamic cast from a pointer to an object of some convertible type */ + template + static Borrow from(Borrow x) { + return Borrow(dynamic_cast(x.get())); + } /*from*/ + + /* promote from native pointer */ + static Borrow from_native(T * x) { + return Borrow(x); + } /*from_native*/ + + T * get() const { return ptr_; } + + rp promote() const { return rp(ptr_); } + + T & operator*() const { return *ptr_; } + T * operator->() const { return ptr_; } + + operator bool() const { return ptr_ != nullptr; } + + static int32_t compare(Borrow const & x, Borrow const & y) { + return ptrdiff_t(x.get() - y.get()); + } /*compare*/ + + static int32_t compare(rp const & x, Borrow const & y) { + return ptrdiff_t(x.get() - y.get()); + } /*compare*/ + + template + Borrow & operator=(const Borrow & x) { + ptr_ = x.get(); + return *this; + } + + Borrow& operator=(const Borrow& x) { + ptr_ = x.get(); + return *this; + } + + private: + T * ptr_ = nullptr; + }; /*Borrow*/ + + template + inline bool operator==(Borrow x, Borrow y) { return Borrow::compare(x, y) == 0; } + + template + inline bool operator==(rp const & x, Borrow y) { return Borrow::compare(x, y) == 0; } + + template + using brw = Borrow; + + template + Borrow + intrusive_ptr::borrow() const { + return Borrow(*this); + } /*borrow*/ + + template + inline std::ostream & + operator<<(std::ostream & os, Borrow x) { + if (x) { + os << *x; + } else { + os << "() << ">"; + } + return os; + } /*operator<<*/ + + } /*namespace ref*/ +} /*namespace xo*/ + +/* end Refcounted.hpp */ diff --git a/xo-refcnt/include/xo/refcnt/Unowned.hpp b/xo-refcnt/include/xo/refcnt/Unowned.hpp new file mode 100644 index 00000000..3f78f0d2 --- /dev/null +++ b/xo-refcnt/include/xo/refcnt/Unowned.hpp @@ -0,0 +1,28 @@ +/* @file Unowned.hpp */ + +namespace xo { + namespace ref { + /* use this is a holder type for pointers that pybind11 should treat + * as "not-my-problem". in particular that pybind11 should never delete. + */ + template + class unowned_ptr { + public: + unowned_ptr(T * x) : ptr_{x} {} + unowned_ptr(unowned_ptr const & x) = default; + ~unowned_ptr() = default; + + T * get() const { return ptr_; } + T * operator->() const { return ptr_; } + + operator bool() const { return ptr_ != nullptr; } + + unowned_ptr & operator=(unowned_ptr const & rhs) = default; + + private: + T * ptr_ = nullptr; + }; /*unowned_ptr*/ + } /*namespace ref*/ +} /*namespace xo*/ + +/* end Unowned.hpp */ diff --git a/xo-refcnt/src/CMakeLists.txt b/xo-refcnt/src/CMakeLists.txt new file mode 100644 index 00000000..9cbc92ff --- /dev/null +++ b/xo-refcnt/src/CMakeLists.txt @@ -0,0 +1,10 @@ +set(SELF_LIB refcnt) +set(SELF_SRCS Refcounted.cpp Displayable.cpp) + +xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) +xo_install_include_tree3(include/xo/cxxutil) + +# NOTE: +# dependency set here must be kept consistent with refcnt/cmake/refcntConfig.cmake.in +# +xo_dependency(${SELF_LIB} indentlog) diff --git a/xo-refcnt/src/Displayable.cpp b/xo-refcnt/src/Displayable.cpp new file mode 100644 index 00000000..ea839841 --- /dev/null +++ b/xo-refcnt/src/Displayable.cpp @@ -0,0 +1,16 @@ +/* @file Displayable.cpp */ + +#include "Displayable.hpp" + +namespace xo { + using xo::tostr; + + namespace ref { + std::string + Displayable::display_string() const { + return tostr(*this); + } /*display_string*/ + } /*namespace ref*/ +} /*namespace xo*/ + +/* end Displayable.cpp */ diff --git a/xo-refcnt/src/Refcounted.cpp b/xo-refcnt/src/Refcounted.cpp new file mode 100644 index 00000000..38accc85 --- /dev/null +++ b/xo-refcnt/src/Refcounted.cpp @@ -0,0 +1,159 @@ +/* @file Refcounted.cpp */ + +#include "Refcounted.hpp" + +namespace xo { + namespace ref { +#ifdef XO_INTRUSIVE_PTR_ENABLE_LOGGING + namespace { + /* verbose logging for intrusive_ptr */ + static bool s_logging_enabled = false; + + void + intrusive_ptr_log_aux(std::string_view const & self_type, + std::string_view const & method_name, + void * this_ptr, + Refcount * x) + { + scope lscope(XO_LITERAL(log_level::verbose, self_type, method_name), + "enter", + xtag("this", this_ptr), + xtag("x", x), + xtag("n", intrusive_ptr_refcount(x))); + } /*intrusive_ptr_log_aux*/ + } /*namespace*/ + + void + intrusive_ptr_set_debug(bool debug_flag) { + s_logging_enabled = debug_flag; + } /*intrusive_ptr_set_debug*/ + + void + intrusive_ptr_log_ctor(std::string_view const & self_type, + void * this_ptr, + Refcount * x) + { + if (s_logging_enabled) + intrusive_ptr_log_aux(self_type, "::ctor", this_ptr, x); + } /*intrusive_ptr_log_ctor*/ + + void + intrusive_ptr_log_actor(std::string_view const & self_type, + void * this_ptr, + Refcount * x) + { + if (s_logging_enabled) + intrusive_ptr_log_aux(self_type, "::actor", this_ptr, x); + } /*intrusive_ptr_log_actor*/ + + void + intrusive_ptr_log_cctor(std::string_view const & self_type, + void * this_ptr, + Refcount * x) + { + if (s_logging_enabled) + intrusive_ptr_log_aux(self_type, "::cctor", this_ptr, x); + } /*intrusive_ptr_log_cctor*/ + + void + intrusive_ptr_log_mctor(std::string_view const & self_type, + void * this_ptr, + Refcount * x) + { + if (s_logging_enabled) + intrusive_ptr_log_aux(self_type, "::mctor", this_ptr, x); + } /*intrusive_ptr_log_mctor*/ + + void + intrusive_ptr_log_dtor(std::string_view const & self_type, + void * this_ptr, + Refcount * x) + { + if (s_logging_enabled) + intrusive_ptr_log_aux(self_type, "::dtor", this_ptr, x); + } /*intrusive_ptr_log_dtor*/ + + void + intrusive_ptr_log_assign(std::string_view const & self_type, + void * this_ptr, + Refcount * x) + { + if (s_logging_enabled) + intrusive_ptr_log_aux(self_type, "::=", this_ptr, x); + } /*intrusive_ptr_log_assign*/ + + void + intrusive_ptr_log_massign(std::string_view const & self_type, + void * this_ptr, + Refcount * x) + { + if (s_logging_enabled) + intrusive_ptr_log_aux(self_type, "::m=", this_ptr, x); + } /*intrusive_ptr_log_massign*/ +#endif + + void + intrusive_ptr_add_ref(Refcount * x) + { + /* for adding reference -- can use relaxed order, + * since any reordering of a set of intrusive_ptr_add_ref() + * calls is ok, provided no intervening intrusive_ptr_release() + * calls. + */ + bool success = (x == nullptr); + + while(!success) { + uint32_t n = x->reference_counter_.load(std::memory_order_relaxed); + + success = x->reference_counter_.compare_exchange_strong(n, n+1, + std::memory_order_relaxed); + } + } /*intrusive_ptr_add_ref*/ + + void + intrusive_ptr_release(Refcount * x) + { + using xo::scope; + using xo::xtag; + + scope log(XO_ENTER0(verbose), + "enter", + xtag("x", x), + xtag("n", x ? x->reference_counter_.load() : 0)); + + /* for decrement, need acq_rel ordering */ + bool success = (x == nullptr); + uint32_t n = 0; + + while(!success) { + n = x->reference_counter_.load(std::memory_order_acquire); + + if(n == static_cast(-1)) { + log && log("detected double-delete attempt", + xtag("x", x), + xtag("n", n)); + assert(false); + } + + success = x->reference_counter_.compare_exchange_strong(n, n-1, + std::memory_order_acq_rel); + } + + if(n == 1) { + /* just deleted the last reference, so recover the object */ + + log && log("delete object with 0 refs"); + + /* for good measure: replace refcount with -1, + * in hope of detecting a double-delete attempt + */ + x->reference_counter_.store(static_cast(-1)); + + delete x; + } + } /*intrusive_ptr_release*/ + + } /*namespace ref*/ +} /*namespace xo*/ + +/* end Refcounted.cpp */ diff --git a/xo-refcnt/utest/CMakeLists.txt b/xo-refcnt/utest/CMakeLists.txt new file mode 100644 index 00000000..2e65b14b --- /dev/null +++ b/xo-refcnt/utest/CMakeLists.txt @@ -0,0 +1,15 @@ +# build unittest 'refcnt/utest/utest.refcnt + +set(SELF_EXECUTABLE_NAME utest.refcnt) +# These tests can use the Catch2-provided main +set(SELF_SOURCE_FILES intrusive_ptr.test.cpp refcnt_utest_main.cpp) + +xo_add_utest_executable(${SELF_EXECUTABLE_NAME} ${SELF_SOURCE_FILES}) + +# ---------------------------------------------------------------- +# 3rd party dependency: catch2: + +xo_self_dependency(${SELF_EXECUTABLE_NAME} refcnt) +xo_external_target_dependency(${SELF_EXECUTABLE_NAME} Catch2 Catch2::Catch2) + +# end CMakeLists.txt diff --git a/xo-refcnt/utest/README b/xo-refcnt/utest/README new file mode 100644 index 00000000..85cc27c2 --- /dev/null +++ b/xo-refcnt/utest/README @@ -0,0 +1,7 @@ +* to run unit tests for this directoyr + + $ cd path/to/kalman/build + $ ./refcnt/utest/utest.refcnt + + + \ No newline at end of file diff --git a/xo-refcnt/utest/intrusive_ptr.test.cpp b/xo-refcnt/utest/intrusive_ptr.test.cpp new file mode 100644 index 00000000..78e3181a --- /dev/null +++ b/xo-refcnt/utest/intrusive_ptr.test.cpp @@ -0,0 +1,300 @@ +/* @file intrusive_ptr.test.cpp */ + +#include "Refcounted.hpp" +#include "xo/indentlog/scope.hpp" +#include +#include +#include + +namespace xo { + using xo::ref::Refcount; + using xo::ref::Borrow; + using xo::ref::brw; + using xo::ref::intrusive_ptr_refcount; + using xo::ref::intrusive_ptr_add_ref; + using xo::ref::intrusive_ptr_release; + + namespace ut { + namespace { + static uint32_t ctor_count = 0; + static uint32_t dtor_count = 0; + + /* empty object, except for refcount */ + class JustRefcount : public ref::Refcount { + public: + JustRefcount() { ++ctor_count; } + ~JustRefcount() { ++dtor_count; } + }; /*JustRefcount*/ + + inline std::ostream & operator<<(std::ostream & os, JustRefcount & x) { + os << "JustRefcount"; + return os; + } /*operator<<*/ + } /*namespace*/ + + TEST_CASE("refcount", "[refcnt][trivial]") { + REQUIRE(std::is_default_constructible() == true); + REQUIRE(std::has_virtual_destructor() == true); + + /* refcount object self-initializes to 0 */ + Refcount x; + REQUIRE(x.reference_counter() == 0); + } /*TEST_CASE(refcount)*/ + + TEST_CASE("null-intrusive-ptr", "[refcnt][trivial]") { + //constexpr std::string_view c_self = "TEST_CASE:null-intrusive-ptr"; + + REQUIRE(std::has_virtual_destructor() == true); + + rp p1; + rp p2; + + REQUIRE(sizeof(p1) == sizeof(JustRefcount*)); + + REQUIRE(p1.get() == nullptr); + REQUIRE(p1.operator->() == nullptr); + + REQUIRE(p2.get() == nullptr); + REQUIRE(p2.operator->() == nullptr); + + /* can assign a nullptr */ + rp p3; + + REQUIRE(p3.get() == nullptr); + p3 = p1; + REQUIRE(p3.get() == nullptr); + + /* can use aux functions on null pointers */ + REQUIRE(intrusive_ptr_refcount(p1.get()) == 0); + + intrusive_ptr_add_ref(nullptr); + intrusive_ptr_release(nullptr); + + /* can borrow a null intrusive_ptr */ + brw p1_brw = p1.borrow(); + brw p2_brw = p2.borrow(); + + REQUIRE(p1_brw.get() == nullptr); + REQUIRE(p1_brw.operator->() == nullptr); + /* null borrow is false-y */ + REQUIRE(p1_brw == false); + + /* can promote a borrowed pointer */ + rp pp = p1_brw.promote(); + + REQUIRE(p1.get() == pp.get()); + + /* comparisons */ + REQUIRE(Borrow::compare(p1_brw, p2_brw) == 0); + REQUIRE(p1_brw == p2_brw); + REQUIRE((p1_brw != p2_brw) == false); + REQUIRE(p1 == p1_brw); + REQUIRE((p1 != p1_brw) == false); + REQUIRE(p1_brw == p1); + REQUIRE((p1_brw != p1) == false); + } /*TEST_CASE(null-intrusive_ptr)*/ + + TEST_CASE("intrusive-ptr-identity", "[refcnt][identity]") + { + uint32_t cc = ctor_count; + uint32_t dc = dtor_count; + + rp p1(new JustRefcount()); + + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + REQUIRE(p1.get() != nullptr); + REQUIRE(p1.get() == p1.operator->()); + REQUIRE(intrusive_ptr_refcount(p1.get()) == 1); + REQUIRE(p1->reference_counter() == 1); + + intrusive_ptr_add_ref(p1.get()); + + REQUIRE(intrusive_ptr_refcount(p1.get()) == 2); + + intrusive_ptr_release(p1.get()); + + REQUIRE(intrusive_ptr_refcount(p1.get()) == 1); + + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + + rp p2(new JustRefcount()); + + REQUIRE(ctor_count == cc + 2); + REQUIRE(dtor_count == dc); + + REQUIRE(p2.get() != nullptr); + REQUIRE(p2.get() != p1.get()); + REQUIRE(p2.get() == p2.operator->()); + REQUIRE(p2->reference_counter() == 1); + + /* can borrow a non-null intrusive-ptr */ + brw p1_brw = p1.borrow(); + + REQUIRE(p1_brw.get() == p1.get()); + + /* borrowing does not change refcount, borrow not tracked */ + REQUIRE(ctor_count == cc + 2); + REQUIRE(dtor_count == dc); + REQUIRE(p1.get()->reference_counter() == 1); + + /* copying borrowed pointer does not touch refcount */ + brw p1_brw2 = p1_brw; + + REQUIRE(ctor_count == cc + 2); + REQUIRE(dtor_count == dc); + REQUIRE(p1_brw2.get() == p1.get()); + + REQUIRE(p1.get()->reference_counter() == 1); + } /*TEST_CASE(identity-intrusive-ptr)*/ + + TEST_CASE("intrusive-ptr-release", "[refcnt][release]") + { + uint32_t cc = ctor_count; + uint32_t dc = dtor_count; + + rp p1(new JustRefcount()); + + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + REQUIRE(p1.get() != nullptr); + REQUIRE(p1->reference_counter() == 1); + + /* reference count going to 0 -> delete object */ + p1 = nullptr; + + REQUIRE(p1.get() == nullptr); + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc + 1); + } /*TEST_CASE(intrusive-ptr-release)*/ + + TEST_CASE("intrusive-ptr-copy", "[refcnt][copy]") + { + uint32_t cc = ctor_count; + uint32_t dc = dtor_count; + + rp p1(new JustRefcount()); + JustRefcount * p1_native = p1.get(); + + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + REQUIRE(p1.get() != nullptr); + REQUIRE(p1->reference_counter() == 1); + + /* copy ctor ran to make copy of p1, did not allocate */ + rp p2(p1); + + REQUIRE(p1->reference_counter() == 2); + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + + } /*TEST_CASE(intrusive-ptr-copy)*/ + + TEST_CASE("intrusive-ptr-move", "[refcnt][move]") + { + uint32_t cc = ctor_count; + uint32_t dc = dtor_count; + + rp p1(new JustRefcount()); + JustRefcount * p1_native = p1.get(); + + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + REQUIRE(p1.get() != nullptr); + REQUIRE(p1->reference_counter() == 1); + + rp p2{std::move(p1)}; + + REQUIRE(p2->reference_counter() == 1); + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + + p2 = nullptr; + + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc + 1); + } /*TEST_CASE(intrusive-ptr-move)*/ + + TEST_CASE("instrusive-ptr-assign", "[refcnt][assign]") + { + uint32_t cc = ctor_count; + uint32_t dc = dtor_count; + + rp p1(new JustRefcount()); + JustRefcount * p1_native = p1.get(); + + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + REQUIRE(p1.get() != nullptr); + REQUIRE(p1->reference_counter() == 1); + + rp p2; + + REQUIRE(p2.get() == nullptr); + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + + p2 = p1; + + REQUIRE(p2.get() == p1.get()); + REQUIRE(p2->reference_counter() == 2); + + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + + p1 = nullptr; + + REQUIRE(p2->reference_counter() == 1); + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + + p2 = nullptr; + + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc + 1); + } /*TEST_CASE(intrusive-ptr-assign)*/ + + TEST_CASE("intrusive-ptr-move-assign", "[refcnt][move-assign]") + { + uint32_t cc = ctor_count; + uint32_t dc = dtor_count; + + rp p1(new JustRefcount()); + JustRefcount * p1_native = p1.get(); + + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + REQUIRE(p1.get() != nullptr); + REQUIRE(p1->reference_counter() == 1); + + rp p2; + + REQUIRE(p2.get() == nullptr); + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + + p2 = std::move(p1); + + REQUIRE(p1.get() == nullptr); + REQUIRE(p2.get() == p1_native); + REQUIRE(p2->reference_counter() == 1); + + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + + p1 = nullptr; /*no-op*/ + + REQUIRE(p2->reference_counter() == 1); + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc); + + p2 = nullptr; + + REQUIRE(ctor_count == cc + 1); + REQUIRE(dtor_count == dc + 1); + } /*TEST_CASE(intrusive-ptr-move-assign)*/ + } /*namespace ut*/ +} /*namespace xo*/ + +/* end intrusive_ptr.test.cpp */ diff --git a/xo-refcnt/utest/refcnt_utest_main.cpp b/xo-refcnt/utest/refcnt_utest_main.cpp new file mode 100644 index 00000000..327713b7 --- /dev/null +++ b/xo-refcnt/utest/refcnt_utest_main.cpp @@ -0,0 +1,6 @@ +/* @file refcnt_utest_main.cpp */ + +#define CATCH_CONFIG_MAIN +#include "catch2/catch.hpp" + +/* end refcnt_utest_main.cpp */