From 8b7d0f61ada52aa1ac4faf55ad87c9dc466c4f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20S=2E=20Ga=C3=9Fmann?= Date: Wed, 29 Mar 2023 10:34:36 +0200 Subject: [PATCH 1/4] build: Update dependencies @2023-03-29 --- .github/workflows/cpp-ci.yml | 4 ++-- vcpkg-configuration.json | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cpp-ci.yml b/.github/workflows/cpp-ci.yml index ebaa015..84c784c 100644 --- a/.github/workflows/cpp-ci.yml +++ b/.github/workflows/cpp-ci.yml @@ -40,7 +40,7 @@ jobs: uses: lukka/run-vcpkg@v10 with: vcpkgDirectory: ${{ github.workspace }}/build/vcpkg - vcpkgGitCommitId: 71d3fa60b67540e9bf5fde2bf2188f579ff09433 + vcpkgGitCommitId: 1271068e139c1fc30bae405c0bca0e379e155bd2 prependedCacheKey: compiler=${{ matrix.compiler }} #appendedCacheKey: r00 @@ -100,7 +100,7 @@ jobs: uses: lukka/run-vcpkg@v10 with: vcpkgDirectory: ${{ github.workspace }}/../vcpkg - vcpkgGitCommitId: 71d3fa60b67540e9bf5fde2bf2188f579ff09433 + vcpkgGitCommitId: 1271068e139c1fc30bae405c0bca0e379e155bd2 prependedCacheKey: compiler=clang-15 #appendedCacheKey: r00 diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index ba3fbfb..1a1a782 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -2,16 +2,14 @@ "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg-configuration.schema.json", "default-registry": { "kind": "builtin", - "baseline": "71d3fa60b67540e9bf5fde2bf2188f579ff09433" + "baseline": "1271068e139c1fc30bae405c0bca0e379e155bd2" }, "registries": [ { "kind": "git", "repository": "https://github.com/deeplex/vcpkg-registry", - "baseline": "e0379d3342d009a2ca0b713af9ced5df6e155ada", - "packages": [ - "concrete" - ] + "baseline": "92fdbb474589c5b134d1dd2083df3f258c56c639", + "packages": ["concrete"] } ] } \ No newline at end of file From a79b2246feaf1a24ef70554836368664902e808b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20S=2E=20Ga=C3=9Fmann?= Date: Wed, 29 Mar 2023 20:20:30 +0200 Subject: [PATCH 2/4] build!: Add `parallel-hashmap` dependency Add a good unordered map implementation as a project dependency. It will serve as a foundation for the upcoming state API implementation. Enforce `_ENABLE_EXTENDED_ALIGNED_STORAGE` on windows. Otherwise `cncr::uuid`'s can't be used with `phmap::flat_hash_map` <= v1.3.8. --- CMakeLists.txt | 5 +++++ vcpkg.json | 1 + 2 files changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 10eefff..ebf4c90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,11 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") /Zc:__cplusplus # correctly define the __cplusplus macro ) endif() +if (WIN32) + target_compile_definitions(compiler_settings INTERFACE + -D_ENABLE_EXTENDED_ALIGNED_STORAGE=1 + ) +endif() ######################################################################## diff --git a/vcpkg.json b/vcpkg.json index de7763d..ec1de3c 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -10,6 +10,7 @@ "concrete", "fmt", "outcome", + "parallel-hashmap", "status-code" ], "features": { From 05952b2d77c9e42cdaf647d0069a1387522d5978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20S=2E=20Ga=C3=9Fmann?= Date: Wed, 29 Mar 2023 20:26:33 +0200 Subject: [PATCH 3/4] feat: Add state management API Some codecs need to pass state or configuration values down the stack. The `state_store` ought to be used for long lived configuration and codec state. The `link_store` on the other hand is better suited to store small trivial values like pointers to stack scoped variables in order to efficiently coordinate codec call hierarchies, e.g. enable the implementation of [`stringref`] tags. [`stringref`]: http://cbor.schmorp.de/stringref --- sources.cmake | 1 + src/dplx/dp/state.hpp | 463 +++++++++++++++++++++++++++++++++++++ src/dplx/dp/state.test.cpp | 86 +++++++ 3 files changed, 550 insertions(+) create mode 100644 src/dplx/dp/state.hpp create mode 100644 src/dplx/dp/state.test.cpp diff --git a/sources.cmake b/sources.cmake index 4c67bce..01bfc7c 100644 --- a/sources.cmake +++ b/sources.cmake @@ -31,6 +31,7 @@ dplx_target_sources(deeppack dp/layout_descriptor dp/macros dp/object_def + dp/state dp/tuple_def dp/codecs/auto_enum diff --git a/src/dplx/dp/state.hpp b/src/dplx/dp/state.hpp new file mode 100644 index 0000000..a3f9091 --- /dev/null +++ b/src/dplx/dp/state.hpp @@ -0,0 +1,463 @@ + +// Copyright 2023 Henrik Steffen Gaßmann +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#pragma once + +#include +#include +#include + +#include + +#include +#include + +#include + +namespace dplx::dp::detail +{ + +template +struct erasure_pad +{ + T value; + std::byte padding[PadSize]; +}; +template +struct erasure_pad +{ + T value; +}; + +template + requires(sizeof(From) < sizeof(To)) +constexpr auto erasure_cast(From const &value) noexcept -> To +{ + return std::bit_cast( + erasure_pad{value, {}}); +} +template + requires(sizeof(From) == sizeof(To)) +constexpr auto erasure_cast(From const &value) noexcept -> To +{ + return std::bit_cast(value); +} + +} // namespace dplx::dp::detail + +namespace dplx::dp::detail +{ + +template > +class unique_erased_state_ptr; + +template +constexpr auto allocate_state(Allocator const &alloc, Args &&...args) + -> unique_erased_state_ptr; + +template +class unique_erased_state_ptr +{ + template + friend constexpr auto allocate_state(Alloc const &alloc, Args &&...args) + -> unique_erased_state_ptr; + + using delete_fn = void (*)(void *obj) noexcept; + + void *mObj{}; + delete_fn mDelete{}; + + template + struct container + { + DPLX_ATTR_NO_UNIQUE_ADDRESS Allocator allocator; + StateT state; + + using allocator_type = Allocator; + + constexpr container() = default; + + container(container const &) = delete; + auto operator=(container const &) -> container & = delete; + + container(container &&) = delete; + auto operator=(container &&) -> container & = delete; + + template + requires(!std::is_same_v, + std::allocator_arg_t>) + constexpr container(Arg &&arg, Args &&...args) + : allocator() + , state(static_cast(arg), static_cast(args)...) + { + } + template + constexpr container(std::allocator_arg_t, + Allocator const &alloc, + Args &&...args) + : allocator(alloc) + , state(std::make_obj_using_allocator( + alloc, static_cast(args)...)) + { + } + }; + +public: + constexpr ~unique_erased_state_ptr() noexcept + { + if (mObj != nullptr && mDelete != nullptr) + { + mDelete(mObj); + } + } + constexpr unique_erased_state_ptr() noexcept = default; + + constexpr unique_erased_state_ptr(unique_erased_state_ptr &&other) noexcept + : mObj(std::exchange(other.mObj, nullptr)) + , mDelete(std::exchange(other.mDelete, nullptr)) + { + } + constexpr auto operator=(unique_erased_state_ptr &&other) noexcept + -> unique_erased_state_ptr & + { + mObj = std::exchange(other.mObj, nullptr); + mDelete = std::exchange(other.mDelete, nullptr); + + return *this; + } + +private: + template + constexpr explicit unique_erased_state_ptr(container *obj) + : mObj(static_cast(obj)) + , mDelete(&deleter) + { + } + +public: + template + [[nodiscard]] auto get() const noexcept -> StateT * + { + return mObj == nullptr ? nullptr + : &static_cast *>(mObj)->state; + } + +private: + template + static void deleter(void *obj) noexcept + { + using allocator_type = typename std::allocator_traits:: + template rebind_alloc::template container>; + using allocator_traits = typename std::allocator_traits; + + auto *wrapped = static_cast *>(obj); + allocator_type allocator(wrapped->allocator); + + allocator_traits::destroy(allocator, wrapped); + allocator_traits::deallocate(allocator, wrapped, 1U); + } +}; + +template +constexpr auto allocate_state(Allocator const &alloc, Args &&...args) + -> unique_erased_state_ptr +{ + using allocator_type = + typename std::allocator_traits::template rebind_alloc< + typename unique_erased_state_ptr< + Allocator>::template container>; + using allocator_traits = typename std::allocator_traits; + + allocator_type allocator(alloc); + typename allocator_traits::pointer obj + = allocator_traits::allocate(allocator, 1U); + try + { + allocator_traits::construct(allocator, obj, + static_cast(args)...); + return unique_erased_state_ptr(obj); + } + catch (...) + { + allocator_traits::deallocate(allocator, obj, 1U); + throw; + } +} + +} // namespace dplx::dp::detail + +namespace dplx::dp::detail +{ + +template +using flat_hash_map = phmap::flat_hash_map< + Key, + T, + std::hash, + std::equal_to<>, + std::pmr::polymorphic_allocator>>; + +} + +namespace dplx::dp +{ + +class state_base +{ +public: + virtual ~state_base() noexcept = default; + +protected: + constexpr state_base() noexcept = default; + + constexpr state_base(state_base const &) noexcept = default; + constexpr auto operator=(state_base const &) noexcept + -> state_base & = default; + + constexpr state_base(state_base &&) noexcept = default; + constexpr auto operator=(state_base &&) noexcept -> state_base & = default; +}; + +template +struct state_key +{ + cncr::uuid const value{}; + + using state_type = StateT; +}; + +class state_store +{ +public: + using allocator_type = std::pmr::polymorphic_allocator; + +private: + using impl_type = detail::flat_hash_map< + cncr::uuid, + detail::unique_erased_state_ptr>; + impl_type mImpl{}; + +public: + state_store() = default; + + state_store(allocator_type const &alloc) + : mImpl(alloc) + { + } + + [[nodiscard]] auto get_allocator() const noexcept -> allocator_type + { + return mImpl.get_allocator(); + } + void reserve(typename impl_type::size_type slots) + { + mImpl.reserve(slots); + } + + [[nodiscard]] auto empty() const noexcept -> bool + { + return mImpl.empty(); + } + template + [[nodiscard]] auto try_access(state_key const &key) const noexcept + -> StateT * + { + if (auto it = mImpl.find(key.value); it != mImpl.end()) + { + return it->second.template get(); + } + return nullptr; + } + + template + auto emplace(state_key const &key, Args &&...args) + -> std::pair + { + auto const h = mImpl.hash(key.value); + if (auto it = mImpl.find(key.value, h); it != mImpl.end()) + { + // this way, we don't need to allocate if key already has a value + return {it->second.template get(), false}; + } + auto [it, emplaced] = mImpl.emplace_with_hash( + h, key.value, + detail::allocate_state( + mImpl.get_allocator(), static_cast(args)...)); + return {it->second.template get(), true}; + } + template + auto erase(state_key const &key) noexcept -> std::size_t + { + return mImpl.erase(key.value); + } + auto erase(cncr::uuid key) noexcept -> std::size_t + { + return mImpl.erase(key); + } + void clear() noexcept + { + mImpl.clear(); + } +}; + +template + requires std::default_initializable +class [[nodiscard]] scoped_state +{ + state_key mKey; + state_store &mStore; + bool owned; + +public: + ~scoped_state() noexcept + { + if (owned) + { + mStore.erase(mKey.value); + } + } + explicit scoped_state(state_store &store, state_key const &key) + : mKey(key) + , mStore(store) + , owned(false) + { + assert(!key.value.is_nil()); + owned = store.emplace(key).second; + } + + [[nodiscard]] auto get() const noexcept -> StateT * + { + return mStore.try_access(mKey); + } +}; + +template +concept state_link = std::regular && std::is_trivial_v + && (sizeof(T) <= sizeof(cncr::uuid)); + +template +struct [[nodiscard]] state_link_key +{ + cncr::uuid const value{}; + + using link_type = T; +}; + +class link_store +{ +public: + using allocator_type = std::pmr::polymorphic_allocator; + +private: + using erased_type + = cncr::blob; + using impl_type = detail::flat_hash_map; + impl_type mImpl{}; + +public: + link_store() = default; + + link_store(allocator_type const &alloc) + : mImpl(alloc) + { + } + + [[nodiscard]] auto get_allocator() const noexcept -> allocator_type + { + return mImpl.get_allocator(); + } + void reserve(typename impl_type::size_type slots) + { + mImpl.reserve(slots); + } + + [[nodiscard]] auto empty() const noexcept -> bool + { + return mImpl.empty(); + } + template + [[nodiscard]] auto try_access(state_link_key const &key) const noexcept + -> T + { + if (auto it = mImpl.find(key.value); it != mImpl.end()) + { + return std::bit_cast>( + it->second) + .value; + } + return T{}; + } + + void clear() noexcept + { + mImpl.clear(); + } + template + auto replace(state_link_key const &key, T value) -> T + { + auto const h = mImpl.hash(key.value); + auto it = mImpl.find(key.value, h); + if (it == mImpl.end()) + { + if (value != T{}) + { + mImpl.emplace_with_hash( + h, key.value, + detail::erasure_cast(value)); + } + return T{}; + } + + auto previousValue = std::bit_cast>(it->second) + .value; + if (value == T{}) + { + mImpl.erase(it); + } + else + { + it->second = detail::erasure_cast(value); + } + return previousValue; + } +}; + +template +class [[nodiscard]] scoped_link +{ + state_link_key mKey; + T mPreviousValue; + link_store &mStore; + +public: + ~scoped_link() noexcept + { + mStore.replace(mKey, mPreviousValue); + } + explicit scoped_link(link_store &store, + state_link_key const &key, + T value) + : mKey(key) + , mPreviousValue(store.replace(key, value)) + , mStore(store) + { + } + + [[nodiscard]] auto get() const noexcept -> T + { + return mStore.try_access(mKey); + } + [[nodiscard]] auto shadowed_value() const noexcept -> T + { + return mPreviousValue; + } +}; + +} // namespace dplx::dp diff --git a/src/dplx/dp/state.test.cpp b/src/dplx/dp/state.test.cpp new file mode 100644 index 0000000..93f0ac9 --- /dev/null +++ b/src/dplx/dp/state.test.cpp @@ -0,0 +1,86 @@ + +// Copyright 2023 Henrik Steffen Gaßmann +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "dplx/dp/state.hpp" + +#include + +#include "test_utils.hpp" + +namespace dp_tests +{ + +using namespace cncr::uuid_literals; + +TEST_CASE("state_map is default constructible") +{ + dp::state_store states; + CHECK(states.empty()); +} + +TEST_CASE("can insert a simple state into state_map") +{ + constexpr dp::state_key k{"fdd790a7-c618-4e58-b412-8042b42bd9bb"_uuid}; + dp::state_store states; + + auto const value = 4; + auto [state, inserted] = states.emplace(k, value); + CHECK(inserted); + CHECK(*state == 4); + + auto *accessed = states.try_access(k); + CHECK(state == accessed); +} + +TEST_CASE("scoped_state manages a state lifetime") +{ + constexpr dp::state_key k{"fdd790a7-c618-4e58-b412-8042b42bd9bb"_uuid}; + dp::state_store states; + + CHECK(states.try_access(k) == nullptr); + { + dp::scoped_state stateScope(states, k); + REQUIRE(stateScope.get() != nullptr); + CHECK(*stateScope.get() == int{}); + } + CHECK(states.try_access(k) == nullptr); +} + +TEST_CASE("link_map is default constructible") +{ + dp::link_store links; + CHECK(links.empty()); +} + +TEST_CASE("can insert a simple link into link_map") +{ + constexpr dp::state_link_key k{ + "fdd790a7-c618-4e58-b412-8042b42bd9bb"_uuid}; + dp::link_store links; + + auto const value = 4; + CHECK(links.replace(k, value) == int{}); + + CHECK(value == links.try_access(k)); +} + +TEST_CASE("scoped_link manages a link lifetime") +{ + constexpr dp::state_link_key k{ + "fdd790a7-c618-4e58-b412-8042b42bd9bb"_uuid}; + dp::link_store links; + + CHECK(links.try_access(k) == int{}); + { + auto const value = 4; + dp::scoped_link linkScope(links, k, value); + CHECK(links.try_access(k) == value); + } + CHECK(links.try_access(k) == int{}); +} + +} // namespace dp_tests From 451c58b80ff5226f524996e5378e9e0a7f32a582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20S=2E=20Ga=C3=9Fmann?= Date: Thu, 30 Mar 2023 08:15:55 +0200 Subject: [PATCH 4/4] feat: Add state API to `parse_context` --- src/dp_tests/test_input_stream.hpp | 9 ++++++--- src/dp_tests/test_output_stream.hpp | 9 ++++++--- src/dplx/dp/items/parse_context.hpp | 22 ++++++++++++++++++++++ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/dp_tests/test_input_stream.hpp b/src/dp_tests/test_input_stream.hpp index a6670c3..a3ac282 100644 --- a/src/dp_tests/test_input_stream.hpp +++ b/src/dp_tests/test_input_stream.hpp @@ -88,22 +88,25 @@ class simple_test_input_stream final : public dp::input_buffer } }; -class simple_test_parse_context final : private dp::parse_context +class simple_test_parse_context final { + dp::parse_context ctx; + public: + // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) simple_test_input_stream stream; explicit simple_test_parse_context( std::span const readBuffer, bool const initiallyEmpty = false) - : parse_context{stream} + : ctx{stream} , stream(readBuffer, initiallyEmpty) { } auto as_parse_context() noexcept -> dp::parse_context & { - return *this; + return ctx; } }; diff --git a/src/dp_tests/test_output_stream.hpp b/src/dp_tests/test_output_stream.hpp index a33ea13..92e0ca4 100644 --- a/src/dp_tests/test_output_stream.hpp +++ b/src/dp_tests/test_output_stream.hpp @@ -250,22 +250,25 @@ class test_output_stream final : public dp::output_buffer } }; -class test_emit_context final : private dp::emit_context +class test_emit_context final { + dp::emit_context ctx; + public: + // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) test_output_stream stream; explicit test_emit_context( std::initializer_list const gatherBufferSizes, bool const initiallyEmpty = false) - : emit_context{stream} + : ctx{stream} , stream(gatherBufferSizes, initiallyEmpty) { } auto as_emit_context() noexcept -> dp::emit_context & { - return *this; + return ctx; } }; diff --git a/src/dplx/dp/items/parse_context.hpp b/src/dplx/dp/items/parse_context.hpp index 46ed654..3b7757f 100644 --- a/src/dplx/dp/items/parse_context.hpp +++ b/src/dplx/dp/items/parse_context.hpp @@ -7,7 +7,11 @@ #pragma once +#include +#include + #include +#include namespace dplx::dp { @@ -15,6 +19,24 @@ namespace dplx::dp struct parse_context { input_buffer ∈ + state_store states; + link_store links; + + explicit parse_context( + input_buffer &inStreamBuffer, + std::pmr::polymorphic_allocator const &allocator + = std::pmr::polymorphic_allocator{}) + : in(inStreamBuffer) + , states(allocator) + , links(allocator) + { + } + + [[nodiscard]] auto get_allocator() const noexcept + -> std::pmr::polymorphic_allocator + { + return states.get_allocator(); + } }; } // namespace dplx::dp