/*** 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 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 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 flagAlwaysExpandable : 1 {}; // it's a long story from how we got from string views to std::vectors 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 = FAlloc(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 ? FAlloc(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 &vector, bool circular = false, bool expandable = false) : flagCircular(circular), flagExpandable(expandable), flagReadError(0), flagWriteError(0) { this->scaleSize = kBufferInitialPower; this->base = vector.size() ? FAlloc(vector.size()) : nullptr; if (!this->base) { Reset(); return; } this->length = vector.size(); this->allocSize = vector.size(); 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(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(length, alignment); if (!this->base) { Reset(); return; } this->length = length; this->allocSize = length; this->readPtr = this->base; this->writePtr = this->base; } template 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(end - base) * sizeof(T); this->base = length ? ZAlloc(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) { 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 ToVector() const; inline AuUInt32 GetAllocationPower() const; inline operator MemoryViewRead() const; inline operator MemoryViewWrite(); // utils: Internal buffer comparison inline bool operator ==(const AuList &) 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 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 ~= std::vector: * 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, bool fast = true); inline auline bool Allocate(AuUInt length, AuUInt alignment, bool fast = true); 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); // 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'); // Typed read/write template T Read(); template bool Write(const T &in); template bool Read(T &out); }; 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 memory; inline SharedByteBuffer(AuSPtr 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 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; } }; }