xo-umbrella2/xo-facet/include/xo/facet/handlestore/DHandleStore.hpp
Roland Conybeare b6d4acca82
Some checks failed
cmake-docker / cmake-build (clang, clang++) (push) Failing after 10m52s
CI / smoke-test (push) Has been cancelled
cmake-docker / cmake-build (gcc, g++) (push) Has been cancelled
xo-facet: common base align recovers alloc size from ptr [REFACTOR]
2026-09-25 08:20:21 -04:00

372 lines
14 KiB
C++

/** @file DHandleStore.hpp
*
* @author Roland Conybeare, Sep 2026
**/
#pragma once
#include "xo/facet/cx/FacetAppcxCreated.hpp"
#include <xo/arena/DArenaVector.hpp>
#include <xo/arena/DArena.hpp>
#include <stdexcept>
#include <string>
namespace xo::facet {
/** @brief Common base class for DHandleStore<Storage,Handle>
**/
class DHandleStoreBase {
public:
/** assign static base alignment for handle storage.
*
* write-once: enforces requirement that all HandleStore instances
* share the same base alignment.
* Necessary to unlock @ref DArena::obj2arena() for HandleStore pointers.
**/
static void assign_storage_base_align(std::size_t z) {
if ((s_storage_base_align != 0) && (s_storage_base_align != z)) {
throw std::runtime_error
("DHandleStoreBase::assign_storage_base_align"
": storage base alignment is write-once, and a"
" conflicting value was already in force");
}
s_storage_base_align = z;
}
/** base alignment every participating storage arena shares.
*
* 0 until a FacetAppcx has been constructed.
* Reader must no call @ref DArena::obj2arena if this is zero.
**/
static std::size_t storage_base_align() { return s_storage_base_align; }
protected:
/** base alignment for storage.
* allows recovering DArena from an allocated address
* within that arena.
*
* (@see FacetConfig::storage_base_align_)
**/
static std::size_t s_storage_base_align;
};
/** @tparam Storage provides allocator api with explicit clear.
* @tparam Handle represents a generic object reference.
* Expect this to be something like obj<ATop> or obj<AGCObject>
*
* Require:
* - Storage provides full RAllocator method suite.
* Not sure what to do about RAllocator::barrier_assign_aux though
* - Storage reports @c config() and @c name().
* - Storage was configured with alloc headers enabled, i.e.
* @c ArenaConfig::with_store_header_flag(true). Enforced by the ctor;
* see there for why it is a hard requirement rather than a preference
* - Handle provides .reset(), putting it into the empty state
* - Handle reports ATop's _typeseq() and opaque_data(), for
* @ref snapshot. Only those two: a snapshot must not rotate to another
* facet, or it would work for some representations and throw for
* others.
* - Handle is contextually convertible to bool, false iff empty.
* Slot integrity depends on this. It is how release distinguishes
* {occupied, empty} slots.
**/
template <typename Storage,
typename Handle>
class DHandleStore : public DHandleStoreBase {
public:
/** @defgroup mm-handlestore-types **/
///@{
using handle_type = Handle;
using handle_index_type = typename xo::mm::DArenaVector<Handle>::size_type;
using typeseq = typename Storage::typeseq;
using size_type = typename Storage::size_type;
using value_type = typename Storage::value_type;
using range_type = typename Storage::range_type;
///@}
public:
/** @defgroup mm-handlestore-ctors **/
///@{
DHandleStore() = default;
/** @p strong_freelist and @p weak_freelist hold the index positions of
* empty slots in @p strong / @p weak respectively;
* in no particular order.
**/
DHandleStore(Storage && storage,
xo::mm::DArenaVector<Handle> && strong,
xo::mm::DArenaVector<handle_index_type> && strong_freelist,
FacetAppcxCreated evidence)
: storage_{std::move(storage)},
strong_refs_{std::move(strong)},
strong_freelist_{std::move(strong_freelist)},
facetappcx_evidence_{evidence}
{
/* Alloc headers are REQUIRED, not merely useful.
*
* DArena::alloc_info() reads the header that store_header_flag_
* controls. Without it the read is not a graceful failure -- it
* SEGFAULTS (measured 2026-09-15),
*/
if (!storage_.config().store_header_flag_) {
throw std::runtime_error
(std::string("DHandleStore: storage arena must have alloc"
" headers enabled -- see"
" ArenaConfig::with_store_header_flag; arena=")
+ std::string(this->name()));
}
if (storage_.config().base_align_z_ == 0) {
throw std::runtime_error
(std::string("DHandleStore: storage arena must have non-zero base alignment")
+ std::string(this->name()));
}
if (storage_.config().base_align_z_ != DHandleStoreBase::s_storage_base_align) {
throw std::runtime_error
(std::string("DhandleStore: storage base alignment must match DHandleStoreBase")
+ std::string(this->name()));
}
}
/** move-assignment **/
DHandleStore & operator=(DHandleStore && other) = default;
///@}
/** @defgroup mm-handlestore-access-methods **/
///@{
/** false -> not eligible for GC (allocates own memory + not moveable) **/
static constexpr bool is_gc_eligible() { return false; }
FacetAppcxCreated facetappcx_evidence() const { return facetappcx_evidence_; }
std::string_view name() const noexcept { return storage_.name(); }
size_type reserved() const noexcept { return storage_.reserved(); }
size_type size() const noexcept { return storage_.size(); }
size_type committed() const noexcept { return storage_.committed(); }
size_type available() const noexcept { return storage_.available(); }
size_type allocated() const noexcept { return storage_.allocated(); }
/** one entry per pool, in the order below. **/
void visit_pools(const xo::mm::MemorySizeVisitor & fn) const {
storage_.visit_pools(fn);
strong_refs_.visit_pools(fn);
strong_freelist_.visit_pools(fn);
}
/** visit every root slot in order, cleared ones included.
*
* Mirrors @ref visit_pools: lets a reader walk the root set without
* the container being exposed, and without materialising a copy.
* @c RootSetInfo existed to carry that copy until 2026-09-21.
*
* A slot's INDEX is its position in this walk -- which is what lets a
* consumer match a slot against the free list, and why cleared slots
* are visited rather than skipped.
**/
template <typename Fn>
void visit_object_slots(Fn && fn) const {
for (std::size_t i = 0, n = strong_refs_.size(); i < n; ++i)
fn(strong_refs_[i]);
}
/** visit each RELEASED slot's index, in the order the free list
* STORES them -- oldest release first.
*
* Reuse runs the other way: @ref _add_ref pops the BACK, so the LAST
* index visited here is the one the next @ref add_strong_ref takes.
*
* The same SET could be derived from @ref visit_object_slots (a
* released slot is a cleared one), but not the same ORDER, and the
* order is the part a reader cannot reconstruct -- which is why this
* exists rather than the printer inferring the free list from the
* cleared slots.
**/
template <typename Fn>
void visit_free_list(Fn && fn) const {
for (std::size_t i = 0, n = strong_freelist_.size(); i < n; ++i)
fn(strong_freelist_[i]);
}
/** slots ever allocated: a HIGH-WATER MARK, not the population --
* with a free list the vector never shrinks. @ref strong_root_count
* is the population
**/
handle_index_type strong_size() const { return strong_refs_.size(); }
/** slots this root set can hold -- not derivable from the walk **/
handle_index_type strong_capacity() const { return strong_refs_.capacity(); }
/** enumerates the same vectors as visit_pools() **/
bool contains(const void * p) const noexcept {
return (storage_.contains(p)
|| strong_refs_.contains(p) || strong_freelist_.contains(p));
}
xo::mm::AllocError last_error() const noexcept { return storage_.last_error(); }
xo::mm::AllocInfo alloc_info(value_type mem) const noexcept { return storage_.alloc_info(mem); }
range_type alloc_range(xo::mm::DArena & mm) const noexcept { return storage_.alloc_range(mm); }
/** counts non-empty strong slots **/
handle_index_type strong_root_count() const {
return strong_refs_.size() - strong_freelist_.size();
}
///@}
/** @defgroup mm-handlestore-mutable-methods **/
///@{
Storage & storage() { return storage_; }
/* const overload: a snapshot reads bounds and must not be able to
* disturb what it measures
*/
const Storage & storage() const { return storage_; }
bool expand(size_type z) { return storage_.expand(z); }
value_type alloc(typeseq tseq, size_type z) noexcept {
return storage_.alloc(tseq, z);
}
value_type super_alloc(typeseq tseq, size_type z) noexcept {
return storage_.super_alloc(tseq, z);
}
value_type sub_alloc(size_type z, bool complete_flag) noexcept {
return storage_.sub_alloc(z, complete_flag);
}
value_type alloc_copy(value_type src) noexcept { return storage_.alloc_copy(src); }
/** adopt the erased fop (@p iface, @p data) into the strong reference
* set, as a new Handle.
*
* @return pair (i, &h), where i indexes &h in the strong reference
* set. &h is nullptr iff the set is full -- a DArenaVector fixes
* capacity at construction, so that is how root-set exhaustion
* reaches a caller.
*
* @throw std::runtime_error if @p data was not allocated from
* @ref storage_.
*
* This store is the ONLY thing that can make a non-empty Handle (see
* ObjectSlot's Provenance note), which is why the pair arrives here
* rather than an assembled Handle: a caller assembling one could not
* have had its provenance checked. The check was this function's
* documented precondition for a year and is now enforced.
*
* NB @c storage_.contains, not @c this->contains. The latter also
* spans the root-set and free-list arenas, which are mapped WITHOUT
* alloc headers -- a pointer into one would pass and then fail
* DArena::alloc_info, which is exactly the outcome being excluded.
**/
std::pair<handle_index_type, Handle*>
add_strong_ref(const typename Handle::ATop * iface, void * data) {
if (!storage_.contains(data)) {
throw std::runtime_error
(std::string("DHandleStore::add_strong_ref"
": object was not allocated from this store's"
" storage arena; arena=")
+ std::string(this->name()));
}
return _add_ref(strong_refs_, strong_freelist_, Handle(iface, data));
}
/** release the strong slot at @p ix, returning it to the free list.
* Idempotent.
**/
void remove_strong_ref(size_type ix) {
_remove_ref(strong_refs_, strong_freelist_, ix);
}
void clear() {
// 1. clears refs + freelists.
strong_refs_.clear();
strong_freelist_.clear();
// 2. clear storage
storage_.clear();
}
///@}
private:
/** @defgroup mm-handlestore-impl-methods **/
///@{
/** reuse a released slot if there is one, else grow the vector **/
static std::pair<handle_index_type, Handle*>
_add_ref(xo::mm::DArenaVector<Handle> & refs,
xo::mm::DArenaVector<handle_index_type> & freelist,
Handle x)
{
if (!freelist.empty()) {
handle_index_type ix = freelist.back();
freelist.pop_back();
refs[ix] = x;
return std::make_pair(ix, &refs[ix]);
}
auto ix = refs.size();
/* nullptr when the vector is full: a DArenaVector fixes capacity at
* construction, so this is how root-set exhaustion reaches a caller
*/
Handle * ref = refs.push_back(x);
return std::make_pair(ix, ref);
}
/** reset a slot, and if it was non-empty return to freelist **/
static void _remove_ref(xo::mm::DArenaVector<Handle> & refs,
xo::mm::DArenaVector<handle_index_type> & freelist,
size_type ix)
{
if (ix >= refs.size())
return;
if (refs[ix]) {
refs[ix].reset();
/* cannot fail: freelist capacity >= refs capacity, and at most
* every slot is free at once
*/
freelist.push_back(ix);
} else {
/* already released. See remove_strong_ref's note on what this
* does and does not protect
*/
}
}
///@}
private:
/** @defgroup mm-handlestore-instance-vars **/
///@{
/** Storage. Must satisfy allocator api **/
Storage storage_;
/** Strong references.
*
* Promise: memory allocated from @ref storage_ remains
* valid while any non-null references remain in @ref strong_
**/
xo::mm::DArenaVector<Handle> strong_refs_;
/** Index positions of empty slots in @ref strong_refs_ **/
xo::mm::DArenaVector<handle_index_type> strong_freelist_;
/** evidence that a FacetAppcx instance was created **/
FacetAppcxCreated facetappcx_evidence_;
///@}
};
template <typename Handle>
using DHandleArena = DHandleStore<xo::mm::DArena, Handle>;
} /*namespace xo::facet*/
/* end DHandleStore.hpp */