Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 29 additions & 3 deletions docs/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
24 changes: 23 additions & 1 deletion scripts/make.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------------------

Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
# ---------------------------------------------------------------------------------------
Expand Down Expand Up @@ -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")
Expand Down
114 changes: 39 additions & 75 deletions src/libmem/slab.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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::uint32_t>(std::countr_zero(word_))};
const std::uint32_t index{word_idx_ * bitmap_word_bits + bit};
return const_cast<void*>(static_cast<const void*>(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;
}

Expand All @@ -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<std::uint32_t>(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 {}; }
Expand All @@ -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<std::uint32_t>(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<std::uint32_t>(bitmap_words), 0};
}
constexpr iterator make_iterator(const std::uint32_t index) const noexcept { return iterator{this, next_allocated(index)}; }

private:
std::byte* memory_{};
Expand All @@ -347,6 +291,26 @@ private:

constexpr void* index_to_ptr(const std::uint32_t index) const noexcept { return static_cast<void*>(memory_ + static_cast<std::size_t>(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<std::uint32_t>(word_idx * bitmap_word_bits + static_cast<std::size_t>(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<std::ptrdiff_t>(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<std::size_t>(hit - bitmap_.begin()), *hit);
}

constexpr std::uint32_t ptr_to_index(const void* ptr) const noexcept {
return static_cast<std::uint32_t>((reinterpret_cast<std::uintptr_t>(ptr) - reinterpret_cast<std::uintptr_t>(memory_)) / BlockSize);
}
Expand Down
21 changes: 16 additions & 5 deletions tests/src/pool_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::int32_t> 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) {
Expand Down
51 changes: 51 additions & 0 deletions tests/src/slab_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <cstddef>
#include <cstdint>
#include <ranges>
#include <vector>

import libmem;

Expand Down Expand Up @@ -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<std::byte, block * 8> storage{};
slab<block, 8> 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<std::byte, block * 8> storage{};
slab<block, 8> s{storage.data(), storage.size()};

static_cast<void>(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<std::byte, block * capacity> storage{};
slab<block, capacity> s{storage.data(), storage.size()};

std::array<void*, capacity> 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<void*> visited{};
for (void* p : s) {
visited.push_back(p);
}
EXPECT_EQ(visited, (std::vector<void*>{blocks[0], blocks[130]}));
}
Loading