Add source-generated binary reader support
Introduced source-generated binary readers for all serializable types, alongside writers. Added IGeneratedBinaryReader interface and registry for fast lookup in deserializer. Generated readers eliminate runtime overhead by directly reading properties, bypassing wrapper and delegate calls. Updated test models to disable metadata for markerless mode. Integrated fast path in deserializer for improved performance.
This commit is contained in:
parent
48c737024f
commit
19b15554cf
|
|
@ -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
|
|||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<TOutput>.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<TOutput>.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<TOutput>.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<TOutput>.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<TOutput>.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
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static string GenReader(SerializableClassInfo ci)
|
||||
{
|
||||
var sb = new StringBuilder(4096);
|
||||
sb.AppendLine("// <auto-generated/>");
|
||||
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<TInput>(AcBinaryDeserializer.BinaryDeserializationContext<TInput> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<int, {p.TypeNameForTypeof}>(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}}}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits raw value read — no type code in stream. Mirrors EmitMarkerless exactly.
|
||||
/// </summary>
|
||||
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<long>());"); break;
|
||||
case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}{a} = context.ReadDateTimeOffsetUnsafe();"); break;
|
||||
case PropertyTypeKind.Boolean: sb.AppendLine($"{i}{a} = context.ReadByte() != 0;"); break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits inline string read from type code. Handles all string wire formats.
|
||||
/// </summary>
|
||||
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;");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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())!;");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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)!;");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<T> 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}}}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits per-element read inside collection loop.
|
||||
/// SGen reader = non-metadata mode → no ObjectWithMetadata fallback.
|
||||
/// !needsRefScan → only Object/Null possible → 1 branch per element.
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits markered value read for primitive types (with type code already read).
|
||||
/// Handles TinyInt encoding for integer types.
|
||||
/// </summary>
|
||||
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<SerializableClassInfo> 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("}");
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ public enum TestUserRole
|
|||
/// Implements IId<int> for semantic $id/$ref serialization.
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class SharedTag : IId<int>
|
||||
{
|
||||
|
|
@ -80,7 +80,7 @@ public partial class SharedTag : IId<int>
|
|||
/// Shared category - for hierarchical cross-reference testing.
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class SharedCategory : IId<int>
|
||||
{
|
||||
|
|
@ -106,7 +106,7 @@ public partial class SharedCategory : IId<int>
|
|||
/// Shared user reference - appears in many places to test $ref deduplication.
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class SharedUser : IId<int>
|
||||
{
|
||||
|
|
@ -136,7 +136,7 @@ public partial class SharedUser : IId<int>
|
|||
/// User preferences - non-IId nested object
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[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
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class TestOrder : IId<int>
|
||||
{
|
||||
|
|
@ -250,7 +250,7 @@ public partial class TestOrder : IId<int>
|
|||
/// Level 2: Order item with pallets
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class TestOrderItem : IId<int>
|
||||
{
|
||||
|
|
@ -290,7 +290,7 @@ public partial class TestOrderItem : IId<int>
|
|||
/// Level 3: Pallet containing measurements
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class TestPallet : IId<int>
|
||||
{
|
||||
|
|
@ -333,7 +333,7 @@ public partial class TestPallet : IId<int>
|
|||
/// Level 4: Measurement with multiple points
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class TestMeasurement : IId<int>
|
||||
{
|
||||
|
|
@ -368,7 +368,7 @@ public partial class TestMeasurement : IId<int>
|
|||
/// Level 5: Deepest level - measurement point
|
||||
/// </summary>
|
||||
[MemoryPackable]
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
[MessagePackObject]
|
||||
public partial class TestMeasurementPoint : IId<int>
|
||||
{
|
||||
|
|
@ -402,7 +402,7 @@ public partial class TestMeasurementPoint : IId<int>
|
|||
/// <summary>
|
||||
/// Order with Guid Id - for testing Guid-based IId
|
||||
/// </summary>
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class TestGuidOrder : IId<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
|
@ -414,7 +414,7 @@ public class TestGuidOrder : IId<Guid>
|
|||
/// <summary>
|
||||
/// Item with Guid Id
|
||||
/// </summary>
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class TestGuidItem : IId<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
|
@ -430,7 +430,7 @@ public class TestGuidItem : IId<Guid>
|
|||
/// Simulates NopCommerce GenericAttribute - stores key-value pairs where DateTime values
|
||||
/// are stored as strings in the database.
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class TestDtoWithGenericAttributes : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -453,7 +453,7 @@ public class TestDtoWithGenericAttributes : IId<int>
|
|||
/// <summary>
|
||||
/// Order with nullable collections for null vs empty testing
|
||||
/// </summary>
|
||||
[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
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class ExtendedPrimitiveTestClass
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -519,7 +519,7 @@ public class ExtendedPrimitiveTestClass
|
|||
/// <summary>
|
||||
/// Class with array of objects containing null items for WriteNull coverage
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class ServerCustomerDto : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -567,7 +567,7 @@ public class ServerCustomerDto : IId<int>
|
|||
/// the deserializer must skip unknown properties correctly
|
||||
/// while still maintaining string intern table consistency.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class ClientCustomerDto : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -581,7 +581,7 @@ public class ClientCustomerDto : IId<int>
|
|||
/// Server DTO with nested objects that client doesn't know about.
|
||||
/// Tests skipping complex nested structures.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class ServerOrderWithExtras : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
@ -602,7 +602,7 @@ public class ServerOrderWithExtras : IId<int>
|
|||
/// <summary>
|
||||
/// Client version of the order - doesn't have Customer/RelatedCustomers properties.
|
||||
/// </summary>
|
||||
[AcBinarySerializable(true)]
|
||||
[AcBinarySerializable(false)]
|
||||
public class ClientOrderSimple : IId<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
|
|||
|
|
@ -29,6 +29,32 @@ public static partial class AcBinaryDeserializer
|
|||
{
|
||||
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe registry for generated readers. Looked up in ReadObjectCore to bypass runtime path.
|
||||
/// </summary>
|
||||
internal static class GeneratedReaderRegistry
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, IGeneratedBinaryReader> Readers = new();
|
||||
|
||||
internal static void Register(Type type, IGeneratedBinaryReader reader) => Readers[type] = reader;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static IGeneratedBinaryReader? TryGet(Type type) =>
|
||||
Readers.TryGetValue(type, out var reader) ? reader : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a source-generated binary reader for the specified type.
|
||||
/// Once registered, ReadObjectCore bypasses the runtime wrapper/property loop
|
||||
/// and calls the generated reader directly.
|
||||
/// </summary>
|
||||
internal static void RegisterGeneratedReader(Type type, IGeneratedBinaryReader reader)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
ArgumentNullException.ThrowIfNull(reader);
|
||||
GeneratedReaderRegistry.Register(type, reader);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ThreadLocal cache for type conversion info.
|
||||
/// </summary>
|
||||
|
|
@ -827,6 +853,17 @@ public static partial class AcBinaryDeserializer
|
|||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bridge for generated readers to call ReadValue for unknown/complex/collection property types.
|
||||
/// Reads typeCode + dispatches via TypeReaderTable — same as runtime ReadValue.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static object? ReadValueGenerated<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||||
where TInput : struct, IBinaryInputBase
|
||||
{
|
||||
return ReadValue(context, targetType, depth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized value reader using FrozenDictionary dispatch table.
|
||||
/// </summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
internal interface IGeneratedBinaryReader
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
/// <param name="context">The deserialization context (owns buffer, position, options).</param>
|
||||
/// <param name="depth">Current depth in the object graph.</param>
|
||||
/// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index for ref tracking.</param>
|
||||
/// <typeparam name="TInput">Input strategy (ArrayBinaryInput or SequenceBinaryInput).</typeparam>
|
||||
/// <returns>The deserialized object, or null if creation failed.</returns>
|
||||
object? ReadObject<TInput>(AcBinaryDeserializer.BinaryDeserializationContext<TInput> context, int depth, int cacheIndex)
|
||||
where TInput : struct, IBinaryInputBase;
|
||||
}
|
||||
Loading…
Reference in New Issue