diff --git a/dev/connection_holder.h b/dev/connection_holder.h index b51e8451..b9a0d84e 100644 --- a/dev/connection_holder.h +++ b/dev/connection_holder.h @@ -3,11 +3,14 @@ #include #ifndef SQLITE_ORM_IMPORT_STD_MODULE #include +#ifdef SQLITE_ORM_CPP20_SEMAPHORE_SUPPORTED +#include +#endif #include // std::function #include // std::string #endif -#include "functional/gsl.h" +#include "functional/cxx_new.h" #include "error_code.h" #include "vfs_name.h" #include "db_open_mode.h" @@ -16,6 +19,127 @@ namespace sqlite_orm { namespace internal { +#ifdef SQLITE_ORM_CPP20_SEMAPHORE_SUPPORTED + /** + * The connection holder should be performant in all variants: + 1. single-threaded use + 2. opened once (open forever) + 3. concurrent open/close + + Hence, a light-weight binary semaphore is used to synchronize opening and closing a database connection. + */ + struct connection_holder { + struct maybe_lock { + maybe_lock(std::binary_semaphore& sync, bool shouldLock) noexcept(noexcept(sync.acquire())) : + isSynced{shouldLock}, sync{sync} { + if (shouldLock) { + sync.acquire(); + } + } + + ~maybe_lock() { + if (isSynced) { + sync.release(); + } + } + + const bool isSynced; + std::binary_semaphore& sync; + }; + + connection_holder(std::string filename, + std::function didOpenDb, + const connection_control& options = {}) : + _didOpenDb{std::move(didOpenDb)}, filename(std::move(filename)), vfs_name(options.vfs_name), + open_mode(options.open_mode) {} + + connection_holder(const connection_holder&) = delete; + + connection_holder(const connection_holder& other, std::function didOpenDb) : + _didOpenDb{std::move(didOpenDb)}, filename{other.filename}, vfs_name(other.vfs_name), + open_mode{other.open_mode} {} + + void retain() { + const maybe_lock maybeLock{_sync, !_openedForeverHint}; + + // `maybeLock.isSynced`: the lock above already synchronized everything, so we can just atomically increment the counter + // `!maybeLock.isSynced`: we presume that the connection is opened once in a single-threaded context [also open forever]. + // therefore we can just use an atomic increment but don't need sequencing due to `prevCount > 0`. + if (int prevCount = _retainCount.fetch_add(1, std::memory_order_relaxed); prevCount > 0) { + return; + } + + // first one opens and sets up the connection. + + int open_flags = internal::db_open_mode_to_int_flags(this->open_mode); +#if SQLITE_VERSION_NUMBER >= 3037002 + open_flags |= SQLITE_OPEN_EXRESCODE; +#endif + + if (int rc = sqlite3_open_v2(this->filename.c_str(), &this->db, open_flags, this->vfs_name.c_str()); + rc != SQLITE_OK) [[unlikely]] /*possible, but unexpected*/ { + throw_translated_sqlite_error(this->db); + } + + if (_didOpenDb) { + _didOpenDb(this->db); + } + } + + void release() { + const maybe_lock maybeLock{_sync, !_openedForeverHint}; + + if (int prevCount = _retainCount.fetch_sub( + 1, + maybeLock.isSynced + // the lock above already synchronized everything, so we can just atomically decrement the counter + ? std::memory_order_relaxed + // the counter must serve as a synchronization point + : std::memory_order_acq_rel); + prevCount > 1) { + return; + } + + // last one closes the connection. + + if (int rc = sqlite3_close_v2(this->db); rc != SQLITE_OK) [[unlikely]] { + throw_translated_sqlite_error(this->db); + } else { + this->db = nullptr; + } + } + + sqlite3* get() const { + // note: ensuring a valid DB handle was already memory ordered with `retain()` + return this->db; + } + + /** + * @attention While retrieving the reference count value is atomic it makes only sense at single-threaded points in code. + */ + int retain_count() const { + return _retainCount.load(std::memory_order_relaxed); + } + + protected: + SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(alignas(polyfill::hardware_destructive_interference_size)) + orm_gsl::owner db = nullptr; + + private: + std::atomic_int _retainCount{}; + const bool _openedForeverHint = false; + std::binary_semaphore _sync{1}; + + private: + SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(alignas(polyfill::hardware_destructive_interference_size)) + const std::function _didOpenDb; + + public: + const std::string filename; + const std::string vfs_name; + const db_open_mode open_mode; + }; +#else struct connection_holder { connection_holder(std::string filename, std::function didOpenDb, @@ -78,12 +202,14 @@ namespace sqlite_orm { } protected: + SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(alignas(polyfill::hardware_destructive_interference_size)) orm_gsl::owner db = nullptr; private: std::atomic_int _retainCount{}; private: + SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(alignas(polyfill::hardware_destructive_interference_size)) const std::function _didOpenDb; public: @@ -91,6 +217,7 @@ namespace sqlite_orm { const std::string vfs_name; const db_open_mode open_mode; }; +#endif struct connection_ref { connection_ref(connection_holder& holder) : holder(&holder) { diff --git a/dev/functional/config.h b/dev/functional/config.h index 170e6120..1d0866d0 100644 --- a/dev/functional/config.h +++ b/dev/functional/config.h @@ -47,6 +47,10 @@ #define SQLITE_ORM_CPP20_RANGES_SUPPORTED #endif +#if __cpp_lib_semaphore >= 201907L +#define SQLITE_ORM_CPP20_SEMAPHORE_SUPPORTED +#endif + #ifdef SQLITE_ORM_STATIC_CALL_OPERATOR_SUPPORTED #define SQLITE_ORM_STATIC_CALLOP static #define SQLITE_ORM_OR_CONST_CALLOP diff --git a/dev/functional/cxx_compiler_quirks.h b/dev/functional/cxx_compiler_quirks.h index e666ad36..14e0b05d 100644 --- a/dev/functional/cxx_compiler_quirks.h +++ b/dev/functional/cxx_compiler_quirks.h @@ -20,6 +20,16 @@ #define SQLITE_ORM_CLANG_SUPPRESS(warnoption, ...) __VA_ARGS__ #endif +#if defined(_MSC_VER) && !defined(__clang__) +#define SQLITE_ORM_DO_PRAGMA(...) __pragma(__VA_ARGS__) +#endif + +#if defined(_MSC_VER) && !defined(__clang__) +#define SQLITE_ORM_MSVC_SUPPRESS(warncode, ...) SQLITE_ORM_DO_PRAGMA(warning(suppress : warncode)) +#else +#define SQLITE_ORM_MSVC_SUPPRESS(warcode, ...) __VA_ARGS__ +#endif + // clang has the bad habit of diagnosing missing brace-init-lists when constructing aggregates with base classes. // This is a false positive, since the C++ standard is quite clear that braces for nested or base objects may be omitted, // see https://en.cppreference.com/w/cpp/language/aggregate_initialization: @@ -30,6 +40,9 @@ // Because we know what we are doing, we suppress the diagnostic message #define SQLITE_ORM_CLANG_SUPPRESS_MISSING_BRACES(...) SQLITE_ORM_CLANG_SUPPRESS("-Wmissing-braces", __VA_ARGS__) +// msvc has the bad habit of diagnosing overalignment of types with an explicit alignment specifier. +#define SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(...) SQLITE_ORM_MSVC_SUPPRESS(4324, __VA_ARGS__) + #if defined(_MSC_VER) && (_MSC_VER < 1920) #define SQLITE_ORM_BROKEN_VARIADIC_PACK_EXPANSION // Type replacement may fail if an alias template has a non-type template parameter from a dependent expression in it, diff --git a/dev/functional/cxx_new.h b/dev/functional/cxx_new.h new file mode 100644 index 00000000..c360d9c9 --- /dev/null +++ b/dev/functional/cxx_new.h @@ -0,0 +1,23 @@ +#pragma once + +#ifdef SQLITE_ORM_IMPORT_STD_MODULE +#include +#else +#include +#endif + +namespace sqlite_orm { + namespace internal { + namespace polyfill { +#if __cpp_lib_hardware_interference_size >= 201703L + using std::hardware_constructive_interference_size; + using std::hardware_destructive_interference_size; +#else + constexpr size_t hardware_constructive_interference_size = 64; + constexpr size_t hardware_destructive_interference_size = 64; +#endif + } + } + + namespace polyfill = internal::polyfill; +} diff --git a/dev/storage.h b/dev/storage.h index 4129baef..5023ea80 100644 --- a/dev/storage.h +++ b/dev/storage.h @@ -1783,7 +1783,7 @@ namespace sqlite_orm { if (this->executor.did_run_query) { this->executor.did_run_query(sql); } - switch (stepRes) { + switch (rc) { case SQLITE_ROW: { T res; object_from_column_builder builder{res, stmt}; @@ -1954,9 +1954,6 @@ SQLITE_ORM_EXPORT namespace sqlite_orm { create_from_tuple(std::move(specTuple), opt_index_sequence{})); } #else - /* - * Factory function for a storage instance, from a database file and a bunch of database object definitions. - */ template internal::storage_t make_storage(std::string filename, DBO... dbObjects) { return {std::move(filename), {std::forward(dbObjects)...}, std::tuple<>{}}; diff --git a/include/sqlite_orm/sqlite_orm.h b/include/sqlite_orm/sqlite_orm.h index 0020f47c..e7be7aab 100644 --- a/include/sqlite_orm/sqlite_orm.h +++ b/include/sqlite_orm/sqlite_orm.h @@ -150,6 +150,16 @@ using std::nullptr_t; #define SQLITE_ORM_CLANG_SUPPRESS(warnoption, ...) __VA_ARGS__ #endif +#if defined(_MSC_VER) && !defined(__clang__) +#define SQLITE_ORM_DO_PRAGMA(...) __pragma(__VA_ARGS__) +#endif + +#if defined(_MSC_VER) && !defined(__clang__) +#define SQLITE_ORM_MSVC_SUPPRESS(warncode, ...) SQLITE_ORM_DO_PRAGMA(warning(suppress : warncode)) +#else +#define SQLITE_ORM_MSVC_SUPPRESS(warcode, ...) __VA_ARGS__ +#endif + // clang has the bad habit of diagnosing missing brace-init-lists when constructing aggregates with base classes. // This is a false positive, since the C++ standard is quite clear that braces for nested or base objects may be omitted, // see https://en.cppreference.com/w/cpp/language/aggregate_initialization: @@ -160,6 +170,9 @@ using std::nullptr_t; // Because we know what we are doing, we suppress the diagnostic message #define SQLITE_ORM_CLANG_SUPPRESS_MISSING_BRACES(...) SQLITE_ORM_CLANG_SUPPRESS("-Wmissing-braces", __VA_ARGS__) +// msvc has the bad habit of diagnosing overalignment of types with an explicit alignment specifier. +#define SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(...) SQLITE_ORM_MSVC_SUPPRESS(4324, __VA_ARGS__) + #if defined(_MSC_VER) && (_MSC_VER < 1920) #define SQLITE_ORM_BROKEN_VARIADIC_PACK_EXPANSION // Type replacement may fail if an alias template has a non-type template parameter from a dependent expression in it, @@ -274,6 +287,10 @@ using std::nullptr_t; #define SQLITE_ORM_CPP20_RANGES_SUPPORTED #endif +#if __cpp_lib_semaphore >= 201907L +#define SQLITE_ORM_CPP20_SEMAPHORE_SUPPORTED +#endif + #ifdef SQLITE_ORM_STATIC_CALL_OPERATOR_SUPPORTED #define SQLITE_ORM_STATIC_CALLOP static #define SQLITE_ORM_OR_CONST_CALLOP @@ -14115,11 +14132,36 @@ namespace sqlite_orm { #include #ifndef SQLITE_ORM_IMPORT_STD_MODULE #include +#ifdef SQLITE_ORM_CPP20_SEMAPHORE_SUPPORTED +#include +#endif #include // std::function #include // std::string #endif -// #include "functional/gsl.h" +// #include "functional/cxx_new.h" + +#ifdef SQLITE_ORM_IMPORT_STD_MODULE +#include +#else +#include +#endif + +namespace sqlite_orm { + namespace internal { + namespace polyfill { +#if __cpp_lib_hardware_interference_size >= 201703L + using std::hardware_constructive_interference_size; + using std::hardware_destructive_interference_size; +#else + constexpr size_t hardware_constructive_interference_size = 64; + constexpr size_t hardware_destructive_interference_size = 64; +#endif + } + } + + namespace polyfill = internal::polyfill; +} // #include "error_code.h" @@ -14250,6 +14292,127 @@ SQLITE_ORM_EXPORT namespace sqlite_orm { namespace sqlite_orm { namespace internal { +#ifdef SQLITE_ORM_CPP20_SEMAPHORE_SUPPORTED + /** + * The connection holder should be performant in all variants: + 1. single-threaded use + 2. opened once (open forever) + 3. concurrent open/close + + Hence, a light-weight binary semaphore is used to synchronize opening and closing a database connection. + */ + struct connection_holder { + struct maybe_lock { + maybe_lock(std::binary_semaphore& sync, bool shouldLock) noexcept(noexcept(sync.acquire())) : + isSynced{shouldLock}, sync{sync} { + if (shouldLock) { + sync.acquire(); + } + } + + ~maybe_lock() { + if (isSynced) { + sync.release(); + } + } + + const bool isSynced; + std::binary_semaphore& sync; + }; + + connection_holder(std::string filename, + std::function didOpenDb, + const connection_control& options = {}) : + _didOpenDb{std::move(didOpenDb)}, filename(std::move(filename)), vfs_name(options.vfs_name), + open_mode(options.open_mode) {} + + connection_holder(const connection_holder&) = delete; + + connection_holder(const connection_holder& other, std::function didOpenDb) : + _didOpenDb{std::move(didOpenDb)}, filename{other.filename}, vfs_name(other.vfs_name), + open_mode{other.open_mode} {} + + void retain() { + const maybe_lock maybeLock{_sync, !_openedForeverHint}; + + // `maybeLock.isSynced`: the lock above already synchronized everything, so we can just atomically increment the counter + // `!maybeLock.isSynced`: we presume that the connection is opened once in a single-threaded context [also open forever]. + // therefore we can just use an atomic increment but don't need sequencing due to `prevCount > 0`. + if (int prevCount = _retainCount.fetch_add(1, std::memory_order_relaxed); prevCount > 0) { + return; + } + + // first one opens and sets up the connection. + + int open_flags = internal::db_open_mode_to_int_flags(this->open_mode); +#if SQLITE_VERSION_NUMBER >= 3037002 + open_flags |= SQLITE_OPEN_EXRESCODE; +#endif + + if (int rc = sqlite3_open_v2(this->filename.c_str(), &this->db, open_flags, this->vfs_name.c_str()); + rc != SQLITE_OK) [[unlikely]] /*possible, but unexpected*/ { + throw_translated_sqlite_error(this->db); + } + + if (_didOpenDb) { + _didOpenDb(this->db); + } + } + + void release() { + const maybe_lock maybeLock{_sync, !_openedForeverHint}; + + if (int prevCount = _retainCount.fetch_sub( + 1, + maybeLock.isSynced + // the lock above already synchronized everything, so we can just atomically decrement the counter + ? std::memory_order_relaxed + // the counter must serve as a synchronization point + : std::memory_order_acq_rel); + prevCount > 1) { + return; + } + + // last one closes the connection. + + if (int rc = sqlite3_close_v2(this->db); rc != SQLITE_OK) [[unlikely]] { + throw_translated_sqlite_error(this->db); + } else { + this->db = nullptr; + } + } + + sqlite3* get() const { + // note: ensuring a valid DB handle was already memory ordered with `retain()` + return this->db; + } + + /** + * @attention While retrieving the reference count value is atomic it makes only sense at single-threaded points in code. + */ + int retain_count() const { + return _retainCount.load(std::memory_order_relaxed); + } + + protected: + SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(alignas(polyfill::hardware_destructive_interference_size)) + orm_gsl::owner db = nullptr; + + private: + std::atomic_int _retainCount{}; + const bool _openedForeverHint = false; + std::binary_semaphore _sync{1}; + + private: + SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(alignas(polyfill::hardware_destructive_interference_size)) + const std::function _didOpenDb; + + public: + const std::string filename; + const std::string vfs_name; + const db_open_mode open_mode; + }; +#else struct connection_holder { connection_holder(std::string filename, std::function didOpenDb, @@ -14312,12 +14475,14 @@ namespace sqlite_orm { } protected: + SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(alignas(polyfill::hardware_destructive_interference_size)) orm_gsl::owner db = nullptr; private: std::atomic_int _retainCount{}; private: + SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(alignas(polyfill::hardware_destructive_interference_size)) const std::function _didOpenDb; public: @@ -14325,6 +14490,7 @@ namespace sqlite_orm { const std::string vfs_name; const db_open_mode open_mode; }; +#endif struct connection_ref { connection_ref(connection_holder& holder) : holder(&holder) { @@ -24888,7 +25054,7 @@ namespace sqlite_orm { if (this->executor.did_run_query) { this->executor.did_run_query(sql); } - switch (stepRes) { + switch (rc) { case SQLITE_ROW: { T res; object_from_column_builder builder{res, stmt}; @@ -25059,9 +25225,6 @@ SQLITE_ORM_EXPORT namespace sqlite_orm { create_from_tuple(std::move(specTuple), opt_index_sequence{})); } #else - /* - * Factory function for a storage instance, from a database file and a bunch of database object definitions. - */ template internal::storage_t make_storage(std::string filename, DBO... dbObjects) { return {std::move(filename), {std::forward(dbObjects)...}, std::tuple<>{}}; diff --git a/tests/user_defined_functions.cpp b/tests/user_defined_functions.cpp index 006e79a6..c54a0b01 100644 --- a/tests/user_defined_functions.cpp +++ b/tests/user_defined_functions.cpp @@ -190,8 +190,7 @@ int MultiSum::objectsCount = 0; int FirstFunction::objectsCount = 0; int FirstFunction::callsCount = 0; -#if __cpp_aligned_new >= 201606L -struct alignas(2 * __STDCPP_DEFAULT_NEW_ALIGNMENT__) OverAlignedScalarFunction { +struct SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(alignas(2 * __STDCPP_DEFAULT_NEW_ALIGNMENT__)) OverAlignedScalarFunction { int operator()(int arg) const { return arg; } @@ -201,7 +200,8 @@ struct alignas(2 * __STDCPP_DEFAULT_NEW_ALIGNMENT__) OverAlignedScalarFunction { } }; -struct alignas(2 * __STDCPP_DEFAULT_NEW_ALIGNMENT__) OverAlignedAggregateFunction { +struct SQLITE_ORM_MSVC_SUPPRESS_OVERALIGNMENT(alignas(2 * + __STDCPP_DEFAULT_NEW_ALIGNMENT__)) OverAlignedAggregateFunction { double sum = 0; void step(double arg) { @@ -215,7 +215,6 @@ struct alignas(2 * __STDCPP_DEFAULT_NEW_ALIGNMENT__) OverAlignedAggregateFunctio return "OVERALIGNED2"; } }; -#endif #ifdef SQLITE_ORM_STATIC_CALL_OPERATOR_SUPPORTED struct StaticCallOpFunction { @@ -486,14 +485,12 @@ TEST_CASE("custom functions") { } storage.delete_aggregate_function(); -#if __cpp_aligned_new >= 201606L { storage.create_scalar_function(); REQUIRE_NOTHROW(storage.delete_scalar_function()); storage.create_aggregate_function(); REQUIRE_NOTHROW(storage.delete_aggregate_function()); } -#endif #ifdef SQLITE_ORM_STATIC_CALL_OPERATOR_SUPPORTED storage.create_scalar_function();