AyCode.Core/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs

2270 lines
90 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 System.Threading;
using System.Threading.Tasks;
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.GetValueOrDefault(type);
}
/// <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 (var 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 (var 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(new SequenceBinaryInput(data), targetType, options);
}
/// <summary>
/// Deserialize from a <see cref="SegmentBufferReader"/> with streaming pipeline parallelism.
/// The producer thread writes chunk data via <see cref="SegmentBufferReader.Write"/>,
/// while this method (running on a background thread) deserializes incrementally,
/// blocking on <see cref="System.Threading.ManualResetEventSlim"/> when data is exhausted.
/// </summary>
public static object? Deserialize(SegmentBufferReader reader, Type targetType, AcBinarySerializerOptions options)
=> DeserializeSequence(new SegmentBufferReaderInput(reader), targetType, options);
/// <summary>
/// Deserialize from an <see cref="AsyncPipeReaderInput"/> with streaming pipeline parallelism.
/// The producer thread feeds chunk data via <see cref="AsyncPipeReaderInput.Feed"/>,
/// while this method (running on a background thread) deserializes incrementally,
/// blocking on <see cref="System.Threading.ManualResetEventSlim"/> when data is exhausted.
///
/// <para>Use these overloads for SignalR <c>AsyncSegment</c>-style chunked streaming,
/// NamedPipe IPC, FileStream, or any other transport that produces an
/// <see cref="AsyncPipeReaderInput"/>. The internal <see cref="AsyncPipeReaderInputAdapter"/>
/// struct satisfies the JIT-specialization constraint of the generic deserialization path
/// without exposing a value-type wrapper to the public API.</para>
/// </summary>
public static T? Deserialize<T>(AsyncPipeReaderInput input) => Deserialize<T>(input, AcBinarySerializerOptions.Default);
/// <inheritdoc cref="Deserialize{T}(AsyncPipeReaderInput)"/>
public static T? Deserialize<T>(AsyncPipeReaderInput input, AcBinarySerializerOptions options)
=> DeserializeSequence<T, AsyncPipeReaderInputAdapter>(new AsyncPipeReaderInputAdapter(input), typeof(T), options);
/// <inheritdoc cref="Deserialize{T}(AsyncPipeReaderInput)"/>
public static object? Deserialize(AsyncPipeReaderInput input, Type targetType, AcBinarySerializerOptions options)
=> DeserializeSequence(new AsyncPipeReaderInputAdapter(input), targetType, options);
/// <summary>
/// Deserialize from a <see cref="System.IO.Pipelines.PipeReader"/> with full streaming pipeline
/// parallelism — drains the reader on the calling thread, while a background <c>Task.Run</c>
/// deserializes incrementally from the same shared <see cref="AsyncPipeReaderInput"/>.
///
/// <para>Transport-agnostic: works with any <c>PipeReader</c> source — NamedPipe IPC
/// (<c>PipeReader.Create(namedPipeServerStream)</c>), file-stream
/// (<c>PipeReader.Create(fileStream)</c>), TCP (<c>PipeReader.Create(networkStream)</c>),
/// or custom <c>PipeReader</c> implementations. Reads <b>raw AcBinary bytes</b> verbatim from
/// the pipe — no wire-format unwrapping. Pair with the producer-side
/// <see cref="AcBinarySerializer.SerializeChunked{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions)"/>
/// (or its <see cref="System.IO.Pipelines.Pipe"/> overload), which writes the same raw byte
/// stream as <see cref="AcBinarySerializer.Serialize{T}(T, AcBinarySerializerOptions)"/>'s
/// <c>byte[]</c> output.</para>
///
/// <para>Receive buffer initial capacity is derived from <c>options.BufferWriterChunkSize × 2</c>
/// — two-chunks-worth of headroom plus reset-to-0 cycling reuses the same buffer for the
/// message's lifetime regardless of total payload size.</para>
///
/// <para><b>For the multiplexed wire format</b> (per-chunk <c>[201][UINT16][data]</c> headers,
/// produced by <c>SerializeChunkedFramed</c> or SignalR's AsyncSegment mode): the parser
/// strips framing on its own (e.g. <c>AcBinaryHubProtocol.TryParseChunkData</c>) and feeds
/// only the data bytes here.</para>
/// </summary>
/// <param name="reader">Source pipe reader. Caller owns lifecycle (creation + completion).</param>
/// <param name="options">Serializer options. Defaults to <see cref="AcBinarySerializerOptions.Default"/>.
/// <c>BufferWriterChunkSize</c> controls the receive-side initial buffer (× 2 headroom).</param>
/// <param name="ct">Cancellation token. For connect-timeout, pass the token of a
/// <c>new CancellationTokenSource(timeout)</c>.</param>
public static async Task<T?> DeserializeFromPipeReaderAsync<T>(System.IO.Pipelines.PipeReader reader, AcBinarySerializerOptions? options = null, CancellationToken ct = default)
{
if (reader is null) throw new ArgumentNullException(nameof(reader));
var opts = options ?? AcBinarySerializerOptions.Default;
// Raw mode (stripChunkFraming: false) — bytes drained from the PipeReader are forwarded
// verbatim to the deserialization buffer. Pair with AcBinarySerializer.SerializeChunked
// (raw byte stream) on the producer side; for chunked-framed wire formats the parser
// strips framing upstream and feeds only data bytes here.
using var input = new AsyncPipeReaderInput(initialCapacity: opts.BufferWriterChunkSize * 2, stripChunkFraming: false);
var deserTask = Task.Run(() => Deserialize<T>(input, opts), ct);
await input.DrainFromAsync(reader, ct).ConfigureAwait(false);
return await deserTask.ConfigureAwait(false);
}
/// <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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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 (var 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