AyCode.Core/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs

253 lines
8.7 KiB
C#

using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Binary output that writes directly to an IBufferWriter (e.g. SignalR pipe, network stream).
///
/// Two usage modes:
/// 1. Context mode: Initialize/Grow/Flush — all write methods live in BinarySerializationContext.
/// 2. Standalone mode: direct write methods (WriteByte, WriteVarUInt, etc.) for use outside
/// the serialization pipeline (e.g. AcBinaryHubProtocol frame headers).
///
/// Uses a cached chunk pattern: acquires a large chunk once via GetMemory, extracts the backing
/// array, and writes into it with direct indexing. Only calls Advance + GetMemory when the chunk fills up.
///
/// Call <see cref="Flush()"/> after all standalone writes to commit any pending bytes.
/// </summary>
public struct BufferWriterBinaryOutput : IBinaryOutputBase
{
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private const int MinChunkRequest = 256;
private readonly IBufferWriter<byte> _writer;
private int _committedBytes; // total bytes Advanced to writer so far
private int _currentChunkStart; // _position value at start of current chunk
private bool _ownedBuffer; // true if current buffer is from ArrayPool (fallback path)
// Standalone mode buffer state (used by direct write methods)
private byte[] _buffer = null!;
private int _position;
private int _bufferEnd;
public BufferWriterBinaryOutput(IBufferWriter<byte> writer)
{
_writer = writer;
// Initialize standalone buffer for direct write usage
_committedBytes = 0;
AcquireChunk(MinChunkRequest, out _buffer, out _position, out _bufferEnd);
_currentChunkStart = _position;
}
/// <summary>
/// Provides the initial buffer from the IBufferWriter.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Initialize(out byte[] buffer, out int position, out int bufferEnd)
{
_committedBytes = 0;
AcquireChunk(MinChunkRequest, out buffer, out position, out bufferEnd);
_currentChunkStart = position;
}
/// <summary>
/// Called when the context's buffer is full. Commits current chunk to the IBufferWriter
/// and acquires a new chunk.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
public void Grow(ref byte[] buffer, ref int position, ref int bufferEnd, int needed)
{
// Commit bytes written in current chunk
var bytesInChunk = position - _currentChunkStart;
if (bytesInChunk > 0)
{
if (_ownedBuffer)
{
FlushOwnedBuffer(buffer, bytesInChunk);
}
else
{
_writer.Advance(bytesInChunk);
}
_committedBytes += bytesInChunk;
}
// Acquire new chunk
AcquireChunk(Math.Max(needed, MinChunkRequest), out buffer, out position, out bufferEnd);
_currentChunkStart = position;
}
/// <summary>
/// Returns total bytes written: committed + pending in current chunk.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetTotalPosition(int currentPosition)
=> _committedBytes + (currentPosition - _currentChunkStart);
/// <summary>
/// Commits any pending bytes to the underlying IBufferWriter.
/// Must be called after all writes are complete.
/// Takes the context's current buffer state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Flush(byte[] buffer, int position)
{
var bytesInChunk = position - _currentChunkStart;
if (bytesInChunk > 0)
{
if (_ownedBuffer)
{
FlushOwnedBuffer(buffer, bytesInChunk);
}
else
{
_writer.Advance(bytesInChunk);
}
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void FlushOwnedBuffer(byte[] buffer, int bytesInChunk)
{
// Copy from our owned array to the writer, then return to pool
var span = _writer.GetSpan(bytesInChunk);
buffer.AsSpan(_currentChunkStart, bytesInChunk).CopyTo(span);
_writer.Advance(bytesInChunk);
ArrayPool<byte>.Shared.Return(buffer);
_ownedBuffer = false;
}
private void AcquireChunk(int requestSize, out byte[] buffer, out int position, out int bufferEnd)
{
// Use GetMemory so we can extract the backing array via TryGetArray
var actualRequest = Math.Max(requestSize, MinChunkRequest);
var memory = _writer.GetMemory(actualRequest);
if (MemoryMarshal.TryGetArray(memory, out ArraySegment<byte> segment) && segment.Array != null)
{
buffer = segment.Array;
position = segment.Offset;
bufferEnd = segment.Offset + segment.Count;
_ownedBuffer = false;
}
else
{
// Fallback for non-array-backed IBufferWriter (native memory).
// Rent our own buffer; FlushOwnedBuffer copies to writer on next Grow/Flush.
var owned = ArrayPool<byte>.Shared.Rent(actualRequest);
buffer = owned;
position = 0;
bufferEnd = owned.Length;
_ownedBuffer = true;
}
}
/// <summary>
/// No-op for BufferWriterBinaryOutput — chunks are owned by IBufferWriter, not us.
/// </summary>
public void Reset() { }
#region Standalone Write Methods for direct usage outside serialization pipeline (e.g. AcBinaryHubProtocol)
/// <summary>
/// Total bytes written in standalone mode (committed + pending in current chunk).
/// </summary>
public int Position
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _committedBytes + (_position - _currentChunkStart);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void StandaloneEnsureCapacity(int additionalBytes)
{
if (_position + additionalBytes > _bufferEnd)
Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteByte(byte value)
{
if (_position >= _bufferEnd)
Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
_buffer[_position++] = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteBytes(ReadOnlySpan<byte> data)
{
StandaloneEnsureCapacity(data.Length);
data.CopyTo(_buffer.AsSpan(_position));
_position += data.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRaw<T>(T value) where T : unmanaged
{
var size = Unsafe.SizeOf<T>();
StandaloneEnsureCapacity(size);
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += size;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarUInt(uint value)
{
if (value < 0x80)
{
if (_position >= _bufferEnd)
Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
_buffer[_position++] = (byte)value;
return;
}
StandaloneEnsureCapacity(5);
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[_position++] = (byte)value;
}
public void WriteStringUtf8(string value)
{
var charLength = value.Length;
// Speculative ASCII fast path: single-pass Ascii.FromUtf16
var savedPosition = _position;
WriteVarUInt((uint)charLength);
StandaloneEnsureCapacity(charLength);
if (Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, charLength), out _) == OperationStatus.Done)
{
_position += charLength;
return;
}
// Non-ASCII fallback: rewind VarUInt, encode with UTF-8
_position = savedPosition;
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
StandaloneEnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
/// <summary>
/// Commits any pending bytes in standalone mode to the underlying IBufferWriter.
/// Call after all standalone writes are complete.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Flush()
{
Flush(_buffer, _position);
}
#endregion
}