253 lines
8.7 KiB
C#
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
|
|
}
|