Add high-performance binary output abstraction
Introduce ArrayBinaryOutput, BufferWriterBinaryOutput, BinaryOutputBase, and IBinaryOutput for flexible, efficient serialization to pooled arrays and streaming buffers. Refactor AcBinaryHubProtocol to use new output for SignalR. Make UseMetadata runtime-settable in AcBinarySerializerOptions. Update QuickBenchmark for new options. Enables allocation-free, extensible binary serialization infrastructure.
This commit is contained in:
parent
97b7813633
commit
0bde311aa1
|
|
@ -507,9 +507,12 @@ public class QuickBenchmark
|
|||
|
||||
// Options
|
||||
var withRefOptions = AcBinarySerializerOptions.Default;
|
||||
withRefOptions.UseMetadata = false;
|
||||
//withRefOptions.UseStringInterning = StringInterningMode.None;
|
||||
|
||||
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
|
||||
noRefOptions.UseStringInterning = StringInterningMode.None;
|
||||
noRefOptions.UseMetadata = false;
|
||||
//noRefOptions.UseStringInterning = StringInterningMode.None;
|
||||
|
||||
// Pre-serialize
|
||||
var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions);
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
/// allowing the deserializer to match properties by name between different types.
|
||||
/// Default: false (no overhead)
|
||||
/// </summary>
|
||||
public bool UseMetadata { get; init; } = false;
|
||||
public bool UseMetadata { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,415 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// High-performance binary output backed by a byte[] from ArrayPool.
|
||||
/// Matches the exact performance characteristics of the original BinarySerializationContext buffer code:
|
||||
/// direct _buffer[_position++] indexing, Unsafe.WriteUnaligned, SIMD bulk copy.
|
||||
///
|
||||
/// This is the fastest output path — use when the result is needed as byte[]/Span.
|
||||
/// </summary>
|
||||
public sealed class ArrayBinaryOutput : BinaryOutputBase, IDisposable
|
||||
{
|
||||
private const int MinBufferSize = 256;
|
||||
|
||||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||
|
||||
private byte[] _buffer;
|
||||
private int _position;
|
||||
|
||||
public ArrayBinaryOutput(int initialCapacity = 4096)
|
||||
{
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(Math.Max(initialCapacity, MinBufferSize));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Position
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _position;
|
||||
}
|
||||
|
||||
#region Abstract Overrides — Core Primitives
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteByte(byte value)
|
||||
{
|
||||
if (_position >= _buffer.Length)
|
||||
GrowBuffer(_position + 1);
|
||||
_buffer[_position++] = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteTwoBytes(byte b1, byte b2)
|
||||
{
|
||||
EnsureCapacity(2);
|
||||
_buffer[_position++] = b1;
|
||||
_buffer[_position++] = b2;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteBytes(ReadOnlySpan<byte> data)
|
||||
{
|
||||
EnsureCapacity(data.Length);
|
||||
data.CopyTo(_buffer.AsSpan(_position));
|
||||
_position += data.Length;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteRaw<T>(T value)
|
||||
{
|
||||
var size = Unsafe.SizeOf<T>();
|
||||
EnsureCapacity(size);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value);
|
||||
_position += size;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void EnsureCapacity(int additionalBytes)
|
||||
{
|
||||
var required = _position + additionalBytes;
|
||||
if (required <= _buffer.Length)
|
||||
return;
|
||||
GrowBuffer(required);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void GrowBuffer(int required)
|
||||
{
|
||||
var newSize = Math.Max(_buffer.Length * 2, required);
|
||||
var newBuffer = ArrayPool<byte>.Shared.Rent(newSize);
|
||||
_buffer.AsSpan(0, _position).CopyTo(newBuffer);
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = newBuffer;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Optimized Overrides — Specialized Types (direct buffer access)
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: single EnsureCapacity + direct Unsafe.WriteUnaligned + indexer.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteTypeCodeAndRaw<T>(byte typeCode, T value)
|
||||
{
|
||||
var size = 1 + Unsafe.SizeOf<T>();
|
||||
EnsureCapacity(size);
|
||||
_buffer[_position++] = typeCode;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value);
|
||||
_position += Unsafe.SizeOf<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: VarUInt with direct _buffer[_position++] access.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteVarUInt(uint value)
|
||||
{
|
||||
if (value < 0x80)
|
||||
{
|
||||
if (_position >= _buffer.Length)
|
||||
GrowBuffer(_position + 1);
|
||||
_buffer[_position++] = (byte)value;
|
||||
return;
|
||||
}
|
||||
EnsureCapacity(5);
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_buffer[_position++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
_buffer[_position++] = (byte)value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: VarInt with direct buffer access.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteVarInt(int value)
|
||||
{
|
||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||
WriteVarUInt(encoded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: VarULong with direct buffer access.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteVarULong(ulong value)
|
||||
{
|
||||
if (value < 0x80)
|
||||
{
|
||||
if (_position >= _buffer.Length)
|
||||
GrowBuffer(_position + 1);
|
||||
_buffer[_position++] = (byte)value;
|
||||
return;
|
||||
}
|
||||
EnsureCapacity(10);
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_buffer[_position++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
_buffer[_position++] = (byte)value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: VarLong with direct buffer access.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteVarLong(long value)
|
||||
{
|
||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||
WriteVarULong(encoded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: direct Unsafe.WriteUnaligned for decimal bits.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteDecimalBits(decimal value)
|
||||
{
|
||||
EnsureCapacity(16);
|
||||
Span<int> bits = stackalloc int[4];
|
||||
decimal.TryGetBits(value, bits, out _);
|
||||
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: direct Unsafe.WriteUnaligned + indexer for DateTime.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteDateTimeBits(DateTime value)
|
||||
{
|
||||
EnsureCapacity(9);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
|
||||
_buffer[_position + 8] = (byte)value.Kind;
|
||||
_position += 9;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: direct TryWriteBytes into buffer.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteGuidBits(Guid value)
|
||||
{
|
||||
EnsureCapacity(16);
|
||||
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: direct Unsafe.WriteUnaligned for DateTimeOffset.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteDateTimeOffsetBits(DateTimeOffset value)
|
||||
{
|
||||
EnsureCapacity(10);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
|
||||
_position += 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: direct ASCII fast path into _buffer.
|
||||
/// </summary>
|
||||
public override void WriteStringUtf8(string value)
|
||||
{
|
||||
if (Ascii.IsValid(value))
|
||||
{
|
||||
WriteVarUInt((uint)value.Length);
|
||||
EnsureCapacity(value.Length);
|
||||
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
|
||||
_position += value.Length;
|
||||
return;
|
||||
}
|
||||
|
||||
var byteCount = Utf8NoBom.GetByteCount(value);
|
||||
WriteVarUInt((uint)byteCount);
|
||||
EnsureCapacity(byteCount);
|
||||
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
|
||||
_position += byteCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: FixStr with direct buffer write.
|
||||
/// </summary>
|
||||
public override void WriteFixStr(string value)
|
||||
{
|
||||
var length = value.Length;
|
||||
EnsureCapacity(1 + length);
|
||||
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
|
||||
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
|
||||
_position += length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: FixStrDirect with SIMD try into buffer.
|
||||
/// </summary>
|
||||
public override void WriteFixStrDirect(string value)
|
||||
{
|
||||
var length = value.Length;
|
||||
EnsureCapacity(1 + length);
|
||||
|
||||
var destSpan = _buffer.AsSpan(_position + 1, length);
|
||||
var status = Ascii.FromUtf16(value.AsSpan(), destSpan, out var bytesWritten);
|
||||
|
||||
if (status == System.Buffers.OperationStatus.Done && bytesWritten == length)
|
||||
{
|
||||
_buffer[_position] = BinaryTypeCode.EncodeFixStr(length);
|
||||
_position += 1 + length;
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.String;
|
||||
WriteStringUtf8Internal(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: FixStrBytes with direct buffer copy.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteFixStrBytes(ReadOnlySpan<byte> utf8Bytes)
|
||||
{
|
||||
var length = utf8Bytes.Length;
|
||||
EnsureCapacity(1 + length);
|
||||
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
|
||||
utf8Bytes.CopyTo(_buffer.AsSpan(_position, length));
|
||||
_position += length;
|
||||
}
|
||||
|
||||
private void WriteStringUtf8Internal(string value)
|
||||
{
|
||||
var byteCount = Utf8NoBom.GetByteCount(value);
|
||||
WriteVarUInt((uint)byteCount);
|
||||
EnsureCapacity(byteCount);
|
||||
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
|
||||
_position += byteCount;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Optimized Overrides — Bulk Arrays (direct buffer, batched capacity)
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteDoubleArrayBulk(double[] array)
|
||||
{
|
||||
EnsureCapacity(array.Length * 9);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Float64;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
||||
_position += 8;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteFloatArrayBulk(float[] array)
|
||||
{
|
||||
EnsureCapacity(array.Length * 5);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Float32;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
||||
_position += 4;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteGuidArrayBulk(Guid[] array)
|
||||
{
|
||||
EnsureCapacity(array.Length * 17);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Guid;
|
||||
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteBytesSimd(ReadOnlySpan<byte> source)
|
||||
{
|
||||
EnsureCapacity(source.Length);
|
||||
var destination = _buffer.AsSpan(_position, source.Length);
|
||||
|
||||
if (Vector.IsHardwareAccelerated && source.Length >= Vector<byte>.Count * 2)
|
||||
{
|
||||
var vectorSize = Vector<byte>.Count;
|
||||
var i = 0;
|
||||
var length = source.Length;
|
||||
var vectorCount = length / vectorSize;
|
||||
for (var v = 0; v < vectorCount; v++)
|
||||
{
|
||||
var vec = new Vector<byte>(source.Slice(i, vectorSize));
|
||||
vec.CopyTo(destination.Slice(i, vectorSize));
|
||||
i += vectorSize;
|
||||
}
|
||||
if (i < length)
|
||||
source.Slice(i).CopyTo(destination.Slice(i));
|
||||
}
|
||||
else
|
||||
{
|
||||
source.CopyTo(destination);
|
||||
}
|
||||
|
||||
_position += source.Length;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Output Methods
|
||||
|
||||
/// <summary>Returns the written data as a ReadOnlySpan without allocation.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ReadOnlySpan<byte> AsSpan() => _buffer.AsSpan(0, _position);
|
||||
|
||||
/// <summary>Copies the written data to a new exactly-sized array.</summary>
|
||||
public byte[] ToArray()
|
||||
{
|
||||
var result = GC.AllocateUninitializedArray<byte>(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Copies the written data to an IBufferWriter (single memcpy).</summary>
|
||||
public void WriteTo(IBufferWriter<byte> writer)
|
||||
{
|
||||
var span = writer.GetSpan(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(span);
|
||||
writer.Advance(_position);
|
||||
}
|
||||
|
||||
/// <summary>Resets position for reuse without deallocation.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset() => _position = 0;
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_buffer != null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = null!;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for binary output implementations.
|
||||
/// Provides common serialization logic (VarInt, strings, specialized types, bulk arrays)
|
||||
/// built on top of a small set of abstract core primitives that derived classes implement.
|
||||
///
|
||||
/// Derived classes only need to implement the core buffer operations:
|
||||
/// WriteByte, WriteTwoBytes, WriteBytes, WriteRaw, EnsureCapacity, Position.
|
||||
///
|
||||
/// All higher-level methods are virtual — derived classes can override any method
|
||||
/// for backing-specific optimizations (e.g. batched GetSpan, direct buffer indexing).
|
||||
/// </summary>
|
||||
public abstract class BinaryOutputBase : IBinaryOutput
|
||||
{
|
||||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||
|
||||
#region Abstract — Core Primitives (derived must implement)
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void WriteByte(byte value);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void WriteTwoBytes(byte b1, byte b2);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void WriteBytes(ReadOnlySpan<byte> data);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void WriteRaw<T>(T value) where T : unmanaged;
|
||||
|
||||
/// <summary>
|
||||
/// Ensure the backing storage can accept at least <paramref name="additionalBytes"/> more bytes
|
||||
/// without reallocation. Called by higher-level methods to batch capacity checks.
|
||||
/// </summary>
|
||||
protected abstract void EnsureCapacity(int additionalBytes);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract int Position { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Virtual — WriteTypeCodeAndRaw
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged
|
||||
{
|
||||
EnsureCapacity(1 + Unsafe.SizeOf<T>());
|
||||
WriteByte(typeCode);
|
||||
WriteRaw(value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Virtual — VarInt Encoding
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteVarInt(int value)
|
||||
{
|
||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||
WriteVarUInt(encoded);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void WriteVarUInt(uint value)
|
||||
{
|
||||
if (value < 0x80)
|
||||
{
|
||||
WriteByte((byte)value);
|
||||
return;
|
||||
}
|
||||
|
||||
WriteVarUIntMultiByte(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void WriteVarUIntMultiByte(uint value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
WriteByte((byte)(value | 0x80));
|
||||
value >>= 7;
|
||||
}
|
||||
WriteByte((byte)value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteVarLong(long value)
|
||||
{
|
||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||
WriteVarULong(encoded);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void WriteVarULong(ulong value)
|
||||
{
|
||||
if (value < 0x80)
|
||||
{
|
||||
WriteByte((byte)value);
|
||||
return;
|
||||
}
|
||||
|
||||
WriteVarULongMultiByte(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void WriteVarULongMultiByte(ulong value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
WriteByte((byte)(value | 0x80));
|
||||
value >>= 7;
|
||||
}
|
||||
WriteByte((byte)value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Virtual — Specialized Types
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteDecimalBits(decimal value)
|
||||
{
|
||||
Span<int> bits = stackalloc int[4];
|
||||
decimal.TryGetBits(value, bits, out _);
|
||||
WriteBytes(MemoryMarshal.AsBytes(bits));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteDateTimeBits(DateTime value)
|
||||
{
|
||||
WriteRaw(value.Ticks);
|
||||
WriteByte((byte)value.Kind);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteGuidBits(Guid value)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[16];
|
||||
value.TryWriteBytes(buf);
|
||||
WriteBytes(buf);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteDateTimeOffsetBits(DateTimeOffset value)
|
||||
{
|
||||
WriteRaw(value.UtcTicks);
|
||||
WriteRaw((short)value.Offset.TotalMinutes);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Virtual — String Writes
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteStringUtf8(string value)
|
||||
{
|
||||
if (Ascii.IsValid(value))
|
||||
{
|
||||
WriteVarUInt((uint)value.Length);
|
||||
Span<byte> buf = value.Length <= 256
|
||||
? stackalloc byte[value.Length]
|
||||
: new byte[value.Length];
|
||||
Ascii.FromUtf16(value.AsSpan(), buf, out _);
|
||||
WriteBytes(buf);
|
||||
return;
|
||||
}
|
||||
|
||||
var byteCount = Utf8NoBom.GetByteCount(value);
|
||||
WriteVarUInt((uint)byteCount);
|
||||
Span<byte> utf8Buf = byteCount <= 256
|
||||
? stackalloc byte[byteCount]
|
||||
: new byte[byteCount];
|
||||
Utf8NoBom.GetBytes(value.AsSpan(), utf8Buf);
|
||||
WriteBytes(utf8Buf);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteFixStr(string value)
|
||||
{
|
||||
var length = value.Length;
|
||||
WriteByte(BinaryTypeCode.EncodeFixStr(length));
|
||||
Span<byte> buf = length <= 256
|
||||
? stackalloc byte[length]
|
||||
: new byte[length];
|
||||
Ascii.FromUtf16(value.AsSpan(), buf, out _);
|
||||
WriteBytes(buf);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteFixStrDirect(string value)
|
||||
{
|
||||
var length = value.Length;
|
||||
Span<byte> buf = length <= 256
|
||||
? stackalloc byte[length]
|
||||
: new byte[length];
|
||||
|
||||
var status = Ascii.FromUtf16(value.AsSpan(), buf, out var bytesWritten);
|
||||
|
||||
if (status == OperationStatus.Done && bytesWritten == length)
|
||||
{
|
||||
WriteByte(BinaryTypeCode.EncodeFixStr(length));
|
||||
WriteBytes(buf.Slice(0, length));
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.String);
|
||||
WriteStringUtf8Internal(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteFixStrBytes(ReadOnlySpan<byte> utf8Bytes)
|
||||
{
|
||||
WriteByte(BinaryTypeCode.EncodeFixStr(utf8Bytes.Length));
|
||||
WriteBytes(utf8Bytes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name)
|
||||
{
|
||||
WriteByte(BinaryTypeCode.String);
|
||||
WriteVarUInt((uint)utf8Name.Length);
|
||||
WriteBytes(utf8Name);
|
||||
}
|
||||
|
||||
private void WriteStringUtf8Internal(string value)
|
||||
{
|
||||
var byteCount = Utf8NoBom.GetByteCount(value);
|
||||
WriteVarUInt((uint)byteCount);
|
||||
Span<byte> buf = byteCount <= 256
|
||||
? stackalloc byte[byteCount]
|
||||
: new byte[byteCount];
|
||||
Utf8NoBom.GetBytes(value.AsSpan(), buf);
|
||||
WriteBytes(buf);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Virtual — Bulk Array Writes (overridable for optimization)
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteDoubleArrayBulk(double[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Float64);
|
||||
WriteRaw(array[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteFloatArrayBulk(float[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Float32);
|
||||
WriteRaw(array[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteGuidArrayBulk(Guid[] array)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[16];
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Guid);
|
||||
array[i].TryWriteBytes(buf);
|
||||
WriteBytes(buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteInt32ArrayOptimized(int[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
var value = array[i];
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
||||
{
|
||||
WriteByte(tiny);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int32);
|
||||
WriteVarInt(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteLongArrayOptimized(long[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
var value = array[i];
|
||||
if (value >= int.MinValue && value <= int.MaxValue)
|
||||
{
|
||||
var intValue = (int)value;
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
||||
{
|
||||
WriteByte(tiny);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int32);
|
||||
WriteVarInt(intValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int64);
|
||||
WriteVarLong(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteBytesSimd(ReadOnlySpan<byte> source)
|
||||
{
|
||||
WriteBytes(source);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,350 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// Binary output that writes directly to an IBufferWriter (e.g. SignalR pipe, network stream).
|
||||
/// Uses a cached chunk pattern: acquires a large chunk once via GetMemory, extracts the backing
|
||||
/// array, and writes into it with direct indexing (zero interface calls per write).
|
||||
/// Only calls Advance + GetMemory when the chunk fills up.
|
||||
///
|
||||
/// Call <see cref="Flush"/> after all writes to commit any pending bytes to the underlying writer.
|
||||
/// </summary>
|
||||
public sealed class BufferWriterBinaryOutput : BinaryOutputBase
|
||||
{
|
||||
private const int MinChunkRequest = 256;
|
||||
|
||||
private readonly IBufferWriter<byte> _writer;
|
||||
private int _written;
|
||||
|
||||
// Cached chunk state — avoids GetSpan/Advance per write
|
||||
private byte[] _chunkArray; // backing array (from GetMemory or ArrayPool fallback)
|
||||
private int _chunkOffset; // start offset within _chunkArray
|
||||
private int _chunkPos; // bytes written into current chunk
|
||||
private int _chunkLength; // usable length of current chunk
|
||||
private bool _ownedBuffer; // true if _chunkArray is from ArrayPool (fallback path)
|
||||
|
||||
public BufferWriterBinaryOutput(IBufferWriter<byte> writer)
|
||||
{
|
||||
_writer = writer;
|
||||
_chunkArray = null!;
|
||||
RentChunk(MinChunkRequest);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Position
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _written;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commits any pending bytes to the underlying IBufferWriter.
|
||||
/// Must be called after all writes are complete.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Flush()
|
||||
{
|
||||
if (_chunkPos > 0)
|
||||
{
|
||||
if (_ownedBuffer)
|
||||
FlushOwnedBuffer();
|
||||
else
|
||||
_writer.Advance(_chunkPos);
|
||||
_chunkPos = 0;
|
||||
_chunkLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void FlushOwnedBuffer()
|
||||
{
|
||||
// Copy from our owned array to the writer, then return to pool
|
||||
var span = _writer.GetSpan(_chunkPos);
|
||||
_chunkArray.AsSpan(_chunkOffset, _chunkPos).CopyTo(span);
|
||||
_writer.Advance(_chunkPos);
|
||||
ArrayPool<byte>.Shared.Return(_chunkArray);
|
||||
_ownedBuffer = false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void RentChunk(int minSize)
|
||||
{
|
||||
// Commit whatever we wrote so far
|
||||
if (_chunkPos > 0)
|
||||
{
|
||||
if (_ownedBuffer)
|
||||
FlushOwnedBuffer();
|
||||
else
|
||||
_writer.Advance(_chunkPos);
|
||||
}
|
||||
|
||||
// Use GetMemory so we can extract the backing array via TryGetArray
|
||||
var requestSize = Math.Max(minSize, MinChunkRequest);
|
||||
var memory = _writer.GetMemory(requestSize);
|
||||
|
||||
if (MemoryMarshal.TryGetArray(memory, out ArraySegment<byte> segment) && segment.Array != null)
|
||||
{
|
||||
_chunkArray = segment.Array;
|
||||
_chunkOffset = segment.Offset;
|
||||
_chunkLength = segment.Count;
|
||||
_ownedBuffer = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback for non-array-backed IBufferWriter (native memory).
|
||||
// Rent our own buffer; FlushOwnedBuffer copies to writer on next RentChunk/Flush.
|
||||
_chunkArray = ArrayPool<byte>.Shared.Rent(requestSize);
|
||||
_chunkOffset = 0;
|
||||
_chunkLength = _chunkArray.Length;
|
||||
_ownedBuffer = true;
|
||||
}
|
||||
_chunkPos = 0;
|
||||
}
|
||||
|
||||
#region Abstract Overrides — Core Primitives
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteByte(byte value)
|
||||
{
|
||||
if (_chunkPos >= _chunkLength)
|
||||
RentChunk(1);
|
||||
_chunkArray[_chunkOffset + _chunkPos++] = value;
|
||||
_written++;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteTwoBytes(byte b1, byte b2)
|
||||
{
|
||||
if (_chunkPos + 2 > _chunkLength)
|
||||
RentChunk(2);
|
||||
var off = _chunkOffset + _chunkPos;
|
||||
_chunkArray[off] = b1;
|
||||
_chunkArray[off + 1] = b2;
|
||||
_chunkPos += 2;
|
||||
_written += 2;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteBytes(ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (_chunkPos + data.Length > _chunkLength)
|
||||
RentChunk(data.Length);
|
||||
data.CopyTo(_chunkArray.AsSpan(_chunkOffset + _chunkPos, data.Length));
|
||||
_chunkPos += data.Length;
|
||||
_written += data.Length;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteRaw<T>(T value)
|
||||
{
|
||||
var size = Unsafe.SizeOf<T>();
|
||||
if (_chunkPos + size > _chunkLength)
|
||||
RentChunk(size);
|
||||
Unsafe.WriteUnaligned(ref _chunkArray[_chunkOffset + _chunkPos], value);
|
||||
_chunkPos += size;
|
||||
_written += size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the cached chunk has room for at least <paramref name="additionalBytes"/>.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void EnsureCapacity(int additionalBytes)
|
||||
{
|
||||
if (_chunkPos + additionalBytes > _chunkLength)
|
||||
RentChunk(additionalBytes);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Optimized Overrides — Batched Writes
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: single capacity check for type code + value, direct buffer write.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteTypeCodeAndRaw<T>(byte typeCode, T value)
|
||||
{
|
||||
var size = 1 + Unsafe.SizeOf<T>();
|
||||
if (_chunkPos + size > _chunkLength)
|
||||
RentChunk(size);
|
||||
var off = _chunkOffset + _chunkPos;
|
||||
_chunkArray[off] = typeCode;
|
||||
Unsafe.WriteUnaligned(ref _chunkArray[off + 1], value);
|
||||
_chunkPos += size;
|
||||
_written += size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: VarUInt with direct cached buffer access.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteVarUInt(uint value)
|
||||
{
|
||||
if (value < 0x80)
|
||||
{
|
||||
if (_chunkPos >= _chunkLength)
|
||||
RentChunk(1);
|
||||
_chunkArray[_chunkOffset + _chunkPos++] = (byte)value;
|
||||
_written++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_chunkPos + 5 > _chunkLength)
|
||||
RentChunk(5);
|
||||
var off = _chunkOffset + _chunkPos;
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_chunkArray[off++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
_chunkArray[off++] = (byte)value;
|
||||
var bytesWritten = off - _chunkOffset - _chunkPos;
|
||||
_chunkPos += bytesWritten;
|
||||
_written += bytesWritten;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: ZigZag + batched VarUInt.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteVarInt(int value)
|
||||
{
|
||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||
WriteVarUInt(encoded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: VarULong with direct cached buffer access.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteVarULong(ulong value)
|
||||
{
|
||||
if (value < 0x80)
|
||||
{
|
||||
if (_chunkPos >= _chunkLength)
|
||||
RentChunk(1);
|
||||
_chunkArray[_chunkOffset + _chunkPos++] = (byte)value;
|
||||
_written++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_chunkPos + 10 > _chunkLength)
|
||||
RentChunk(10);
|
||||
var off = _chunkOffset + _chunkPos;
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_chunkArray[off++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
_chunkArray[off++] = (byte)value;
|
||||
var bytesWritten = off - _chunkOffset - _chunkPos;
|
||||
_chunkPos += bytesWritten;
|
||||
_written += bytesWritten;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: ZigZag + batched VarULong.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteVarLong(long value)
|
||||
{
|
||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||
WriteVarULong(encoded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: single capacity check for DateTime (9 bytes), direct buffer write.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteDateTimeBits(DateTime value)
|
||||
{
|
||||
if (_chunkPos + 9 > _chunkLength)
|
||||
RentChunk(9);
|
||||
var off = _chunkOffset + _chunkPos;
|
||||
Unsafe.WriteUnaligned(ref _chunkArray[off], value.Ticks);
|
||||
_chunkArray[off + 8] = (byte)value.Kind;
|
||||
_chunkPos += 9;
|
||||
_written += 9;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized: single capacity check for DateTimeOffset (10 bytes), direct buffer write.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void WriteDateTimeOffsetBits(DateTimeOffset value)
|
||||
{
|
||||
if (_chunkPos + 10 > _chunkLength)
|
||||
RentChunk(10);
|
||||
var off = _chunkOffset + _chunkPos;
|
||||
Unsafe.WriteUnaligned(ref _chunkArray[off], value.UtcTicks);
|
||||
Unsafe.WriteUnaligned(ref _chunkArray[off + 8], (short)value.Offset.TotalMinutes);
|
||||
_chunkPos += 10;
|
||||
_written += 10;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Optimized Overrides — Bulk Arrays (single capacity check + tight loop)
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteDoubleArrayBulk(double[] array)
|
||||
{
|
||||
var totalSize = array.Length * 9;
|
||||
if (_chunkPos + totalSize > _chunkLength)
|
||||
RentChunk(totalSize);
|
||||
var off = _chunkOffset + _chunkPos;
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_chunkArray[off++] = BinaryTypeCode.Float64;
|
||||
Unsafe.WriteUnaligned(ref _chunkArray[off], array[i]);
|
||||
off += 8;
|
||||
}
|
||||
_chunkPos += totalSize;
|
||||
_written += totalSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteFloatArrayBulk(float[] array)
|
||||
{
|
||||
var totalSize = array.Length * 5;
|
||||
if (_chunkPos + totalSize > _chunkLength)
|
||||
RentChunk(totalSize);
|
||||
var off = _chunkOffset + _chunkPos;
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_chunkArray[off++] = BinaryTypeCode.Float32;
|
||||
Unsafe.WriteUnaligned(ref _chunkArray[off], array[i]);
|
||||
off += 4;
|
||||
}
|
||||
_chunkPos += totalSize;
|
||||
_written += totalSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteGuidArrayBulk(Guid[] array)
|
||||
{
|
||||
var totalSize = array.Length * 17;
|
||||
if (_chunkPos + totalSize > _chunkLength)
|
||||
RentChunk(totalSize);
|
||||
var off = _chunkOffset + _chunkPos;
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_chunkArray[off++] = BinaryTypeCode.Guid;
|
||||
array[i].TryWriteBytes(_chunkArray.AsSpan(off, 16));
|
||||
off += 16;
|
||||
}
|
||||
_chunkPos += totalSize;
|
||||
_written += totalSize;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction for binary serialization output.
|
||||
/// Implementations can write to byte[] (ArrayBinaryOutput) or IBufferWriter (BufferWriterBinaryOutput).
|
||||
/// Custom implementations can be provided for specialized output targets.
|
||||
/// </summary>
|
||||
public interface IBinaryOutput
|
||||
{
|
||||
#region Core Writes
|
||||
|
||||
/// <summary>Write a single byte.</summary>
|
||||
void WriteByte(byte value);
|
||||
|
||||
/// <summary>Write two bytes efficiently.</summary>
|
||||
void WriteTwoBytes(byte b1, byte b2);
|
||||
|
||||
/// <summary>Write a span of bytes.</summary>
|
||||
void WriteBytes(ReadOnlySpan<byte> data);
|
||||
|
||||
/// <summary>Write an unmanaged value directly (no encoding).</summary>
|
||||
void WriteRaw<T>(T value) where T : unmanaged;
|
||||
|
||||
/// <summary>Write a type code byte followed by an unmanaged value. Batches capacity check.</summary>
|
||||
void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region VarInt Encoding
|
||||
|
||||
/// <summary>Write a ZigZag-encoded variable-length int32.</summary>
|
||||
void WriteVarInt(int value);
|
||||
|
||||
/// <summary>Write a variable-length uint32.</summary>
|
||||
void WriteVarUInt(uint value);
|
||||
|
||||
/// <summary>Write a ZigZag-encoded variable-length int64.</summary>
|
||||
void WriteVarLong(long value);
|
||||
|
||||
/// <summary>Write a variable-length uint64.</summary>
|
||||
void WriteVarULong(ulong value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Specialized Types
|
||||
|
||||
/// <summary>Write decimal as 16 raw bytes (4 x int32 bits).</summary>
|
||||
void WriteDecimalBits(decimal value);
|
||||
|
||||
/// <summary>Write DateTime as 8 bytes ticks + 1 byte kind.</summary>
|
||||
void WriteDateTimeBits(DateTime value);
|
||||
|
||||
/// <summary>Write Guid as 16 raw bytes.</summary>
|
||||
void WriteGuidBits(Guid value);
|
||||
|
||||
/// <summary>Write DateTimeOffset as 8 bytes UTC ticks + 2 bytes offset minutes.</summary>
|
||||
void WriteDateTimeOffsetBits(DateTimeOffset value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region String Writes
|
||||
|
||||
/// <summary>Write UTF8 string with VarUInt length prefix. Fast path for ASCII.</summary>
|
||||
void WriteStringUtf8(string value);
|
||||
|
||||
/// <summary>Write short ASCII string using FixStr encoding (type+length in single byte).</summary>
|
||||
void WriteFixStr(string value);
|
||||
|
||||
/// <summary>Write FixStr with SIMD ASCII try, falls back to standard UTF8.</summary>
|
||||
void WriteFixStrDirect(string value);
|
||||
|
||||
/// <summary>Write pre-encoded UTF8 bytes using FixStr encoding.</summary>
|
||||
void WriteFixStrBytes(ReadOnlySpan<byte> utf8Bytes);
|
||||
|
||||
/// <summary>Write pre-encoded property name with String type code + VarUInt length + bytes.</summary>
|
||||
void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bulk Array Writes
|
||||
|
||||
/// <summary>Write double[] with per-element type codes. Override for optimized bulk write.</summary>
|
||||
void WriteDoubleArrayBulk(double[] array);
|
||||
|
||||
/// <summary>Write float[] with per-element type codes. Override for optimized bulk write.</summary>
|
||||
void WriteFloatArrayBulk(float[] array);
|
||||
|
||||
/// <summary>Write Guid[] with per-element type codes. Override for optimized bulk write.</summary>
|
||||
void WriteGuidArrayBulk(Guid[] array);
|
||||
|
||||
/// <summary>Write int[] with TinyInt optimization per element.</summary>
|
||||
void WriteInt32ArrayOptimized(int[] array);
|
||||
|
||||
/// <summary>Write long[] with TinyInt/Int32 downcast optimization per element.</summary>
|
||||
void WriteLongArrayOptimized(long[] array);
|
||||
|
||||
/// <summary>Write bytes using SIMD when available, standard copy otherwise.</summary>
|
||||
void WriteBytesSimd(ReadOnlySpan<byte> source);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Position
|
||||
|
||||
/// <summary>Current write position (total bytes written so far).</summary>
|
||||
int Position { get; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,615 @@
|
|||
using System.Buffers;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||
|
||||
namespace AyCode.Services.SignalRs;
|
||||
|
||||
/// <summary>
|
||||
/// Custom SignalR hub protocol using AcBinarySerializer for wire format.
|
||||
/// Eliminates JSON+Base64 overhead by serializing all HubMessages directly to binary.
|
||||
///
|
||||
/// Wire format per message:
|
||||
/// [4 bytes: payload length (little-endian)] [payload bytes]
|
||||
///
|
||||
/// Payload structure:
|
||||
/// [1 byte: message type] [message-specific fields serialized via AcBinary]
|
||||
///
|
||||
/// Message types map 1:1 to SignalR HubMessageType values.
|
||||
/// Arguments are serialized individually with a VarUInt length prefix each,
|
||||
/// enabling deferred deserialization via IHubProtocol's binder pattern.
|
||||
///
|
||||
/// All writes go directly to the IBufferWriter provided by SignalR via BufferWriterBinaryOutput.
|
||||
/// Length prefix is patched in-place after payload is written.
|
||||
/// </summary>
|
||||
public sealed class AcBinaryHubProtocol : IHubProtocol
|
||||
{
|
||||
private const int LengthPrefixSize = 4;
|
||||
|
||||
// Message type markers (matching HubMessageType enum values)
|
||||
private const byte MsgInvocation = 1;
|
||||
private const byte MsgStreamItem = 2;
|
||||
private const byte MsgCompletion = 3;
|
||||
private const byte MsgStreamInvocation = 4;
|
||||
private const byte MsgCancelInvocation = 5;
|
||||
private const byte MsgPing = 6;
|
||||
private const byte MsgClose = 7;
|
||||
private const byte MsgAck = 8;
|
||||
private const byte MsgSequence = 9;
|
||||
|
||||
private volatile AcBinarySerializerOptions _options;
|
||||
|
||||
public AcBinaryHubProtocol() : this(AcBinarySerializerOptions.Default) { }
|
||||
|
||||
public AcBinaryHubProtocol(AcBinarySerializerOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime-replaceable serializer options.
|
||||
/// Thread-safe: uses volatile field, callers see the new options on next message.
|
||||
/// </summary>
|
||||
public AcBinarySerializerOptions Options
|
||||
{
|
||||
get => _options;
|
||||
set => _options = value;
|
||||
}
|
||||
|
||||
public string Name => "acbinary";
|
||||
public int Version => 1;
|
||||
public TransferFormat TransferFormat => TransferFormat.Binary;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsVersionSupported(int version) => version <= Version;
|
||||
|
||||
#region WriteMessage
|
||||
|
||||
public ReadOnlyMemory<byte> GetMessageBytes(HubMessage message)
|
||||
{
|
||||
var writer = new ArrayBufferWriter<byte>(256);
|
||||
WriteMessage(message, writer);
|
||||
return writer.WrittenMemory;
|
||||
}
|
||||
|
||||
public void WriteMessage(HubMessage message, IBufferWriter<byte> output)
|
||||
{
|
||||
// Reserve 4 bytes for the length prefix — we'll patch it after writing the payload.
|
||||
// GetMemory returns a contiguous block; we keep a reference to write the length later.
|
||||
var lengthMemory = output.GetMemory(LengthPrefixSize);
|
||||
output.Advance(LengthPrefixSize);
|
||||
|
||||
// Wrap the IBufferWriter in BufferWriterBinaryOutput for optimized writes.
|
||||
var w = new BufferWriterBinaryOutput(output);
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case InvocationMessage m:
|
||||
WriteInvocation(w, m);
|
||||
break;
|
||||
|
||||
case StreamInvocationMessage m:
|
||||
WriteStreamInvocation(w, m);
|
||||
break;
|
||||
|
||||
case StreamItemMessage m:
|
||||
WriteStreamItem(w, m);
|
||||
break;
|
||||
|
||||
case CompletionMessage m:
|
||||
WriteCompletion(w, m);
|
||||
break;
|
||||
|
||||
case CancelInvocationMessage m:
|
||||
WriteCancelInvocation(w, m);
|
||||
break;
|
||||
|
||||
case PingMessage:
|
||||
w.WriteByte(MsgPing);
|
||||
break;
|
||||
|
||||
case CloseMessage m:
|
||||
WriteClose(w, m);
|
||||
break;
|
||||
|
||||
case AckMessage m:
|
||||
WriteAck(w, m);
|
||||
break;
|
||||
|
||||
case SequenceMessage m:
|
||||
WriteSequence(w, m);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new HubException($"Unexpected message type: {message.GetType().Name}");
|
||||
}
|
||||
|
||||
// Flush pending chunk bytes to the underlying IBufferWriter, then patch length prefix.
|
||||
w.Flush();
|
||||
Unsafe.WriteUnaligned(ref lengthMemory.Span[0], w.Position);
|
||||
}
|
||||
|
||||
private void WriteInvocation(BufferWriterBinaryOutput w, InvocationMessage m)
|
||||
{
|
||||
w.WriteByte(MsgInvocation);
|
||||
WriteNullableString(w, m.InvocationId);
|
||||
WriteString(w, m.Target);
|
||||
WriteArguments(w, m.Arguments);
|
||||
WriteStringArray(w, m.StreamIds);
|
||||
WriteHeaders(w, m.Headers);
|
||||
}
|
||||
|
||||
private void WriteStreamInvocation(BufferWriterBinaryOutput w, StreamInvocationMessage m)
|
||||
{
|
||||
w.WriteByte(MsgStreamInvocation);
|
||||
WriteString(w, m.InvocationId!);
|
||||
WriteString(w, m.Target);
|
||||
WriteArguments(w, m.Arguments);
|
||||
WriteStringArray(w, m.StreamIds);
|
||||
WriteHeaders(w, m.Headers);
|
||||
}
|
||||
|
||||
private void WriteStreamItem(BufferWriterBinaryOutput w, StreamItemMessage m)
|
||||
{
|
||||
w.WriteByte(MsgStreamItem);
|
||||
WriteString(w, m.InvocationId!);
|
||||
WriteArgument(w, m.Item);
|
||||
WriteHeaders(w, m.Headers);
|
||||
}
|
||||
|
||||
private void WriteCompletion(BufferWriterBinaryOutput w, CompletionMessage m)
|
||||
{
|
||||
w.WriteByte(MsgCompletion);
|
||||
WriteString(w, m.InvocationId!);
|
||||
WriteNullableString(w, m.Error);
|
||||
|
||||
// Result presence flags: 0 = no result, 1 = has result
|
||||
var hasResult = m.HasResult;
|
||||
w.WriteByte(hasResult ? (byte)1 : (byte)0);
|
||||
if (hasResult)
|
||||
WriteArgument(w, m.Result);
|
||||
|
||||
WriteHeaders(w, m.Headers);
|
||||
}
|
||||
|
||||
private static void WriteCancelInvocation(BufferWriterBinaryOutput w, CancelInvocationMessage m)
|
||||
{
|
||||
w.WriteByte(MsgCancelInvocation);
|
||||
WriteString(w, m.InvocationId!);
|
||||
WriteHeaders(w, m.Headers);
|
||||
}
|
||||
|
||||
private static void WriteClose(BufferWriterBinaryOutput w, CloseMessage m)
|
||||
{
|
||||
w.WriteByte(MsgClose);
|
||||
WriteNullableString(w, m.Error);
|
||||
w.WriteByte(m.AllowReconnect ? (byte)1 : (byte)0);
|
||||
}
|
||||
|
||||
private static void WriteAck(BufferWriterBinaryOutput w, AckMessage m)
|
||||
{
|
||||
w.WriteByte(MsgAck);
|
||||
w.WriteRaw(m.SequenceId);
|
||||
}
|
||||
|
||||
private static void WriteSequence(BufferWriterBinaryOutput w, SequenceMessage m)
|
||||
{
|
||||
w.WriteByte(MsgSequence);
|
||||
w.WriteRaw(m.SequenceId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryParseMessage
|
||||
|
||||
public bool TryParseMessage(ref ReadOnlySequence<byte> input, IInvocationBinder binder, [NotNullWhen(true)] out HubMessage? message)
|
||||
{
|
||||
message = null;
|
||||
|
||||
if (input.Length < LengthPrefixSize)
|
||||
return false;
|
||||
|
||||
// Read length prefix
|
||||
int payloadLength;
|
||||
if (input.FirstSpan.Length >= LengthPrefixSize)
|
||||
{
|
||||
payloadLength = Unsafe.ReadUnaligned<int>(ref Unsafe.AsRef(in input.FirstSpan[0]));
|
||||
}
|
||||
else
|
||||
{
|
||||
Span<byte> lenBuf = stackalloc byte[LengthPrefixSize];
|
||||
input.Slice(0, LengthPrefixSize).CopyTo(lenBuf);
|
||||
payloadLength = Unsafe.ReadUnaligned<int>(ref lenBuf[0]);
|
||||
}
|
||||
|
||||
var totalLength = LengthPrefixSize + payloadLength;
|
||||
if (input.Length < totalLength)
|
||||
return false;
|
||||
|
||||
var payload = input.Slice(LengthPrefixSize, payloadLength);
|
||||
|
||||
// Linearize payload for span-based reading
|
||||
ReadOnlySpan<byte> span;
|
||||
byte[]? rentedBuffer = null;
|
||||
|
||||
if (payload.IsSingleSegment)
|
||||
{
|
||||
span = payload.FirstSpan;
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedBuffer = ArrayPool<byte>.Shared.Rent(payloadLength);
|
||||
payload.CopyTo(rentedBuffer);
|
||||
span = rentedBuffer.AsSpan(0, payloadLength);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
message = ParseMessage(span, binder);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedBuffer != null)
|
||||
ArrayPool<byte>.Shared.Return(rentedBuffer);
|
||||
}
|
||||
|
||||
input = input.Slice(totalLength);
|
||||
return message != null;
|
||||
}
|
||||
|
||||
private HubMessage? ParseMessage(ReadOnlySpan<byte> span, IInvocationBinder binder)
|
||||
{
|
||||
if (span.Length == 0)
|
||||
return null;
|
||||
|
||||
var reader = new SpanReader(span);
|
||||
var msgType = reader.ReadByte();
|
||||
|
||||
return msgType switch
|
||||
{
|
||||
MsgInvocation => ParseInvocation(ref reader, binder),
|
||||
MsgStreamInvocation => ParseStreamInvocation(ref reader, binder),
|
||||
MsgStreamItem => ParseStreamItem(ref reader, binder),
|
||||
MsgCompletion => ParseCompletion(ref reader, binder),
|
||||
MsgCancelInvocation => ParseCancelInvocation(ref reader),
|
||||
MsgPing => PingMessage.Instance,
|
||||
MsgClose => ParseClose(ref reader),
|
||||
MsgAck => new AckMessage(reader.ReadInt64()),
|
||||
MsgSequence => new SequenceMessage(reader.ReadInt64()),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private HubMessage ParseInvocation(ref SpanReader r, IInvocationBinder binder)
|
||||
{
|
||||
var invocationId = r.ReadNullableString();
|
||||
var target = r.ReadString();
|
||||
var paramTypes = binder.GetParameterTypes(target);
|
||||
var args = ReadArguments(ref r, paramTypes);
|
||||
var streamIds = r.ReadStringArray();
|
||||
var headers = ReadHeaders(ref r);
|
||||
|
||||
var msg = streamIds is { Length: > 0 }
|
||||
? new InvocationMessage(invocationId, target, args, streamIds)
|
||||
: ApplyInvocationId(new InvocationMessage(target, args), invocationId);
|
||||
|
||||
if (headers != null)
|
||||
SetHeaders(msg, headers);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
private HubMessage ParseStreamInvocation(ref SpanReader r, IInvocationBinder binder)
|
||||
{
|
||||
var invocationId = r.ReadString();
|
||||
var target = r.ReadString();
|
||||
var paramTypes = binder.GetParameterTypes(target);
|
||||
var args = ReadArguments(ref r, paramTypes);
|
||||
var streamIds = r.ReadStringArray();
|
||||
var headers = ReadHeaders(ref r);
|
||||
|
||||
var msg = new StreamInvocationMessage(invocationId, target, args, streamIds);
|
||||
if (headers != null)
|
||||
SetHeaders(msg, headers);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
private HubMessage ParseStreamItem(ref SpanReader r, IInvocationBinder binder)
|
||||
{
|
||||
var invocationId = r.ReadString();
|
||||
var itemType = binder.GetStreamItemType(invocationId);
|
||||
var item = ReadSingleArgument(ref r, itemType);
|
||||
var headers = ReadHeaders(ref r);
|
||||
|
||||
var msg = new StreamItemMessage(invocationId, item);
|
||||
if (headers != null)
|
||||
SetHeaders(msg, headers);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
private HubMessage ParseCompletion(ref SpanReader r, IInvocationBinder binder)
|
||||
{
|
||||
var invocationId = r.ReadString();
|
||||
var error = r.ReadNullableString();
|
||||
var hasResult = r.ReadByte() == 1;
|
||||
|
||||
object? result = null;
|
||||
if (hasResult)
|
||||
{
|
||||
var resultType = binder.GetReturnType(invocationId);
|
||||
result = ReadSingleArgument(ref r, resultType);
|
||||
}
|
||||
|
||||
var headers = ReadHeaders(ref r);
|
||||
|
||||
CompletionMessage msg;
|
||||
if (error != null)
|
||||
msg = CompletionMessage.WithError(invocationId, error);
|
||||
else if (hasResult)
|
||||
msg = CompletionMessage.WithResult(invocationId, result);
|
||||
else
|
||||
msg = CompletionMessage.Empty(invocationId);
|
||||
|
||||
if (headers != null)
|
||||
SetHeaders(msg, headers);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
private static HubMessage ParseCancelInvocation(ref SpanReader r)
|
||||
{
|
||||
var invocationId = r.ReadString();
|
||||
var headers = ReadHeaders(ref r);
|
||||
|
||||
var msg = new CancelInvocationMessage(invocationId);
|
||||
if (headers != null)
|
||||
SetHeaders(msg, headers);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
private static HubMessage ParseClose(ref SpanReader r)
|
||||
{
|
||||
var error = r.ReadNullableString();
|
||||
var allowReconnect = r.Remaining > 0 && r.ReadByte() == 1;
|
||||
return new CloseMessage(error, allowReconnect);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Argument Serialization (AcBinary payload per argument)
|
||||
|
||||
private void WriteArguments(BufferWriterBinaryOutput w, object?[] arguments)
|
||||
{
|
||||
w.WriteVarUInt((uint)arguments.Length);
|
||||
for (var i = 0; i < arguments.Length; i++)
|
||||
WriteArgument(w, arguments[i]);
|
||||
}
|
||||
|
||||
private void WriteArgument(BufferWriterBinaryOutput w, object? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
w.WriteVarUInt(1);
|
||||
w.WriteByte(0); // BinaryTypeCode.Null
|
||||
return;
|
||||
}
|
||||
|
||||
// AcBinarySerializer needs the full payload size upfront (2-pass),
|
||||
// so we serialize to a pooled byte[] first, then copy length-prefixed.
|
||||
var serialized = AcBinarySerializer.Serialize(value, _options);
|
||||
w.WriteVarUInt((uint)serialized.Length);
|
||||
w.WriteBytes(serialized);
|
||||
}
|
||||
|
||||
private object?[] ReadArguments(ref SpanReader r, IReadOnlyList<Type> paramTypes)
|
||||
{
|
||||
var count = (int)r.ReadVarUInt();
|
||||
var args = new object?[count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var targetType = i < paramTypes.Count ? paramTypes[i] : typeof(object);
|
||||
args[i] = ReadSingleArgument(ref r, targetType);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private object? ReadSingleArgument(ref SpanReader r, Type targetType)
|
||||
{
|
||||
var argLength = (int)r.ReadVarUInt();
|
||||
if (argLength == 0)
|
||||
return null;
|
||||
|
||||
var argSpan = r.ReadSpan(argLength);
|
||||
|
||||
if (argLength == 1 && argSpan[0] == 0) // BinaryTypeCode.Null
|
||||
return null;
|
||||
|
||||
return AcBinaryDeserializer.Deserialize(argSpan, targetType, _options);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Framing Helpers (string, nullable string, string array, headers)
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteString(BufferWriterBinaryOutput w, string value)
|
||||
{
|
||||
w.WriteStringUtf8(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteNullableString(BufferWriterBinaryOutput w, string? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
w.WriteByte(0); // null marker
|
||||
return;
|
||||
}
|
||||
|
||||
w.WriteByte(1); // present marker
|
||||
w.WriteStringUtf8(value);
|
||||
}
|
||||
|
||||
private static void WriteStringArray(BufferWriterBinaryOutput w, string[]? array)
|
||||
{
|
||||
if (array == null || array.Length == 0)
|
||||
{
|
||||
w.WriteVarUInt(0);
|
||||
return;
|
||||
}
|
||||
|
||||
w.WriteVarUInt((uint)array.Length);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
w.WriteStringUtf8(array[i]);
|
||||
}
|
||||
|
||||
private static void WriteHeaders(BufferWriterBinaryOutput w, IDictionary<string, string>? headers)
|
||||
{
|
||||
if (headers == null || headers.Count == 0)
|
||||
{
|
||||
w.WriteVarUInt(0);
|
||||
return;
|
||||
}
|
||||
|
||||
w.WriteVarUInt((uint)headers.Count);
|
||||
foreach (var kv in headers)
|
||||
{
|
||||
w.WriteStringUtf8(kv.Key);
|
||||
w.WriteStringUtf8(kv.Value);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
private static InvocationMessage ApplyInvocationId(InvocationMessage msg, string? invocationId)
|
||||
{
|
||||
if (invocationId != null)
|
||||
return new InvocationMessage(invocationId, msg.Target, msg.Arguments);
|
||||
return msg;
|
||||
}
|
||||
|
||||
private static void SetHeaders(HubMessage msg, Dictionary<string, string> headers)
|
||||
{
|
||||
if (msg is HubInvocationMessage invMsg)
|
||||
invMsg.Headers = headers;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string>? ReadHeaders(ref SpanReader r)
|
||||
{
|
||||
if (r.Remaining == 0)
|
||||
return null;
|
||||
|
||||
var count = (int)r.ReadVarUInt();
|
||||
if (count == 0)
|
||||
return null;
|
||||
|
||||
var headers = new Dictionary<string, string>(count, StringComparer.Ordinal);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var key = r.ReadString();
|
||||
var value = r.ReadString();
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SpanReader
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight ref struct for sequential reading from a ReadOnlySpan.
|
||||
/// </summary>
|
||||
private ref struct SpanReader
|
||||
{
|
||||
private readonly ReadOnlySpan<byte> _span;
|
||||
private int _pos;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public SpanReader(ReadOnlySpan<byte> span)
|
||||
{
|
||||
_span = span;
|
||||
_pos = 0;
|
||||
}
|
||||
|
||||
public int Remaining
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _span.Length - _pos;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public byte ReadByte() => _span[_pos++];
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long ReadInt64()
|
||||
{
|
||||
var value = Unsafe.ReadUnaligned<long>(ref Unsafe.AsRef(in _span[_pos]));
|
||||
_pos += 8;
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public uint ReadVarUInt()
|
||||
{
|
||||
uint value = 0;
|
||||
var shift = 0;
|
||||
while (true)
|
||||
{
|
||||
var b = _span[_pos++];
|
||||
value |= (uint)(b & 0x7F) << shift;
|
||||
if ((b & 0x80) == 0)
|
||||
return value;
|
||||
shift += 7;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ReadOnlySpan<byte> ReadSpan(int length)
|
||||
{
|
||||
var result = _span.Slice(_pos, length);
|
||||
_pos += length;
|
||||
return result;
|
||||
}
|
||||
|
||||
public string ReadString()
|
||||
{
|
||||
var byteCount = (int)ReadVarUInt();
|
||||
if (byteCount == 0)
|
||||
return string.Empty;
|
||||
var bytes = ReadSpan(byteCount);
|
||||
return System.Text.Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
public string? ReadNullableString()
|
||||
{
|
||||
var marker = ReadByte();
|
||||
return marker == 0 ? null : ReadString();
|
||||
}
|
||||
|
||||
public string[]? ReadStringArray()
|
||||
{
|
||||
var count = (int)ReadVarUInt();
|
||||
if (count == 0)
|
||||
return null;
|
||||
|
||||
var array = new string[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
array[i] = ReadString();
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Loading…
Reference in New Issue