diff --git a/.devcontainer/Containerfile b/.devcontainer/Containerfile index 622f1fd2b..cc2b2f8ca 100644 --- a/.devcontainer/Containerfile +++ b/.devcontainer/Containerfile @@ -68,6 +68,7 @@ RUN DEBIAN_FRONTEND="noninteractive" sudo apt-get update && \ libjansson-dev \ libxml2-dev \ libzip-dev \ + libuv1-dev \ rapidjson-dev \ uuid-dev && \ sudo apt-get clean diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index eceb860b5..f8279ae81 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -80,7 +80,7 @@ jobs: uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c #v3.3.0 - name: Install dependencies run: | - brew install lcov jansson rapidjson libzip ccache ninja openssl@1.1 google-benchmark + brew install lcov jansson rapidjson libzip ccache ninja openssl@1.1 google-benchmark libuv - name: Prepare ccache timestamp id: ccache_cache_timestamp run: | diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index dc0964f4a..f2a0e5147 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -123,6 +123,7 @@ jobs: libjansson-dev \ libcurl4-openssl-dev \ libbenchmark-dev \ + libuv1-dev \ default-jdk \ cmake \ libffi-dev \ diff --git a/cmake/Modules/Findlibuv.cmake b/cmake/Modules/Findlibuv.cmake new file mode 100644 index 000000000..4c71e79f6 --- /dev/null +++ b/cmake/Modules/Findlibuv.cmake @@ -0,0 +1,61 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +# - Try to find libuv +# Once done this will define +# libuv_FOUND - System has libuv +# LIBUV_INCLUDE_DIR - The libuv include directory +# LIBUV_LIBRARY - The libuv library +# uv - Imported target for libuv + +find_package(libuv CONFIG QUIET) + +if (NOT libuv_FOUND) + find_package(PkgConfig QUIET) + if (PkgConfig_FOUND) + pkg_check_modules(LIBUV QUIET libuv) + endif () +endif () + +if (NOT libuv_FOUND) + find_path(LIBUV_INCLUDE_DIR + NAMES uv.h + HINTS ${LIBUV_INCLUDEDIR} ${LIBUV_INCLUDE_DIRS} + ) + + find_library(LIBUV_LIBRARY + NAMES uv libuv + HINTS ${LIBUV_LIBDIR} ${LIBUV_LIBRARY_DIRS} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(libuv DEFAULT_MSG LIBUV_LIBRARY LIBUV_INCLUDE_DIR) + mark_as_advanced(LIBUV_INCLUDE_DIR LIBUV_LIBRARY) +endif () + +if (libuv_FOUND AND NOT TARGET libuv::uv AND TARGET libuv::libuv) + #Note: libuv cmake config possible defines libuv::libuv target + # and conan libuv package defines uv target + # so create an alias target uv for consistency + add_library(libuv::uv ALIAS libuv::libuv) +elseif (libuv_FOUND AND NOT TARGET libuv::uv AND LIBUV_LIBRARY AND LIBUV_INCLUDE_DIR) + add_library(libuv::uv SHARED IMPORTED) + set_target_properties(libuv::uv PROPERTIES + IMPORTED_LOCATION "${LIBUV_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${LIBUV_INCLUDE_DIR}" + ) +endif () diff --git a/conanfile.py b/conanfile.py index bb1c63907..f5c5c6ecd 100644 --- a/conanfile.py +++ b/conanfile.py @@ -309,6 +309,7 @@ def configure(self): # https://github.com/conan-io/conan/issues/14528#issuecomment-1685344080 if self.options.build_utils: self.options['libzip'].shared = True + self.options['libuv'].shared = True if self.options.build_framework: self.options['util-linux-libuuid'].shared = True if ((self.options.build_framework and self.options.framework_curlinit) @@ -340,6 +341,7 @@ def configure(self): def requirements(self): if self.options.build_utils: self.requires("libzip/[>=1.7.3 <2.0.0]") + self.requires("libuv/[>=1.49.2 <2.0.0]") if self.options.build_framework: self.requires("util-linux-libuuid/[>=2.39 <3.0.0]") if self.settings.os == "Macos": diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt index e28317b49..bbb7474a1 100644 --- a/libs/utils/CMakeLists.txt +++ b/libs/utils/CMakeLists.txt @@ -18,7 +18,13 @@ celix_subproject(UTILS "Option to enable building the Utilities library" ON) if (UTILS) find_package(libzip REQUIRED) - find_package(jansson REQUIRED) #TODO add jansson dep info to build (conan) and documentation info + find_package(jansson REQUIRED) + find_package(libuv REQUIRED) + + if (NOT TARGET libuv::uv AND TARGET uv) + #Note: conan libuv package 1.49.2 defines uv target, but 1.51.0 defines libuv::uv target + add_library(libuv::uv ALIAS uv) + endif () set(MEMSTREAM_SOURCES ) set(MEMSTREAM_INCLUDES ) @@ -46,7 +52,7 @@ if (UTILS) ${MEMSTREAM_SOURCES} ) set(UTILS_PRIVATE_DEPS libzip::zip jansson::jansson) - set(UTILS_PUBLIC_DEPS) + set(UTILS_PUBLIC_DEPS libuv::uv) add_library(utils SHARED ${UTILS_SRC}) diff --git a/libs/utils/gtest/CMakeLists.txt b/libs/utils/gtest/CMakeLists.txt index 4a25f7d41..84fe3a4c2 100644 --- a/libs/utils/gtest/CMakeLists.txt +++ b/libs/utils/gtest/CMakeLists.txt @@ -35,6 +35,7 @@ add_executable(test_utils src/VersionTestSuite.cc src/ErrTestSuite.cc src/ThreadsTestSuite.cc + src/UvThreadsTestSuite.cc src/CelixErrnoTestSuite.cc src/CelixUtilsAutoCleanupTestSuite.cc src/ArrayListTestSuite.cc @@ -42,7 +43,7 @@ add_executable(test_utils src/CxxExceptionsTestSuite.cc ) -target_link_libraries(test_utils PRIVATE utils_cut Celix::utils GTest::gtest GTest::gtest_main libzip::zip) +target_link_libraries(test_utils PRIVATE utils_cut Celix::utils GTest::gtest GTest::gtest_main libzip::zip libuv::uv) target_include_directories(test_utils PRIVATE ../src) #for version_private (needs refactoring of test) celix_deprecated_utils_headers(test_utils) diff --git a/libs/utils/gtest/src/UvThreadsTestSuite.cc b/libs/utils/gtest/src/UvThreadsTestSuite.cc new file mode 100644 index 000000000..c7dd9bf2d --- /dev/null +++ b/libs/utils/gtest/src/UvThreadsTestSuite.cc @@ -0,0 +1,139 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#include "celix_uv_cleanup.h" + +#include +#include + +class UvThreadsTestSuite : public ::testing::Test { +}; + +static void uvThreadIncrement(void* data) { + auto* counter = static_cast*>(data); + counter->fetch_add(1); +} + +TEST_F(UvThreadsTestSuite, ThreadAutoCleanupTest) { + std::atomic counter{0}; + { + celix_auto(uv_thread_t) thread; + ASSERT_EQ(0, uv_thread_create(&thread, uvThreadIncrement, &counter)); + } //thread out of scope -> join + EXPECT_EQ(1, counter.load()); +} + +static void uvThreadTryLockForMutex(void* data) { + auto* mutex = static_cast(data); + EXPECT_NE(0, uv_mutex_trylock(mutex)); +} + +TEST_F(UvThreadsTestSuite, MutexGuardTest) { + uv_mutex_t mutex; + ASSERT_EQ(0, uv_mutex_init(&mutex)); + celix_autoptr(uv_mutex_t) mutexCleanup = &mutex; + + { + celix_auto(celix_uv_mutex_lock_guard_t) guard = celix_uvMutexLockGuard_init(&mutex); + uv_thread_t thread; + ASSERT_EQ(0, uv_thread_create(&thread, uvThreadTryLockForMutex, &mutex)); + EXPECT_EQ(0, uv_thread_join(&thread)); + } //guard out of scope -> unlock + + ASSERT_EQ(0, uv_mutex_trylock(&mutex)); + uv_mutex_unlock(&mutex); +} + +TEST_F(UvThreadsTestSuite, MutexStealTest) { + uv_mutex_t mutex; + ASSERT_EQ(0, uv_mutex_init(&mutex)); + celix_autoptr(uv_mutex_t) mutexCleanup = &mutex; + celix_steal_ptr(mutexCleanup); + uv_mutex_destroy(&mutex); +} + +static void uvThreadTryLocksForWriteLock(void* data) { + auto* lock = static_cast(data); + EXPECT_NE(0, uv_rwlock_tryrdlock(lock)); + EXPECT_NE(0, uv_rwlock_trywrlock(lock)); +} + +static void uvThreadTryLocksForReadLock(void* data) { + auto* lock = static_cast(data); + EXPECT_NE(0, uv_rwlock_trywrlock(lock)); + EXPECT_EQ(0, uv_rwlock_tryrdlock(lock)); //additional read lock on read lock should succeed + uv_rwlock_rdunlock(lock); +} + +TEST_F(UvThreadsTestSuite, RwlockGuardTest) { + uv_rwlock_t lock; + ASSERT_EQ(0, uv_rwlock_init(&lock)); + celix_autoptr(uv_rwlock_t) lockCleanup = &lock; + + { + celix_auto(celix_uv_write_lock_guard_t) writeGuard = celix_uvWriteLockGuard_init(&lock); + uv_thread_t thread; + ASSERT_EQ(0, uv_thread_create(&thread, uvThreadTryLocksForWriteLock, &lock)); + EXPECT_EQ(0, uv_thread_join(&thread)); + } //guard out of scope -> unlock write lock + + { + celix_auto(celix_uv_read_lock_guard_t) readGuard = celix_uvReadLockGuard_init(&lock); + uv_thread_t thread; + ASSERT_EQ(0, uv_thread_create(&thread, uvThreadTryLocksForReadLock, &lock)); + EXPECT_EQ(0, uv_thread_join(&thread)); + } //guard out of scope -> unlock read lock + + ASSERT_EQ(0, uv_rwlock_trywrlock(&lock)); + uv_rwlock_wrunlock(&lock); +} + +TEST_F(UvThreadsTestSuite, RwlockStealdTest) { + uv_rwlock_t lock; + ASSERT_EQ(0, uv_rwlock_init(&lock)); + celix_autoptr(uv_rwlock_t) lockCleanup = &lock; + celix_steal_ptr(lockCleanup); + uv_rwlock_destroy(&lock); +} + +TEST_F(UvThreadsTestSuite, ConditionAutoCleanupTest) { + uv_cond_t cond; + ASSERT_EQ(0, uv_cond_init(&cond)); + celix_autoptr(uv_cond_t) condCleanup = &cond; +} + +TEST_F(UvThreadsTestSuite, ConditionStealTest) { + uv_cond_t cond; + ASSERT_EQ(0, uv_cond_init(&cond)); + celix_autoptr(uv_cond_t) condCleanup = &cond; + celix_steal_ptr(condCleanup); + uv_cond_destroy(&cond); +} + +TEST_F(UvThreadsTestSuite, LocalThreadStorageKeyAutoCleanupTest) { + uv_key_t key; + ASSERT_EQ(0, uv_key_create(&key)); + celix_autoptr(uv_key_t) keyCleanup = &key; +} + +TEST_F(UvThreadsTestSuite, LocalThreadStorageKeyStealTest) { + uv_key_t key; + ASSERT_EQ(0, uv_key_create(&key)); + celix_autoptr(uv_key_t) keyCleanup = &key; + celix_steal_ptr(keyCleanup); + uv_key_delete(&key); +} diff --git a/libs/utils/include/celix_uv_cleanup.h b/libs/utils/include/celix_uv_cleanup.h new file mode 100644 index 000000000..d269666b7 --- /dev/null +++ b/libs/utils/include/celix_uv_cleanup.h @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +#ifndef CELIX_UV_CLEANUP_H +#define CELIX_UV_CLEANUP_H + +#include + +#include "celix_cleanup.h" + +#ifdef __cplusplus +extern "C" { +#endif + +CELIX_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(uv_thread_t, uv_thread_join) +CELIX_DEFINE_AUTOPTR_CLEANUP_FUNC(uv_mutex_t, uv_mutex_destroy) +CELIX_DEFINE_AUTOPTR_CLEANUP_FUNC(uv_rwlock_t, uv_rwlock_destroy) +CELIX_DEFINE_AUTOPTR_CLEANUP_FUNC(uv_cond_t, uv_cond_destroy) +CELIX_DEFINE_AUTOPTR_CLEANUP_FUNC(uv_key_t, uv_key_delete) + +/** + * @brief Lock guard for uv mutexes. + */ +typedef struct celix_uv_mutex_lock_guard { + uv_mutex_t* mutex; +} celix_uv_mutex_lock_guard_t; + +/** + * @brief Initialize a lock guard for a mutex. + * + * Lock a mutex and return a celix_uv_mutex_lock_guard_t. + * Unlock with celixUvMutexLockGuard_deinit(). Using uv_mutex_lock() on a mutex + * while a celix_uv_mutex_lock_guard_t exists can lead to undefined behaviour. + * + * No allocation is performed, it is equivalent to a uv_mutex_lock() call. + * This is intended to be used with celix_auto(). + * + * @param mutex A mutex to lock. + * @return An initialized lock guard to be used with celix_auto(). + */ +static CELIX_UNUSED inline celix_uv_mutex_lock_guard_t celix_uvMutexLockGuard_init(uv_mutex_t* mutex) { + celix_uv_mutex_lock_guard_t guard; + guard.mutex = mutex; + uv_mutex_lock(mutex); + return guard; +} + +/** + * @brief Deinitialize a lock guard for a mutex. + * + * Unlock the mutex of a guard. + * No memory is freed, it is equivalent to a uv_mutex_unlock() call. + * + * @param guard A celix_uv_mutex_lock_guard_t. + */ +static CELIX_UNUSED inline void celix_uvMutexLockGuard_deinit(celix_uv_mutex_lock_guard_t* guard) { + if (guard->mutex) { + uv_mutex_unlock(guard->mutex); + guard->mutex = NULL; + } +} + +CELIX_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(celix_uv_mutex_lock_guard_t, celix_uvMutexLockGuard_deinit) + +/** + * @brief A RAII style write lock guard for uv_rwlock_t. + * + * The lock is obtained in the constructor and released in the destructor. + * This is intended to be used with celix_auto(). + */ +typedef struct celix_uv_write_lock_guard { + uv_rwlock_t* lock; +} celix_uv_write_lock_guard_t; + +/** + * @brief Initialize a write lock guard for a lock. + * + * Obtain a write lock on a lock and return a celix_uv_write_lock_guard_t. + * Unlock with celixUvWriteLockGuard_deinit(). Using uv_rwlock_wrunlock() + * on lock while a celix_uv_write_lock_guard_t exists can lead to undefined behaviour. + * + * No allocation is performed, it is equivalent to a uv_rwlock_wrlock() call. + * This is intended to be used with celix_auto(). + * + * @param lock A read-write lock to lock. + * @return An initialized write lock guard to be used with celix_auto(). + */ +static CELIX_UNUSED inline celix_uv_write_lock_guard_t celix_uvWriteLockGuard_init(uv_rwlock_t* lock) { + celix_uv_write_lock_guard_t guard; + guard.lock = lock; + uv_rwlock_wrlock(lock); + return guard; +} + +/** + * @brief Deinitialize a write lock guard. + * + * Release a write lock on the read-write lock contained in a guard. + * See celix_uvWriteLockGuard_init() for details. + * No memory is freed, it is equivalent to a uv_rwlock_wrunlock() call. + * + * @param guard A celix_uv_write_lock_guard_t. + */ +static CELIX_UNUSED inline void celix_uvWriteLockGuard_deinit(celix_uv_write_lock_guard_t* guard) { + if (guard->lock) { + uv_rwlock_wrunlock(guard->lock); + guard->lock = NULL; + } +} + +CELIX_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(celix_uv_write_lock_guard_t, celix_uvWriteLockGuard_deinit) + +/** + * @brief A RAII style read lock guard for uv_rwlock_t. + * + * The lock is obtained in the constructor and released in the destructor. + * This is intended to be used with celix_auto(). + */ +typedef struct celix_uv_read_lock_guard { + uv_rwlock_t* lock; +} celix_uv_read_lock_guard_t; + +/** + * @brief Initialize a read lock guard for a lock. + * + * Obtain a read lock on a lock and return a celix_uv_read_lock_guard_t. + * Unlock with celix_uvReadLockGuard_deinit(). Using uv_rwlock_rdunlock() + * on lock while a celix_uv_read_lock_guard_t exists can lead to undefined behaviour. + * + * No allocation is performed, it is equivalent to a uv_rwlock_rdlock() call. + * This is intended to be used with celix_auto(). + * + * @param lock A read-write lock to lock. + * @return A guard to be used with celix_auto(). + */ +static CELIX_UNUSED inline celix_uv_read_lock_guard_t celix_uvReadLockGuard_init(uv_rwlock_t* lock) { + celix_uv_read_lock_guard_t guard; + guard.lock = lock; + uv_rwlock_rdlock(lock); + return guard; +} + +/** + * @brief Deinitialize a read lock guard. + * + * Release a read lock on the read-write lock contained in a guard. + * See celix_uvReadLockGuard_init() for details. + * No memory is freed, it is equivalent to a uv_rwlock_rdunlock() call. + * + * @param guard A celix_uv_read_lock_guard_t. + */ +static CELIX_UNUSED inline void celix_uvReadLockGuard_deinit(celix_uv_read_lock_guard_t* guard) { + if (guard->lock) { + uv_rwlock_rdunlock(guard->lock); + guard->lock = NULL; + } +} + +CELIX_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(celix_uv_read_lock_guard_t, celix_uvReadLockGuard_deinit) + +#ifdef __cplusplus +} +#endif + +#endif /* CELIX_UV_CLEANUP_H */