2213 lines
86 KiB
C#
2213 lines
86 KiB
C#
using System;
|
||
using System.Buffers;
|
||
using System.Collections;
|
||
using System.Collections.Concurrent;
|
||
using System.Collections.Frozen;
|
||
using System.Diagnostics;
|
||
using System.Linq.Expressions;
|
||
using System.Reflection;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using AyCode.Core.Helpers;
|
||
using AyCode.Core.Serializers.Expressions;
|
||
using static AyCode.Core.Helpers.JsonUtilities;
|
||
|
||
namespace AyCode.Core.Serializers.Binaries;
|
||
|
||
/// <summary>
|
||
/// High-performance binary deserializer matching AcBinarySerializer.
|
||
/// Features:
|
||
/// - VarInt decoding for compact integers
|
||
/// - String intern table lookup
|
||
/// - Property name table for fast property resolution
|
||
/// - Reference resolution for circular/shared references
|
||
/// - Populate/Merge mode support
|
||
/// - Optimized with FrozenDictionary for type dispatch
|
||
/// - Zero-allocation hot paths using Span and MemoryMarshal
|
||
/// </summary>
|
||
public static partial class AcBinaryDeserializer
|
||
{
|
||
/// <summary>
|
||
/// Diagnostic logger for deserializer-level debugging (DEBUG builds only).
|
||
/// Set to non-null to log SequenceBinaryInput vs ArrayBinaryInput verification results.
|
||
/// </summary>
|
||
public static Action<string>? DiagnosticLogger { get; set; }
|
||
|
||
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
|
||
|
||
/// <summary>
|
||
/// Thread-safe registry for generated readers. Looked up in ReadObjectCore to bypass runtime path.
|
||
/// </summary>
|
||
internal static class GeneratedReaderRegistry
|
||
{
|
||
private static readonly ConcurrentDictionary<Type, IGeneratedBinaryReader> Readers = new();
|
||
|
||
internal static void Register(Type type, IGeneratedBinaryReader reader) => Readers[type] = reader;
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
internal static IGeneratedBinaryReader? TryGet(Type type) =>
|
||
Readers.TryGetValue(type, out var reader) ? reader : null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Registers a source-generated binary reader for the specified type.
|
||
/// Once registered, ReadObjectCore bypasses the runtime wrapper/property loop
|
||
/// and calls the generated reader directly.
|
||
/// </summary>
|
||
internal static void RegisterGeneratedReader(Type type, IGeneratedBinaryReader reader)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(type);
|
||
ArgumentNullException.ThrowIfNull(reader);
|
||
GeneratedReaderRegistry.Register(type, reader);
|
||
}
|
||
|
||
/// <summary>
|
||
/// ThreadLocal cache for type conversion info.
|
||
/// </summary>
|
||
[ThreadStatic]
|
||
private static Dictionary<Type, TypeConversionInfo>? t_typeConversionLocalCache;
|
||
|
||
// Type dispatch table for fast ReadValue — generic per TInput, JIT specializes each
|
||
private delegate object? TypeReader<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase;
|
||
|
||
private static class TypeReaderTable<TInput> where TInput : struct, IBinaryInputBase
|
||
{
|
||
public static readonly TypeReader<TInput>?[] Readers = InitReaders();
|
||
|
||
private static TypeReader<TInput>?[] InitReaders()
|
||
{
|
||
var readers = new TypeReader<TInput>?[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.ObjectWithTypeName] = ReadObjectWithTypeName;
|
||
readers[BinaryTypeCode.ObjectWithTypeNameRefFirst] = ReadObjectWithTypeNameRefFirst;
|
||
readers[BinaryTypeCode.ObjectWithTypeIndex] = ReadObjectWithTypeIndex;
|
||
readers[BinaryTypeCode.ObjectWithTypeIndexRefFirst] = ReadObjectWithTypeIndexRefFirst;
|
||
readers[BinaryTypeCode.Array] = ReadArray;
|
||
readers[BinaryTypeCode.Dictionary] = ReadDictionary;
|
||
readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx);
|
||
|
||
// Register FixStr readers
|
||
for (byte code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++)
|
||
{
|
||
var length = BinaryTypeCode.DecodeFixStrLength(code);
|
||
readers[code] = CreateFixStrReader<TInput>(length);
|
||
}
|
||
|
||
// Register FixObj slot readers (0..SlotCount-1)
|
||
for (int slot = 0; slot < BinaryTypeCode.SlotCount; slot++)
|
||
{
|
||
readers[slot] = CreateFixObjReader<TInput>(slot);
|
||
}
|
||
|
||
return readers;
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Creates a reader for FixStr with the given length.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static TypeReader<TInput> CreateFixStrReader<TInput>(int length)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
if (length == 0)
|
||
return static (_, _, _) => string.Empty;
|
||
|
||
return (ctx, _, _) => ctx.ReadStringUtf8(length);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a reader for FixObj slot (0..SlotCount-1).
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static TypeReader<TInput> CreateFixObjReader<TInput>(int slot)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
return (ctx, targetType, depth) => ReadObjectFromSlot(ctx, slot, targetType, depth);
|
||
}
|
||
|
||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
|
||
|
||
#region Public API
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to object of type T.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public static T? Deserialize<T>(byte[] data) => Deserialize<T>(data, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to object of type T with options.
|
||
/// Zero-copy: ArrayBinaryInput references the byte[] directly.
|
||
/// </summary>
|
||
public static T? Deserialize<T>(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<ArrayBinaryInput>.Get(options);
|
||
context.InitInput(new ArrayBinaryInput(data));
|
||
try { return (T?)DeserializeCore(context, targetType); }
|
||
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to object of type T from a sub-range of a byte[].
|
||
/// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public static T? Deserialize<T>(byte[] data, int offset, int length)
|
||
=> Deserialize<T>(data, offset, length, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to object of type T from a sub-range with options.
|
||
/// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
|
||
/// </summary>
|
||
public static T? Deserialize<T>(byte[] data, int offset, int length, AcBinarySerializerOptions options)
|
||
{
|
||
if (length == 0) return default;
|
||
if (length == 1 && data[offset] == BinaryTypeCode.Null) return default;
|
||
|
||
var targetType = typeof(T);
|
||
if (AcSerializerCommon.IsExpressionType(targetType))
|
||
return (T?)(object?)DeserializeExpression(data, offset, length, targetType, options);
|
||
|
||
var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
|
||
context.InitInput(new ArrayBinaryInput(data, offset, length));
|
||
try { return (T?)DeserializeCore(context, targetType); }
|
||
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to specified type.
|
||
/// </summary>
|
||
public static object? Deserialize(byte[] data, Type targetType)
|
||
=> Deserialize(data, 0, data.Length, targetType, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to specified type with options.
|
||
/// </summary>
|
||
public static object? Deserialize(byte[] data, Type targetType, AcBinarySerializerOptions options)
|
||
=> Deserialize(data, 0, data.Length, targetType, options);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to specified type from a sub-range.
|
||
/// </summary>
|
||
public static object? Deserialize(byte[] data, int offset, int length, Type targetType)
|
||
=> Deserialize(data, offset, length, targetType, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to specified type from a sub-range with options.
|
||
/// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
|
||
/// </summary>
|
||
public static object? Deserialize(byte[] data, int offset, int length, Type targetType, AcBinarySerializerOptions options)
|
||
{
|
||
if (length == 0) return null;
|
||
if (length == 1 && data[offset] == BinaryTypeCode.Null) return null;
|
||
|
||
if (AcSerializerCommon.IsExpressionType(targetType))
|
||
return DeserializeExpression(data, offset, length, targetType, options);
|
||
|
||
var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
|
||
context.InitInput(new ArrayBinaryInput(data, offset, length));
|
||
try { return DeserializeCore(context, targetType); }
|
||
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data from a ReadOnlySequence (e.g., from SignalR/Pipes).
|
||
/// Single-segment: zero-copy via FirstSpan. Multi-segment: linearize into pooled buffer.
|
||
/// </summary>
|
||
public static T? Deserialize<T>(ReadOnlySequence<byte> data)
|
||
=> Deserialize<T>(data, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data from a ReadOnlySequence with options.
|
||
/// Single-segment: zero-copy via FirstSpan. Multi-segment: uses SequenceBinaryInput for true streaming.
|
||
/// </summary>
|
||
public static T? Deserialize<T>(ReadOnlySequence<byte> data, AcBinarySerializerOptions options)
|
||
{
|
||
if (data.Length == 0) return default;
|
||
|
||
if (data.IsSingleSegment && MemoryMarshal.TryGetArray(data.First, out var seg))
|
||
return Deserialize<T>(seg.Array!, seg.Offset, seg.Count, options);
|
||
|
||
VerifyAgainstLinearized(data, typeof(T), options);
|
||
return DeserializeSequence<T, SequenceBinaryInput>(new SequenceBinaryInput(data), typeof(T), options);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data from a ReadOnlySequence to specified type.
|
||
/// </summary>
|
||
public static object? Deserialize(ReadOnlySequence<byte> data, Type targetType)
|
||
=> Deserialize(data, targetType, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data from a ReadOnlySequence to specified type with options.
|
||
/// </summary>
|
||
public static object? Deserialize(ReadOnlySequence<byte> data, Type targetType, AcBinarySerializerOptions options)
|
||
{
|
||
if (data.Length == 0) return null;
|
||
|
||
if (data.IsSingleSegment && MemoryMarshal.TryGetArray(data.First, out var seg2))
|
||
return Deserialize(seg2.Array!, seg2.Offset, seg2.Count, targetType, options);
|
||
|
||
VerifyAgainstLinearized(data, targetType, options);
|
||
return DeserializeSequence<SequenceBinaryInput>(new SequenceBinaryInput(data), targetType, options);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deserialize from PipeReader with segment streaming (read per chunk via PipeReaderBinaryInput).
|
||
/// Data is consumed as it arrives from the network, enabling pipeline parallelism.
|
||
/// </summary>
|
||
public static T? Deserialize<T>(System.IO.Pipelines.PipeReader pipeReader, AcBinarySerializerOptions options)
|
||
=> DeserializeSequence<T, PipeReaderBinaryInput>(new PipeReaderBinaryInput(pipeReader), typeof(T), options);
|
||
|
||
/// <summary>
|
||
/// Deserialize from PipeReader to specified type with segment streaming.
|
||
/// </summary>
|
||
public static object? Deserialize(System.IO.Pipelines.PipeReader pipeReader, Type targetType, AcBinarySerializerOptions options)
|
||
=> DeserializeSequence<PipeReaderBinaryInput>(new PipeReaderBinaryInput(pipeReader), targetType, options);
|
||
|
||
/// <summary>
|
||
/// Internal: Deserialize with any TInput (multi-segment or other future input types).
|
||
/// </summary>
|
||
private static T? DeserializeSequence<T, TInput>(TInput input, Type targetType, AcBinarySerializerOptions options)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
if (AcSerializerCommon.IsExpressionType(targetType))
|
||
{
|
||
var (buf, off, len) = LinearizeSequence(input);
|
||
return Deserialize<T>(buf, off, len, options);
|
||
}
|
||
|
||
var context = DeserializationContextPool<TInput>.Get(options);
|
||
context.InitInput(input);
|
||
try { return (T?)DeserializeCore(context, targetType); }
|
||
finally
|
||
{
|
||
context.Input.Release();
|
||
DeserializationContextPool<TInput>.Return(context);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Internal: Deserialize with any TInput (multi-segment, non-generic result).
|
||
/// </summary>
|
||
private static object? DeserializeSequence<TInput>(TInput input, Type targetType, AcBinarySerializerOptions options)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
if (AcSerializerCommon.IsExpressionType(targetType))
|
||
{
|
||
var (buf, off, len) = LinearizeSequence(input);
|
||
return Deserialize(buf, off, len, targetType, options);
|
||
}
|
||
|
||
var context = DeserializationContextPool<TInput>.Get(options);
|
||
context.InitInput(input);
|
||
try { return DeserializeCore(context, targetType); }
|
||
finally
|
||
{
|
||
context.Input.Release();
|
||
DeserializationContextPool<TInput>.Return(context);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Fallback: linearize a TInput into a contiguous byte[] range (for Expression deserialization).
|
||
/// </summary>
|
||
private static (byte[] buffer, int offset, int length) LinearizeSequence<TInput>(TInput input)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
input.Initialize(out var buffer, out var position, out var bufferLength);
|
||
return (buffer, position, bufferLength - position);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Core deserialization: ReadHeader + ReadValue with unified error handling.
|
||
/// All public Deserialize overloads delegate here after pool/init setup.
|
||
/// </summary>
|
||
private static object? DeserializeCore<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
return 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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// DEBUG-only verification: linearizes multi-segment data to byte[] and deserializes
|
||
/// with ArrayBinaryInput to determine if drift is caused by SequenceBinaryInput or by
|
||
/// the serialized data itself (generated reader / serializer bug).
|
||
///
|
||
/// Result: VERIFY_OK → SequenceBinaryInput is the culprit.
|
||
/// VERIFY_FAIL → bug is in serializer or generated reader (data itself is bad).
|
||
/// </summary>
|
||
[Conditional("DEBUG")]
|
||
private static void VerifyAgainstLinearized(ReadOnlySequence<byte> data, Type targetType, AcBinarySerializerOptions options)
|
||
{
|
||
if (DiagnosticLogger == null) return;
|
||
|
||
var bytes = data.ToArray();
|
||
var segmentCount = 0;
|
||
foreach (var _ in data) segmentCount++;
|
||
|
||
try
|
||
{
|
||
var result = Deserialize(bytes, targetType, options);
|
||
DiagnosticLogger($"[VERIFY_OK] ArrayBinaryInput succeeded for {targetType.Name}, " +
|
||
$"{bytes.Length} bytes, {segmentCount} segments → SequenceBinaryInput is suspect");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DiagnosticLogger($"[VERIFY_FAIL] ArrayBinaryInput ALSO FAILED for {targetType.Name}, " +
|
||
$"{bytes.Length} bytes, {segmentCount} segments: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deserialize Expression from binary data.
|
||
/// </summary>
|
||
private static Expression? DeserializeExpression(byte[] data, Type targetExpressionType, AcBinarySerializerOptions options)
|
||
=> DeserializeExpression(data, 0, data.Length, targetExpressionType, options);
|
||
|
||
/// <summary>
|
||
/// Deserialize Expression from binary data sub-range.
|
||
/// </summary>
|
||
private static Expression? DeserializeExpression(byte[] data, int offset, int length, Type targetExpressionType, AcBinarySerializerOptions options)
|
||
{
|
||
var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
|
||
context.InitInput(new ArrayBinaryInput(data, offset, length));
|
||
try
|
||
{
|
||
var node = (AcExpressionNode?)DeserializeCore(context, typeof(AcExpressionNode));
|
||
if (node == null) return null;
|
||
|
||
var entityType = AcSerializerCommon.GetExpressionEntityType(targetExpressionType);
|
||
return AcExpressionRebuilder.FromNode(node, entityType);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool<ArrayBinaryInput>.Return(context);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Core populate: ReadHeader + dispatch by typeCode. Used by Populate, PopulateMerge, ThenPopulate.
|
||
/// </summary>
|
||
private static void PopulateCore<TInput>(BinaryDeserializationContext<TInput> context, object target)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var targetType = target.GetType();
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var typeCode = context.PeekByte();
|
||
|
||
if (typeCode < BinaryTypeCode.SlotCount)
|
||
{
|
||
// FixObj slot: marker byte is the slot index
|
||
context.ReadByte();
|
||
context.GetWrapper(targetType, typeCode);
|
||
if (typeCode >= context._nextRuntimeSlot)
|
||
context._nextRuntimeSlot = typeCode + 1;
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.Object)
|
||
{
|
||
context.ReadByte();
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.ObjectRefFirst)
|
||
{
|
||
// ObjectRefFirst: [marker][VarUInt cacheIndex][props...]
|
||
// Read marker + cacheIndex, register target in intern cache, then populate
|
||
context.ReadByte();
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
context.RegisterInternedValueAt(cacheIndex, target);
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.ObjectWithMetadata)
|
||
{
|
||
context.ReadByte();
|
||
ReadInlineMetadataForPopulate(context, targetType);
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.ObjectWithMetadataRefFirst)
|
||
{
|
||
// ObjectWithMetadataRefFirst: [marker][VarUInt cacheIndex][metadata...][props...]
|
||
context.ReadByte();
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
context.RegisterInternedValueAt(cacheIndex, target);
|
||
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 populate object of type '{targetType.Name}': {ex.Message}",
|
||
context.Position, targetType, ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Populate existing object from binary data.
|
||
/// </summary>
|
||
public static void Populate<T>(byte[] data, T target) where T : class
|
||
{
|
||
ArgumentNullException.ThrowIfNull(target);
|
||
if (data.Length == 0) return;
|
||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return;
|
||
|
||
var context = DeserializationContextPool<ArrayBinaryInput>.Get(AcBinarySerializerOptions.Default);
|
||
context.InitInput(new ArrayBinaryInput(data));
|
||
try { PopulateCore(context, target); }
|
||
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Populate existing object from binary data with options.
|
||
/// </summary>
|
||
public static void Populate<T>(byte[] data, T target, AcBinarySerializerOptions options) where T : class
|
||
=> Populate(data, 0, data.Length, target, options);
|
||
|
||
/// <summary>
|
||
/// Populate existing object from binary data sub-range.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public static void Populate<T>(byte[] data, int offset, int length, T target) where T : class
|
||
=> Populate(data, offset, length, target, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Populate existing object from binary data sub-range with options.
|
||
/// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
|
||
/// </summary>
|
||
public static void Populate<T>(byte[] data, int offset, int length, T target, AcBinarySerializerOptions options) where T : class
|
||
{
|
||
ArgumentNullException.ThrowIfNull(target);
|
||
if (length == 0) return;
|
||
if (length == 1 && data[offset] == BinaryTypeCode.Null) return;
|
||
|
||
var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
|
||
context.InitInput(new ArrayBinaryInput(data, offset, length));
|
||
try { PopulateCore(context, target); }
|
||
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Populate with merge semantics for IId collections from byte[] (zero-copy).
|
||
/// </summary>
|
||
public static void PopulateMerge<T>(byte[] data, T target) where T : class
|
||
=> PopulateMerge(data, 0, data.Length, target, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Populate with merge semantics for IId collections from byte[] with options (zero-copy).
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public static void PopulateMerge<T>(byte[] data, T target, AcBinarySerializerOptions options) where T : class
|
||
=> PopulateMerge(data, 0, data.Length, target, options);
|
||
|
||
/// <summary>
|
||
/// Populate with merge semantics for IId collections from a sub-range of byte[].
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public static void PopulateMerge<T>(byte[] data, int offset, int length, T target) where T : class
|
||
=> PopulateMerge(data, offset, length, target, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Populate with merge semantics for IId collections from a sub-range with options.
|
||
/// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
|
||
/// </summary>
|
||
public static void PopulateMerge<T>(byte[] data, int offset, int length, T target, AcBinarySerializerOptions? options) where T : class
|
||
{
|
||
ArgumentNullException.ThrowIfNull(target);
|
||
if (length == 0) return;
|
||
if (length == 1 && data[offset] == BinaryTypeCode.Null) return;
|
||
|
||
var opts = options ?? AcBinarySerializerOptions.Default;
|
||
var context = DeserializationContextPool<ArrayBinaryInput>.Get(opts);
|
||
context.InitInput(new ArrayBinaryInput(data, offset, length));
|
||
context.IsMergeMode = true;
|
||
context.RemoveOrphanedItems = opts.RemoveOrphanedItems;
|
||
|
||
try
|
||
{
|
||
PopulateMergeCore(context, target);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool<ArrayBinaryInput>.Return(context);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Core merge populate: handles IId collection merge for top-level arrays.
|
||
/// </summary>
|
||
private static void PopulateMergeCore<TInput>(BinaryDeserializationContext<TInput> context, object target)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var targetType = target.GetType();
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var typeCode = context.PeekByte();
|
||
|
||
if (typeCode < BinaryTypeCode.SlotCount)
|
||
{
|
||
// FixObj slot: marker byte is the slot index
|
||
context.ReadByte();
|
||
context.GetWrapper(targetType, typeCode);
|
||
if (typeCode >= context._nextRuntimeSlot)
|
||
context._nextRuntimeSlot = typeCode + 1;
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.Object)
|
||
{
|
||
context.ReadByte();
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.ObjectRefFirst)
|
||
{
|
||
context.ReadByte();
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
context.RegisterInternedValueAt(cacheIndex, target);
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.ObjectWithMetadata)
|
||
{
|
||
context.ReadByte();
|
||
ReadInlineMetadataForPopulate(context, targetType);
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.ObjectWithMetadataRefFirst)
|
||
{
|
||
context.ReadByte();
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
context.RegisterInternedValueAt(cacheIndex, target);
|
||
ReadInlineMetadataForPopulate(context, targetType);
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
|
||
{
|
||
context.ReadByte();
|
||
var elementType = GetCollectionElementType(targetType);
|
||
if (elementType != null)
|
||
{
|
||
var wrapper = context.GetWrapper(elementType);
|
||
var elementMetadata = wrapper.Metadata;
|
||
|
||
if (elementMetadata.IsComplexType && elementMetadata.IsIId && elementMetadata.IdGetter != null)
|
||
{
|
||
MergeIIdCollectionWithMetadata(context, targetList, elementType, wrapper, 0);
|
||
return;
|
||
}
|
||
}
|
||
|
||
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 populate/merge object of type '{targetType.Name}': {ex.Message}",
|
||
context.Position, targetType, ex);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Chain API
|
||
|
||
/// <summary>
|
||
/// Create a deserialize chain that parses binary data once and allows multiple deserializations.
|
||
/// Maintains reference identity for IId objects across chain operations.
|
||
/// </summary>
|
||
public static IDeserializeChain<T> CreateDeserializeChain<T>(byte[] data)
|
||
=> CreateDeserializeChain<T>(data, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Create a deserialize chain with options.
|
||
/// </summary>
|
||
public static IDeserializeChain<T> CreateDeserializeChain<T>(byte[] data, AcBinarySerializerOptions options)
|
||
{
|
||
if (data.Length == 0 || (data.Length == 1 && data[0] == BinaryTypeCode.Null))
|
||
return EmptyDeserializeChain<T>.Instance;
|
||
|
||
var chainTracker = new AcSerializerCommon.ChainReferenceTracker();
|
||
var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
|
||
context.InitInput(new ArrayBinaryInput(data));
|
||
context.ChainTracker = chainTracker;
|
||
|
||
try
|
||
{
|
||
var result = (T?)DeserializeCore(context, typeof(T));
|
||
return new BinaryDeserializeChain<T>(data, options, chainTracker, result);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool<ArrayBinaryInput>.Return(context);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Chain Implementations
|
||
|
||
/// <summary>
|
||
/// Binary implementation of deserialize chain.
|
||
/// Maintains reference identity for IId objects across chain operations.
|
||
/// </summary>
|
||
private sealed class BinaryDeserializeChain<T> : IDeserializeChain<T>
|
||
{
|
||
private readonly byte[] _data;
|
||
private readonly AcBinarySerializerOptions _options;
|
||
private readonly AcSerializerCommon.ChainReferenceTracker _chainTracker;
|
||
private bool _isDisposed;
|
||
|
||
public T? Value { get; }
|
||
|
||
public BinaryDeserializeChain(byte[] data, AcBinarySerializerOptions options, AcSerializerCommon.ChainReferenceTracker chainTracker, T? value)
|
||
{
|
||
_data = data;
|
||
_options = options;
|
||
_chainTracker = chainTracker;
|
||
Value = value;
|
||
}
|
||
|
||
public TResult? ThenDeserialize<TResult>()
|
||
{
|
||
ThrowIfDisposed();
|
||
|
||
var context = DeserializationContextPool<ArrayBinaryInput>.Get(_options);
|
||
context.InitInput(new ArrayBinaryInput(_data));
|
||
context.ChainTracker = _chainTracker;
|
||
try { return (TResult?)DeserializeCore(context, typeof(TResult)); }
|
||
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
|
||
}
|
||
|
||
public IDeserializeChain<T> ThenPopulate(object target)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(target);
|
||
ThrowIfDisposed();
|
||
|
||
var context = DeserializationContextPool<ArrayBinaryInput>.Get(_options);
|
||
context.InitInput(new ArrayBinaryInput(_data));
|
||
context.ChainTracker = _chainTracker;
|
||
try
|
||
{
|
||
PopulateCore(context, target);
|
||
return this;
|
||
}
|
||
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
|
||
}
|
||
|
||
private void ThrowIfDisposed()
|
||
{
|
||
ObjectDisposedException.ThrowIf(_isDisposed, this);
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
if (_isDisposed) return;
|
||
_isDisposed = true;
|
||
_chainTracker.Clear();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Value Reading
|
||
|
||
/// <summary>
|
||
/// Tries to read and set a primitive value directly using typed setters to avoid boxing.
|
||
/// The type code marker byte is already consumed by the caller.
|
||
/// Returns true if handled, false if should fall back to generic path.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static bool TryReadAndSetTypedValue<TInput>(BinaryDeserializationContext<TInput> context, object target, BinaryPropertySetterBase propInfo, byte typeCode)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
// Only handle if we have a typed setter
|
||
if (propInfo.AccessorType == PropertyAccessorType.Object)
|
||
return false;
|
||
|
||
// Handle based on property setter type and incoming data type.
|
||
// The marker byte (typeCode) is already consumed — no ReadByte() needed.
|
||
switch (propInfo.AccessorType)
|
||
{
|
||
case PropertyAccessorType.Int32:
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
{
|
||
propInfo.SetInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode));
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.Int32)
|
||
{
|
||
propInfo.SetInt32(target, context.ReadVarInt());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Int64:
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
{
|
||
propInfo.SetInt64(target, BinaryTypeCode.DecodeTinyInt(typeCode));
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.Int32)
|
||
{
|
||
propInfo.SetInt64(target, context.ReadVarInt());
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.Int64)
|
||
{
|
||
propInfo.SetInt64(target, context.ReadVarLong());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Boolean:
|
||
if (typeCode == BinaryTypeCode.True)
|
||
{
|
||
propInfo.SetBoolean(target, true);
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.False)
|
||
{
|
||
propInfo.SetBoolean(target, false);
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Double:
|
||
if (typeCode == BinaryTypeCode.Float64)
|
||
{
|
||
propInfo.SetDouble(target, context.ReadDoubleUnsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Single:
|
||
if (typeCode == BinaryTypeCode.Float32)
|
||
{
|
||
propInfo.SetSingle(target, context.ReadSingleUnsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Decimal:
|
||
if (typeCode == BinaryTypeCode.Decimal)
|
||
{
|
||
propInfo.SetDecimal(target, context.ReadDecimalUnsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.DateTime:
|
||
if (typeCode == BinaryTypeCode.DateTime)
|
||
{
|
||
propInfo.SetDateTime(target, context.ReadDateTimeUnsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Guid:
|
||
if (typeCode == BinaryTypeCode.Guid)
|
||
{
|
||
propInfo.SetGuid(target, context.ReadGuidUnsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Byte:
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
{
|
||
propInfo.SetByte(target, (byte)BinaryTypeCode.DecodeTinyInt(typeCode));
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.UInt8)
|
||
{
|
||
propInfo.SetByte(target, context.ReadByte());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Int16:
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
{
|
||
propInfo.SetInt16(target, (short)BinaryTypeCode.DecodeTinyInt(typeCode));
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.Int16)
|
||
{
|
||
propInfo.SetInt16(target, context.ReadInt16Unsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.UInt16:
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
{
|
||
propInfo.SetUInt16(target, (ushort)BinaryTypeCode.DecodeTinyInt(typeCode));
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.UInt16)
|
||
{
|
||
propInfo.SetUInt16(target, context.ReadUInt16Unsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.UInt32:
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
{
|
||
propInfo.SetUInt32(target, (uint)BinaryTypeCode.DecodeTinyInt(typeCode));
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.UInt32)
|
||
{
|
||
propInfo.SetUInt32(target, context.ReadVarUInt());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.UInt64:
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
{
|
||
propInfo.SetUInt64(target, (ulong)BinaryTypeCode.DecodeTinyInt(typeCode));
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.UInt64)
|
||
{
|
||
propInfo.SetUInt64(target, context.ReadVarULong());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Enum:
|
||
if (typeCode == BinaryTypeCode.Enum)
|
||
{
|
||
var enumByte = context.ReadByte();
|
||
int enumValue;
|
||
if (BinaryTypeCode.IsTinyInt(enumByte))
|
||
enumValue = BinaryTypeCode.DecodeTinyInt(enumByte);
|
||
else if (enumByte == BinaryTypeCode.Int32)
|
||
enumValue = context.ReadVarInt();
|
||
else
|
||
return false;
|
||
propInfo.SetEnumAsInt32(target, enumValue);
|
||
return true;
|
||
}
|
||
// Enum can also be encoded as TinyInt directly
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
{
|
||
propInfo.SetEnumAsInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode));
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.String:
|
||
if (BinaryTypeCode.IsFixStr(typeCode))
|
||
{
|
||
var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
|
||
propInfo.SetValue(target, length == 0 ? string.Empty : context.ReadStringUtf8(length));
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.String)
|
||
{
|
||
propInfo.SetValue(target, ReadPlainString(context));
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.StringEmpty)
|
||
{
|
||
propInfo.SetValue(target, string.Empty);
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.StringInterned)
|
||
{
|
||
propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt()));
|
||
return true;
|
||
}
|
||
if (typeCode == BinaryTypeCode.StringInternFirst)
|
||
{
|
||
propInfo.SetValue(target, ReadAndRegisterInternedString(context));
|
||
return true;
|
||
}
|
||
break;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Bridge for generated readers to call ReadValue for unknown/complex/collection property types.
|
||
/// Reads typeCode + dispatches via TypeReaderTable — same as runtime ReadValue.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
internal static object? ReadValueGenerated<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
return ReadValue(context, targetType, depth);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized value reader using FrozenDictionary dispatch table.
|
||
/// </summary>
|
||
private static object? ReadValue<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
if (context.IsAtEnd) return null;
|
||
|
||
var typeCode = context.ReadByte();
|
||
|
||
// Handle tiny int first (most common case for small integers, >= 192)
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
{
|
||
var intValue = BinaryTypeCode.DecodeTinyInt(typeCode);
|
||
return ConvertToTargetType(intValue, targetType);
|
||
}
|
||
|
||
// Handle null
|
||
if (typeCode == BinaryTypeCode.Null) return null;
|
||
|
||
// Handle FixStr (short strings with length in type code)
|
||
if (BinaryTypeCode.IsFixStr(typeCode))
|
||
{
|
||
var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
|
||
return length == 0 ? string.Empty : context.ReadStringUtf8(length);
|
||
}
|
||
|
||
var reader = TypeReaderTable<TInput>.Readers[typeCode];
|
||
if (reader != null)
|
||
{
|
||
return reader(context, targetType, depth);
|
||
}
|
||
|
||
throw new AcBinaryDeserializationException(
|
||
$"Unknown type code: {typeCode}",
|
||
context.Position, targetType);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sima string olvas<61>sa - NEM regisztr<74>l az intern t<>bl<62>ba.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static string ReadPlainString<TInput>(BinaryDeserializationContext<TInput> context)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var length = (int)context.ReadVarUInt();
|
||
if (length == 0) return string.Empty;
|
||
return context.ReadStringUtf8(length);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Read interned string (StringInternFirst marker) and register in cache at specified index.
|
||
/// Wire format: [StringInternFirst][VarUInt cacheIndex][VarUInt length][UTF8 bytes]
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static string ReadAndRegisterInternedString<TInput>(BinaryDeserializationContext<TInput> context)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
// First StringInternFirst marker proves payload uses string interning →
|
||
// plain String entries appear only once, so _stringCache would never hit
|
||
context.DisableStringCaching();
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
var length = (int)context.ReadVarUInt();
|
||
if (length == 0) return string.Empty;
|
||
var str = context.ReadStringUtf8(length);
|
||
context.RegisterInternedValueAt(cacheIndex, str);
|
||
return str;
|
||
}
|
||
|
||
///// <summary>
|
||
///// Read a string and register it in the intern table for future references.
|
||
///// </summary>
|
||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
//private static string ReadAndInternString<TInput>(BinaryDeserializationContext<TInput> context, int streamPosition)
|
||
//{
|
||
// var length = (int)context.ReadVarUInt();
|
||
// if (length == 0) return string.Empty;
|
||
// var str = context.ReadStringUtf8(length);
|
||
// // Always register strings that meet the minimum intern length threshold
|
||
// if (str.Length >= context.MinStringInternLength)
|
||
// {
|
||
// context.RegisterInternedString(str, streamPosition);
|
||
// }
|
||
|
||
// return str;
|
||
//}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object ReadInt32Value<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var value = context.ReadVarInt();
|
||
return ConvertToTargetType(value, targetType);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object ConvertToTargetType(int value, Type targetType)
|
||
{
|
||
var info = GetConversionInfo(targetType);
|
||
if (info.IsEnum)
|
||
return Enum.ToObject(info.UnderlyingType, value);
|
||
|
||
return info.TypeCode switch
|
||
{
|
||
TypeCode.Int32 => value,
|
||
TypeCode.Int64 => (long)value,
|
||
TypeCode.Int16 => (short)value,
|
||
TypeCode.Byte => (byte)value,
|
||
TypeCode.SByte => (sbyte)value,
|
||
TypeCode.UInt16 => (ushort)value,
|
||
TypeCode.UInt32 => (uint)value,
|
||
TypeCode.UInt64 => (ulong)value,
|
||
TypeCode.Double => (double)value,
|
||
TypeCode.Single => (float)value,
|
||
TypeCode.Decimal => (decimal)value,
|
||
_ => value
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets type conversion info with ThreadLocal caching.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static TypeConversionInfo GetConversionInfo(Type targetType)
|
||
{
|
||
// Fast path: check ThreadLocal cache first
|
||
var localCache = t_typeConversionLocalCache;
|
||
if (localCache != null && localCache.TryGetValue(targetType, out var cached))
|
||
{
|
||
return cached;
|
||
}
|
||
|
||
// Slow path
|
||
return GetConversionInfoSlow(targetType);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||
private static TypeConversionInfo GetConversionInfoSlow(Type targetType)
|
||
{
|
||
var info = TypeConversionCache.GetOrAdd(targetType, static type =>
|
||
{
|
||
var underlying = Nullable.GetUnderlyingType(type) ?? type;
|
||
return new TypeConversionInfo(underlying, Type.GetTypeCode(underlying), underlying.IsEnum);
|
||
});
|
||
|
||
// Get or create local cache
|
||
var localCache = t_typeConversionLocalCache ??= new Dictionary<Type, TypeConversionInfo>();
|
||
|
||
// Clear when full - reuses internal array, better than new Dictionary()
|
||
if (localCache.Count >= TypeMetadataBase.MaxLocalCacheSize)
|
||
{
|
||
localCache.Clear();
|
||
}
|
||
|
||
localCache[targetType] = info;
|
||
return info;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object ReadEnumValue<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var info = GetConversionInfo(targetType);
|
||
var nextByte = context.ReadByte();
|
||
|
||
int intValue;
|
||
if (BinaryTypeCode.IsTinyInt(nextByte))
|
||
{
|
||
intValue = BinaryTypeCode.DecodeTinyInt(nextByte);
|
||
}
|
||
else if (nextByte == BinaryTypeCode.Int32)
|
||
{
|
||
intValue = context.ReadVarInt();
|
||
}
|
||
else
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Invalid enum encoding: {nextByte}",
|
||
context.Position, targetType);
|
||
}
|
||
|
||
return info.IsEnum ? Enum.ToObject(info.UnderlyingType, intValue) : intValue;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static byte[] ReadByteArray<TInput>(BinaryDeserializationContext<TInput> context)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var length = (int)context.ReadVarUInt();
|
||
if (length == 0) return [];
|
||
return context.ReadBytes(length);
|
||
}
|
||
|
||
|
||
|
||
#endregion
|
||
|
||
#region Object Reading
|
||
|
||
/// <summary>
|
||
/// Reads an ObjectRef - looks up previously registered object from shared intern cache.
|
||
/// Wire format: [ObjectRef][VarUInt cacheIndex]
|
||
///
|
||
/// IMPORTANT / BUG FIX TODO:
|
||
/// Cross-type deserialization esetén ha egy objektum SkipObject-tel lett átugorva
|
||
/// (mert a target típuson nincs megfelelő property), de később ObjectRef hivatkozik rá,
|
||
/// az intern cache-ben nincs objektum → exception.
|
||
///
|
||
/// Megoldás: SkipObject során a stream pozíciót kell beírni a cache-be (boxolt int).
|
||
/// Itt az "is int" check-kel meg kell különböztetni:
|
||
/// - Ha a cached value valódi objektum → visszaadjuk (jelenlegi működés)
|
||
/// - Ha a cached value boxolt int (stream pozíció) → reposition + ReadObject a target type-ra,
|
||
/// majd az eredményt visszaírjuk a cache-be (hogy a következő ref ne olvasson újra)
|
||
///
|
||
/// Az "is int" check biztonságos, mert az intern cache-be csak string és class instance
|
||
/// kerülhet — egyik sem matchel az "is int"-re. A check az ObjectRef path-ban van,
|
||
/// ami ritka eset (2+ referencia), tehát nem lassítja a hot path-ot.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object? ReadObjectRef<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
return context.GetInternedObject(cacheIndex);
|
||
}
|
||
|
||
/// <summary>
|
||
/// FixObj slot read: marker byte (0..SlotCount-1) is the slot index.
|
||
/// First occurrence: wrapper is null in slot → resolve from targetType, cache in slot.
|
||
/// Subsequent: direct array access (~1-2ns).
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object? ReadObjectFromSlot<TInput>(
|
||
BinaryDeserializationContext<TInput> context,
|
||
int slot,
|
||
Type targetType,
|
||
int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var wrapper = context.GetWrapper(targetType, slot);
|
||
|
||
// Track highest slot used for Clear()
|
||
if (slot >= context._nextRuntimeSlot)
|
||
context._nextRuntimeSlot = slot + 1;
|
||
|
||
// SGen fast path (same as ReadObjectCore)
|
||
if (!context.HasMetadata && !context.IsChainMode && context.Options.UseGeneratedCode)
|
||
{
|
||
var generatedReader = wrapper.GeneratedReader;
|
||
if (generatedReader != null)
|
||
return generatedReader.ReadObject(context, depth, cacheIndex: -1);
|
||
}
|
||
|
||
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex: -1);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Object olvasása (nem tracked, vagy UseMetadata nélkül).
|
||
/// Wire format: [Object][props...]
|
||
/// </summary>
|
||
private static object? ReadObject<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
return ReadObjectCore(context, targetType, depth, cacheIndex: -1);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Object olvasása első előforduláskor (ObjectRefFirst marker).
|
||
/// Wire format: [ObjectRefFirst][VarUInt cacheIndex][props...]
|
||
/// Az objektumot regisztráljuk a cache-be a megadott index-re.
|
||
/// </summary>
|
||
private static object? ReadObjectRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
return ReadObjectCore(context, targetType, depth, cacheIndex: cacheIndex);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Polymorphic PREFIX marker: declared type ≠ runtime type.
|
||
/// Wire format: [ObjectWithTypeName (68)] [TypeName string] [inner marker: Object/Array/Dict/...] [body...]
|
||
/// Reads the runtime type name, resolves it, registers wrapper in poly slot cache,
|
||
/// then reads the inner marker via ReadValue.
|
||
/// </summary>
|
||
private static object? ReadObjectWithTypeName<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var typeName = ReadPlainString(context);
|
||
var resolvedType = AcSerializerCommon.ResolveTypeName(typeName)
|
||
?? throw new AcBinaryDeserializationException(
|
||
$"Cannot resolve type '{typeName}' for ObjectWithTypeName at position {context.Position}.",
|
||
context.Position, null);
|
||
var wrapper = context.GetWrapper(resolvedType);
|
||
context.RegisterPolymorphicWrapper(wrapper);
|
||
// Next byte is the actual inner marker (Object/Array/Dict/etc.) — read it via ReadValue
|
||
return ReadValue(context, resolvedType, depth);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Polymorphic COMBINED marker: first type occurrence + ref tracking first occurrence.
|
||
/// Wire format: [ObjectWithTypeNameRefFirst (69)] [TypeName string] [VarUInt refCacheIndex] [properties...]
|
||
/// Object body follows directly — no inner Object/ObjectRefFirst marker.
|
||
/// </summary>
|
||
private static object? ReadObjectWithTypeNameRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var typeName = ReadPlainString(context);
|
||
var resolvedType = AcSerializerCommon.ResolveTypeName(typeName)
|
||
?? throw new AcBinaryDeserializationException(
|
||
$"Cannot resolve type '{typeName}' for ObjectWithTypeNameRefFirst at position {context.Position}.",
|
||
context.Position, null);
|
||
var wrapper = context.GetWrapper(resolvedType);
|
||
context.RegisterPolymorphicWrapper(wrapper);
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Polymorphic PREFIX marker with cached type index.
|
||
/// Wire format: [ObjectWithTypeIndex (70)] [VarUInt typeIndex] [inner marker: Object/Array/Dict/...] [body...]
|
||
/// Looks up the previously registered wrapper by index (~1-2ns array access),
|
||
/// then reads the inner marker via ReadValue.
|
||
/// </summary>
|
||
private static object? ReadObjectWithTypeIndex<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var typeIndex = (int)context.ReadVarUInt();
|
||
var wrapper = context.GetPolymorphicWrapper(typeIndex);
|
||
return ReadValue(context, wrapper.Metadata.SourceType, depth);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Polymorphic COMBINED marker: cached type index + ref tracking first occurrence.
|
||
/// Wire format: [ObjectWithTypeIndexRefFirst (71)] [VarUInt typeIndex] [VarUInt refCacheIndex] [properties...]
|
||
/// Object body follows directly — no inner Object/ObjectRefFirst marker. 0 dictionary lookup.
|
||
/// </summary>
|
||
private static object? ReadObjectWithTypeIndexRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var typeIndex = (int)context.ReadVarUInt();
|
||
var wrapper = context.GetPolymorphicWrapper(typeIndex);
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Object olvasás core implementáció.
|
||
/// </summary>
|
||
/// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index</param>
|
||
private static object? ReadObjectCore<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth, int cacheIndex)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
// Handle dictionary types
|
||
if (IsDictionaryType(targetType, out var keyType, out var valueType))
|
||
{
|
||
return ReadDictionaryAsObject(context, keyType!, valueType!, depth);
|
||
}
|
||
|
||
var wrapper = context.GetWrapper(targetType);
|
||
|
||
// SGen fast path: generated reader bypasses GetWrapper + CreateInstance + PopulateObject entirely.
|
||
// Only when not in UseMetadata mode (cross-type CacheMap not known at compile time),
|
||
// not in ChainMode (needs post-read identity tracking),
|
||
// and UseGeneratedCode is enabled (matching serializer-side check).
|
||
if (!context.HasMetadata && !context.IsChainMode && context.Options.UseGeneratedCode)
|
||
{
|
||
var generatedReader = wrapper.GeneratedReader;
|
||
if (generatedReader != null)
|
||
return generatedReader.ReadObject(context, depth, cacheIndex);
|
||
}
|
||
|
||
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Object olvasás with pre-resolved wrapper (eliminates GetWrapper dictionary lookup).
|
||
/// </summary>
|
||
private static object? ReadObjectCoreWithWrapper<TInput>(BinaryDeserializationContext<TInput> context, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int depth, int cacheIndex)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var metadata = wrapper.Metadata;
|
||
var targetType = metadata.SourceType;
|
||
|
||
var instance = CreateInstance(targetType, metadata);
|
||
if (instance == null) return null;
|
||
|
||
if (cacheIndex >= 0)
|
||
{
|
||
context.RegisterInternedValueAt(cacheIndex, instance);
|
||
}
|
||
|
||
PopulateObject(context, instance, wrapper, depth, skipDefaultWrite: true);
|
||
|
||
// ChainMode kezelés
|
||
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
|
||
{
|
||
var id = GetIdBoxed(instance, metadata);
|
||
if (id != null && !IsDefaultValue(id, metadata.IdType))
|
||
{
|
||
if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj))
|
||
{
|
||
CopyProperties(instance, existingObj!, metadata);
|
||
return existingObj;
|
||
}
|
||
context.ChainTracker.TryRegisterIIdObject(instance);
|
||
}
|
||
}
|
||
|
||
return instance;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Object olvasása UseMetadata módban (nem tracked).
|
||
/// Wire format:
|
||
/// Első előfordulás: [ObjectWithMetadata][propNameHash (4b)][propCount (VarUInt)][hash0..N][props...]
|
||
/// Ismételt: [ObjectWithMetadata][propNameHash (4b)][props...]
|
||
/// </summary>
|
||
private static object? ReadObjectWithMetadata<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
return ReadObjectWithMetadataCore(context, targetType, depth, cacheIndex: -1);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Object olvasása UseMetadata módban, első tracked előfordulás (ObjectWithMetadataRefFirst marker).
|
||
/// Wire format: [ObjectWithMetadataRefFirst][VarUInt cacheIndex][propNameHash (4b)][...][props...]
|
||
/// Az objektumot regisztráljuk a cache-be a megadott index-re.
|
||
/// </summary>
|
||
private static object? ReadObjectWithMetadataRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
return ReadObjectWithMetadataCore(context, targetType, depth, cacheIndex: cacheIndex);
|
||
}
|
||
|
||
/// <summary>
|
||
/// ObjectWithMetadata olvasás core implementáció.
|
||
/// </summary>
|
||
/// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index</param>
|
||
private static object? ReadObjectWithMetadataCore<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth, int cacheIndex)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
// Inline metadata: propNameHash mindig jön
|
||
var propNameHash = context.ReadInt32Raw();
|
||
|
||
// Source hash-ek keresése — ha nincs, első előfordulás → olvasás a stream-ből
|
||
var sourceHashes = context.FindSourceHashes(propNameHash);
|
||
if (sourceHashes == null)
|
||
{
|
||
var propCount = (int)context.ReadVarUInt();
|
||
sourceHashes = new int[propCount];
|
||
for (var i = 0; i < propCount; i++)
|
||
{
|
||
sourceHashes[i] = context.ReadInt32Raw();
|
||
}
|
||
context.RegisterInlineMetadata(propNameHash, sourceHashes);
|
||
}
|
||
|
||
// Handle dictionary types
|
||
if (IsDictionaryType(targetType, out var keyType, out var valueType))
|
||
{
|
||
return ReadDictionaryAsObject(context, keyType!, valueType!, depth);
|
||
}
|
||
|
||
var wrapper = context.GetWrapper(targetType);
|
||
var metadata = wrapper.Metadata;
|
||
|
||
var instance = CreateInstance(targetType, metadata);
|
||
if (instance == null) return null;
|
||
|
||
if (cacheIndex >= 0)
|
||
{
|
||
context.RegisterInternedValueAt(cacheIndex, instance);
|
||
}
|
||
|
||
// CacheMap felépítése ha még nincs
|
||
if (wrapper.CacheMap == null)
|
||
BuildCacheMap(wrapper, sourceHashes);
|
||
|
||
PopulateObject(context, instance, wrapper, depth, skipDefaultWrite: true);
|
||
|
||
// ChainMode kezelés
|
||
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
|
||
{
|
||
var id = GetIdBoxed(instance, metadata);
|
||
if (id != null && !IsDefaultValue(id, metadata.IdType))
|
||
{
|
||
if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj))
|
||
{
|
||
CopyProperties(instance, existingObj!, metadata);
|
||
return existingObj;
|
||
}
|
||
context.ChainTracker.TryRegisterIIdObject(instance);
|
||
}
|
||
}
|
||
|
||
return instance;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Inline metadata olvasása a Populate path-hoz.
|
||
/// 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.
|
||
/// </summary>
|
||
private static void ReadInlineMetadataForPopulate<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var propNameHash = context.ReadInt32Raw();
|
||
|
||
var sourceHashes = context.FindSourceHashes(propNameHash);
|
||
if (sourceHashes == null)
|
||
{
|
||
var propCount = (int)context.ReadVarUInt();
|
||
sourceHashes = new int[propCount];
|
||
for (var i = 0; i < propCount; i++)
|
||
{
|
||
sourceHashes[i] = context.ReadInt32Raw();
|
||
}
|
||
context.RegisterInlineMetadata(propNameHash, sourceHashes);
|
||
}
|
||
|
||
var wrapper = context.GetWrapper(targetType);
|
||
if (wrapper.CacheMap == null)
|
||
BuildCacheMap(wrapper, sourceHashes);
|
||
}
|
||
|
||
/// <summary>
|
||
/// CacheMap felépítése: source property hash-ek → target PropertySetter mapping.
|
||
/// Fast path: same-type esetén props[index+1] egyezik. Cross-type: keresés index+1-től.
|
||
/// </summary>
|
||
private static void BuildCacheMap(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int[] sourceHashes)
|
||
{
|
||
var properties = wrapper.Metadata.PropertiesArray;
|
||
var cacheMap = new BinaryPropertySetterBase?[sourceHashes.Length];
|
||
var index = -1;
|
||
for (var i = 0; i < sourceHashes.Length; i++)
|
||
{
|
||
var hash = sourceHashes[i];
|
||
var nextIdx = index + 1;
|
||
if (nextIdx < properties.Length && properties[nextIdx].PropertyNameHash == hash)
|
||
{
|
||
cacheMap[i] = properties[nextIdx];
|
||
index = nextIdx;
|
||
}
|
||
else
|
||
{
|
||
for (var j = nextIdx; j < properties.Length; j++)
|
||
{
|
||
if (properties[j].PropertyNameHash == hash)
|
||
{
|
||
cacheMap[i] = properties[j];
|
||
index = j;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
wrapper.CacheMap = cacheMap;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Array Reading
|
||
|
||
private static object? ReadArray<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var elementType = GetCollectionElementType(targetType);
|
||
if (elementType == null) elementType = typeof(object);
|
||
|
||
var count = (int)context.ReadVarUInt();
|
||
var nextDepth = depth + 1;
|
||
|
||
// Optimized path for primitive arrays
|
||
if (targetType.IsArray && count > 0)
|
||
{
|
||
var result = TryReadPrimitiveArray(context, elementType, count);
|
||
if (result != null) return result;
|
||
}
|
||
|
||
if (targetType.IsArray)
|
||
{
|
||
var array = Array.CreateInstance(elementType, count);
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var value = ReadValue(context, elementType, nextDepth);
|
||
array.SetValue(value, i);
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
IList? list = null;
|
||
try
|
||
{
|
||
var instance = Activator.CreateInstance(targetType);
|
||
if (instance is IList l) list = l;
|
||
}
|
||
catch
|
||
{
|
||
/* Fallback to List<T> */
|
||
}
|
||
|
||
list ??= GetOrCreateListFactory(elementType)(count);
|
||
|
||
var acObservable = list as IAcObservableCollection;
|
||
acObservable?.BeginUpdate();
|
||
|
||
try
|
||
{
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var value = ReadValue(context, elementType, nextDepth);
|
||
list.Add(value);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
acObservable?.EndUpdate();
|
||
}
|
||
|
||
return list;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized primitive array reader using bulk operations.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static Array? TryReadPrimitiveArray<TInput>(BinaryDeserializationContext<TInput> context, Type elementType, int count)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
// Int32 array
|
||
if (ReferenceEquals(elementType, IntType))
|
||
{
|
||
var array = new int[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
array[i] = BinaryTypeCode.DecodeTinyInt(typeCode);
|
||
else if (typeCode == BinaryTypeCode.Int32)
|
||
array[i] = context.ReadVarInt();
|
||
else
|
||
return null; // Fall back to generic path
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Double array
|
||
if (ReferenceEquals(elementType, DoubleType))
|
||
{
|
||
var array = new double[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.Float64) return null;
|
||
array[i] = context.ReadDoubleUnsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Long array
|
||
if (ReferenceEquals(elementType, LongType))
|
||
{
|
||
var array = new long[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
array[i] = BinaryTypeCode.DecodeTinyInt(typeCode);
|
||
else if (typeCode == BinaryTypeCode.Int32)
|
||
array[i] = context.ReadVarInt();
|
||
else if (typeCode == BinaryTypeCode.Int64)
|
||
array[i] = context.ReadVarLong();
|
||
else
|
||
return null;
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Bool array
|
||
if (ReferenceEquals(elementType, BoolType))
|
||
{
|
||
var array = new bool[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode == BinaryTypeCode.True) array[i] = true;
|
||
else if (typeCode == BinaryTypeCode.False) array[i] = false;
|
||
else return null;
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Guid array
|
||
if (ReferenceEquals(elementType, GuidType))
|
||
{
|
||
var array = new Guid[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.Guid) return null;
|
||
array[i] = context.ReadGuidUnsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Decimal array
|
||
if (ReferenceEquals(elementType, DecimalType))
|
||
{
|
||
var array = new decimal[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.Decimal) return null;
|
||
array[i] = context.ReadDecimalUnsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// DateTime array
|
||
if (ReferenceEquals(elementType, DateTimeType))
|
||
{
|
||
var array = new DateTime[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.DateTime) return null;
|
||
array[i] = context.ReadDateTimeUnsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Float array
|
||
if (ReferenceEquals(elementType, FloatType))
|
||
{
|
||
var array = new float[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.Float32) return null;
|
||
array[i] = context.ReadSingleUnsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Short (Int16) array
|
||
if (ReferenceEquals(elementType, ShortType))
|
||
{
|
||
var array = new short[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.Int16) return null;
|
||
array[i] = context.ReadInt16Unsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// UShort (UInt16) array
|
||
if (ReferenceEquals(elementType, UShortType))
|
||
{
|
||
var array = new ushort[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.UInt16) return null;
|
||
array[i] = context.ReadUInt16Unsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// UInt32 array
|
||
if (ReferenceEquals(elementType, UIntType))
|
||
{
|
||
var array = new uint[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.UInt32) return null;
|
||
array[i] = context.ReadVarUInt();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// UInt64 array
|
||
if (ReferenceEquals(elementType, ULongType))
|
||
{
|
||
var array = new ulong[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.UInt64) return null;
|
||
array[i] = context.ReadVarULong();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// SByte (Int8) array
|
||
if (ReferenceEquals(elementType, SByteType))
|
||
{
|
||
var array = new sbyte[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.Int8) return null;
|
||
array[i] = unchecked((sbyte)context.ReadByte());
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Char array
|
||
if (ReferenceEquals(elementType, CharType))
|
||
{
|
||
var array = new char[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.Char) return null;
|
||
array[i] = context.ReadCharUnsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// DateTimeOffset array
|
||
if (ReferenceEquals(elementType, DateTimeOffsetType))
|
||
{
|
||
var array = new DateTimeOffset[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.DateTimeOffset) return null;
|
||
array[i] = context.ReadDateTimeOffsetUnsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// TimeSpan array
|
||
if (ReferenceEquals(elementType, TimeSpanType))
|
||
{
|
||
var array = new TimeSpan[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.TimeSpan) return null;
|
||
array[i] = context.ReadTimeSpanUnsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Dictionary Reading
|
||
|
||
private static object? ReadDictionary<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
if (!IsDictionaryType(targetType, out var keyType, out var valueType))
|
||
{
|
||
keyType = typeof(string);
|
||
valueType = typeof(object);
|
||
}
|
||
|
||
return ReadDictionaryAsObject(context, keyType!, valueType!, depth);
|
||
}
|
||
|
||
private static object ReadDictionaryAsObject<TInput>(BinaryDeserializationContext<TInput> context, Type keyType, Type valueType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
|
||
var count = (int)context.ReadVarUInt();
|
||
var dict = (IDictionary)Activator.CreateInstance(dictType, count)!;
|
||
var nextDepth = depth + 1;
|
||
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var key = ReadValue(context, keyType, nextDepth);
|
||
var value = ReadValue(context, valueType, nextDepth);
|
||
if (key != null)
|
||
dict.Add(key, value);
|
||
}
|
||
|
||
return dict;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Skip Value
|
||
|
||
private static void SkipValue<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
|
||
if (typeCode == BinaryTypeCode.Null) return;
|
||
|
||
if (BinaryTypeCode.IsTinyInt(typeCode)) return;
|
||
|
||
// Handle FixStr (short strings)
|
||
if (BinaryTypeCode.IsFixStr(typeCode))
|
||
{
|
||
var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
|
||
if (length > 0)
|
||
context.Skip(length);
|
||
return;
|
||
}
|
||
|
||
switch (typeCode)
|
||
{
|
||
case BinaryTypeCode.True:
|
||
case BinaryTypeCode.False:
|
||
case BinaryTypeCode.StringEmpty:
|
||
return;
|
||
case BinaryTypeCode.Int8:
|
||
case BinaryTypeCode.UInt8:
|
||
context.Skip(1);
|
||
return;
|
||
case BinaryTypeCode.Int16:
|
||
case BinaryTypeCode.UInt16:
|
||
case BinaryTypeCode.Char:
|
||
context.Skip(2);
|
||
return;
|
||
case BinaryTypeCode.Int32:
|
||
context.ReadVarInt();
|
||
return;
|
||
case BinaryTypeCode.UInt32:
|
||
context.ReadVarUInt();
|
||
return;
|
||
case BinaryTypeCode.Float32:
|
||
context.Skip(4);
|
||
return;
|
||
case BinaryTypeCode.Int64:
|
||
context.ReadVarLong();
|
||
return;
|
||
case BinaryTypeCode.UInt64:
|
||
context.ReadVarULong();
|
||
return;
|
||
case BinaryTypeCode.Float64:
|
||
case BinaryTypeCode.TimeSpan:
|
||
context.Skip(8);
|
||
return;
|
||
case BinaryTypeCode.DateTime:
|
||
context.Skip(9);
|
||
return;
|
||
case BinaryTypeCode.DateTimeOffset:
|
||
context.Skip(10);
|
||
return;
|
||
case BinaryTypeCode.Guid:
|
||
case BinaryTypeCode.Decimal:
|
||
context.Skip(16);
|
||
return;
|
||
case BinaryTypeCode.String:
|
||
SkipPlainString(context);
|
||
return;
|
||
case BinaryTypeCode.StringInterned:
|
||
context.ReadVarUInt();
|
||
return;
|
||
case BinaryTypeCode.StringInternFirst:
|
||
// First occurrence - must register even when skipping
|
||
SkipAndRegisterInternedString(context);
|
||
return;
|
||
case BinaryTypeCode.ByteArray:
|
||
var byteLen = (int)context.ReadVarUInt();
|
||
context.Skip(byteLen);
|
||
return;
|
||
case BinaryTypeCode.Enum:
|
||
var enumByte = context.ReadByte();
|
||
if (BinaryTypeCode.IsTinyInt(enumByte)) return;
|
||
if (enumByte == BinaryTypeCode.Int32) context.ReadVarInt();
|
||
return;
|
||
case BinaryTypeCode.Object:
|
||
SkipObject(context, metaData);
|
||
return;
|
||
case BinaryTypeCode.ObjectRefFirst:
|
||
SkipObjectRefFirst(context, metaData);
|
||
return;
|
||
case BinaryTypeCode.ObjectWithMetadata:
|
||
SkipObjectWithMetadata(context, metaData, cacheIndex: -1);
|
||
return;
|
||
case BinaryTypeCode.ObjectWithMetadataRefFirst:
|
||
{
|
||
var cacheIdx = (int)context.ReadVarUInt();
|
||
SkipObjectWithMetadata(context, metaData, cacheIndex: cacheIdx);
|
||
return;
|
||
}
|
||
case BinaryTypeCode.ObjectRef:
|
||
context.ReadVarUInt();
|
||
return;
|
||
case BinaryTypeCode.Array:
|
||
SkipArray(context, metaData);
|
||
return;
|
||
case BinaryTypeCode.Dictionary:
|
||
SkipDictionary(context, metaData);
|
||
return;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sima string kihagy<67>sa - NEM regisztr<74>l.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void SkipPlainString<TInput>(BinaryDeserializationContext<TInput> context)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var byteLen = (int)context.ReadVarUInt();
|
||
if (byteLen > 0)
|
||
{
|
||
context.Skip(byteLen);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Skip an interned string (StringInternFirst) - must still read cacheIndex and register in cache.
|
||
/// Wire format: [StringInternFirst][VarUInt cacheIndex][VarUInt length][UTF8 bytes]
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void SkipAndRegisterInternedString<TInput>(BinaryDeserializationContext<TInput> context)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
var byteLen = (int)context.ReadVarUInt();
|
||
if (byteLen == 0) return;
|
||
var str = context.ReadStringUtf8(byteLen);
|
||
context.RegisterInternedValueAt(cacheIndex, str);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Skip ObjectRefFirst - must read cacheIndex and register placeholder in cache.
|
||
/// Wire format: [ObjectRefFirst][VarUInt cacheIndex][props...]
|
||
/// </summary>
|
||
private static void SkipObjectRefFirst<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
// Register placeholder (stream position as boxed int for potential lazy load)
|
||
context.RegisterInternedValueAt(cacheIndex, context.Position);
|
||
SkipObject(context, metaData);
|
||
}
|
||
|
||
///// <summary>
|
||
///// Skip a string but still register it in the intern table if it meets the length threshold.
|
||
///// </summary>
|
||
///// <param name="context">Deserialization context</param>
|
||
///// <param name="streamPosition">Position before the type code was read</param>
|
||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
//private static void SkipAndInternString<TInput>(BinaryDeserializationContext<TInput> context, int streamPosition)
|
||
//{
|
||
// var byteLen = (int)context.ReadVarUInt();
|
||
// if (byteLen == 0) return;
|
||
|
||
// var str = context.ReadStringUtf8(byteLen);
|
||
// if (str.Length >= context.MinStringInternLength)
|
||
// {
|
||
// context.RegisterInternedString(str, streamPosition);
|
||
// }
|
||
//}
|
||
|
||
/// <summary>
|
||
/// Object kihagyása metadata nélkül — nem támogatott, mert nem tudjuk a property számot.
|
||
/// </summary>
|
||
private static void SkipObject<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
throw new NotSupportedException(
|
||
"SkipObject nem támogatott metadata nélkül. " +
|
||
"A property szám nem határozható meg típus metadata nélkül.");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Skip ObjectWithMetadata/ObjectWithMetadataRefFirst.
|
||
/// </summary>
|
||
/// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index</param>
|
||
private static void SkipObjectWithMetadata<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData, int cacheIndex)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
if (cacheIndex >= 0)
|
||
{
|
||
// Register placeholder for potential lazy load
|
||
context.RegisterInternedValueAt(cacheIndex, context.Position);
|
||
}
|
||
|
||
var propNameHash = context.ReadInt32Raw();
|
||
|
||
var sourceHashes = context.FindSourceHashes(propNameHash);
|
||
if (sourceHashes == null)
|
||
{
|
||
var propCount = (int)context.ReadVarUInt();
|
||
sourceHashes = new int[propCount];
|
||
for (var i = 0; i < propCount; i++)
|
||
{
|
||
sourceHashes[i] = context.ReadInt32Raw();
|
||
}
|
||
context.RegisterInlineMetadata(propNameHash, sourceHashes);
|
||
}
|
||
|
||
// Skip all properties
|
||
for (var i = 0; i < sourceHashes.Length; i++)
|
||
{
|
||
SkipValue(context, metaData);
|
||
}
|
||
}
|
||
|
||
private static void SkipArray<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var count = (int)context.ReadVarUInt();
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
SkipValue(context, metaData);
|
||
}
|
||
}
|
||
|
||
private static void SkipDictionary<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var count = (int)context.ReadVarUInt();
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
SkipValue(context, metaData); // key
|
||
SkipValue(context, metaData); // value
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region IId Registration Helpers
|
||
|
||
/// <summary>
|
||
/// Gets Id as boxed object - only used for ChainMode (rare path).
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object? GetIdBoxed(object instance, BinaryDeserializeTypeMetadata metadata)
|
||
{
|
||
return metadata.IdAccessorType switch
|
||
{
|
||
IdAccessorType.Int32 => metadata.GetIdInt32(instance),
|
||
IdAccessorType.Int64 => metadata.GetIdInt64(instance),
|
||
IdAccessorType.Guid => metadata.GetIdGuid(instance),
|
||
_ => null
|
||
};
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Type Metadata
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata)
|
||
{
|
||
if (metadata.CompiledConstructor != null)
|
||
return metadata.CompiledConstructor();
|
||
|
||
try
|
||
{
|
||
return Activator.CreateInstance(type);
|
||
}
|
||
catch (MissingMethodException ex)
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Cannot create instance of type '{type.FullName}' because it does not have a parameterless constructor.",
|
||
0, type, ex);
|
||
}
|
||
}
|
||
|
||
private static Func<object, object?> CreateCompiledGetter(Type declaringType, PropertyInfo prop)
|
||
{
|
||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||
var castExpr = Expression.Convert(objParam, declaringType);
|
||
var propAccess = Expression.Property(castExpr, prop);
|
||
var boxed = Expression.Convert(propAccess, typeof(object));
|
||
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
|
||
// Implementation moved to AcBinaryDeserializer.TypeConversionInfo.cs
|