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
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@ if(LIBMEM_INSTALL)
FILE_SET CXX_MODULES DESTINATION "${CMAKE_INSTALL_DATADIR}/libmem/modules"
)

# Next to the .cppm files: they include it by a relative quoted path.
install(FILES src/libmem/assert.hpp DESTINATION "${CMAKE_INSTALL_DATADIR}/libmem/modules/libmem")

install(EXPORT libmem-targets
FILE libmem-targets.cmake
NAMESPACE libmem::
Expand Down
10 changes: 10 additions & 0 deletions docs/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ set options like `-fno-rtti`, LTO, and sanitizers project-wide, and have libmem
the rest of the build agree. A mismatch forks the std BMI at best and is ill-formed
at worst.

## Precondition checks

Library checks are `assert`. Defining `LIBMEM_CONTRACTS` project-wide turns them
into `contract_assert` where the compiler has contracts, governed by
`-fcontract-evaluation-semantic` instead of `NDEBUG`. Not usable yet: GCC 16 fails
to compile or link importers of such modules.

A bad release (foreign or misaligned pointer, double free) is rejected without
touching allocator state, even with the check compiled out.

## Option naming

`USE_SANITIZERS` and `THREAD_SANITIZER` are intentionally **not** prefixed:
Expand Down
2 changes: 1 addition & 1 deletion src/libmem/allocator.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
*/
module;

#include <cassert>
#include "assert.hpp"

export module libmem:allocator;

Expand Down
10 changes: 5 additions & 5 deletions src/libmem/arena.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
*/
module;

#include <cassert>
#include "assert.hpp"

export module libmem:arena;

Expand Down Expand Up @@ -65,7 +65,7 @@ public:
*/
constexpr arena(const std::span<std::byte> buffer, const std::size_t align = default_alignment) noexcept
: begin_{buffer.data()}, end_{buffer.data() + buffer.size()}, cursor_{buffer.data()}, default_alignment_{align}, owns_buffer_{false} {
assert(align > 0 && (align & (align - 1)) == 0 && "alignment must be a power of two");
LIBMEM_ASSERT(align > 0 && (align & (align - 1)) == 0 && "alignment must be a power of two");
}

/**
Expand All @@ -76,8 +76,8 @@ public:
*/
inline explicit arena(const std::size_t size, const std::size_t align = default_alignment)
: begin_{new std::byte[size]}, end_{begin_ + size}, cursor_{begin_}, default_alignment_{align}, owns_buffer_{true} {
assert(size > 0);
assert(align > 0 && (align & (align - 1)) == 0 && "alignment must be a power of two");
LIBMEM_ASSERT(size > 0);
LIBMEM_ASSERT(align > 0 && (align & (align - 1)) == 0 && "alignment must be a power of two");
}

arena(const arena&) = delete;
Expand Down Expand Up @@ -110,7 +110,7 @@ public:
* @return Pointer to the freshly bumped region, or `nullptr` if exhausted.
*/
[[nodiscard]] inline void* allocate(const std::size_t bytes, const std::size_t alignment) noexcept {
assert(alignment > 0 && (alignment & (alignment - 1)) == 0 && "alignment must be a power of two");
LIBMEM_ASSERT(alignment > 0 && (alignment & (alignment - 1)) == 0 && "alignment must be a power of two");

const auto current{reinterpret_cast<std::uintptr_t>(cursor_)};
const auto aligned{(current + alignment - 1) & ~static_cast<std::uintptr_t>(alignment - 1)};
Expand Down
17 changes: 17 additions & 0 deletions src/libmem/assert.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) 2026 Aoto
// SPDX-License-Identifier: MIT
/**
* @file assert.hpp
* @brief `LIBMEM_ASSERT`: `contract_assert` when opted in and supported, `assert` otherwise.
*
* Include in a module's global fragment. Opt in by defining `LIBMEM_CONTRACTS`
* project-wide; see docs/integration.md for what a consumer then needs.
*/
#pragma once

#if defined(LIBMEM_CONTRACTS) && defined(__cpp_contracts)
#define LIBMEM_ASSERT(...) contract_assert(__VA_ARGS__) // NOLINT(cppcoreguidelines-macro-usage)
#else
#include <cassert>
#define LIBMEM_ASSERT(...) assert(__VA_ARGS__) // NOLINT(cppcoreguidelines-macro-usage)
#endif
17 changes: 12 additions & 5 deletions src/libmem/concepts.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/
module;

#include <cassert>
#include "assert.hpp"

export module libmem:concepts;

Expand Down Expand Up @@ -77,12 +77,19 @@ concept valid_alignment = power_of_two<N>;
export template <std::size_t Size, std::size_t Align>
concept valid_block_geometry = (Size > 0) && valid_alignment<Align> && (Size % Align == 0);

/** @brief A valid block geometry for `Count > 0` blocks whose total size fits in `std::size_t`. */
export template <std::size_t Size, std::uint32_t Count, std::size_t Align>
concept valid_slab_geometry = valid_block_geometry<Size, Align> && (Count > 0) && (Count <= std::numeric_limits<std::size_t>::max() / Size);

static_assert(valid_block_geometry<sizeof(std::max_align_t), alignof(std::max_align_t)>);
static_assert(valid_block_geometry<4, 4>);
static_assert(!valid_block_geometry<4, 8>);
static_assert(!valid_block_geometry<24, 16>);
static_assert(!valid_alignment<0>);
static_assert(!valid_alignment<24>);
static_assert(valid_slab_geometry<64, 64, 64>);
static_assert(!valid_slab_geometry<64, 0, 64>);
static_assert(!valid_slab_geometry<std::numeric_limits<std::size_t>::max() / 2 + 1, 2, 1>);

/* ============================================================================
* Memory resource concept & default implementation
Expand Down Expand Up @@ -190,26 +197,26 @@ public:
constexpr explicit resource_ref(R& resource) noexcept : resource_{std::addressof(resource)} {}

void* allocate(const std::size_t size) {
assert(resource_ != nullptr && "resource_ref: no referent");
LIBMEM_ASSERT(resource_ != nullptr && "resource_ref: no referent");
return resource_->allocate(size);
}

void deallocate(void* ptr, const std::size_t size) noexcept {
assert(resource_ != nullptr && "resource_ref: no referent");
LIBMEM_ASSERT(resource_ != nullptr && "resource_ref: no referent");
resource_->deallocate(ptr, size);
}

void* allocate(const std::size_t size, const std::size_t align)
requires aligned_memory_resource<R>
{
assert(resource_ != nullptr && "resource_ref: no referent");
LIBMEM_ASSERT(resource_ != nullptr && "resource_ref: no referent");
return resource_->allocate(size, align);
}

void deallocate(void* ptr, const std::size_t size, const std::size_t align) noexcept
requires aligned_memory_resource<R>
{
assert(resource_ != nullptr && "resource_ref: no referent");
LIBMEM_ASSERT(resource_ != nullptr && "resource_ref: no referent");
resource_->deallocate(ptr, size, align);
}

Expand Down
51 changes: 33 additions & 18 deletions src/libmem/multislab.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/
module;

#include <cassert>
#include "assert.hpp"

export module libmem:multislab;

Expand Down Expand Up @@ -83,7 +83,7 @@ template <std::size_t BlockSize, std::uint32_t BlocksPerSlab, std::size_t BlockA
*/
export template <std::size_t BlockSize, std::uint32_t BlocksPerSlab, memory_resource Resource = default_resource, shrink_policy Policy = threshold_policy,
std::size_t BlockAlign = default_alignment>
requires valid_block_geometry<BlockSize, BlockAlign> && (BlocksPerSlab > 0)
requires valid_slab_geometry<BlockSize, BlocksPerSlab, BlockAlign>
class multislab {
using node_type = detail::slab_node<BlockSize, BlocksPerSlab, BlockAlign>;
using slab_type = typename node_type::slab_type;
Expand Down Expand Up @@ -155,16 +155,18 @@ public:

/**
* @brief Release a block previously obtained from `allocate()`.
* @pre `ptr` was allocated from this multislab and has not been double-freed.
* @pre `ptr` is a live block of this multislab. A violation is rejected
* without touching any state: a foreign or misaligned pointer, or a double free.
* @note O(S): finds the owning slab by scanning. Prefer `deallocate(iterator)`
* when a position is at hand.
*/
void deallocate(void* ptr) noexcept {
assert(ptr != nullptr);

node_type* node{find_owner(ptr)};
assert(node != nullptr && "pointer not owned by this allocator");

const bool ours{node != nullptr && node->allocator.owns(ptr)};
LIBMEM_ASSERT(ours && "multislab: pointer is not a block of this allocator");
if (!ours) [[unlikely]] {
return;
}
release(node, node->allocator.index_of(ptr));
}

Expand Down Expand Up @@ -307,10 +309,14 @@ public:

/**
* @brief Release the block `pos` points at, in O(1).
* @pre `pos` is dereferenceable. Iterators to other blocks stay valid.
* @pre `pos` is dereferenceable and its block live; rejected like `deallocate(ptr)` otherwise.
* Iterators to other blocks stay valid.
*/
void deallocate(const iterator& pos) noexcept {
assert(pos != end());
LIBMEM_ASSERT(pos != end());
if (pos == end()) [[unlikely]] {
return;
}
release(pos.node_, pos.slab_iter_.index());
}

Expand Down Expand Up @@ -358,7 +364,7 @@ private:

node_type* node{active_};
const auto alloc{node->allocator.allocate_at()};
assert(alloc.ptr != nullptr && "a slab on the active list has no free slot");
LIBMEM_ASSERT(alloc.ptr != nullptr && "a slab on the active list has no free slot");

if (node->used++ == 0) {
--empty_count_;
Expand All @@ -370,11 +376,13 @@ private:
}

void release(node_type* node, const std::uint32_t index) noexcept {
/* First: a rejected release must not move the node or count it down. */
if (!node->allocator.deallocate_at(index)) [[unlikely]] {
return;
}
if (node->full()) [[unlikely]] {
move_to_active(node);
}

node->allocator.deallocate_at(index);
node->used--;

/* Became empty: apply shrink policy. */
Expand Down Expand Up @@ -428,20 +436,27 @@ private:
return false;
}

/* Allocate the node. */
void* node_mem{resource_.allocate(sizeof(node_type))};
if (!node_mem) [[unlikely]] {
/* Owns the node memory until the node is built, so a failing or throwing
* slab allocation cannot leak it. */
struct node_memory {
multislab& owner;
void* ptr;
~node_memory() {
if (ptr) {
owner.resource_.deallocate(ptr, sizeof(node_type));
}
}
} node_mem{*this, resource_.allocate(sizeof(node_type))};
if (!node_mem.ptr) [[unlikely]] {
return false;
}

/* Allocate the slab backing memory. */
void* slab_mem{allocate_slab_memory()};
if (!slab_mem) [[unlikely]] {
resource_.deallocate(node_mem, sizeof(node_type));
return false;
}

auto* node{::new (node_mem) node_type{slab_mem, slab_memory_size}};
auto* node{::new (std::exchange(node_mem.ptr, nullptr)) node_type{slab_mem, slab_memory_size}};
push_front<&node_type::space>(active_, node);
push_front<&node_type::all>(nodes_, node);

Expand Down
11 changes: 7 additions & 4 deletions src/libmem/pool.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
*/
module;

#include <cassert>
#include "assert.hpp"

export module libmem:pool;

Expand Down Expand Up @@ -81,10 +81,12 @@ template <typename T> consteval std::uint32_t default_pool_blocks_per_slab() noe
export struct slab_limit {
std::uint32_t value{};

/* Non-explicit and integral-taking, so `slab_limit{4}` needs no `u` suffix. */
constexpr slab_limit() noexcept = default;

template <std::integral N> constexpr slab_limit(const N count) noexcept : value{static_cast<std::uint32_t>(count)} {} // NOLINT(google-explicit-constructor)
/* Explicit, so `pool<int> p(4)` does not compile. Any integral, so `slab_limit{4}` needs no `u` suffix. */
template <std::integral N> constexpr explicit slab_limit(const N count) noexcept : value{static_cast<std::uint32_t>(count)} {
LIBMEM_ASSERT(std::in_range<std::uint32_t>(count) && "slab_limit: count must fit in std::uint32_t");
}

constexpr bool operator==(const slab_limit&) const noexcept = default;
};
Expand Down Expand Up @@ -381,13 +383,14 @@ public:

/**
* @brief Destroy the element referenced by `it` and free its slot.
* @pre `it` is dereferenceable.
* @return Iterator pointing to the element following the erased one,
* or `end()` if `it` referenced the last live element.
*
* Iterators and pointers to other elements remain valid.
*/
iterator erase(const_iterator it) noexcept(std::is_nothrow_destructible_v<T>) {
assert(size_ > 0);
LIBMEM_ASSERT(it != end() && "pool: erase(end())");

pool_iterator inner{it.inner_};
pool_iterator next{inner};
Expand Down
Loading
Loading