Per-type metadata control for binary serialization

Adds EnableMetadataFeature to [AcBinarySerializable], allowing types to opt out of inline metadata even when global UseMetadata is enabled. Source generator, serializer, and deserializer now respect this flag for child and element types. Default UseMetadata is set to true. Enables fine-grained control over serialization overhead and compatibility.
This commit is contained in:
Loretta 2026-02-20 09:55:21 +01:00
parent dcd9783b3b
commit 3e935cad2f
5 changed files with 218 additions and 110 deletions

View File

@ -47,6 +47,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
var enableIdTracking = true; var enableIdTracking = true;
var enableRefHandling = true; var enableRefHandling = true;
var enableInternString = true; var enableInternString = true;
var enableMetadata = true;
var binarySerializableAttr = typeSymbol.GetAttributes().FirstOrDefault(a => var binarySerializableAttr = typeSymbol.GetAttributes().FirstOrDefault(a =>
a.AttributeClass?.ToDisplayString() == AttributeName); a.AttributeClass?.ToDisplayString() == AttributeName);
if (binarySerializableAttr != null) if (binarySerializableAttr != null)
@ -58,10 +59,12 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
enableIdTracking = all; enableIdTracking = all;
enableRefHandling = all; enableRefHandling = all;
enableInternString = all; enableInternString = all;
enableMetadata = all;
} }
else if (binarySerializableAttr.ConstructorArguments.Length == 4) else if (binarySerializableAttr.ConstructorArguments.Length == 4)
{ {
// Four bool ctor: (metadata, idTracking, refHandling, internString) // Four bool ctor: (metadata, idTracking, refHandling, internString)
enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!; enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!;
enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!; enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!;
enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!; enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!;
@ -110,6 +113,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
var kind = GetKind(p.Type); var kind = GetKind(p.Type);
bool hasGenWriter = false; bool hasGenWriter = false;
bool propTypeIsIId = false; bool propTypeIsIId = false;
bool propEnableMetadata = true;
string? writerClassName = null; string? writerClassName = null;
string? propIdTypeName = null; string? propIdTypeName = null;
int childTypeNameHash = 0; int childTypeNameHash = 0;
@ -127,6 +131,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
if (hasGenWriter) if (hasGenWriter)
{ {
// Read child type's EnableMetadataFeature
propEnableMetadata = ReadEnableMetadata(resolvedType);
var iidIface = resolvedType.AllInterfaces.FirstOrDefault(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>");
@ -153,6 +159,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
PropertyTypeKind elemKind = PropertyTypeKind.Unknown; PropertyTypeKind elemKind = PropertyTypeKind.Unknown;
bool elemHasGenWriter = false; bool elemHasGenWriter = false;
bool elemIsIId = false; bool elemIsIId = false;
bool elemEnableMetadata = true;
string? elemWriterClassName = null; string? elemWriterClassName = null;
string? elemIdTypeName = null; string? elemIdTypeName = null;
string? collKind = null; string? collKind = null;
@ -197,6 +204,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
a.AttributeClass?.ToDisplayString() == AttributeName); a.AttributeClass?.ToDisplayString() == AttributeName);
if (elemHasGenWriter) if (elemHasGenWriter)
{ {
// Read element type's EnableMetadataFeature
elemEnableMetadata = ReadEnableMetadata(resolvedElem);
var elemIidIface = resolvedElem.AllInterfaces.FirstOrDefault(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>");
@ -228,7 +237,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName, stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName,
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName, elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName,
childTypeNameHash, childPropertyHashes, childTypeNameHash, childPropertyHashes,
elementTypeNameHash, elementPropertyHashes)); elementTypeNameHash, elementPropertyHashes,
propEnableMetadata, elemEnableMetadata));
} }
} }
@ -253,7 +263,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, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes); return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata);
} }
private static void Execute(ImmutableArray<SerializableClassInfo?> classes, SourceProductionContext context) private static void Execute(ImmutableArray<SerializableClassInfo?> classes, SourceProductionContext context)
@ -299,7 +309,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
foreach (var p in ci.Properties) foreach (var p in ci.Properties)
{ {
sb.AppendLine(); sb.AppendLine();
EmitProp(sb, p, " ", ci.FullTypeName); EmitProp(sb, p, " ", ci.FullTypeName, ci.EnableMetadata);
} }
sb.AppendLine(" }"); sb.AppendLine(" }");
@ -406,25 +416,33 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine(" }"); 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, bool enableMetadata)
{ {
var a = $"obj.{p.Name}"; var a = $"obj.{p.Name}";
// Markerless types: write raw value only, no type marker, no PropertySkip // Markerless types: write raw value only, no type marker, no PropertySkip
// Matches runtime WritePropertyMarkerless — these have ExpectedTypeCode // Matches runtime WritePropertyMarkerless — these have ExpectedTypeCode
// NEVER filtered (runtime doesn't filter markerless properties either) // NEVER filtered (runtime doesn't filter markerless properties either)
// UseMetadata=true: markerless path NOT available — must use markered path (EmitSkip) // When EnableMetadataFeature=false: always markerless (no UseMetadata branch needed)
// to match runtime WritePropertyOrSkip behavior (every property gets a type marker byte) // When EnableMetadataFeature=true: UseMetadata=true uses markered path (EmitSkip)
if (IsMarkerless(p.TypeKind)) if (IsMarkerless(p.TypeKind))
{ {
sb.AppendLine($"{i}if (context.UseMetadata)"); if (!enableMetadata)
sb.AppendLine($"{i}{{"); {
EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i + " "); // Per-type metadata disabled — always markerless, no branch
sb.AppendLine($"{i}}}"); EmitMarkerless(sb, p.TypeKind, a, i);
sb.AppendLine($"{i}else"); }
sb.AppendLine($"{i}{{"); else
EmitMarkerless(sb, p.TypeKind, a, i + " "); {
sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}if (context.UseMetadata)");
sb.AppendLine($"{i}{{");
EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i + " ");
sb.AppendLine($"{i}}}");
sb.AppendLine($"{i}else");
sb.AppendLine($"{i}{{");
EmitMarkerless(sb, p.TypeKind, a, i + " ");
sb.AppendLine($"{i}}}");
}
return; return;
} }
@ -759,49 +777,79 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} else"); sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
// UseMetadata: register type for first/repeated tracking via slot (no GetWrapper needed) if (!p.ChildEnableMetadata)
sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);"); {
// Child type has EnableMetadataFeature=false — no metadata, always Object marker
// Inline ref tracking still needed for IId/All mode
var refGuard = p.IsIId
? "context.ReferenceHandling != ReferenceHandlingMode.None"
: "context.ReferenceHandling == ReferenceHandlingMode.All";
sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);");
sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);");
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});");
sb.AppendLine($"{i} }}");
}
else
{
// UseMetadata: register type for first/repeated tracking via slot (no GetWrapper needed)
sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);");
// Inline ref tracking: guard depends on IId vs non-IId to match scan pass behavior. // Inline ref tracking: guard depends on IId vs non-IId to match scan pass behavior.
var refGuard = p.IsIId var refGuard = p.IsIId
? "context.ReferenceHandling != ReferenceHandlingMode.None" ? "context.ReferenceHandling != ReferenceHandlingMode.None"
: "context.ReferenceHandling == ReferenceHandlingMode.All"; : "context.ReferenceHandling == ReferenceHandlingMode.All";
sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))"); sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)"); sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else"); sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
// RefFirst: UseMetadata → ObjectWithMetadataRefFirst + metadata, else → ObjectRefFirst // RefFirst: UseMetadata → ObjectWithMetadataRefFirst + metadata, else → ObjectRefFirst
sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} if (context.UseMetadata)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);");
sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);");
EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " "); EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " ");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else"); sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);");
sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else"); sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
// No ref tracking: UseMetadata → ObjectWithMetadata + metadata, else → Object // No ref tracking: UseMetadata → ObjectWithMetadata + metadata, else → Object
sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} if (context.UseMetadata)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " "); EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " ");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else"); sb.AppendLine($"{i} else");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
}
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}}}");
@ -878,49 +926,76 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}");
sb.AppendLine($"{i} if (nextDepth_{p.Name} > context.MaxDepth) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); sb.AppendLine($"{i} if (nextDepth_{p.Name} > context.MaxDepth) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}");
// UseMetadata: register element type for first/repeated tracking
sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);");
// Inline ref tracking // Inline ref tracking
var elemRefGuard = p.ElementIsIId var elemRefGuard = p.ElementIsIId
? "context.ReferenceHandling != ReferenceHandlingMode.None" ? "context.ReferenceHandling != ReferenceHandlingMode.None"
: "context.ReferenceHandling == ReferenceHandlingMode.All"; : "context.ReferenceHandling == ReferenceHandlingMode.All";
sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))");
sb.AppendLine($"{i} {{"); if (!p.ElementEnableMetadata)
sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)"); {
sb.AppendLine($"{i} {{"); // Element type has EnableMetadataFeature=false — no metadata, always Object marker
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))");
sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)");
sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
// RefFirst: UseMetadata → ObjectWithMetadataRefFirst + metadata, else → ObjectRefFirst sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} else");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);");
EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " "); sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
sb.AppendLine($"{i} else"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); sb.AppendLine($"{i} else");
sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} }}"); }
sb.AppendLine($"{i} else"); else
sb.AppendLine($"{i} {{"); {
// No ref tracking: UseMetadata → ObjectWithMetadata + metadata, else → Object // UseMetadata: register element type for first/repeated tracking
sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))");
EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " "); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)");
sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
// RefFirst: UseMetadata → ObjectWithMetadataRefFirst + metadata, else → ObjectRefFirst
sb.AppendLine($"{i} if (context.UseMetadata)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);");
sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " ");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);");
sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
// No ref tracking: UseMetadata → ObjectWithMetadata + metadata, else → Object
sb.AppendLine($"{i} if (context.UseMetadata)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " ");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
sb.AppendLine($"{i} }}");
}
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}}}");
@ -1077,6 +1152,22 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
return string.Join("_", parts); return string.Join("_", parts);
} }
/// <summary>
/// Reads EnableMetadataFeature from a type's [AcBinarySerializable] attribute.
/// Returns true (default) if no attribute or enableAllFeatures=true.
/// </summary>
private static bool ReadEnableMetadata(ITypeSymbol type)
{
var attr = type.GetAttributes().FirstOrDefault(a =>
a.AttributeClass?.ToDisplayString() == AttributeName);
if (attr == null) return true;
if (attr.ConstructorArguments.Length == 1)
return (bool)attr.ConstructorArguments[0].Value!;
if (attr.ConstructorArguments.Length == 4)
return (bool)attr.ConstructorArguments[0].Value!;
return true;
}
#region FNV-1a Hash (compile-time) #region FNV-1a Hash (compile-time)
private static int ComputeFnvHash(string value) private static int ComputeFnvHash(string value)
@ -1222,8 +1313,10 @@ internal sealed class SerializableClassInfo
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, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes) /// <summary>When false, skip inline metadata and use markerless property write for this type.</summary>
{ Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; } public bool EnableMetadata { get; }
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata)
{ Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; }
} }
internal sealed class PropInfo internal sealed class PropInfo
@ -1278,13 +1371,18 @@ internal sealed class PropInfo
public int ElementTypeNameHash { get; } public int ElementTypeNameHash { get; }
/// <summary>FNV-1a hashes of collection element type's properties. Only set when ElementHasGeneratedWriter.</summary> /// <summary>FNV-1a hashes of collection element type's properties. Only set when ElementHasGeneratedWriter.</summary>
public int[]? ElementPropertyHashes { get; } public int[]? ElementPropertyHashes { get; }
/// <summary>When false, child Complex type skips inline metadata in generated code.</summary>
public bool ChildEnableMetadata { get; }
/// <summary>When false, collection element type skips inline metadata in generated code.</summary>
public bool ElementEnableMetadata { 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, string? idTypeName = 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? elementIdTypeName = 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,
bool childEnableMetadata = true, bool elementEnableMetadata = true)
{ {
Name = n; Name = n;
TypeName = tn; TypeName = tn;
@ -1306,6 +1404,8 @@ internal sealed class PropInfo
ChildPropertyHashes = childPropertyHashes; ChildPropertyHashes = childPropertyHashes;
ElementTypeNameHash = elementTypeNameHash; ElementTypeNameHash = elementTypeNameHash;
ElementPropertyHashes = elementPropertyHashes; ElementPropertyHashes = elementPropertyHashes;
ChildEnableMetadata = childEnableMetadata;
ElementEnableMetadata = elementEnableMetadata;
// Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase // Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase
int flags = 0; int flags = 0;
if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit

View File

@ -128,10 +128,11 @@ public static partial class AcBinaryDeserializer
SkipDefaultWrite = skipDefaultWrite SkipDefaultWrite = skipDefaultWrite
}; };
if (!context.HasMetadata) if (!context.HasMetadata || !metadata.EnableMetadataFeature)
{ {
// Markerless loop: properties with ExpectedTypeCode read raw values directly. // Markerless loop: properties with ExpectedTypeCode read raw values directly.
// Properties without ExpectedTypeCode (bool, enum, string, object) use standard marker path. // Properties without ExpectedTypeCode (bool, enum, string, object) use standard marker path.
// Also used when EnableMetadataFeature=false on the type (per-type metadata opt-out).
for (int i = 0; i < propCount; i++) for (int i = 0; i < propCount; i++)
{ {
var propInfo = properties[i]; var propInfo = properties[i];

View File

@ -1038,15 +1038,14 @@ public static partial class AcBinarySerializer
{ {
var metadata = wrapper.Metadata; var metadata = wrapper.Metadata;
// Wire format: // Per-type metadata flag: when EnableMetadataFeature=false on [AcBinarySerializable],
// - UseMetadata=false: [Object][props...] // skip inline metadata and use markerless property write — even when global UseMetadata=true.
// - UseMetadata=true, első: [ObjectWithMetadata][propNameHash (4b)][propCount (VarUInt)][hash0..N][props...] // Deserializer must have the same attribute on the type (developer responsibility).
// - UseMetadata=true, ismételt: [ObjectWithMetadata][propNameHash (4b)][props...] var useMetaForType = context.UseMetadata && metadata.EnableMetadataFeature;
// ObjectRef: [ObjectRef][cacheIndex]
// UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking) // UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking)
var isFirstMetadataOccurrence = false; var isFirstMetadataOccurrence = false;
if (context.UseMetadata) if (useMetaForType)
{ {
isFirstMetadataOccurrence = BinarySerializationContext<TOutput>.RegisterMetadataType(wrapper); isFirstMetadataOccurrence = BinarySerializationContext<TOutput>.RegisterMetadataType(wrapper);
} }
@ -1076,7 +1075,7 @@ public static partial class AcBinarySerializer
// Marker kiírása: // Marker kiírása:
// - Cached object first occurrence: ObjectRefFirst/ObjectWithMetadataRefFirst + cacheIndex // - Cached object first occurrence: ObjectRefFirst/ObjectWithMetadataRefFirst + cacheIndex
// - Non-cached: Object/ObjectWithMetadata // - Non-cached: Object/ObjectWithMetadata
if (context.UseMetadata) if (useMetaForType)
{ {
if (cachedObjectCacheIndex >= 0) if (cachedObjectCacheIndex >= 0)
{ {
@ -1122,12 +1121,13 @@ public static partial class AcBinarySerializer
return; return;
} }
} }
if (!context.UseMetadata) if (!useMetaForType)
{ {
// Markerless loop: no extra branching per property for the common case. // Markerless loop: no extra branching per property for the common case.
// Properties with ExpectedTypeCode write raw values (no type marker, no skip). // Properties with ExpectedTypeCode write raw values (no type marker, no skip).
// Properties without ExpectedTypeCode (bool, enum, string, object) use the standard path. // Properties without ExpectedTypeCode (bool, enum, string, object) use the standard path.
// Also used when EnableMetadataFeature=false on the type (per-type metadata opt-out).
for (var i = 0; i < propCount; i++) for (var i = 0; i < propCount; i++)
{ {
var prop = properties[i]; var prop = properties[i];

View File

@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// allowing the deserializer to match properties by name between different types. /// allowing the deserializer to match properties by name between different types.
/// Default: false (no overhead) /// Default: false (no overhead)
/// </summary> /// </summary>
public bool UseMetadata { get; set; } = false; public bool UseMetadata { get; set; } = true;
public bool UseGeneratedCode { get; set; } = true; public bool UseGeneratedCode { get; set; } = true;

View File

@ -130,7 +130,13 @@ public abstract class TypeMetadataBase
/// False for sealed value-like types with only primitives - these never need reference tracking. /// False for sealed value-like types with only primitives - these never need reference tracking.
/// </summary> /// </summary>
public bool NeedsReferenceTracking { get; protected set; } public bool NeedsReferenceTracking { get; protected set; }
/// <summary>
/// When false, this type skips inline metadata and uses markerless property write/read
/// — even when global UseMetadata=true. Read from [AcBinarySerializable] attribute.
/// </summary>
public bool EnableMetadataFeature { get; }
/// <summary> /// <summary>
/// True if this type is a primitive, string, enum, Guid, DateTime, etc. /// True if this type is a primitive, string, enum, Guid, DateTime, etc.
/// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path. /// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path.
@ -215,6 +221,7 @@ public abstract class TypeMetadataBase
} }
var serializableAttr = type.GetCustomAttribute<AcBinarySerializableAttribute>(inherit: false); var serializableAttr = type.GetCustomAttribute<AcBinarySerializableAttribute>(inherit: false);
EnableMetadataFeature = serializableAttr == null || serializableAttr.EnableMetadataFeature;
if (serializableAttr is { EnableIdTrackingFeature: false }) if (serializableAttr is { EnableIdTrackingFeature: false })
{ {
IsIId = false; IsIId = false;