blob: 9747b944ac9801ae207e44e7148d694b617739ef [file] [log] [blame]
// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#pragma once
#include <optional>
#include "pw_assert/assert.h"
#include "pw_bytes/span.h"
#include "pw_sync/mutex.h"
namespace pw::multibuf {
class ChunkRegionTracker;
class OwnedChunk;
/// A handle to a contiguous slice of data.
///
/// A ``Chunk`` is similar to a ``ByteSpan``, but is aware of the underlying
/// memory allocation, and is able to split, shrink, and grow into neighboring
/// empty space.
///
/// This class is optimized to allow multiple owners to write into neighboring
/// regions of the same allocation. One important usecase for this is
/// communication protocols which want to reserve space at the front or rear of
/// a buffer for headers or footers.
///
/// In order to support zero-copy DMA of communications buffers, allocators can
/// create properly-aligned ``Chunk`` regions in appropriate memory. The driver
/// can then ``DiscardFront`` in order to reserve bytes for headers,
/// ``Truncate`` in order to reserve bytes for footers, and then pass the
/// ``Chunk`` to the user to fill in. The header and footer space can then
/// be reclaimed using the ``ClaimPrefix`` and ``ClaimSuffix`` methods.
class Chunk {
public:
Chunk() = delete;
// Not copyable or movable.
Chunk(Chunk&) = delete;
Chunk& operator=(Chunk&) = delete;
Chunk(Chunk&&) = delete;
Chunk& operator=(Chunk&&) = delete;
/// Creates the first ``Chunk`` referencing a whole region of memory.
///
/// This must only be called once per ``ChunkRegionTracker``, when the region
/// is first created. Multiple calls will result in undefined behavior.
///
/// Returns ``std::nullopt`` if ``AllocateChunkStorage`` returns ``nullptr``.
static std::optional<OwnedChunk> CreateFirstForRegion(
ChunkRegionTracker& region_tracker);
std::byte* data() { return span_.data(); }
const std::byte* data() const { return span_.data(); }
size_t size() const { return span_.size(); }
ByteSpan span() { return span_; }
ConstByteSpan span() const { return span_; }
std::byte& operator[](size_t index) { return span_[index]; }
const std::byte& operator[](size_t index) const { return span_[index]; }
// Container declarations
using element_type = std::byte;
using value_type = std::byte;
using size_type = size_t;
using difference_type = ptrdiff_t;
using pointer = std::byte*;
using const_pointer = const std::byte*;
using reference = std::byte&;
using const_reference = const std::byte&;
using iterator = std::byte*;
using const_iterator = const std::byte*;
using reverse_iterator = std::byte*;
using const_reverse_iterator = const std::byte*;
std::byte* begin() { return span_.data(); }
const std::byte* cbegin() const { return span_.data(); }
std::byte* end() { return span_.data() + span_.size(); }
const std::byte* cend() const { return span_.data() + span_.size(); }
/// Returns if ``next_chunk`` is mergeable into the end of this ``Chunk``.
///
/// This will only succeed when the two ``Chunk`` s are adjacent in
/// memory and originated from the same allocation.
[[nodiscard]] bool CanMerge(const Chunk& next_chunk) const;
/// Attempts to merge ``next_chunk`` into the end of this ``Chunk``.
///
/// If the chunks are successfully merged, this ``Chunk`` will be extended
/// forwards to encompass the space of ``next_chunk``, and ``next_chunk``
/// will be emptied and ``Release``d.
///
/// This will only succeed when the two ``Chunk`` s are adjacent in
/// memory and originated from the same allocation.
///
/// If the chunks are not mergeable, neither ``Chunk`` will be modified.
bool Merge(OwnedChunk& next_chunk);
/// Attempts to add ``bytes_to_claim`` to the front of this buffer by
/// advancing its range backwards in memory. Returns ``true`` if the operation
/// succeeded.
///
/// This will only succeed if this ``Chunk`` points to a section of a region
/// that has unreferenced bytes preceeding it. For example, a ``Chunk`` which
/// has been shrunk using ``DiscardFront`` can be re-expanded using
/// ``ClaimPrefix``.
///
/// This method will acquire a mutex and is not IRQ safe.
[[nodiscard]] bool ClaimPrefix(size_t bytes_to_claim);
/// Attempts to add ``bytes_to_claim`` to the front of this buffer by
/// advancing its range forwards in memory. Returns ``true`` if the operation
/// succeeded.
///
/// This will only succeed if this ``Chunk`` points to a section of a region
/// that has unreferenced bytes following it. For example, a ``Chunk`` which
/// has been shrunk using ``Truncate`` can be re-expanded using
///
/// This method will acquire a mutex and is not IRQ safe. /// ``ClaimSuffix``.
[[nodiscard]] bool ClaimSuffix(size_t bytes_to_claim);
/// Shrinks this handle to refer to the data beginning at offset
/// ``bytes_to_discard``.
///
/// Does not modify the underlying data.
///
/// This method will acquire a mutex and is not IRQ safe.
void DiscardFront(size_t bytes_to_discard);
/// Shrinks this handle to refer to data in the range ``begin..<end``.
///
/// Does not modify the underlying data.
///
/// This method will acquire a mutex and is not IRQ safe.
void Slice(size_t begin, size_t end);
/// Shrinks this handle to refer to only the first ``len`` bytes.
///
/// Does not modify the underlying data.
///
/// This method will acquire a mutex and is not IRQ safe.
void Truncate(size_t len);
/// Attempts to shrink this handle to refer to the data beginning at
/// offset ``bytes_to_take``, returning the first ``bytes_to_take`` bytes as
/// a new ``OwnedChunk``.
///
/// If the inner call to ``AllocateChunkClass`` fails, this function
/// will return ``std::nullopt` and this handle's span will not change.
///
/// This method will acquire a mutex and is not IRQ safe.
std::optional<OwnedChunk> TakeFront(size_t bytes_to_take);
/// Attempts to shrink this handle to refer only the first
/// ``len - bytes_to_take`` bytes, returning the last ``bytes_to_take``
/// bytes as a new ``OwnedChunk``.
///
/// If the inner call to ``AllocateChunkClass`` fails, this function
/// will return ``std::nullopt`` and this handle's span will not change.
///
/// This method will acquire a mutex and is not IRQ safe.
std::optional<OwnedChunk> TakeTail(size_t bytes_to_take);
private:
Chunk(ChunkRegionTracker* region_tracker, ByteSpan span)
: region_tracker_(region_tracker),
next_in_region_(nullptr),
prev_in_region_(nullptr),
span_(span) {}
// NOTE: these functions are logically
// `PW_EXCLUSIVE_LOCKS_REQUIRED(region_tracker_->lock_)`, however this
// annotation cannot be applied due to the forward declaration of
// `region_tracker_`.
void InsertAfterInRegionList(Chunk* new_chunk);
void InsertBeforeInRegionList(Chunk* new_chunk);
void RemoveFromRegionList();
/// Frees this ``Chunk``. Future accesses to this class after calls to
/// ``Free`` are undefined behavior.
///
/// Decrements the reference count on the underlying chunk of data.
///
/// Does not modify the underlying data, but may cause it to be deallocated
/// if this was the only remaining ``Chunk`` referring to its region.
///
/// This method will acquire a mutex and is not IRQ safe.
void Free();
/// The region_tracker_ for this chunk.
ChunkRegionTracker* const region_tracker_;
/// Pointer to the next chunk in the same ``region_tracker_``.
///
/// Guarded by ``region_tracker_->lock_`` (but not annotated as such due to
/// the fact that annotations aren't smart enough to understand that all
/// chunks within a region share a tracker + lock).
///
/// ``nullptr`` if this is the last ``Chunk`` in its ``region_tracker_``.
Chunk* next_in_region_;
/// Pointer to the previous chunk in the same ``region_tracker_``.
///
/// Guarded by ``region_tracker_->lock_`` (but not annotated as such due to
/// the fact that annotations aren't smart enough to understand that all
/// chunks within a region share a tracker + lock).
///
/// ``nullptr`` if this is the first ``Chunk`` in its ``region_tracker_``.
Chunk* prev_in_region_;
/// Pointer to the next chunk in a ``MultiBuf``.
///
/// This is ``nullptr`` if this chunk is not in a ``MultiBuf``, or is the
/// last element of a ``MultiBuf``.
//
// reserved for use by the MultiBuf class.
Chunk* next_in_buf_;
/// Pointer to the sub-region to which this chunk has exclusive access.
///
/// This ``span_`` is conceptually owned by this ``Chunk`` object, and
/// may be read or written to by a ``Chunk`` user (the normal rules of
/// thread compatibility apply-- `const` methods may be called
/// concurrently, non-`const` methods may not).
///
/// Neighboring ``Chunk`` objects in this region looking to expand via
/// ``ClaimPrefix`` or ``ClaimSuffix`` may read this span's
/// boundaries. These other ``Chunk``s must acquire
/// ``region_tracker_->lock_`` before reading, and this ``Chunk`` must
/// acquire ``region_tracker_->lock_`` before changing ``span_``.
ByteSpan span_;
friend class OwnedChunk; // for ``Free``.
friend class MultiBuf; // for ``Free`` and ``next_in_buf_``.
};
/// An object that manages a single allocated region which is referenced by one
/// or more ``Chunk`` objects.
///
/// This class is typically implemented by ``MultiBufAllocator``
/// implementations in order to customize the behavior of region deallocation.
///
/// ``ChunkRegionTracker`` s have three responsibilities:
///
/// - Tracking the region of memory into which ``Chunk`` s can expand.
/// This is reported via the ``Region`` method. ``Chunk`` s in this region
/// can refer to memory within this region sparsely, but they can grow or
/// shrink so long as they stay within the bounds of the ``Region``.
///
/// - Deallocating the region and the ``ChunkRegionTracker`` itself.
/// This is implemented via the ``Destroy`` method, which is called once all
/// of the ``Chunk`` s in a region have been released.
///
/// - Allocating and deallocating space for ``Chunk`` classes. This is merely
/// allocating space for the ``Chunk`` object itself, not the memory to which
/// it refers. This can be implemented straightforwardly by delegating to an
/// existing generic allocator such as ``malloc`` or a
/// ``pw::allocator::Allocator`` implementation.
class ChunkRegionTracker {
protected:
ChunkRegionTracker() = default;
virtual ~ChunkRegionTracker() = default;
/// Destroys the ``ChunkRegionTracker``.
///
/// Typical implementations will call ``std::destroy_at(this)`` and then free
/// the memory associated with the region and the tracker.
virtual void Destroy() = 0;
/// Returns the entire span of the region being managed.
///
/// ``Chunk`` s referencing this tracker will not expand beyond this region,
/// nor into one another's portions of the region.
///
/// This region must not change for the lifetime of this
/// ``ChunkRegionTracker``.
virtual ByteSpan Region() const = 0;
/// Returns a pointer to ``sizeof(Chunk)`` bytes.
/// Returns ``nullptr`` on failure.
virtual void* AllocateChunkClass() = 0;
/// Deallocates a pointer returned by ``AllocateChunkClass``.
virtual void DeallocateChunkClass(void*) = 0;
private:
/// A lock used to manage an internal linked-list of ``Chunk``s.
///
/// This allows chunks to:
/// - know whether they can expand to fill neighboring regions of memory.
/// - know when the last chunk has been destructed, triggering `Destroy`.
pw::sync::Mutex lock_;
friend Chunk;
};
/// An RAII handle to a contiguous slice of data.
///
/// Note: ``OwnedChunk`` may acquire a ``pw::sync::Mutex`` during destruction,
/// and so must not be destroyed within ISR contexts.
class OwnedChunk {
public:
OwnedChunk() : inner_(nullptr) {}
OwnedChunk(OwnedChunk&& other) noexcept : inner_(other.inner_) {
other.inner_ = nullptr;
}
OwnedChunk& operator=(OwnedChunk&& other) noexcept {
inner_ = other.inner_;
other.inner_ = nullptr;
return *this;
}
/// This method will acquire a mutex and is not IRQ safe.
~OwnedChunk() { Release(); }
std::byte* data() { return span().data(); }
const std::byte* data() const { return span().data(); }
size_t size() const { return span().size(); }
ByteSpan span() { return inner_ == nullptr ? ByteSpan() : inner_->span(); }
ConstByteSpan span() const {
return inner_ == nullptr ? ConstByteSpan() : inner_->span();
}
std::byte& operator[](size_t index) { return span()[index]; }
std::byte operator[](size_t index) const { return span()[index]; }
Chunk& operator*() { return *inner_; }
const Chunk& operator*() const { return *inner_; }
Chunk* operator->() { return inner_; }
const Chunk* operator->() const { return inner_; }
/// Decrements the reference count on the underlying chunk of data and
/// empties this handle so that `span()` now returns an empty (zero-sized)
/// span.
///
/// Does not modify the underlying data, but may cause it to be deallocated
/// if this was the only remaining ``Chunk`` referring to its region.
///
/// This method is equivalent to ``{ Chunk _unused = std::move(chunk_ref); }``
///
/// This method will acquire a mutex and is not IRQ safe.
void Release();
/// Returns the contained ``Chunk*`` and empties this ``OwnedChunk`` without
/// releasing the underlying ``Chunk``.
Chunk* Take() && {
Chunk* result = inner_;
inner_ = nullptr;
return result;
}
private:
/// Constructs a new ``OwnedChunk`` from an existing ``Chunk*``.
OwnedChunk(Chunk* inner) : inner_(inner) {}
/// A pointer to the owned ``Chunk``.
Chunk* inner_;
/// Allow ``Chunk`` and ``MultiBuf`` to create ``OwnedChunk``s using the
/// private constructor above.
friend class Chunk;
friend class MultiBuf;
};
} // namespace pw::multibuf