diff --git a/README.md b/README.md index 13e316f..25c37ad 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ elements should not touch the heap. ./scripts/make.sh --test # Build and run tests ./scripts/make.sh --shared # Shared library ./scripts/make.sh --clangd # Editor database in build-clangd/ +./scripts/make.sh --libstdcxx # Clang with libstdc++, as CI builds ``` The editor database is a directory of its own because `build/` alternates diff --git a/docs/integration.md b/docs/integration.md index 485742a..f440645 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -7,6 +7,20 @@ std;` enabled in your project. ## FetchContent ```cmake +cmake_minimum_required(VERSION 3.30 FATAL_ERROR) + +# Before project(): CMAKE_EXPERIMENTAL_CXX_IMPORT_STD is read when CXX is enabled, +# and libmem is not fetched yet, so copy cmake/enable_standard_modules.cmake out of +# this repo rather than including its copy. +include(enable_standard_modules) +enable_experimental_std() + +project(my_project LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 26) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_MODULE_STD ON) + include(FetchContent) FetchContent_Declare( libmem @@ -19,6 +33,14 @@ FetchContent_MakeAvailable(libmem) target_link_libraries(my_target PRIVATE libmem::libmem) ``` +Neither `enable_experimental_std()` nor `CMAKE_CXX_MODULE_STD` is optional, and a +fetching consumer has to supply both itself: without the first, configure fails at +generate time with `The "CXX_MODULE_STD" property on target ... requires toolchain +support`; without the second, your own `import std;` fails to compile with `unknown +compiled module interface: no such module`. The activation token is a uuid that +changes with every CMake release, which is why it lives in a helper that maps +version to token rather than in a line you paste. + A tag can be force-pushed, so `GIT_TAG` with a full commit sha is the reproducible pin. `libmem::libmem` and plain `libmem` are the same target here; the namespaced name is the one `find_package` also gives you. @@ -64,9 +86,13 @@ a Clang-built install with GCC works and vice versa. `CMAKE_CXX_EXTENSIONS` is yours to set. libmem pins no dialect, so its modules compile as `c++26` or `gnu++26` to match yours and there is only ever one -`import std` BMI. Both dialects are built and tested in CI. `CMAKE_CXX_STANDARD` -is optional too: `libmem::libmem` carries `cxx_std_26`, which raises a consumer -that sets nothing. +`import std` BMI. Both dialects are built and tested in CI. + +Set `CMAKE_CXX_STANDARD 26` even though `libmem::libmem` carries `cxx_std_26` and +will raise you without it. Omitting it builds and runs, but your own targets stay +at CMake's default while libmem's are raised, and two standards are two `import std` +BMIs: CMake then synthesises a second set of module targets and compiles everything +twice. Setting it leaves one BMI and no synthesised targets. Compatibility is `SameMinorVersion`. Pre-1.0 a minor bump is a break, so `0.9` accepts 0.9.x and rejects 0.10. diff --git a/scripts/make.sh b/scripts/make.sh index bde6929..1e743e6 100755 --- a/scripts/make.sh +++ b/scripts/make.sh @@ -10,6 +10,7 @@ # ./scripts/make.sh --shared # Build as shared library # ./scripts/make.sh --clean # Remove build directory first # ./scripts/make.sh --clangd # Build build-clangd/, the database .clangd reads +# ./scripts/make.sh --libstdcxx # Clang with libstdc++, as CI builds, in build-libstdcxx/ # ./scripts/make.sh --release --test # Combine flags as needed # --------------------------------------------------------------------------------------- @@ -25,6 +26,7 @@ RUN_TESTS=false BUILD_SHARED=false CLEAN=false CLANGD=false +LIBSTDCXX=false SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) SOURCE_DIR=$(cd "${SCRIPT_DIR}/.." && pwd) @@ -41,9 +43,10 @@ for arg in "$@"; do --shared) BUILD_SHARED=true ;; --clean) CLEAN=true ;; --clangd) CLANGD=true ;; + --libstdcxx) LIBSTDCXX=true ;; *) echo "Unknown option: ${arg}" - echo "Usage: $0 [--release] [--gcc] [--test] [--shared] [--clean] [--clangd]" + echo "Usage: $0 [--release] [--gcc] [--test] [--shared] [--clean] [--clangd] [--libstdcxx]" exit 1 ;; esac @@ -62,6 +65,16 @@ if ${CLANGD}; then RUN_TESTS=false fi +# Own directory: build/ caches the toolchain file's -stdlib=libc++, and the compiler path +# does not change, so CMake would keep it. +if ${LIBSTDCXX}; then + if ${USE_GCC} || ${CLANGD}; then + echo "--libstdcxx is a Clang build; it cannot combine with --gcc or --clangd" + exit 1 + fi + BUILD_DIR="${SOURCE_DIR}/build-libstdcxx" +fi + # --------------------------------------------------------------------------------------- # Clean # --------------------------------------------------------------------------------------- @@ -100,6 +113,15 @@ if ${USE_GCC}; then if [ -z "${CXX:-}" ]; then export CXX="g++" fi +elif ${LIBSTDCXX}; then + echo "==> Using Clang with libstdc++" + # No toolchain file: it forces -stdlib=libc++. + if [ -z "${CC:-}" ]; then + export CC="clang" + fi + if [ -z "${CXX:-}" ]; then + export CXX="clang++" + fi else echo "==> Using Clang via LLVM toolchain" CMAKE_ARGS+=("-DCMAKE_TOOLCHAIN_FILE=${SOURCE_DIR}/cmake/llvm_toolchain.cmake") diff --git a/src/libmem/slab.cppm b/src/libmem/slab.cppm index 8fdabba..bec26c2 100644 --- a/src/libmem/slab.cppm +++ b/src/libmem/slab.cppm @@ -226,27 +226,26 @@ public: * Iterator: walks allocated blocks via bitmap scanning * ======================================================================== */ + /** + * @brief Iterator over allocated blocks. + * + * Holds a bit index and re-reads the live bitmap on every increment: a block + * freed after the iterator was built is skipped, one allocated ahead of it is + * visited. + */ class iterator { + friend class slab; + public: using difference_type = std::ptrdiff_t; using value_type = void*; constexpr iterator() noexcept = default; - constexpr iterator(const slab* s, std::uint32_t word_idx, std::uint64_t word) noexcept : slab_{s}, word_idx_{word_idx}, word_{word} {} - - constexpr void* operator*() const noexcept { - const auto bit{static_cast(std::countr_zero(word_))}; - const std::uint32_t index{word_idx_ * bitmap_word_bits + bit}; - return const_cast(static_cast(slab_->memory_ + index * BlockSize)); - } + constexpr void* operator*() const noexcept { return slab_->index_to_ptr(index_); } constexpr iterator& operator++() noexcept { - /* Clear the lowest set bit. */ - word_ &= word_ - 1; - if (!word_) { - advance_word(); - } + index_ = slab_->next_allocated(index_ + 1); return *this; } @@ -256,58 +255,22 @@ public: return tmp; } - constexpr bool operator==(std::default_sentinel_t) const noexcept { return slab_ == nullptr || (!word_ && word_idx_ >= bitmap_words); } + constexpr bool operator==(std::default_sentinel_t) const noexcept { return index_ == capacity; } friend constexpr bool operator==(std::default_sentinel_t s, const iterator& it) noexcept { return it == s; } - /** - * @brief Iterator-to-iterator equality. - * - * Two iterators compare equal when they reference the same slab and - * either both reached their end state or both currently dereference - * to the same allocated block. - */ - constexpr bool operator==(const iterator& rhs) const noexcept { - if (slab_ != rhs.slab_) { - return false; - } - const bool lhs_end{slab_ == nullptr || (!word_ && word_idx_ >= bitmap_words)}; - const bool rhs_end{rhs.slab_ == nullptr || (!rhs.word_ && rhs.word_idx_ >= bitmap_words)}; - if (lhs_end || rhs_end) { - return lhs_end && rhs_end; - } - if (word_idx_ != rhs.word_idx_) { - return false; - } - return std::countr_zero(word_) == std::countr_zero(rhs.word_); - } + /** @brief Equal when both reference the same slab and index; end is index `capacity`. */ + constexpr bool operator==(const iterator&) const noexcept = default; private: const slab* slab_{}; - std::uint32_t word_idx_{}; - std::uint64_t word_{}; - - constexpr void advance_word() noexcept { - ++word_idx_; - while (word_idx_ < bitmap_words) { - word_ = slab_->bitmap_[word_idx_]; - if (word_) { - return; - } - ++word_idx_; - } - } + std::uint32_t index_{capacity}; + + constexpr iterator(const slab* s, const std::uint32_t index) noexcept : slab_{s}, index_{index} {} }; /** @brief Begin iterator over allocated blocks. */ - constexpr iterator begin() const noexcept { - for (std::uint32_t i{0}; i < bitmap_words; ++i) { - if (bitmap_[i]) { - return iterator{this, i, bitmap_[i]}; - } - } - return iterator{this, static_cast(bitmap_words), 0}; - } + constexpr iterator begin() const noexcept { return iterator{this, next_allocated(0)}; } /** @brief Sentinel marking the end of iteration. */ static constexpr std::default_sentinel_t end() noexcept { return {}; } @@ -319,26 +282,7 @@ public: * ascending order. * @pre `index < capacity` and the bit at `index` is set. */ - constexpr iterator make_iterator(const std::uint32_t index) const noexcept { - const std::uint32_t word_idx{index / bitmap_word_bits}; - const std::uint32_t bit{index & (bitmap_word_bits - 1)}; - if (word_idx >= bitmap_words) [[unlikely]] { - return iterator{this, static_cast(bitmap_words), 0}; - } - /* Mask out bits strictly below `bit` so the iterator visits this - * index first. */ - const std::uint64_t masked{bitmap_[word_idx] & (~std::uint64_t{0} << bit)}; - if (masked) { - return iterator{this, word_idx, masked}; - } - /* Empty word after masking -> advance to the next non-empty word. */ - for (std::uint32_t i{word_idx + 1}; i < bitmap_words; ++i) { - if (bitmap_[i]) { - return iterator{this, i, bitmap_[i]}; - } - } - return iterator{this, static_cast(bitmap_words), 0}; - } + constexpr iterator make_iterator(const std::uint32_t index) const noexcept { return iterator{this, next_allocated(index)}; } private: std::byte* memory_{}; @@ -347,6 +291,26 @@ private: constexpr void* index_to_ptr(const std::uint32_t index) const noexcept { return static_cast(memory_ + static_cast(index) * BlockSize); } + /** @brief Index of the lowest set bit of `word`, the `word_idx`-th bitmap word. */ + static constexpr std::uint32_t first_in(const std::size_t word_idx, const std::uint64_t word) noexcept { + return static_cast(word_idx * bitmap_word_bits + static_cast(std::countr_zero(word))); + } + + /** @brief Index of the first allocated block at or after `from`, or `capacity` if none. */ + constexpr std::uint32_t next_allocated(const std::uint32_t from) const noexcept { + const std::size_t word_idx{from / bitmap_word_bits}; + if (word_idx >= bitmap_words) { + return capacity; + } + if (const std::uint64_t rest{bitmap_[word_idx] & (~std::uint64_t{0} << (from % bitmap_word_bits))}) { + return first_in(word_idx, rest); + } + /* Call form, not a pipe: Clang cannot find libstdc++'s adaptor operator| when a consumer instantiates this. */ + const auto later{std::views::drop(bitmap_, static_cast(word_idx + 1))}; + const auto hit{std::ranges::find_if(later, [](const std::uint64_t word) { return word != 0; })}; + return hit == bitmap_.end() ? capacity : first_in(static_cast(hit - bitmap_.begin()), *hit); + } + constexpr std::uint32_t ptr_to_index(const void* ptr) const noexcept { return static_cast((reinterpret_cast(ptr) - reinterpret_cast(memory_)) / BlockSize); } diff --git a/tests/src/pool_test.cpp b/tests/src/pool_test.cpp index 44c3f25..ca5433d 100644 --- a/tests/src/pool_test.cpp +++ b/tests/src/pool_test.cpp @@ -87,11 +87,22 @@ TEST(PoolTest, erase_returns_next) { EXPECT_EQ(seen.size(), 2u); EXPECT_TRUE(std::ranges::find(seen, 2) == seen.end()); - /* `next` should reference one of the live elements. */ - if (next != p.end()) { - const std::int32_t v{*next}; - EXPECT_TRUE(v == 1 || v == 3); - } + /* 3 went in after `e2` was built, in the same bitmap word. */ + ASSERT_NE(next, p.end()); + EXPECT_EQ(*next, 3); +} + +TEST(PoolTest, iterator_skips_an_element_erased_after_it_was_built) { + pool p{}; + p.emplace(1); + const auto e2 = p.emplace(2); + + auto it = p.begin(); + p.erase(e2); + + EXPECT_EQ(*it, 1); + ++it; + EXPECT_EQ(it, p.end()); } TEST(PoolTest, ranges_pipe_views) { diff --git a/tests/src/slab_test.cpp b/tests/src/slab_test.cpp index 17189ae..4d24dea 100644 --- a/tests/src/slab_test.cpp +++ b/tests/src/slab_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include import libmem; @@ -154,3 +155,53 @@ TEST(SlabTest, allocate_at_reports_reused_index_after_deallocate) { EXPECT_EQ(reused.ptr, first.ptr); EXPECT_EQ(reused.index, first.index); } + +TEST(SlabTest, iterator_skips_a_block_freed_after_it_was_built) { + alignas(block) std::array storage{}; + slab s{storage.data(), storage.size()}; + + void* first{s.allocate()}; + void* second{s.allocate()}; + auto it{s.begin()}; + s.deallocate(second); + + EXPECT_EQ(*it, first); + ++it; + EXPECT_EQ(it, s.end()); +} + +TEST(SlabTest, iterator_visits_a_block_allocated_ahead_of_it) { + alignas(block) std::array storage{}; + slab s{storage.data(), storage.size()}; + + static_cast(s.allocate()); + auto it{s.begin()}; + void* second{s.allocate()}; + + ++it; + ASSERT_NE(it, s.end()); + EXPECT_EQ(*it, second); +} + +TEST(SlabTest, iterator_crosses_empty_words) { + constexpr std::uint32_t capacity{192}; + alignas(block) std::array storage{}; + slab s{storage.data(), storage.size()}; + + std::array blocks{}; + for (auto& b : blocks) { + b = s.allocate(); + } + /* Keep bit 0 of word 0 and bit 2 of word 2; word 1 is empty. */ + for (std::uint32_t i{0}; i < capacity; ++i) { + if (i != 0 && i != 130) { + s.deallocate(blocks[i]); + } + } + + std::vector visited{}; + for (void* p : s) { + visited.push_back(p); + } + EXPECT_EQ(visited, (std::vector{blocks[0], blocks[130]})); +}