From 96409fe321a8f2afa1d07205f8df692fb2447219 Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 13 Feb 2026 09:32:32 +0100 Subject: [PATCH] Refactor: generic binary input for deserialization Refactored deserialization to use IBinaryInputBase abstraction, supporting both ArrayBinaryInput (byte[]) and SequenceBinaryInput (ReadOnlySequence). All context and methods are now generic over TInput, enabling zero-copy for arrays and true streaming for multi-segment sources. Internal logic (ReadValue, Populate, Skip, etc.) is specialized per input type, improving performance and flexibility. This enables future extensibility for other input sources and optimizes handling of large or segmented payloads. --- ...lizer.BinaryDeserializationContext.Read.cs | 5 +- ...serializer.BinaryDeserializationContext.cs | 38 +- .../AcBinaryDeserializer.CrossType.cs | 30 +- .../Binaries/AcBinaryDeserializer.Populate.cs | 42 +- .../Binaries/AcBinaryDeserializer.cs | 627 ++++++++---------- .../Serializers/Binaries/ArrayBinaryInput.cs | 46 ++ .../Serializers/Binaries/IBinaryInputBase.cs | 34 + .../Binaries/SequenceBinaryInput.cs | 143 ++++ 8 files changed, 551 insertions(+), 414 deletions(-) create mode 100644 AyCode.Core/Serializers/Binaries/ArrayBinaryInput.cs create mode 100644 AyCode.Core/Serializers/Binaries/IBinaryInputBase.cs create mode 100644 AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs index 802dd2b..b1c7e00 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs @@ -8,7 +8,7 @@ namespace AyCode.Core.Serializers.Binaries; public static partial class AcBinaryDeserializer { - internal sealed partial class BinaryDeserializationContext + internal sealed partial class BinaryDeserializationContext { private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); @@ -370,7 +370,8 @@ public static partial class AcBinaryDeserializer { if (_position > _bufferLength - length) { - throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position); + if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, length)) + throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position); } } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index 023e7d2..efc200b 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -12,12 +12,12 @@ public static partial class AcBinaryDeserializer /// Pool for BinaryDeserializationContext instances. /// Eliminates per-call heap allocation — mirrors BinarySerializationContextPool pattern. /// - private static class DeserializationContextPool + private static class DeserializationContextPool where TInput : struct, IBinaryInputBase { - private static readonly ConcurrentQueue Pool = new(); + private static readonly ConcurrentQueue> Pool = new(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static BinaryDeserializationContext Get(AcBinarySerializerOptions options) + public static BinaryDeserializationContext Get(AcBinarySerializerOptions options) { if (Pool.TryDequeue(out var ctx)) { @@ -25,13 +25,13 @@ public static partial class AcBinaryDeserializer return ctx; } - var newCtx = new BinaryDeserializationContext(); + var newCtx = new BinaryDeserializationContext(); newCtx.Reset(options); return newCtx; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Return(BinaryDeserializationContext ctx) + public static void Return(BinaryDeserializationContext ctx) { if (Pool.Count < ctx.Options.MaxContextPoolSize) { @@ -46,10 +46,17 @@ public static partial class AcBinaryDeserializer /// Holds all state: buffer, position, caches, options, metadata, interning. /// Buffer state and read methods are directly in the context (via partial Read.cs) /// for zero-indirection hot-path access — mirrors the serializer pattern. + /// TInput handles buffer lifecycle (Initialize/AdvanceSegment) — mirrors BinarySerializationContext<TOutput>. /// - internal sealed partial class BinaryDeserializationContext + internal sealed partial class BinaryDeserializationContext : AcSerializerContextBase + where TInput : struct, IBinaryInputBase { + /// + /// Input target — handles only Initialize (startup) and AdvanceSegment (cold path). + /// + public TInput Input; + // Marker-based interning: sequential cache (no footer needed) private object?[]? _internCache; @@ -116,15 +123,14 @@ public static partial class AcBinaryDeserializer } /// - /// Sets the buffer for a new deserialization operation from a byte array. - /// Zero-copy: context references the byte[] directly. + /// Initializes the context with the given TInput. + /// Calls Input.Initialize to set up buffer/position/length. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void InitBuffer(byte[] data, int length) + public void InitInput(TInput input) { - _buffer = data; - _bufferLength = length; - _position = 0; + Input = input; + Input.Initialize(out _buffer, out _position, out _bufferLength); _useStringCaching = Options.UseStringCaching; _maxCachedStringLength = Options.MaxCachedStringLength; if (_useStringCaching) GetOrCreateStringCache(); @@ -136,14 +142,14 @@ public static partial class AcBinaryDeserializer } /// - /// Sets the buffer from a ReadOnlySpan by copying to a linearized buffer. - /// Used when the source is not a byte[] (e.g. stackalloc, pinned). + /// Initializes the context from a ReadOnlySpan by copying to a pooled linearized buffer, + /// then creating an ArrayBinaryInput. Used when TInput is ArrayBinaryInput. /// - public void InitBufferFromSpan(ReadOnlySpan data) + public void InitFromSpan(ReadOnlySpan data) { var buffer = RentLinearizedBuffer(data.Length); data.CopyTo(buffer); - InitBuffer(buffer, data.Length); + InitInput((TInput)(object)new ArrayBinaryInput(buffer, data.Length)); } #region Header diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs index fb7d09b..b65a497 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs @@ -55,8 +55,8 @@ public static partial class AcBinaryDeserializer // Cross-type path: use index mapping var indexMapping = GetIndexMapping(sourceType, destType, options.PropertyMapper); - var context = DeserializationContextPool.Get(options); - context.InitBufferFromSpan(data); + var context = DeserializationContextPool.Get(options); + context.InitFromSpan(data); try { @@ -76,7 +76,7 @@ public static partial class AcBinaryDeserializer } finally { - DeserializationContextPool.Return(context); + DeserializationContextPool.Return(context); } } @@ -141,8 +141,8 @@ public static partial class AcBinaryDeserializer // Cross-type path: use index mapping var indexMapping = GetIndexMapping(sourceType, destType, options.PropertyMapper); - var context = DeserializationContextPool.Get(options); - context.InitBufferFromSpan(data); + var context = DeserializationContextPool.Get(options); + context.InitFromSpan(data); try { @@ -179,7 +179,7 @@ public static partial class AcBinaryDeserializer } finally { - DeserializationContextPool.Return(context); + DeserializationContextPool.Return(context); } } @@ -207,7 +207,8 @@ public static partial class AcBinaryDeserializer /// Reads a value with index mapping applied. /// Maps source PropertyIndex to destination PropertyIndex using the provided mapping. /// - private static object? ReadValueWithMapping(BinaryDeserializationContext context, Type destType, int[] indexMapping, int depth) + private static object? ReadValueWithMapping(BinaryDeserializationContext context, Type destType, int[] indexMapping, int depth) + where TInput : struct, IBinaryInputBase { var typeCode = context.ReadByte(); @@ -224,7 +225,8 @@ public static partial class AcBinaryDeserializer /// Reads an object using index mapping for property resolution. /// Note: Object marker already consumed by caller. /// - private static object? ReadObjectWithMapping(BinaryDeserializationContext context, Type destType, int[] indexMapping, int depth, bool registerInCache) + private static object? ReadObjectWithMapping(BinaryDeserializationContext context, Type destType, int[] indexMapping, int depth, bool registerInCache) + where TInput : struct, IBinaryInputBase { var wrapper = context.GetWrapper(destType); var metadata = wrapper.Metadata; @@ -246,12 +248,13 @@ public static partial class AcBinaryDeserializer /// Populates an object using index mapping. /// Source property indices are remapped to destination indices. /// - private static void PopulateObjectWithMapping( - BinaryDeserializationContext context, + private static void PopulateObjectWithMapping( + BinaryDeserializationContext context, object target, Type destType, int[] indexMapping, int depth) + where TInput : struct, IBinaryInputBase { var wrapper = context.GetWrapper(destType); var metadata = wrapper.Metadata; @@ -303,8 +306,8 @@ public static partial class AcBinaryDeserializer /// Common logic for populating a single property value. /// Shared between normal populate and cross-type populate. /// - private static void PopulatePropertyValue( - BinaryDeserializationContext context, + private static void PopulatePropertyValue( + BinaryDeserializationContext context, object target, BinaryPropertySetterInfo propInfo, TypeMetadataWrapper wrapper, @@ -314,6 +317,7 @@ public static partial class AcBinaryDeserializer int currentIndex, int totalCount, int depth) + where TInput : struct, IBinaryInputBase { var peekCode = context.PeekByte(); @@ -387,4 +391,4 @@ public static partial class AcBinaryDeserializer } #endregion -} +} \ No newline at end of file diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index 7df78e7..7da9a30 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -32,7 +32,8 @@ public static partial class AcBinaryDeserializer /// - Non-IId + All: [Object][hashcode][props 0-t�l...] - hashcode prefix /// - Ref=Off: [Object][props 0-t�l...] - no prefix /// - private static void PopulateObject(BinaryDeserializationContext context, object target, Type targetType, int depth) + private static void PopulateObject(BinaryDeserializationContext context, object target, Type targetType, int depth) + where TInput : struct, IBinaryInputBase { var wrapper = context.GetWrapper(targetType); @@ -45,12 +46,13 @@ public static partial class AcBinaryDeserializer /// Wire format: All properties are written WITH type markers (including Id for IId types). /// No hashcode prefix - position-based footer handles reference tracking. /// - private static void PopulateObjectCore( - BinaryDeserializationContext context, + private static void PopulateObjectCore( + BinaryDeserializationContext context, object target, TypeMetadataWrapper wrapper, int depth, bool skipDefaultWrite) + where TInput : struct, IBinaryInputBase { PopulateObjectPropertiesIndexed(context, target, wrapper, depth, skipDefaultWrite); } @@ -60,12 +62,13 @@ public static partial class AcBinaryDeserializer /// UseMetadata=true: cacheMap[i] gives the setter (null → skip). /// UseMetadata=false: properties[i] gives the setter directly. /// - private static void PopulateObjectPropertiesIndexed( - BinaryDeserializationContext context, + private static void PopulateObjectPropertiesIndexed( + BinaryDeserializationContext context, object target, TypeMetadataWrapper wrapper, int depth, bool skipDefaultWrite) + where TInput : struct, IBinaryInputBase { var metadata = wrapper.Metadata; var properties = metadata.PropertiesArray; @@ -110,8 +113,8 @@ public static partial class AcBinaryDeserializer /// /// Standard marker-based property read. Extracted to avoid duplicating logic in both loops. /// - private static void PopulatePropertyWithMarker( - BinaryDeserializationContext context, + private static void PopulatePropertyWithMarker( + BinaryDeserializationContext context, object target, BinaryPropertySetterBase? propInfo, BinaryDeserializeTypeMetadata metadata, @@ -120,6 +123,7 @@ public static partial class AcBinaryDeserializer bool skipDefaultWrite, int propertyIndex, int depth) + where TInput : struct, IBinaryInputBase { var peekCode = context.PeekByte(); @@ -227,7 +231,8 @@ public static partial class AcBinaryDeserializer /// Only called for non-nullable value types with ExpectedTypeCode set. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ReadAndSetMarkerlessValue(BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo) + private static void ReadAndSetMarkerlessValue(BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo) + where TInput : struct, IBinaryInputBase { switch (propInfo.AccessorType) { @@ -274,7 +279,8 @@ public static partial class AcBinaryDeserializer /// Called from ReadObject/ReadObjectWithMetadata for new instances. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void PopulateObject(BinaryDeserializationContext context, object target, TypeMetadataWrapper wrapper, int depth, bool skipDefaultWrite) + private static void PopulateObject(BinaryDeserializationContext context, object target, TypeMetadataWrapper wrapper, int depth, bool skipDefaultWrite) + where TInput : struct, IBinaryInputBase { PopulateObjectPropertiesIndexed(context, target, wrapper, depth, skipDefaultWrite); } @@ -283,7 +289,8 @@ public static partial class AcBinaryDeserializer #region Populate List Methods - private static void PopulateList(BinaryDeserializationContext context, IList targetList, Type listType, int depth) + private static void PopulateList(BinaryDeserializationContext context, IList targetList, Type listType, int depth) + where TInput : struct, IBinaryInputBase { var elementType = GetCollectionElementType(listType) ?? typeof(object); @@ -313,7 +320,8 @@ public static partial class AcBinaryDeserializer /// /// Optimized list populate that reuses existing items when possible. /// - private static void PopulateListOptimized(BinaryDeserializationContext context, IList existingList, BinaryPropertySetterBase propInfo, int depth) + private static void PopulateListOptimized(BinaryDeserializationContext context, IList existingList, BinaryPropertySetterBase propInfo, int depth) + where TInput : struct, IBinaryInputBase { var elementType = propInfo.ElementType ?? typeof(object); var count = (int)context.ReadVarUInt(); @@ -385,7 +393,8 @@ public static partial class AcBinaryDeserializer /// IId collection merge using cached property info. /// Matches items by Id, updates existing, adds new, optionally removes orphans. /// - private static void MergeIIdCollection(BinaryDeserializationContext context, IList existingList, BinaryPropertySetterBase propInfo, int depth) + private static void MergeIIdCollection(BinaryDeserializationContext context, IList existingList, BinaryPropertySetterBase propInfo, int depth) + where TInput : struct, IBinaryInputBase { var elementType = propInfo.ElementType!; var idGetter = propInfo.ElementIdGetter!; @@ -486,12 +495,13 @@ public static partial class AcBinaryDeserializer /// /// IId collection merge using type metadata (for top-level list merge). /// - private static void MergeIIdCollectionWithMetadata( - BinaryDeserializationContext context, + private static void MergeIIdCollectionWithMetadata( + BinaryDeserializationContext context, IList existingList, Type elementType, TypeMetadataWrapper wrapper, int depth) + where TInput : struct, IBinaryInputBase { var elementMetadata = wrapper.Metadata; var idGetter = elementMetadata.IdGetter!; @@ -603,7 +613,8 @@ public static partial class AcBinaryDeserializer /// Reads Id value without type marker. The serializer didn't write a marker for IId types. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ReadIdValueWithoutMarker(BinaryDeserializationContext context, object target, BinaryPropertySetterInfo propInfo, IdAccessorType idType) + private static void ReadIdValueWithoutMarker(BinaryDeserializationContext context, object target, BinaryPropertySetterInfo propInfo, IdAccessorType idType) + where TInput : struct, IBinaryInputBase { switch (idType) { @@ -646,4 +657,3 @@ public static partial class AcBinaryDeserializer #endregion } - diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 7d6efbd..96daac3 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -35,71 +35,76 @@ public static partial class AcBinaryDeserializer [ThreadStatic] private static Dictionary? t_typeConversionLocalCache; - // Type dispatch table for fast ReadValue - private delegate object? TypeReader(BinaryDeserializationContext context, Type targetType, int depth); + // Type dispatch table for fast ReadValue — generic per TInput, JIT specializes each + private delegate object? TypeReader(BinaryDeserializationContext context, Type targetType, int depth) + where TInput : struct, IBinaryInputBase; - private static readonly TypeReader?[] TypeReaders = new TypeReader[byte.MaxValue + 1]; - private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); - - static AcBinaryDeserializer() + private static class TypeReaderTable where TInput : struct, IBinaryInputBase { - RegisterReader(BinaryTypeCode.Null, static (BinaryDeserializationContext _, Type _, int _) => null); - RegisterReader(BinaryTypeCode.True, static (BinaryDeserializationContext _, Type _, int _) => true); - RegisterReader(BinaryTypeCode.False, static (BinaryDeserializationContext _, Type _, int _) => false); - RegisterReader(BinaryTypeCode.Int8, static (BinaryDeserializationContext ctx, Type _, int _) => (sbyte)ctx.ReadByte()); - RegisterReader(BinaryTypeCode.UInt8, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadByte()); - RegisterReader(BinaryTypeCode.Int16, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadInt16Unsafe()); - RegisterReader(BinaryTypeCode.UInt16, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadUInt16Unsafe()); - RegisterReader(BinaryTypeCode.Int32, static (BinaryDeserializationContext ctx, Type type, int _) => ReadInt32Value(ctx, type)); - RegisterReader(BinaryTypeCode.UInt32, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarUInt()); - RegisterReader(BinaryTypeCode.Int64, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarLong()); - RegisterReader(BinaryTypeCode.UInt64, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarULong()); - RegisterReader(BinaryTypeCode.Float32, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadSingleUnsafe()); - RegisterReader(BinaryTypeCode.Float64, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe()); - RegisterReader(BinaryTypeCode.Decimal, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe()); - RegisterReader(BinaryTypeCode.Char, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe()); - RegisterReader(BinaryTypeCode.String, static (BinaryDeserializationContext ctx, Type _, int _) => ReadPlainString(ctx)); - RegisterReader(BinaryTypeCode.StringInterned, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt())); - RegisterReader(BinaryTypeCode.StringEmpty, static (BinaryDeserializationContext _, Type _, int _) => string.Empty); - // StringInternFirst: first occurrence of interned string - read cacheIndex + content + register in cache - RegisterReader(BinaryTypeCode.StringInternFirst, static (BinaryDeserializationContext ctx, Type _, int _) => - ReadAndRegisterInternedString(ctx)); - RegisterReader(BinaryTypeCode.DateTime, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe()); - RegisterReader(BinaryTypeCode.DateTimeOffset, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe()); - RegisterReader(BinaryTypeCode.TimeSpan, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe()); - RegisterReader(BinaryTypeCode.Guid, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe()); - RegisterReader(BinaryTypeCode.Enum, static (BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ctx, type)); - RegisterReader(BinaryTypeCode.Object, ReadObject); - RegisterReader(BinaryTypeCode.ObjectRefFirst, ReadObjectRefFirst); - RegisterReader(BinaryTypeCode.ObjectWithMetadata, ReadObjectWithMetadata); - RegisterReader(BinaryTypeCode.ObjectWithMetadataRefFirst, ReadObjectWithMetadataRefFirst); - RegisterReader(BinaryTypeCode.ObjectRef, ReadObjectRef); - RegisterReader(BinaryTypeCode.Array, ReadArray); - RegisterReader(BinaryTypeCode.Dictionary, ReadDictionary); - RegisterReader(BinaryTypeCode.ByteArray, static (BinaryDeserializationContext ctx, Type _, int _) => ReadByteArray(ctx)); - - // Register FixStr readers (34-65) - for (byte code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++) + public static readonly TypeReader?[] Readers = InitReaders(); + + private static TypeReader?[] InitReaders() { - var length = BinaryTypeCode.DecodeFixStrLength(code); - RegisterReader(code, CreateFixStrReader(length)); + var readers = new TypeReader?[byte.MaxValue + 1]; + readers[BinaryTypeCode.Null] = static (_, _, _) => null; + readers[BinaryTypeCode.True] = static (_, _, _) => true; + readers[BinaryTypeCode.False] = static (_, _, _) => false; + readers[BinaryTypeCode.Int8] = static (ctx, _, _) => (sbyte)ctx.ReadByte(); + readers[BinaryTypeCode.UInt8] = static (ctx, _, _) => ctx.ReadByte(); + readers[BinaryTypeCode.Int16] = static (ctx, _, _) => ctx.ReadInt16Unsafe(); + readers[BinaryTypeCode.UInt16] = static (ctx, _, _) => ctx.ReadUInt16Unsafe(); + readers[BinaryTypeCode.Int32] = static (ctx, type, _) => ReadInt32Value(ctx, type); + readers[BinaryTypeCode.UInt32] = static (ctx, _, _) => ctx.ReadVarUInt(); + readers[BinaryTypeCode.Int64] = static (ctx, _, _) => ctx.ReadVarLong(); + readers[BinaryTypeCode.UInt64] = static (ctx, _, _) => ctx.ReadVarULong(); + readers[BinaryTypeCode.Float32] = static (ctx, _, _) => ctx.ReadSingleUnsafe(); + readers[BinaryTypeCode.Float64] = static (ctx, _, _) => ctx.ReadDoubleUnsafe(); + readers[BinaryTypeCode.Decimal] = static (ctx, _, _) => ctx.ReadDecimalUnsafe(); + readers[BinaryTypeCode.Char] = static (ctx, _, _) => ctx.ReadCharUnsafe(); + readers[BinaryTypeCode.String] = static (ctx, _, _) => ReadPlainString(ctx); + readers[BinaryTypeCode.StringInterned] = static (ctx, _, _) => ctx.GetInternedString((int)ctx.ReadVarUInt()); + readers[BinaryTypeCode.StringEmpty] = static (_, _, _) => string.Empty; + readers[BinaryTypeCode.StringInternFirst] = static (ctx, _, _) => ReadAndRegisterInternedString(ctx); + readers[BinaryTypeCode.DateTime] = static (ctx, _, _) => ctx.ReadDateTimeUnsafe(); + readers[BinaryTypeCode.DateTimeOffset] = static (ctx, _, _) => ctx.ReadDateTimeOffsetUnsafe(); + readers[BinaryTypeCode.TimeSpan] = static (ctx, _, _) => ctx.ReadTimeSpanUnsafe(); + readers[BinaryTypeCode.Guid] = static (ctx, _, _) => ctx.ReadGuidUnsafe(); + readers[BinaryTypeCode.Enum] = static (ctx, type, _) => ReadEnumValue(ctx, type); + readers[BinaryTypeCode.Object] = ReadObject; + readers[BinaryTypeCode.ObjectRefFirst] = ReadObjectRefFirst; + readers[BinaryTypeCode.ObjectWithMetadata] = ReadObjectWithMetadata; + readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst; + readers[BinaryTypeCode.ObjectRef] = ReadObjectRef; + readers[BinaryTypeCode.Array] = ReadArray; + readers[BinaryTypeCode.Dictionary] = ReadDictionary; + readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx); + + // Register FixStr readers (34-65) + for (byte code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++) + { + var length = BinaryTypeCode.DecodeFixStrLength(code); + readers[code] = CreateFixStrReader(length); + } + + return readers; } } + /// /// Creates a reader for FixStr with the given length. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static TypeReader CreateFixStrReader(int length) + private static TypeReader CreateFixStrReader(int length) + where TInput : struct, IBinaryInputBase { if (length == 0) - return static (BinaryDeserializationContext _, Type _, int _) => string.Empty; - - return (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadStringUtf8(length); + return static (_, _, _) => string.Empty; + + return (ctx, _, _) => ctx.ReadStringUtf8(length); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void RegisterReader(byte typeCode, TypeReader reader) => TypeReaders[typeCode] = reader; + private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); #region Public API @@ -107,13 +112,26 @@ public static partial class AcBinaryDeserializer /// Deserialize binary data to object of type T. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T? Deserialize(byte[] data) => Deserialize(data.AsSpan(), AcBinarySerializerOptions.Default); + public static T? Deserialize(byte[] data) => Deserialize(data, AcBinarySerializerOptions.Default); /// /// Deserialize binary data to object of type T with options. + /// Zero-copy: ArrayBinaryInput references the byte[] directly. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T? Deserialize(byte[] data, AcBinarySerializerOptions options) => Deserialize(data.AsSpan(), options); + public static T? Deserialize(byte[] data, AcBinarySerializerOptions options) + { + if (data.Length == 0) return default; + if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default; + + var targetType = typeof(T); + if (AcSerializerCommon.IsExpressionType(targetType)) + return (T?)(object?)DeserializeExpression(data, targetType, options); + + var context = DeserializationContextPool.Get(options); + context.InitInput(new ArrayBinaryInput(data)); + try { return (T?)DeserializeCore(context, targetType); } + finally { DeserializationContextPool.Return(context); } + } /// /// Deserialize binary data to object of type T. @@ -129,37 +147,13 @@ public static partial class AcBinaryDeserializer if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default; var targetType = typeof(T); - - // Handle Expression types - deserialize as AcExpressionNode and rebuild if (AcSerializerCommon.IsExpressionType(targetType)) - { return (T?)(object?)DeserializeExpression(data, targetType, options); - } - var context = DeserializationContextPool.Get(options); - context.InitBufferFromSpan(data); - - try - { - context.ReadHeader(); - var result = ReadValue(context, targetType, 0); - // Position-based string interning - no validation needed - return (T?)result; - } - catch (AcBinaryDeserializationException) - { - throw; - } - catch (Exception ex) - { - throw new AcBinaryDeserializationException( - $"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}", - context.Position, targetType, ex); - } - finally - { - DeserializationContextPool.Return(context); - } + var context = DeserializationContextPool.Get(options); + context.InitFromSpan(data); + try { return (T?)DeserializeCore(context, targetType); } + finally { DeserializationContextPool.Return(context); } } /// @@ -176,36 +170,13 @@ public static partial class AcBinaryDeserializer if (data.Length == 0) return null; if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return null; - // Handle Expression types - deserialize as AcExpressionNode and rebuild if (AcSerializerCommon.IsExpressionType(targetType)) - { return DeserializeExpression(data, targetType, options); - } - var context = DeserializationContextPool.Get(options); - context.InitBufferFromSpan(data); - - try - { - context.ReadHeader(); - var result = ReadValue(context, targetType, 0); - // Position-based string interning - no validation needed - return result; - } - catch (AcBinaryDeserializationException) - { - throw; - } - catch (Exception ex) - { - throw new AcBinaryDeserializationException( - $"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}", - context.Position, targetType, ex); - } - finally - { - DeserializationContextPool.Return(context); - } + var context = DeserializationContextPool.Get(options); + context.InitFromSpan(data); + try { return DeserializeCore(context, targetType); } + finally { DeserializationContextPool.Return(context); } } /// @@ -217,6 +188,7 @@ public static partial class AcBinaryDeserializer /// /// Deserialize binary data from a ReadOnlySequence with options. + /// Single-segment: zero-copy via FirstSpan. Multi-segment: uses SequenceBinaryInput for true streaming. /// public static T? Deserialize(ReadOnlySequence data, AcBinarySerializerOptions options) { @@ -225,17 +197,7 @@ public static partial class AcBinaryDeserializer if (data.IsSingleSegment) return Deserialize(data.FirstSpan, options); - var context = DeserializationContextPool.Get(options); - try - { - var buffer = context.RentLinearizedBuffer((int)data.Length); - data.CopyTo(buffer); - return Deserialize(buffer.AsSpan(0, (int)data.Length), context); - } - finally - { - DeserializationContextPool.Return(context); - } + return DeserializeSequence(new SequenceBinaryInput(data), typeof(T), options); } /// @@ -254,56 +216,56 @@ public static partial class AcBinaryDeserializer if (data.IsSingleSegment) return Deserialize(data.FirstSpan, targetType, options); - var context = DeserializationContextPool.Get(options); - try - { - var buffer = context.RentLinearizedBuffer((int)data.Length); - data.CopyTo(buffer); - return Deserialize(buffer.AsSpan(0, (int)data.Length), targetType, context); - } - finally - { - DeserializationContextPool.Return(context); - } + return DeserializeSequence(new SequenceBinaryInput(data), targetType, options); } /// - /// Internal: Deserialize with pre-pooled context (used by ReadOnlySequence multi-segment path). + /// Internal: Deserialize with any TInput (multi-segment or other future input types). /// - private static T? Deserialize(ReadOnlySpan data, BinaryDeserializationContext context) + private static T? DeserializeSequence(TInput input, Type targetType, AcBinarySerializerOptions options) + where TInput : struct, IBinaryInputBase { - if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default; - - var targetType = typeof(T); if (AcSerializerCommon.IsExpressionType(targetType)) - return (T?)(object?)DeserializeExpression(data, targetType, context); + return Deserialize(LinearizeSequence(input), options); - context.InitBufferFromSpan(data); - try - { - context.ReadHeader(); - return (T?)ReadValue(context, targetType, 0); - } - catch (AcBinaryDeserializationException) { throw; } - catch (Exception ex) - { - throw new AcBinaryDeserializationException( - $"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}", - context.Position, targetType, ex); - } + var context = DeserializationContextPool.Get(options); + context.InitInput(input); + try { return (T?)DeserializeCore(context, targetType); } + finally { DeserializationContextPool.Return(context); } } /// - /// Internal: Deserialize with pre-pooled context (used by ReadOnlySequence multi-segment path). + /// Internal: Deserialize with any TInput (multi-segment, non-generic result). /// - private static object? Deserialize(ReadOnlySpan data, Type targetType, BinaryDeserializationContext context) + private static object? DeserializeSequence(TInput input, Type targetType, AcBinarySerializerOptions options) + where TInput : struct, IBinaryInputBase { - if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return null; - if (AcSerializerCommon.IsExpressionType(targetType)) - return DeserializeExpression(data, targetType, context); + return Deserialize(LinearizeSequence(input), targetType, options); - context.InitBufferFromSpan(data); + var context = DeserializationContextPool.Get(options); + context.InitInput(input); + try { return DeserializeCore(context, targetType); } + finally { DeserializationContextPool.Return(context); } + } + + /// + /// Fallback: linearize a TInput into a contiguous ReadOnlySpan (for Expression deserialization). + /// + private static ReadOnlySpan LinearizeSequence(TInput input) + where TInput : struct, IBinaryInputBase + { + input.Initialize(out var buffer, out var position, out var bufferLength); + return buffer.AsSpan(position, bufferLength - position); + } + + /// + /// Core deserialization: ReadHeader + ReadValue with unified error handling. + /// All public Deserialize overloads delegate here after pool/init setup. + /// + private static object? DeserializeCore(BinaryDeserializationContext context, Type targetType) + where TInput : struct, IBinaryInputBase + { try { context.ReadHeader(); @@ -319,61 +281,67 @@ public static partial class AcBinaryDeserializer } /// - /// Internal: DeserializeExpression with pre-pooled context. + /// Deserialize Expression from binary data. /// - private static Expression? DeserializeExpression(ReadOnlySpan data, Type targetExpressionType, BinaryDeserializationContext context) + private static Expression? DeserializeExpression(ReadOnlySpan data, Type targetExpressionType, AcBinarySerializerOptions options) { - context.InitBufferFromSpan(data); + var context = DeserializationContextPool.Get(options); + context.InitFromSpan(data); try { - context.ReadHeader(); - var node = (AcExpressionNode?)ReadValue(context, typeof(AcExpressionNode), 0); + var node = (AcExpressionNode?)DeserializeCore(context, typeof(AcExpressionNode)); if (node == null) return null; var entityType = AcSerializerCommon.GetExpressionEntityType(targetExpressionType); return AcExpressionRebuilder.FromNode(node, entityType); } + finally + { + DeserializationContextPool.Return(context); + } + } + + /// + /// Core populate: ReadHeader + dispatch by typeCode. Used by Populate, PopulateMerge, ThenPopulate. + /// + private static void PopulateCore(BinaryDeserializationContext context, object target) + where TInput : struct, IBinaryInputBase + { + var targetType = target.GetType(); + try + { + context.ReadHeader(); + var typeCode = context.PeekByte(); + + if (typeCode == BinaryTypeCode.Object) + { + context.ReadByte(); + PopulateObject(context, target, targetType, 0); + } + else if (typeCode == BinaryTypeCode.ObjectWithMetadata) + { + context.ReadByte(); + ReadInlineMetadataForPopulate(context, targetType); + PopulateObject(context, target, targetType, 0); + } + else if (typeCode == BinaryTypeCode.Array && target is IList targetList) + { + context.ReadByte(); + PopulateList(context, targetList, targetType, 0); + } + else + { + throw new AcBinaryDeserializationException( + $"Cannot populate type '{targetType.Name}' from binary type code {typeCode}", + context.Position, targetType); + } + } catch (AcBinaryDeserializationException) { throw; } catch (Exception ex) { throw new AcBinaryDeserializationException( - $"Failed to deserialize Expression from binary data: {ex.Message}", - context.Position, targetExpressionType, ex); - } - } - - /// - /// Deserialize Expression from binary data. - /// First deserializes as AcExpressionNode, then rebuilds the Expression tree. - /// - private static Expression? DeserializeExpression(ReadOnlySpan data, Type targetExpressionType, AcBinarySerializerOptions options) - { - var context = DeserializationContextPool.Get(options); - context.InitBufferFromSpan(data); - - try - { - context.ReadHeader(); - var node = (AcExpressionNode?)ReadValue(context, typeof(AcExpressionNode), 0); - // Position-based string interning - no validation needed - if (node == null) return null; - - var entityType = AcSerializerCommon.GetExpressionEntityType(targetExpressionType); - return AcExpressionRebuilder.FromNode(node, entityType); - } - catch (AcBinaryDeserializationException) - { - throw; - } - catch (Exception ex) - { - throw new AcBinaryDeserializationException( - $"Failed to deserialize Expression from binary data: {ex.Message}", - context.Position, targetExpressionType, ex); - } - finally - { - DeserializationContextPool.Return(context); + $"Failed to populate object of type '{targetType.Name}': {ex.Message}", + context.Position, targetType, ex); } } @@ -404,54 +372,10 @@ public static partial class AcBinaryDeserializer if (data.Length == 0) return; if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return; - var targetType = target.GetType(); - var context = DeserializationContextPool.Get(options); - context.InitBufferFromSpan(data); - - try - { - context.ReadHeader(); - var typeCode = context.PeekByte(); - - if (typeCode == BinaryTypeCode.Object) - { - context.ReadByte(); // consume Object marker - PopulateObject(context, target, targetType, 0); - } - else if (typeCode == BinaryTypeCode.ObjectWithMetadata) - { - context.ReadByte(); // consume ObjectWithMetadata marker - ReadInlineMetadataForPopulate(context, targetType); - PopulateObject(context, target, targetType, 0); - } - else if (typeCode == BinaryTypeCode.Array && target is IList targetList) - { - context.ReadByte(); // consume Array marker - PopulateList(context, targetList, targetType, 0); - } - else - { - throw new AcBinaryDeserializationException( - $"Cannot populate type '{targetType.Name}' from binary type code {typeCode}", - context.Position, targetType); - } - - // Position-based string interning - no validation needed - } - catch (AcBinaryDeserializationException) - { - throw; - } - catch (Exception ex) - { - throw new AcBinaryDeserializationException( - $"Failed to populate object of type '{targetType.Name}': {ex.Message}", - context.Position, targetType, ex); - } - finally - { - DeserializationContextPool.Return(context); - } + var context = DeserializationContextPool.Get(options); + context.InitFromSpan(data); + try { PopulateCore(context, target); } + finally { DeserializationContextPool.Return(context); } } /// @@ -474,12 +398,28 @@ public static partial class AcBinaryDeserializer if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return; var opts = options ?? AcBinarySerializerOptions.Default; - var targetType = target.GetType(); - var context = DeserializationContextPool.Get(opts); - context.InitBufferFromSpan(data); + var context = DeserializationContextPool.Get(opts); + context.InitFromSpan(data); context.IsMergeMode = true; context.RemoveOrphanedItems = opts.RemoveOrphanedItems; + try + { + PopulateMergeCore(context, target); + } + finally + { + DeserializationContextPool.Return(context); + } + } + + /// + /// Core merge populate: handles IId collection merge for top-level arrays. + /// + private static void PopulateMergeCore(BinaryDeserializationContext context, object target) + where TInput : struct, IBinaryInputBase + { + var targetType = target.GetType(); try { context.ReadHeader(); @@ -499,7 +439,6 @@ public static partial class AcBinaryDeserializer else if (typeCode == BinaryTypeCode.Array && target is IList targetList) { context.ReadByte(); - // For top-level list merge, check if it's an IId collection var elementType = GetCollectionElementType(targetType); if (elementType != null) { @@ -509,12 +448,10 @@ public static partial class AcBinaryDeserializer if (elementMetadata.IsComplexType && elementMetadata.IsIId && elementMetadata.IdGetter != null) { MergeIIdCollectionWithMetadata(context, targetList, elementType, wrapper, 0); - // Position-based string interning - no validation needed return; } } - // Non-IId collection, just populate PopulateList(context, targetList, targetType, 0); } else @@ -523,23 +460,14 @@ public static partial class AcBinaryDeserializer $"Cannot populate type '{targetType.Name}' from binary type code {typeCode}", context.Position, targetType); } - - // Position-based string interning - no validation needed - } - catch (AcBinaryDeserializationException) - { - throw; } + catch (AcBinaryDeserializationException) { throw; } catch (Exception ex) { throw new AcBinaryDeserializationException( $"Failed to populate/merge object of type '{targetType.Name}': {ex.Message}", context.Position, targetType, ex); } - finally - { - DeserializationContextPool.Return(context); - } } #endregion @@ -561,25 +489,20 @@ public static partial class AcBinaryDeserializer if (data.Length == 0 || (data.Length == 1 && data[0] == BinaryTypeCode.Null)) return EmptyDeserializeChain.Instance; - var targetType = typeof(T); - - // Copy data to array for chain storage var dataArray = data.ToArray(); var chainTracker = new AcSerializerCommon.ChainReferenceTracker(); - var context = DeserializationContextPool.Get(options); - context.InitBuffer(dataArray, dataArray.Length); + var context = DeserializationContextPool.Get(options); + context.InitInput(new ArrayBinaryInput(dataArray)); context.ChainTracker = chainTracker; try { - context.ReadHeader(); - var result = ReadValue(context, targetType, 0); - // Position-based string interning - no validation needed - return new BinaryDeserializeChain(dataArray, options, chainTracker, (T?)result); + var result = (T?)DeserializeCore(context, typeof(T)); + return new BinaryDeserializeChain(dataArray, options, chainTracker, result); } finally { - DeserializationContextPool.Return(context); + DeserializationContextPool.Return(context); } } @@ -612,29 +535,11 @@ public static partial class AcBinaryDeserializer { ThrowIfDisposed(); - var targetType = typeof(TResult); - var context = DeserializationContextPool.Get(_options); - context.InitBuffer(_data, _data.Length); + var context = DeserializationContextPool.Get(_options); + context.InitInput(new ArrayBinaryInput(_data)); context.ChainTracker = _chainTracker; - - try - { - context.ReadHeader(); - var result = ReadValue(context, targetType, 0); - // Position-based string interning - no validation needed - return (TResult?)result; - } - catch (AcBinaryDeserializationException) { throw; } - catch (Exception ex) - { - throw new AcBinaryDeserializationException( - $"Failed to deserialize to type '{targetType.Name}' in chain: {ex.Message}", - 0, targetType, ex); - } - finally - { - DeserializationContextPool.Return(context); - } + try { return (TResult?)DeserializeCore(context, typeof(TResult)); } + finally { DeserializationContextPool.Return(context); } } public IDeserializeChain ThenPopulate(object target) @@ -642,53 +547,15 @@ public static partial class AcBinaryDeserializer ArgumentNullException.ThrowIfNull(target); ThrowIfDisposed(); - var targetType = target.GetType(); - var context = DeserializationContextPool.Get(_options); - context.InitBuffer(_data, _data.Length); + var context = DeserializationContextPool.Get(_options); + context.InitInput(new ArrayBinaryInput(_data)); context.ChainTracker = _chainTracker; - try { - context.ReadHeader(); - var typeCode = context.PeekByte(); - - if (typeCode == BinaryTypeCode.Object) - { - context.ReadByte(); - PopulateObject(context, target, targetType, 0); - } - else if (typeCode == BinaryTypeCode.ObjectWithMetadata) - { - context.ReadByte(); - ReadInlineMetadataForPopulate(context, targetType); - PopulateObject(context, target, targetType, 0); - } - else if (typeCode == BinaryTypeCode.Array && target is IList targetList) - { - context.ReadByte(); - PopulateList(context, targetList, targetType, 0); - } - else - { - throw new AcBinaryDeserializationException( - $"Cannot populate type '{targetType.Name}' from binary type code {typeCode}", - context.Position, targetType); - } - - // Position-based string interning - no validation needed + PopulateCore(context, target); return this; } - catch (AcBinaryDeserializationException) { throw; } - catch (Exception ex) - { - throw new AcBinaryDeserializationException( - $"Failed to populate object of type '{targetType.Name}' in chain: {ex.Message}", - 0, targetType, ex); - } - finally - { - DeserializationContextPool.Return(context); - } + finally { DeserializationContextPool.Return(context); } } private void ThrowIfDisposed() @@ -713,7 +580,8 @@ public static partial class AcBinaryDeserializer /// Returns true if handled, false if should fall back to generic path. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryReadAndSetTypedValue(BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo, byte peekCode) + private static bool TryReadAndSetTypedValue(BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo, byte peekCode) + where TInput : struct, IBinaryInputBase { // Only handle if we have a typed setter if (propInfo.AccessorType == PropertyAccessorType.Object) @@ -958,7 +826,8 @@ public static partial class AcBinaryDeserializer /// /// Optimized value reader using FrozenDictionary dispatch table. /// - private static object? ReadValue(BinaryDeserializationContext context, Type targetType, int depth) + private static object? ReadValue(BinaryDeserializationContext context, Type targetType, int depth) + where TInput : struct, IBinaryInputBase { if (context.IsAtEnd) return null; @@ -981,7 +850,7 @@ public static partial class AcBinaryDeserializer return length == 0 ? string.Empty : context.ReadStringUtf8(length); } - var reader = TypeReaders[typeCode]; + var reader = TypeReaderTable.Readers[typeCode]; if (reader != null) { return reader(context, targetType, depth); @@ -996,7 +865,8 @@ public static partial class AcBinaryDeserializer /// Sima string olvas�sa - NEM regisztr�l az intern t�bl�ba. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static string ReadPlainString(BinaryDeserializationContext context) + private static string ReadPlainString(BinaryDeserializationContext context) + where TInput : struct, IBinaryInputBase { var length = (int)context.ReadVarUInt(); if (length == 0) return string.Empty; @@ -1008,7 +878,8 @@ public static partial class AcBinaryDeserializer /// Wire format: [StringInternFirst][VarUInt cacheIndex][VarUInt length][UTF8 bytes] /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static string ReadAndRegisterInternedString(BinaryDeserializationContext context) + private static string ReadAndRegisterInternedString(BinaryDeserializationContext context) + where TInput : struct, IBinaryInputBase { var cacheIndex = (int)context.ReadVarUInt(); var length = (int)context.ReadVarUInt(); @@ -1022,7 +893,7 @@ public static partial class AcBinaryDeserializer ///// Read a string and register it in the intern table for future references. ///// //[MethodImpl(MethodImplOptions.AggressiveInlining)] - //private static string ReadAndInternString(BinaryDeserializationContext context, int streamPosition) + //private static string ReadAndInternString(BinaryDeserializationContext context, int streamPosition) //{ // var length = (int)context.ReadVarUInt(); // if (length == 0) return string.Empty; @@ -1037,7 +908,8 @@ public static partial class AcBinaryDeserializer //} [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object ReadInt32Value(BinaryDeserializationContext context, Type targetType) + private static object ReadInt32Value(BinaryDeserializationContext context, Type targetType) + where TInput : struct, IBinaryInputBase { var value = context.ReadVarInt(); return ConvertToTargetType(value, targetType); @@ -1107,7 +979,8 @@ public static partial class AcBinaryDeserializer } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object ReadEnumValue(BinaryDeserializationContext context, Type targetType) + private static object ReadEnumValue(BinaryDeserializationContext context, Type targetType) + where TInput : struct, IBinaryInputBase { var info = GetConversionInfo(targetType); var nextByte = context.ReadByte(); @@ -1132,7 +1005,8 @@ public static partial class AcBinaryDeserializer } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static byte[] ReadByteArray(BinaryDeserializationContext context) + private static byte[] ReadByteArray(BinaryDeserializationContext context) + where TInput : struct, IBinaryInputBase { var length = (int)context.ReadVarUInt(); if (length == 0) return []; @@ -1165,7 +1039,8 @@ public static partial class AcBinaryDeserializer /// ami ritka eset (2+ referencia), tehát nem lassítja a hot path-ot. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ReadObjectRef(BinaryDeserializationContext context, Type targetType, int depth) + private static object? ReadObjectRef(BinaryDeserializationContext context, Type targetType, int depth) + where TInput : struct, IBinaryInputBase { var cacheIndex = (int)context.ReadVarUInt(); return context.GetInternedObject(cacheIndex); @@ -1175,7 +1050,8 @@ public static partial class AcBinaryDeserializer /// Object olvasása (nem tracked, vagy UseMetadata nélkül). /// Wire format: [Object][props...] /// - private static object? ReadObject(BinaryDeserializationContext context, Type targetType, int depth) + private static object? ReadObject(BinaryDeserializationContext context, Type targetType, int depth) + where TInput : struct, IBinaryInputBase { return ReadObjectCore(context, targetType, depth, cacheIndex: -1); } @@ -1185,7 +1061,8 @@ public static partial class AcBinaryDeserializer /// Wire format: [ObjectRefFirst][VarUInt cacheIndex][props...] /// Az objektumot regisztráljuk a cache-be a megadott index-re. /// - private static object? ReadObjectRefFirst(BinaryDeserializationContext context, Type targetType, int depth) + private static object? ReadObjectRefFirst(BinaryDeserializationContext context, Type targetType, int depth) + where TInput : struct, IBinaryInputBase { var cacheIndex = (int)context.ReadVarUInt(); return ReadObjectCore(context, targetType, depth, cacheIndex: cacheIndex); @@ -1195,7 +1072,8 @@ public static partial class AcBinaryDeserializer /// Object olvasás core implementáció. /// /// -1 = not cached, 0+ = register at this cache index - private static object? ReadObjectCore(BinaryDeserializationContext context, Type targetType, int depth, int cacheIndex) + private static object? ReadObjectCore(BinaryDeserializationContext context, Type targetType, int depth, int cacheIndex) + where TInput : struct, IBinaryInputBase { // Handle dictionary types if (IsDictionaryType(targetType, out var keyType, out var valueType)) @@ -1240,7 +1118,8 @@ public static partial class AcBinaryDeserializer /// Első előfordulás: [ObjectWithMetadata][propNameHash (4b)][propCount (VarUInt)][hash0..N][props...] /// Ismételt: [ObjectWithMetadata][propNameHash (4b)][props...] /// - private static object? ReadObjectWithMetadata(BinaryDeserializationContext context, Type targetType, int depth) + private static object? ReadObjectWithMetadata(BinaryDeserializationContext context, Type targetType, int depth) + where TInput : struct, IBinaryInputBase { return ReadObjectWithMetadataCore(context, targetType, depth, cacheIndex: -1); } @@ -1250,7 +1129,8 @@ public static partial class AcBinaryDeserializer /// Wire format: [ObjectWithMetadataRefFirst][VarUInt cacheIndex][propNameHash (4b)][...][props...] /// Az objektumot regisztráljuk a cache-be a megadott index-re. /// - private static object? ReadObjectWithMetadataRefFirst(BinaryDeserializationContext context, Type targetType, int depth) + private static object? ReadObjectWithMetadataRefFirst(BinaryDeserializationContext context, Type targetType, int depth) + where TInput : struct, IBinaryInputBase { var cacheIndex = (int)context.ReadVarUInt(); return ReadObjectWithMetadataCore(context, targetType, depth, cacheIndex: cacheIndex); @@ -1260,7 +1140,8 @@ public static partial class AcBinaryDeserializer /// ObjectWithMetadata olvasás core implementáció. /// /// -1 = not cached, 0+ = register at this cache index - private static object? ReadObjectWithMetadataCore(BinaryDeserializationContext context, Type targetType, int depth, int cacheIndex) + private static object? ReadObjectWithMetadataCore(BinaryDeserializationContext context, Type targetType, int depth, int cacheIndex) + where TInput : struct, IBinaryInputBase { // Inline metadata: propNameHash mindig jön var propNameHash = context.ReadInt32Raw(); @@ -1324,7 +1205,8 @@ public static partial class AcBinaryDeserializer /// Az ObjectWithMetadata marker már consume-álva van. /// Beolvassa a propNameHash-t + hash-eket (első előfordulásnál), és felépíti a cacheMap-et. /// - private static void ReadInlineMetadataForPopulate(BinaryDeserializationContext context, Type targetType) + private static void ReadInlineMetadataForPopulate(BinaryDeserializationContext context, Type targetType) + where TInput : struct, IBinaryInputBase { var propNameHash = context.ReadInt32Raw(); @@ -1383,7 +1265,8 @@ public static partial class AcBinaryDeserializer #region Array Reading - private static object? ReadArray(BinaryDeserializationContext context, Type targetType, int depth) + private static object? ReadArray(BinaryDeserializationContext context, Type targetType, int depth) + where TInput : struct, IBinaryInputBase { var elementType = GetCollectionElementType(targetType); if (elementType == null) elementType = typeof(object); @@ -1446,7 +1329,8 @@ public static partial class AcBinaryDeserializer /// Optimized primitive array reader using bulk operations. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Array? TryReadPrimitiveArray(BinaryDeserializationContext context, Type elementType, int count) + private static Array? TryReadPrimitiveArray(BinaryDeserializationContext context, Type elementType, int count) + where TInput : struct, IBinaryInputBase { // Int32 array if (ReferenceEquals(elementType, IntType)) @@ -1564,7 +1448,8 @@ public static partial class AcBinaryDeserializer #region Dictionary Reading - private static object? ReadDictionary(BinaryDeserializationContext context, Type targetType, int depth) + private static object? ReadDictionary(BinaryDeserializationContext context, Type targetType, int depth) + where TInput : struct, IBinaryInputBase { if (!IsDictionaryType(targetType, out var keyType, out var valueType)) { @@ -1575,7 +1460,8 @@ public static partial class AcBinaryDeserializer return ReadDictionaryAsObject(context, keyType!, valueType!, depth); } - private static object ReadDictionaryAsObject(BinaryDeserializationContext context, Type keyType, Type valueType, int depth) + private static object ReadDictionaryAsObject(BinaryDeserializationContext context, Type keyType, Type valueType, int depth) + where TInput : struct, IBinaryInputBase { var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType); var count = (int)context.ReadVarUInt(); @@ -1597,7 +1483,8 @@ public static partial class AcBinaryDeserializer #region Skip Value - private static void SkipValue(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) + private static void SkipValue(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) + where TInput : struct, IBinaryInputBase { var typeCode = context.ReadByte(); @@ -1708,7 +1595,8 @@ public static partial class AcBinaryDeserializer /// Sima string kihagy�sa - NEM regisztr�l. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SkipPlainString(BinaryDeserializationContext context) + private static void SkipPlainString(BinaryDeserializationContext context) + where TInput : struct, IBinaryInputBase { var byteLen = (int)context.ReadVarUInt(); if (byteLen > 0) @@ -1722,7 +1610,8 @@ public static partial class AcBinaryDeserializer /// Wire format: [StringInternFirst][VarUInt cacheIndex][VarUInt length][UTF8 bytes] /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SkipAndRegisterInternedString(BinaryDeserializationContext context) + private static void SkipAndRegisterInternedString(BinaryDeserializationContext context) + where TInput : struct, IBinaryInputBase { var cacheIndex = (int)context.ReadVarUInt(); var byteLen = (int)context.ReadVarUInt(); @@ -1735,7 +1624,8 @@ public static partial class AcBinaryDeserializer /// Skip ObjectRefFirst - must read cacheIndex and register placeholder in cache. /// Wire format: [ObjectRefFirst][VarUInt cacheIndex][props...] /// - private static void SkipObjectRefFirst(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) + private static void SkipObjectRefFirst(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) + where TInput : struct, IBinaryInputBase { var cacheIndex = (int)context.ReadVarUInt(); // Register placeholder (stream position as boxed int for potential lazy load) @@ -1749,7 +1639,7 @@ public static partial class AcBinaryDeserializer ///// Deserialization context ///// Position before the type code was read //[MethodImpl(MethodImplOptions.AggressiveInlining)] - //private static void SkipAndInternString(BinaryDeserializationContext context, int streamPosition) + //private static void SkipAndInternString(BinaryDeserializationContext context, int streamPosition) //{ // var byteLen = (int)context.ReadVarUInt(); // if (byteLen == 0) return; @@ -1764,7 +1654,8 @@ public static partial class AcBinaryDeserializer /// /// Object kihagyása metadata nélkül — nem támogatott, mert nem tudjuk a property számot. /// - private static void SkipObject(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) + private static void SkipObject(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) + where TInput : struct, IBinaryInputBase { throw new NotSupportedException( "SkipObject nem támogatott metadata nélkül. " + @@ -1775,7 +1666,8 @@ public static partial class AcBinaryDeserializer /// Skip ObjectWithMetadata/ObjectWithMetadataRefFirst. /// /// -1 = not cached, 0+ = register at this cache index - private static void SkipObjectWithMetadata(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData, int cacheIndex) + private static void SkipObjectWithMetadata(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData, int cacheIndex) + where TInput : struct, IBinaryInputBase { if (cacheIndex >= 0) { @@ -1804,7 +1696,8 @@ public static partial class AcBinaryDeserializer } } - private static void SkipArray(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) + private static void SkipArray(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) + where TInput : struct, IBinaryInputBase { var count = (int)context.ReadVarUInt(); for (int i = 0; i < count; i++) @@ -1813,7 +1706,8 @@ public static partial class AcBinaryDeserializer } } - private static void SkipDictionary(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) + private static void SkipDictionary(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) + where TInput : struct, IBinaryInputBase { var count = (int)context.ReadVarUInt(); for (int i = 0; i < count; i++) @@ -1877,4 +1771,3 @@ public static partial class AcBinaryDeserializer } // Implementation moved to AcBinaryDeserializer.TypeConversionInfo.cs - diff --git a/AyCode.Core/Serializers/Binaries/ArrayBinaryInput.cs b/AyCode.Core/Serializers/Binaries/ArrayBinaryInput.cs new file mode 100644 index 0000000..c7e2014 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/ArrayBinaryInput.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.CompilerServices; + +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Binary input backed by a byte[] — the fastest input path. +/// Zero-copy: context references the byte[] directly, no linearization needed. +/// TryAdvanceSegment always returns false (single contiguous buffer). +/// +/// Mirrors ArrayBinaryOutput pattern from the serializer side. +/// +public struct ArrayBinaryInput : IBinaryInputBase +{ + private readonly byte[] _data; + private readonly int _length; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ArrayBinaryInput(byte[] data, int length) + { + _data = data; + _length = length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ArrayBinaryInput(byte[] data) : this(data, data.Length) { } + + /// + /// Provides the buffer directly — zero copy. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Initialize(out byte[] buffer, out int position, out int bufferLength) + { + buffer = _data; + position = 0; + bufferLength = _length; + } + + /// + /// Single-buffer input — no more segments available. + /// The JIT sees this always returns false and eliminates the call at the callsite. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public bool TryAdvanceSegment(ref byte[] buffer, ref int position, ref int bufferLength, int needed) + => false; +} diff --git a/AyCode.Core/Serializers/Binaries/IBinaryInputBase.cs b/AyCode.Core/Serializers/Binaries/IBinaryInputBase.cs new file mode 100644 index 0000000..8220343 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/IBinaryInputBase.cs @@ -0,0 +1,34 @@ +using System.Runtime.CompilerServices; + +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Abstract base interface for binary input implementations. +/// Provides only the buffer management strategy (initialize/advance) — all read methods live in +/// BinaryDeserializationContext which owns the _buffer/_position state for zero virtual dispatch. +/// +/// Mirrors IBinaryOutputBase pattern from the serializer side. +/// +/// Derived structs implement: +/// - Initialize: provide the initial buffer, position, and length +/// - TryAdvanceSegment: handle buffer exhaustion (return false for single-buffer, advance for multi-segment) +/// +public interface IBinaryInputBase +{ + /// + /// Provides the initial buffer, starting position, and buffer length. + /// Called once before deserialization begins. + /// For ArrayBinaryInput: sets buffer = byte[], position = 0, bufferLength = data.Length. + /// For SequenceBinaryInput: sets buffer to first segment's array, position/length to segment bounds. + /// + void Initialize(out byte[] buffer, out int position, out int bufferLength); + + /// + /// Called when the context's buffer cannot satisfy 'needed' more bytes. Cold path only. + /// Returns true if more data is available (segment advanced), false if end of input. + /// For ArrayBinaryInput: always returns false (single contiguous buffer). + /// For SequenceBinaryInput: advances to next segment, handles cross-boundary reads. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + bool TryAdvanceSegment(ref byte[] buffer, ref int position, ref int bufferLength, int needed); +} diff --git a/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs b/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs new file mode 100644 index 0000000..d7d2990 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs @@ -0,0 +1,143 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Binary input that reads directly from a ReadOnlySequence (e.g. SignalR pipe, network stream). +/// Processes segments one-by-one without linearizing the entire payload. +/// +/// For values that span segment boundaries (e.g. a 4-byte int split across 2 segments), +/// copies the overlapping bytes into a small scratch buffer and reads from there. +/// +/// Mirrors BufferWriterBinaryOutput pattern from the serializer side. +/// +public struct SequenceBinaryInput : IBinaryInputBase +{ + // Pre-extracted segments from the ReadOnlySequence. + // Using ArraySegment avoids holding onto ReadOnlyMemory (which can't get byte[] without TryGetArray). + private readonly ArraySegment[] _segments; + private int _currentSegment; + + // Scratch buffer for cross-boundary reads (max 16 bytes for Guid/Decimal) + private byte[]? _scratchBuffer; + private int _scratchLength; + + /// + /// Creates a SequenceBinaryInput from a multi-segment ReadOnlySequence. + /// Pre-extracts all segments as ArraySegment for fast iteration. + /// + public SequenceBinaryInput(ReadOnlySequence sequence) + { + var segmentCount = 0; + foreach (var _ in sequence) + segmentCount++; + + _segments = new ArraySegment[segmentCount]; + var i = 0; + foreach (var memory in sequence) + { + if (MemoryMarshal.TryGetArray(memory, out var segment)) + { + _segments[i++] = segment; + } + else + { + // Non-array-backed memory: copy to a temp array + var temp = new byte[memory.Length]; + memory.Span.CopyTo(temp); + _segments[i++] = new ArraySegment(temp, 0, temp.Length); + } + } + + _currentSegment = 0; + _scratchBuffer = null; + _scratchLength = 0; + } + + /// + /// Provides the first segment's buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Initialize(out byte[] buffer, out int position, out int bufferLength) + { + if (_segments.Length == 0) + throw new AcBinaryDeserializationException("Empty sequence — no segments to read."); + + var seg = _segments[0]; + buffer = seg.Array!; + position = seg.Offset; + bufferLength = seg.Offset + seg.Count; + } + + /// + /// Advances to the next segment when the current one is exhausted. + /// Handles cross-boundary reads by copying overlapping bytes to a scratch buffer. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + public bool TryAdvanceSegment(ref byte[] buffer, ref int position, ref int bufferLength, int needed) + { + // Calculate remaining bytes in current segment + var remaining = bufferLength - position; + + if (remaining > 0 && remaining < needed) + { + // Cross-boundary read: value spans two segments + return TryReadCrossBoundary(ref buffer, ref position, ref bufferLength, needed, remaining); + } + + // Current segment fully consumed — advance to next + _currentSegment++; + if (_currentSegment >= _segments.Length) + return false; + + var seg = _segments[_currentSegment]; + buffer = seg.Array!; + position = seg.Offset; + bufferLength = seg.Offset + seg.Count; + return true; + } + + /// + /// Handles a read that spans two segments by copying the overlapping bytes + /// into a scratch buffer, then setting up the context to read from it. + /// After this read, the next EnsureAvailable will advance to the remainder of the new segment. + /// + private bool TryReadCrossBoundary(ref byte[] buffer, ref int position, ref int bufferLength, int needed, int remaining) + { + _currentSegment++; + if (_currentSegment >= _segments.Length) + return false; + + // Ensure scratch buffer is large enough (max 16 bytes for Guid/Decimal) + _scratchBuffer ??= new byte[32]; + + // Copy tail of current segment + Buffer.BlockCopy(buffer, position, _scratchBuffer, 0, remaining); + + // Copy head of next segment + var nextSeg = _segments[_currentSegment]; + var fromNext = Math.Min(needed - remaining, nextSeg.Count); + Buffer.BlockCopy(nextSeg.Array!, nextSeg.Offset, _scratchBuffer, remaining, fromNext); + + _scratchLength = remaining + fromNext; + + // Set up context to read from scratch buffer + buffer = _scratchBuffer; + position = 0; + bufferLength = _scratchLength; + + // After the read completes, the position will be at 'needed' in scratch. + // The next EnsureAvailable will fail (scratch is consumed) and call TryAdvanceSegment again, + // which will set up the remainder of the current segment. + // We need to adjust the current segment's offset to skip the bytes we already copied. + _segments[_currentSegment] = new ArraySegment( + nextSeg.Array!, + nextSeg.Offset + fromNext, + nextSeg.Count - fromNext); + + return true; + } +}