diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 1647e58..d4a27d5 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -661,9 +661,8 @@ 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, Type? polyRuntimeType = null) + private static void WriteValueNonPrimitiveWithWrapper(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase { var type = wrapper.Metadata.SourceType; @@ -677,7 +676,6 @@ public static partial class AcBinarySerializer if (depth > context.MaxDepth) { - if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType); context.WriteByte(BinaryTypeCode.Null); return; } @@ -685,7 +683,6 @@ 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; } @@ -693,7 +690,6 @@ public static partial class AcBinarySerializer // Handle dictionaries if (value is IDictionary dictionary) { - if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType); WriteDictionary(dictionary, context, depth); return; } @@ -701,13 +697,61 @@ 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 — combined poly+ref markers handled inside WriteObject - WriteObject(value, wrapper, context, depth, polyRuntimeType); + // Handle complex objects + WriteObject(value, wrapper, context, depth); + } + + /// + /// Polymorphic variant of WriteValueNonPrimitiveWithWrapper. + /// Cold path: polymorphism is rare. Writes poly prefix for non-object types, + /// delegates to WriteObjectPolymorphic for combined poly+ref marker handling. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WriteValueNonPrimitiveWithWrapperPoly(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, Type polyRuntimeType) + where TOutput : struct, IBinaryOutputBase + { + var type = wrapper.Metadata.SourceType; + + if (type.IsValueType) + { + if (TryWritePrimitive(value, value.GetType(), context)) + return; + } + + if (depth > context.MaxDepth) + { + context.WritePolymorphicPrefix(polyRuntimeType); + context.WriteByte(BinaryTypeCode.Null); + return; + } + + if (value is byte[] byteArray) + { + context.WritePolymorphicPrefix(polyRuntimeType); + WriteByteArray(byteArray, context); + return; + } + + if (value is IDictionary dictionary) + { + context.WritePolymorphicPrefix(polyRuntimeType); + WriteDictionary(dictionary, context, depth); + return; + } + + if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) + { + context.WritePolymorphicPrefix(polyRuntimeType); + WriteArray(enumerable, wrapper, context, depth); + return; + } + + // Complex object — handles combined poly+ref markers + WriteObjectPolymorphic(value, wrapper, context, depth, polyRuntimeType); } /// @@ -1025,9 +1069,7 @@ public static partial class AcBinarySerializer } else { - // StringRef: write index reference only (no getter call, no string data) - context.WriteByte(BinaryTypeCode.StringInterned); - context.WriteVarUInt((uint)planEntry.CacheMapIndex); + WriteStringInternRef(context, planEntry.CacheMapIndex); } return; } @@ -1069,101 +1111,173 @@ public static partial class AcBinarySerializer context.WriteBytes(value); } + /// + /// String intern 2nd occurrence — cold path, just writes reference index. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WriteStringInternRef(BinarySerializationContext context, int cacheMapIndex) + where TOutput : struct, IBinaryOutputBase + { + context.WriteByte(BinaryTypeCode.StringInterned); + context.WriteVarUInt((uint)cacheMapIndex); + } + + /// + /// Object ref 2nd occurrence — cold path, just writes reference index. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WriteObjectRef(BinarySerializationContext context, int cacheMapIndex) + where TOutput : struct, IBinaryOutputBase + { + context.WriteByte(BinaryTypeCode.ObjectRef); + context.WriteVarUInt((uint)cacheMapIndex); + } + #endregion #region Complex Type Writers - private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, Type? polyRuntimeType = null) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase { var metadata = wrapper.Metadata; - - // Per-type metadata flag: when EnableMetadataFeature=false on [AcBinarySerializable], - // skip inline metadata and use markerless property write — even when global UseMetadata=true. - // Deserializer must have the same attribute on the type (developer responsibility). var useMetaForType = context.UseMetadata && metadata.EnableMetadataFeature; - // UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking) - var isFirstMetadataOccurrence = false; - if (useMetaForType) - { - isFirstMetadataOccurrence = BinarySerializationContext.RegisterMetadataType(wrapper); - } - - // Reference handling: consume pre-computed write plan entry from scan pass cursor - var cachedObjectCacheIndex = -1; // -1 = not cached, 0+ = cache index for first write + // Only IId types with ref handling enabled go to cold path if (context.UseTypeReferenceHandling(metadata)) { - if (context.TryConsumeWritePlanEntry(out var planEntry)) - { - ValidateWritePlanObject(in planEntry, value, wrapper); - if (planEntry.IsFirst) - { - // First occurrence of a cached IId object — write full object + cache index - cachedObjectCacheIndex = planEntry.CacheMapIndex; - } - else - { - // 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; - } - } + if (useMetaForType) + WriteObjectWithRefHandlingMeta(value, wrapper, context, depth); + else + WriteObjectWithRefHandling(value, wrapper, context, depth); + return; } - // Marker kiírása — polymorphic vs non-polymorphic paths - if (polyRuntimeType != null) + if (useMetaForType) { - WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex); - } - else if (useMetaForType) - { - if (cachedObjectCacheIndex >= 0) - { - context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst); - context.WriteVarUInt((uint)cachedObjectCacheIndex); - } - else - { - context.WriteByte(BinaryTypeCode.ObjectWithMetadata); - } + // UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking) + var isFirstMetadataOccurrence = BinarySerializationContext.RegisterMetadataType(wrapper); + + // Marker kiírása — no ref handling, no cachedObjectCacheIndex + context.WriteByte(BinaryTypeCode.ObjectWithMetadata); context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence); } else { - if (cachedObjectCacheIndex >= 0) + // FixObj: assign slot on first occurrence this session + if (!wrapper.PolymorphicSeen) { - context.WriteByte(BinaryTypeCode.ObjectRefFirst); - context.WriteVarUInt((uint)cachedObjectCacheIndex); + wrapper.PolymorphicSeen = true; + wrapper.PolymorphicCacheIndex = context._nextTypeSlot++; + } + if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount) + context.WriteByte((byte)wrapper.PolymorphicCacheIndex); + else + context.WriteByte(BinaryTypeCode.Object); + } + + WriteObjectProperties(value, metadata, wrapper, context, depth, useMetaForType); + } + + /// + /// WriteObject variant with reference handling, no metadata. + /// Cold path: only IId types with ref tracking enabled. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WriteObjectWithRefHandling(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) + where TOutput : struct, IBinaryOutputBase + { + // Reference handling: consume pre-computed write plan entry from scan pass cursor + var cachedObjectCacheIndex = -1; + if (context.TryConsumeWritePlanEntry(out var planEntry)) + { + ValidateWritePlanObject(in planEntry, value, wrapper); + if (planEntry.IsFirst) + { + cachedObjectCacheIndex = planEntry.CacheMapIndex; } else { - // 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); + WriteObjectRef(context, planEntry.CacheMapIndex); + return; } } - // Write all properties (startIndex=0, including Id for IId types) + // Marker kiírása — no metadata + if (cachedObjectCacheIndex >= 0) + { + context.WriteByte(BinaryTypeCode.ObjectRefFirst); + context.WriteVarUInt((uint)cachedObjectCacheIndex); + } + else + { + 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); + } + + WriteObjectProperties(value, wrapper.Metadata, wrapper, context, depth, useMetaForType: false); + } + + /// + /// WriteObject variant with reference handling + metadata. + /// Cold path: IId types with ref tracking + UseMetadata enabled. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WriteObjectWithRefHandlingMeta(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) + where TOutput : struct, IBinaryOutputBase + { + var isFirstMetadataOccurrence = BinarySerializationContext.RegisterMetadataType(wrapper); + + // Reference handling: consume pre-computed write plan entry from scan pass cursor + var cachedObjectCacheIndex = -1; + if (context.TryConsumeWritePlanEntry(out var planEntry)) + { + ValidateWritePlanObject(in planEntry, value, wrapper); + if (planEntry.IsFirst) + { + cachedObjectCacheIndex = planEntry.CacheMapIndex; + } + else + { + WriteObjectRef(context, planEntry.CacheMapIndex); + return; + } + } + + // Marker kiírása — with metadata + if (cachedObjectCacheIndex >= 0) + { + context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst); + context.WriteVarUInt((uint)cachedObjectCacheIndex); + } + else + { + context.WriteByte(BinaryTypeCode.ObjectWithMetadata); + } + context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence); + + WriteObjectProperties(value, wrapper.Metadata, wrapper, context, depth, useMetaForType: true); + } + + /// + /// Shared property writing loop — used by WriteObject, WriteObjectWithRefHandling, WriteObjectPolymorphic. + /// + private static void WriteObjectProperties(object value, BinarySerializeTypeMetadata metadata, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, bool useMetaForType) + where TOutput : struct, IBinaryOutputBase + { var nextDepth = depth + 1; var properties = metadata.Properties; var propCount = properties.Length; var hasPropertyFilter = context.HasPropertyFilter; - // Source-generated fast path: bypass the entire switch/delegate loop. - // Reference handling is safe: ref tracking happens in WriteObject (before WriteProperties) - // and child objects go through WriteValueGenerated → WriteObject → runtime ref tracking. - // String interning is safe: generated code uses pre-computed interningFlags bit-check - // matching runtime UseStringPropertyInterning — cursor alignment guaranteed for all modes. if (context.UseGeneratedCode) { var generatedWriter = wrapper.GeneratedWriter; @@ -1176,14 +1290,9 @@ public static partial class AcBinarySerializer if (!useMetaForType) { - // Markerless loop: no extra branching per property for the common case. - // Properties with ExpectedTypeCode write raw values (no type marker, no skip). - // Properties without ExpectedTypeCode (bool, enum, string, object) use the standard path. - // Also used when EnableMetadataFeature=false on the type (per-type metadata opt-out). for (var i = 0; i < propCount; i++) { var prop = properties[i]; - //context.CurrentProperty = prop; if (prop.ExpectedTypeCode.HasValue) { @@ -1201,11 +1310,9 @@ public static partial class AcBinarySerializer } else { - // UseMetadata=true loop — UNCHANGED, zero extra overhead for (var i = 0; i < propCount; i++) { var prop = properties[i]; - //context.CurrentProperty = prop; if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop)) { @@ -1251,6 +1358,42 @@ public static partial class AcBinarySerializer } } + /// + /// Polymorphic object writing — handles combined poly+ref markers. + /// Cold path: polymorphism is rare, NoInlining acceptable. + /// Poly always implies UseMetadata=false (checked in WritePropertyOrSkip). + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WriteObjectPolymorphic(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, Type polyRuntimeType) + where TOutput : struct, IBinaryOutputBase + { + var metadata = wrapper.Metadata; + + // Reference handling + var cachedObjectCacheIndex = -1; + if (context.UseTypeReferenceHandling(metadata)) + { + if (context.TryConsumeWritePlanEntry(out var planEntry)) + { + ValidateWritePlanObject(in planEntry, value, wrapper); + if (planEntry.IsFirst) + { + cachedObjectCacheIndex = planEntry.CacheMapIndex; + } + else + { + WriteObjectRef(context, planEntry.CacheMapIndex); + return; + } + } + } + + // Poly marker (handles combined poly+ref) + WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex); + + WriteObjectProperties(value, metadata, wrapper, context, depth, false); + } + /// /// Checks if a property value is null or default without boxing for value types. /// @@ -1560,12 +1703,6 @@ public static partial class AcBinarySerializer { var runtimeType = value.GetType(); - // 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) { @@ -1575,12 +1712,16 @@ public static partial class AcBinarySerializer propWrapper = context.GetWrapper(runtimeType); parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper); } - WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth, isPoly ? runtimeType : null); + if (!context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType) + WriteValueNonPrimitiveWithWrapperPoly(value, propWrapper, context, depth, runtimeType); + else + WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth); } else { // Non-complex in default case (nullable value type, etc.) - if (isPoly) context.WritePolymorphicPrefix(runtimeType); + if (!context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType) + context.WritePolymorphicPrefix(runtimeType); WriteValueNonPrimitive(value, runtimeType, context, depth); } }