diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4cf40ad..7fc4202 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -44,7 +44,9 @@ "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.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)" ] } } diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 3ec0eca..96cc02d 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -36,12 +36,14 @@ public static class Program // Serializer name constants private const string SerializerMessagePack = "MessagePack"; private const string SerializerAcBinaryDefault = "AcBinary (Default)"; + private const string SerializerAcBinaryDefaultNoSgen = "AcBinary (Def, NoSgen)"; private const string SerializerAcBinaryNoRef = "AcBinary (NoRef)"; private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)"; + private const string SerializerAcBinaryFastNoSgen = "AcBinary (Fast, NoSgen)"; private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)"; private const string SerializerMemoryPack = "MemoryPack"; - private const string SerializerAcBinaryBufferWriter = "AcBinary (BufferWriter)"; - private const string SerializerSystemTextJson = "System.Text.Json"; + //private const string SerializerAcBinaryBufferWriter = "AcBinary (BufferWriter)"; + //private const string SerializerSystemTextJson = "System.Text.Json"; private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); @@ -208,20 +210,34 @@ public static class Program private static List 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 { // 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, 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, 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), + // MemoryPack new MemoryPackBenchmark(testData.Order, SerializerMemoryPack), @@ -229,10 +245,10 @@ public static class Program new MessagePackBenchmark(testData.Order, SerializerMessagePack), // AcBinary BufferWriter - new AcBinaryBufferWriterBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryBufferWriter), + //new AcBinaryBufferWriterBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryBufferWriter), // System.Text.Json - new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson) + //new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson) }; } diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index 87199b1..57ab9ff 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -583,7 +583,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine(); sb.AppendLine(" if (context.HasRefHandling)"); 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($" if (!wrapper.{tryTrackMethod}(obj.Id, visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))"); sb.AppendLine(" {"); @@ -600,7 +600,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine(); sb.AppendLine(" if (context.HasAllRefHandling)"); 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(" if (!wrapper.TryTrackInt32(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value), visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))"); sb.AppendLine(" {"); @@ -1227,7 +1227,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator else { // No ref, but metadata possible → UseMetadata branch only - sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));"); + sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapper(typeof({p.TypeNameForTypeof}), {writer}.s_wrapperSlot));"); sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); @@ -1267,7 +1267,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator else { // Full path: ref tracking + metadata - sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));"); + sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapper(typeof({p.TypeNameForTypeof}), {writer}.s_wrapperSlot));"); var refGuard = p.IsIId ? "context.HasRefHandling" : "context.HasAllRefHandling"; @@ -1398,7 +1398,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator else { // No ref, but metadata possible → UseMetadata branch only - sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));"); + sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapper(typeof({p.ElementFullTypeName}), {writer}.s_wrapperSlot));"); sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); @@ -1442,7 +1442,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator else { // Full path: ref tracking + metadata - sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));"); + sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapper(typeof({p.ElementFullTypeName}), {writer}.s_wrapperSlot));"); sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)"); @@ -1655,7 +1655,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator else { // No ref, metadata possible - sb.AppendLine($"{i} var isFirstMeta_dv_{s} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({valType})));"); + sb.AppendLine($"{i} var isFirstMeta_dv_{s} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapper(typeof({valType}), {writer}.s_wrapperSlot));"); sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); @@ -1698,7 +1698,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator else { // Full path: ref tracking + metadata - sb.AppendLine($"{i} var isFirstMeta_dv_{s} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({valType})));"); + sb.AppendLine($"{i} var isFirstMeta_dv_{s} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapper(typeof({valType}), {writer}.s_wrapperSlot));"); sb.AppendLine($"{i} if ({dvRefGuard} && context.TryConsumeWritePlanEntry(out var dpe_{s}))"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if (!dpe_{s}.IsFirst)"); diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerBasicTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerBasicTests.cs index 47dd827..c3695ad 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerBasicTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerBasicTests.cs @@ -13,7 +13,7 @@ public class AcBinarySerializerBasicTests { var result = AcBinarySerializer.Serialize(null); Assert.AreEqual(1, result.Length); - Assert.AreEqual((byte)0, result[0]); + Assert.AreEqual(BinaryTypeCode.Null, result[0]); } [TestMethod] diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs index 8a5c84d..96f1a83 100644 --- a/AyCode.Core/Serializers/AcSerializerContextBase.cs +++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs @@ -55,8 +55,9 @@ public abstract class AcSerializerContextBase private readonly Dictionary> _wrappers = new(); /// - /// Slot-indexed wrapper cache for SGen types. Indexed by AllocateWrapperSlot() value. - /// Avoids dictionary lookup — direct array access for types with compile-time known slot index. + /// Slot-indexed wrapper cache. Shared between SGen types (RuntimeSlotCount+) and + /// 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. /// private TypeMetadataWrapper?[]? _wrapperSlots; @@ -107,33 +108,35 @@ public abstract class AcSerializerContextBase } /// - /// Gets or creates a wrapper for the specified type using a pre-allocated slot index. - /// SGen types call this with their compile-time known slot — avoids dictionary lookup. - /// First call per slot per context: falls back to GetWrapper + stores in slot array. - /// Subsequent calls: direct array index (~1-2ns vs ~15-25ns dictionary lookup). + /// Gets or creates a wrapper for the specified type using a slot index. + /// Slot checked first (array access ~1-2ns), falls back to dictionary if slot empty. + /// Used by both SGen (compile-time slot) and runtime polymorphic cache (0..RuntimeSlotCount-1). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TypeMetadataWrapper GetWrapperBySlot(int slot, Type type) + public TypeMetadataWrapper GetWrapper(Type type, int slotIndex) { - var slots = _wrapperSlots; - if (slots != null && slot < slots.Length) - { - var wrapper = slots[slot]; - if (wrapper != null) - return wrapper; - } + var slots = _wrapperSlots!; + var wrapper = slots[slotIndex]; + if (wrapper != null) + return wrapper; - return GetWrapperBySlotSlow(slot, type); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private TypeMetadataWrapper GetWrapperBySlotSlow(int slot, Type type) - { - var wrapper = GetWrapper(type); - _wrapperSlots![slot] = wrapper; + wrapper = GetWrapper(type); + slots[slotIndex] = wrapper; return wrapper; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected TypeMetadataWrapper? GetWrapperSlot(int slotIndex) + => _wrapperSlots![slotIndex]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void SetWrapperSlot(int slotIndex, TypeMetadataWrapper wrapper) + => _wrapperSlots![slotIndex] = wrapper; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void ClearWrapperSlots(int count) + => Array.Clear(_wrapperSlots!, 0, 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. diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index 4e1c736..b8ebbe1 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Threading; namespace AyCode.Core.Serializers.Binaries; @@ -104,6 +105,19 @@ public static partial class AcBinaryDeserializer private MetadataEntry[]? _metadataEntries; private int _metadataEntryCount; + /// + /// Runtime polymorphic slot counter. Indices 0..RuntimeSlotCount-1 stored in _wrapperSlots. + /// Overflow (index >= RuntimeSlotCount) stored in _polyOverflow. + /// + internal int _nextRuntimeSlot; + + /// + /// Overflow array for polymorphic types beyond RuntimeSlotCount. + /// Only allocated when >RuntimeSlotCount distinct poly types (very rare). + /// Indexed by (polyIndex - RuntimeSlotCount). + /// + private TypeMetadataWrapper[]? _polyOverflow; + /// /// A metadata entry for the deserializer. /// @@ -121,6 +135,7 @@ public static partial class AcBinaryDeserializer public BinaryDeserializationContext() { + InitializeWrapperSlots(Volatile.Read(ref AcBinarySerializer.s_nextWrapperSlot)); } /// @@ -367,6 +382,54 @@ public static partial class AcBinaryDeserializer #endregion + #region Polymorphic Wrapper Cache + + /// + /// 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). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void RegisterPolymorphicWrapper(TypeMetadataWrapper wrapper) + { + var slot = _nextRuntimeSlot++; + if (slot < AcBinarySerializer.RuntimeSlotCount) + { + SetWrapperSlot(slot, wrapper); + } + else + { + RegisterPolymorphicWrapperOverflow(wrapper, slot - AcBinarySerializer.RuntimeSlotCount); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void RegisterPolymorphicWrapperOverflow(TypeMetadataWrapper wrapper, int overflowIndex) + { + if (_polyOverflow == null || overflowIndex >= _polyOverflow.Length) + { + var newSize = _polyOverflow == null ? 4 : _polyOverflow.Length * 2; + var newArray = new TypeMetadataWrapper[newSize]; + if (_polyOverflow != null) + Array.Copy(_polyOverflow, newArray, overflowIndex); + _polyOverflow = newArray; + } + _polyOverflow[overflowIndex] = wrapper; + } + + /// + /// Gets a previously registered polymorphic wrapper by index (called by ReadObjectWithTypeIndex/RefFirst). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal TypeMetadataWrapper GetPolymorphicWrapper(int index) + { + if (index < AcBinarySerializer.RuntimeSlotCount) + return GetWrapperSlot(index)!; + return _polyOverflow![index - AcBinarySerializer.RuntimeSlotCount]!; + } + + #endregion + #region Reset & Clear public override void Reset(AcBinarySerializerOptions options) @@ -381,6 +444,17 @@ public static partial class AcBinaryDeserializer _metadataEntryCount = 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 _stringCache?.Clear(); diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 024c7ef..b2d2fbf 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -102,17 +102,26 @@ public static partial class AcBinaryDeserializer 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 (34-65) + // Register FixStr readers for (byte code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++) { var length = BinaryTypeCode.DecodeFixStrLength(code); readers[code] = CreateFixStrReader(length); } + // Register FixObj slot readers (0..SlotCount-1) + for (int slot = 0; slot < BinaryTypeCode.SlotCount; slot++) + { + readers[slot] = CreateFixObjReader(slot); + } + return readers; } } @@ -131,6 +140,16 @@ public static partial class AcBinaryDeserializer return (ctx, _, _) => ctx.ReadStringUtf8(length); } + /// + /// Creates a reader for FixObj slot (0..SlotCount-1). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TypeReader CreateFixObjReader(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 @@ -908,16 +927,16 @@ public static partial class AcBinaryDeserializer var typeCode = context.ReadByte(); - // Handle null first - if (typeCode == BinaryTypeCode.Null) return null; - - // Handle tiny int (most common case for small integers) + // 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)) { @@ -1124,6 +1143,36 @@ public static partial class AcBinaryDeserializer return context.GetInternedObject(cacheIndex); } + /// + /// 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). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? ReadObjectFromSlot( + BinaryDeserializationContext 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); + } + /// /// Object olvasása (nem tracked, vagy UseMetadata nélkül). /// Wire format: [Object][props...] @@ -1147,10 +1196,10 @@ public static partial class AcBinaryDeserializer } /// - /// Polymorphic object prefix: declared property type is System.Object. - /// Wire format: [ObjectWithTypeName (68)] [TypeName string] [Object (25) or ObjectRefFirst (66) ...] [props...] - /// Reads the runtime type name, resolves it, then delegates to ReadValue with the resolved type - /// so the next marker (Object/ObjectRefFirst/etc.) is processed normally. + /// 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. /// private static object? ReadObjectWithTypeName(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase @@ -1160,10 +1209,59 @@ public static partial class AcBinaryDeserializer ?? throw new AcBinaryDeserializationException( $"Cannot resolve type '{typeName}' for ObjectWithTypeName at position {context.Position}.", 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); } + /// + /// 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. + /// + private static object? ReadObjectWithTypeNameRefFirst(BinaryDeserializationContext 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); + } + + /// + /// 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. + /// + private static object? ReadObjectWithTypeIndex(BinaryDeserializationContext 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); + } + + /// + /// 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. + /// + private static object? ReadObjectWithTypeIndexRefFirst(BinaryDeserializationContext 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); + } + /// /// Object olvasás core implementáció. /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 26ea2eb..5f2b5c2 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -160,6 +160,12 @@ public static partial class AcBinarySerializer /// internal bool StringInternEligible; + /// + /// Next polymorphic type cache index. Assigned sequentially on first polymorphic write per runtime type. + /// Used together with TypeMetadataWrapper.PolymorphicSeen/PolymorphicCacheIndex. + /// + internal int _nextTypeSlot; + /// /// Tries to consume the next write plan entry at the current WriteVisitIndex. /// Returns true if the entry matches (duplicate exists at this visit point). @@ -301,6 +307,7 @@ public static partial class AcBinarySerializer WriteVisitIndex = 0; _nextWritePlanVisitIndex = int.MaxValue; StringInternEligible = false; + _nextTypeSlot = 0; // Clear write plan string references to avoid GC pinning, keep array if small enough if (_writePlan != null) @@ -906,6 +913,61 @@ public static partial class AcBinarySerializer #endregion + #region Polymorphic Type Prefix + + /// + /// Writes a polymorphic type prefix when the runtime type differs from the declared property type. + /// + /// When is -1 (default): PREFIX markers. + /// An inner Object/Array/Dict marker follows. + /// First type occurrence: ObjectWithTypeName (68) + typename + /// Cached type: ObjectWithTypeIndex (70) + typeIndex + /// + /// + /// When >= 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 + /// + /// + [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 /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index dc55d1c..1647e58 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -248,12 +248,22 @@ public static partial class AcBinarySerializer #region SGen Slot Allocation - private static int s_nextWrapperSlot; + /// + /// 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. + /// + internal const int RuntimeSlotCount = BinaryTypeCode.SlotCount; + + /// + /// Next available wrapper slot index. Starts at RuntimeSlotCount so SGen slots + /// don't collide with runtime polymorphic slots (0..RuntimeSlotCount-1). + /// + internal static int s_nextWrapperSlot = RuntimeSlotCount + 1; /// /// Allocates a unique slot index for SGen wrapper cache. - /// Indexes _wrapperSlots array on AcSerializerContextBase. - /// Used for: IdentityMap ref tracking (scan pass), MetadataSeen (write pass). + /// Returns RuntimeSlotCount, RuntimeSlotCount+1, RuntimeSlotCount+2, ... /// internal static int AllocateWrapperSlot() => Interlocked.Increment(ref s_nextWrapperSlot) - 1; @@ -574,7 +584,7 @@ public static partial class AcBinarySerializer return; } - var wrapper = context.GetWrapperBySlot(wrapperSlot, type); + var wrapper = context.GetWrapper(type, wrapperSlot); WriteObject(value, wrapper, context, depth); } @@ -651,8 +661,9 @@ public static partial class AcBinarySerializer /// /// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache). /// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects. + /// When polyRuntimeType is set, writes polymorphic prefix/combined markers. /// - private static void WriteValueNonPrimitiveWithWrapper(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) + private static void WriteValueNonPrimitiveWithWrapper(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, Type? polyRuntimeType = null) where TOutput : struct, IBinaryOutputBase { var type = wrapper.Metadata.SourceType; @@ -666,6 +677,7 @@ public static partial class AcBinarySerializer if (depth > context.MaxDepth) { + if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType); context.WriteByte(BinaryTypeCode.Null); return; } @@ -673,6 +685,7 @@ public static partial class AcBinarySerializer // Handle byte arrays specially (value-like, no reference tracking) if (value is byte[] byteArray) { + if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType); WriteByteArray(byteArray, context); return; } @@ -680,6 +693,7 @@ public static partial class AcBinarySerializer // Handle dictionaries if (value is IDictionary dictionary) { + if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType); WriteDictionary(dictionary, context, depth); return; } @@ -687,12 +701,13 @@ public static partial class AcBinarySerializer // Handle collections/arrays if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) { + if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType); WriteArray(enumerable, wrapper, context, depth); return; } - // Handle complex objects with single-pass reference tracking - WriteObject(value, wrapper, context, depth); + // Handle complex objects — combined poly+ref markers handled inside WriteObject + WriteObject(value, wrapper, context, depth, polyRuntimeType); } /// @@ -1058,7 +1073,7 @@ public static partial class AcBinarySerializer #region Complex Type Writers - private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) + private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, Type? polyRuntimeType = null) where TOutput : struct, IBinaryOutputBase { var metadata = wrapper.Metadata; @@ -1089,7 +1104,8 @@ public static partial class AcBinarySerializer } 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.WriteVarUInt((uint)planEntry.CacheMapIndex); return; @@ -1097,10 +1113,12 @@ public static partial class AcBinarySerializer } } - // Marker kiírása: - // - Cached object first occurrence: ObjectRefFirst/ObjectWithMetadataRefFirst + cacheIndex - // - Non-cached: Object/ObjectWithMetadata - if (useMetaForType) + // Marker kiírása — polymorphic vs non-polymorphic paths + if (polyRuntimeType != null) + { + WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex); + } + else if (useMetaForType) { if (cachedObjectCacheIndex >= 0) { @@ -1122,7 +1140,16 @@ public static partial class AcBinarySerializer } 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 } } + /// + /// Polymorphic marker writing — extracted from WriteObject to keep hot path small. + /// Cold path: polymorphism is rare, NoInlining call overhead acceptable. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WritePolymorphicMarker( + BinarySerializationContext 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); + } + } + } + /// /// Checks if a property value is null or default without boxing for value types. /// @@ -1500,14 +1560,11 @@ public static partial class AcBinarySerializer { var runtimeType = value.GetType(); - // System.Object declared property → prefix with ObjectWithTypeName marker + TypeName - // so the deserializer can resolve the concrete runtime type. - // The normal Object/ObjectRefFirst marker follows as usual. - if (prop.IsObjectDeclaredType && !context.UseMetadata) - { - context.WriteByte(BinaryTypeCode.ObjectWithTypeName); - context.WriteStringUtf8(runtimeType.AssemblyQualifiedName!); - } + // Polymorphic detection: when declared type ≠ runtime type, pass polyRuntimeType + // to WriteValueNonPrimitiveWithWrapper → WriteObject for combined marker handling. + // For collections: normal prefix pattern (68/70 + inner Array/Dict marker). + // For objects: combined markers (69/71) when RefFirst, no prefix for ObjectRef. + var isPoly = !context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType; var complexIdx = prop.ComplexPropertyIndex; if (complexIdx >= 0) @@ -1518,11 +1575,12 @@ public static partial class AcBinarySerializer propWrapper = context.GetWrapper(runtimeType); parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper); } - WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth); + WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth, isPoly ? runtimeType : null); } else { // Non-complex in default case (nullable value type, etc.) + if (isPoly) context.WritePolymorphicPrefix(runtimeType); WriteValueNonPrimitive(value, runtimeType, context, depth); } } diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs index ac453e7..71e029f 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs @@ -34,6 +34,15 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase /// public bool IsObjectDeclaredType { get; } + /// + /// 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. + /// + public bool IsPolymorphicCandidate { get; } + /// /// Cached [AcStringIntern] attribute value for this property. /// null = no attribute (follow global StringInterningMode) @@ -66,6 +75,9 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase { IsStringCollectionProperty = IsStringCollection(prop.PropertyType); IsObjectDeclaredType = prop.PropertyType == typeof(object); + IsPolymorphicCandidate = !prop.PropertyType.IsSealed + && !prop.PropertyType.IsValueType + && prop.PropertyType != typeof(string); // All typed getters are initialized in PropertyAccessorBase if (enableInternString && (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty)) diff --git a/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs b/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs index a83c1be..ba07470 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs @@ -4,150 +4,151 @@ namespace AyCode.Core.Serializers.Binaries; /// /// Binary type codes for serialization. -/// Designed for fast switch dispatch and compact storage. -/// Lower 5 bits = type code (0-31) -/// Upper 3 bits = flags (interned, reference, has-type-info) +/// Markers 0..(SlotCount-1) are reserved for object type slot indices (FixObj). +/// All other type markers are defined relative to SlotCount. /// internal static class BinaryTypeCode { - // Primitive types (0-15) - public const byte Null = 0; - public const byte True = 1; - public const byte False = 2; - public const byte Int8 = 3; - public const byte UInt8 = 4; - public const byte Int16 = 5; - 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) - public const byte String = 16; // Inline UTF8 string (non-interned) - public const byte StringInterned = 17; // Reference to interned string by index (2+ occurrence) - public const byte StringEmpty = 18; // Empty string marker - public const byte StringInternFirst = 19; // First occurrence of interned string - read content + register in cache - - // Date/Time types (20-23) - public const byte DateTime = 20; - public const byte DateTimeOffset = 21; - public const byte TimeSpan = 22; - public const byte Guid = 23; - - // Enum (24) - public const byte Enum = 24; - - // Complex types (25-31) - public const byte Object = 25; // Start of object (non-tracked OR first occurrence when ref tracking) - //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 ObjectRef = 27; // Reference to previously serialized object (2+ occurrence) - public const byte Array = 28; // Start of array/list - 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) + /// + /// Number of reserved FixObj slot markers (0..SlotCount-1). + /// When a marker byte is less than SlotCount, it represents an object + /// whose type wrapper is cached at _wrapperSlots[marker]. + /// All type markers are defined relative to this constant. + /// + public const int SlotCount = 64; - // Extended markers for first occurrence tracking (66-67, after FixStr range) - public const byte ObjectRefFirst = 66; // First occurrence of tracked object (ref handling enabled) - public const byte ObjectWithMetadataRefFirst = 67; // First occurrence of tracked object with metadata - - // Polymorphic object markers (68-69): self-describing object for polymorphic properties. - // Used when declared property type ≠ runtime type AND UseMetadata=false. - // 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) - // Header flags byte structure (for values >= 64): - // Bit 0 (0x01): HasMetadata - // 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) + // Complex types (SlotCount + 0..7) + public const byte Object = SlotCount + 0; // 64 — Start of object (fallback when >SlotCount types) + public const byte ObjectRef = SlotCount + 1; // 65 — Reference to previously serialized object (2+ occurrence) + public const byte Array = SlotCount + 2; // 66 — Start of array/list + 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 + + // Polymorphic object markers (SlotCount + 8..11) + // Used when declared property type != runtime type AND UseMetadata=false. + // + // PREFIX markers (inner Object/Array/Dict marker follows): + // [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 + + // Primitive types (SlotCount + 12..26) + 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 + + // String types (SlotCount + 27..30) + public const byte String = SlotCount + 27; // 91 — Inline UTF8 string (non-interned) + public const byte StringInterned = SlotCount + 28; // 92 — Reference to interned string by index (2+ occurrence) + public const byte StringEmpty = SlotCount + 29; // 93 — Empty string marker + public const byte StringInternFirst = SlotCount + 30; // 94 — First occurrence of interned string + + // Date/Time types (SlotCount + 31..34) + public const byte DateTime = SlotCount + 31; // 95 + public const byte DateTimeOffset = SlotCount + 32; // 96 + public const byte TimeSpan = SlotCount + 33; // 97 + public const byte Guid = SlotCount + 34; // 98 + + // Enum (SlotCount + 35) + public const byte Enum = SlotCount + 35; // 99 + + // Legacy header markers (SlotCount + 36..37) + public const byte MetadataHeader = SlotCount + 36; // 100 — Binary has metadata section (legacy, implies HasReferenceHandling=true) + public const byte NoMetadataHeader = SlotCount + 37; // 101 — Binary has no metadata (legacy, implies HasReferenceHandling=true) + + // 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) // 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 FixStrMax = 65; // FixStrBase + 31 = maximum FixStr code - public const int FixStrMaxLength = 31; // Maximum string length encodable as FixStr - - // New flag-based header markers (48+) - moved to after FixStr range - // Note: FixStr range 34-65 overlaps with old HeaderFlagsBase, so headers use version byte prefix + public const byte FixStrBase = SlotCount + 39; // 103 + public const byte FixStrMax = FixStrBase + 31; // 134 + public const int FixStrMaxLength = 31; + + // Flag-based header markers (must be 16-aligned for flag bits in lower nibble) // 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 HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included + public const byte HeaderFlagsBase = 144; // 0x90 — next 16-aligned value after FixStrMax + public const byte HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included // Reference handling uses 2 separate bits: // Bit 1 (0x02): OnlyId - reference handling for IId objects only // Bit 2 (0x04): All - reference handling for all objects (includes OnlyId) // None = both false, OnlyId = 0x02, All = 0x06 (both bits set) public const byte HeaderFlag_RefHandling_OnlyId = 0x02; 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_HasCacheCount = 0x08; // Bit 3 (reused): VarUInt cache count follows flags (new marker-based format) - - // Compact integer variants (for VarInt optimization) - public const byte Int32Tiny = 192; // -16 to 63 stored in single byte (value = code - 192 - 16) + 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) + + // Compact integer variants (unchanged) + 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) - // 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) - /// /// Check if type code represents a reference (string or object). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsReference(byte code) => code is StringInterned or ObjectRef; - + /// /// Check if type code is a FixStr (short string with length encoded in type code). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsFixStr(byte code) => code is >= FixStrBase and <= FixStrMax; - + /// /// Decode FixStr length from type code. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int DecodeFixStrLength(byte code) => code - FixStrBase; - + /// /// Encode FixStr type code for given byte length (0-31). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte EncodeFixStr(int byteLength) => (byte)(FixStrBase + byteLength); - + /// /// Check if byte length can be encoded as FixStr. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool CanEncodeAsFixStr(int byteLength) => byteLength is >= 0 and <= 31; - + /// /// Check if type code is a tiny int (single byte int32 encoding). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsTinyInt(byte code) => code >= Int32Tiny; - + /// /// Decode tiny int value from type code. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16; - + /// /// Encode small int value (-16 to 47) as type code. /// Returns true if value fits in tiny encoding. @@ -164,4 +165,4 @@ internal static class BinaryTypeCode code = 0; return false; } -} \ No newline at end of file +} diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index 833e48d..362c308 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -43,6 +43,21 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// internal bool MetadataSeen; + /// + /// 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. + /// + internal bool PolymorphicSeen; + + /// + /// 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). + /// + internal int PolymorphicCacheIndex = -1; + /// /// UseMetadata cachemap: source property index → target PropertySetter. /// Per-context (wrapper-szintű), mert futásonként eltérő source type-pal találkozhat. @@ -193,6 +208,8 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat public void ResetTracking(bool preRentBuckets = false) { MetadataSeen = false; + PolymorphicSeen = false; + PolymorphicCacheIndex = -1; CacheMap = null; // Options may change between sessions (pool reuse) → rebuild on next scan