611 lines
22 KiB
C++
611 lines
22 KiB
C++
/***
|
|
Copyright (C) 2021 J Reece Wilson (a/k/a "Reece"). All rights reserved.
|
|
|
|
File: ByteBuffer.hpp
|
|
Date: 2021-8-5
|
|
Author: Reece
|
|
***/
|
|
#pragma once
|
|
|
|
#include <Aurora/Locale/ECodePage.hpp>
|
|
|
|
namespace Aurora::Memory
|
|
{
|
|
enum class EStringType
|
|
{
|
|
eStringTerminated,
|
|
eStringByte,
|
|
eStringWord,
|
|
eStringDword,
|
|
eStringQword
|
|
};
|
|
|
|
static const auto kBufferPageSize = 512;
|
|
//static const auto kBufferBasePower = 8;
|
|
static const auto kBufferInitialPower = 9;// -kBufferBasePower; // 4-bit integer
|
|
|
|
/***
|
|
* A bytebuffer object represents a "page" aligned (optionally) resizable buffer **or** a ring buffer.
|
|
|
|
* Trivial linear serialization use cases will follow the linear fast paths, not those of a ring buffer.
|
|
*
|
|
* Ring buffers are used to wrap streams when the consumer may expect arbitrary stream seeks of an otherwise
|
|
* limited consume-once stream
|
|
*
|
|
* EG (old):
|
|
* -> Peeking a header in a datagram, or tcp stream; where instead of freeing the datagram or double
|
|
* buffering the network stack when required, a ring buffer is used to prevent reallocation on each frame
|
|
* -> Peeking, or seeking back after, compression read. A compression api could be fed on-known-input-available
|
|
* or on-demand, writing to its write head pointer, while never running out of space so long as the
|
|
* decompressed ring read head continues moving
|
|
*
|
|
* EG (2022):
|
|
* -> AuProtocol ProtocolPiece buffers
|
|
* -> AuCompression internal buffer
|
|
* -> A socket's output stream / space for user-write and overlapped submission
|
|
* -> ...
|
|
*
|
|
* Deprecates INetworkStream, fixes allocation issues around compression backends
|
|
* Superseeds abuse of AuList<AuUInt8> for binary blobs alongside other fixed u8 arrays
|
|
*/
|
|
struct ByteBuffer
|
|
{
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Stable ByteBuffer ABI Header; length and read/write head pointers //
|
|
///////////////////////////////////////////////////////////////////////
|
|
|
|
/// Internal capacity to mitigate trivial reallocs
|
|
AuUInt allocSize;
|
|
|
|
/// Abstract size
|
|
AuUInt length;
|
|
|
|
/// Buffer pointer
|
|
AuUInt8 *base {};
|
|
/// Stream pointer
|
|
AuUInt8 *readPtr {};
|
|
/// Stream pointer
|
|
AuUInt8 *writePtr {};
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Stable ByteBuffer ABI Header; u32 flags //
|
|
///////////////////////////////////////////////////////////////////////
|
|
/// Is ring buffer?
|
|
AuUInt8 flagCircular : 1 {};
|
|
|
|
/// Should resize linear buffer to accommodate additional writes
|
|
AuUInt8 flagExpandable : 1 {};
|
|
|
|
AuUInt8 flagReadError : 1 {};
|
|
AuUInt8 flagWriteError : 1 {};
|
|
AuUInt8 flagNoFree : 1 {};
|
|
AuUInt8 flagNoRealloc: 1 {};
|
|
AuUInt8 flagAlwaysExpandable : 1 {}; // it's a long story from how we got from string views to std::vector<std::uint8_t>s to current day AuByteBuffer.
|
|
// anyway long story short, if you want a buffered api to write into us linearly and grow, enable me.
|
|
// if you just want ::Write and similar functions to work keem me false and enable flagExpandable.
|
|
// flagExpandable is for when the default constructor is called. i'm for apis that use us as an interface to grow.
|
|
// TODO: flag: allow circular overrun to allow for 100% access of the buffer from either read head
|
|
// - implicit padding
|
|
AuUInt8 scaleSize {};//
|
|
///////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* @brief Move constructor
|
|
* @param buffer
|
|
*/
|
|
inline ByteBuffer(ByteBuffer &&buffer)
|
|
{
|
|
this->base = buffer.base;
|
|
this->length = buffer.length;
|
|
this->allocSize = buffer.length;
|
|
this->writePtr = this->base + (buffer.writePtr - buffer.base);
|
|
this->readPtr = this->base + (buffer.readPtr - buffer.base);
|
|
this->flagCircular = buffer.flagCircular;
|
|
this->flagExpandable = buffer.flagExpandable;
|
|
this->scaleSize = buffer.scaleSize;
|
|
this->flagReadError = buffer.flagReadError;
|
|
this->flagWriteError = buffer.flagWriteError;
|
|
buffer.base = {};
|
|
buffer.length = {};
|
|
buffer.allocSize = {};
|
|
buffer.writePtr = {};
|
|
buffer.readPtr = {};
|
|
buffer.flagCircular = {};
|
|
buffer.flagExpandable = {};
|
|
buffer.flagAlwaysExpandable = {};
|
|
buffer.scaleSize = {};
|
|
}
|
|
|
|
/**
|
|
* @brief Copy with possible preserve pointers
|
|
* @param buffer
|
|
* @param preservePointers
|
|
*/
|
|
inline ByteBuffer(const ByteBuffer &buffer, bool preservePointers = true)
|
|
{
|
|
if (buffer.length)
|
|
{
|
|
this->base = ZAlloc<AuUInt8 *>(buffer.length);
|
|
}
|
|
this->scaleSize = buffer.scaleSize;
|
|
this->flagCircular = buffer.flagCircular;
|
|
this->flagExpandable = buffer.flagExpandable;
|
|
this->flagAlwaysExpandable = buffer.flagAlwaysExpandable;
|
|
if (!this->base)
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
this->length = buffer.length;
|
|
this->allocSize = buffer.length;
|
|
if (preservePointers)
|
|
{
|
|
this->writePtr = this->base + (buffer.writePtr - buffer.base);
|
|
this->readPtr = this->base + (buffer.readPtr - buffer.base);
|
|
}
|
|
else
|
|
{
|
|
this->writePtr = this->base;
|
|
this->readPtr = this->base;
|
|
}
|
|
AuMemcpy(this->base, buffer.base, this->length);
|
|
}
|
|
|
|
/**
|
|
* @brief Copy pointer range into a new ring or byte buffer
|
|
* @param in
|
|
* @param length
|
|
* @param circular
|
|
* @param expandable
|
|
*/
|
|
inline ByteBuffer(const void *in, AuUInt length, bool circular = false, bool expandable = false) : flagCircular(circular), flagExpandable(expandable), flagReadError(0), flagWriteError(0)
|
|
{
|
|
this->scaleSize = kBufferInitialPower;
|
|
this->base = length ? ZAlloc<AuUInt8 *>(length) : nullptr;
|
|
if (!this->base)
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
if (!in)
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
this->length = length;
|
|
this->allocSize = length;
|
|
this->readPtr = this->base;
|
|
this->writePtr = this->readPtr + this->length;
|
|
AuMemcpy(this->base, in, this->length);
|
|
}
|
|
|
|
inline ByteBuffer(const AuList<AuUInt8> &vector, bool circular = false, bool expandable = false) : flagCircular(circular), flagExpandable(expandable), flagReadError(0), flagWriteError(0)
|
|
{
|
|
this->scaleSize = kBufferInitialPower;
|
|
this->base = vector.size() ? ZAlloc<AuUInt8 *>(vector.size()) : nullptr;
|
|
if (!this->base)
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
this->length = vector.size();
|
|
this->allocSize = this->length;
|
|
this->readPtr = this->base;
|
|
this->writePtr = this->readPtr + this->length;
|
|
AuMemcpy(this->base, vector.data(), this->length);
|
|
}
|
|
|
|
inline ByteBuffer(AuUInt length, bool circular = false, bool expandable = false) : flagCircular(circular), flagExpandable(expandable), flagReadError(0), flagWriteError(0)
|
|
{
|
|
this->scaleSize = kBufferInitialPower;
|
|
if (!length)
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
this->base = ZAlloc<AuUInt8 *>(length);
|
|
if (!this->base)
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
this->length = length;
|
|
this->allocSize = length;
|
|
this->readPtr = this->base;
|
|
this->writePtr = this->base;
|
|
}
|
|
|
|
inline ByteBuffer(AuUInt length, AuUInt alignment, bool circular = false, bool expandable = false) : flagCircular(circular), flagExpandable(expandable), flagReadError(0), flagWriteError(0)
|
|
{
|
|
if (!length)
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
this->scaleSize = kBufferInitialPower;
|
|
this->base = ZAlloc<AuUInt8 *>(length, alignment);
|
|
if (!this->base)
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
this->length = length;
|
|
this->allocSize = length;
|
|
this->readPtr = this->base;
|
|
this->writePtr = this->base;
|
|
}
|
|
|
|
template<typename T>
|
|
ByteBuffer(T *base, T *end, bool circular = false, bool expandable = false) : flagCircular(circular), flagExpandable(expandable), flagReadError(0), flagWriteError(0)
|
|
{
|
|
if (!base)
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
if (!end)
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
auto length = static_cast<AuUInt>(end - base) * sizeof(T);
|
|
this->base = length ? ZAlloc<AuUInt8 *>(length) : nullptr;
|
|
this->scaleSize = kBufferInitialPower;
|
|
if (!this->base)
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
this->length = length;
|
|
this->allocSize = length;
|
|
this->readPtr = this->base;
|
|
this->writePtr = this->base + length;
|
|
AuMemcpy(this->base, base, length);
|
|
}
|
|
|
|
/**
|
|
* @brief Default constructor, allocates an auto-expanding linear bytebuffer
|
|
*/
|
|
inline ByteBuffer() : flagCircular(0), flagExpandable(true), flagReadError(0), flagWriteError(0)
|
|
{
|
|
this->base = {};
|
|
this->length = {};
|
|
this->allocSize = {};
|
|
this->readPtr = {};
|
|
this->writePtr = {};
|
|
this->scaleSize = kBufferInitialPower;
|
|
}
|
|
|
|
inline ~ByteBuffer()
|
|
{
|
|
if (this->base && !this->flagNoFree)
|
|
{
|
|
Free(this->base);
|
|
}
|
|
}
|
|
|
|
inline void ResetPositions()
|
|
{
|
|
this->flagReadError = 0;
|
|
this->flagWriteError = 0;
|
|
this->readPtr = base;
|
|
this->writePtr = base;
|
|
}
|
|
|
|
// utils: Iterator
|
|
|
|
/**
|
|
* @brief base of the bytebuffer
|
|
* @return
|
|
*/
|
|
inline auline AuUInt8 * data() const;
|
|
|
|
/**
|
|
* @brief size of the byte array as requested by the caller (actual allocation may differ, ::allocSize)
|
|
* @return
|
|
*/
|
|
inline auline AuUInt size() const;
|
|
|
|
/**
|
|
* @brief linear read begin
|
|
* @warning writers should use ::GetLinearWriteable(uDesiredLength) or ::GetOrAllocateLinearWriteable(...)
|
|
* @return
|
|
*/
|
|
inline auline const AuUInt8 * begin();
|
|
inline auline const AuUInt8 * cbegin() const;
|
|
|
|
/**
|
|
* @brief linear read end
|
|
* @warning writers should use ::GetLinearWriteable(uDesiredLength) or ::GetOrAllocateLinearWriteable(...)
|
|
* @return
|
|
*/
|
|
inline auline const AuUInt8 * end();
|
|
inline auline const AuUInt8 * cend() const;
|
|
|
|
inline auline bool empty() const;
|
|
|
|
inline void clear();
|
|
inline void resize(AuUInt size);
|
|
inline void reserve(AuUInt size);
|
|
|
|
// utils: Utils to alternative types
|
|
inline auline AuList<AuUInt8> ToVector() const;
|
|
|
|
inline AuUInt32 GetAllocationPower() const;
|
|
inline operator MemoryViewRead() const;
|
|
inline operator MemoryViewWrite();
|
|
|
|
// utils: Internal buffer comparison
|
|
inline bool operator ==(const AuList<AuUInt8> &) const;
|
|
inline bool operator ==(const MemoryViewRead &) const;
|
|
inline bool operator ==(const ByteBuffer &) const;
|
|
|
|
// utils: Move assignment
|
|
inline ByteBuffer &operator =(ByteBuffer &&);
|
|
inline ByteBuffer &operator =(const ByteBuffer &buffer);
|
|
|
|
// utils: &byteArray[n]
|
|
|
|
/**
|
|
* @brief read u8 relative to the read head
|
|
* @param idx
|
|
* @return
|
|
*/
|
|
inline AuUInt8 &operator [](AuUInt idx) const;
|
|
|
|
// utils: if (byteArray) -> if (byteArray->IsValid())
|
|
inline operator bool() const;
|
|
|
|
inline AuList<AuUInt8> RemainingBytesToVector(bool endAtWrite = true) const;
|
|
// ... utils are mostly const functions that provide language intrinsics, access, and container-like compatibility
|
|
// ByteBuffer specific utils can be found under Utilities
|
|
|
|
// Seek / Position
|
|
inline auline bool ReaderTryGoForward(AuUInt32 offset);
|
|
inline auline bool ReaderTryGoBack(AuUInt32 offset);
|
|
|
|
inline auline bool WriterTryGoForward(AuUInt32 offset);
|
|
|
|
inline auline AuUInt RemainingWrite(bool endAtRead = true) const;
|
|
inline auline AuUInt RemainingBytes(bool endAtWrite = true) const;
|
|
|
|
inline auline bool Skip(AuUInt count);
|
|
inline auline AuUInt GetReadOffset() const;
|
|
inline auline AuUInt GetWriteOffset() const;
|
|
|
|
inline auline void ResetReadPointer();
|
|
|
|
inline auline MemoryViewRead GetNextLinearRead();
|
|
inline auline MemoryViewWrite GetNextLinearWrite();
|
|
|
|
inline auline bool CanWrite(AuUInt length);
|
|
inline auline bool CanRead(AuUInt length);
|
|
|
|
/**
|
|
* @brief Returns a linear amount of memory for exactly length of linear bytes
|
|
* @param length
|
|
* @return
|
|
*/
|
|
inline auline MemoryViewRead GetLinearReadableForExactly(AuUInt length)
|
|
{
|
|
return GetLinearReadable(length);
|
|
}
|
|
|
|
inline auline MemoryViewWrite GetLinearWriteableForExactly(AuUInt length)
|
|
{
|
|
return GetLinearWriteable(length);
|
|
}
|
|
|
|
/**
|
|
* @brief Returns a linear amount of memory of at least length
|
|
* @param length
|
|
* @return
|
|
*/
|
|
inline auline MemoryViewRead GetLinearReadableForAtleast(AuUInt length);
|
|
|
|
inline auline MemoryViewWrite GetLinearWriteableForAtleast(AuUInt length);
|
|
|
|
/**
|
|
* @brief Returns a linear amount of memory capped to length
|
|
* @param length
|
|
* @return
|
|
*/
|
|
inline auline MemoryViewRead GetLinearReadableForNoMore(AuUInt length);
|
|
|
|
inline auline MemoryViewWrite GetLinearWriteableForNoMore(AuUInt length);
|
|
|
|
/// legacy: ForExactly variant
|
|
inline auline MemoryViewRead GetLinearReadable(AuUInt length);
|
|
|
|
/// legacy: ForExactly variant
|
|
inline auline MemoryViewWrite GetLinearWriteable(AuUInt length);
|
|
|
|
/**
|
|
* @brief Long story short, AuList<AuUInt8> ~= std::vector<AuUInt8>:
|
|
* AuByteBuffer should be (was) a drag and drop replacement for trash code that uses lists as bytebuffers. This is opposed to ye old std::string contains anything buffers.
|
|
* Either way, to the point, AuByteBuffer's default constructor is to allow for expandability on Write. GetOrAllocateLinearWriteable is used by certain parse APIs to
|
|
* expand ONCE into a buffer that is uninitialized. Should the caller setup the AuByteBuffer, preallocate a runway, etc, etc, GetOrAllocateLinearWriteable will run on
|
|
* the memory - the read/write heads of the already setup buffer.
|
|
* Otherwise, the default constructor shall allow this to ALLOCATE ONCE given a buffer in an uninitialized/!IsValid()/!refByteBuffer state.
|
|
* @param length
|
|
* @return
|
|
*/
|
|
inline auline MemoryViewWrite GetOrAllocateLinearWriteable(AuUInt length);
|
|
|
|
// Memory operations
|
|
|
|
inline auline bool Allocate(AuUInt length);
|
|
inline auline bool Allocate(AuUInt length, AuUInt alignment);
|
|
inline auline bool SetBuffer(MemoryViewRead readView, bool bMoveWriteHeadForReaders = true);
|
|
|
|
/**
|
|
* @brief Releases excess memory (like, shrink to fit in c++)
|
|
* @return
|
|
*/
|
|
inline auline void GC();
|
|
|
|
/**
|
|
* @brief Releases all resources and resets the bytebuffer without an allocation
|
|
*/
|
|
inline void Reset();
|
|
|
|
/**
|
|
* @brief Expands the underlying buffer allocation to at least length.
|
|
* Does nothing on failure. Programs can try to allocate in real time
|
|
* and handle the write error flag condition from there. To pull the
|
|
* real buffer size, see member field allocSize.
|
|
* @param length
|
|
*/
|
|
inline void Reserve(AuUInt length);
|
|
|
|
/**
|
|
* @brief Is allocated or dummy object?
|
|
* @return
|
|
*/
|
|
inline auline bool IsEmpty() const;
|
|
|
|
/**
|
|
* @brief Is an error flag set?
|
|
* @return
|
|
*/
|
|
inline auline bool HasStreamError() const;
|
|
|
|
/**
|
|
* @brief Returns true so long as
|
|
* 1) an error has not occured or,
|
|
* 1) the relevant error flag was reset; and
|
|
* 2) there is a valid underlying buffer
|
|
*
|
|
* @return
|
|
*/
|
|
inline auline bool IsValid() const;
|
|
|
|
/**
|
|
* @brief Allocate at least length bytes, without adjusting the relative read/write head offsets
|
|
* @param length
|
|
* @return
|
|
*/
|
|
inline auline bool Resize(AuUInt length);
|
|
|
|
// Basic Read Write
|
|
|
|
inline auline AuUInt Write(const void *buffer, AuUInt requestLength);
|
|
inline auline AuUInt Read(void *out, AuUInt requestedLength, bool peek = false);
|
|
|
|
inline auline AuUInt Write(const MemoryViewRead &read)
|
|
{
|
|
return Write(read.ptr, read.length);
|
|
}
|
|
|
|
inline auline AuUInt Read(const MemoryViewWrite &write)
|
|
{
|
|
return Read(write.ptr, write.length);
|
|
}
|
|
|
|
// String API
|
|
|
|
inline bool WriteString(std::string_view string, EStringType type = EStringType::eStringDword, Locale::ECodePage codepage = Locale::ECodePage::eUTF8);
|
|
inline bool ReadString(AuString &string, EStringType type = EStringType::eStringDword, Locale::ECodePage codepage = Locale::ECodePage::eUTF8);
|
|
|
|
// Copy, concat, etc
|
|
inline AuUInt WriteFromEx(ByteBuffer &buffer,
|
|
AuUInt uLength);
|
|
inline bool WriteFrom(ByteBuffer &buffer);
|
|
|
|
// Utilities
|
|
inline bool Trim(AuUInt tail);
|
|
inline bool Pad(AuUInt16 aPowOf2, AuUInt8 magicCharacter = '\x00');
|
|
inline bool Fill(AuUInt length, AuUInt8 magicCharacter = '\x00');
|
|
|
|
// Templated read/write for **PODs**
|
|
// (yes, there's some support for non-primitive classes this early into the abstraction.)
|
|
// (AuString and some other containers may work, but really, we need a typed bytebuffer.)
|
|
template<typename T>
|
|
T Read();
|
|
|
|
// Panics if flagReadError is true by the end of deserialization
|
|
template<typename T>
|
|
T ReadChecked();
|
|
|
|
template<typename T>
|
|
bool Write(const T &in);
|
|
|
|
template<typename T>
|
|
bool Write(T &in);
|
|
|
|
template<typename T>
|
|
bool Read(T &out);
|
|
|
|
template<typename T>
|
|
bool WriteTagged(const T &in);
|
|
|
|
template<typename T>
|
|
bool WriteTagged(T &in);
|
|
|
|
template<typename T>
|
|
bool ReadTagged(T &out);
|
|
|
|
//
|
|
|
|
inline AuUInt calcDifferenceBetweenHeadsUnsigned(AuUInt8 *pHeadPointer, AuUInt8 *pSubtrahend)
|
|
{
|
|
if (pHeadPointer > pSubtrahend)
|
|
{
|
|
return pHeadPointer - pSubtrahend;
|
|
}
|
|
else
|
|
{
|
|
return (pSubtrahend - this->base) + ((this->base + this->length) - pHeadPointer);
|
|
}
|
|
}
|
|
|
|
inline AuSInt calcDifferenceBetweenHeadsSigned(AuUInt8 *pHeadPointer, AuUInt8 *pSubtrahend)
|
|
{
|
|
if (pHeadPointer > pSubtrahend)
|
|
{
|
|
return pHeadPointer - pSubtrahend;
|
|
}
|
|
else
|
|
{
|
|
return -(pSubtrahend - this->base);
|
|
}
|
|
}
|
|
};
|
|
|
|
static ByteBuffer NewResizableBuffer(AuUInt32 length = 0)
|
|
{
|
|
return ByteBuffer(length, false, true);
|
|
}
|
|
|
|
static ByteBuffer NewRingBuffer(AuUInt32 length = 1024 * 5)
|
|
{
|
|
return ByteBuffer(length, true, false);
|
|
}
|
|
|
|
struct SharedByteBuffer : ByteBuffer
|
|
{
|
|
AuSPtr<void> memory;
|
|
|
|
inline SharedByteBuffer(AuSPtr<MemoryViewWrite> pReadView) : ByteBuffer()
|
|
{
|
|
this->allocSize = 0;
|
|
this->base = (AuUInt8 *)pReadView->ptr;
|
|
this->length = pReadView->length;
|
|
this->readPtr = this->base;
|
|
this->writePtr = this->base + this->length;
|
|
this->flagNoFree = true;
|
|
this->memory = pReadView;
|
|
}
|
|
|
|
inline SharedByteBuffer(AuSPtr<void> pRAIIParentOwner, MemoryViewWrite view) : ByteBuffer()
|
|
{
|
|
this->allocSize = 0;
|
|
this->base = (AuUInt8 *)view.ptr;
|
|
this->length = view.length;
|
|
this->readPtr = this->base;
|
|
this->writePtr = this->base + this->length;
|
|
this->flagNoFree = true;
|
|
this->memory = pRAIIParentOwner;
|
|
}
|
|
};
|
|
} |