diff --git a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs index 34edc15..3396f1a 100644 --- a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs +++ b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs @@ -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); diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index f58ef46..624babf 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// allowing the deserializer to match properties by name between different types. /// Default: false (no overhead) /// - public bool UseMetadata { get; init; } = false; + public bool UseMetadata { get; set; } = true; /// /// When true, checks for duplicate property name hashes during serialization (UseMetadata mode). diff --git a/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs b/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs new file mode 100644 index 0000000..b205d12 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs @@ -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; + +/// +/// 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. +/// +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.Shared.Rent(Math.Max(initialCapacity, MinBufferSize)); + } + + /// + public override int Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _position; + } + + #region Abstract Overrides — Core Primitives + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteByte(byte value) + { + if (_position >= _buffer.Length) + GrowBuffer(_position + 1); + _buffer[_position++] = value; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteTwoBytes(byte b1, byte b2) + { + EnsureCapacity(2); + _buffer[_position++] = b1; + _buffer[_position++] = b2; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteBytes(ReadOnlySpan data) + { + EnsureCapacity(data.Length); + data.CopyTo(_buffer.AsSpan(_position)); + _position += data.Length; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteRaw(T value) + { + var size = Unsafe.SizeOf(); + EnsureCapacity(size); + Unsafe.WriteUnaligned(ref _buffer[_position], value); + _position += size; + } + + /// + [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.Shared.Rent(newSize); + _buffer.AsSpan(0, _position).CopyTo(newBuffer); + ArrayPool.Shared.Return(_buffer); + _buffer = newBuffer; + } + + #endregion + + #region Optimized Overrides — Specialized Types (direct buffer access) + + /// + /// Optimized: single EnsureCapacity + direct Unsafe.WriteUnaligned + indexer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteTypeCodeAndRaw(byte typeCode, T value) + { + var size = 1 + Unsafe.SizeOf(); + EnsureCapacity(size); + _buffer[_position++] = typeCode; + Unsafe.WriteUnaligned(ref _buffer[_position], value); + _position += Unsafe.SizeOf(); + } + + /// + /// Optimized: VarUInt with direct _buffer[_position++] access. + /// + [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; + } + + /// + /// Optimized: VarInt with direct buffer access. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteVarInt(int value) + { + var encoded = (uint)((value << 1) ^ (value >> 31)); + WriteVarUInt(encoded); + } + + /// + /// Optimized: VarULong with direct buffer access. + /// + [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; + } + + /// + /// Optimized: VarLong with direct buffer access. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteVarLong(long value) + { + var encoded = (ulong)((value << 1) ^ (value >> 63)); + WriteVarULong(encoded); + } + + /// + /// Optimized: direct Unsafe.WriteUnaligned for decimal bits. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteDecimalBits(decimal value) + { + EnsureCapacity(16); + Span bits = stackalloc int[4]; + decimal.TryGetBits(value, bits, out _); + MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16)); + _position += 16; + } + + /// + /// Optimized: direct Unsafe.WriteUnaligned + indexer for DateTime. + /// + [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; + } + + /// + /// Optimized: direct TryWriteBytes into buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteGuidBits(Guid value) + { + EnsureCapacity(16); + value.TryWriteBytes(_buffer.AsSpan(_position, 16)); + _position += 16; + } + + /// + /// Optimized: direct Unsafe.WriteUnaligned for DateTimeOffset. + /// + [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; + } + + /// + /// Optimized: direct ASCII fast path into _buffer. + /// + 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; + } + + /// + /// Optimized: FixStr with direct buffer write. + /// + 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; + } + + /// + /// Optimized: FixStrDirect with SIMD try into buffer. + /// + 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); + } + } + + /// + /// Optimized: FixStrBytes with direct buffer copy. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteFixStrBytes(ReadOnlySpan 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) + + /// + 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; + } + } + + /// + 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; + } + } + + /// + 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; + } + } + + /// + public override void WriteBytesSimd(ReadOnlySpan source) + { + EnsureCapacity(source.Length); + var destination = _buffer.AsSpan(_position, source.Length); + + if (Vector.IsHardwareAccelerated && source.Length >= Vector.Count * 2) + { + var vectorSize = Vector.Count; + var i = 0; + var length = source.Length; + var vectorCount = length / vectorSize; + for (var v = 0; v < vectorCount; v++) + { + var vec = new Vector(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 + + /// Returns the written data as a ReadOnlySpan without allocation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan AsSpan() => _buffer.AsSpan(0, _position); + + /// Copies the written data to a new exactly-sized array. + public byte[] ToArray() + { + var result = GC.AllocateUninitializedArray(_position); + _buffer.AsSpan(0, _position).CopyTo(result); + return result; + } + + /// Copies the written data to an IBufferWriter (single memcpy). + public void WriteTo(IBufferWriter writer) + { + var span = writer.GetSpan(_position); + _buffer.AsSpan(0, _position).CopyTo(span); + writer.Advance(_position); + } + + /// Resets position for reuse without deallocation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() => _position = 0; + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_buffer != null) + { + ArrayPool.Shared.Return(_buffer); + _buffer = null!; + } + } + + #endregion +} diff --git a/AyCode.Core/Serializers/Binaries/BinaryOutputBase.cs b/AyCode.Core/Serializers/Binaries/BinaryOutputBase.cs new file mode 100644 index 0000000..95aaeec --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/BinaryOutputBase.cs @@ -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; + +/// +/// 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). +/// +public abstract class BinaryOutputBase : IBinaryOutput +{ + private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + #region Abstract — Core Primitives (derived must implement) + + /// + public abstract void WriteByte(byte value); + + /// + public abstract void WriteTwoBytes(byte b1, byte b2); + + /// + public abstract void WriteBytes(ReadOnlySpan data); + + /// + public abstract void WriteRaw(T value) where T : unmanaged; + + /// + /// Ensure the backing storage can accept at least more bytes + /// without reallocation. Called by higher-level methods to batch capacity checks. + /// + protected abstract void EnsureCapacity(int additionalBytes); + + /// + public abstract int Position { get; } + + #endregion + + #region Virtual — WriteTypeCodeAndRaw + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void WriteTypeCodeAndRaw(byte typeCode, T value) where T : unmanaged + { + EnsureCapacity(1 + Unsafe.SizeOf()); + WriteByte(typeCode); + WriteRaw(value); + } + + #endregion + + #region Virtual — VarInt Encoding + + /// + public virtual void WriteVarInt(int value) + { + var encoded = (uint)((value << 1) ^ (value >> 31)); + WriteVarUInt(encoded); + } + + /// + [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); + } + + /// + public virtual void WriteVarLong(long value) + { + var encoded = (ulong)((value << 1) ^ (value >> 63)); + WriteVarULong(encoded); + } + + /// + [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 + + /// + public virtual void WriteDecimalBits(decimal value) + { + Span bits = stackalloc int[4]; + decimal.TryGetBits(value, bits, out _); + WriteBytes(MemoryMarshal.AsBytes(bits)); + } + + /// + public virtual void WriteDateTimeBits(DateTime value) + { + WriteRaw(value.Ticks); + WriteByte((byte)value.Kind); + } + + /// + public virtual void WriteGuidBits(Guid value) + { + Span buf = stackalloc byte[16]; + value.TryWriteBytes(buf); + WriteBytes(buf); + } + + /// + public virtual void WriteDateTimeOffsetBits(DateTimeOffset value) + { + WriteRaw(value.UtcTicks); + WriteRaw((short)value.Offset.TotalMinutes); + } + + #endregion + + #region Virtual — String Writes + + /// + public virtual void WriteStringUtf8(string value) + { + if (Ascii.IsValid(value)) + { + WriteVarUInt((uint)value.Length); + Span 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 utf8Buf = byteCount <= 256 + ? stackalloc byte[byteCount] + : new byte[byteCount]; + Utf8NoBom.GetBytes(value.AsSpan(), utf8Buf); + WriteBytes(utf8Buf); + } + + /// + public virtual void WriteFixStr(string value) + { + var length = value.Length; + WriteByte(BinaryTypeCode.EncodeFixStr(length)); + Span buf = length <= 256 + ? stackalloc byte[length] + : new byte[length]; + Ascii.FromUtf16(value.AsSpan(), buf, out _); + WriteBytes(buf); + } + + /// + public virtual void WriteFixStrDirect(string value) + { + var length = value.Length; + Span 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); + } + } + + /// + public virtual void WriteFixStrBytes(ReadOnlySpan utf8Bytes) + { + WriteByte(BinaryTypeCode.EncodeFixStr(utf8Bytes.Length)); + WriteBytes(utf8Bytes); + } + + /// + public virtual void WritePreencodedPropertyName(ReadOnlySpan utf8Name) + { + WriteByte(BinaryTypeCode.String); + WriteVarUInt((uint)utf8Name.Length); + WriteBytes(utf8Name); + } + + private void WriteStringUtf8Internal(string value) + { + var byteCount = Utf8NoBom.GetByteCount(value); + WriteVarUInt((uint)byteCount); + Span 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) + + /// + public virtual void WriteDoubleArrayBulk(double[] array) + { + for (var i = 0; i < array.Length; i++) + { + WriteByte(BinaryTypeCode.Float64); + WriteRaw(array[i]); + } + } + + /// + public virtual void WriteFloatArrayBulk(float[] array) + { + for (var i = 0; i < array.Length; i++) + { + WriteByte(BinaryTypeCode.Float32); + WriteRaw(array[i]); + } + } + + /// + public virtual void WriteGuidArrayBulk(Guid[] array) + { + Span buf = stackalloc byte[16]; + for (var i = 0; i < array.Length; i++) + { + WriteByte(BinaryTypeCode.Guid); + array[i].TryWriteBytes(buf); + WriteBytes(buf); + } + } + + /// + 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); + } + } + } + + /// + 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); + } + } + } + + /// + public virtual void WriteBytesSimd(ReadOnlySpan source) + { + WriteBytes(source); + } + + #endregion +} diff --git a/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs b/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs new file mode 100644 index 0000000..02c3703 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/BufferWriterBinaryOutput.cs @@ -0,0 +1,350 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace AyCode.Core.Serializers.Binaries; + +/// +/// 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 after all writes to commit any pending bytes to the underlying writer. +/// +public sealed class BufferWriterBinaryOutput : BinaryOutputBase +{ + private const int MinChunkRequest = 256; + + private readonly IBufferWriter _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 writer) + { + _writer = writer; + _chunkArray = null!; + RentChunk(MinChunkRequest); + } + + /// + public override int Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _written; + } + + /// + /// Commits any pending bytes to the underlying IBufferWriter. + /// Must be called after all writes are complete. + /// + [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.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 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.Shared.Rent(requestSize); + _chunkOffset = 0; + _chunkLength = _chunkArray.Length; + _ownedBuffer = true; + } + _chunkPos = 0; + } + + #region Abstract Overrides — Core Primitives + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteByte(byte value) + { + if (_chunkPos >= _chunkLength) + RentChunk(1); + _chunkArray[_chunkOffset + _chunkPos++] = value; + _written++; + } + + /// + [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; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteBytes(ReadOnlySpan data) + { + if (_chunkPos + data.Length > _chunkLength) + RentChunk(data.Length); + data.CopyTo(_chunkArray.AsSpan(_chunkOffset + _chunkPos, data.Length)); + _chunkPos += data.Length; + _written += data.Length; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteRaw(T value) + { + var size = Unsafe.SizeOf(); + if (_chunkPos + size > _chunkLength) + RentChunk(size); + Unsafe.WriteUnaligned(ref _chunkArray[_chunkOffset + _chunkPos], value); + _chunkPos += size; + _written += size; + } + + /// + /// Ensures the cached chunk has room for at least . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void EnsureCapacity(int additionalBytes) + { + if (_chunkPos + additionalBytes > _chunkLength) + RentChunk(additionalBytes); + } + + #endregion + + #region Optimized Overrides — Batched Writes + + /// + /// Optimized: single capacity check for type code + value, direct buffer write. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteTypeCodeAndRaw(byte typeCode, T value) + { + var size = 1 + Unsafe.SizeOf(); + if (_chunkPos + size > _chunkLength) + RentChunk(size); + var off = _chunkOffset + _chunkPos; + _chunkArray[off] = typeCode; + Unsafe.WriteUnaligned(ref _chunkArray[off + 1], value); + _chunkPos += size; + _written += size; + } + + /// + /// Optimized: VarUInt with direct cached buffer access. + /// + [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; + } + + /// + /// Optimized: ZigZag + batched VarUInt. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteVarInt(int value) + { + var encoded = (uint)((value << 1) ^ (value >> 31)); + WriteVarUInt(encoded); + } + + /// + /// Optimized: VarULong with direct cached buffer access. + /// + [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; + } + + /// + /// Optimized: ZigZag + batched VarULong. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteVarLong(long value) + { + var encoded = (ulong)((value << 1) ^ (value >> 63)); + WriteVarULong(encoded); + } + + /// + /// Optimized: single capacity check for DateTime (9 bytes), direct buffer write. + /// + [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; + } + + /// + /// Optimized: single capacity check for DateTimeOffset (10 bytes), direct buffer write. + /// + [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) + + /// + 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; + } + + /// + 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; + } + + /// + 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 +} diff --git a/AyCode.Core/Serializers/Binaries/IBinaryOutput.cs b/AyCode.Core/Serializers/Binaries/IBinaryOutput.cs new file mode 100644 index 0000000..711d7b5 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/IBinaryOutput.cs @@ -0,0 +1,111 @@ +using System; +using System.Runtime.CompilerServices; + +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Abstraction for binary serialization output. +/// Implementations can write to byte[] (ArrayBinaryOutput) or IBufferWriter (BufferWriterBinaryOutput). +/// Custom implementations can be provided for specialized output targets. +/// +public interface IBinaryOutput +{ + #region Core Writes + + /// Write a single byte. + void WriteByte(byte value); + + /// Write two bytes efficiently. + void WriteTwoBytes(byte b1, byte b2); + + /// Write a span of bytes. + void WriteBytes(ReadOnlySpan data); + + /// Write an unmanaged value directly (no encoding). + void WriteRaw(T value) where T : unmanaged; + + /// Write a type code byte followed by an unmanaged value. Batches capacity check. + void WriteTypeCodeAndRaw(byte typeCode, T value) where T : unmanaged; + + #endregion + + #region VarInt Encoding + + /// Write a ZigZag-encoded variable-length int32. + void WriteVarInt(int value); + + /// Write a variable-length uint32. + void WriteVarUInt(uint value); + + /// Write a ZigZag-encoded variable-length int64. + void WriteVarLong(long value); + + /// Write a variable-length uint64. + void WriteVarULong(ulong value); + + #endregion + + #region Specialized Types + + /// Write decimal as 16 raw bytes (4 x int32 bits). + void WriteDecimalBits(decimal value); + + /// Write DateTime as 8 bytes ticks + 1 byte kind. + void WriteDateTimeBits(DateTime value); + + /// Write Guid as 16 raw bytes. + void WriteGuidBits(Guid value); + + /// Write DateTimeOffset as 8 bytes UTC ticks + 2 bytes offset minutes. + void WriteDateTimeOffsetBits(DateTimeOffset value); + + #endregion + + #region String Writes + + /// Write UTF8 string with VarUInt length prefix. Fast path for ASCII. + void WriteStringUtf8(string value); + + /// Write short ASCII string using FixStr encoding (type+length in single byte). + void WriteFixStr(string value); + + /// Write FixStr with SIMD ASCII try, falls back to standard UTF8. + void WriteFixStrDirect(string value); + + /// Write pre-encoded UTF8 bytes using FixStr encoding. + void WriteFixStrBytes(ReadOnlySpan utf8Bytes); + + /// Write pre-encoded property name with String type code + VarUInt length + bytes. + void WritePreencodedPropertyName(ReadOnlySpan utf8Name); + + #endregion + + #region Bulk Array Writes + + /// Write double[] with per-element type codes. Override for optimized bulk write. + void WriteDoubleArrayBulk(double[] array); + + /// Write float[] with per-element type codes. Override for optimized bulk write. + void WriteFloatArrayBulk(float[] array); + + /// Write Guid[] with per-element type codes. Override for optimized bulk write. + void WriteGuidArrayBulk(Guid[] array); + + /// Write int[] with TinyInt optimization per element. + void WriteInt32ArrayOptimized(int[] array); + + /// Write long[] with TinyInt/Int32 downcast optimization per element. + void WriteLongArrayOptimized(long[] array); + + /// Write bytes using SIMD when available, standard copy otherwise. + void WriteBytesSimd(ReadOnlySpan source); + + #endregion + + #region Position + + /// Current write position (total bytes written so far). + int Position { get; } + + #endregion +} diff --git a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs new file mode 100644 index 0000000..54b3305 --- /dev/null +++ b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs @@ -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; + +/// +/// 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. +/// +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; + } + + /// + /// Runtime-replaceable serializer options. + /// Thread-safe: uses volatile field, callers see the new options on next message. + /// + 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 GetMessageBytes(HubMessage message) + { + var writer = new ArrayBufferWriter(256); + WriteMessage(message, writer); + return writer.WrittenMemory; + } + + public void WriteMessage(HubMessage message, IBufferWriter 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 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(ref Unsafe.AsRef(in input.FirstSpan[0])); + } + else + { + Span lenBuf = stackalloc byte[LengthPrefixSize]; + input.Slice(0, LengthPrefixSize).CopyTo(lenBuf); + payloadLength = Unsafe.ReadUnaligned(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 span; + byte[]? rentedBuffer = null; + + if (payload.IsSingleSegment) + { + span = payload.FirstSpan; + } + else + { + rentedBuffer = ArrayPool.Shared.Rent(payloadLength); + payload.CopyTo(rentedBuffer); + span = rentedBuffer.AsSpan(0, payloadLength); + } + + try + { + message = ParseMessage(span, binder); + } + finally + { + if (rentedBuffer != null) + ArrayPool.Shared.Return(rentedBuffer); + } + + input = input.Slice(totalLength); + return message != null; + } + + private HubMessage? ParseMessage(ReadOnlySpan 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 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? 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 headers) + { + if (msg is HubInvocationMessage invMsg) + invMsg.Headers = headers; + } + + private static Dictionary? ReadHeaders(ref SpanReader r) + { + if (r.Remaining == 0) + return null; + + var count = (int)r.ReadVarUInt(); + if (count == 0) + return null; + + var headers = new Dictionary(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 + + /// + /// Lightweight ref struct for sequential reading from a ReadOnlySpan. + /// + private ref struct SpanReader + { + private readonly ReadOnlySpan _span; + private int _pos; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SpanReader(ReadOnlySpan 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(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 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 +}