Source-generated scan pass for SGen binary serialization
Implements a fully source-generated scan pass for duplicate/reference tracking in SGen binary serialization. The generator now emits ScanObject and ScanForDuplicates methods for each writer, handling null/depth checks, slot-based ref tracking (by Id or object hash), and recursive property scanning (strings, complex types, collections). String interning and reference tracking are feature-flagged via attributes. The runtime scan path now delegates to generated code when available, eliminating reflection and delegate overhead. Adds slot-based IdentityMap arrays to the serialization context for efficient duplicate detection. Also updates metadata, attributes, and test stubs to support these features.
This commit is contained in:
parent
deffb77de4
commit
77ea512c1f
|
|
@ -80,6 +80,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
bool hasGenWriter = false;
|
bool hasGenWriter = false;
|
||||||
bool propTypeIsIId = false;
|
bool propTypeIsIId = false;
|
||||||
string? writerClassName = null;
|
string? writerClassName = null;
|
||||||
|
string? propIdTypeName = null;
|
||||||
int childTypeNameHash = 0;
|
int childTypeNameHash = 0;
|
||||||
int[]? childPropertyHashes = null;
|
int[]? childPropertyHashes = null;
|
||||||
if (kind == PropertyTypeKind.Complex)
|
if (kind == PropertyTypeKind.Complex)
|
||||||
|
|
@ -95,9 +96,13 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
|
|
||||||
if (hasGenWriter)
|
if (hasGenWriter)
|
||||||
{
|
{
|
||||||
propTypeIsIId = resolvedType.AllInterfaces.Any(i =>
|
var iidIface = resolvedType.AllInterfaces.FirstOrDefault(i =>
|
||||||
i.IsGenericType &&
|
i.IsGenericType &&
|
||||||
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
||||||
|
propTypeIsIId = iidIface != null;
|
||||||
|
if (iidIface != null)
|
||||||
|
propIdTypeName = iidIface.TypeArguments[0].ToDisplayString();
|
||||||
|
|
||||||
// Writer class: {Namespace}.{FlatName}_GeneratedWriter
|
// Writer class: {Namespace}.{FlatName}_GeneratedWriter
|
||||||
var flatName = BuildFlatName((INamedTypeSymbol)resolvedType);
|
var flatName = BuildFlatName((INamedTypeSymbol)resolvedType);
|
||||||
var ns = resolvedType.ContainingNamespace.IsGlobalNamespace
|
var ns = resolvedType.ContainingNamespace.IsGlobalNamespace
|
||||||
|
|
@ -118,6 +123,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
bool elemHasGenWriter = false;
|
bool elemHasGenWriter = false;
|
||||||
bool elemIsIId = false;
|
bool elemIsIId = false;
|
||||||
string? elemWriterClassName = null;
|
string? elemWriterClassName = null;
|
||||||
|
string? elemIdTypeName = null;
|
||||||
string? collKind = null;
|
string? collKind = null;
|
||||||
string? elemFullTypeName = null;
|
string? elemFullTypeName = null;
|
||||||
int elementTypeNameHash = 0;
|
int elementTypeNameHash = 0;
|
||||||
|
|
@ -160,9 +166,13 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
a.AttributeClass?.ToDisplayString() == AttributeName);
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
||||||
if (elemHasGenWriter)
|
if (elemHasGenWriter)
|
||||||
{
|
{
|
||||||
elemIsIId = resolvedElem.AllInterfaces.Any(ifc =>
|
var elemIidIface = resolvedElem.AllInterfaces.FirstOrDefault(ifc =>
|
||||||
ifc.IsGenericType &&
|
ifc.IsGenericType &&
|
||||||
ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
||||||
|
elemIsIId = elemIidIface != null;
|
||||||
|
if (elemIidIface != null)
|
||||||
|
elemIdTypeName = elemIidIface.TypeArguments[0].ToDisplayString();
|
||||||
|
|
||||||
var elemFlatName = BuildFlatName((INamedTypeSymbol)resolvedElem);
|
var elemFlatName = BuildFlatName((INamedTypeSymbol)resolvedElem);
|
||||||
var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace
|
var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace
|
||||||
? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString();
|
? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString();
|
||||||
|
|
@ -184,17 +194,24 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
typeNameForTypeof,
|
typeNameForTypeof,
|
||||||
kind,
|
kind,
|
||||||
p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type),
|
p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type),
|
||||||
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName,
|
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName,
|
||||||
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, collKind, elemFullTypeName,
|
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName,
|
||||||
childTypeNameHash, childPropertyHashes,
|
childTypeNameHash, childPropertyHashes,
|
||||||
elementTypeNameHash, elementPropertyHashes));
|
elementTypeNameHash, elementPropertyHashes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IId<T>: Id first (index 0), then alphabetical — matches runtime TypeMetadataBase ordering
|
// IId<T>: Id first (index 0), then alphabetical — matches runtime TypeMetadataBase ordering
|
||||||
var isIId = typeSymbol.AllInterfaces.Any(i =>
|
var isIId = false;
|
||||||
|
string? idTypeName = null;
|
||||||
|
var iidInterface = typeSymbol.AllInterfaces.FirstOrDefault(i =>
|
||||||
i.IsGenericType &&
|
i.IsGenericType &&
|
||||||
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
||||||
|
if (iidInterface != null)
|
||||||
|
{
|
||||||
|
isIId = true;
|
||||||
|
idTypeName = iidInterface.TypeArguments[0].ToDisplayString();
|
||||||
|
}
|
||||||
|
|
||||||
if (isIId)
|
if (isIId)
|
||||||
properties.Sort((a, b) =>
|
properties.Sort((a, b) =>
|
||||||
|
|
@ -210,7 +227,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
var className = BuildFlatName(typeSymbol);
|
var className = BuildFlatName(typeSymbol);
|
||||||
var typeNameHash = ComputeFnvHash(typeSymbol.Name);
|
var typeNameHash = ComputeFnvHash(typeSymbol.Name);
|
||||||
var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray();
|
var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray();
|
||||||
return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, typeNameHash, propertyNameHashes);
|
return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, typeNameHash, propertyNameHashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Execute(ImmutableArray<SerializableClassInfo?> classes, SourceProductionContext context)
|
private static void Execute(ImmutableArray<SerializableClassInfo?> classes, SourceProductionContext context)
|
||||||
|
|
@ -227,14 +244,13 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
|
|
||||||
private static string GenWriter(SerializableClassInfo ci)
|
private static string GenWriter(SerializableClassInfo ci)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(2048);
|
var sb = new StringBuilder(4096);
|
||||||
sb.AppendLine("// <auto-generated/>");
|
sb.AppendLine("// <auto-generated/>");
|
||||||
sb.AppendLine("#nullable enable");
|
sb.AppendLine("#nullable enable");
|
||||||
sb.AppendLine("using System.Runtime.CompilerServices;");
|
sb.AppendLine("using System.Runtime.CompilerServices;");
|
||||||
sb.AppendLine("using AyCode.Core.Serializers.Binaries;");
|
sb.AppendLine("using AyCode.Core.Serializers.Binaries;");
|
||||||
// ReferenceHandlingMode is needed when any Complex/Collection property has direct object write
|
// ReferenceHandlingMode is needed for ScanObject self ref tracking and direct object write/scan
|
||||||
if (ci.Properties.Any(p => p.HasGeneratedWriter || p.ElementHasGeneratedWriter))
|
sb.AppendLine("using AyCode.Core.Serializers;");
|
||||||
sb.AppendLine("using AyCode.Core.Serializers;");
|
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
if (!string.IsNullOrEmpty(ci.Namespace))
|
if (!string.IsNullOrEmpty(ci.Namespace))
|
||||||
sb.AppendLine($"namespace {ci.Namespace};");
|
sb.AppendLine($"namespace {ci.Namespace};");
|
||||||
|
|
@ -243,6 +259,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
sb.AppendLine("{");
|
sb.AppendLine("{");
|
||||||
sb.AppendLine($" internal static readonly {ci.ClassName}_GeneratedWriter Instance = new();");
|
sb.AppendLine($" internal static readonly {ci.ClassName}_GeneratedWriter Instance = new();");
|
||||||
sb.AppendLine($" internal static readonly int s_metadataSlot = AcBinarySerializer.AllocateMetadataSlot();");
|
sb.AppendLine($" internal static readonly int s_metadataSlot = AcBinarySerializer.AllocateMetadataSlot();");
|
||||||
|
sb.AppendLine($" internal static readonly int s_trackingSlot = AcBinarySerializer.AllocateTrackingSlot();");
|
||||||
sb.AppendLine($" internal static readonly int s_typeNameHash = {ci.TypeNameHash};");
|
sb.AppendLine($" internal static readonly int s_typeNameHash = {ci.TypeNameHash};");
|
||||||
sb.Append( $" internal static readonly int[] s_propertyHashes = new int[] {{ ");
|
sb.Append( $" internal static readonly int[] s_propertyHashes = new int[] {{ ");
|
||||||
sb.Append(string.Join(", ", ci.PropertyNameHashes));
|
sb.Append(string.Join(", ", ci.PropertyNameHashes));
|
||||||
|
|
@ -259,10 +276,108 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.AppendLine(" }");
|
sb.AppendLine(" }");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// ScanObject — full scan pass (null/depth + self ref tracking + property scan)
|
||||||
|
GenScanProperties(sb, ci);
|
||||||
|
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// ScanForDuplicates — instance method on IGeneratedBinaryWriter, called from Serialize
|
||||||
|
sb.AppendLine(" public void ScanForDuplicates<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context)");
|
||||||
|
sb.AppendLine(" where TOutput : struct, IBinaryOutputBase");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
sb.AppendLine(" if (!context.HasCaching) return;");
|
||||||
|
sb.AppendLine(" ScanObject(value, context, 0);");
|
||||||
|
sb.AppendLine(" context.SortWritePlan();");
|
||||||
|
sb.AppendLine(" }");
|
||||||
|
|
||||||
sb.AppendLine("}");
|
sb.AppendLine("}");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the ScanObject method — full scan pass entry point for this type.
|
||||||
|
/// Includes: null/depth check, self ref tracking (IId or All mode), property scan.
|
||||||
|
/// Only emits code for reference properties (strings + complex types) — primitives are skipped.
|
||||||
|
/// </summary>
|
||||||
|
private static void GenScanProperties(StringBuilder sb, SerializableClassInfo ci)
|
||||||
|
{
|
||||||
|
sb.AppendLine(" public void ScanObject<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth) where TOutput : struct, IBinaryOutputBase");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
|
||||||
|
// Null/depth guard — matches runtime ScanValue entry
|
||||||
|
sb.AppendLine(" if (value == null || depth > context.MaxDepth) return;");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);");
|
||||||
|
|
||||||
|
// Self ref tracking — matches runtime ScanValue UseTypeReferenceHandling block
|
||||||
|
if (ci.IsIId)
|
||||||
|
{
|
||||||
|
// IId type: track when ReferenceHandling != None
|
||||||
|
var trackMethod = ci.IdTypeName switch
|
||||||
|
{
|
||||||
|
"int" => "ScanTrackObjectInt32",
|
||||||
|
"long" => "ScanTrackObjectInt64",
|
||||||
|
"System.Guid" => "ScanTrackObjectGuid",
|
||||||
|
_ => "ScanTrackObjectInt32"
|
||||||
|
};
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.None)");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
sb.AppendLine($" if (!context.{trackMethod}(s_trackingSlot, obj.Id))");
|
||||||
|
sb.AppendLine(" return;");
|
||||||
|
sb.AppendLine(" }");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Non-IId type: track when ReferenceHandling == All
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(" if (context.ReferenceHandling == ReferenceHandlingMode.All)");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
sb.AppendLine(" if (!context.ScanTrackObjectInt32(s_trackingSlot, System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value)))");
|
||||||
|
sb.AppendLine(" return;");
|
||||||
|
sb.AppendLine(" }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect scannable properties
|
||||||
|
var scanProps = ci.Properties.Where(p =>
|
||||||
|
p.TypeKind == PropertyTypeKind.String ||
|
||||||
|
p.TypeKind == PropertyTypeKind.Complex ||
|
||||||
|
p.TypeKind == PropertyTypeKind.Collection).ToList();
|
||||||
|
|
||||||
|
// Hoist UseStringInterning + IsValidForInterningString checks if any string scanning needed
|
||||||
|
var hasStringScan = scanProps.Any(p =>
|
||||||
|
(p.TypeKind == PropertyTypeKind.String && p.InterningFlags != 0) ||
|
||||||
|
(p.TypeKind == PropertyTypeKind.Collection && p.ElementKind == PropertyTypeKind.String && p.InterningFlags != 0));
|
||||||
|
|
||||||
|
if (hasStringScan)
|
||||||
|
{
|
||||||
|
// Hoist the shift once — per-property InterningFlags check uses internBit directly.
|
||||||
|
// Cannot combine flags (OR) because different properties may have different flags
|
||||||
|
// and Attribute mode must NOT scan All-only properties.
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(" var internBit = 1 << (int)context.Options.UseStringInterning;");
|
||||||
|
sb.AppendLine(" int minIntern = 0, maxIntern = 0;");
|
||||||
|
sb.AppendLine(" if (internBit > 1) { minIntern = context.MinStringInternLength; maxIntern = context.MaxStringInternLength; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasAnyScanProp = false;
|
||||||
|
foreach (var p in scanProps)
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
hasAnyScanProp = true;
|
||||||
|
EmitScanProp(sb, p, " ", ci.FullTypeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAnyScanProp)
|
||||||
|
{
|
||||||
|
sb.AppendLine(" // No reference properties to scan");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine(" }");
|
||||||
|
}
|
||||||
|
|
||||||
private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName)
|
private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName)
|
||||||
{
|
{
|
||||||
var a = $"obj.{p.Name}";
|
var a = $"obj.{p.Name}";
|
||||||
|
|
@ -396,6 +511,200 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Emits direct object write — bypasses GetWrapper + WriteObject entirely.
|
/// Emits direct object write — bypasses GetWrapper + WriteObject entirely.
|
||||||
|
#region Scan Pass Code Generation
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Emits scan pass code for a single property.
|
||||||
|
/// String: interning check + ScanInternString.
|
||||||
|
/// Complex (SGen): ref tracking via slot IdentityMap + recursive ScanProperties.
|
||||||
|
/// Complex (no SGen): fallback to ScanValueGenerated (runtime wrapper lookup).
|
||||||
|
/// Collection: iterate elements with same patterns.
|
||||||
|
/// </summary>
|
||||||
|
private static void EmitScanProp(StringBuilder sb, PropInfo p, string i, string fullTypeName)
|
||||||
|
{
|
||||||
|
var a = $"obj.{p.Name}";
|
||||||
|
|
||||||
|
// PropertyFilter: must match write pass — if filter skips property, scan must skip too
|
||||||
|
// Only for non-markerless properties (matching EmitProp behavior)
|
||||||
|
if (!IsMarkerless(p.TypeKind))
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{i}if (context.HasPropertyFilter)");
|
||||||
|
sb.AppendLine($"{i}{{");
|
||||||
|
sb.AppendLine($"{i} var fc_{p.Name} = new BinaryPropertyFilterContext(obj, typeof({fullTypeName}), \"{p.Name}\", typeof({p.TypeNameForTypeof}), static o => (({fullTypeName})o).{p.Name});");
|
||||||
|
sb.AppendLine($"{i} if (!context.PropertyFilter!(in fc_{p.Name}))");
|
||||||
|
sb.AppendLine($"{i} goto scanskip_{p.Name};");
|
||||||
|
sb.AppendLine($"{i}}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (p.TypeKind)
|
||||||
|
{
|
||||||
|
case PropertyTypeKind.String:
|
||||||
|
EmitScanString(sb, p, a, i);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PropertyTypeKind.Complex:
|
||||||
|
if (p.HasGeneratedWriter)
|
||||||
|
EmitScanComplexSGen(sb, p, a, i);
|
||||||
|
else
|
||||||
|
EmitScanComplexRuntime(sb, p, a, i);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PropertyTypeKind.Collection:
|
||||||
|
EmitScanCollection(sb, p, a, i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsMarkerless(p.TypeKind))
|
||||||
|
sb.AppendLine($"{i}scanskip_{p.Name}:;");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Emits scan pass code for a string property: interning flags check + ScanInternString.
|
||||||
|
/// </summary>
|
||||||
|
private static void EmitScanString(StringBuilder sb, PropInfo p, string a, string i)
|
||||||
|
{
|
||||||
|
if (p.InterningFlags == 0)
|
||||||
|
{
|
||||||
|
// Never interned (explicit [AcStringIntern(false)] or no flags) — skip entirely
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-property InterningFlags check with hoisted internBit (context.Options read once)
|
||||||
|
sb.AppendLine($"{i}if (({p.InterningFlags} & internBit) != 0)");
|
||||||
|
sb.AppendLine($"{i}{{");
|
||||||
|
sb.AppendLine($"{i} var str_{p.Name} = {a};");
|
||||||
|
sb.AppendLine($"{i} if (str_{p.Name} != null)");
|
||||||
|
sb.AppendLine($"{i} {{");
|
||||||
|
sb.AppendLine($"{i} var slen_{p.Name} = str_{p.Name}.Length;");
|
||||||
|
sb.AppendLine($"{i} if (slen_{p.Name} >= minIntern && (maxIntern == 0 || slen_{p.Name} <= maxIntern))");
|
||||||
|
sb.AppendLine($"{i} context.ScanInternString(str_{p.Name});");
|
||||||
|
sb.AppendLine($"{i} }}");
|
||||||
|
sb.AppendLine($"{i}}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Emits scan pass code for a Complex property with SGen writer.
|
||||||
|
/// No parent-side ref tracking — child ScanObject handles its own (ScanTrackObjectXxx).
|
||||||
|
/// </summary>
|
||||||
|
private static void EmitScanComplexSGen(StringBuilder sb, PropInfo p, string a, string i)
|
||||||
|
{
|
||||||
|
var writer = p.WriterClassName;
|
||||||
|
var childVar = $"sc_{p.Name}";
|
||||||
|
|
||||||
|
// Null check only — ScanObject handles depth + ref tracking internally
|
||||||
|
sb.AppendLine($"{i}var {childVar} = {a};");
|
||||||
|
sb.AppendLine($"{i}if ({childVar} != null)");
|
||||||
|
sb.AppendLine($"{i} {writer}.Instance.ScanObject({childVar}, context, depth + 1);");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Emits scan pass code for a Complex property without SGen writer (runtime fallback).
|
||||||
|
/// </summary>
|
||||||
|
private static void EmitScanComplexRuntime(StringBuilder sb, PropInfo p, string a, string i)
|
||||||
|
{
|
||||||
|
var childVar = $"sc_{p.Name}";
|
||||||
|
sb.AppendLine($"{i}var {childVar} = {a};");
|
||||||
|
sb.AppendLine($"{i}if ({childVar} != null)");
|
||||||
|
sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated({childVar}, typeof({p.TypeNameForTypeof}), context, depth + 1);");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Emits scan pass code for a Collection property.
|
||||||
|
/// Handles string collections (interning) and complex element collections (SGen or runtime fallback).
|
||||||
|
/// </summary>
|
||||||
|
private static void EmitScanCollection(StringBuilder sb, PropInfo p, string a, string i)
|
||||||
|
{
|
||||||
|
// String element collection
|
||||||
|
if (p.ElementKind == PropertyTypeKind.String)
|
||||||
|
{
|
||||||
|
if (p.InterningFlags == 0) return; // never interned
|
||||||
|
|
||||||
|
sb.AppendLine($"{i}var scol_{p.Name} = {a};");
|
||||||
|
sb.AppendLine($"{i}if (scol_{p.Name} != null && ({p.InterningFlags} & internBit) != 0)");
|
||||||
|
sb.AppendLine($"{i}{{");
|
||||||
|
|
||||||
|
if (p.CollectionKind == "Array")
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Length; si_{p.Name}++)");
|
||||||
|
sb.AppendLine($"{i} {{");
|
||||||
|
sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
|
||||||
|
}
|
||||||
|
else if (p.CollectionKind == "List")
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)");
|
||||||
|
sb.AppendLine($"{i} {{");
|
||||||
|
sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{i} foreach (var se_{p.Name} in scol_{p.Name})");
|
||||||
|
sb.AppendLine($"{i} {{");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine($"{i} if (se_{p.Name} != null)");
|
||||||
|
sb.AppendLine($"{i} {{");
|
||||||
|
sb.AppendLine($"{i} var slen_{p.Name} = se_{p.Name}.Length;");
|
||||||
|
sb.AppendLine($"{i} if (slen_{p.Name} >= minIntern && (maxIntern == 0 || slen_{p.Name} <= maxIntern))");
|
||||||
|
sb.AppendLine($"{i} context.ScanInternString(se_{p.Name});");
|
||||||
|
sb.AppendLine($"{i} }}");
|
||||||
|
sb.AppendLine($"{i} }}");
|
||||||
|
sb.AppendLine($"{i}}}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complex element collection with SGen writer
|
||||||
|
if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.CollectionKind != null)
|
||||||
|
{
|
||||||
|
var writer = p.ElementWriterClassName;
|
||||||
|
|
||||||
|
sb.AppendLine($"{i}var scol_{p.Name} = {a};");
|
||||||
|
sb.AppendLine($"{i}if (scol_{p.Name} != null)");
|
||||||
|
sb.AppendLine($"{i}{{");
|
||||||
|
sb.AppendLine($"{i} var snd_{p.Name} = depth + 1;");
|
||||||
|
|
||||||
|
if (p.CollectionKind == "Array")
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Length; si_{p.Name}++)");
|
||||||
|
sb.AppendLine($"{i} {{");
|
||||||
|
sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
|
||||||
|
}
|
||||||
|
else if (p.CollectionKind == "List")
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)");
|
||||||
|
sb.AppendLine($"{i} {{");
|
||||||
|
sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
|
||||||
|
}
|
||||||
|
else // Counted (foreach)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{i} foreach (var se_{p.Name} in scol_{p.Name})");
|
||||||
|
sb.AppendLine($"{i} {{");
|
||||||
|
}
|
||||||
|
|
||||||
|
var e = $"se_{p.Name}";
|
||||||
|
// Null check only — ScanObject handles depth + ref tracking internally
|
||||||
|
sb.AppendLine($"{i} if ({e} == null) continue;");
|
||||||
|
sb.AppendLine($"{i} {writer}.Instance.ScanObject({e}, context, snd_{p.Name});");
|
||||||
|
sb.AppendLine($"{i} }}");
|
||||||
|
sb.AppendLine($"{i}}}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complex element collection without SGen writer — runtime fallback
|
||||||
|
if (p.ElementKind == PropertyTypeKind.Complex)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{i}var scol_{p.Name} = {a};");
|
||||||
|
sb.AppendLine($"{i}if (scol_{p.Name} != null)");
|
||||||
|
sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated(scol_{p.Name}, typeof({p.TypeNameForTypeof}), context, depth);");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primitive element collection — no scanning needed
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Emits inline object write for a Complex property that has a generated writer.
|
||||||
/// Writes marker bytes + inline metadata (UseMetadata) + calls child GeneratedWriter.WriteProperties.
|
/// Writes marker bytes + inline metadata (UseMetadata) + calls child GeneratedWriter.WriteProperties.
|
||||||
/// IId types: guard ReferenceHandling != None (tracked in OnlyId + All).
|
/// IId types: guard ReferenceHandling != None (tracked in OnlyId + All).
|
||||||
/// Non-IId types: guard ReferenceHandling == All (tracked only in All mode).
|
/// Non-IId types: guard ReferenceHandling == All (tracked only in All mode).
|
||||||
|
|
@ -888,12 +1197,16 @@ internal sealed class SerializableClassInfo
|
||||||
public string ClassName { get; }
|
public string ClassName { get; }
|
||||||
public string FullTypeName { get; }
|
public string FullTypeName { get; }
|
||||||
public List<PropInfo> Properties { get; }
|
public List<PropInfo> Properties { get; }
|
||||||
|
/// <summary>True if this type implements IId<T></summary>
|
||||||
|
public bool IsIId { get; }
|
||||||
|
/// <summary>The Id type name ("int", "long", "System.Guid") if IsIId, null otherwise</summary>
|
||||||
|
public string? IdTypeName { get; }
|
||||||
/// <summary>FNV-1a hash of ClassName (matches runtime SourceType.Name hash)</summary>
|
/// <summary>FNV-1a hash of ClassName (matches runtime SourceType.Name hash)</summary>
|
||||||
public int TypeNameHash { get; }
|
public int TypeNameHash { get; }
|
||||||
/// <summary>FNV-1a hash of each property name, in property order</summary>
|
/// <summary>FNV-1a hash of each property name, in property order</summary>
|
||||||
public int[] PropertyNameHashes { get; }
|
public int[] PropertyNameHashes { get; }
|
||||||
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> p, int typeNameHash, int[] propertyNameHashes)
|
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> p, bool isIId, string? idTypeName, int typeNameHash, int[] propertyNameHashes)
|
||||||
{ Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; }
|
{ Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class PropInfo
|
internal sealed class PropInfo
|
||||||
|
|
@ -920,6 +1233,8 @@ internal sealed class PropInfo
|
||||||
public bool IsIId { get; }
|
public bool IsIId { get; }
|
||||||
/// <summary>Generated writer class name, e.g. "SharedTag_GeneratedWriter". Only set when HasGeneratedWriter.</summary>
|
/// <summary>Generated writer class name, e.g. "SharedTag_GeneratedWriter". Only set when HasGeneratedWriter.</summary>
|
||||||
public string? WriterClassName { get; }
|
public string? WriterClassName { get; }
|
||||||
|
/// <summary>Id type name ("int", "long", "System.Guid") for IId child types. Null if not IId.</summary>
|
||||||
|
public string? IdTypeName { get; }
|
||||||
|
|
||||||
// Collection element metadata — set when TypeKind == Collection and element type is Complex with generated writer
|
// Collection element metadata — set when TypeKind == Collection and element type is Complex with generated writer
|
||||||
/// <summary>Element type kind for collection properties. Only meaningful when TypeKind == Collection.</summary>
|
/// <summary>Element type kind for collection properties. Only meaningful when TypeKind == Collection.</summary>
|
||||||
|
|
@ -930,6 +1245,8 @@ internal sealed class PropInfo
|
||||||
public bool ElementIsIId { get; }
|
public bool ElementIsIId { get; }
|
||||||
/// <summary>Generated writer class name for collection element type.</summary>
|
/// <summary>Generated writer class name for collection element type.</summary>
|
||||||
public string? ElementWriterClassName { get; }
|
public string? ElementWriterClassName { get; }
|
||||||
|
/// <summary>Id type name for collection element IId types. Null if not IId.</summary>
|
||||||
|
public string? ElementIdTypeName { get; }
|
||||||
/// <summary>Collection type: "List", "Array", or null (unknown — fallback to runtime).</summary>
|
/// <summary>Collection type: "List", "Array", or null (unknown — fallback to runtime).</summary>
|
||||||
public string? CollectionKind { get; }
|
public string? CollectionKind { get; }
|
||||||
/// <summary>Full element type name for generated code (e.g. "SharedTag").</summary>
|
/// <summary>Full element type name for generated code (e.g. "SharedTag").</summary>
|
||||||
|
|
@ -946,9 +1263,9 @@ internal sealed class PropInfo
|
||||||
public int[]? ElementPropertyHashes { get; }
|
public int[]? ElementPropertyHashes { get; }
|
||||||
|
|
||||||
public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable,
|
public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable,
|
||||||
bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null,
|
bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null, string? idTypeName = null,
|
||||||
PropertyTypeKind elementKind = PropertyTypeKind.Unknown, bool elementHasGenWriter = false, bool elementIsIId = false,
|
PropertyTypeKind elementKind = PropertyTypeKind.Unknown, bool elementHasGenWriter = false, bool elementIsIId = false,
|
||||||
string? elementWriterClassName = null, string? collectionKind = null, string? elementFullTypeName = null,
|
string? elementWriterClassName = null, string? elementIdTypeName = null, string? collectionKind = null, string? elementFullTypeName = null,
|
||||||
int childTypeNameHash = 0, int[]? childPropertyHashes = null,
|
int childTypeNameHash = 0, int[]? childPropertyHashes = null,
|
||||||
int elementTypeNameHash = 0, int[]? elementPropertyHashes = null)
|
int elementTypeNameHash = 0, int[]? elementPropertyHashes = null)
|
||||||
{
|
{
|
||||||
|
|
@ -960,10 +1277,12 @@ internal sealed class PropInfo
|
||||||
HasGeneratedWriter = hasGeneratedWriter;
|
HasGeneratedWriter = hasGeneratedWriter;
|
||||||
IsIId = isIId;
|
IsIId = isIId;
|
||||||
WriterClassName = writerClassName;
|
WriterClassName = writerClassName;
|
||||||
|
IdTypeName = idTypeName;
|
||||||
ElementKind = elementKind;
|
ElementKind = elementKind;
|
||||||
ElementHasGeneratedWriter = elementHasGenWriter;
|
ElementHasGeneratedWriter = elementHasGenWriter;
|
||||||
ElementIsIId = elementIsIId;
|
ElementIsIId = elementIsIId;
|
||||||
ElementWriterClassName = elementWriterClassName;
|
ElementWriterClassName = elementWriterClassName;
|
||||||
|
ElementIdTypeName = elementIdTypeName;
|
||||||
CollectionKind = collectionKind;
|
CollectionKind = collectionKind;
|
||||||
ElementFullTypeName = elementFullTypeName;
|
ElementFullTypeName = elementFullTypeName;
|
||||||
ChildTypeNameHash = childTypeNameHash;
|
ChildTypeNameHash = childTypeNameHash;
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,18 @@ internal sealed class TestOrderWriter : IGeneratedBinaryWriter
|
||||||
context.WriteDecimalBits(obj.TotalAmount);
|
context.WriteDecimalBits(obj.TotalAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ScanObject<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth) where TOutput : struct, IBinaryOutputBase
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ScanForDuplicates<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context) where TOutput : struct, IBinaryOutputBase
|
||||||
|
{
|
||||||
|
if (!context.HasCaching) return;
|
||||||
|
ScanObject(value, context, 0);
|
||||||
|
context.SortWritePlan();
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void WriteComplexOrNull<TOutput>(object? value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth)
|
private static void WriteComplexOrNull<TOutput>(object? value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth)
|
||||||
where TOutput : struct, IBinaryOutputBase
|
where TOutput : struct, IBinaryOutputBase
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ public abstract class AcSerializerOptions
|
||||||
set => _referenceHandling = value;
|
set => _referenceHandling = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ReferenceHandlingMode _referenceHandling = ReferenceHandlingMode.All;
|
private ReferenceHandlingMode _referenceHandling = ReferenceHandlingMode.OnlyId;
|
||||||
|
|
||||||
private readonly byte _maxDepth = byte.MaxValue;
|
private readonly byte _maxDepth = byte.MaxValue;
|
||||||
private readonly bool _throwOnCircularReference = true;
|
private readonly bool _throwOnCircularReference = true;
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,28 @@ namespace AyCode.Core.Serializers.Attributes;
|
||||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
|
||||||
public sealed class AcBinarySerializableAttribute : Attribute
|
public sealed class AcBinarySerializableAttribute : Attribute
|
||||||
{
|
{
|
||||||
|
public bool EnableMetadataFeature { get; }
|
||||||
|
public bool EnableIdTrackingFeature { get; }
|
||||||
|
public bool EnableRefHandlingFeature { get; }
|
||||||
|
public bool EnableInternStringFeature { get; }
|
||||||
|
|
||||||
|
public AcBinarySerializableAttribute() : this(true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public AcBinarySerializableAttribute(bool enableAllFeatures)
|
||||||
|
{
|
||||||
|
EnableMetadataFeature = enableAllFeatures;
|
||||||
|
EnableIdTrackingFeature = enableAllFeatures;
|
||||||
|
EnableRefHandlingFeature = enableAllFeatures;
|
||||||
|
EnableInternStringFeature = enableAllFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AcBinarySerializableAttribute(bool enableMetadataFeature, bool enableIdTrackingFeature, bool enableRefHandlingFeature, bool enableInternStringFeature)
|
||||||
|
{
|
||||||
|
EnableMetadataFeature = enableMetadataFeature;
|
||||||
|
EnableIdTrackingFeature = enableIdTrackingFeature;
|
||||||
|
EnableRefHandlingFeature = enableRefHandlingFeature;
|
||||||
|
EnableInternStringFeature = enableInternStringFeature;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,14 @@ public static partial class AcBinarySerializer
|
||||||
private int _nextCacheIndex; // Next dense cache index to assign (starts at 0, uses ++_nextCacheIndex)
|
private int _nextCacheIndex; // Next dense cache index to assign (starts at 0, uses ++_nextCacheIndex)
|
||||||
public int NextFirstIndex; // Next first occurrence index for scan pass. Direct access for performance.
|
public int NextFirstIndex; // Next first occurrence index for scan pass. Direct access for performance.
|
||||||
|
|
||||||
|
#region Slot-based IdentityMaps for SGen scan pass (replaces wrapper-based tracking)
|
||||||
|
|
||||||
|
private IdentityMap<int, InternEntry>?[]? _slottedIdMapsInt32;
|
||||||
|
private IdentityMap<long, InternEntry>?[]? _slottedIdMapsInt64;
|
||||||
|
private IdentityMap<Guid, InternEntry>?[]? _slottedIdMapsGuid;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region WriteDuplicateEntry — scan pass output for write pass cursor
|
#region WriteDuplicateEntry — scan pass output for write pass cursor
|
||||||
|
|
||||||
private WriteDuplicateEntry[]? _writePlan;
|
private WriteDuplicateEntry[]? _writePlan;
|
||||||
|
|
@ -281,6 +289,9 @@ public static partial class AcBinarySerializer
|
||||||
_stringInternMap?.Reset();
|
_stringInternMap?.Reset();
|
||||||
_metadataSeenBits.AsSpan().Clear();
|
_metadataSeenBits.AsSpan().Clear();
|
||||||
_metadataSeenOverflow?.Clear();
|
_metadataSeenOverflow?.Clear();
|
||||||
|
ResetSlottedMaps(_slottedIdMapsInt32);
|
||||||
|
ResetSlottedMaps(_slottedIdMapsInt64);
|
||||||
|
ResetSlottedMaps(_slottedIdMapsGuid);
|
||||||
_nextCacheIndex = 0;
|
_nextCacheIndex = 0;
|
||||||
NextFirstIndex = 0;
|
NextFirstIndex = 0;
|
||||||
ScanVisitIndex = 0;
|
ScanVisitIndex = 0;
|
||||||
|
|
@ -769,6 +780,140 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Slot-based Scan Pass Ref Tracking (SGen)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SGen scan pass: tracks an object by Int32 Id (IId or RuntimeHelpers.GetHashCode for non-IId).
|
||||||
|
/// Increments ScanVisitIndex, manages IdentityMap via slot, and builds WriteDuplicateEntry on duplicates.
|
||||||
|
/// Returns true if first occurrence (caller should scan children), false if duplicate (skip children).
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool ScanTrackObjectInt32(int slot, int id)
|
||||||
|
{
|
||||||
|
var visitIndex = ScanVisitIndex++;
|
||||||
|
|
||||||
|
if (id == 0) return true; // default Id — no tracking
|
||||||
|
|
||||||
|
ref var maps = ref _slottedIdMapsInt32;
|
||||||
|
if (maps == null || maps.Length <= slot)
|
||||||
|
GrowSlottedMaps(ref maps, slot);
|
||||||
|
|
||||||
|
var map = maps[slot] ??= new IdentityMap<int, InternEntry>();
|
||||||
|
|
||||||
|
if (!map.TryAdd(id, out var si))
|
||||||
|
{
|
||||||
|
ref var entry = ref map.GetValueRef(si);
|
||||||
|
if (entry.CacheIndex == -1)
|
||||||
|
{
|
||||||
|
entry.CacheIndex = ++_nextCacheIndex;
|
||||||
|
entry.IsFirstWrite = true;
|
||||||
|
AddWriteDuplicateEntry(entry.FirstIndex, entry.CacheIndex, isFirst: true, value: null);
|
||||||
|
}
|
||||||
|
AddWriteDuplicateEntry(visitIndex, entry.CacheIndex, isFirst: false, value: null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var ne = ref map.GetValueRef(si);
|
||||||
|
ne.FirstIndex = visitIndex;
|
||||||
|
ne.CacheIndex = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SGen scan pass: tracks an object by Int64 Id.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool ScanTrackObjectInt64(int slot, long id)
|
||||||
|
{
|
||||||
|
var visitIndex = ScanVisitIndex++;
|
||||||
|
|
||||||
|
if (id == 0) return true;
|
||||||
|
|
||||||
|
ref var maps = ref _slottedIdMapsInt64;
|
||||||
|
if (maps == null || maps.Length <= slot)
|
||||||
|
GrowSlottedMaps(ref maps, slot);
|
||||||
|
|
||||||
|
var map = maps[slot] ??= new IdentityMap<long, InternEntry>();
|
||||||
|
|
||||||
|
if (!map.TryAdd(id, out var si))
|
||||||
|
{
|
||||||
|
ref var entry = ref map.GetValueRef(si);
|
||||||
|
if (entry.CacheIndex == -1)
|
||||||
|
{
|
||||||
|
entry.CacheIndex = ++_nextCacheIndex;
|
||||||
|
entry.IsFirstWrite = true;
|
||||||
|
AddWriteDuplicateEntry(entry.FirstIndex, entry.CacheIndex, isFirst: true, value: null);
|
||||||
|
}
|
||||||
|
AddWriteDuplicateEntry(visitIndex, entry.CacheIndex, isFirst: false, value: null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var ne = ref map.GetValueRef(si);
|
||||||
|
ne.FirstIndex = visitIndex;
|
||||||
|
ne.CacheIndex = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SGen scan pass: tracks an object by Guid Id.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool ScanTrackObjectGuid(int slot, Guid id)
|
||||||
|
{
|
||||||
|
var visitIndex = ScanVisitIndex++;
|
||||||
|
|
||||||
|
if (id == Guid.Empty) return true;
|
||||||
|
|
||||||
|
ref var maps = ref _slottedIdMapsGuid;
|
||||||
|
if (maps == null || maps.Length <= slot)
|
||||||
|
GrowSlottedMaps(ref maps, slot);
|
||||||
|
|
||||||
|
var map = maps[slot] ??= new IdentityMap<Guid, InternEntry>();
|
||||||
|
|
||||||
|
if (!map.TryAdd(id, out var si))
|
||||||
|
{
|
||||||
|
ref var entry = ref map.GetValueRef(si);
|
||||||
|
if (entry.CacheIndex == -1)
|
||||||
|
{
|
||||||
|
entry.CacheIndex = ++_nextCacheIndex;
|
||||||
|
entry.IsFirstWrite = true;
|
||||||
|
AddWriteDuplicateEntry(entry.FirstIndex, entry.CacheIndex, isFirst: true, value: null);
|
||||||
|
}
|
||||||
|
AddWriteDuplicateEntry(visitIndex, entry.CacheIndex, isFirst: false, value: null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var ne = ref map.GetValueRef(si);
|
||||||
|
ne.FirstIndex = visitIndex;
|
||||||
|
ne.CacheIndex = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private static void GrowSlottedMaps<TMap>(ref TMap?[]? maps, int slot) where TMap : class
|
||||||
|
{
|
||||||
|
var newSize = Math.Max(slot + 1, 16);
|
||||||
|
if (maps == null)
|
||||||
|
{
|
||||||
|
maps = new TMap?[newSize];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var newMaps = new TMap?[newSize];
|
||||||
|
maps.AsSpan().CopyTo(newMaps);
|
||||||
|
maps = newMaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static void ResetSlottedMaps<TKey, TValue>(IdentityMap<TKey, TValue>?[]? maps)
|
||||||
|
where TKey : notnull where TValue : struct
|
||||||
|
{
|
||||||
|
if (maps == null) return;
|
||||||
|
for (var i = 0; i < maps.Length; i++)
|
||||||
|
maps[i]?.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region UseMetadata Type Tracking
|
#region UseMetadata Type Tracking
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ public static partial class AcBinarySerializer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Type? GeneratedSerializerType { get; }
|
public Type? GeneratedSerializerType { get; }
|
||||||
|
|
||||||
|
public bool EnableInternString { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lazy-computed FNV-1a hash of the type name (SourceType.Name).
|
/// Lazy-computed FNV-1a hash of the type name (SourceType.Name).
|
||||||
/// Only computed once per type, on first access when UseMetadata=true.
|
/// Only computed once per type, on first access when UseMetadata=true.
|
||||||
|
|
@ -78,7 +80,7 @@ public static partial class AcBinarySerializer
|
||||||
for (var i = 0; i < Properties.Length; i++)
|
for (var i = 0; i < Properties.Length; i++)
|
||||||
{
|
{
|
||||||
var prop = Properties[i];
|
var prop = Properties[i];
|
||||||
if (prop.IsComplexType || prop.AccessorType == PropertyAccessorType.String)
|
if (prop.IsComplexType || (EnableInternString && prop.AccessorType == PropertyAccessorType.String))
|
||||||
list.Add(prop);
|
list.Add(prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,12 +122,16 @@ public static partial class AcBinarySerializer
|
||||||
// Use pre-computed WritableProperties directly - no method call overhead!
|
// Use pre-computed WritableProperties directly - no method call overhead!
|
||||||
var orderedProperties = WritableProperties;
|
var orderedProperties = WritableProperties;
|
||||||
|
|
||||||
|
// Read [AcBinarySerializable] once per type — passed to property accessors
|
||||||
|
var serializableAttr = type.GetCustomAttribute<AcBinarySerializableAttribute>(inherit: false);
|
||||||
|
EnableInternString = serializableAttr == null || serializableAttr.EnableInternStringFeature;
|
||||||
|
|
||||||
Properties = new BinaryPropertyAccessor[orderedProperties.Length];
|
Properties = new BinaryPropertyAccessor[orderedProperties.Length];
|
||||||
var complexCount = 0;
|
var complexCount = 0;
|
||||||
|
|
||||||
for (var i = 0; i < orderedProperties.Length; i++)
|
for (var i = 0; i < orderedProperties.Length; i++)
|
||||||
{
|
{
|
||||||
var accessor = new BinaryPropertyAccessor(orderedProperties[i], type);
|
var accessor = new BinaryPropertyAccessor(orderedProperties[i], type, EnableInternString);
|
||||||
accessor.PropertyIndex = i;
|
accessor.PropertyIndex = i;
|
||||||
Properties[i] = accessor;
|
Properties[i] = accessor;
|
||||||
|
|
||||||
|
|
@ -176,8 +182,8 @@ public static partial class AcBinarySerializer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class BinaryPropertyAccessor : BinaryPropertyAccessorBase
|
internal sealed class BinaryPropertyAccessor : BinaryPropertyAccessorBase
|
||||||
{
|
{
|
||||||
public BinaryPropertyAccessor(PropertyInfo prop, Type declaringType)
|
public BinaryPropertyAccessor(PropertyInfo prop, Type declaringType, bool enableInternString)
|
||||||
: base(prop, declaringType)
|
: base(prop, declaringType, enableInternString)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ public static partial class AcBinarySerializer
|
||||||
/// - Consistent with write pass which writes ObjectRef (no children) for 2nd occurrence
|
/// - Consistent with write pass which writes ObjectRef (no children) for 2nd occurrence
|
||||||
/// - Strings/objects skipped here are never written anyway (parent is ObjectRef)
|
/// - Strings/objects skipped here are never written anyway (parent is ObjectRef)
|
||||||
/// CacheIndex is assigned immediately on 2nd occurrence (no post-processing needed).
|
/// CacheIndex is assigned immediately on 2nd occurrence (no post-processing needed).
|
||||||
|
/// Uses wrapper.GeneratedWriter when available — the wrapper is already cached,
|
||||||
|
/// so no dictionary lookup overhead. SGen types call generated ScanForDuplicates
|
||||||
|
/// which bypasses the entire runtime scan path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void ScanForDuplicates<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context)
|
private static void ScanForDuplicates<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context)
|
||||||
where TOutput : struct, IBinaryOutputBase
|
where TOutput : struct, IBinaryOutputBase
|
||||||
|
|
@ -22,8 +25,18 @@ public static partial class AcBinarySerializer
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var wrapper = context.GetWrapper(type);
|
var wrapper = context.GetWrapper(type);
|
||||||
ScanValue(value, wrapper, context, 0);
|
|
||||||
|
|
||||||
|
// SGen path: wrapper.GeneratedWriter is cached (no registry lookup per call).
|
||||||
|
// Generated ScanForDuplicates handles HasCaching + ScanObject + SortWritePlan.
|
||||||
|
var genWriter = wrapper.GeneratedWriter;
|
||||||
|
if (genWriter != null && context.Options.UseGeneratedCode)
|
||||||
|
{
|
||||||
|
genWriter.ScanObject(value, context, 0);
|
||||||
|
context.SortWritePlan();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanValue(value, wrapper, context, 0);
|
||||||
context.SortWritePlan();
|
context.SortWritePlan();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,6 +99,15 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
// Object → ref tracking + recursive scan
|
// Object → ref tracking + recursive scan
|
||||||
|
|
||||||
|
// SGen path: ScanObject handles its own ref tracking (ScanTrackObjectXxx)
|
||||||
|
// Must be checked BEFORE runtime ref tracking to avoid double ScanVisitIndex++
|
||||||
|
var genWriter = wrapper.GeneratedWriter;
|
||||||
|
if (genWriter != null && context.Options.UseGeneratedCode)
|
||||||
|
{
|
||||||
|
genWriter.ScanObject(value, context, depth);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Reference tracking for IId types (or all types when ReferenceHandling == All)
|
// Reference tracking for IId types (or all types when ReferenceHandling == All)
|
||||||
// 2nd occurrence → skip children because:
|
// 2nd occurrence → skip children because:
|
||||||
// 1. Write pass writes ObjectRef (no children) → strings/objects here are never in output
|
// 1. Write pass writes ObjectRef (no children) → strings/objects here are never in output
|
||||||
|
|
@ -135,8 +157,7 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursive scan on reference properties only
|
// Fallback: runtime property loop (no SGen writer for this type)
|
||||||
// Use typed getter for strings (much faster than reflection GetValue)
|
|
||||||
var refProperties = metadata.ReferenceProperties;
|
var refProperties = metadata.ReferenceProperties;
|
||||||
var hasPropertyFilter = context.HasPropertyFilter;
|
var hasPropertyFilter = context.HasPropertyFilter;
|
||||||
var nextDepth2 = depth + 1;
|
var nextDepth2 = depth + 1;
|
||||||
|
|
@ -144,7 +165,6 @@ public static partial class AcBinarySerializer
|
||||||
for (var i = 0; i < refProperties.Length; i++)
|
for (var i = 0; i < refProperties.Length; i++)
|
||||||
{
|
{
|
||||||
var prop = refProperties[i];
|
var prop = refProperties[i];
|
||||||
//context.CurrentProperty = prop;
|
|
||||||
|
|
||||||
// Must match write pass: filtered properties write PropertySkip (no value) →
|
// Must match write pass: filtered properties write PropertySkip (no value) →
|
||||||
// scanning them would assign CacheIndex for strings/objects never in output
|
// scanning them would assign CacheIndex for strings/objects never in output
|
||||||
|
|
@ -155,14 +175,12 @@ public static partial class AcBinarySerializer
|
||||||
{
|
{
|
||||||
if (!prop.UseStringPropertyInterning(context.Options.UseStringInterning)) continue;
|
if (!prop.UseStringPropertyInterning(context.Options.UseStringInterning)) continue;
|
||||||
|
|
||||||
// Fast path: typed getter for string
|
|
||||||
var str2 = prop.GetString(value);
|
var str2 = prop.GetString(value);
|
||||||
if (str2 != null && context.IsValidForInterningString(str2.Length))
|
if (str2 != null && context.IsValidForInterningString(str2.Length))
|
||||||
context.ScanInternString(str2);
|
context.ScanInternString(str2);
|
||||||
}
|
}
|
||||||
else if (prop.IsStringCollectionProperty)
|
else if (prop.IsStringCollectionProperty)
|
||||||
{
|
{
|
||||||
// String collection: per-property interning control, no GetWrapper needed
|
|
||||||
if (!prop.UseStringPropertyInterning(context.Options.UseStringInterning)) continue;
|
if (!prop.UseStringPropertyInterning(context.Options.UseStringInterning)) continue;
|
||||||
|
|
||||||
var propValue = prop.GetValue(value);
|
var propValue = prop.GetValue(value);
|
||||||
|
|
@ -171,7 +189,6 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Object property: use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism
|
|
||||||
var propValue = prop.GetValue(value);
|
var propValue = prop.GetValue(value);
|
||||||
if (propValue != null)
|
if (propValue != null)
|
||||||
{
|
{
|
||||||
|
|
@ -188,6 +205,17 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Public entry point for SGen-generated ScanProperties to call back into runtime ScanValue
|
||||||
|
/// for child types that don't have a generated writer (fallback to runtime path with wrapper lookup).
|
||||||
|
/// </summary>
|
||||||
|
internal static void ScanValueGenerated<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context, int depth)
|
||||||
|
where TOutput : struct, IBinaryOutputBase
|
||||||
|
{
|
||||||
|
var wrapper = context.GetWrapper(type);
|
||||||
|
ScanValue(value, wrapper, context, depth);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Scans string elements of a collection for interning. Uses IList fast path when available.
|
/// Scans string elements of a collection for interning. Uses IList fast path when available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -264,6 +264,19 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Scan Pass Tracking Slot Allocation
|
||||||
|
|
||||||
|
private static int s_nextTrackingSlot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocates a unique slot index for SGen scan pass ref tracking IdentityMaps.
|
||||||
|
/// Called once per SGen type at startup (ModuleInitializer). Thread-safe.
|
||||||
|
/// Slot indexes the per-context IdentityMap arrays in BinarySerializationContext.
|
||||||
|
/// </summary>
|
||||||
|
internal static int AllocateTrackingSlot() => Interlocked.Increment(ref s_nextTrackingSlot) - 1;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Thread-safe registry for generated writers. Looked up once per TypeMetadataWrapper creation.
|
/// Thread-safe registry for generated writers. Looked up once per TypeMetadataWrapper creation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using AyCode.Core.Serializers.Attributes;
|
||||||
using static AyCode.Core.Helpers.JsonUtilities;
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Binaries;
|
namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
@ -53,13 +54,13 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte? ExpectedTypeCode { get; }
|
public byte? ExpectedTypeCode { get; }
|
||||||
|
|
||||||
protected BinaryPropertyAccessorBase(PropertyInfo prop, Type declaringType)
|
protected BinaryPropertyAccessorBase(PropertyInfo prop, Type declaringType, bool enableInternString)
|
||||||
: base(prop, declaringType)
|
: base(prop, declaringType)
|
||||||
{
|
{
|
||||||
IsStringCollectionProperty = IsStringCollection(prop.PropertyType);
|
IsStringCollectionProperty = IsStringCollection(prop.PropertyType);
|
||||||
|
|
||||||
// All typed getters are initialized in PropertyAccessorBase
|
// All typed getters are initialized in PropertyAccessorBase
|
||||||
if (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty)
|
if (enableInternString && (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty))
|
||||||
{
|
{
|
||||||
// Cache [AcStringIntern] attribute (inherit: true to check base class properties)
|
// Cache [AcStringIntern] attribute (inherit: true to check base class properties)
|
||||||
var internAttr = prop.GetCustomAttribute<AcStringInternAttribute>(inherit: true);
|
var internAttr = prop.GetCustomAttribute<AcStringInternAttribute>(inherit: true);
|
||||||
|
|
@ -67,7 +68,7 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||||
var stringInternAttributeValue = internAttr?.Enabled;
|
var stringInternAttributeValue = internAttr?.Enabled;
|
||||||
|
|
||||||
byte flags = 0;
|
byte flags = 0;
|
||||||
if (stringInternAttributeValue == true) flags |= (1 << (int)StringInterningMode.Attribute);
|
if (stringInternAttributeValue == true) flags |= (1 << (int)StringInterningMode.Attribute);
|
||||||
if (stringInternAttributeValue != false) flags |= (1 << (int)StringInterningMode.All);
|
if (stringInternAttributeValue != false) flags |= (1 << (int)StringInterningMode.All);
|
||||||
_interningFlags = flags;
|
_interningFlags = flags;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,20 @@ internal interface IGeneratedBinaryWriter
|
||||||
/// <typeparam name="TOutput">Output strategy (ArrayBinaryOutput or BufferWriterBinaryOutput).</typeparam>
|
/// <typeparam name="TOutput">Output strategy (ArrayBinaryOutput or BufferWriterBinaryOutput).</typeparam>
|
||||||
void WriteProperties<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth)
|
void WriteProperties<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth)
|
||||||
where TOutput : struct, IBinaryOutputBase;
|
where TOutput : struct, IBinaryOutputBase;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full scan pass for this type: null/depth check, ref tracking (slot-based IdentityMap),
|
||||||
|
/// and recursive property scan (strings + complex types).
|
||||||
|
/// Replaces the entire runtime ScanValue for SGen types — no GetWrapper, no delegate invoke.
|
||||||
|
/// Called from ScanForDuplicates or from parent SGen ScanObject (child).
|
||||||
|
/// </summary>
|
||||||
|
void ScanObject<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth)
|
||||||
|
where TOutput : struct, IBinaryOutputBase;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SGen scan pass entry point. Called from Serialize instead of runtime ScanForDuplicates.
|
||||||
|
/// HasCaching check + ScanObject + SortWritePlan — eliminates GetWrapper for the root type.
|
||||||
|
/// </summary>
|
||||||
|
void ScanForDuplicates<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context)
|
||||||
|
where TOutput : struct, IBinaryOutputBase;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue