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; + } +}