diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index 54d42f1..b401f03 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -290,7 +290,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (valid.Count == 0) return; foreach (var ci in valid) + { context.AddSource($"{ci.ClassName}_GeneratedWriter.g.cs", SourceText.From(GenWriter(ci), Encoding.UTF8)); + context.AddSource($"{ci.ClassName}_GeneratedReader.g.cs", SourceText.From(GenReader(ci), Encoding.UTF8)); + } context.AddSource("AcBinaryGeneratedWriters_Init.g.cs", SourceText.From(GenInit(valid), Encoding.UTF8)); } @@ -846,10 +849,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator /// /// Emits inline object write for a Complex property that has a generated writer. - /// Writes marker bytes + inline metadata (UseMetadata) + calls child GeneratedWriter.WriteProperties. - /// IId types: guard ReferenceHandling != None (tracked in OnlyId + All). - /// Non-IId types: guard ReferenceHandling == All (tracked only in All mode). - /// No fallback to WriteObjectGenerated — handles both UseMetadata=true and false inline. + /// Compile-time ChildNeedsRefScan eliminates TryConsumeWritePlanEntry when scan never tracks child. + /// !ChildNeedsRefScan + !ChildEnableMetadata → ZERO branches: just Object + WriteProperties. /// private static void EmitDirectObjectWrite(StringBuilder sb, PropInfo p, string a, string i) { @@ -872,10 +873,32 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); - if (!p.ChildEnableMetadata) + if (!p.ChildNeedsRefScan) { - // Child type has EnableMetadataFeature=false — no metadata, always Object marker - // Inline ref tracking still needed for IId/All mode + // Compile-time proven: scan never tracks child → TryConsumeWritePlanEntry always false + if (!p.ChildEnableMetadata) + { + // No ref, no metadata → ZERO branches: always Object + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); + } + 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} if (context.UseMetadata)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); + EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " "); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); + } + } + else if (!p.ChildEnableMetadata) + { + // Ref tracking possible, no metadata — Object or ObjectRefFirst/ObjectRef var refGuard = p.IsIId ? "context.ReferenceHandling != ReferenceHandlingMode.None" : "context.ReferenceHandling == ReferenceHandlingMode.All"; @@ -901,10 +924,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } else { - // UseMetadata: register type for first/repeated tracking via slot (no GetWrapper needed) + // 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})));"); - - // Inline ref tracking: guard depends on IId vs non-IId to match scan pass behavior. var refGuard = p.IsIId ? "context.ReferenceHandling != ReferenceHandlingMode.None" : "context.ReferenceHandling == ReferenceHandlingMode.All"; @@ -917,7 +938,6 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); - // RefFirst: UseMetadata → ObjectWithMetadataRefFirst + metadata, else → ObjectRefFirst sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);"); @@ -934,7 +954,6 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); - // No ref tracking: UseMetadata → ObjectWithMetadata + metadata, else → Object sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); @@ -1021,75 +1040,98 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); sb.AppendLine($"{i} if (nextDepth_{p.Name} > context.MaxDepth) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); - // Inline ref tracking - var elemRefGuard = p.ElementIsIId - ? "context.ReferenceHandling != ReferenceHandlingMode.None" - : "context.ReferenceHandling == ReferenceHandlingMode.All"; - - if (!p.ElementEnableMetadata) + if (!p.ElementNeedsRefScan) { - // Element type has EnableMetadataFeature=false — no metadata, always Object marker - sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))"); - sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)"); - sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); - sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); - sb.AppendLine($"{i} }}"); - sb.AppendLine($"{i} else"); - sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); - sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); - sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); - sb.AppendLine($"{i} }}"); - sb.AppendLine($"{i} }}"); - sb.AppendLine($"{i} else"); - sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); - sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); - sb.AppendLine($"{i} }}"); + // Compile-time proven: scan never tracks element → TryConsumeWritePlanEntry always false + if (!p.ElementEnableMetadata) + { + // No ref, no metadata → ZERO branches per element: always Object + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); + } + 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} if (context.UseMetadata)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); + EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " "); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); + } } else { - // UseMetadata: register element type for first/repeated tracking - sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));"); + // Inline ref tracking + var elemRefGuard = p.ElementIsIId + ? "context.ReferenceHandling != ReferenceHandlingMode.None" + : "context.ReferenceHandling == ReferenceHandlingMode.All"; - sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))"); - sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)"); - sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); - sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); - sb.AppendLine($"{i} }}"); - sb.AppendLine($"{i} else"); - sb.AppendLine($"{i} {{"); - // RefFirst: UseMetadata → ObjectWithMetadataRefFirst + metadata, else → ObjectRefFirst - sb.AppendLine($"{i} if (context.UseMetadata)"); - sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);"); - sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); - EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " "); - sb.AppendLine($"{i} }}"); - sb.AppendLine($"{i} else"); - sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); - sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); - sb.AppendLine($"{i} }}"); - sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); - sb.AppendLine($"{i} }}"); - sb.AppendLine($"{i} }}"); - sb.AppendLine($"{i} else"); - sb.AppendLine($"{i} {{"); - // No ref tracking: UseMetadata → ObjectWithMetadata + metadata, else → Object - sb.AppendLine($"{i} if (context.UseMetadata)"); - sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); - EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " "); - sb.AppendLine($"{i} }}"); - sb.AppendLine($"{i} else"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); - sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); - sb.AppendLine($"{i} }}"); + if (!p.ElementEnableMetadata) + { + // Ref tracking possible, no metadata — Object or ObjectRefFirst/ObjectRef + sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); + sb.AppendLine($"{i} }}"); + } + 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} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} if (context.UseMetadata)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); + EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " "); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} if (context.UseMetadata)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); + EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " "); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); + sb.AppendLine($"{i} }}"); + } } sb.AppendLine($"{i} }}"); @@ -1201,6 +1243,430 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } } + #region Reader Code Generation + + /// + /// Generates the IGeneratedBinaryReader implementation for a type. + /// Phase 1: handles markerless path (no UseMetadata). UseMetadata/ChainMode → runtime fallback. + /// Eliminates: GetWrapper dictionary lookup, CreateInstance delegate, property setter delegates, + /// AccessorType switch dispatch, ReadValue dispatch table. + /// + private static string GenReader(SerializableClassInfo ci) + { + var sb = new StringBuilder(4096); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine("using System.Runtime.CompilerServices;"); + sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); + sb.AppendLine(); + if (!string.IsNullOrEmpty(ci.Namespace)) + sb.AppendLine($"namespace {ci.Namespace};"); + sb.AppendLine(); + sb.AppendLine($"internal sealed class {ci.ClassName}_GeneratedReader : IGeneratedBinaryReader"); + sb.AppendLine("{"); + sb.AppendLine($" internal static readonly {ci.ClassName}_GeneratedReader Instance = new();"); + sb.AppendLine(); + + // ReadObject — IGeneratedBinaryReader implementation + sb.AppendLine(" public object? ReadObject(AcBinaryDeserializer.BinaryDeserializationContext context, int depth, int cacheIndex)"); + sb.AppendLine(" where TInput : struct, IBinaryInputBase"); + sb.AppendLine(" {"); + sb.AppendLine($" var obj = new {ci.FullTypeName}();"); + sb.AppendLine(); + sb.AppendLine(" if (cacheIndex >= 0)"); + sb.AppendLine(" context.RegisterInternedValueAt(cacheIndex, obj);"); + sb.AppendLine(); + + // Emit property reads — markerless for primitive types, markered for the rest + foreach (var p in ci.Properties) + { + sb.AppendLine(); + EmitReadProp(sb, p, " ", ci.EnableMetadata); + } + + sb.AppendLine(); + sb.AppendLine(" return obj;"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + return sb.ToString(); + } + + /// + /// Emits inline read code for a single property. + /// Markerless types: read raw value directly (no type code in stream). + /// Markered types: read type code byte, then dispatch. + /// Mirrors the serializer's EmitProp symmetry. + /// + private static void EmitReadProp(StringBuilder sb, PropInfo p, string i, bool enableMetadata) + { + var a = $"obj.{p.Name}"; + + // Markerless types: read raw value directly — mirrors EmitMarkerless in writer + if (IsMarkerless(p.TypeKind)) + { + if (p.TypeKind == PropertyTypeKind.Enum) + sb.AppendLine($"{i}{{ var ev = context.ReadVarInt(); {a} = Unsafe.As(ref ev); }}"); + else + EmitReadMarkerless(sb, p.TypeKind, a, i); + return; + } + + // Markered types: read type code, then dispatch + var tc = $"tc_{p.Name}"; + sb.AppendLine($"{i}var {tc} = context.ReadByte();"); + + // PropertySkip → leave default + sb.AppendLine($"{i}if ({tc} != BinaryTypeCode.PropertySkip)"); + sb.AppendLine($"{i}{{"); + + // Nullable value types + if (IsNullableVTKind(p.TypeKind)) + { + sb.AppendLine($"{i} if ({tc} == BinaryTypeCode.Null) {{ /* null */ }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + EmitReadMarkeredValue(sb, Underlying(p.TypeKind), a, tc, i + " ", p, nullable: true); + sb.AppendLine($"{i} }}"); + } + else + { + switch (p.TypeKind) + { + case PropertyTypeKind.String: + EmitReadString(sb, a, tc, i + " "); + break; + + case PropertyTypeKind.Complex: + EmitReadComplex(sb, p, a, tc, i + " "); + break; + + case PropertyTypeKind.Collection: + EmitReadCollection(sb, p, a, tc, i + " "); + break; + + default: + // Unknown markered type (char, sbyte, etc.) — rewind + runtime fallback + sb.AppendLine($"{i} context._position--;"); + if (p.IsNullable) + sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1);"); + else + sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1)!;"); + break; + } + } + + sb.AppendLine($"{i}}}"); + } + + /// + /// Emits raw value read — no type code in stream. Mirrors EmitMarkerless exactly. + /// + private static void EmitReadMarkerless(StringBuilder sb, PropertyTypeKind k, string a, string i) + { + switch (k) + { + case PropertyTypeKind.Int32: sb.AppendLine($"{i}{a} = context.ReadVarInt();"); break; + case PropertyTypeKind.Int64: sb.AppendLine($"{i}{a} = context.ReadVarLong();"); break; + case PropertyTypeKind.Double: sb.AppendLine($"{i}{a} = context.ReadDoubleUnsafe();"); break; + case PropertyTypeKind.Single: sb.AppendLine($"{i}{a} = context.ReadSingleUnsafe();"); break; + case PropertyTypeKind.Decimal: sb.AppendLine($"{i}{a} = context.ReadDecimalUnsafe();"); break; + case PropertyTypeKind.DateTime: sb.AppendLine($"{i}{a} = context.ReadDateTimeUnsafe();"); break; + case PropertyTypeKind.Guid: sb.AppendLine($"{i}{a} = context.ReadGuidUnsafe();"); break; + case PropertyTypeKind.Byte: sb.AppendLine($"{i}{a} = context.ReadByte();"); break; + case PropertyTypeKind.Int16: sb.AppendLine($"{i}{a} = context.ReadInt16Unsafe();"); break; + case PropertyTypeKind.UInt16: sb.AppendLine($"{i}{a} = context.ReadUInt16Unsafe();"); break; + case PropertyTypeKind.UInt32: sb.AppendLine($"{i}{a} = context.ReadVarUInt();"); break; + case PropertyTypeKind.UInt64: sb.AppendLine($"{i}{a} = context.ReadVarULong();"); break; + case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}{a} = new System.TimeSpan(context.ReadRaw());"); break; + case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}{a} = context.ReadDateTimeOffsetUnsafe();"); break; + case PropertyTypeKind.Boolean: sb.AppendLine($"{i}{a} = context.ReadByte() != 0;"); break; + } + } + + /// + /// Emits inline string read from type code. Handles all string wire formats. + /// + private static void EmitReadString(StringBuilder sb, string a, string tc, string i) + { + // FixStr is the hot path — most strings are short (1-31 bytes, encoded in the type code itself) + sb.AppendLine($"{i}if (BinaryTypeCode.IsFixStr({tc}))"); + sb.AppendLine($"{i}{{"); + sb.AppendLine($"{i} var flen = BinaryTypeCode.DecodeFixStrLength({tc});"); + sb.AppendLine($"{i} {a} = flen == 0 ? string.Empty : context.ReadStringUtf8(flen);"); + sb.AppendLine($"{i}}}"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.String)"); + sb.AppendLine($"{i}{{"); + sb.AppendLine($"{i} var slen = (int)context.ReadVarUInt();"); + sb.AppendLine($"{i} {a} = slen == 0 ? string.Empty : context.ReadStringUtf8(slen);"); + sb.AppendLine($"{i}}}"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.StringInternFirst)"); + sb.AppendLine($"{i}{{"); + sb.AppendLine($"{i} context.DisableStringCaching();"); + sb.AppendLine($"{i} var sci = (int)context.ReadVarUInt();"); + sb.AppendLine($"{i} var slen2 = (int)context.ReadVarUInt();"); + sb.AppendLine($"{i} var sv = slen2 == 0 ? string.Empty : context.ReadStringUtf8(slen2);"); + sb.AppendLine($"{i} context.RegisterInternedValueAt(sci, sv);"); + sb.AppendLine($"{i} {a} = sv;"); + sb.AppendLine($"{i}}}"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.StringInterned)"); + sb.AppendLine($"{i} {a} = context.GetInternedString((int)context.ReadVarUInt());"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Null) {a} = null;"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.StringEmpty) {a} = string.Empty;"); + } + + /// + /// Emits inline read for a Complex property. + /// SGen reader only runs in non-metadata mode → ObjectWithMetadata never appears. + /// Compile-time ChildNeedsRefScan eliminates ObjectRefFirst/ObjectRef when provably unused. + /// Non-nullable + no ref → ZERO branches (tc consumed but ignored). + /// No SGen → runtime fallback via ReadValueGenerated. + /// + private static void EmitReadComplex(StringBuilder sb, PropInfo p, string a, string tc, string i) + { + if (!p.HasGeneratedWriter) + { + // No SGen reader — runtime fallback (rewind + ReadValueGenerated) + if (p.IsNullable) + { + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;"); + sb.AppendLine($"{i}else"); + sb.AppendLine($"{i}{{"); + sb.AppendLine($"{i} context._position--;"); + sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1);"); + sb.AppendLine($"{i}}}"); + } + else + { + sb.AppendLine($"{i}context._position--;"); + sb.AppendLine($"{i}{a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1)!;"); + } + return; + } + + var reader = p.WriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"); + var cast = $"({p.TypeNameForTypeof})"; + var nd = "depth + 1"; + + if (!p.ChildNeedsRefScan) + { + // Compile-time proven: child never tracked → only Object (+ Null for nullable) in stream + if (p.IsNullable) + { + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {{ /* null */ }}"); + sb.AppendLine($"{i}else {a} = {cast}{reader}.Instance.ReadObject(context, {nd}, -1)!;"); + } + else + { + // ZERO branches — tc is always Object, just call ReadObject + sb.AppendLine($"{i}{a} = {cast}{reader}.Instance.ReadObject(context, {nd}, -1)!;"); + } + } + else + { + // Ref tracking possible — Object/ObjectRefFirst/ObjectRef dispatch + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Object)"); + sb.AppendLine($"{i} {a} = {cast}{reader}.Instance.ReadObject(context, {nd}, -1)!;"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.ObjectRefFirst)"); + sb.AppendLine($"{i} {a} = {cast}{reader}.Instance.ReadObject(context, {nd}, (int)context.ReadVarUInt())!;"); + if (p.IsNullable) + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Null) {{ /* null */ }}"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.ObjectRef)"); + sb.AppendLine($"{i} {a} = {cast}context.GetInternedObject((int)context.ReadVarUInt())!;"); + } + } + + /// + /// Emits inline read for a Collection property. + /// Complex element with SGen + known collection kind → inline Array loop with direct element reader calls. + /// Else → runtime fallback via ReadValueGenerated. + /// + private static void EmitReadCollection(StringBuilder sb, PropInfo p, string a, string tc, string i) + { + // Check if we can inline: need SGen element reader + known collection shape + Array marker + if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.CollectionKind != null) + { + EmitReadCollectionInline(sb, p, a, tc, i); + return; + } + + // Runtime fallback + if (p.IsNullable) + { + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;"); + sb.AppendLine($"{i}else"); + sb.AppendLine($"{i}{{"); + sb.AppendLine($"{i} context._position--;"); + sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1);"); + sb.AppendLine($"{i}}}"); + } + else + { + sb.AppendLine($"{i}context._position--;"); + sb.AppendLine($"{i}{a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1)!;"); + } + } + + /// + /// Emits inline collection read: Array marker already consumed as tc. + /// Reads count + loops with direct element reader calls. + /// Eliminates per-element: ReadValue dispatch, ReadObjectCore dict lookup, Activator.CreateInstance. + /// + private static void EmitReadCollectionInline(StringBuilder sb, PropInfo p, string a, string tc, string i) + { + var reader = p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"); + var elemType = p.ElementFullTypeName!; + var elemCast = $"({elemType})"; + var s = p.Name; + + // Null check + if (p.IsNullable) + { + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Array)"); + } + else + { + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Array)"); + } + + sb.AppendLine($"{i}{{"); + sb.AppendLine($"{i} var cnt_{s} = (int)context.ReadVarUInt();"); + sb.AppendLine($"{i} var nd_{s} = depth + 1;"); + + // Create collection based on kind + if (p.CollectionKind == "Array") + { + sb.AppendLine($"{i} var col_{s} = new {elemType}[cnt_{s}];"); + sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)"); + sb.AppendLine($"{i} {{"); + EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan); + sb.AppendLine($"{i} }}"); + } + else // List, Counted — all use List with Add + { + sb.AppendLine($"{i} var col_{s} = new System.Collections.Generic.List<{elemType}>(cnt_{s});"); + sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)"); + sb.AppendLine($"{i} {{"); + EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan); + sb.AppendLine($"{i} }}"); + } + + sb.AppendLine($"{i} {a} = col_{s};"); + sb.AppendLine($"{i}}}"); + } + + /// + /// Emits per-element read inside collection loop. + /// SGen reader = non-metadata mode → no ObjectWithMetadata fallback. + /// !needsRefScan → only Object/Null possible → 1 branch per element. + /// + private static void EmitReadCollectionElement(StringBuilder sb, string reader, string elemCast, string indexVar, string propSuffix, string i, bool isArray, bool needsRefScan) + { + var etc = $"etc_{propSuffix}"; + sb.AppendLine($"{i}var {etc} = context.ReadByte();"); + + var assignNull = isArray ? $"col_{propSuffix}[{indexVar}] = null!;" : $"col_{propSuffix}.Add(null!);"; + var assignExpr = isArray ? $"col_{propSuffix}[{indexVar}] = " : $"col_{propSuffix}.Add("; + var assignEnd = isArray ? ";" : ");"; + + if (!needsRefScan) + { + // No ref tracking → only Object or Null in stream — 1 branch + sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Null) {{ {assignNull} }}"); + sb.AppendLine($"{i}else {assignExpr}{elemCast}{reader}.Instance.ReadObject(context, nd_{propSuffix}, -1)!{assignEnd}"); + } + else + { + // Object hot path first, then ref markers + sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Object)"); + sb.AppendLine($"{i} {assignExpr}{elemCast}{reader}.Instance.ReadObject(context, nd_{propSuffix}, -1)!{assignEnd}"); + sb.AppendLine($"{i}else if ({etc} == BinaryTypeCode.ObjectRefFirst)"); + sb.AppendLine($"{i} {assignExpr}{elemCast}{reader}.Instance.ReadObject(context, nd_{propSuffix}, (int)context.ReadVarUInt())!{assignEnd}"); + sb.AppendLine($"{i}else if ({etc} == BinaryTypeCode.Null) {{ {assignNull} }}"); + sb.AppendLine($"{i}else if ({etc} == BinaryTypeCode.ObjectRef)"); + sb.AppendLine($"{i} {assignExpr}{elemCast}context.GetInternedObject((int)context.ReadVarUInt())!{assignEnd}"); + } + } + + /// + /// Emits markered value read for primitive types (with type code already read). + /// Handles TinyInt encoding for integer types. + /// + private static void EmitReadMarkeredValue(StringBuilder sb, PropertyTypeKind k, string a, string tc, string i, PropInfo p, bool nullable) + { + var assign = nullable ? $"{a} = " : $"{a} = "; + switch (k) + { + case PropertyTypeKind.Int32: + sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}BinaryTypeCode.DecodeTinyInt({tc});"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {assign}context.ReadVarInt();"); + break; + case PropertyTypeKind.Int64: + sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}BinaryTypeCode.DecodeTinyInt({tc});"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {assign}context.ReadVarInt();"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int64) {assign}context.ReadVarLong();"); + break; + case PropertyTypeKind.Boolean: + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.True) {assign}true;"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.False) {assign}false;"); + break; + case PropertyTypeKind.Double: + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float64) {assign}context.ReadDoubleUnsafe();"); + break; + case PropertyTypeKind.Single: + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float32) {assign}context.ReadSingleUnsafe();"); + break; + case PropertyTypeKind.Decimal: + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Decimal) {assign}context.ReadDecimalUnsafe();"); + break; + case PropertyTypeKind.DateTime: + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTime) {assign}context.ReadDateTimeUnsafe();"); + break; + case PropertyTypeKind.Guid: + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Guid) {assign}context.ReadGuidUnsafe();"); + break; + case PropertyTypeKind.Byte: + sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(byte)BinaryTypeCode.DecodeTinyInt({tc});"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt8) {assign}context.ReadByte();"); + break; + case PropertyTypeKind.Int16: + sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(short)BinaryTypeCode.DecodeTinyInt({tc});"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int16) {assign}context.ReadInt16Unsafe();"); + break; + case PropertyTypeKind.UInt16: + sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(ushort)BinaryTypeCode.DecodeTinyInt({tc});"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt16) {assign}context.ReadUInt16Unsafe();"); + break; + case PropertyTypeKind.UInt32: + sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(uint)BinaryTypeCode.DecodeTinyInt({tc});"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt32) {assign}context.ReadVarUInt();"); + break; + case PropertyTypeKind.UInt64: + sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(ulong)BinaryTypeCode.DecodeTinyInt({tc});"); + sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt64) {assign}context.ReadVarULong();"); + break; + case PropertyTypeKind.Enum: + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Enum)"); + sb.AppendLine($"{i}{{"); + sb.AppendLine($"{i} var eb = context.ReadByte();"); + sb.AppendLine($"{i} int ev;"); + sb.AppendLine($"{i} if (BinaryTypeCode.IsTinyInt(eb)) ev = BinaryTypeCode.DecodeTinyInt(eb);"); + sb.AppendLine($"{i} else ev = context.ReadVarInt();"); + sb.AppendLine($"{i} {assign}({p.TypeNameForTypeof})(object)ev;"); + sb.AppendLine($"{i}}}"); + sb.AppendLine($"{i}else if (BinaryTypeCode.IsTinyInt({tc})) {assign}({p.TypeNameForTypeof})(object)BinaryTypeCode.DecodeTinyInt({tc});"); + break; + case PropertyTypeKind.TimeSpan: + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.TimeSpan) {assign}context.ReadTimeSpanUnsafe();"); + break; + case PropertyTypeKind.DateTimeOffset: + sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTimeOffset) {assign}context.ReadDateTimeOffsetUnsafe();"); + break; + } + } + + #endregion + private static string GenInit(List classes) { var sb = new StringBuilder(512); @@ -1220,7 +1686,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator var writerRef = string.IsNullOrEmpty(ci.Namespace) ? $"{ci.ClassName}_GeneratedWriter" : $"{ci.Namespace}.{ci.ClassName}_GeneratedWriter"; + var readerRef = string.IsNullOrEmpty(ci.Namespace) + ? $"{ci.ClassName}_GeneratedReader" + : $"{ci.Namespace}.{ci.ClassName}_GeneratedReader"; sb.AppendLine($" AcBinarySerializer.RegisterGeneratedWriter(typeof({ci.FullTypeName}), {writerRef}.Instance);"); + sb.AppendLine($" AcBinaryDeserializer.RegisterGeneratedReader(typeof({ci.FullTypeName}), {readerRef}.Instance);"); } sb.AppendLine(" }"); sb.AppendLine("}"); diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs index 11129a1..0d4f97e 100644 --- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs +++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs @@ -55,7 +55,7 @@ public enum TestUserRole /// Implements IId<int> for semantic $id/$ref serialization. /// [MemoryPackable] -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class SharedTag : IId { @@ -80,7 +80,7 @@ public partial class SharedTag : IId /// Shared category - for hierarchical cross-reference testing. /// [MemoryPackable] -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class SharedCategory : IId { @@ -106,7 +106,7 @@ public partial class SharedCategory : IId /// Shared user reference - appears in many places to test $ref deduplication. /// [MemoryPackable] -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class SharedUser : IId { @@ -136,7 +136,7 @@ public partial class SharedUser : IId /// User preferences - non-IId nested object /// [MemoryPackable] -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class UserPreferences { @@ -162,7 +162,7 @@ public partial class UserPreferences /// Does NOT implement IId, so uses standard Newtonsoft reference tracking. /// [MemoryPackable] -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class MetadataInfo { @@ -190,7 +190,7 @@ public partial class MetadataInfo /// Level 1: Main order - root of the hierarchy /// [MemoryPackable] -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class TestOrder : IId { @@ -250,7 +250,7 @@ public partial class TestOrder : IId /// Level 2: Order item with pallets /// [MemoryPackable] -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class TestOrderItem : IId { @@ -290,7 +290,7 @@ public partial class TestOrderItem : IId /// Level 3: Pallet containing measurements /// [MemoryPackable] -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class TestPallet : IId { @@ -333,7 +333,7 @@ public partial class TestPallet : IId /// Level 4: Measurement with multiple points /// [MemoryPackable] -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class TestMeasurement : IId { @@ -368,7 +368,7 @@ public partial class TestMeasurement : IId /// Level 5: Deepest level - measurement point /// [MemoryPackable] -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] [MessagePackObject] public partial class TestMeasurementPoint : IId { @@ -402,7 +402,7 @@ public partial class TestMeasurementPoint : IId /// /// Order with Guid Id - for testing Guid-based IId /// -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] public class TestGuidOrder : IId { public Guid Id { get; set; } @@ -414,7 +414,7 @@ public class TestGuidOrder : IId /// /// Item with Guid Id /// -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] public class TestGuidItem : IId { public Guid Id { get; set; } @@ -430,7 +430,7 @@ public class TestGuidItem : IId /// Simulates NopCommerce GenericAttribute - stores key-value pairs where DateTime values /// are stored as strings in the database. /// -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] public class TestGenericAttribute { public int Id { get; set; } @@ -442,7 +442,7 @@ public class TestGenericAttribute /// DTO with GenericAttributes collection - simulates OrderDto with string-stored DateTime values. /// This reproduces the production bug where Binary serialization was thought to corrupt DateTime strings. /// -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] public class TestDtoWithGenericAttributes : IId { public int Id { get; set; } @@ -453,7 +453,7 @@ public class TestDtoWithGenericAttributes : IId /// /// Order with nullable collections for null vs empty testing /// -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] public class TestOrderWithNullableCollections { public int Id { get; set; } @@ -466,7 +466,7 @@ public class TestOrderWithNullableCollections /// Class with all primitive types for WASM/serialization testing /// [MemoryPackable] -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] public partial class PrimitiveTestClass { public int IntValue { get; set; } @@ -489,7 +489,7 @@ public partial class PrimitiveTestClass /// Class with extended primitive types for full serializer coverage. /// Includes DateTimeOffset, TimeSpan, Dictionary, null properties. /// -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] public class ExtendedPrimitiveTestClass { public int Id { get; set; } @@ -519,7 +519,7 @@ public class ExtendedPrimitiveTestClass /// /// Class with array of objects containing null items for WriteNull coverage /// -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] public class ObjectWithNullItems { public int Id { get; set; } @@ -534,7 +534,7 @@ public class ObjectWithNullItems /// "Server-side" DTO with extra properties that the "client" doesn't know about. /// Used to test SkipValue functionality when deserializing unknown properties. /// -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] public class ServerCustomerDto : IId { public int Id { get; set; } @@ -567,7 +567,7 @@ public class ServerCustomerDto : IId /// the deserializer must skip unknown properties correctly /// while still maintaining string intern table consistency. /// -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] public class ClientCustomerDto : IId { public int Id { get; set; } @@ -581,7 +581,7 @@ public class ClientCustomerDto : IId /// Server DTO with nested objects that client doesn't know about. /// Tests skipping complex nested structures. /// -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] public class ServerOrderWithExtras : IId { public int Id { get; set; } @@ -602,7 +602,7 @@ public class ServerOrderWithExtras : IId /// /// Client version of the order - doesn't have Customer/RelatedCustomers properties. /// -[AcBinarySerializable(true)] +[AcBinarySerializable(false)] public class ClientOrderSimple : IId { public int Id { get; set; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 2c84df0..58f0139 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -29,6 +29,32 @@ public static partial class AcBinaryDeserializer { private static readonly ConcurrentDictionary TypeConversionCache = new(); + /// + /// Thread-safe registry for generated readers. Looked up in ReadObjectCore to bypass runtime path. + /// + internal static class GeneratedReaderRegistry + { + private static readonly ConcurrentDictionary Readers = new(); + + internal static void Register(Type type, IGeneratedBinaryReader reader) => Readers[type] = reader; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static IGeneratedBinaryReader? TryGet(Type type) => + Readers.TryGetValue(type, out var reader) ? reader : null; + } + + /// + /// Registers a source-generated binary reader for the specified type. + /// Once registered, ReadObjectCore bypasses the runtime wrapper/property loop + /// and calls the generated reader directly. + /// + internal static void RegisterGeneratedReader(Type type, IGeneratedBinaryReader reader) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(reader); + GeneratedReaderRegistry.Register(type, reader); + } + /// /// ThreadLocal cache for type conversion info. /// @@ -827,6 +853,17 @@ public static partial class AcBinaryDeserializer return false; } + /// + /// Bridge for generated readers to call ReadValue for unknown/complex/collection property types. + /// Reads typeCode + dispatches via TypeReaderTable — same as runtime ReadValue. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static object? ReadValueGenerated(BinaryDeserializationContext context, Type targetType, int depth) + where TInput : struct, IBinaryInputBase + { + return ReadValue(context, targetType, depth); + } + /// /// Optimized value reader using FrozenDictionary dispatch table. /// @@ -1088,6 +1125,16 @@ public static partial class AcBinaryDeserializer return ReadDictionaryAsObject(context, keyType!, valueType!, depth); } + // SGen fast path: generated reader bypasses GetWrapper + CreateInstance + PopulateObject entirely. + // Only when not in UseMetadata mode (cross-type CacheMap not known at compile time) + // and not in ChainMode (needs post-read identity tracking). + if (!context.HasMetadata && !context.IsChainMode) + { + var generatedReader = GeneratedReaderRegistry.TryGet(targetType); + if (generatedReader != null) + return generatedReader.ReadObject(context, depth, cacheIndex); + } + var wrapper = context.GetWrapper(targetType); return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex); } diff --git a/AyCode.Core/Serializers/Binaries/IGeneratedBinaryReader.cs b/AyCode.Core/Serializers/Binaries/IGeneratedBinaryReader.cs new file mode 100644 index 0000000..5e63aee --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/IGeneratedBinaryReader.cs @@ -0,0 +1,29 @@ +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Interface for source-generated binary property readers. +/// Implementations bypass the runtime property loop, wrapper lookup, and delegate-based setters. +/// Each generated reader handles all properties of a specific type using direct property access. +/// +/// Performance gains over runtime path: +/// - No GetWrapper() dictionary lookup per object (~20-50ns saved) +/// - No property setter delegate calls (~5-8ns/property saved) +/// - No AccessorType switch dispatch (~2-3ns/property saved) +/// - No boxing for value type properties (direct obj.Prop = context.ReadXxx()) +/// - No ReadValue dispatch table for known property types +/// +internal interface IGeneratedBinaryReader +{ + /// + /// Creates a new instance and reads all properties from the stream. + /// Handles both markerless (no UseMetadata) and markered wire formats. + /// UseMetadata=true falls back to runtime path (cross-type CacheMap not known at compile time). + /// + /// The deserialization context (owns buffer, position, options). + /// Current depth in the object graph. + /// -1 = not cached, 0+ = register at this cache index for ref tracking. + /// Input strategy (ArrayBinaryInput or SequenceBinaryInput). + /// The deserialized object, or null if creation failed. + object? ReadObject(AcBinaryDeserializer.BinaryDeserializationContext context, int depth, int cacheIndex) + where TInput : struct, IBinaryInputBase; +}