diff --git a/.github/workflows/gtest.yml b/.github/workflows/gtest.yml new file mode 100644 index 0000000..cf1a630 --- /dev/null +++ b/.github/workflows/gtest.yml @@ -0,0 +1,66 @@ +name: gtest + +on: + - push + - pull_request + +jobs: + gtest: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Figure out version + id: tag + run: | + TAG=$(git describe --tags --abbrev=0) + COMMITS_SINCE_TAG=$(git rev-list ${TAG}..HEAD --count) + if [ "${COMMITS_SINCE_TAG}" -eq 0 ]; then + echo "VERSION=${TAG}" >> $GITHUB_ENV + else + echo "VERSION="$(git describe --tags --abbrev=8) >> $GITHUB_ENV + fi + - name: Cache Conan2 dependencies + uses: actions/cache@v3 + with: + path: ~/.conan2 + key: ${{ runner.os }}-conan2-${{ hashFiles('**/conanfile.py') }} + restore-keys: | + ${{ runner.os }}-conan2- + - name: Set up Python 3.8 for gcovr + uses: actions/setup-python@v4 + - name: SonarQube install + uses: SonarSource/sonarcloud-github-c-cpp@v3 + - name: Install Conan + run: pip install conan + - name: Configure Conan Profile + run: | + conan profile detect -e + conan remote add conan-nexus https://nexus.cridland.io/repository/dwd-conan --force + conan remote login conan-nexus ci --password ${{ secrets.NEXUS_PASSWORD }} + - name: Conan Deps (Debug) + run: conan install . -s build_type=Debug -s compiler.cppstd=gnu23 -b missing --version=${{ env.VERSION }} + - name: Conan Deps (Debug) + run: conan install . -s build_type=Debug -s compiler.cppstd=gnu23 -b missing --version=${{ env.VERSION }} + - name: Conan Deps (Release) + run: conan install . -s build_type=Release -s compiler.cppstd=gnu23 -b missing --version=${{ env.VERSION }} + - name: Conan Deps (RelWithDebInfo) + run: conan install . -s build_type=RelWithDebInfo -s compiler.cppstd=gnu23 -b missing --version=${{ env.VERSION }} + - name: Conan Deps (Debug+Tests) + run: conan install . -o tests=True -s build_type=Debug -s compiler.cppstd=gnu23 -b missing --version=${{ env.VERSION }} + - name: CMake tests + run: cmake -B gh-build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="conan_provider.cmake" -DCONAN_INSTALL_ARGS="-o=tests=True;--settings=compiler.cppstd=gnu23;--build=missing;--version=${{ env.VERSION }}" + - name: Build + run: cmake --build gh-build + - name: Run Tests + run: cd gh-build && ./sigslot-test + - name: Create package (Debug) + run: conan create . -s build_type=Debug -s compiler.cppstd=gnu23 -b missing --version=${{ env.VERSION }} + - name: Create package (Release) + run: conan create . -s build_type=Release -s compiler.cppstd=gnu23 -b missing --version=${{ env.VERSION }} + - name: Create package (RelWithDebInfo) + run: conan create . -s build_type=RelWithDebInfo -s compiler.cppstd=gnu23 -b missing --version=${{ env.VERSION }} + - name: Upload + run: conan upload -r conan-nexus --confirm 'st-sigslot/*' diff --git a/CMakeLists.txt b/CMakeLists.txt index e8227e0..dfa658c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,40 +5,18 @@ project(sigslot) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -include(FetchContent) -FetchContent_Declare( - googletest - URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip -) -FetchContent_MakeAvailable(googletest) - +find_package(GTest) +find_package(sentry) enable_testing() -link_libraries(GTest::gtest_main) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}) add_executable(sigslot-test test/sigslot.cc - test/coroutine.cc - sigslot/sigslot.h - sigslot/tasklet.h - sigslot/resume.h -) -add_executable(sigslot-test-resume - sigslot/sigslot.h - sigslot/tasklet.h - test/resume.cc - sigslot/resume.h -) -add_executable(sigslot-test-cothread - sigslot/sigslot.h - sigslot/tasklet.h - test/cothread.cc - sigslot/resume.h - sigslot/cothread.h + test/main.cpp ) +target_link_libraries(sigslot-test PUBLIC gtest::gtest sentry-native::sentry-native) +target_include_directories(sigslot-test SYSTEM PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(sigslot-test PRIVATE DWD_GTEST_SENTRY=1) include(GoogleTest) gtest_discover_tests(sigslot-test) -gtest_discover_tests(sigslot-test-resume) -gtest_discover_tests(sigslot-test-cothread) if (UNIX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcoroutines") @@ -46,3 +24,5 @@ endif () if (WIN32) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /await") endif() + +install(DIRECTORY sigslot TYPE INCLUDE) \ No newline at end of file diff --git a/README.md b/README.md index fa0ff01..089f27d 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ Various patches and fixes applied by Cat Nap Games: To make this compile under Xcode 4.3 with Clang 3.0 I made some changes myself and also used some diffs published in the original project's Sourceforge forum. I don't remember which ones though. -C++11-erization (and C++2x-erixation, and mini coroutine library) by Dave Cridland: +C++11-erization (and C++2x-erixation) by Dave Cridland: -See example.cc and co_example.cc for some documentation and a walk-through example, or read the tests. +See example.cc for some documentation and a walk-through example, or read the tests. This is public domain; no copyright is claimed or asserted. @@ -34,23 +34,3 @@ Loosely, calling "emit(...)" on the signal will then call all the connected "slo If a class is derived (publicly) from has_slots, you can pass in the instance of the class you want to control the lifetime. For calling a specific member directly, that's an easy decision; but if you pass in a lambda or some other arbitrary function, it might not be. If there's nothing obvious to hand, something still needs to control the scope - leaving out the has_slots argument therefore returns you a (deliberately undocumented) placeholder class, which acts in lieu of a has_slots derived class of your choice. - - - -This has a somewhat integrated coroutine library. Tasklets are coroutines, and like most coroutines they can be started, resumed, etc. There's no generator defined, just simple coroutines. - -Tasklets expose co_await, so can be awaited by other coroutines. Signals can also be awaited upon, and will resolve to nothing (ie, void), or the single type, or a std::tuple of the types. - - - -Coroutine resumption can be tricky, and is usually best integrated into some kind of event loop. Failure to do so will make it very hard to do anything that you couldn't do as well (or better!) without. - -You can define your own resume function which will be called when a coroutine should be resumed, a trivial (and rather poor) example is at the beginning of the co_thread tests. - -If you don't, then std::coroutine_handle<>::resume() will be called directly (which works for trivial cases, but not for anything useful). - - - -sigslot::co_thread is a convenient (but very simple) wrapper to run a non-coroutine in a std::jthread, but outwardly behave as a coroutine. Construct once, and it can be treated as a coroutine definition thereafter, and called multiple times. - -This will not work with the built-in resumption, you'll need to implement *some* kind of event loop. diff --git a/co_example.cc b/co_example.cc deleted file mode 100644 index 09f054e..0000000 --- a/co_example.cc +++ /dev/null @@ -1,117 +0,0 @@ -#include -#include -#include -#include - -/** - * We'll use some global signals here. - */ -sigslot::signal<> tick; -sigslot::signal tock; -sigslot::signal splat; - -/** - * Our simple coroutine. - * - * All it's going to do is await the two signals - we won't do anything with it. - */ - -sigslot::tasklet coroutine_example() { - std::cout << "C: Ready." << std::endl; - /** - * If you co_await a signal, execution stops and control moves to the caller. - */ - co_await tick; - /** - * And now the signal must have been triggered. - * - * Awaiting a signal is inherently one-shot, if the signal is triggered twice, - * we won't know about it. - */ - std::cout << "C: Got a tick." << std::endl; - /** - * For signals with a single argument, the argument gets returned by the - * co_await when it completes: - */ - auto foo = co_await tock; - std::cout << "C: Got a tock of " << foo << std::endl; - /** - * Signals that have multiple arguments also work, but it passes back a std::tuple - * in this case. This is relatively easy to unwrap with a std::tie, though it would - * be simpler with C++17's Structured Bindings. - */ - int x; - std::string s; - std::tie(x, s) = co_await splat; - std::cout << "C: Got a splat of " << x << ", " << s << std::endl; - co_return foo; -} - -sigslot::tasklet wrapping_coroutine() { - auto task = coroutine_example(); - std::cout << "W: Starting an inner coroutine." << std::endl; - task.start(); - std::cout << "W: Waiting" << std::endl; - auto foo = co_await task; - std::cout << "W: Inner coroutine completed with " << foo << std::endl; - co_return foo; -} - -sigslot::tasklet throws_exception() { - std::cout << "I shall throw an exception:" << std::endl; - throw std::runtime_error("This is an exception."); - co_return; // This is unreachable, but needed since otherwise it's not a coroutine! -} - -sigslot::tasklet catch_exceptions() { - try { - co_await throws_exception(); - co_return false; - } catch(std::runtime_error & e) { - std::cout << "Caught: " << e.what() << std::endl; - co_return true; - } -} - -int main(int argc, char *argv[]) { - try { - /** - * First with the coroutine awaiting: - */ - std::cout << "M: Executing coroutine." << std::endl; - auto c = wrapping_coroutine(); - c.start(); // Start the tasklet. It'll execute until it needs to await a signal, then stop and return. - // Tasklets automatically start if you get() or co_await them, but - // if they then suspend you'll get a runtime error. - std::cout << "M: Coroutine started, now running: " << c.running() << std::endl; - std::cout << "M: Tick:" << std::endl; - tick(); // When we emit the signal, it'll start executing the coroutine again. Again, it'll stop when it awaits the next signal. - std::cout << "M: Tock(42):" << std::endl; - tock(42); - std::cout << "M: Splat(17, \"Gerbils\")" << std::endl; - splat(17, "Gerbils"); - std::cout << "M: Answer is " << c.get() << std::endl; - /** - * If we sent the second signal before the first, the coroutine would wait forever. - * This is because it wouldn't fire when the coroutine is suspended in co_await. - */ - auto ex = catch_exceptions(); - ex.start(); - if (ex.get()) { - std::cout << "Caught the exception properly" << std::endl; - } else { - throw std::runtime_error("Didn't catch exception!"); - } - try { - auto ex1 = throws_exception(); - std::cout << "Here we go." << std::endl; - ex1.get(); - throw std::runtime_error("Didn't catch exception!"); - } catch (std::runtime_error & e) { - std::cout << "Expected exception caught: " << e.what() << std::endl; - } - } catch (std::exception const & e) { - std::cerr << e.what() << std::endl; - throw; - } -} \ No newline at end of file diff --git a/conan_provider.cmake b/conan_provider.cmake new file mode 100644 index 0000000..0d1eee6 --- /dev/null +++ b/conan_provider.cmake @@ -0,0 +1,679 @@ +# This file is managed by Conan, contents will be overwritten. +# To keep your changes, remove these comment lines, but the plugin won't be able to modify your requirements + +# The MIT License (MIT) +# +# Copyright (c) 2024 JFrog +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +set(CONAN_MINIMUM_VERSION 2.0.5) + +# Create a new policy scope and set the minimum required cmake version so the +# features behind a policy setting like if(... IN_LIST ...) behaves as expected +# even if the parent project does not specify a minimum cmake version or a minimum +# version less than this module requires (e.g. 3.0) before the first project() call. +# (see: https://cmake.org/cmake/help/latest/variable/CMAKE_PROJECT_TOP_LEVEL_INCLUDES.html) +# +# The policy-affecting calls like cmake_policy(SET...) or `cmake_minimum_required` only +# affects the current policy scope, i.e. between the PUSH and POP in this case. +# +# https://cmake.org/cmake/help/book/mastering-cmake/chapter/Policies.html#the-policy-stack +cmake_policy(PUSH) +cmake_minimum_required(VERSION 3.24) + + +function(detect_os os os_api_level os_sdk os_subsystem os_version) + # it could be cross compilation + message(STATUS "CMake-Conan: cmake_system_name=${CMAKE_SYSTEM_NAME}") + if(CMAKE_SYSTEM_NAME AND NOT CMAKE_SYSTEM_NAME STREQUAL "Generic") + if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(${os} Macos PARENT_SCOPE) + elseif(CMAKE_SYSTEM_NAME STREQUAL "QNX") + set(${os} Neutrino PARENT_SCOPE) + elseif(CMAKE_SYSTEM_NAME STREQUAL "CYGWIN") + set(${os} Windows PARENT_SCOPE) + set(${os_subsystem} cygwin PARENT_SCOPE) + elseif(CMAKE_SYSTEM_NAME MATCHES "^MSYS") + set(${os} Windows PARENT_SCOPE) + set(${os_subsystem} msys2 PARENT_SCOPE) + else() + set(${os} ${CMAKE_SYSTEM_NAME} PARENT_SCOPE) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Android") + if(DEFINED ANDROID_PLATFORM) + string(REGEX MATCH "[0-9]+" _os_api_level ${ANDROID_PLATFORM}) + elseif(DEFINED CMAKE_SYSTEM_VERSION) + set(_os_api_level ${CMAKE_SYSTEM_VERSION}) + endif() + message(STATUS "CMake-Conan: android api level=${_os_api_level}") + set(${os_api_level} ${_os_api_level} PARENT_SCOPE) + endif() + if(CMAKE_SYSTEM_NAME MATCHES "Darwin|iOS|tvOS|watchOS") + # CMAKE_OSX_SYSROOT contains the full path to the SDK for MakeFile/Ninja + # generators, but just has the original input string for Xcode. + if(NOT IS_DIRECTORY ${CMAKE_OSX_SYSROOT}) + set(_os_sdk ${CMAKE_OSX_SYSROOT}) + else() + if(CMAKE_OSX_SYSROOT MATCHES Simulator) + set(apple_platform_suffix simulator) + else() + set(apple_platform_suffix os) + endif() + if(CMAKE_OSX_SYSROOT MATCHES AppleTV) + set(_os_sdk "appletv${apple_platform_suffix}") + elseif(CMAKE_OSX_SYSROOT MATCHES iPhone) + set(_os_sdk "iphone${apple_platform_suffix}") + elseif(CMAKE_OSX_SYSROOT MATCHES Watch) + set(_os_sdk "watch${apple_platform_suffix}") + endif() + endif() + if(DEFINED os_sdk) + message(STATUS "CMake-Conan: cmake_osx_sysroot=${CMAKE_OSX_SYSROOT}") + set(${os_sdk} ${_os_sdk} PARENT_SCOPE) + endif() + if(DEFINED CMAKE_OSX_DEPLOYMENT_TARGET) + message(STATUS "CMake-Conan: cmake_osx_deployment_target=${CMAKE_OSX_DEPLOYMENT_TARGET}") + set(${os_version} ${CMAKE_OSX_DEPLOYMENT_TARGET} PARENT_SCOPE) + endif() + endif() + endif() +endfunction() + + +function(detect_arch arch) + # CMAKE_OSX_ARCHITECTURES can contain multiple architectures, but Conan only supports one. + # Therefore this code only finds one. If the recipes support multiple architectures, the + # build will work. Otherwise, there will be a linker error for the missing architecture(s). + if(DEFINED CMAKE_OSX_ARCHITECTURES) + string(REPLACE " " ";" apple_arch_list "${CMAKE_OSX_ARCHITECTURES}") + list(LENGTH apple_arch_list apple_arch_count) + if(apple_arch_count GREATER 1) + message(WARNING "CMake-Conan: Multiple architectures detected, this will only work if Conan recipe(s) produce fat binaries.") + endif() + endif() + if(CMAKE_SYSTEM_NAME MATCHES "Darwin|iOS|tvOS|watchOS" AND NOT CMAKE_OSX_ARCHITECTURES STREQUAL "") + set(host_arch ${CMAKE_OSX_ARCHITECTURES}) + elseif(MSVC) + set(host_arch ${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}) + else() + set(host_arch ${CMAKE_SYSTEM_PROCESSOR}) + endif() + if(host_arch MATCHES "aarch64|arm64|ARM64") + set(_arch armv8) + elseif(host_arch MATCHES "armv7|armv7-a|armv7l|ARMV7") + set(_arch armv7) + elseif(host_arch MATCHES armv7s) + set(_arch armv7s) + elseif(host_arch MATCHES "i686|i386|X86") + set(_arch x86) + elseif(host_arch MATCHES "AMD64|amd64|x86_64|x64") + set(_arch x86_64) + endif() + message(STATUS "CMake-Conan: cmake_system_processor=${_arch}") + set(${arch} ${_arch} PARENT_SCOPE) +endfunction() + + +function(detect_cxx_standard cxx_standard) + set(${cxx_standard} ${CMAKE_CXX_STANDARD} PARENT_SCOPE) + if(CMAKE_CXX_EXTENSIONS) + set(${cxx_standard} "gnu${CMAKE_CXX_STANDARD}" PARENT_SCOPE) + endif() +endfunction() + + +macro(detect_gnu_libstdcxx) + # _conan_is_gnu_libstdcxx true if GNU libstdc++ + check_cxx_source_compiles(" + #include + #if !defined(__GLIBCXX__) && !defined(__GLIBCPP__) + static_assert(false); + #endif + int main(){}" _conan_is_gnu_libstdcxx) + + # _conan_gnu_libstdcxx_is_cxx11_abi true if C++11 ABI + check_cxx_source_compiles(" + #include + static_assert(sizeof(std::string) != sizeof(void*), \"using libstdc++\"); + int main () {}" _conan_gnu_libstdcxx_is_cxx11_abi) + + set(_conan_gnu_libstdcxx_suffix "") + if(_conan_gnu_libstdcxx_is_cxx11_abi) + set(_conan_gnu_libstdcxx_suffix "11") + endif() + unset (_conan_gnu_libstdcxx_is_cxx11_abi) +endmacro() + + +macro(detect_libcxx) + # _conan_is_libcxx true if LLVM libc++ + check_cxx_source_compiles(" + #include + #if !defined(_LIBCPP_VERSION) + static_assert(false); + #endif + int main(){}" _conan_is_libcxx) +endmacro() + + +function(detect_lib_cxx lib_cxx) + if(CMAKE_SYSTEM_NAME STREQUAL "Android") + message(STATUS "CMake-Conan: android_stl=${CMAKE_ANDROID_STL_TYPE}") + set(${lib_cxx} ${CMAKE_ANDROID_STL_TYPE} PARENT_SCOPE) + return() + endif() + + include(CheckCXXSourceCompiles) + + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + detect_gnu_libstdcxx() + set(${lib_cxx} "libstdc++${_conan_gnu_libstdcxx_suffix}" PARENT_SCOPE) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "AppleClang") + set(${lib_cxx} "libc++" PARENT_SCOPE) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_SYSTEM_NAME MATCHES "Windows") + # Check for libc++ + detect_libcxx() + if(_conan_is_libcxx) + set(${lib_cxx} "libc++" PARENT_SCOPE) + return() + endif() + + # Check for libstdc++ + detect_gnu_libstdcxx() + if(_conan_is_gnu_libstdcxx) + set(${lib_cxx} "libstdc++${_conan_gnu_libstdcxx_suffix}" PARENT_SCOPE) + return() + endif() + + # TODO: it would be an error if we reach this point + elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + # Do nothing - compiler.runtime and compiler.runtime_type + # should be handled separately: https://github.com/conan-io/cmake-conan/pull/516 + return() + else() + # TODO: unable to determine, ask user to provide a full profile file instead + endif() +endfunction() + + +function(detect_compiler compiler compiler_version compiler_runtime compiler_runtime_type) + if(DEFINED CMAKE_CXX_COMPILER_ID) + set(_compiler ${CMAKE_CXX_COMPILER_ID}) + set(_compiler_version ${CMAKE_CXX_COMPILER_VERSION}) + else() + if(NOT DEFINED CMAKE_C_COMPILER_ID) + message(FATAL_ERROR "C or C++ compiler not defined") + endif() + set(_compiler ${CMAKE_C_COMPILER_ID}) + set(_compiler_version ${CMAKE_C_COMPILER_VERSION}) + endif() + + message(STATUS "CMake-Conan: CMake compiler=${_compiler}") + message(STATUS "CMake-Conan: CMake compiler version=${_compiler_version}") + + if(_compiler MATCHES MSVC) + set(_compiler "msvc") + string(SUBSTRING ${MSVC_VERSION} 0 3 _compiler_version) + # Configure compiler.runtime and compiler.runtime_type settings for MSVC + if(CMAKE_MSVC_RUNTIME_LIBRARY) + set(_msvc_runtime_library ${CMAKE_MSVC_RUNTIME_LIBRARY}) + else() + set(_msvc_runtime_library MultiThreaded$<$:Debug>DLL) # default value documented by CMake + endif() + + set(_KNOWN_MSVC_RUNTIME_VALUES "") + list(APPEND _KNOWN_MSVC_RUNTIME_VALUES MultiThreaded MultiThreadedDLL) + list(APPEND _KNOWN_MSVC_RUNTIME_VALUES MultiThreadedDebug MultiThreadedDebugDLL) + list(APPEND _KNOWN_MSVC_RUNTIME_VALUES MultiThreaded$<$:Debug> MultiThreaded$<$:Debug>DLL) + + # only accept the 6 possible values, otherwise we don't don't know to map this + if(NOT _msvc_runtime_library IN_LIST _KNOWN_MSVC_RUNTIME_VALUES) + message(FATAL_ERROR "CMake-Conan: unable to map MSVC runtime: ${_msvc_runtime_library} to Conan settings") + endif() + + # Runtime is "dynamic" in all cases if it ends in DLL + if(_msvc_runtime_library MATCHES ".*DLL$") + set(_compiler_runtime "dynamic") + else() + set(_compiler_runtime "static") + endif() + message(STATUS "CMake-Conan: CMake compiler.runtime=${_compiler_runtime}") + + # Only define compiler.runtime_type when explicitly requested + # If a generator expression is used, let Conan handle it conditional on build_type + if(NOT _msvc_runtime_library MATCHES ":Debug>") + if(_msvc_runtime_library MATCHES "Debug") + set(_compiler_runtime_type "Debug") + else() + set(_compiler_runtime_type "Release") + endif() + message(STATUS "CMake-Conan: CMake compiler.runtime_type=${_compiler_runtime_type}") + endif() + + unset(_KNOWN_MSVC_RUNTIME_VALUES) + + elseif(_compiler MATCHES AppleClang) + set(_compiler "apple-clang") + string(REPLACE "." ";" VERSION_LIST ${CMAKE_CXX_COMPILER_VERSION}) + list(GET VERSION_LIST 0 _compiler_version) + elseif(_compiler MATCHES Clang) + set(_compiler "clang") + string(REPLACE "." ";" VERSION_LIST ${CMAKE_CXX_COMPILER_VERSION}) + list(GET VERSION_LIST 0 _compiler_version) + elseif(_compiler MATCHES GNU) + set(_compiler "gcc") + string(REPLACE "." ";" VERSION_LIST ${CMAKE_CXX_COMPILER_VERSION}) + list(GET VERSION_LIST 0 _compiler_version) + endif() + + message(STATUS "CMake-Conan: [settings] compiler=${_compiler}") + message(STATUS "CMake-Conan: [settings] compiler.version=${_compiler_version}") + if (_compiler_runtime) + message(STATUS "CMake-Conan: [settings] compiler.runtime=${_compiler_runtime}") + endif() + if (_compiler_runtime_type) + message(STATUS "CMake-Conan: [settings] compiler.runtime_type=${_compiler_runtime_type}") + endif() + + set(${compiler} ${_compiler} PARENT_SCOPE) + set(${compiler_version} ${_compiler_version} PARENT_SCOPE) + set(${compiler_runtime} ${_compiler_runtime} PARENT_SCOPE) + set(${compiler_runtime_type} ${_compiler_runtime_type} PARENT_SCOPE) +endfunction() + + +function(detect_build_type build_type) + get_property(multiconfig_generator GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + if(NOT multiconfig_generator) + # Only set when we know we are in a single-configuration generator + # Note: we may want to fail early if `CMAKE_BUILD_TYPE` is not defined + set(${build_type} ${CMAKE_BUILD_TYPE} PARENT_SCOPE) + endif() +endfunction() + + +macro(set_conan_compiler_if_appleclang lang command output_variable) + if(CMAKE_${lang}_COMPILER_ID STREQUAL "AppleClang") + execute_process(COMMAND xcrun --find ${command} + OUTPUT_VARIABLE _xcrun_out OUTPUT_STRIP_TRAILING_WHITESPACE) + cmake_path(GET _xcrun_out PARENT_PATH _xcrun_toolchain_path) + cmake_path(GET CMAKE_${lang}_COMPILER PARENT_PATH _compiler_parent_path) + if ("${_xcrun_toolchain_path}" STREQUAL "${_compiler_parent_path}") + set(${output_variable} "") + endif() + unset(_xcrun_out) + unset(_xcrun_toolchain_path) + unset(_compiler_parent_path) + endif() +endmacro() + + +macro(append_compiler_executables_configuration) + set(_conan_c_compiler "") + set(_conan_cpp_compiler "") + set(_conan_rc_compiler "") + set(_conan_compilers_list "") + if(CMAKE_C_COMPILER) + set(_conan_c_compiler "\"c\":\"${CMAKE_C_COMPILER}\"") + set_conan_compiler_if_appleclang(C cc _conan_c_compiler) + list(APPEND _conan_compilers_list ${_conan_c_compiler}) + else() + message(WARNING "CMake-Conan: The C compiler is not defined. " + "Please define CMAKE_C_COMPILER or enable the C language.") + endif() + if(CMAKE_CXX_COMPILER) + set(_conan_cpp_compiler "\"cpp\":\"${CMAKE_CXX_COMPILER}\"") + set_conan_compiler_if_appleclang(CXX c++ _conan_cpp_compiler) + list(APPEND _conan_compilers_list ${_conan_cpp_compiler}) + else() + message(WARNING "CMake-Conan: The C++ compiler is not defined. " + "Please define CMAKE_CXX_COMPILER or enable the C++ language.") + endif() + if(CMAKE_RC_COMPILER) + set(_conan_rc_compiler "\"rc\":\"${CMAKE_RC_COMPILER}\"") + list(APPEND _conan_compilers_list ${_conan_rc_compiler}) + # Not necessary to warn if RC not defined + endif() + if(NOT "x${_conan_compilers_list}" STREQUAL "x") + string(REPLACE ";" "," _conan_compilers_list "${_conan_compilers_list}") + string(APPEND profile "tools.build:compiler_executables={${_conan_compilers_list}}\n") + endif() + unset(_conan_c_compiler) + unset(_conan_cpp_compiler) + unset(_conan_rc_compiler) + unset(_conan_compilers_list) +endmacro() + + +function(detect_host_profile output_file) + detect_os(os os_api_level os_sdk os_subsystem os_version) + detect_arch(arch) + detect_compiler(compiler compiler_version compiler_runtime compiler_runtime_type) + detect_cxx_standard(compiler_cppstd) + detect_lib_cxx(compiler_libcxx) + detect_build_type(build_type) + + set(profile "") + string(APPEND profile "[settings]\n") + if(arch) + string(APPEND profile arch=${arch} "\n") + endif() + if(os) + string(APPEND profile os=${os} "\n") + endif() + if(os_api_level) + string(APPEND profile os.api_level=${os_api_level} "\n") + endif() + if(os_version) + string(APPEND profile os.version=${os_version} "\n") + endif() + if(os_sdk) + string(APPEND profile os.sdk=${os_sdk} "\n") + endif() + if(os_subsystem) + string(APPEND profile os.subsystem=${os_subsystem} "\n") + endif() + if(compiler) + string(APPEND profile compiler=${compiler} "\n") + endif() + if(compiler_version) + string(APPEND profile compiler.version=${compiler_version} "\n") + endif() + if(compiler_runtime) + string(APPEND profile compiler.runtime=${compiler_runtime} "\n") + endif() + if(compiler_runtime_type) + string(APPEND profile compiler.runtime_type=${compiler_runtime_type} "\n") + endif() + if(compiler_cppstd) + string(APPEND profile compiler.cppstd=${compiler_cppstd} "\n") + endif() + if(compiler_libcxx) + string(APPEND profile compiler.libcxx=${compiler_libcxx} "\n") + endif() + if(build_type) + string(APPEND profile "build_type=${build_type}\n") + endif() + + if(NOT DEFINED output_file) + set(file_name "${CMAKE_BINARY_DIR}/profile") + else() + set(file_name ${output_file}) + endif() + + string(APPEND profile "[conf]\n") + string(APPEND profile "tools.cmake.cmaketoolchain:generator=${CMAKE_GENERATOR}\n") + + # propagate compilers via profile + append_compiler_executables_configuration() + + if(os STREQUAL "Android") + string(APPEND profile "tools.android:ndk_path=${CMAKE_ANDROID_NDK}\n") + endif() + + message(STATUS "CMake-Conan: Creating profile ${file_name}") + file(WRITE ${file_name} ${profile}) + message(STATUS "CMake-Conan: Profile: \n${profile}") +endfunction() + + +function(conan_profile_detect_default) + message(STATUS "CMake-Conan: Checking if a default profile exists") + execute_process(COMMAND ${CONAN_COMMAND} profile path default + RESULT_VARIABLE return_code + OUTPUT_VARIABLE conan_stdout + ERROR_VARIABLE conan_stderr + ECHO_ERROR_VARIABLE # show the text output regardless + ECHO_OUTPUT_VARIABLE + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + if(NOT ${return_code} EQUAL "0") + message(STATUS "CMake-Conan: The default profile doesn't exist, detecting it.") + execute_process(COMMAND ${CONAN_COMMAND} profile detect + RESULT_VARIABLE return_code + OUTPUT_VARIABLE conan_stdout + ERROR_VARIABLE conan_stderr + ECHO_ERROR_VARIABLE # show the text output regardless + ECHO_OUTPUT_VARIABLE + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + endif() +endfunction() + + +function(conan_install) + cmake_parse_arguments(ARGS conan_args ${ARGN}) + set(conan_output_folder ${CMAKE_BINARY_DIR}/conan) + # Invoke "conan install" with the provided arguments + set(conan_args ${conan_args} -of=${conan_output_folder}) + message(STATUS "CMake-Conan: conan install ${CMAKE_SOURCE_DIR} ${conan_args} ${ARGN}") + + + # In case there was not a valid cmake executable in the PATH, we inject the + # same we used to invoke the provider to the PATH + if(DEFINED PATH_TO_CMAKE_BIN) + set(old_path $ENV{PATH}) + set(ENV{PATH} "$ENV{PATH}:${PATH_TO_CMAKE_BIN}") + endif() + + execute_process(COMMAND ${CONAN_COMMAND} install ${CMAKE_SOURCE_DIR} ${conan_args} ${ARGN} --format=json + RESULT_VARIABLE return_code + OUTPUT_VARIABLE conan_stdout + ERROR_VARIABLE conan_stderr + ECHO_ERROR_VARIABLE # show the text output regardless + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + + if(DEFINED PATH_TO_CMAKE_BIN) + set(ENV{PATH} "${old_path}") + endif() + + if(NOT "${return_code}" STREQUAL "0") + message(FATAL_ERROR "Conan install failed='${return_code}'") + endif() + + # the files are generated in a folder that depends on the layout used, if + # one is specified, but we don't know a priori where this is. + # TODO: this can be made more robust if Conan can provide this in the json output + string(JSON conan_generators_folder GET "${conan_stdout}" graph nodes 0 generators_folder) + cmake_path(CONVERT ${conan_generators_folder} TO_CMAKE_PATH_LIST conan_generators_folder) + + message(STATUS "CMake-Conan: CONAN_GENERATORS_FOLDER=${conan_generators_folder}") + set_property(GLOBAL PROPERTY CONAN_GENERATORS_FOLDER "${conan_generators_folder}") + # reconfigure on conanfile changes + string(JSON conanfile GET "${conan_stdout}" graph nodes 0 label) + message(STATUS "CMake-Conan: CONANFILE=${CMAKE_SOURCE_DIR}/${conanfile}") + set_property(DIRECTORY ${CMAKE_SOURCE_DIR} APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/${conanfile}") + # success + set_property(GLOBAL PROPERTY CONAN_INSTALL_SUCCESS TRUE) + +endfunction() + + +function(conan_get_version conan_command conan_current_version) + execute_process( + COMMAND ${conan_command} --version + OUTPUT_VARIABLE conan_output + RESULT_VARIABLE conan_result + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(conan_result) + message(FATAL_ERROR "CMake-Conan: Error when trying to run Conan") + endif() + + string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" conan_version ${conan_output}) + set(${conan_current_version} ${conan_version} PARENT_SCOPE) +endfunction() + + +function(conan_version_check) + set(options ) + set(one_value_args MINIMUM CURRENT) + set(multi_value_args ) + cmake_parse_arguments(conan_version_check + "${options}" "${one_value_args}" "${multi_value_args}" ${ARGN}) + + if(NOT conan_version_check_MINIMUM) + message(FATAL_ERROR "CMake-Conan: Required parameter MINIMUM not set!") + endif() + if(NOT conan_version_check_CURRENT) + message(FATAL_ERROR "CMake-Conan: Required parameter CURRENT not set!") + endif() + + if(conan_version_check_CURRENT VERSION_LESS conan_version_check_MINIMUM) + message(FATAL_ERROR "CMake-Conan: Conan version must be ${conan_version_check_MINIMUM} or later") + endif() +endfunction() + + +macro(construct_profile_argument argument_variable profile_list) + set(${argument_variable} "") + if("${profile_list}" STREQUAL "CONAN_HOST_PROFILE") + set(_arg_flag "--profile:host=") + elseif("${profile_list}" STREQUAL "CONAN_BUILD_PROFILE") + set(_arg_flag "--profile:build=") + endif() + + set(_profile_list "${${profile_list}}") + list(TRANSFORM _profile_list REPLACE "auto-cmake" "${CMAKE_BINARY_DIR}/conan_host_profile") + list(TRANSFORM _profile_list PREPEND ${_arg_flag}) + set(${argument_variable} ${_profile_list}) + + unset(_arg_flag) + unset(_profile_list) +endmacro() + + +macro(conan_provide_dependency method package_name) + set_property(GLOBAL PROPERTY CONAN_PROVIDE_DEPENDENCY_INVOKED TRUE) + get_property(_conan_install_success GLOBAL PROPERTY CONAN_INSTALL_SUCCESS) + if(NOT _conan_install_success) + find_program(CONAN_COMMAND "conan" REQUIRED) + conan_get_version(${CONAN_COMMAND} CONAN_CURRENT_VERSION) + conan_version_check(MINIMUM ${CONAN_MINIMUM_VERSION} CURRENT ${CONAN_CURRENT_VERSION}) + message(STATUS "CMake-Conan: first find_package() found. Installing dependencies with Conan") + if("default" IN_LIST CONAN_HOST_PROFILE OR "default" IN_LIST CONAN_BUILD_PROFILE) + conan_profile_detect_default() + endif() + if("auto-cmake" IN_LIST CONAN_HOST_PROFILE) + detect_host_profile(${CMAKE_BINARY_DIR}/conan_host_profile) + endif() + construct_profile_argument(_host_profile_flags CONAN_HOST_PROFILE) + construct_profile_argument(_build_profile_flags CONAN_BUILD_PROFILE) + if(EXISTS "${CMAKE_SOURCE_DIR}/conanfile.py") + file(READ "${CMAKE_SOURCE_DIR}/conanfile.py" outfile) + if(NOT "${outfile}" MATCHES ".*CMakeDeps.*") + message(WARNING "Cmake-conan: CMakeDeps generator was not defined in the conanfile") + endif() + set(generator "") + elseif (EXISTS "${CMAKE_SOURCE_DIR}/conanfile.txt") + file(READ "${CMAKE_SOURCE_DIR}/conanfile.txt" outfile) + if(NOT "${outfile}" MATCHES ".*CMakeDeps.*") + message(WARNING "Cmake-conan: CMakeDeps generator was not defined in the conanfile. " + "Please define the generator as it will be mandatory in the future") + endif() + set(generator "-g;CMakeDeps") + endif() + get_property(_multiconfig_generator GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + if(NOT _multiconfig_generator) + message(STATUS "CMake-Conan: Installing single configuration ${CMAKE_BUILD_TYPE}") + conan_install(${_host_profile_flags} ${_build_profile_flags} ${CONAN_INSTALL_ARGS} ${generator}) + else() + message(STATUS "CMake-Conan: Installing both Debug and Release") + conan_install(${_host_profile_flags} ${_build_profile_flags} -s build_type=Release ${CONAN_INSTALL_ARGS} ${generator}) + conan_install(${_host_profile_flags} ${_build_profile_flags} -s build_type=Debug ${CONAN_INSTALL_ARGS} ${generator}) + endif() + unset(_host_profile_flags) + unset(_build_profile_flags) + unset(_multiconfig_generator) + unset(_conan_install_success) + else() + message(STATUS "CMake-Conan: find_package(${ARGV1}) found, 'conan install' already ran") + unset(_conan_install_success) + endif() + + get_property(_conan_generators_folder GLOBAL PROPERTY CONAN_GENERATORS_FOLDER) + + # Ensure that we consider Conan-provided packages ahead of any other, + # irrespective of other settings that modify the search order or search paths + # This follows the guidelines from the find_package documentation + # (https://cmake.org/cmake/help/latest/command/find_package.html): + # find_package ( PATHS paths... NO_DEFAULT_PATH) + # find_package () + + # Filter out `REQUIRED` from the argument list, as the first call may fail + set(_find_args_${package_name} "${ARGN}") + list(REMOVE_ITEM _find_args_${package_name} "REQUIRED") + if(NOT "MODULE" IN_LIST _find_args_${package_name}) + find_package(${package_name} ${_find_args_${package_name}} BYPASS_PROVIDER PATHS "${_conan_generators_folder}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) + unset(_find_args_${package_name}) + endif() + + # Invoke find_package a second time - if the first call succeeded, + # this will simply reuse the result. If not, fall back to CMake default search + # behaviour, also allowing modules to be searched. + if(NOT ${package_name}_FOUND) + list(FIND CMAKE_MODULE_PATH "${_conan_generators_folder}" _index) + if(_index EQUAL -1) + list(PREPEND CMAKE_MODULE_PATH "${_conan_generators_folder}") + endif() + unset(_index) + find_package(${package_name} ${ARGN} BYPASS_PROVIDER) + list(REMOVE_ITEM CMAKE_MODULE_PATH "${_conan_generators_folder}") + endif() +endmacro() + + +cmake_language( + SET_DEPENDENCY_PROVIDER conan_provide_dependency + SUPPORTED_METHODS FIND_PACKAGE +) + + +macro(conan_provide_dependency_check) + set(_conan_provide_dependency_invoked FALSE) + get_property(_conan_provide_dependency_invoked GLOBAL PROPERTY CONAN_PROVIDE_DEPENDENCY_INVOKED) + if(NOT _conan_provide_dependency_invoked) + message(WARNING "Conan is correctly configured as dependency provider, " + "but Conan has not been invoked. Please add at least one " + "call to `find_package()`.") + if(DEFINED CONAN_COMMAND) + # supress warning in case `CONAN_COMMAND` was specified but unused. + set(_conan_command ${CONAN_COMMAND}) + unset(_conan_command) + endif() + endif() + unset(_conan_provide_dependency_invoked) +endmacro() + + +# Add a deferred call at the end of processing the top-level directory +# to check if the dependency provider was invoked at all. +cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" CALL conan_provide_dependency_check) + +# Configurable variables for Conan profiles +set(CONAN_HOST_PROFILE "default;auto-cmake" CACHE STRING "Conan host profile") +set(CONAN_BUILD_PROFILE "default" CACHE STRING "Conan build profile") +set(CONAN_INSTALL_ARGS "--build=missing" CACHE STRING "Command line arguments for conan install") + +find_program(_cmake_program NAMES cmake NO_PACKAGE_ROOT_PATH NO_CMAKE_PATH NO_CMAKE_ENVIRONMENT_PATH NO_CMAKE_SYSTEM_PATH NO_CMAKE_FIND_ROOT_PATH) +if(NOT _cmake_program) + get_filename_component(PATH_TO_CMAKE_BIN "${CMAKE_COMMAND}" DIRECTORY) + set(PATH_TO_CMAKE_BIN "${PATH_TO_CMAKE_BIN}" CACHE INTERNAL "Path where the CMake executable is") +endif() + +cmake_policy(POP) diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 0000000..21a3829 --- /dev/null +++ b/conanfile.py @@ -0,0 +1,56 @@ +from conan import ConanFile +from conan.tools.cmake import CMakeDeps, CMakeToolchain, CMake, cmake_layout +from conan.tools.files import copy + +class Siglot(ConanFile): + name = "st-sigslot" + license = "MIT" + author = "Dave Cridland " + url = "https://github.com/dwd/SigSlot" + description = "A simple header-only Signal/Slot C++ library" + topics = ("signal", "slot") + exports_sources = "sigslot/*", "CMakeLists.txt", "test/*" + no_copy_source = True + options = { + "tests": [True, False] + } + default_options = { + "tests": False + } + settings = "os", "compiler", "build_type", "arch" + + def configure(self): + if not self.options.get_safe("tests"): + self.settings.clear() + else: + self.options["sentry-native"].backend = "inproc" + + def requirements(self): + if self.options.get_safe("tests"): + self.requires("gtest/1.12.1") + self.requires("sentry-native/0.7.15") + + def layout(self): + if self.options.tests: + cmake_layout(self) + else: + self.folders.source = '.' + + def generate(self): + if self.options.get_safe("tests"): + deps = CMakeDeps(self) + deps.generate() + cmake = CMakeToolchain(self) + cmake.generate() + + def build(self): + if self.options.get_safe("tests"): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + copy(self, "*.h", self.source_folder, self.package_folder + '/include') + + def package_info(self): + self.cpp_info.includedirs = ["include"] diff --git a/sigslot/cothread.h b/sigslot/cothread.h deleted file mode 100644 index 7f8830f..0000000 --- a/sigslot/cothread.h +++ /dev/null @@ -1,203 +0,0 @@ -// -// Created by dwd on 21/12/2021. -// - -#ifndef SIGSLOT_COTHREAD_H -#define SIGSLOT_COTHREAD_H - -#include -#include "sigslot/sigslot.h" -#include "sigslot/tasklet.h" - -namespace sigslot { - namespace cothread_internal { - template - struct awaitable { - std::coroutine_handle<> awaiting = nullptr; - - awaitable() = default; - awaitable(awaitable && other) = delete; - awaitable(awaitable const &) = delete; - - bool await_ready() { - return has_payload(); - } - - void await_suspend(std::coroutine_handle<> h) { - // The awaiting coroutine is already suspended. - awaiting = h; - await(); - } - - auto await_resume() { - return payload(); - } - - void resolve() { - std::coroutine_handle<> a = nullptr; - std::swap(a, awaiting); - if (a) sigslot::resume_switch(a); - } - - template - void run(Fn & fn, Args&&... args) { - auto wrapped_fn = [this, &fn](Args... a) { - try { - auto result = fn(a...); - { - std::lock_guard l_(m_mutex); - m_payload.emplace(result); - resolve(); - } - } catch(...) { - std::lock_guard l_(m_mutex); - m_eptr = std::current_exception(); - resolve(); - } - }; - m_thread.emplace(wrapped_fn, args...); - } - - void check_await() { - if (!m_thread.has_value()) throw std::logic_error("No thread started"); - } - - bool has_payload() { - if (!m_thread.has_value()) throw std::logic_error("No thread started"); - std::lock_guard l_(m_mutex); - return m_eptr || m_payload.has_value(); - } - - auto payload() { - if (!m_thread.has_value()) throw std::logic_error("No thread started"); - m_thread->join(); - m_thread.reset(); - if (m_eptr) std::rethrow_exception(m_eptr); - return *m_payload; - } - - void await() { - if (!m_thread.has_value()) throw std::logic_error("No thread started"); - std::lock_guard l_(m_mutex); - if (m_eptr || m_payload.has_value()) { - resolve(); - } - } - - private: - std::optional m_thread; - std::optional m_payload; - std::recursive_mutex m_mutex; - std::exception_ptr m_eptr; - }; - template<> - struct awaitable { - std::coroutine_handle<> awaiting = nullptr; - - awaitable() = default; - awaitable(awaitable && other) = delete; - awaitable(awaitable const &) = delete; - - bool await_ready() { - return is_done(); - } - - void await_suspend(std::coroutine_handle<> h) { - // The awaiting coroutine is already suspended. - awaiting = h; - await(); - } - - void await_resume() { - done(); - } - - void resolve() { - std::coroutine_handle<> a = nullptr; - std::swap(a, awaiting); - if (a) sigslot::resume_switch(a); - } - - template - void run(Fn & fn, Args&&... args) { - auto wrapped_fn = [this, &fn](Args... a) { - try { - fn(a...); - { - std::lock_guard l_(m_mutex); - m_done = true; - resolve(); - } - } catch(...) { - std::lock_guard l_(m_mutex); - m_eptr = std::current_exception(); - resolve(); - } - }; - m_thread.emplace(wrapped_fn, args...); - } - - void check_await() { - if (!m_thread.has_value()) throw std::logic_error("No thread started"); - } - - bool is_done() { - if (!m_thread.has_value()) throw std::logic_error("No thread started"); - std::lock_guard l_(m_mutex); - return m_eptr || m_done; - } - - void done() { - if (!m_thread.has_value()) throw std::logic_error("No thread started"); - m_thread->join(); - m_thread.reset(); - if (m_eptr) std::rethrow_exception(m_eptr); - } - - void await() { - if (!m_thread.has_value()) throw std::logic_error("No thread started"); - std::lock_guard l_(m_mutex); - if (m_eptr || m_done) { - resolve(); - } - } - - private: - std::optional m_thread; - bool m_done = false; - std::exception_ptr m_eptr; - std::recursive_mutex m_mutex; - }; - template - struct awaitable_ptr { - std::unique_ptr> m_guts; - - awaitable_ptr() : m_guts(std::make_unique>()) {} - awaitable_ptr(awaitable_ptr &&) = default; - - awaitable & operator co_await() { - m_guts->check_await(); - return *m_guts; - } - }; - } - - template - class co_thread { - public: - private: - Callable m_fn; - public: - - template - [[nodiscard]] auto operator() (Args && ...args) { - cothread_internal::awaitable_ptr awaitable; - awaitable.m_guts->run(m_fn, args...); - return std::move(awaitable); - } - - explicit co_thread(Callable && fn) : m_fn(std::move(fn)) {} - }; -} - -#endif diff --git a/sigslot/resume.h b/sigslot/resume.h deleted file mode 100644 index 93242d5..0000000 --- a/sigslot/resume.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// Created by dave on 22/07/2024. -// - -#ifndef SIGSLOT_RESUME_H -#define SIGSLOT_RESUME_H - -#ifndef SIGSLOT_NO_COROUTINES -#include - -namespace sigslot { - namespace coroutines { - struct sentinel {}; - } - coroutines::sentinel resume(...); - coroutines::sentinel register_coro(...); - coroutines::sentinel deregister_coro(...); -} -#endif - -#endif //SIGSLOT_RESUME_H diff --git a/sigslot/sigslot.h b/sigslot/sigslot.h index 1b85534..432f34c 100644 --- a/sigslot/sigslot.h +++ b/sigslot/sigslot.h @@ -37,50 +37,14 @@ #include #include #include +#include #ifndef SIGSLOT_NO_COROUTINES #include #include #include #endif -#include - namespace sigslot { -#ifndef SIGSLOT_NO_COROUTINES - template - inline void resume_dispatch(std::coroutine_handle<> coro) { - resume(coro); - } - template<> - inline void resume_dispatch(std::coroutine_handle<> coro) { - coro.resume(); - } - inline void resume_switch(std::coroutine_handle<> coro) { - using return_type = decltype(resume(coro)); - resume_dispatch(coro); - } - template - inline void register_dispatch(std::coroutine_handle<> coro) { - register_coro(coro); - } - template<> - inline void register_dispatch(std::coroutine_handle<>) {} - inline void register_switch(std::coroutine_handle<> coro) { - using return_type = decltype(register_coro(coro)); - register_dispatch(coro); - } - template - inline void deregister_dispatch(std::coroutine_handle<> coro) { - register_coro(coro); - } - template<> - inline void deregister_dispatch(std::coroutine_handle<>) {} - inline void deregister_switch(std::coroutine_handle<> coro) { - using return_type = decltype(deregister_coro(coro)); - deregister_dispatch(coro); - } -#endif - class has_slots; namespace internal { @@ -336,7 +300,7 @@ namespace sigslot { signal.connect(this, &awaitable::resolve); } - bool await_ready() { + bool await_ready() const { return payload.has_value(); } @@ -345,13 +309,13 @@ namespace sigslot { awaiting = h; } - auto await_resume() { + auto await_resume() const { return *payload; } void resolve(Args... a) { payload.emplace(a...); - if (awaiting) ::sigslot::resume_switch(awaiting); + if (awaiting) awaiting.resume(); } }; @@ -371,7 +335,7 @@ namespace sigslot { signal.connect(this, &awaitable::resolve); } - bool await_ready() { + bool await_ready() const { return payload.has_value(); } @@ -380,13 +344,13 @@ namespace sigslot { awaiting = h; } - auto await_resume() { + auto await_resume() const { return *payload; } void resolve(T a) { payload.emplace(a); - if (awaiting) ::sigslot::resume_switch(awaiting); + if (awaiting) awaiting.resume(); } }; @@ -421,7 +385,7 @@ namespace sigslot { void resolve(T & a) { payload = &a; - if (awaiting) ::sigslot::resume_switch(awaiting); + if (awaiting) awaiting.resume(); } }; @@ -454,7 +418,7 @@ namespace sigslot { void resolve() { ready = true; - if (awaiting) ::sigslot::resume_switch(awaiting); + if (awaiting) awaiting.resume(); } }; diff --git a/sigslot/tasklet.h b/sigslot/tasklet.h deleted file mode 100644 index 691f366..0000000 --- a/sigslot/tasklet.h +++ /dev/null @@ -1,253 +0,0 @@ -// -// Created by Dave Cridland on 2019-01-23. -// - -#ifndef SIGSLOT_TASKLET_H -#define SIGSLOT_TASKLET_H - -#include -#include -#include -#include - -namespace sigslot { - template struct tasklet; - - struct tracker { - virtual void terminate() {} - virtual void exception(std::exception_ptr const & eptr) {} - virtual ~tracker() {} - }; - template - requires std::is_base_of_v - std::shared_ptr track(Args&&... args) { - return std::make_shared(std::forward(args)...); - } - - namespace internal { - template - struct tasklet { - handle_type coro; - - explicit tasklet() : coro(nullptr) {} - - tasklet(handle_type h) : coro(h) { - ::sigslot::register_switch(coro); - } - - tasklet(tasklet &&other) noexcept : coro(other.coro) { - other.coro = nullptr; - ::sigslot::register_switch(coro); - } - - tasklet(tasklet const &other) : coro(other.coro) { - ::sigslot::register_switch(coro); - } - - tasklet &operator=(tasklet &&other) noexcept { - coro = other.coro; - ::sigslot::register_switch(coro); - other.coro = nullptr; - return *this; - } - - tasklet &operator=(tasklet const &) = delete; - - ~tasklet() { - if (coro) { - ::sigslot::deregister_switch(coro); - coro.destroy(); - } - } - - auto get() const { - if (!coro.promise().started) { - // Never started, so start now. - const_cast(this)->start(); - } - if (!coro.promise().finished) { - throw std::runtime_error("Not finished yet"); - } - return coro.promise().get(); - } - void await_suspend(std::coroutine_handle<> h) const { - // The awaiting coroutine is already suspended. - if (coro.promise().awaiting) throw std::logic_error("Already an awaiter for this task"); - coro.promise().awaiting = h; - } - bool await_ready() const { - if (!coro.promise().started) { - // Never started, so start now. - const_cast(this)->start(); - } - return coro.promise().finished; - } - auto await_resume() const { - return get(); - } - auto const & operator co_await() const { - return *this; - } - - - bool started() const { - return coro.promise().started; - } - - void start() { - if (!coro) throw std::logic_error("No coroutine to start"); - if (coro.done()) throw std::logic_error("Already run"); - if (coro.promise().started) throw std::logic_error("Already started"); - if (coro.promise().finished) throw std::logic_error("Already finished"); - coro.promise().started = true; - coro.resume(); - } - - bool running() const { - if (!coro) return false; - return !coro.done(); - } - - sigslot::signal<> &complete() { - return coro.promise().complete; - } - - sigslot::signal &exception() { - return coro.promise().exception; - } - - void set_name(std::string const &s) { - coro.promise().set_name(s); - } - - auto operator*() const { - return get(); - } - }; - - struct promise_type_base { - std::string name; - std::exception_ptr eptr; - sigslot::signal<> complete; - sigslot::signal exception; - bool started = false; - bool finished = false; - std::coroutine_handle<> awaiting; - std::shared_ptr track; - - promise_type_base() {} - promise_type_base(promise_type_base const &) = delete; - promise_type_base(promise_type_base &&) = delete; - template - promise_type_base(std::shared_ptr const & t) : track(t) {} - - void set_name(std::string const &s) { - name = s; - } - - auto final_suspend() noexcept { - finished = true; - complete(); - if (track) { - track->terminate(); - track = nullptr; - } - if (awaiting) ::sigslot::resume_switch(awaiting); - return std::suspend_always{}; - } - - auto initial_suspend() { - return std::suspend_always{}; - } - - void unhandled_exception() { - eptr = std::current_exception(); - if (track) { - track->exception(eptr); - track = nullptr; - } - exception(eptr); - } - - void throw_exception() const { - if (eptr) { - std::rethrow_exception(eptr); - } - } - - virtual ~promise_type_base() { - if (track) { - track->terminate(); - } - using namespace std::string_literals; - name = "** Destroyed **"s; - } - }; - - template - struct promise_type : public promise_type_base { - T value; - typedef std::coroutine_handle> handle_type; - - promise_type() : value() {} - - template - requires std::is_base_of_v - promise_type(std::shared_ptr const & t, Args&&...) : promise_type_base(t), value() {} - - auto get_return_object() { - return R{handle_type::from_promise(*this)}; - } - - auto return_value(T v) { - if (track) { - track->terminate(); - track = nullptr; - } - value = v; - return std::suspend_never{}; - } - - auto get() const { - throw_exception(); - return value; - } - }; - - template - struct promise_type : public promise_type_base { - typedef std::coroutine_handle> handle_type; - - template - requires std::is_base_of_v - promise_type(std::shared_ptr const & t, Args&&...) : promise_type_base(t) {} - - promise_type() {} - - auto get_return_object() { - return R{handle_type::from_promise(*this)}; - } - - auto return_void() { - if (track) { - track->terminate(); - track = nullptr; - } - return std::suspend_never{}; - } - - void get() const { - throw_exception(); - } - }; - } - - - template - struct tasklet : public internal::tasklet,T>>> { - using promise_type = internal::promise_type,T>; - using value_type = T; - }; -} - -#endif //SIGSLOT_TASKLET_H diff --git a/test/coroutine.cc b/test/coroutine.cc deleted file mode 100644 index 05ccee8..0000000 --- a/test/coroutine.cc +++ /dev/null @@ -1,128 +0,0 @@ -// -// Created by dave on 29/03/2024. -// - -#include -#include -#include - -namespace { - sigslot::tasklet trivial_task(int i) { - co_return i; - } - - struct trivial_flag { - bool flag; - trivial_flag(bool f) : flag(f) {} - trivial_flag(trivial_flag const &) = delete; - }; - struct trivial : sigslot::tracker { - trivial_flag & flag; - explicit trivial(trivial_flag & f) : flag(f) { - flag.flag = false; - } - void terminate() const override { - flag.flag = true; - } - }; - - sigslot::tasklet tracked_task(std::shared_ptr &&, int i) { - co_return i; - } - - sigslot::tasklet tracked_task2(std::shared_ptr triv, int i) { - EXPECT_FALSE(triv->flag.flag); - co_return i; - } - - sigslot::tasklet basic_task(sigslot::signal &signal) { - co_return co_await signal; - } - - sigslot::tasklet nested_task(int i) { - co_return co_await trivial_task(i); - } - - sigslot::tasklet exception_task(int i) { - if (i == 42) { - // Have to do this conditionally with a co_return otherwise it's not a coroutine. - throw std::runtime_error("Help"); - } - co_return i; - } -} - -TEST(Tasklet, Trivial) { - auto coro = trivial_task(42); - EXPECT_TRUE(coro.running()); - EXPECT_FALSE(coro.started()); - auto result = coro.get(); - EXPECT_FALSE(coro.running()); - EXPECT_TRUE(coro.started()); - EXPECT_EQ(result, 42); -} - -TEST(Tasklet, Basic) { - sigslot::signal signal; - - auto coro = basic_task(signal); - EXPECT_TRUE(coro.running()); - EXPECT_FALSE(coro.started()); - coro.start(); - EXPECT_TRUE(coro.running()); - EXPECT_TRUE(coro.started()); - signal(42); - auto result = coro.get(); - EXPECT_EQ(result, 42); -} - -TEST(Tasklet, Nested) { - auto coro = nested_task(42); - EXPECT_TRUE(coro.running()); - EXPECT_FALSE(coro.started()); - auto result = coro.get(); - EXPECT_FALSE(coro.running()); - EXPECT_TRUE(coro.started()); - EXPECT_EQ(result, 42); -} - - -TEST(Tasklet, Throw) { - auto coro = exception_task(42); - EXPECT_TRUE(coro.running()); - EXPECT_FALSE(coro.started()); - EXPECT_THROW( - auto result = coro.get(), - std::runtime_error - ); - EXPECT_FALSE(coro.running()); - EXPECT_TRUE(coro.started()); -} - -TEST(Tracker, Simple) { - trivial_flag flag = {true}; - EXPECT_TRUE(flag.flag); - auto coro = tracked_task(sigslot::track(flag), 42); - EXPECT_TRUE(coro.running()); - EXPECT_FALSE(flag.flag); - EXPECT_FALSE(coro.started()); - auto result = coro.get(); - EXPECT_FALSE(coro.running()); - EXPECT_TRUE(coro.started()); - EXPECT_TRUE(flag.flag); - EXPECT_EQ(result, 42); -} - -TEST(Tracker, Copy) { - trivial_flag flag = {true}; - EXPECT_TRUE(flag.flag); - auto coro = tracked_task2(sigslot::track(flag), 42); - EXPECT_TRUE(coro.running()); - EXPECT_FALSE(flag.flag); - EXPECT_FALSE(coro.started()); - auto result = coro.get(); - EXPECT_FALSE(coro.running()); - EXPECT_TRUE(coro.started()); - EXPECT_TRUE(flag.flag); - EXPECT_EQ(result, 42); -} \ No newline at end of file diff --git a/test/cothread.cc b/test/cothread.cc deleted file mode 100644 index 8b2a3c2..0000000 --- a/test/cothread.cc +++ /dev/null @@ -1,174 +0,0 @@ -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -// Tiny event loop. co_thread won't work properly without, -// since it's got to (essentially) block while the thread runs. - -std::mutex lock_me; -std::vector> resume_me; - - -namespace sigslot { - void resume(std::coroutine_handle<> coro) { - std::lock_guard l(lock_me); - resume_me.push_back(coro); - } -} - -#include -#include -#include - -template -void run_until_complete_low(sigslot::tasklet & coro) { - if (!coro.started()) coro.start(); - while (coro.running()) { - std::vector> current; - { - std::lock_guard l(lock_me); - current.swap(resume_me); - } - std::cout << "Resuming " << current.size() << " coroutines." << std::endl; - for (auto coro : current) { - coro.resume(); - } - current.clear(); - sleep(1); - std::cout << "... tick" << std::endl; - } -} -template -R run_until_complete(sigslot::tasklet & coro) { - run_until_complete_low(coro); - return coro.get(); -} -template<> -void run_until_complete(sigslot::tasklet & coro) { - run_until_complete_low(coro); - coro.get(); -} - - -sigslot::tasklet inner(std::string const & s) { - std::cout << "Here!" << std::endl; - sigslot::co_thread thread1([](std::string const &s) { - std::cout << "There 1! " << s << std::endl; - return true; - }); - sigslot::co_thread thread2([]() { - std::cout << "+ Launch" << std::endl; - sleep(1); - std::cout << "+ There 2!" << std::endl; - sleep(1); - std::cout << "+ End" << std::endl; - return true; - }); - std::cout << "Still here!" << std::endl; - auto thread2_await = thread2(); - auto result1 = co_await thread1(s); - std::cout << "Got result1:" << result1 << std::endl; - auto result2 = co_await thread2_await; - std::cout << "Got result2:" << result2 << std::endl; - co_return true; -} - -sigslot::tasklet start() { - std::string s = "Hello world!"; - auto result = co_await inner(s); - std::cout << "Completed test with result " << result << std::endl; -} - -namespace { - sigslot::tasklet trivial_task(int i) { - co_return i; - } - - sigslot::tasklet basic_task(sigslot::signal &signal) { - co_return co_await signal; - } - - sigslot::tasklet signal_thread_task() { - sigslot::signal signal; - sigslot::co_thread thread([&signal]() { - sleep(1); - signal(42); - sleep(1); - return 42; - }); - auto thread_result = thread(); - auto result = co_await signal; - co_await thread_result; - co_return result; - } - - sigslot::tasklet nested_task(int i) { - co_return co_await trivial_task(i); - } - - sigslot::tasklet exception_task(int i) { - if (i == 42) { - // Have to do this conditionally with a co_return otherwise it's not a coroutine. - throw std::runtime_error("Help"); - } - co_return i; - } - - sigslot::tasklet thread_exception_task() { - sigslot::co_thread t([]{throw std::runtime_error("Potato!");}); - co_await t(); - } -} - -TEST(CoThreadTest, CheckLoop) { - auto coro = trivial_task(42); - auto result = run_until_complete(coro); - EXPECT_EQ(result, 42); -} - -TEST(CoThreadTest, CheckLoop2) { - sigslot::signal signal; - auto coro = basic_task(signal); - coro.start(); - int i = 0; - while (coro.running()) { - std::vector> current; - { - std::lock_guard l(lock_me); - current.swap(resume_me); - } - std::cout << "Resuming " << current.size() << " coroutines." << std::endl; - for (auto coro : current) { - coro.resume(); - } - current.clear(); - sleep(1); - if (i == 2) { - std::cout << "Signalling" << std::endl; - signal(42); - } - ++i; - std::cout << "... tick" << std::endl; - } - auto result = coro.get(); - std::cout << "Result: " << result << std::endl; -} - -TEST(CoThreadTest, Tests) { - std::cout << "Start" << std::endl; - auto coro = start(); - run_until_complete(coro); - std::cout << "*** END ***" << std::endl; -} - -TEST(CoThreadTest, Exception) { - std::cout << "Start" << std::endl; - auto coro = thread_exception_task(); - EXPECT_THROW( - run_until_complete(coro), - std::runtime_error - ); - std::cout << "*** END ***" << std::endl; -} diff --git a/test/main.cpp b/test/main.cpp new file mode 100644 index 0000000..4be79d1 --- /dev/null +++ b/test/main.cpp @@ -0,0 +1,148 @@ +// +// Created by dwd on 8/24/24. +// +// +// Created by dave on 30/07/2024. +// + +#include "gtest/gtest.h" +#ifdef DWD_GTEST_SENTRY +#include + +class EventListener : public ::testing::TestEventListener { + sentry_transaction_context_t *tx_ctx = nullptr; + sentry_transaction_t *tx = nullptr; + sentry_span_t *main_span = nullptr; + sentry_span_t *suite_span = nullptr; + sentry_span_t *test_span = nullptr; + std::string const & m_progname; +public: + EventListener(std::string const & progname) : m_progname(progname) {} + ~EventListener() override = default; + + // Override this to define how to set up the environment. + void OnTestProgramStart(const ::testing::UnitTest & u) override { + sentry_options_t *options = sentry_options_new(); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_init(options); + } + void OnTestProgramEnd(const ::testing::UnitTest &) override { + sentry_shutdown(); + } + + void OnTestStart(::testing::TestInfo const & test_info) override { + const char * testName = test_info.name(); + std::string tname = test_info.test_suite_name(); + tname += "."; + tname += testName; + test_span = sentry_span_start_child( + suite_span, + "test", + tname.c_str() + ); + } + + // Override this to define how to tear down the environment. + void OnTestEnd(const ::testing::TestInfo & ti) override { + if (ti.result()->Failed()) { + sentry_span_set_status(test_span, sentry_span_status_t::SENTRY_SPAN_STATUS_INTERNAL_ERROR); + } + sentry_span_finish(test_span); // Mark the span as finished + } + + void OnTestIterationStart(const testing::UnitTest &unit_test, int iteration) override { + tx_ctx = sentry_transaction_context_new( + m_progname.c_str(), + "googletest" + ); + tx = sentry_transaction_start(tx_ctx, sentry_value_new_null()); + main_span = sentry_transaction_start_child( + tx, + "googletest", + m_progname.c_str() + ); + } + + void OnEnvironmentsSetUpStart(const testing::UnitTest &unit_test) override { + + } + + void OnEnvironmentsSetUpEnd(const testing::UnitTest &unit_test) override { + + } + + void OnTestSuiteStart(const testing::TestSuite &suite) override { + suite_span = sentry_span_start_child( + main_span, + "test.suite", + suite.name() + ); + TestEventListener::OnTestSuiteStart(suite); + } + + void OnTestCaseStart(const testing::TestCase &aCase) override { + TestEventListener::OnTestCaseStart(aCase); + } + + void OnTestDisabled(const testing::TestInfo &info) override { + TestEventListener::OnTestDisabled(info); + } + + void OnTestPartResult(const testing::TestPartResult &test_part_result) override { + sentry_set_span(test_span); + auto val = sentry_value_new_breadcrumb("test", test_part_result.message()); + sentry_add_breadcrumb(val); + if (test_part_result.failed()) { + auto ev = sentry_value_new_event(); + auto exc = sentry_value_new_exception("GoogleTest", test_part_result.message()); + sentry_value_set_stacktrace(exc, nullptr, 0); + sentry_event_add_exception(ev, exc); + sentry_capture_event(ev); + } + } + + void OnTestSuiteEnd(const testing::TestSuite &suite) override { + TestEventListener::OnTestSuiteEnd(suite); + if (suite.failed_test_count() > 0) { + sentry_span_set_status(suite_span, sentry_span_status_t::SENTRY_SPAN_STATUS_INTERNAL_ERROR); + } + sentry_span_finish(suite_span); // Mark the span as finished + } + + void OnTestCaseEnd(const testing::TestCase &aCase) override { + TestEventListener::OnTestCaseEnd(aCase); + } + + void OnEnvironmentsTearDownStart(const testing::UnitTest &unit_test) override { + + } + + void OnEnvironmentsTearDownEnd(const testing::UnitTest &unit_test) override { + + } + + void OnTestIterationEnd(const testing::UnitTest &unit_test, int iteration) override { + if (unit_test.failed_test_count() > 0) { + sentry_span_set_status(main_span, sentry_span_status_t::SENTRY_SPAN_STATUS_INTERNAL_ERROR); + sentry_transaction_set_status(tx, sentry_span_status_t::SENTRY_SPAN_STATUS_INTERNAL_ERROR); + } + sentry_span_finish(main_span); // Mark the span as finished + sentry_transaction_finish(tx); + } +}; +#endif + +int main(int argc, char ** argv) { + std::string progname(argv[0]); + auto slash = progname.find_last_of("/\\"); + if (slash != std::string::npos) { + progname = progname.substr(slash + 1); + } + ::testing::InitGoogleTest(&argc, argv); + auto & listeners = ::testing::UnitTest::GetInstance()->listeners(); +#ifdef DWD_GTEST_SENTRY + listeners.Append(new EventListener(progname)); +#endif + auto ret = RUN_ALL_TESTS(); + return ret; +} diff --git a/test/resume.cc b/test/resume.cc deleted file mode 100644 index 1774f9c..0000000 --- a/test/resume.cc +++ /dev/null @@ -1,58 +0,0 @@ -// -// Created by dave on 29/03/2024. -// - -#include - -int resumptions = 0; - -namespace sigslot { - static inline void resume(std::coroutine_handle<> coro) { - ++resumptions; - coro.resume(); - } -} -#include -#include -#include - - -namespace { - sigslot::tasklet trivial_task(int i) { - co_return i; - } - - sigslot::tasklet basic_task(sigslot::signal &signal) { - co_return co_await signal; - } -} - -TEST(Resume, Trivial) { - EXPECT_EQ(resumptions, 0); - auto coro = trivial_task(42); - EXPECT_TRUE(coro.running()); - EXPECT_FALSE(coro.started()); - auto result = coro.get(); - EXPECT_FALSE(coro.running()); - EXPECT_TRUE(coro.started()); - EXPECT_EQ(result, 42); - EXPECT_EQ(resumptions, 0); - resumptions = 0; -} - -TEST(Resume, Basic) { - EXPECT_EQ(resumptions, 0); - sigslot::signal signal; - - auto coro = basic_task(signal); - EXPECT_TRUE(coro.running()); - EXPECT_FALSE(coro.started()); - coro.start(); - EXPECT_TRUE(coro.running()); - EXPECT_TRUE(coro.started()); - signal(42); - auto result = coro.get(); - EXPECT_EQ(result, 42); - EXPECT_EQ(resumptions, 1); - resumptions = 0; -}