From 87b4bfa7953f254e716611f8a30e4679a39bfd54 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 23 Sep 2023 15:48:36 -0400 Subject: [PATCH] + implementation --- CMakeLists.txt | 52 +++ cmake/code-coverage.cmake | 678 +++++++++++++++++++++++++++++++++ cmake/cxx.cmake | 86 +++++ include/cxxutil/demangle.hpp | 92 +++++ include/refcnt/Displayable.hpp | 29 ++ include/refcnt/Refcounted.hpp | 294 ++++++++++++++ include/refcnt/Unowned.hpp | 28 ++ src/CMakeLists.txt | 14 + src/Displayable.cpp | 16 + src/Refcounted.cpp | 157 ++++++++ 10 files changed, 1446 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 cmake/code-coverage.cmake create mode 100644 cmake/cxx.cmake create mode 100644 include/cxxutil/demangle.hpp create mode 100644 include/refcnt/Displayable.hpp create mode 100644 include/refcnt/Refcounted.hpp create mode 100644 include/refcnt/Unowned.hpp create mode 100644 src/CMakeLists.txt create mode 100644 src/Displayable.cpp create mode 100644 src/Refcounted.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..1e8053f9 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,52 @@ +# refcnt/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project(xo-refcnt VERSION 0.1) +enable_language(CXX) + +include(cmake/cxx.cmake) +include(cmake/code-coverage.cmake) + +# ---------------------------------------------------------------- +# 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. +# +add_code_coverage_all_targets(EXCLUDE /nix/store/* utest/*) + +# ---------------------------------------------------------------- +# c++ settings + +set(PROJECT_CXX_FLAGS "-fconcepts-diagnostics-depth=2") + +add_definitions(${PROJECT_CXX_FLAGS}) + +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 20) +endif() + +set(CMAKE_CXX_STANDARD_REQUIRED True) + +# always write compile_commands.json +set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "") + +# ---------------------------------------------------------------- +# sources + +add_subdirectory(src) + +# ---------------------------------------------------------------- +# install .hpp + +install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/ DESTINATION include) + +# end CMakeLists.txt diff --git a/cmake/code-coverage.cmake b/cmake/code-coverage.cmake new file mode 100644 index 00000000..b6b36064 --- /dev/null +++ b/cmake/code-coverage.cmake @@ -0,0 +1,678 @@ +# +# Copyright (C) 2018-2020 by George Cave - gcave@stablecoder.ca +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# USAGE: To enable any code coverage instrumentation/targets, the single CMake +# option of `CODE_COVERAGE` needs to be set to 'ON', either by GUI, ccmake, or +# on the command line. +# +# From this point, there are two primary methods for adding instrumentation to +# targets: +# +# 1 - A blanket instrumentation by calling `add_code_coverage()`, where +# all targets in that directory and all subdirectories are automatically +# instrumented. +# +# 2 - Per-target instrumentation by calling +# `target_code_coverage()`, where the target is given and thus only +# that target is instrumented. This applies to both libraries and executables. +# +# To add coverage targets, such as calling `make ccov` to generate the actual +# coverage information for perusal or consumption, call +# `target_code_coverage()` on an *executable* target. +# +# Example 1: All targets instrumented +# +# In this case, the coverage information reported will will be that of the +# `theLib` library target and `theExe` executable. +# +# 1a: Via global command +# +# ~~~ +# add_code_coverage() # Adds instrumentation to all targets +# +# add_library(theLib lib.cpp) +# +# add_executable(theExe main.cpp) +# target_link_libraries(theExe PRIVATE theLib) +# target_code_coverage(theExe) # As an executable target, adds the 'ccov-theExe' target +# # (instrumentation already added via global anyways) +# # for generating code coverage reports. +# ~~~ +# +# 1b: Via target commands +# +# ~~~ +# add_library(theLib lib.cpp) +# target_code_coverage(theLib) # As a library target, adds coverage instrumentation but no targets. +# +# add_executable(theExe main.cpp) +# target_link_libraries(theExe PRIVATE theLib) +# target_code_coverage(theExe) # As an executable target, adds the 'ccov-theExe' target and instrumentation for generating code coverage reports. +# ~~~ +# +# Example 2: Target instrumented, but with regex pattern of files to be excluded +# from report +# +# ~~~ +# add_executable(theExe main.cpp non_covered.cpp) +# target_code_coverage(theExe EXCLUDE non_covered.cpp test/*) # As an executable target, the reports will exclude the non-covered.cpp file, and any files in a test/ folder. +# ~~~ +# +# Example 3: Target added to the 'ccov' and 'ccov-all' targets +# +# ~~~ +# add_code_coverage_all_targets(EXCLUDE test/*) # Adds the 'ccov-all' target set and sets it to exclude all files in test/ folders. +# +# add_executable(theExe main.cpp non_covered.cpp) +# target_code_coverage(theExe AUTO ALL EXCLUDE non_covered.cpp test/*) # As an executable target, adds to the 'ccov' and ccov-all' targets, and the reports will exclude the non-covered.cpp file, and any files in a test/ folder. +# ~~~ + +# Options +option( + CODE_COVERAGE + "Builds targets with code coverage instrumentation. (Requires GCC or Clang)" + OFF) + +# Programs +find_program(LLVM_COV_PATH llvm-cov) +find_program(LLVM_PROFDATA_PATH llvm-profdata) +find_program(LCOV_PATH lcov) +find_program(GENHTML_PATH genhtml) +# Hide behind the 'advanced' mode flag for GUI/ccmake +mark_as_advanced(FORCE LLVM_COV_PATH LLVM_PROFDATA_PATH LCOV_PATH GENHTML_PATH) + +# Variables +set(CMAKE_COVERAGE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/ccov) +set_property(GLOBAL PROPERTY JOB_POOLS ccov_serial_pool=1) + +# Common initialization/checks +if(CODE_COVERAGE AND NOT CODE_COVERAGE_ADDED) + set(CODE_COVERAGE_ADDED ON) + + # Common Targets + add_custom_target( + ccov-preprocessing + COMMAND ${CMAKE_COMMAND} -E make_directory + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY} + DEPENDS ccov-clean) + + if(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang" + OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang") + # Messages + message(STATUS "Building with llvm Code Coverage Tools") + + if(NOT LLVM_COV_PATH) + message(FATAL_ERROR "llvm-cov not found! Aborting.") + else() + # Version number checking for 'EXCLUDE' compatibility + execute_process(COMMAND ${LLVM_COV_PATH} --version + OUTPUT_VARIABLE LLVM_COV_VERSION_CALL_OUTPUT) + string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" LLVM_COV_VERSION + ${LLVM_COV_VERSION_CALL_OUTPUT}) + + if(LLVM_COV_VERSION VERSION_LESS "7.0.0") + message( + WARNING + "target_code_coverage()/add_code_coverage_all_targets() 'EXCLUDE' option only available on llvm-cov >= 7.0.0" + ) + endif() + endif() + + # Targets + if(${CMAKE_VERSION} VERSION_LESS "3.17.0") + add_custom_target( + ccov-clean + COMMAND ${CMAKE_COMMAND} -E remove -f + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list + COMMAND ${CMAKE_COMMAND} -E remove -f + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/profraw.list) + else() + add_custom_target( + ccov-clean + COMMAND ${CMAKE_COMMAND} -E rm -f + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list + COMMAND ${CMAKE_COMMAND} -E rm -f + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/profraw.list) + endif() + + # Used to get the shared object file list before doing the main all- + # processing + add_custom_target( + ccov-libs + COMMAND ; + COMMENT "libs ready for coverage report.") + + elseif(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES + "GNU") + # Messages + message(STATUS "Building with lcov Code Coverage Tools") + + if(CMAKE_BUILD_TYPE) + string(TOUPPER ${CMAKE_BUILD_TYPE} upper_build_type) + if(NOT ${upper_build_type} STREQUAL "DEBUG") + message( + WARNING + "Code coverage results with an optimized (non-Debug) build may be misleading" + ) + endif() + else() + message( + WARNING + "Code coverage results with an optimized (non-Debug) build may be misleading" + ) + endif() + if(NOT LCOV_PATH) + message(FATAL_ERROR "lcov not found! Aborting...") + endif() + if(NOT GENHTML_PATH) + message(FATAL_ERROR "genhtml not found! Aborting...") + endif() + + # Targets + add_custom_target(ccov-clean COMMAND ${LCOV_PATH} --directory + ${CMAKE_BINARY_DIR} --zerocounters) + + else() + message(FATAL_ERROR "Code coverage requires Clang or GCC. Aborting.") + endif() +endif() + +# Adds code coverage instrumentation to a library, or instrumentation/targets +# for an executable target. +# ~~~ +# EXECUTABLE ADDED TARGETS: +# GCOV/LCOV: +# ccov : Generates HTML code coverage report for every target added with 'AUTO' parameter. +# ccov-${TARGET_NAME} : Generates HTML code coverage report for the associated named target. +# ccov-all : Generates HTML code coverage report, merging every target added with 'ALL' parameter into a single detailed report. +# +# LLVM-COV: +# ccov : Generates HTML code coverage report for every target added with 'AUTO' parameter. +# ccov-report : Generates HTML code coverage report for every target added with 'AUTO' parameter. +# ccov-${TARGET_NAME} : Generates HTML code coverage report. +# ccov-report-${TARGET_NAME} : Prints to command line summary per-file coverage information. +# ccov-export-${TARGET_NAME} : Exports the coverage report to a JSON file. +# ccov-show-${TARGET_NAME} : Prints to command line detailed per-line coverage information. +# ccov-all : Generates HTML code coverage report, merging every target added with 'ALL' parameter into a single detailed report. +# ccov-all-report : Prints summary per-file coverage information for every target added with ALL' parameter to the command line. +# ccov-all-export : Exports the coverage report to a JSON file. +# +# Required: +# TARGET_NAME - Name of the target to generate code coverage for. +# Optional: +# PUBLIC - Sets the visibility for added compile options to targets to PUBLIC instead of the default of PRIVATE. +# INTERFACE - Sets the visibility for added compile options to targets to INTERFACE instead of the default of PRIVATE. +# PLAIN - Do not set any target visibility (backward compatibility with old cmake projects) +# AUTO - Adds the target to the 'ccov' target so that it can be run in a batch with others easily. Effective on executable targets. +# ALL - Adds the target to the 'ccov-all' and 'ccov-all-report' targets, which merge several executable targets coverage data to a single report. Effective on executable targets. +# EXTERNAL - For GCC's lcov, allows the profiling of 'external' files from the processing directory +# COVERAGE_TARGET_NAME - For executables ONLY, changes the outgoing target name so instead of `ccov-${TARGET_NAME}` it becomes `ccov-${COVERAGE_TARGET_NAME}`. +# EXCLUDE - Excludes files of the patterns provided from coverage. Note that GCC/lcov excludes by glob pattern, and clang/LLVM excludes via regex! **These do not copy to the 'all' targets.** +# OBJECTS - For executables ONLY, if the provided targets are shared libraries, adds coverage information to the output +# ARGS - For executables ONLY, appends the given arguments to the associated ccov-* executable call +# ~~~ +function(target_code_coverage TARGET_NAME) + # Argument parsing + set(options AUTO ALL EXTERNAL PUBLIC INTERFACE PLAIN) + set(single_value_keywords COVERAGE_TARGET_NAME) + set(multi_value_keywords EXCLUDE OBJECTS ARGS) + cmake_parse_arguments( + target_code_coverage "${options}" "${single_value_keywords}" + "${multi_value_keywords}" ${ARGN}) + + # Set the visibility of target functions to PUBLIC, INTERFACE or default to + # PRIVATE. + if(target_code_coverage_PUBLIC) + set(TARGET_VISIBILITY PUBLIC) + set(TARGET_LINK_VISIBILITY PUBLIC) + elseif(target_code_coverage_INTERFACE) + set(TARGET_VISIBILITY INTERFACE) + set(TARGET_LINK_VISIBILITY INTERFACE) + elseif(target_code_coverage_PLAIN) + set(TARGET_VISIBILITY PUBLIC) + set(TARGET_LINK_VISIBILITY) + else() + set(TARGET_VISIBILITY PRIVATE) + set(TARGET_LINK_VISIBILITY PRIVATE) + endif() + + if(NOT target_code_coverage_COVERAGE_TARGET_NAME) + # If a specific name was given, use that instead. + set(target_code_coverage_COVERAGE_TARGET_NAME ${TARGET_NAME}) + endif() + + if(CODE_COVERAGE) + + # Add code coverage instrumentation to the target's linker command + if(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang" + OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang") + target_compile_options(${TARGET_NAME} ${TARGET_VISIBILITY} + -fprofile-instr-generate -fcoverage-mapping) + target_link_options(${TARGET_NAME} ${TARGET_VISIBILITY} + -fprofile-instr-generate -fcoverage-mapping) + elseif(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES + "GNU") + target_compile_options(${TARGET_NAME} ${TARGET_VISIBILITY} -fprofile-arcs + -ftest-coverage) + target_link_libraries(${TARGET_NAME} ${TARGET_LINK_VISIBILITY} gcov) + endif() + + # Targets + get_target_property(target_type ${TARGET_NAME} TYPE) + + # Add shared library to processing for 'all' targets + if(target_type STREQUAL "SHARED_LIBRARY" AND target_code_coverage_ALL) + if(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang" + OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang") + add_custom_target( + ccov-run-${target_code_coverage_COVERAGE_TARGET_NAME} + COMMAND + ${CMAKE_COMMAND} -E echo "-object=$" >> + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list + DEPENDS ccov-preprocessing ${TARGET_NAME}) + + if(NOT TARGET ccov-libs) + message( + FATAL_ERROR + "Calling target_code_coverage with 'ALL' must be after a call to 'add_code_coverage_all_targets'." + ) + endif() + + add_dependencies(ccov-libs + ccov-run-${target_code_coverage_COVERAGE_TARGET_NAME}) + endif() + endif() + + # For executables add targets to run and produce output + if(target_type STREQUAL "EXECUTABLE") + if(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang" + OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang") + + # If there are shared objects to also work with, generate the string to + # add them here + foreach(SO_TARGET ${target_code_coverage_OBJECTS}) + # Check to see if the target is a shared object + if(TARGET ${SO_TARGET}) + get_target_property(SO_TARGET_TYPE ${SO_TARGET} TYPE) + if(${SO_TARGET_TYPE} STREQUAL "SHARED_LIBRARY") + set(SO_OBJECTS ${SO_OBJECTS} -object=$) + endif() + endif() + endforeach() + + # Run the executable, generating raw profile data Make the run data + # available for further processing. Separated to allow Windows to run + # this target serially. + add_custom_target( + ccov-run-${target_code_coverage_COVERAGE_TARGET_NAME} + COMMAND + ${CMAKE_COMMAND} -E env + LLVM_PROFILE_FILE=${target_code_coverage_COVERAGE_TARGET_NAME}.profraw + $ ${target_code_coverage_ARGS} + COMMAND + ${CMAKE_COMMAND} -E echo "-object=$" + ${SO_OBJECTS} >> ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list + COMMAND + ${CMAKE_COMMAND} -E echo + "${CMAKE_CURRENT_BINARY_DIR}/${target_code_coverage_COVERAGE_TARGET_NAME}.profraw" + >> ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/profraw.list + JOB_POOL ccov_serial_pool + DEPENDS ccov-preprocessing ccov-libs ${TARGET_NAME}) + + # Merge the generated profile data so llvm-cov can process it + add_custom_target( + ccov-processing-${target_code_coverage_COVERAGE_TARGET_NAME} + COMMAND + ${LLVM_PROFDATA_PATH} merge -sparse + ${target_code_coverage_COVERAGE_TARGET_NAME}.profraw -o + ${target_code_coverage_COVERAGE_TARGET_NAME}.profdata + DEPENDS ccov-run-${target_code_coverage_COVERAGE_TARGET_NAME}) + + # Ignore regex only works on LLVM >= 7 + if(LLVM_COV_VERSION VERSION_GREATER_EQUAL "7.0.0") + foreach(EXCLUDE_ITEM ${target_code_coverage_EXCLUDE}) + set(EXCLUDE_REGEX ${EXCLUDE_REGEX} + -ignore-filename-regex='${EXCLUDE_ITEM}') + endforeach() + endif() + + # Print out details of the coverage information to the command line + add_custom_target( + ccov-show-${target_code_coverage_COVERAGE_TARGET_NAME} + COMMAND + ${LLVM_COV_PATH} show $ ${SO_OBJECTS} + -instr-profile=${target_code_coverage_COVERAGE_TARGET_NAME}.profdata + -show-line-counts-or-regions ${EXCLUDE_REGEX} + DEPENDS ccov-processing-${target_code_coverage_COVERAGE_TARGET_NAME}) + + # Print out a summary of the coverage information to the command line + add_custom_target( + ccov-report-${target_code_coverage_COVERAGE_TARGET_NAME} + COMMAND + ${LLVM_COV_PATH} report $ ${SO_OBJECTS} + -instr-profile=${target_code_coverage_COVERAGE_TARGET_NAME}.profdata + ${EXCLUDE_REGEX} + DEPENDS ccov-processing-${target_code_coverage_COVERAGE_TARGET_NAME}) + + # Export coverage information so continuous integration tools (e.g. + # Jenkins) can consume it + add_custom_target( + ccov-export-${target_code_coverage_COVERAGE_TARGET_NAME} + COMMAND + ${LLVM_COV_PATH} export $ ${SO_OBJECTS} + -instr-profile=${target_code_coverage_COVERAGE_TARGET_NAME}.profdata + -format="text" ${EXCLUDE_REGEX} > + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/${target_code_coverage_COVERAGE_TARGET_NAME}.json + DEPENDS ccov-processing-${target_code_coverage_COVERAGE_TARGET_NAME}) + + # Generates HTML output of the coverage information for perusal + add_custom_target( + ccov-${target_code_coverage_COVERAGE_TARGET_NAME} + COMMAND + ${LLVM_COV_PATH} show $ ${SO_OBJECTS} + -instr-profile=${target_code_coverage_COVERAGE_TARGET_NAME}.profdata + -show-line-counts-or-regions + -output-dir=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/${target_code_coverage_COVERAGE_TARGET_NAME} + -format="html" ${EXCLUDE_REGEX} + DEPENDS ccov-processing-${target_code_coverage_COVERAGE_TARGET_NAME}) + + elseif(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES + "GNU") + set(COVERAGE_INFO + "${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/${target_code_coverage_COVERAGE_TARGET_NAME}.info" + ) + + # Run the executable, generating coverage information + add_custom_target( + ccov-run-${target_code_coverage_COVERAGE_TARGET_NAME} + COMMAND $ ${target_code_coverage_ARGS} + DEPENDS ccov-preprocessing ${TARGET_NAME}) + + # Generate exclusion string for use + foreach(EXCLUDE_ITEM ${target_code_coverage_EXCLUDE}) + set(EXCLUDE_REGEX ${EXCLUDE_REGEX} --remove ${COVERAGE_INFO} + '${EXCLUDE_ITEM}') + endforeach() + + if(EXCLUDE_REGEX) + set(EXCLUDE_COMMAND ${LCOV_PATH} ${EXCLUDE_REGEX} --output-file + ${COVERAGE_INFO}) + else() + set(EXCLUDE_COMMAND ;) + endif() + + if(NOT ${target_code_coverage_EXTERNAL}) + set(EXTERNAL_OPTION --no-external) + endif() + + # Capture coverage data + if(${CMAKE_VERSION} VERSION_LESS "3.17.0") + add_custom_target( + ccov-capture-${target_code_coverage_COVERAGE_TARGET_NAME} + COMMAND ${CMAKE_COMMAND} -E remove -f ${COVERAGE_INFO} + COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --zerocounters + COMMAND $ ${target_code_coverage_ARGS} + COMMAND + ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --base-directory + ${CMAKE_SOURCE_DIR} --capture ${EXTERNAL_OPTION} --output-file + ${COVERAGE_INFO} + COMMAND ${EXCLUDE_COMMAND} + DEPENDS ccov-preprocessing ${TARGET_NAME}) + else() + add_custom_target( + ccov-capture-${target_code_coverage_COVERAGE_TARGET_NAME} + COMMAND ${CMAKE_COMMAND} -E rm -f ${COVERAGE_INFO} + COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --zerocounters + COMMAND $ ${target_code_coverage_ARGS} + COMMAND + ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --base-directory + ${CMAKE_SOURCE_DIR} --capture ${EXTERNAL_OPTION} --output-file + ${COVERAGE_INFO} + COMMAND ${EXCLUDE_COMMAND} + DEPENDS ccov-preprocessing ${TARGET_NAME}) + endif() + + # Generates HTML output of the coverage information for perusal + add_custom_target( + ccov-${target_code_coverage_COVERAGE_TARGET_NAME} + COMMAND + ${GENHTML_PATH} -o + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/${target_code_coverage_COVERAGE_TARGET_NAME} + ${COVERAGE_INFO} + DEPENDS ccov-capture-${target_code_coverage_COVERAGE_TARGET_NAME}) + endif() + + add_custom_command( + TARGET ccov-${target_code_coverage_COVERAGE_TARGET_NAME} + POST_BUILD + COMMAND ; + COMMENT + "Open ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/${target_code_coverage_COVERAGE_TARGET_NAME}/index.html in your browser to view the coverage report." + ) + + # AUTO + if(target_code_coverage_AUTO) + if(NOT TARGET ccov) + add_custom_target(ccov) + endif() + add_dependencies(ccov ccov-${target_code_coverage_COVERAGE_TARGET_NAME}) + + if(NOT CMAKE_C_COMPILER_ID MATCHES "GNU" AND NOT CMAKE_CXX_COMPILER_ID + MATCHES "GNU") + if(NOT TARGET ccov-report) + add_custom_target(ccov-report) + endif() + add_dependencies( + ccov-report + ccov-report-${target_code_coverage_COVERAGE_TARGET_NAME}) + endif() + endif() + + # ALL + if(target_code_coverage_ALL) + if(NOT TARGET ccov-all-processing) + message( + FATAL_ERROR + "Calling target_code_coverage with 'ALL' must be after a call to 'add_code_coverage_all_targets'." + ) + endif() + + add_dependencies(ccov-all-processing + ccov-run-${target_code_coverage_COVERAGE_TARGET_NAME}) + endif() + endif() + endif() +endfunction() + +# Adds code coverage instrumentation to all targets in the current directory and +# any subdirectories. To add coverage instrumentation to only specific targets, +# use `target_code_coverage`. +function(add_code_coverage) + if(CODE_COVERAGE) + if(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang" + OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang") + add_compile_options(-fprofile-instr-generate -fcoverage-mapping) + add_link_options(-fprofile-instr-generate -fcoverage-mapping) + elseif(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES + "GNU") + add_compile_options(-fprofile-arcs -ftest-coverage) + link_libraries(gcov) + endif() + endif() +endfunction() + +# Adds the 'ccov-all' type targets that calls all targets added via +# `target_code_coverage` with the `ALL` parameter, but merges all the coverage +# data from them into a single large report instead of the numerous smaller +# reports. Also adds the ccov-all-capture Generates an all-merged.info file, for +# use with coverage dashboards (e.g. codecov.io, coveralls). +# ~~~ +# Optional: +# EXCLUDE - Excludes files of the patterns provided from coverage. Note that GCC/lcov excludes by glob pattern, and clang/LLVM excludes via regex! +# ~~~ +function(add_code_coverage_all_targets) + # Argument parsing + set(multi_value_keywords EXCLUDE) + cmake_parse_arguments(add_code_coverage_all_targets "" "" + "${multi_value_keywords}" ${ARGN}) + + if(CODE_COVERAGE) + if(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang" + OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang") + + # Merge the profile data for all of the run executables + if(WIN32) + add_custom_target( + ccov-all-processing + COMMAND + powershell -Command $$FILELIST = Get-Content + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/profraw.list\; llvm-profdata.exe + merge -o ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata + -sparse $$FILELIST) + else() + add_custom_target( + ccov-all-processing + COMMAND + ${LLVM_PROFDATA_PATH} merge -o + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata -sparse `cat + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/profraw.list`) + endif() + + # Regex exclude only available for LLVM >= 7 + if(LLVM_COV_VERSION VERSION_GREATER_EQUAL "7.0.0") + foreach(EXCLUDE_ITEM ${add_code_coverage_all_targets_EXCLUDE}) + set(EXCLUDE_REGEX ${EXCLUDE_REGEX} + -ignore-filename-regex='${EXCLUDE_ITEM}') + endforeach() + endif() + + # Print summary of the code coverage information to the command line + if(WIN32) + add_custom_target( + ccov-all-report + COMMAND + powershell -Command $$FILELIST = Get-Content + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list\; llvm-cov.exe + report $$FILELIST + -instr-profile=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata + ${EXCLUDE_REGEX} + DEPENDS ccov-all-processing) + else() + add_custom_target( + ccov-all-report + COMMAND + ${LLVM_COV_PATH} report `cat + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list` + -instr-profile=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata + ${EXCLUDE_REGEX} + DEPENDS ccov-all-processing) + endif() + + # Export coverage information so continuous integration tools (e.g. + # Jenkins) can consume it + add_custom_target( + ccov-all-export + COMMAND + ${LLVM_COV_PATH} export `cat + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list` + -instr-profile=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata + -format="text" ${EXCLUDE_REGEX} > + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/coverage.json + DEPENDS ccov-all-processing) + + # Generate HTML output of all added targets for perusal + if(WIN32) + add_custom_target( + ccov-all + COMMAND + powershell -Command $$FILELIST = Get-Content + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list\; llvm-cov.exe show + $$FILELIST + -instr-profile=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata + -show-line-counts-or-regions + -output-dir=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged + -format="html" ${EXCLUDE_REGEX} + DEPENDS ccov-all-processing) + else() + add_custom_target( + ccov-all + COMMAND + ${LLVM_COV_PATH} show `cat + ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list` + -instr-profile=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata + -show-line-counts-or-regions + -output-dir=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged + -format="html" ${EXCLUDE_REGEX} + DEPENDS ccov-all-processing) + endif() + + elseif(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES + "GNU") + set(COVERAGE_INFO "${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.info") + + # Nothing required for gcov + add_custom_target(ccov-all-processing COMMAND ;) + + # Exclusion regex string creation + set(EXCLUDE_REGEX) + foreach(EXCLUDE_ITEM ${add_code_coverage_all_targets_EXCLUDE}) + set(EXCLUDE_REGEX ${EXCLUDE_REGEX} --remove ${COVERAGE_INFO} + '${EXCLUDE_ITEM}') + endforeach() + + if(EXCLUDE_REGEX) + set(EXCLUDE_COMMAND ${LCOV_PATH} ${EXCLUDE_REGEX} --output-file + ${COVERAGE_INFO}) + else() + set(EXCLUDE_COMMAND ;) + endif() + + # Capture coverage data + if(${CMAKE_VERSION} VERSION_LESS "3.17.0") + add_custom_target( + ccov-all-capture + COMMAND ${CMAKE_COMMAND} -E remove -f ${COVERAGE_INFO} + COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --capture + --output-file ${COVERAGE_INFO} + COMMAND ${EXCLUDE_COMMAND} + DEPENDS ccov-preprocessing ccov-all-processing) + else() + add_custom_target( + ccov-all-capture + COMMAND ${CMAKE_COMMAND} -E rm -f ${COVERAGE_INFO} + COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --capture + --output-file ${COVERAGE_INFO} + COMMAND ${EXCLUDE_COMMAND} + DEPENDS ccov-preprocessing ccov-all-processing) + endif() + + # Generates HTML output of all targets for perusal + add_custom_target( + ccov-all + COMMAND ${GENHTML_PATH} -o ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged + ${COVERAGE_INFO} -p ${CMAKE_SOURCE_DIR} + DEPENDS ccov-all-capture) + + endif() + + add_custom_command( + TARGET ccov-all + POST_BUILD + COMMAND ; + COMMENT + "Open ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged/index.html in your browser to view the coverage report." + ) + endif() +endfunction() diff --git a/cmake/cxx.cmake b/cmake/cxx.cmake new file mode 100644 index 00000000..9fc84e60 --- /dev/null +++ b/cmake/cxx.cmake @@ -0,0 +1,86 @@ +# ---------------------------------------------------------------- +# use this in subdirs that compile c++ code +# +macro(xo_include_options target) + # ---------------------------------------------------------------- + # PROJECT_SOURCE_DIR: + # so we can for example write + # #include "ordinaltree/foo.hpp" + # from anywhere in the project + # PROJECT_BINARY_DIR: + # since generated version file will be in build directory, + # need that build directory to also appear in + # compiler's include path + # + target_include_directories( + ${target} PUBLIC + ${PROJECT_SOURCE_DIR}/include # e.g. for #include "indentlog/scope.hpp" + ${PROJECT_SOURCE_DIR}/include/${target} # e.g. for #include "Refcounted.hpp" in refcnt/src + ${PROJECT_BINARY_DIR} # e.g. for generated config.hpp file + ) + + # ---------------------------------------------------------------- + # make standard directories for std:: includes explicit + # so that + # (1) they appear in compile_commands.json. + # (2) clangd (run from emacs lsp-mode) can find them + # + if(CMAKE_EXPORT_COMPILE_COMMANDS) + set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES}) + endif() +endmacro() + +# ---------------------------------------------------------------- +# variable +# XO_ADDRESS_SANITIZE +# determines whether to enable address sanitizer for the XO project +# (see toplevel CMakeLists.txt) +# ---------------------------------------------------------------- +if(XO_ADDRESS_SANITIZE) + add_compile_options(-fsanitize=address) + add_link_options(-fsanitize=address) +endif() + +# XO_STANDARD_COMPILE_OPTIONS: use these when XO_ADDRESS_SANITIZE=OFF +set(XO_STANDARD_COMPILE_OPTIONS -Werror -Wall -Wextra) + +# XO_ADDRESS_SANITIZE_COMPILE_OPTIONS: use when XO_ADDRESS_SANITIZE=ON +# +# address sanitizer build complains about _FORTIFY_SOURCE redefines +# In file included from :460: +# :1:9: error: '_FORTIFY_SOURCE' macro redefined [-Werror,-Wmacro-redefined] +# #define _FORTIFY_SOURCE 2 +# +set(XO_ADDRESS_SANITIZE_COMPILE_OPTIONS -Werror -Wall -Wextra -Wno-macro-redefined) + +# XO_COMPILE_OPTIONS: use these with xo_compile_options() macro +if(XO_ADDRESS_SANITIZE) + set(XO_COMPILE_OPTIONS ${XO_ADDRESS_SANITIZE_COMPILE_OPTIONS}) +else() + set(XO_COMPILE_OPTIONS ${XO_STANDARD_COMPILE_OPTIONS}) +endif() + +# ---------------------------------------------------------------- +# generally want all the errors+warnings! +# however: address sanitizer generates error on _FORTIFY_SOURCE +# +macro(xo_compile_options target) + target_compile_options(${target} PRIVATE ${XO_COMPILE_OPTIONS}) +endmacro() + +# ---------------------------------------------------------------- +# use this for a subdir that builds a library +# +macro(xo_install_library target) + install(TARGETS ${target} DESTINATION lib) +endmacro() + +# ---------------------------------------------------------------- +# use this when relying on indentlog [[https://github.com/rconybea/indentlog]] headers +# +macro(xo_indentlog_dependency target) + find_package(indentlog REQUIRED) + #add_dependencies(${target} indentlog) + target_link_libraries(${target} PUBLIC indentlog) + #target_include_directories(${target} PUBLIC ${indentlog_DIR}/../../../include) +endmacro() diff --git a/include/cxxutil/demangle.hpp b/include/cxxutil/demangle.hpp new file mode 100644 index 00000000..8b8b8ba3 --- /dev/null +++ b/include/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/include/refcnt/Displayable.hpp b/include/refcnt/Displayable.hpp new file mode 100644 index 00000000..74708573 --- /dev/null +++ b/include/refcnt/Displayable.hpp @@ -0,0 +1,29 @@ +/* @file Displayable.hpp */ + +#pragma once + +#include "refcnt/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/include/refcnt/Refcounted.hpp b/include/refcnt/Refcounted.hpp new file mode 100644 index 00000000..17d778c7 --- /dev/null +++ b/include/refcnt/Refcounted.hpp @@ -0,0 +1,294 @@ +/* @file Refcounted.hpp */ + +#pragma once + +#include "indentlog/scope.hpp" +#include "cxxutil/demangle.hpp" + +//#include +#include +#include + +namespace xo { + 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) { + intrusive_ptr_log_ctor(sc_self_type, this, x); + 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()) { + intrusive_ptr_log_cctor(sc_self_type, this, x.get()); + 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()) { + intrusive_ptr_log_cctor(sc_self_type, this, x.get()); + 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_)} { + intrusive_ptr_log_mctor(sc_self_type, this, ptr_); + /* 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()) { + intrusive_ptr_log_actor(sc_self_type, this, y); + 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_; + + intrusive_ptr_log_dtor(sc_self_type, this, x); + + 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(); + + intrusive_ptr_log_assign(sc_self_type, this, x); + + 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) { + intrusive_ptr_log_massign(sc_self_type, this, rhs.get()); + + 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; } + + template + using rp = intrusive_ptr; + + 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*/ + + void intrusive_ptr_set_debug(bool x); + void intrusive_ptr_log_ctor(std::string_view const & self_type, + void * this_ptr, + Refcount * x); + /* here actor short for 'aliasing ctor' */ + void intrusive_ptr_log_actor(std::string_view const & self_type, + void * this_ptr, + Refcount * x); + void intrusive_ptr_log_cctor(std::string_view const & self_type, + void * this_ptr, + Refcount * x); + void intrusive_ptr_log_mctor(std::string_view const & self_type, + void *this_ptr, + Refcount * x); + void intrusive_ptr_log_dtor(std::string_view const & self_type, + void * this_ptr, + Refcount * x); + void intrusive_ptr_log_assign(std::string_view const & self_type, + void * this_ptr, + Refcount * x); + void intrusive_ptr_log_massign(std::string_view const & self_type, + void *this_ptr, + Refcount * x); + void intrusive_ptr_add_ref(Refcount * x); + 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<<*/ + + /* 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: + template + Borrow(rp const & x) : ptr_(x.get()) {} + + 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*/ + + private: + Borrow(T * x) : ptr_(x) {} + + 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*/ + + } /*namespace ref*/ +} /*namespace xo*/ + +/* end Refcounted.hpp */ diff --git a/include/refcnt/Unowned.hpp b/include/refcnt/Unowned.hpp new file mode 100644 index 00000000..3f78f0d2 --- /dev/null +++ b/include/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/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 00000000..8083cee0 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,14 @@ +set(SELF_LIBRARY_NAME refcnt) +set(SELF_SOURCE_FILES Refcounted.cpp Displayable.cpp) +add_library(${SELF_LIBRARY_NAME} SHARED ${SELF_SOURCE_FILES}) + +set_target_properties(${SELF_LIBRARY_NAME} + PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 1) + +xo_indentlog_dependency(${SELF_LIBRARY_NAME}) + +xo_include_options(${SELF_LIBRARY_NAME}) +xo_compile_options(${SELF_LIBRARY_NAME}) +xo_install_library(${SELF_LIBRARY_NAME}) diff --git a/src/Displayable.cpp b/src/Displayable.cpp new file mode 100644 index 00000000..b8793ad3 --- /dev/null +++ b/src/Displayable.cpp @@ -0,0 +1,16 @@ +/* @file Displayable.cpp */ + +#include "refcnt/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/src/Refcounted.cpp b/src/Refcounted.cpp new file mode 100644 index 00000000..9aeb579d --- /dev/null +++ b/src/Refcounted.cpp @@ -0,0 +1,157 @@ +/* @file Refcounted.cpp */ + +#include "Refcounted.hpp" + +namespace xo { + namespace ref { + 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(verbose, self_type, method_name), + "enter", + xtag("this", this_ptr), + xtag("x", x), + xtag("n", intrusive_ptr_refcount(x))); + } /*intrusive_ptr_log_aux*/ + } /*namespace*/ + + bool + 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*/ + + 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 */