diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13942231..1c84830a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,6 +45,20 @@ jobs: run: scripts/local-build.sh shell: bash # Specify bash so we can reuse the build script on Windows (runs on Git bash) working-directory: ${{ github.workspace }} + cmake-windows-deinit-tests: + runs-on: windows-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + - name: Install ninja + run: choco install ninja + - name: Configure and build with deinit enabled + run: scripts/local-build.sh -DCPUINFO_ENABLE_DEINIT=ON + shell: bash + working-directory: ${{ github.workspace }} + - name: Run init-test + run: ctest --output-on-failure -R init-test + working-directory: ${{ github.workspace }}/build/local cmake-uwp: runs-on: windows-latest timeout-minutes: 40 diff --git a/CMakeLists.txt b/CMakeLists.txt index 072c9873..bf1476b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ OPTION(CPUINFO_BUILD_UNIT_TESTS "Build cpuinfo unit tests" ON) OPTION(CPUINFO_BUILD_MOCK_TESTS "Build cpuinfo mock tests" ON) OPTION(CPUINFO_BUILD_BENCHMARKS "Build cpuinfo micro-benchmarks" ON) OPTION(CPUINFO_BUILD_PKG_CONFIG "Build pkg-config manifest" ON) +OPTION(CPUINFO_ENABLE_DEINIT "Release cpuinfo global state on cpuinfo_deinitialize()" OFF) OPTION(USE_SYSTEM_LIBS "Use system libraries instead of downloading and building them" OFF) OPTION(USE_SYSTEM_GOOGLEBENCHMARK "Use system Google Benchmark library instead of downloading and building it" ${USE_SYSTEM_LIBS}) OPTION(USE_SYSTEM_GOOGLETEST "Use system Google Test library instead of downloading and building it" ${USE_SYSTEM_LIBS}) @@ -281,6 +282,8 @@ TARGET_INCLUDE_DIRECTORIES(cpuinfo BEFORE PUBLIC $") +TARGET_COMPILE_DEFINITIONS(cpuinfo PRIVATE "CPUINFO_ENABLE_DEINIT=$") +TARGET_COMPILE_DEFINITIONS(cpuinfo_internals PRIVATE "CPUINFO_ENABLE_DEINIT=$") IF(CPUINFO_LOG_LEVEL STREQUAL "default") # default logging level: error (subject to change) TARGET_COMPILE_DEFINITIONS(cpuinfo PRIVATE "CPUINFO_LOG_LEVEL=2") @@ -812,6 +815,7 @@ IF(CPUINFO_SUPPORTED_PLATFORM AND CPUINFO_BUILD_UNIT_TESTS) ADD_EXECUTABLE(init-test test/init.cc) CPUINFO_TARGET_ENABLE_CXX11(init-test) CPUINFO_TARGET_RUNTIME_LIBRARY(init-test) + TARGET_COMPILE_DEFINITIONS(init-test PRIVATE "CPUINFO_ENABLE_DEINIT=$") TARGET_LINK_LIBRARIES(init-test PRIVATE cpuinfo gtest gtest_main) ADD_TEST(NAME init-test COMMAND init-test) diff --git a/src/init.c b/src/init.c index e71b1867..483ee9d7 100644 --- a/src/init.c +++ b/src/init.c @@ -12,15 +12,18 @@ #include "TargetConditionals.h" #endif +#ifndef CPUINFO_ENABLE_DEINIT +#define CPUINFO_ENABLE_DEINIT 0 +#endif + +#if !CPUINFO_ENABLE_DEINIT + #if defined(_WIN32) || defined(__CYGWIN__) static INIT_ONCE init_guard = INIT_ONCE_STATIC_INIT; -static INIT_ONCE deinit_guard = INIT_ONCE_STATIC_INIT; #elif !defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__) static pthread_once_t init_guard = PTHREAD_ONCE_INIT; -static pthread_once_t deinit_guard = PTHREAD_ONCE_INIT; #else static bool init_guard = false; -static bool deinit_guard = false; #endif bool CPUINFO_ABI cpuinfo_initialize(void) { @@ -67,52 +70,105 @@ bool CPUINFO_ABI cpuinfo_initialize(void) { return cpuinfo_is_initialized; } -void CPUINFO_ABI cpuinfo_deinitialize(void) { - if (!cpuinfo_is_initialized) { - return; - } - cpuinfo_is_initialized = false; +void CPUINFO_ABI cpuinfo_deinitialize(void) {} + +#else /* CPUINFO_ENABLE_DEINIT */ + +#if defined(_WIN32) || defined(__CYGWIN__) +static SRWLOCK cpuinfo_lifecycle_lock = SRWLOCK_INIT; +#define CPUINFO_LIFECYCLE_LOCK() AcquireSRWLockExclusive(&cpuinfo_lifecycle_lock) +#define CPUINFO_LIFECYCLE_UNLOCK() ReleaseSRWLockExclusive(&cpuinfo_lifecycle_lock) +#elif !defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__) +static pthread_mutex_t cpuinfo_lifecycle_lock = PTHREAD_MUTEX_INITIALIZER; +#define CPUINFO_LIFECYCLE_LOCK() pthread_mutex_lock(&cpuinfo_lifecycle_lock) +#define CPUINFO_LIFECYCLE_UNLOCK() pthread_mutex_unlock(&cpuinfo_lifecycle_lock) +#else +/* Single-threaded build (e.g. Emscripten without pthreads): no lock needed. */ +#define CPUINFO_LIFECYCLE_LOCK() \ + do { \ + } while (0) +#define CPUINFO_LIFECYCLE_UNLOCK() \ + do { \ + } while (0) +#endif +static unsigned int cpuinfo_init_refcount = 0; + +bool CPUINFO_ABI cpuinfo_initialize(void) { + CPUINFO_LIFECYCLE_LOCK(); + if (cpuinfo_init_refcount == 0) { #if CPUINFO_ARCH_X86 || CPUINFO_ARCH_X86_64 #if defined(__MACH__) && defined(__APPLE__) - pthread_once(&deinit_guard, &cpuinfo_x86_mach_deinit); + cpuinfo_x86_mach_init(); #elif defined(__FreeBSD__) - pthread_once(&deinit_guard, &cpuinfo_x86_freebsd_deinit); + cpuinfo_x86_freebsd_init(); #elif defined(__linux__) - pthread_once(&deinit_guard, &cpuinfo_x86_linux_deinit); + cpuinfo_x86_linux_init(); #elif defined(_WIN32) || defined(__CYGWIN__) - InitOnceExecuteOnce(&deinit_guard, &cpuinfo_x86_windows_deinit, NULL, NULL); + cpuinfo_x86_windows_init(NULL, NULL, NULL); +#else + cpuinfo_log_error("operating system is not supported in cpuinfo"); #endif #elif CPUINFO_ARCH_ARM || CPUINFO_ARCH_ARM64 #if defined(__linux__) - pthread_once(&deinit_guard, &cpuinfo_arm_linux_deinit); + cpuinfo_arm_linux_init(); #elif defined(__MACH__) && defined(__APPLE__) - pthread_once(&deinit_guard, &cpuinfo_arm_mach_deinit); + cpuinfo_arm_mach_init(); #elif defined(_WIN32) - InitOnceExecuteOnce(&deinit_guard, &cpuinfo_arm_windows_deinit, NULL, NULL); + cpuinfo_arm_windows_init(NULL, NULL, NULL); +#else + cpuinfo_log_error("operating system is not supported in cpuinfo"); #endif #elif CPUINFO_ARCH_RISCV32 || CPUINFO_ARCH_RISCV64 #if defined(__linux__) - pthread_once(&deinit_guard, &cpuinfo_riscv_linux_deinit); + cpuinfo_riscv_linux_init(); +#else + cpuinfo_log_error("operating system is not supported in cpuinfo"); #endif #elif CPUINFO_ARCH_ASMJS || CPUINFO_ARCH_WASM || CPUINFO_ARCH_WASMSIMD -#if defined(__EMSCRIPTEN_PTHREADS__) - pthread_once(&deinit_guard, &cpuinfo_emscripten_deinit); + cpuinfo_emscripten_init(); #else - if (!deinit_guard) { - cpuinfo_emscripten_deinit(); + cpuinfo_log_error("processor architecture is not supported in cpuinfo"); +#endif } - deinit_guard = true; + if (cpuinfo_is_initialized) { + cpuinfo_init_refcount++; + } + const bool initialized = cpuinfo_is_initialized; + CPUINFO_LIFECYCLE_UNLOCK(); + return initialized; +} + +void CPUINFO_ABI cpuinfo_deinitialize(void) { + CPUINFO_LIFECYCLE_LOCK(); + if (cpuinfo_init_refcount != 0 && --cpuinfo_init_refcount == 0) { + cpuinfo_is_initialized = false; +#if CPUINFO_ARCH_X86 || CPUINFO_ARCH_X86_64 +#if defined(__MACH__) && defined(__APPLE__) + cpuinfo_x86_mach_deinit(); +#elif defined(__FreeBSD__) + cpuinfo_x86_freebsd_deinit(); +#elif defined(__linux__) + cpuinfo_x86_linux_deinit(); +#elif defined(_WIN32) || defined(__CYGWIN__) + cpuinfo_x86_windows_deinit(NULL, NULL, NULL); #endif +#elif CPUINFO_ARCH_ARM || CPUINFO_ARCH_ARM64 +#if defined(__linux__) + cpuinfo_arm_linux_deinit(); +#elif defined(__MACH__) && defined(__APPLE__) + cpuinfo_arm_mach_deinit(); +#elif defined(_WIN32) + cpuinfo_arm_windows_deinit(NULL, NULL, NULL); #endif - /* Reset guards to allow re-initialization */ -#if defined(_WIN32) || defined(__CYGWIN__) - init_guard = (INIT_ONCE)INIT_ONCE_STATIC_INIT; - deinit_guard = (INIT_ONCE)INIT_ONCE_STATIC_INIT; -#elif !defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__) - init_guard = (pthread_once_t)PTHREAD_ONCE_INIT; - deinit_guard = (pthread_once_t)PTHREAD_ONCE_INIT; -#else - init_guard = false; - deinit_guard = false; +#elif CPUINFO_ARCH_RISCV32 || CPUINFO_ARCH_RISCV64 +#if defined(__linux__) + cpuinfo_riscv_linux_deinit(); #endif +#elif CPUINFO_ARCH_ASMJS || CPUINFO_ARCH_WASM || CPUINFO_ARCH_WASMSIMD + cpuinfo_emscripten_deinit(); +#endif + } + CPUINFO_LIFECYCLE_UNLOCK(); } + +#endif /* CPUINFO_ENABLE_DEINIT */ diff --git a/test/init.cc b/test/init.cc index a6128e35..28692a56 100644 --- a/test/init.cc +++ b/test/init.cc @@ -2,6 +2,16 @@ #include +#ifndef CPUINFO_ENABLE_DEINIT +#define CPUINFO_ENABLE_DEINIT 0 +#endif + +#if CPUINFO_ENABLE_DEINIT +#include +#include +#include +#endif + TEST(PROCESSORS_COUNT, non_zero) { ASSERT_TRUE(cpuinfo_initialize()); EXPECT_NE(0, cpuinfo_get_processors_count()); @@ -1519,3 +1529,58 @@ TEST(L4_CACHE, consistent_processors) { } cpuinfo_deinitialize(); } + +#if CPUINFO_ENABLE_DEINIT +TEST(INIT_REFCOUNT, deinitialize_balances_initialize) { + ASSERT_TRUE(cpuinfo_initialize()); + ASSERT_TRUE(cpuinfo_initialize()); + + const uint32_t processors_count = cpuinfo_get_processors_count(); + EXPECT_NE(0, processors_count); + + cpuinfo_deinitialize(); + EXPECT_EQ(processors_count, cpuinfo_get_processors_count()); + EXPECT_TRUE(cpuinfo_get_processors()); + + cpuinfo_deinitialize(); +} + +TEST(INIT_STRESS, concurrent_deinitialize_does_not_disturb_other_consumers) { + constexpr int kChurnThreads = 10; + constexpr int kChurnIterations = 5000; + + std::atomic stop_holder{false}; + + // Models a long lived consumer that keeps using cpuinfo for its whole lifetime + // If a concurrent deinitialize from another consumer (churn) frees the shared state + // then cpuinfo aborts the process, which fails the test + const auto holder = [&stop_holder]() { + cpuinfo_initialize(); + while (!stop_holder.load(std::memory_order_relaxed)) { + (void)cpuinfo_get_processors(); + (void)cpuinfo_get_processor(0); + } + cpuinfo_deinitialize(); + }; + const auto churn = [kChurnIterations]() { + for (int iteration = 0; iteration < kChurnIterations; iteration++) { + cpuinfo_initialize(); + cpuinfo_deinitialize(); + } + }; + + std::thread holder_thread(holder); + std::vector churn_threads; + churn_threads.reserve(kChurnThreads); + for (int t = 0; t < kChurnThreads; t++) { + churn_threads.emplace_back(churn); + } + for (std::thread& thread : churn_threads) { + thread.join(); + } + stop_holder.store(true, std::memory_order_relaxed); + holder_thread.join(); + + // Reaching here without the process aborting means that lifecycle handling is correct +} +#endif // CPUINFO_ENABLE_DEINIT