Polymorphic serialization: slot-based prefix system overhaul
Refactored BinaryTypeCode to reserve 0..63 for FixObj slot indices, enabling direct array access for object wrappers. Introduced a new polymorphic type prefix system for properties whose runtime type differs from their declared type, with first/repeated occurrence markers and combined ref-tracking support. Unified wrapper slot caching for SGen and runtime types, improving performance and eliminating dictionary lookups in hot paths. Updated code generation, tests, and constants to use the new slot system. Added new settings and utility scripts. Overall, serialization is now faster, more robust, and extensible.
This commit is contained in:
parent
11a15bfa64
commit
68c25b2381
|
|
@ -44,7 +44,9 @@
|
||||||
"Bash(curl -s \"https://api.github.com/repos/Cysharp/MemoryPack/git/trees/main?recursive=1\")",
|
"Bash(curl -s \"https://api.github.com/repos/Cysharp/MemoryPack/git/trees/main?recursive=1\")",
|
||||||
"Bash(curl -sL \"https://raw.githubusercontent.com/Cysharp/MemoryPack/main/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs\")",
|
"Bash(curl -sL \"https://raw.githubusercontent.com/Cysharp/MemoryPack/main/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs\")",
|
||||||
"Bash(curl -sL \"https://raw.githubusercontent.com/Cysharp/MemoryPack/main/src/MemoryPack.Core/MemoryPackCode.cs\")",
|
"Bash(curl -sL \"https://raw.githubusercontent.com/Cysharp/MemoryPack/main/src/MemoryPack.Core/MemoryPackCode.cs\")",
|
||||||
"Bash(curl -sL \"https://raw.githubusercontent.com/Cysharp/MemoryPack/main/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs\")"
|
"Bash(curl -sL \"https://raw.githubusercontent.com/Cysharp/MemoryPack/main/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs\")",
|
||||||
|
"Bash(perl -i -pe 's/GetWrapperBySlot\\\\\\(\\([^,]+\\), \\(typeof\\\\\\([^\\)]+\\\\\\)\\)\\\\\\)/GetWrapper\\($2, $1\\)/g' \"H:/Applications/Aycode/Source/AyCode.Core/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs\")",
|
||||||
|
"Bash(wc -l H:/Applications/Aycode/Source/AyCode.Core/AyCode.Core/Serializers/Binaries/*.cs)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,14 @@ public static class Program
|
||||||
// Serializer name constants
|
// Serializer name constants
|
||||||
private const string SerializerMessagePack = "MessagePack";
|
private const string SerializerMessagePack = "MessagePack";
|
||||||
private const string SerializerAcBinaryDefault = "AcBinary (Default)";
|
private const string SerializerAcBinaryDefault = "AcBinary (Default)";
|
||||||
|
private const string SerializerAcBinaryDefaultNoSgen = "AcBinary (Def, NoSgen)";
|
||||||
private const string SerializerAcBinaryNoRef = "AcBinary (NoRef)";
|
private const string SerializerAcBinaryNoRef = "AcBinary (NoRef)";
|
||||||
private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)";
|
private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)";
|
||||||
|
private const string SerializerAcBinaryFastNoSgen = "AcBinary (Fast, NoSgen)";
|
||||||
private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)";
|
private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)";
|
||||||
private const string SerializerMemoryPack = "MemoryPack";
|
private const string SerializerMemoryPack = "MemoryPack";
|
||||||
private const string SerializerAcBinaryBufferWriter = "AcBinary (BufferWriter)";
|
//private const string SerializerAcBinaryBufferWriter = "AcBinary (BufferWriter)";
|
||||||
private const string SerializerSystemTextJson = "System.Text.Json";
|
//private const string SerializerSystemTextJson = "System.Text.Json";
|
||||||
|
|
||||||
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
|
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
|
||||||
|
|
||||||
|
|
@ -208,20 +210,34 @@ public static class Program
|
||||||
|
|
||||||
private static List<ISerializerBenchmark> CreateSerializers(TestDataSet testData)
|
private static List<ISerializerBenchmark> CreateSerializers(TestDataSet testData)
|
||||||
{
|
{
|
||||||
|
var binaryNoInternOption = AcBinarySerializerOptions.Default;
|
||||||
|
binaryNoInternOption.UseStringInterning = StringInterningMode.None;
|
||||||
|
|
||||||
|
var binaryDefaultNoSgenOption = AcBinarySerializerOptions.Default;
|
||||||
|
binaryDefaultNoSgenOption.UseGeneratedCode = false;
|
||||||
|
|
||||||
|
var binaryFastModeNoSgenOption = AcBinarySerializerOptions.FastMode;
|
||||||
|
binaryFastModeNoSgenOption.UseGeneratedCode = false;
|
||||||
|
|
||||||
return new List<ISerializerBenchmark>
|
return new List<ISerializerBenchmark>
|
||||||
{
|
{
|
||||||
|
|
||||||
// AcBinary variants
|
// AcBinary variants
|
||||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
|
||||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
|
|
||||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||||||
new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
|
new AcBinaryBenchmark(testData.Order, binaryFastModeNoSgenOption, SerializerAcBinaryFastNoSgen),
|
||||||
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
||||||
|
new AcBinaryBenchmark(testData.Order, binaryDefaultNoSgenOption, SerializerAcBinaryDefaultNoSgen),
|
||||||
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
|
||||||
|
new AcBinaryBenchmark(testData.Order, binaryNoInternOption, SerializerAcBinaryNoIntern),
|
||||||
|
|
||||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
|
|
||||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
|
|
||||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||||||
|
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastNoSgen),
|
||||||
|
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
|
||||||
|
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefaultNoSgen),
|
||||||
|
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
|
||||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
|
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
|
||||||
|
|
||||||
|
|
||||||
// MemoryPack
|
// MemoryPack
|
||||||
new MemoryPackBenchmark(testData.Order, SerializerMemoryPack),
|
new MemoryPackBenchmark(testData.Order, SerializerMemoryPack),
|
||||||
|
|
||||||
|
|
@ -229,10 +245,10 @@ public static class Program
|
||||||
new MessagePackBenchmark(testData.Order, SerializerMessagePack),
|
new MessagePackBenchmark(testData.Order, SerializerMessagePack),
|
||||||
|
|
||||||
// AcBinary BufferWriter
|
// AcBinary BufferWriter
|
||||||
new AcBinaryBufferWriterBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryBufferWriter),
|
//new AcBinaryBufferWriterBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryBufferWriter),
|
||||||
|
|
||||||
// System.Text.Json
|
// System.Text.Json
|
||||||
new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson)
|
//new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -583,7 +583,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine(" if (context.HasRefHandling)");
|
sb.AppendLine(" if (context.HasRefHandling)");
|
||||||
sb.AppendLine(" {");
|
sb.AppendLine(" {");
|
||||||
sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));");
|
sb.AppendLine($" var wrapper = context.GetWrapper(typeof({ci.FullTypeName}), s_wrapperSlot);");
|
||||||
sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;");
|
sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;");
|
||||||
sb.AppendLine($" if (!wrapper.{tryTrackMethod}(obj.Id, visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))");
|
sb.AppendLine($" if (!wrapper.{tryTrackMethod}(obj.Id, visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))");
|
||||||
sb.AppendLine(" {");
|
sb.AppendLine(" {");
|
||||||
|
|
@ -600,7 +600,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine(" if (context.HasAllRefHandling)");
|
sb.AppendLine(" if (context.HasAllRefHandling)");
|
||||||
sb.AppendLine(" {");
|
sb.AppendLine(" {");
|
||||||
sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));");
|
sb.AppendLine($" var wrapper = context.GetWrapper(typeof({ci.FullTypeName}), s_wrapperSlot);");
|
||||||
sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;");
|
sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;");
|
||||||
sb.AppendLine(" if (!wrapper.TryTrackInt32(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value), visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))");
|
sb.AppendLine(" if (!wrapper.TryTrackInt32(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value), visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))");
|
||||||
sb.AppendLine(" {");
|
sb.AppendLine(" {");
|
||||||
|
|
@ -1227,7 +1227,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// No ref, but metadata possible → UseMetadata branch only
|
// No ref, but metadata possible → UseMetadata branch only
|
||||||
sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));");
|
sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapper(typeof({p.TypeNameForTypeof}), {writer}.s_wrapperSlot));");
|
||||||
sb.AppendLine($"{i} if (context.UseMetadata)");
|
sb.AppendLine($"{i} if (context.UseMetadata)");
|
||||||
sb.AppendLine($"{i} {{");
|
sb.AppendLine($"{i} {{");
|
||||||
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
|
||||||
|
|
@ -1267,7 +1267,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Full path: ref tracking + metadata
|
// Full path: ref tracking + metadata
|
||||||
sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));");
|
sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapper(typeof({p.TypeNameForTypeof}), {writer}.s_wrapperSlot));");
|
||||||
var refGuard = p.IsIId
|
var refGuard = p.IsIId
|
||||||
? "context.HasRefHandling"
|
? "context.HasRefHandling"
|
||||||
: "context.HasAllRefHandling";
|
: "context.HasAllRefHandling";
|
||||||
|
|
@ -1398,7 +1398,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// No ref, but metadata possible → UseMetadata branch only
|
// No ref, but metadata possible → UseMetadata branch only
|
||||||
sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));");
|
sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapper(typeof({p.ElementFullTypeName}), {writer}.s_wrapperSlot));");
|
||||||
sb.AppendLine($"{i} if (context.UseMetadata)");
|
sb.AppendLine($"{i} if (context.UseMetadata)");
|
||||||
sb.AppendLine($"{i} {{");
|
sb.AppendLine($"{i} {{");
|
||||||
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
|
||||||
|
|
@ -1442,7 +1442,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Full path: ref tracking + metadata
|
// Full path: ref tracking + metadata
|
||||||
sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));");
|
sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapper(typeof({p.ElementFullTypeName}), {writer}.s_wrapperSlot));");
|
||||||
sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))");
|
sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))");
|
||||||
sb.AppendLine($"{i} {{");
|
sb.AppendLine($"{i} {{");
|
||||||
sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)");
|
sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)");
|
||||||
|
|
@ -1655,7 +1655,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// No ref, metadata possible
|
// No ref, metadata possible
|
||||||
sb.AppendLine($"{i} var isFirstMeta_dv_{s} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({valType})));");
|
sb.AppendLine($"{i} var isFirstMeta_dv_{s} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapper(typeof({valType}), {writer}.s_wrapperSlot));");
|
||||||
sb.AppendLine($"{i} if (context.UseMetadata)");
|
sb.AppendLine($"{i} if (context.UseMetadata)");
|
||||||
sb.AppendLine($"{i} {{");
|
sb.AppendLine($"{i} {{");
|
||||||
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
|
||||||
|
|
@ -1698,7 +1698,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Full path: ref tracking + metadata
|
// Full path: ref tracking + metadata
|
||||||
sb.AppendLine($"{i} var isFirstMeta_dv_{s} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({valType})));");
|
sb.AppendLine($"{i} var isFirstMeta_dv_{s} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapper(typeof({valType}), {writer}.s_wrapperSlot));");
|
||||||
sb.AppendLine($"{i} if ({dvRefGuard} && context.TryConsumeWritePlanEntry(out var dpe_{s}))");
|
sb.AppendLine($"{i} if ({dvRefGuard} && context.TryConsumeWritePlanEntry(out var dpe_{s}))");
|
||||||
sb.AppendLine($"{i} {{");
|
sb.AppendLine($"{i} {{");
|
||||||
sb.AppendLine($"{i} if (!dpe_{s}.IsFirst)");
|
sb.AppendLine($"{i} if (!dpe_{s}.IsFirst)");
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ public class AcBinarySerializerBasicTests
|
||||||
{
|
{
|
||||||
var result = AcBinarySerializer.Serialize<object?>(null);
|
var result = AcBinarySerializer.Serialize<object?>(null);
|
||||||
Assert.AreEqual(1, result.Length);
|
Assert.AreEqual(1, result.Length);
|
||||||
Assert.AreEqual((byte)0, result[0]);
|
Assert.AreEqual(BinaryTypeCode.Null, result[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,9 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
|
||||||
private readonly Dictionary<Type, TypeMetadataWrapper<TMetadata>> _wrappers = new();
|
private readonly Dictionary<Type, TypeMetadataWrapper<TMetadata>> _wrappers = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Slot-indexed wrapper cache for SGen types. Indexed by AllocateWrapperSlot() value.
|
/// Slot-indexed wrapper cache. Shared between SGen types (RuntimeSlotCount+) and
|
||||||
/// Avoids dictionary lookup — direct array access for types with compile-time known slot index.
|
/// runtime polymorphic type cache (0..RuntimeSlotCount-1).
|
||||||
|
/// Avoids dictionary lookup — direct array access (~1-2ns vs ~15-25ns).
|
||||||
/// Not cleared on pool return: wrapper references are stable across serialization calls.
|
/// Not cleared on pool return: wrapper references are stable across serialization calls.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private TypeMetadataWrapper<TMetadata>?[]? _wrapperSlots;
|
private TypeMetadataWrapper<TMetadata>?[]? _wrapperSlots;
|
||||||
|
|
@ -107,33 +108,35 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or creates a wrapper for the specified type using a pre-allocated slot index.
|
/// Gets or creates a wrapper for the specified type using a slot index.
|
||||||
/// SGen types call this with their compile-time known slot — avoids dictionary lookup.
|
/// Slot checked first (array access ~1-2ns), falls back to dictionary if slot empty.
|
||||||
/// First call per slot per context: falls back to GetWrapper + stores in slot array.
|
/// Used by both SGen (compile-time slot) and runtime polymorphic cache (0..RuntimeSlotCount-1).
|
||||||
/// Subsequent calls: direct array index (~1-2ns vs ~15-25ns dictionary lookup).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public TypeMetadataWrapper<TMetadata> GetWrapperBySlot(int slot, Type type)
|
public TypeMetadataWrapper<TMetadata> GetWrapper(Type type, int slotIndex)
|
||||||
{
|
{
|
||||||
var slots = _wrapperSlots;
|
var slots = _wrapperSlots!;
|
||||||
if (slots != null && slot < slots.Length)
|
var wrapper = slots[slotIndex];
|
||||||
{
|
if (wrapper != null)
|
||||||
var wrapper = slots[slot];
|
return wrapper;
|
||||||
if (wrapper != null)
|
|
||||||
return wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetWrapperBySlotSlow(slot, type);
|
wrapper = GetWrapper(type);
|
||||||
}
|
slots[slotIndex] = wrapper;
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
||||||
private TypeMetadataWrapper<TMetadata> GetWrapperBySlotSlow(int slot, Type type)
|
|
||||||
{
|
|
||||||
var wrapper = GetWrapper(type);
|
|
||||||
_wrapperSlots![slot] = wrapper;
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
protected TypeMetadataWrapper<TMetadata>? GetWrapperSlot(int slotIndex)
|
||||||
|
=> _wrapperSlots![slotIndex];
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
protected void SetWrapperSlot(int slotIndex, TypeMetadataWrapper<TMetadata> wrapper)
|
||||||
|
=> _wrapperSlots![slotIndex] = wrapper;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
protected void ClearWrapperSlots(int count)
|
||||||
|
=> Array.Clear(_wrapperSlots!, 0, count);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pre-allocates the wrapper slot array with the known total slot count.
|
/// Pre-allocates the wrapper slot array with the known total slot count.
|
||||||
/// Called once from derived context constructor after all AllocateWrapperSlot() calls have completed.
|
/// Called once from derived context constructor after all AllocateWrapperSlot() calls have completed.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Buffers;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Binaries;
|
namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
||||||
|
|
@ -104,6 +105,19 @@ public static partial class AcBinaryDeserializer
|
||||||
private MetadataEntry[]? _metadataEntries;
|
private MetadataEntry[]? _metadataEntries;
|
||||||
private int _metadataEntryCount;
|
private int _metadataEntryCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime polymorphic slot counter. Indices 0..RuntimeSlotCount-1 stored in _wrapperSlots.
|
||||||
|
/// Overflow (index >= RuntimeSlotCount) stored in _polyOverflow.
|
||||||
|
/// </summary>
|
||||||
|
internal int _nextRuntimeSlot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overflow array for polymorphic types beyond RuntimeSlotCount.
|
||||||
|
/// Only allocated when >RuntimeSlotCount distinct poly types (very rare).
|
||||||
|
/// Indexed by (polyIndex - RuntimeSlotCount).
|
||||||
|
/// </summary>
|
||||||
|
private TypeMetadataWrapper<BinaryDeserializeTypeMetadata>[]? _polyOverflow;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A metadata entry for the deserializer.
|
/// A metadata entry for the deserializer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -121,6 +135,7 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
public BinaryDeserializationContext()
|
public BinaryDeserializationContext()
|
||||||
{
|
{
|
||||||
|
InitializeWrapperSlots(Volatile.Read(ref AcBinarySerializer.s_nextWrapperSlot));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -367,6 +382,54 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Polymorphic Wrapper Cache
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a wrapper in the polymorphic cache (called by ReadObjectWithTypeName/RefFirst).
|
||||||
|
/// Indices 0..RuntimeSlotCount-1 → _wrapperSlots (fast path, ~1-2ns).
|
||||||
|
/// Indices RuntimeSlotCount+ → _polyOverflow (still O(1) array access).
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
internal void RegisterPolymorphicWrapper(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
||||||
|
{
|
||||||
|
var slot = _nextRuntimeSlot++;
|
||||||
|
if (slot < AcBinarySerializer.RuntimeSlotCount)
|
||||||
|
{
|
||||||
|
SetWrapperSlot(slot, wrapper);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RegisterPolymorphicWrapperOverflow(wrapper, slot - AcBinarySerializer.RuntimeSlotCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private void RegisterPolymorphicWrapperOverflow(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int overflowIndex)
|
||||||
|
{
|
||||||
|
if (_polyOverflow == null || overflowIndex >= _polyOverflow.Length)
|
||||||
|
{
|
||||||
|
var newSize = _polyOverflow == null ? 4 : _polyOverflow.Length * 2;
|
||||||
|
var newArray = new TypeMetadataWrapper<BinaryDeserializeTypeMetadata>[newSize];
|
||||||
|
if (_polyOverflow != null)
|
||||||
|
Array.Copy(_polyOverflow, newArray, overflowIndex);
|
||||||
|
_polyOverflow = newArray;
|
||||||
|
}
|
||||||
|
_polyOverflow[overflowIndex] = wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a previously registered polymorphic wrapper by index (called by ReadObjectWithTypeIndex/RefFirst).
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
internal TypeMetadataWrapper<BinaryDeserializeTypeMetadata> GetPolymorphicWrapper(int index)
|
||||||
|
{
|
||||||
|
if (index < AcBinarySerializer.RuntimeSlotCount)
|
||||||
|
return GetWrapperSlot(index)!;
|
||||||
|
return _polyOverflow![index - AcBinarySerializer.RuntimeSlotCount]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Reset & Clear
|
#region Reset & Clear
|
||||||
|
|
||||||
public override void Reset(AcBinarySerializerOptions options)
|
public override void Reset(AcBinarySerializerOptions options)
|
||||||
|
|
@ -381,6 +444,17 @@ public static partial class AcBinaryDeserializer
|
||||||
_metadataEntryCount = 0;
|
_metadataEntryCount = 0;
|
||||||
_nextCacheIndex = 0;
|
_nextCacheIndex = 0;
|
||||||
|
|
||||||
|
// Clear runtime FixObj slots to prevent stale wrapper reuse on pool return.
|
||||||
|
// Slot-to-type mapping changes between sessions (slot 0 may be TestOrder in session A
|
||||||
|
// but TestGenericAttribute in session B). Without clearing, ReadObjectFromSlot
|
||||||
|
// would reuse the stale wrapper → wrong type → stream misalignment.
|
||||||
|
if (_nextRuntimeSlot > 0)
|
||||||
|
{
|
||||||
|
ClearWrapperSlots(Math.Min(_nextRuntimeSlot, AcBinarySerializer.RuntimeSlotCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
_nextRuntimeSlot = 0;
|
||||||
|
|
||||||
// String cache: clear content but keep dictionary allocated for reuse
|
// String cache: clear content but keep dictionary allocated for reuse
|
||||||
_stringCache?.Clear();
|
_stringCache?.Clear();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,17 +102,26 @@ public static partial class AcBinaryDeserializer
|
||||||
readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst;
|
readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst;
|
||||||
readers[BinaryTypeCode.ObjectRef] = ReadObjectRef;
|
readers[BinaryTypeCode.ObjectRef] = ReadObjectRef;
|
||||||
readers[BinaryTypeCode.ObjectWithTypeName] = ReadObjectWithTypeName;
|
readers[BinaryTypeCode.ObjectWithTypeName] = ReadObjectWithTypeName;
|
||||||
|
readers[BinaryTypeCode.ObjectWithTypeNameRefFirst] = ReadObjectWithTypeNameRefFirst;
|
||||||
|
readers[BinaryTypeCode.ObjectWithTypeIndex] = ReadObjectWithTypeIndex;
|
||||||
|
readers[BinaryTypeCode.ObjectWithTypeIndexRefFirst] = ReadObjectWithTypeIndexRefFirst;
|
||||||
readers[BinaryTypeCode.Array] = ReadArray;
|
readers[BinaryTypeCode.Array] = ReadArray;
|
||||||
readers[BinaryTypeCode.Dictionary] = ReadDictionary;
|
readers[BinaryTypeCode.Dictionary] = ReadDictionary;
|
||||||
readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx);
|
readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx);
|
||||||
|
|
||||||
// Register FixStr readers (34-65)
|
// Register FixStr readers
|
||||||
for (byte code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++)
|
for (byte code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++)
|
||||||
{
|
{
|
||||||
var length = BinaryTypeCode.DecodeFixStrLength(code);
|
var length = BinaryTypeCode.DecodeFixStrLength(code);
|
||||||
readers[code] = CreateFixStrReader<TInput>(length);
|
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;
|
return readers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -131,6 +140,16 @@ public static partial class AcBinaryDeserializer
|
||||||
return (ctx, _, _) => ctx.ReadStringUtf8(length);
|
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);
|
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
|
||||||
|
|
||||||
#region Public API
|
#region Public API
|
||||||
|
|
@ -908,16 +927,16 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
var typeCode = context.ReadByte();
|
var typeCode = context.ReadByte();
|
||||||
|
|
||||||
// Handle null first
|
// Handle tiny int first (most common case for small integers, >= 192)
|
||||||
if (typeCode == BinaryTypeCode.Null) return null;
|
|
||||||
|
|
||||||
// Handle tiny int (most common case for small integers)
|
|
||||||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||||||
{
|
{
|
||||||
var intValue = BinaryTypeCode.DecodeTinyInt(typeCode);
|
var intValue = BinaryTypeCode.DecodeTinyInt(typeCode);
|
||||||
return ConvertToTargetType(intValue, targetType);
|
return ConvertToTargetType(intValue, targetType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle null
|
||||||
|
if (typeCode == BinaryTypeCode.Null) return null;
|
||||||
|
|
||||||
// Handle FixStr (short strings with length in type code)
|
// Handle FixStr (short strings with length in type code)
|
||||||
if (BinaryTypeCode.IsFixStr(typeCode))
|
if (BinaryTypeCode.IsFixStr(typeCode))
|
||||||
{
|
{
|
||||||
|
|
@ -1124,6 +1143,36 @@ public static partial class AcBinaryDeserializer
|
||||||
return context.GetInternedObject(cacheIndex);
|
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>
|
/// <summary>
|
||||||
/// Object olvasása (nem tracked, vagy UseMetadata nélkül).
|
/// Object olvasása (nem tracked, vagy UseMetadata nélkül).
|
||||||
/// Wire format: [Object][props...]
|
/// Wire format: [Object][props...]
|
||||||
|
|
@ -1147,10 +1196,10 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Polymorphic object prefix: declared property type is System.Object.
|
/// Polymorphic PREFIX marker: declared type ≠ runtime type.
|
||||||
/// Wire format: [ObjectWithTypeName (68)] [TypeName string] [Object (25) or ObjectRefFirst (66) ...] [props...]
|
/// Wire format: [ObjectWithTypeName (68)] [TypeName string] [inner marker: Object/Array/Dict/...] [body...]
|
||||||
/// Reads the runtime type name, resolves it, then delegates to ReadValue with the resolved type
|
/// Reads the runtime type name, resolves it, registers wrapper in poly slot cache,
|
||||||
/// so the next marker (Object/ObjectRefFirst/etc.) is processed normally.
|
/// then reads the inner marker via ReadValue.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static object? ReadObjectWithTypeName<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
private static object? ReadObjectWithTypeName<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||||||
where TInput : struct, IBinaryInputBase
|
where TInput : struct, IBinaryInputBase
|
||||||
|
|
@ -1160,10 +1209,59 @@ public static partial class AcBinaryDeserializer
|
||||||
?? throw new AcBinaryDeserializationException(
|
?? throw new AcBinaryDeserializationException(
|
||||||
$"Cannot resolve type '{typeName}' for ObjectWithTypeName at position {context.Position}.",
|
$"Cannot resolve type '{typeName}' for ObjectWithTypeName at position {context.Position}.",
|
||||||
context.Position, null);
|
context.Position, null);
|
||||||
// Next byte is the actual object marker (Object/ObjectRefFirst/etc.) — read it via ReadValue
|
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);
|
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>
|
/// <summary>
|
||||||
/// Object olvasás core implementáció.
|
/// Object olvasás core implementáció.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,12 @@ public static partial class AcBinarySerializer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal bool StringInternEligible;
|
internal bool StringInternEligible;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Next polymorphic type cache index. Assigned sequentially on first polymorphic write per runtime type.
|
||||||
|
/// Used together with TypeMetadataWrapper.PolymorphicSeen/PolymorphicCacheIndex.
|
||||||
|
/// </summary>
|
||||||
|
internal int _nextTypeSlot;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to consume the next write plan entry at the current WriteVisitIndex.
|
/// Tries to consume the next write plan entry at the current WriteVisitIndex.
|
||||||
/// Returns true if the entry matches (duplicate exists at this visit point).
|
/// Returns true if the entry matches (duplicate exists at this visit point).
|
||||||
|
|
@ -301,6 +307,7 @@ public static partial class AcBinarySerializer
|
||||||
WriteVisitIndex = 0;
|
WriteVisitIndex = 0;
|
||||||
_nextWritePlanVisitIndex = int.MaxValue;
|
_nextWritePlanVisitIndex = int.MaxValue;
|
||||||
StringInternEligible = false;
|
StringInternEligible = false;
|
||||||
|
_nextTypeSlot = 0;
|
||||||
|
|
||||||
// Clear write plan string references to avoid GC pinning, keep array if small enough
|
// Clear write plan string references to avoid GC pinning, keep array if small enough
|
||||||
if (_writePlan != null)
|
if (_writePlan != null)
|
||||||
|
|
@ -906,6 +913,61 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Polymorphic Type Prefix
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a polymorphic type prefix when the runtime type differs from the declared property type.
|
||||||
|
/// <para>
|
||||||
|
/// When <paramref name="cachedObjectCacheIndex"/> is -1 (default): PREFIX markers.
|
||||||
|
/// An inner Object/Array/Dict marker follows.
|
||||||
|
/// First type occurrence: ObjectWithTypeName (68) + typename
|
||||||
|
/// Cached type: ObjectWithTypeIndex (70) + typeIndex
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// When <paramref name="cachedObjectCacheIndex"/> >= 0: COMBINED markers.
|
||||||
|
/// Object body follows directly (no inner Object/ObjectRefFirst marker).
|
||||||
|
/// First type occurrence: ObjectWithTypeNameRefFirst (69) + typename + refCacheIndex
|
||||||
|
/// Cached type: ObjectWithTypeIndexRefFirst (71) + typeIndex + refCacheIndex
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void WritePolymorphicPrefix(Type runtimeType, int cachedObjectCacheIndex = -1)
|
||||||
|
{
|
||||||
|
var rtWrapper = GetWrapper(runtimeType);
|
||||||
|
if (!rtWrapper.PolymorphicSeen)
|
||||||
|
{
|
||||||
|
rtWrapper.PolymorphicSeen = true;
|
||||||
|
rtWrapper.PolymorphicCacheIndex = _nextTypeSlot++;
|
||||||
|
if (cachedObjectCacheIndex >= 0)
|
||||||
|
{
|
||||||
|
WriteByte(BinaryTypeCode.ObjectWithTypeNameRefFirst);
|
||||||
|
WriteStringUtf8(runtimeType.FullName!);
|
||||||
|
WriteVarUInt((uint)cachedObjectCacheIndex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteByte(BinaryTypeCode.ObjectWithTypeName);
|
||||||
|
WriteStringUtf8(runtimeType.FullName!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (cachedObjectCacheIndex >= 0)
|
||||||
|
{
|
||||||
|
WriteByte(BinaryTypeCode.ObjectWithTypeIndexRefFirst);
|
||||||
|
WriteVarUInt((uint)rtWrapper.PolymorphicCacheIndex);
|
||||||
|
WriteVarUInt((uint)cachedObjectCacheIndex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteByte(BinaryTypeCode.ObjectWithTypeIndex);
|
||||||
|
WriteVarUInt((uint)rtWrapper.PolymorphicCacheIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region UseMetadata Type Tracking
|
#region UseMetadata Type Tracking
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -248,12 +248,22 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
#region SGen Slot Allocation
|
#region SGen Slot Allocation
|
||||||
|
|
||||||
private static int s_nextWrapperSlot;
|
/// <summary>
|
||||||
|
/// Number of runtime wrapper slots reserved for polymorphic type cache (indices 0..RuntimeSlotCount-1).
|
||||||
|
/// SGen compile-time slots start at RuntimeSlotCount and above.
|
||||||
|
/// Easily modifiable — all code references this constant instead of literal values.
|
||||||
|
/// </summary>
|
||||||
|
internal const int RuntimeSlotCount = BinaryTypeCode.SlotCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Next available wrapper slot index. Starts at RuntimeSlotCount so SGen slots
|
||||||
|
/// don't collide with runtime polymorphic slots (0..RuntimeSlotCount-1).
|
||||||
|
/// </summary>
|
||||||
|
internal static int s_nextWrapperSlot = RuntimeSlotCount + 1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Allocates a unique slot index for SGen wrapper cache.
|
/// Allocates a unique slot index for SGen wrapper cache.
|
||||||
/// Indexes _wrapperSlots array on AcSerializerContextBase.
|
/// Returns RuntimeSlotCount, RuntimeSlotCount+1, RuntimeSlotCount+2, ...
|
||||||
/// Used for: IdentityMap ref tracking (scan pass), MetadataSeen (write pass).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static int AllocateWrapperSlot() => Interlocked.Increment(ref s_nextWrapperSlot) - 1;
|
internal static int AllocateWrapperSlot() => Interlocked.Increment(ref s_nextWrapperSlot) - 1;
|
||||||
|
|
||||||
|
|
@ -574,7 +584,7 @@ public static partial class AcBinarySerializer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var wrapper = context.GetWrapperBySlot(wrapperSlot, type);
|
var wrapper = context.GetWrapper(type, wrapperSlot);
|
||||||
WriteObject(value, wrapper, context, depth);
|
WriteObject(value, wrapper, context, depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -651,8 +661,9 @@ public static partial class AcBinarySerializer
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache).
|
/// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache).
|
||||||
/// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects.
|
/// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects.
|
||||||
|
/// When polyRuntimeType is set, writes polymorphic prefix/combined markers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void WriteValueNonPrimitiveWithWrapper<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
private static void WriteValueNonPrimitiveWithWrapper<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, Type? polyRuntimeType = null)
|
||||||
where TOutput : struct, IBinaryOutputBase
|
where TOutput : struct, IBinaryOutputBase
|
||||||
{
|
{
|
||||||
var type = wrapper.Metadata.SourceType;
|
var type = wrapper.Metadata.SourceType;
|
||||||
|
|
@ -666,6 +677,7 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
if (depth > context.MaxDepth)
|
if (depth > context.MaxDepth)
|
||||||
{
|
{
|
||||||
|
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
|
||||||
context.WriteByte(BinaryTypeCode.Null);
|
context.WriteByte(BinaryTypeCode.Null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -673,6 +685,7 @@ public static partial class AcBinarySerializer
|
||||||
// Handle byte arrays specially (value-like, no reference tracking)
|
// Handle byte arrays specially (value-like, no reference tracking)
|
||||||
if (value is byte[] byteArray)
|
if (value is byte[] byteArray)
|
||||||
{
|
{
|
||||||
|
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
|
||||||
WriteByteArray(byteArray, context);
|
WriteByteArray(byteArray, context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -680,6 +693,7 @@ public static partial class AcBinarySerializer
|
||||||
// Handle dictionaries
|
// Handle dictionaries
|
||||||
if (value is IDictionary dictionary)
|
if (value is IDictionary dictionary)
|
||||||
{
|
{
|
||||||
|
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
|
||||||
WriteDictionary(dictionary, context, depth);
|
WriteDictionary(dictionary, context, depth);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -687,12 +701,13 @@ public static partial class AcBinarySerializer
|
||||||
// Handle collections/arrays
|
// Handle collections/arrays
|
||||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||||
{
|
{
|
||||||
|
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
|
||||||
WriteArray(enumerable, wrapper, context, depth);
|
WriteArray(enumerable, wrapper, context, depth);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle complex objects with single-pass reference tracking
|
// Handle complex objects — combined poly+ref markers handled inside WriteObject
|
||||||
WriteObject(value, wrapper, context, depth);
|
WriteObject(value, wrapper, context, depth, polyRuntimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1058,7 +1073,7 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
#region Complex Type Writers
|
#region Complex Type Writers
|
||||||
|
|
||||||
private static void WriteObject<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
private static void WriteObject<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, Type? polyRuntimeType = null)
|
||||||
where TOutput : struct, IBinaryOutputBase
|
where TOutput : struct, IBinaryOutputBase
|
||||||
{
|
{
|
||||||
var metadata = wrapper.Metadata;
|
var metadata = wrapper.Metadata;
|
||||||
|
|
@ -1089,7 +1104,8 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 2+ occurrence → write ObjectRef (no children, no properties)
|
// 2+ occurrence → write ObjectRef directly (no poly prefix needed —
|
||||||
|
// object already in cache, deser knows the type)
|
||||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||||
context.WriteVarUInt((uint)planEntry.CacheMapIndex);
|
context.WriteVarUInt((uint)planEntry.CacheMapIndex);
|
||||||
return;
|
return;
|
||||||
|
|
@ -1097,10 +1113,12 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marker kiírása:
|
// Marker kiírása — polymorphic vs non-polymorphic paths
|
||||||
// - Cached object first occurrence: ObjectRefFirst/ObjectWithMetadataRefFirst + cacheIndex
|
if (polyRuntimeType != null)
|
||||||
// - Non-cached: Object/ObjectWithMetadata
|
{
|
||||||
if (useMetaForType)
|
WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex);
|
||||||
|
}
|
||||||
|
else if (useMetaForType)
|
||||||
{
|
{
|
||||||
if (cachedObjectCacheIndex >= 0)
|
if (cachedObjectCacheIndex >= 0)
|
||||||
{
|
{
|
||||||
|
|
@ -1122,7 +1140,16 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
context.WriteByte(BinaryTypeCode.Object);
|
// FixObj: assign slot on first occurrence this session
|
||||||
|
if (!wrapper.PolymorphicSeen)
|
||||||
|
{
|
||||||
|
wrapper.PolymorphicSeen = true;
|
||||||
|
wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
|
||||||
|
}
|
||||||
|
if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
|
||||||
|
context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
|
||||||
|
else
|
||||||
|
context.WriteByte(BinaryTypeCode.Object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1191,6 +1218,39 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Polymorphic marker writing — extracted from WriteObject to keep hot path small.
|
||||||
|
/// Cold path: polymorphism is rare, NoInlining call overhead acceptable.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private static void WritePolymorphicMarker<TOutput>(
|
||||||
|
BinarySerializationContext<TOutput> context,
|
||||||
|
Type polyRuntimeType,
|
||||||
|
int cachedObjectCacheIndex)
|
||||||
|
where TOutput : struct, IBinaryOutputBase
|
||||||
|
{
|
||||||
|
if (cachedObjectCacheIndex >= 0)
|
||||||
|
{
|
||||||
|
// Combined poly + RefFirst marker (69/71)
|
||||||
|
context.WritePolymorphicPrefix(polyRuntimeType, cachedObjectCacheIndex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var rtWrapper = context.GetWrapper(polyRuntimeType);
|
||||||
|
if (rtWrapper.PolymorphicSeen && rtWrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
|
||||||
|
{
|
||||||
|
// 2+ poly in this session → FixObj (1 byte)
|
||||||
|
context.WriteByte((byte)rtWrapper.PolymorphicCacheIndex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// First poly in this session → ObjectWithTypeName + assigns slot
|
||||||
|
context.WritePolymorphicPrefix(polyRuntimeType);
|
||||||
|
context.WriteByte(BinaryTypeCode.Object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if a property value is null or default without boxing for value types.
|
/// Checks if a property value is null or default without boxing for value types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1500,14 +1560,11 @@ public static partial class AcBinarySerializer
|
||||||
{
|
{
|
||||||
var runtimeType = value.GetType();
|
var runtimeType = value.GetType();
|
||||||
|
|
||||||
// System.Object declared property → prefix with ObjectWithTypeName marker + TypeName
|
// Polymorphic detection: when declared type ≠ runtime type, pass polyRuntimeType
|
||||||
// so the deserializer can resolve the concrete runtime type.
|
// to WriteValueNonPrimitiveWithWrapper → WriteObject for combined marker handling.
|
||||||
// The normal Object/ObjectRefFirst marker follows as usual.
|
// For collections: normal prefix pattern (68/70 + inner Array/Dict marker).
|
||||||
if (prop.IsObjectDeclaredType && !context.UseMetadata)
|
// For objects: combined markers (69/71) when RefFirst, no prefix for ObjectRef.
|
||||||
{
|
var isPoly = !context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType;
|
||||||
context.WriteByte(BinaryTypeCode.ObjectWithTypeName);
|
|
||||||
context.WriteStringUtf8(runtimeType.AssemblyQualifiedName!);
|
|
||||||
}
|
|
||||||
|
|
||||||
var complexIdx = prop.ComplexPropertyIndex;
|
var complexIdx = prop.ComplexPropertyIndex;
|
||||||
if (complexIdx >= 0)
|
if (complexIdx >= 0)
|
||||||
|
|
@ -1518,11 +1575,12 @@ public static partial class AcBinarySerializer
|
||||||
propWrapper = context.GetWrapper(runtimeType);
|
propWrapper = context.GetWrapper(runtimeType);
|
||||||
parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper);
|
parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper);
|
||||||
}
|
}
|
||||||
WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth);
|
WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth, isPoly ? runtimeType : null);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Non-complex in default case (nullable value type, etc.)
|
// Non-complex in default case (nullable value type, etc.)
|
||||||
|
if (isPoly) context.WritePolymorphicPrefix(runtimeType);
|
||||||
WriteValueNonPrimitive(value, runtimeType, context, depth);
|
WriteValueNonPrimitive(value, runtimeType, context, depth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,15 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsObjectDeclaredType { get; }
|
public bool IsObjectDeclaredType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when declared property type is a non-sealed reference type (not string).
|
||||||
|
/// Polymorphism is possible: runtime type may differ from declared type.
|
||||||
|
/// Covers object, interface, abstract, and non-sealed class properties.
|
||||||
|
/// When true, serializer checks GetType() != PropertyType and writes polymorphic prefix.
|
||||||
|
/// When false (sealed, value type, string): 0 overhead, no check needed.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsPolymorphicCandidate { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cached [AcStringIntern] attribute value for this property.
|
/// Cached [AcStringIntern] attribute value for this property.
|
||||||
/// null = no attribute (follow global StringInterningMode)
|
/// null = no attribute (follow global StringInterningMode)
|
||||||
|
|
@ -66,6 +75,9 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||||
{
|
{
|
||||||
IsStringCollectionProperty = IsStringCollection(prop.PropertyType);
|
IsStringCollectionProperty = IsStringCollection(prop.PropertyType);
|
||||||
IsObjectDeclaredType = prop.PropertyType == typeof(object);
|
IsObjectDeclaredType = prop.PropertyType == typeof(object);
|
||||||
|
IsPolymorphicCandidate = !prop.PropertyType.IsSealed
|
||||||
|
&& !prop.PropertyType.IsValueType
|
||||||
|
&& prop.PropertyType != typeof(string);
|
||||||
|
|
||||||
// All typed getters are initialized in PropertyAccessorBase
|
// All typed getters are initialized in PropertyAccessorBase
|
||||||
if (enableInternString && (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty))
|
if (enableInternString && (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty))
|
||||||
|
|
|
||||||
|
|
@ -4,108 +4,109 @@ namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Binary type codes for serialization.
|
/// Binary type codes for serialization.
|
||||||
/// Designed for fast switch dispatch and compact storage.
|
/// Markers 0..(SlotCount-1) are reserved for object type slot indices (FixObj).
|
||||||
/// Lower 5 bits = type code (0-31)
|
/// All other type markers are defined relative to SlotCount.
|
||||||
/// Upper 3 bits = flags (interned, reference, has-type-info)
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class BinaryTypeCode
|
internal static class BinaryTypeCode
|
||||||
{
|
{
|
||||||
// Primitive types (0-15)
|
/// <summary>
|
||||||
public const byte Null = 0;
|
/// Number of reserved FixObj slot markers (0..SlotCount-1).
|
||||||
public const byte True = 1;
|
/// When a marker byte is less than SlotCount, it represents an object
|
||||||
public const byte False = 2;
|
/// whose type wrapper is cached at _wrapperSlots[marker].
|
||||||
public const byte Int8 = 3;
|
/// All type markers are defined relative to this constant.
|
||||||
public const byte UInt8 = 4;
|
/// </summary>
|
||||||
public const byte Int16 = 5;
|
public const int SlotCount = 64;
|
||||||
public const byte UInt16 = 6;
|
|
||||||
public const byte Int32 = 7;
|
|
||||||
public const byte UInt32 = 8;
|
|
||||||
public const byte Int64 = 9;
|
|
||||||
public const byte UInt64 = 10;
|
|
||||||
public const byte Float32 = 11;
|
|
||||||
public const byte Float64 = 12;
|
|
||||||
public const byte Decimal = 13;
|
|
||||||
public const byte Char = 14;
|
|
||||||
|
|
||||||
// String types (16-19)
|
// Complex types (SlotCount + 0..7)
|
||||||
public const byte String = 16; // Inline UTF8 string (non-interned)
|
public const byte Object = SlotCount + 0; // 64 — Start of object (fallback when >SlotCount types)
|
||||||
public const byte StringInterned = 17; // Reference to interned string by index (2+ occurrence)
|
public const byte ObjectRef = SlotCount + 1; // 65 — Reference to previously serialized object (2+ occurrence)
|
||||||
public const byte StringEmpty = 18; // Empty string marker
|
public const byte Array = SlotCount + 2; // 66 — Start of array/list
|
||||||
public const byte StringInternFirst = 19; // First occurrence of interned string - read content + register in cache
|
public const byte Dictionary = SlotCount + 3; // 67 — Start of dictionary
|
||||||
|
public const byte ByteArray = SlotCount + 4; // 68 — Optimized byte[] storage
|
||||||
|
public const byte ObjectWithMetadata = SlotCount + 5; // 69 — Object with metadata (UseMetadata mode)
|
||||||
|
public const byte ObjectRefFirst = SlotCount + 6; // 70 — First occurrence of tracked object (ref handling enabled)
|
||||||
|
public const byte ObjectWithMetadataRefFirst = SlotCount + 7; // 71 — First occurrence of tracked object with metadata
|
||||||
|
|
||||||
// Date/Time types (20-23)
|
// Polymorphic object markers (SlotCount + 8..11)
|
||||||
public const byte DateTime = 20;
|
// Used when declared property type != runtime type AND UseMetadata=false.
|
||||||
public const byte DateTimeOffset = 21;
|
//
|
||||||
public const byte TimeSpan = 22;
|
// PREFIX markers (inner Object/Array/Dict marker follows):
|
||||||
public const byte Guid = 23;
|
// [ObjectWithTypeName] [UTF8 typeName] [Object | Array | ...] [body...]
|
||||||
|
// [ObjectWithTypeIndex] [VarUInt typeIndex] [Object | Array | ...] [body...]
|
||||||
|
//
|
||||||
|
// COMBINED markers (no inner marker — object body follows directly):
|
||||||
|
// [ObjectWithTypeNameRefFirst] [UTF8 typeName] [VarUInt refCacheIndex] [properties...]
|
||||||
|
// [ObjectWithTypeIndexRefFirst] [VarUInt typeIndex] [VarUInt refCacheIndex] [properties...]
|
||||||
|
//
|
||||||
|
// ObjectRef for 2+ occurrence: written directly, NO poly prefix needed.
|
||||||
|
public const byte ObjectWithTypeName = SlotCount + 8; // 72
|
||||||
|
public const byte ObjectWithTypeNameRefFirst = SlotCount + 9; // 73
|
||||||
|
public const byte ObjectWithTypeIndex = SlotCount + 10; // 74
|
||||||
|
public const byte ObjectWithTypeIndexRefFirst = SlotCount + 11; // 75
|
||||||
|
|
||||||
// Enum (24)
|
// Primitive types (SlotCount + 12..26)
|
||||||
public const byte Enum = 24;
|
public const byte Null = SlotCount + 12; // 76
|
||||||
|
public const byte True = SlotCount + 13; // 77
|
||||||
|
public const byte False = SlotCount + 14; // 78
|
||||||
|
public const byte Int8 = SlotCount + 15; // 79
|
||||||
|
public const byte UInt8 = SlotCount + 16; // 80
|
||||||
|
public const byte Int16 = SlotCount + 17; // 81
|
||||||
|
public const byte UInt16 = SlotCount + 18; // 82
|
||||||
|
public const byte Int32 = SlotCount + 19; // 83
|
||||||
|
public const byte UInt32 = SlotCount + 20; // 84
|
||||||
|
public const byte Int64 = SlotCount + 21; // 85
|
||||||
|
public const byte UInt64 = SlotCount + 22; // 86
|
||||||
|
public const byte Float32 = SlotCount + 23; // 87
|
||||||
|
public const byte Float64 = SlotCount + 24; // 88
|
||||||
|
public const byte Decimal = SlotCount + 25; // 89
|
||||||
|
public const byte Char = SlotCount + 26; // 90
|
||||||
|
|
||||||
// Complex types (25-31)
|
// String types (SlotCount + 27..30)
|
||||||
public const byte Object = 25; // Start of object (non-tracked OR first occurrence when ref tracking)
|
public const byte String = SlotCount + 27; // 91 — Inline UTF8 string (non-interned)
|
||||||
//public const byte ObjectEnd = 26; // UNUSED — property count is known at compile-time (SGen) or reflection-time (runtime), no end marker needed
|
public const byte StringInterned = SlotCount + 28; // 92 — Reference to interned string by index (2+ occurrence)
|
||||||
public const byte ObjectRef = 27; // Reference to previously serialized object (2+ occurrence)
|
public const byte StringEmpty = SlotCount + 29; // 93 — Empty string marker
|
||||||
public const byte Array = 28; // Start of array/list
|
public const byte StringInternFirst = SlotCount + 30; // 94 — First occurrence of interned string
|
||||||
public const byte Dictionary = 29; // Start of dictionary
|
|
||||||
public const byte ByteArray = 30; // Optimized byte[] storage
|
|
||||||
public const byte ObjectWithMetadata = 31; // Object with metadata (UseMetadata mode, non-tracked OR first occurrence)
|
|
||||||
|
|
||||||
// Extended markers for first occurrence tracking (66-67, after FixStr range)
|
// Date/Time types (SlotCount + 31..34)
|
||||||
public const byte ObjectRefFirst = 66; // First occurrence of tracked object (ref handling enabled)
|
public const byte DateTime = SlotCount + 31; // 95
|
||||||
public const byte ObjectWithMetadataRefFirst = 67; // First occurrence of tracked object with metadata
|
public const byte DateTimeOffset = SlotCount + 32; // 96
|
||||||
|
public const byte TimeSpan = SlotCount + 33; // 97
|
||||||
|
public const byte Guid = SlotCount + 34; // 98
|
||||||
|
|
||||||
// Polymorphic object markers (68-69): self-describing object for polymorphic properties.
|
// Enum (SlotCount + 35)
|
||||||
// Used when declared property type ≠ runtime type AND UseMetadata=false.
|
public const byte Enum = SlotCount + 35; // 99
|
||||||
// Serializer writes runtime type name inline so deserializer can resolve the concrete type.
|
|
||||||
// Format: [ObjectWithTypeName (68)] [VarUInt typeNameLen] [UTF8 typeName] [properties...] [ObjectEnd]
|
|
||||||
// Format: [ObjectWithTypeNameRefFirst (69)] [VarUInt cacheIndex] [VarUInt typeNameLen] [UTF8 typeName] [properties...] [ObjectEnd]
|
|
||||||
public const byte ObjectWithTypeName = 68;
|
|
||||||
public const byte ObjectWithTypeNameRefFirst = 69;
|
|
||||||
|
|
||||||
// Special markers (32+, for header/meta)
|
// Legacy header markers (SlotCount + 36..37)
|
||||||
// Header flags byte structure (for values >= 64):
|
public const byte MetadataHeader = SlotCount + 36; // 100 — Binary has metadata section (legacy, implies HasReferenceHandling=true)
|
||||||
// Bit 0 (0x01): HasMetadata
|
public const byte NoMetadataHeader = SlotCount + 37; // 101 — Binary has no metadata (legacy, implies HasReferenceHandling=true)
|
||||||
// Bit 1 (0x02): HasReferenceHandling
|
|
||||||
// Values 32, 33 are legacy for backward compatibility
|
|
||||||
public const byte MetadataHeader = 32; // Binary has metadata section (legacy, implies HasReferenceHandling=true)
|
|
||||||
public const byte NoMetadataHeader = 33; // Binary has no metadata (legacy, implies HasReferenceHandling=true)
|
|
||||||
|
|
||||||
// FixStr range: 34-65 (32 values for strings 0-31 bytes)
|
// Property skip marker (SlotCount + 38)
|
||||||
|
public const byte PropertySkip = SlotCount + 38; // 102 — Marks a property with default/null value (skipped during serialization)
|
||||||
|
|
||||||
|
// FixStr range: SlotCount + 39 .. SlotCount + 70 (32 values for strings 0-31 bytes)
|
||||||
// FixStr encoding: FixStrBase + length (0-31)
|
// FixStr encoding: FixStrBase + length (0-31)
|
||||||
// This saves 1 byte for short strings by combining type + length in single byte
|
// This saves 1 byte for short strings by combining type + length in single byte
|
||||||
public const byte FixStrBase = 34; // Base value for FixStr (0xA0 in MessagePack style, but we use 34)
|
public const byte FixStrBase = SlotCount + 39; // 103
|
||||||
public const byte FixStrMax = 65; // FixStrBase + 31 = maximum FixStr code
|
public const byte FixStrMax = FixStrBase + 31; // 134
|
||||||
public const int FixStrMaxLength = 31; // Maximum string length encodable as FixStr
|
public const int FixStrMaxLength = 31;
|
||||||
|
|
||||||
// New flag-based header markers (48+) - moved to after FixStr range
|
// Flag-based header markers (must be 16-aligned for flag bits in lower nibble)
|
||||||
// Note: FixStr range 34-65 overlaps with old HeaderFlagsBase, so headers use version byte prefix
|
|
||||||
// Header byte structure: (marker & 0xF0) == HeaderFlagsBase, flags in (marker & 0x0F)
|
// Header byte structure: (marker & 0xF0) == HeaderFlagsBase, flags in (marker & 0x0F)
|
||||||
public const byte HeaderFlagsBase = 48; // Base value for flag-based headers (0x30)
|
public const byte HeaderFlagsBase = 144; // 0x90 — next 16-aligned value after FixStrMax
|
||||||
public const byte HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included
|
public const byte HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included
|
||||||
// Reference handling uses 2 separate bits:
|
// Reference handling uses 2 separate bits:
|
||||||
// Bit 1 (0x02): OnlyId - reference handling for IId objects only
|
// Bit 1 (0x02): OnlyId - reference handling for IId objects only
|
||||||
// Bit 2 (0x04): All - reference handling for all objects (includes OnlyId)
|
// Bit 2 (0x04): All - reference handling for all objects (includes OnlyId)
|
||||||
// None = both false, OnlyId = 0x02, All = 0x06 (both bits set)
|
// None = both false, OnlyId = 0x02, All = 0x06 (both bits set)
|
||||||
public const byte HeaderFlag_RefHandling_OnlyId = 0x02;
|
public const byte HeaderFlag_RefHandling_OnlyId = 0x02;
|
||||||
public const byte HeaderFlag_RefHandling_All = 0x04;
|
public const byte HeaderFlag_RefHandling_All = 0x04;
|
||||||
public const byte HeaderFlag_HasFooterPosition = 0x08; // Bit 3: 4-byte footer position follows flags (legacy)
|
public const byte HeaderFlag_HasFooterPosition = 0x08; // Bit 3: 4-byte footer position follows flags (legacy)
|
||||||
public const byte HeaderFlag_HasCacheCount = 0x08; // Bit 3 (reused): VarUInt cache count follows flags (new marker-based format)
|
public const byte HeaderFlag_HasCacheCount = 0x08; // Bit 3 (reused): VarUInt cache count follows flags (new marker-based format)
|
||||||
|
|
||||||
// Compact integer variants (for VarInt optimization)
|
// Compact integer variants (unchanged)
|
||||||
public const byte Int32Tiny = 192; // -16 to 63 stored in single byte (value = code - 192 - 16)
|
public const byte Int32Tiny = 192; // -16 to 47 stored in single byte (value = code - 192 - 16)
|
||||||
public const byte Int32TinyMax = 255; // Upper bound for tiny int (192 + 64 - 1 = 255)
|
public const byte Int32TinyMax = 255; // Upper bound for tiny int (192 + 64 - 1 = 255)
|
||||||
|
|
||||||
// Property skip marker (for single-pass serialization optimization)
|
|
||||||
// CRITICAL: Must be in the "reserved" range 67-191 (after FixStr, before TinyInt)
|
|
||||||
// AND must not conflict with any other type codes.
|
|
||||||
// Using 191 (0xBF) - the highest value before TinyInt range starts at 192.
|
|
||||||
// This ensures it won't be confused with:
|
|
||||||
// - Primitive types (0-31)
|
|
||||||
// - FixStr (34-65)
|
|
||||||
// - TinyInt values (192-255)
|
|
||||||
public const byte PropertySkip = 191; // Marks a property with default/null value (skipped during serialization)
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if type code represents a reference (string or object).
|
/// Check if type code represents a reference (string or object).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,21 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal bool MetadataSeen;
|
internal bool MetadataSeen;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Polymorphic type tracking: has this runtime type been written as a polymorphic prefix?
|
||||||
|
/// false = first occurrence → write ObjectWithTypeName (68) + full type name.
|
||||||
|
/// true = repeated → write ObjectWithTypeIndex (70) + PolymorphicCacheIndex.
|
||||||
|
/// Same pattern as MetadataSeen. Reset by ResetTracking.
|
||||||
|
/// </summary>
|
||||||
|
internal bool PolymorphicSeen;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unified type slot index for FixObj system. Used by both poly and non-poly types.
|
||||||
|
/// -1 = not yet assigned. Set together with PolymorphicSeen = true.
|
||||||
|
/// Reset by ResetTracking (per-session, slot order depends on stream encounter order).
|
||||||
|
/// </summary>
|
||||||
|
internal int PolymorphicCacheIndex = -1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UseMetadata cachemap: source property index → target PropertySetter.
|
/// UseMetadata cachemap: source property index → target PropertySetter.
|
||||||
/// Per-context (wrapper-szintű), mert futásonként eltérő source type-pal találkozhat.
|
/// Per-context (wrapper-szintű), mert futásonként eltérő source type-pal találkozhat.
|
||||||
|
|
@ -193,6 +208,8 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
||||||
public void ResetTracking(bool preRentBuckets = false)
|
public void ResetTracking(bool preRentBuckets = false)
|
||||||
{
|
{
|
||||||
MetadataSeen = false;
|
MetadataSeen = false;
|
||||||
|
PolymorphicSeen = false;
|
||||||
|
PolymorphicCacheIndex = -1;
|
||||||
CacheMap = null;
|
CacheMap = null;
|
||||||
|
|
||||||
// Options may change between sessions (pool reuse) → rebuild on next scan
|
// Options may change between sessions (pool reuse) → rebuild on next scan
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue