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 propTypeIsIId = false;
|
||||
string? writerClassName = null;
|
||||
string? propIdTypeName = null;
|
||||
int childTypeNameHash = 0;
|
||||
int[]? childPropertyHashes = null;
|
||||
if (kind == PropertyTypeKind.Complex)
|
||||
|
|
@ -95,9 +96,13 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
|
||||
if (hasGenWriter)
|
||||
{
|
||||
propTypeIsIId = resolvedType.AllInterfaces.Any(i =>
|
||||
var iidIface = resolvedType.AllInterfaces.FirstOrDefault(i =>
|
||||
i.IsGenericType &&
|
||||
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
||||
propTypeIsIId = iidIface != null;
|
||||
if (iidIface != null)
|
||||
propIdTypeName = iidIface.TypeArguments[0].ToDisplayString();
|
||||
|
||||
// Writer class: {Namespace}.{FlatName}_GeneratedWriter
|
||||
var flatName = BuildFlatName((INamedTypeSymbol)resolvedType);
|
||||
var ns = resolvedType.ContainingNamespace.IsGlobalNamespace
|
||||
|
|
@ -118,6 +123,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
bool elemHasGenWriter = false;
|
||||
bool elemIsIId = false;
|
||||
string? elemWriterClassName = null;
|
||||
string? elemIdTypeName = null;
|
||||
string? collKind = null;
|
||||
string? elemFullTypeName = null;
|
||||
int elementTypeNameHash = 0;
|
||||
|
|
@ -160,9 +166,13 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
a.AttributeClass?.ToDisplayString() == AttributeName);
|
||||
if (elemHasGenWriter)
|
||||
{
|
||||
elemIsIId = resolvedElem.AllInterfaces.Any(ifc =>
|
||||
var elemIidIface = resolvedElem.AllInterfaces.FirstOrDefault(ifc =>
|
||||
ifc.IsGenericType &&
|
||||
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 ens = resolvedElem.ContainingNamespace.IsGlobalNamespace
|
||||
? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString();
|
||||
|
|
@ -184,17 +194,24 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
typeNameForTypeof,
|
||||
kind,
|
||||
p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type),
|
||||
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName,
|
||||
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, collKind, elemFullTypeName,
|
||||
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName,
|
||||
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName,
|
||||
childTypeNameHash, childPropertyHashes,
|
||||
elementTypeNameHash, elementPropertyHashes));
|
||||
}
|
||||
}
|
||||
|
||||
// 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.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
||||
if (iidInterface != null)
|
||||
{
|
||||
isIId = true;
|
||||
idTypeName = iidInterface.TypeArguments[0].ToDisplayString();
|
||||
}
|
||||
|
||||
if (isIId)
|
||||
properties.Sort((a, b) =>
|
||||
|
|
@ -210,7 +227,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
var className = BuildFlatName(typeSymbol);
|
||||
var typeNameHash = ComputeFnvHash(typeSymbol.Name);
|
||||
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)
|
||||
|
|
@ -227,14 +244,13 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
|
||||
private static string GenWriter(SerializableClassInfo ci)
|
||||
{
|
||||
var sb = new StringBuilder(2048);
|
||||
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;");
|
||||
// ReferenceHandlingMode is needed when any Complex/Collection property has direct object write
|
||||
if (ci.Properties.Any(p => p.HasGeneratedWriter || p.ElementHasGeneratedWriter))
|
||||
sb.AppendLine("using AyCode.Core.Serializers;");
|
||||
// ReferenceHandlingMode is needed for ScanObject self ref tracking and direct object write/scan
|
||||
sb.AppendLine("using AyCode.Core.Serializers;");
|
||||
sb.AppendLine();
|
||||
if (!string.IsNullOrEmpty(ci.Namespace))
|
||||
sb.AppendLine($"namespace {ci.Namespace};");
|
||||
|
|
@ -243,6 +259,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
sb.AppendLine("{");
|
||||
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_trackingSlot = AcBinarySerializer.AllocateTrackingSlot();");
|
||||
sb.AppendLine($" internal static readonly int s_typeNameHash = {ci.TypeNameHash};");
|
||||
sb.Append( $" internal static readonly int[] s_propertyHashes = new int[] {{ ");
|
||||
sb.Append(string.Join(", ", ci.PropertyNameHashes));
|
||||
|
|
@ -259,10 +276,108 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
}
|
||||
|
||||
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("}");
|
||||
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)
|
||||
{
|
||||
var a = $"obj.{p.Name}";
|
||||
|
|
@ -396,6 +511,200 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// IId types: guard ReferenceHandling != None (tracked in OnlyId + All).
|
||||
/// 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 FullTypeName { 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>
|
||||
public int TypeNameHash { get; }
|
||||
/// <summary>FNV-1a hash of each property name, in property order</summary>
|
||||
public int[] PropertyNameHashes { get; }
|
||||
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> p, int typeNameHash, int[] propertyNameHashes)
|
||||
{ Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; TypeNameHash = typeNameHash; PropertyNameHashes = 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; IsIId = isIId; IdTypeName = idTypeName; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; }
|
||||
}
|
||||
|
||||
internal sealed class PropInfo
|
||||
|
|
@ -920,6 +1233,8 @@ internal sealed class PropInfo
|
|||
public bool IsIId { get; }
|
||||
/// <summary>Generated writer class name, e.g. "SharedTag_GeneratedWriter". Only set when HasGeneratedWriter.</summary>
|
||||
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
|
||||
/// <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; }
|
||||
/// <summary>Generated writer class name for collection element type.</summary>
|
||||
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>
|
||||
public string? CollectionKind { get; }
|
||||
/// <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 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,
|
||||
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 elementTypeNameHash = 0, int[]? elementPropertyHashes = null)
|
||||
{
|
||||
|
|
@ -960,10 +1277,12 @@ internal sealed class PropInfo
|
|||
HasGeneratedWriter = hasGeneratedWriter;
|
||||
IsIId = isIId;
|
||||
WriterClassName = writerClassName;
|
||||
IdTypeName = idTypeName;
|
||||
ElementKind = elementKind;
|
||||
ElementHasGeneratedWriter = elementHasGenWriter;
|
||||
ElementIsIId = elementIsIId;
|
||||
ElementWriterClassName = elementWriterClassName;
|
||||
ElementIdTypeName = elementIdTypeName;
|
||||
CollectionKind = collectionKind;
|
||||
ElementFullTypeName = elementFullTypeName;
|
||||
ChildTypeNameHash = childTypeNameHash;
|
||||
|
|
|
|||
|
|
@ -90,6 +90,18 @@ internal sealed class TestOrderWriter : IGeneratedBinaryWriter
|
|||
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)]
|
||||
private static void WriteComplexOrNull<TOutput>(object? value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public abstract class AcSerializerOptions
|
|||
set => _referenceHandling = value;
|
||||
}
|
||||
|
||||
private ReferenceHandlingMode _referenceHandling = ReferenceHandlingMode.All;
|
||||
private ReferenceHandlingMode _referenceHandling = ReferenceHandlingMode.OnlyId;
|
||||
|
||||
private readonly byte _maxDepth = byte.MaxValue;
|
||||
private readonly bool _throwOnCircularReference = true;
|
||||
|
|
|
|||
|
|
@ -14,4 +14,28 @@ namespace AyCode.Core.Serializers.Attributes;
|
|||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
|
||||
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)
|
||||
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
|
||||
|
||||
private WriteDuplicateEntry[]? _writePlan;
|
||||
|
|
@ -281,6 +289,9 @@ public static partial class AcBinarySerializer
|
|||
_stringInternMap?.Reset();
|
||||
_metadataSeenBits.AsSpan().Clear();
|
||||
_metadataSeenOverflow?.Clear();
|
||||
ResetSlottedMaps(_slottedIdMapsInt32);
|
||||
ResetSlottedMaps(_slottedIdMapsInt64);
|
||||
ResetSlottedMaps(_slottedIdMapsGuid);
|
||||
_nextCacheIndex = 0;
|
||||
NextFirstIndex = 0;
|
||||
ScanVisitIndex = 0;
|
||||
|
|
@ -769,6 +780,140 @@ public static partial class AcBinarySerializer
|
|||
|
||||
#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
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ public static partial class AcBinarySerializer
|
|||
/// </summary>
|
||||
public Type? GeneratedSerializerType { get; }
|
||||
|
||||
public bool EnableInternString { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Lazy-computed FNV-1a hash of the type name (SourceType.Name).
|
||||
/// 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++)
|
||||
{
|
||||
var prop = Properties[i];
|
||||
if (prop.IsComplexType || prop.AccessorType == PropertyAccessorType.String)
|
||||
if (prop.IsComplexType || (EnableInternString && prop.AccessorType == PropertyAccessorType.String))
|
||||
list.Add(prop);
|
||||
}
|
||||
|
||||
|
|
@ -120,12 +122,16 @@ public static partial class AcBinarySerializer
|
|||
// Use pre-computed WritableProperties directly - no method call overhead!
|
||||
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];
|
||||
var complexCount = 0;
|
||||
|
||||
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;
|
||||
Properties[i] = accessor;
|
||||
|
||||
|
|
@ -176,8 +182,8 @@ public static partial class AcBinarySerializer
|
|||
/// </summary>
|
||||
internal sealed class BinaryPropertyAccessor : BinaryPropertyAccessorBase
|
||||
{
|
||||
public BinaryPropertyAccessor(PropertyInfo prop, Type declaringType)
|
||||
: base(prop, declaringType)
|
||||
public BinaryPropertyAccessor(PropertyInfo prop, Type declaringType, bool enableInternString)
|
||||
: 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
|
||||
/// - Strings/objects skipped here are never written anyway (parent is ObjectRef)
|
||||
/// 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>
|
||||
private static void ScanForDuplicates<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
|
|
@ -22,8 +25,18 @@ public static partial class AcBinarySerializer
|
|||
return;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +99,15 @@ public static partial class AcBinarySerializer
|
|||
|
||||
// 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)
|
||||
// 2nd occurrence → skip children because:
|
||||
// 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
|
||||
// Use typed getter for strings (much faster than reflection GetValue)
|
||||
// Fallback: runtime property loop (no SGen writer for this type)
|
||||
var refProperties = metadata.ReferenceProperties;
|
||||
var hasPropertyFilter = context.HasPropertyFilter;
|
||||
var nextDepth2 = depth + 1;
|
||||
|
|
@ -144,7 +165,6 @@ public static partial class AcBinarySerializer
|
|||
for (var i = 0; i < refProperties.Length; i++)
|
||||
{
|
||||
var prop = refProperties[i];
|
||||
//context.CurrentProperty = prop;
|
||||
|
||||
// Must match write pass: filtered properties write PropertySkip (no value) →
|
||||
// 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;
|
||||
|
||||
// Fast path: typed getter for string
|
||||
var str2 = prop.GetString(value);
|
||||
if (str2 != null && context.IsValidForInterningString(str2.Length))
|
||||
context.ScanInternString(str2);
|
||||
}
|
||||
else if (prop.IsStringCollectionProperty)
|
||||
{
|
||||
// String collection: per-property interning control, no GetWrapper needed
|
||||
if (!prop.UseStringPropertyInterning(context.Options.UseStringInterning)) continue;
|
||||
|
||||
var propValue = prop.GetValue(value);
|
||||
|
|
@ -171,7 +189,6 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
else
|
||||
{
|
||||
// Object property: use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism
|
||||
var propValue = prop.GetValue(value);
|
||||
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>
|
||||
/// Scans string elements of a collection for interning. Uses IList fast path when available.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -264,6 +264,19 @@ public static partial class AcBinarySerializer
|
|||
|
||||
#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>
|
||||
/// Thread-safe registry for generated writers. Looked up once per TypeMetadataWrapper creation.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
|
@ -53,13 +54,13 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
|||
/// </summary>
|
||||
public byte? ExpectedTypeCode { get; }
|
||||
|
||||
protected BinaryPropertyAccessorBase(PropertyInfo prop, Type declaringType)
|
||||
protected BinaryPropertyAccessorBase(PropertyInfo prop, Type declaringType, bool enableInternString)
|
||||
: base(prop, declaringType)
|
||||
{
|
||||
IsStringCollectionProperty = IsStringCollection(prop.PropertyType);
|
||||
|
||||
|
||||
// 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)
|
||||
var internAttr = prop.GetCustomAttribute<AcStringInternAttribute>(inherit: true);
|
||||
|
|
@ -67,7 +68,7 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
|||
var stringInternAttributeValue = internAttr?.Enabled;
|
||||
|
||||
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);
|
||||
_interningFlags = flags;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,4 +26,20 @@ internal interface IGeneratedBinaryWriter
|
|||
/// <typeparam name="TOutput">Output strategy (ArrayBinaryOutput or BufferWriterBinaryOutput).</typeparam>
|
||||
void WriteProperties<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth)
|
||||
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