From 4ef65ee5016def698c3efc6aa787244fd6d861cc Mon Sep 17 00:00:00 2001 From: Loretta Date: Sat, 14 Feb 2026 20:50:38 +0100 Subject: [PATCH] Generate IGeneratedBinaryWriter for fast serialization Refactor source generator to emit per-type IGeneratedBinaryWriter classes for [AcBinarySerializable] types, with auto-registration at startup. Integrate generated writers into AcBinarySerializer for direct, delegate-free property writing, bypassing the runtime property loop when possible. Add registry, bridge methods, and update TypeMetadataWrapper for fast lookup. Expand tests to verify generated writers and round-trip correctness. This enables major serialization performance gains and reduces code size for supported types. --- .../AcBinarySourceGenerator.cs | 1194 ++++++----------- .../GeneratedWriters/TestOrderWriter.cs | 141 ++ .../GeneratedSerializerIntegrationTests.cs | 183 ++- .../Binaries/AcBinarySerializer.cs | 80 ++ .../Binaries/IGeneratedBinaryWriter.cs | 29 + .../Serializers/TypeMetadataWrapper.cs | 10 + 6 files changed, 759 insertions(+), 878 deletions(-) create mode 100644 AyCode.Core.Tests/GeneratedWriters/TestOrderWriter.cs create mode 100644 AyCode.Core/Serializers/Binaries/IGeneratedBinaryWriter.cs diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index 4d88643..699838e 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -1,779 +1,415 @@ -//using System; -//using System.Collections.Generic; -//using System.Collections.Immutable; -//using System.Linq; -//using System.Text; -//using Microsoft.CodeAnalysis; -//using Microsoft.CodeAnalysis.CSharp.Syntax; -//using Microsoft.CodeAnalysis.Text; - -//namespace AyCode.Core.Serializers.SourceGenerator; - -///// -///// Source Generator for AcBinary serialization. -///// Generates optimized serialize/deserialize methods for classes marked with [AcBinarySerializable]. -///// -//[Generator] -//public class AcBinarySourceGenerator : IIncrementalGenerator -//{ -// private const string AcBinarySerializableAttributeName = "AyCode.Core.Serializers.Attributes.AcBinarySerializableAttribute"; - -// public void Initialize(IncrementalGeneratorInitializationContext context) -// { -// // Find all classes with [AcBinarySerializable] attribute -// var classDeclarations = context.SyntaxProvider -// .ForAttributeWithMetadataName( -// AcBinarySerializableAttributeName, -// predicate: static (node, _) => node is ClassDeclarationSyntax || node is StructDeclarationSyntax, -// transform: static (ctx, _) => GetClassInfo(ctx)) -// .Where(static info => info != null); - -// // Combine with compilation -// var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect()); - -// // Generate source -// context.RegisterSourceOutput(compilationAndClasses, -// static (spc, source) => Execute(source.Left, source.Right, spc)); -// } - -// private static SerializableClassInfo GetClassInfo(GeneratorAttributeSyntaxContext context) -// { -// if (!(context.TargetSymbol is INamedTypeSymbol typeSymbol)) -// return null; - -// var namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace -// ? string.Empty -// : typeSymbol.ContainingNamespace.ToDisplayString(); - -// var className = typeSymbol.Name; -// var fullTypeName = typeSymbol.ToDisplayString(); -// var isStruct = typeSymbol.IsValueType; - -// // Check if this is a nested type -// var isNestedType = typeSymbol.ContainingType != null; - -// // For nested types, we need the full containing type path for method signatures -// // e.g. "OuterClass.InnerClass" instead of just "InnerClass" -// var typeNameForSignature = isNestedType -// ? GetNestedTypeName(typeSymbol) -// : className; - -// // Get all public properties with getter and setter -// // DUPLICATED LOGIC: Same filtering as TypeMetadataBase.GetSerializableProperties() -// var properties = new List(); -// foreach (var member in typeSymbol.GetMembers()) -// { -// if (member is IPropertySymbol p && -// p.DeclaredAccessibility == Accessibility.Public && -// p.GetMethod != null && -// p.SetMethod != null && -// !p.IsIndexer && -// !p.IsStatic) -// { -// properties.Add(new PropertyInfo( -// p.Name, -// p.Type.ToDisplayString(), -// GetPropertyTypeKind(p.Type), -// p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableValueType(p.Type))); -// } -// } - -// // DUPLICATED LOGIC: Same ordering as TypeMetadataBase.GetSerializableProperties() -// properties.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); - -// return new SerializableClassInfo(namespaceName, className, fullTypeName, isStruct, isNestedType, typeNameForSignature, properties); -// } - -// /// -// /// Gets the nested type name chain (e.g., "OuterClass.MiddleClass.InnerClass") -// /// -// private static string GetNestedTypeName(INamedTypeSymbol typeSymbol) -// { -// var parts = new List(); -// var current = typeSymbol; - -// while (current != null) -// { -// parts.Insert(0, current.Name); -// current = current.ContainingType; -// } - -// return string.Join(".", parts); -// } - -// private static bool IsNullableValueType(ITypeSymbol type) -// { -// return type is INamedTypeSymbol namedType && -// namedType.IsGenericType && -// namedType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; -// } - -// private static PropertyTypeKind GetPropertyTypeKind(ITypeSymbol type) -// { -// // Handle nullable value types -// if (type is INamedTypeSymbol namedType && namedType.IsGenericType && -// namedType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T) -// { -// // Get underlying type -// var underlyingType = namedType.TypeArguments[0]; -// return GetPropertyTypeKindForUnderlying(underlyingType, isNullable: true); -// } - -// return GetPropertyTypeKindForUnderlying(type, isNullable: false); -// } - -// private static PropertyTypeKind GetPropertyTypeKindForUnderlying(ITypeSymbol type, bool isNullable) -// { -// switch (type.SpecialType) -// { -// case SpecialType.System_String: return PropertyTypeKind.String; -// case SpecialType.System_Int32: return isNullable ? PropertyTypeKind.NullableInt32 : PropertyTypeKind.Int32; -// case SpecialType.System_Int64: return isNullable ? PropertyTypeKind.NullableInt64 : PropertyTypeKind.Int64; -// case SpecialType.System_Int16: return isNullable ? PropertyTypeKind.NullableInt16 : PropertyTypeKind.Int16; -// case SpecialType.System_Byte: return isNullable ? PropertyTypeKind.NullableByte : PropertyTypeKind.Byte; -// case SpecialType.System_SByte: return isNullable ? PropertyTypeKind.NullableSByte : PropertyTypeKind.SByte; -// case SpecialType.System_UInt16: return isNullable ? PropertyTypeKind.NullableUInt16 : PropertyTypeKind.UInt16; -// case SpecialType.System_UInt32: return isNullable ? PropertyTypeKind.NullableUInt32 : PropertyTypeKind.UInt32; -// case SpecialType.System_UInt64: return isNullable ? PropertyTypeKind.NullableUInt64 : PropertyTypeKind.UInt64; -// case SpecialType.System_Boolean: return isNullable ? PropertyTypeKind.NullableBoolean : PropertyTypeKind.Boolean; -// case SpecialType.System_Single: return isNullable ? PropertyTypeKind.NullableSingle : PropertyTypeKind.Single; -// case SpecialType.System_Double: return isNullable ? PropertyTypeKind.NullableDouble : PropertyTypeKind.Double; -// case SpecialType.System_Decimal: return isNullable ? PropertyTypeKind.NullableDecimal : PropertyTypeKind.Decimal; -// case SpecialType.System_DateTime: return isNullable ? PropertyTypeKind.NullableDateTime : PropertyTypeKind.DateTime; -// default: return GetNonSpecialTypeKind(type, isNullable); -// } -// } - -// private static PropertyTypeKind GetNonSpecialTypeKind(ITypeSymbol type, bool isNullable) -// { -// var fullName = type.ToDisplayString(); - -// if (fullName == "System.Guid") return isNullable ? PropertyTypeKind.NullableGuid : PropertyTypeKind.Guid; -// if (fullName == "System.TimeSpan") return isNullable ? PropertyTypeKind.NullableTimeSpan : PropertyTypeKind.TimeSpan; -// if (fullName == "System.DateTimeOffset") return isNullable ? PropertyTypeKind.NullableDateTimeOffset : PropertyTypeKind.DateTimeOffset; -// if (fullName == "System.DateOnly") return isNullable ? PropertyTypeKind.NullableDateOnly : PropertyTypeKind.DateOnly; -// if (fullName == "System.TimeOnly") return isNullable ? PropertyTypeKind.NullableTimeOnly : PropertyTypeKind.TimeOnly; -// if (type.TypeKind == TypeKind.Enum) return isNullable ? PropertyTypeKind.NullableEnum : PropertyTypeKind.Enum; -// if (IsCollectionType(type)) return PropertyTypeKind.Collection; -// if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct) return PropertyTypeKind.Complex; - -// return PropertyTypeKind.Unknown; -// } - -// private static bool IsCollectionType(ITypeSymbol type) -// { -// if (type is IArrayTypeSymbol) -// return true; - -// if (type is INamedTypeSymbol namedType) -// { -// foreach (var iface in namedType.AllInterfaces) -// { -// if (iface.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T) -// return true; -// var ifaceName = iface.ToDisplayString(); -// if (ifaceName.StartsWith("System.Collections.Generic.IList<") || -// ifaceName.StartsWith("System.Collections.Generic.ICollection<")) -// return true; -// } -// } - -// return false; -// } - -// private static void Execute(Compilation compilation, ImmutableArray classes, SourceProductionContext context) -// { -// if (classes.IsDefaultOrEmpty) -// return; - -// foreach (var classInfo in classes) -// { -// if (classInfo == null) -// continue; - -// var source = GenerateSerializerClass(classInfo); -// context.AddSource($"{classInfo.ClassName}_AcBinarySerializer.g.cs", SourceText.From(source, Encoding.UTF8)); -// } -// } - -// private static string GenerateSerializerClass(SerializableClassInfo classInfo) -// { -// var sb = new StringBuilder(); - -// sb.AppendLine("// "); -// sb.AppendLine("#nullable enable"); -// sb.AppendLine(); -// sb.AppendLine("using System;"); -// sb.AppendLine("using System.Runtime.CompilerServices;"); -// sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); -// sb.AppendLine(); - -// if (!string.IsNullOrEmpty(classInfo.Namespace)) -// { -// sb.AppendLine($"namespace {classInfo.Namespace}"); -// sb.AppendLine("{"); -// } - -// var indent = string.IsNullOrEmpty(classInfo.Namespace) ? "" : " "; - -// sb.AppendLine($"{indent}/// "); -// sb.AppendLine($"{indent}/// Generated binary serializer for {classInfo.ClassName}."); -// sb.AppendLine($"{indent}/// "); -// sb.AppendLine($"{indent}internal static class {classInfo.ClassName}_AcBinarySerializer"); -// sb.AppendLine($"{indent}{{"); - -// // Generate property count constant -// sb.AppendLine($"{indent} public const int PropertyCount = {classInfo.Properties.Count};"); -// sb.AppendLine(); - -// // Generate property names array for validation -// sb.AppendLine($"{indent} /// "); -// sb.AppendLine($"{indent} /// Property names in serialization order (alphabetical)."); -// sb.AppendLine($"{indent} /// Used for runtime validation against TypeMetadataBase.GetSerializableProperties()."); -// sb.AppendLine($"{indent} /// "); -// sb.Append($"{indent} public static readonly string[] PropertyNames = new[] {{ "); -// sb.Append(string.Join(", ", classInfo.Properties.Select(p => $"\"{p.Name}\""))); -// sb.AppendLine(" };"); -// sb.AppendLine(); - -// // Generate Serialize method -// GenerateSerializeMethod(sb, classInfo, indent); - -// // Generate Deserialize method -// GenerateDeserializeMethod(sb, classInfo, indent); - -// sb.AppendLine($"{indent}}}"); - -// if (!string.IsNullOrEmpty(classInfo.Namespace)) -// { -// sb.AppendLine("}"); -// } - -// return sb.ToString(); -// } - -// private static void GenerateSerializeMethod(StringBuilder sb, SerializableClassInfo classInfo, string indent) -// { -// sb.AppendLine($"{indent} /// "); -// sb.AppendLine($"{indent} /// Serializes a {classInfo.ClassName} instance to the binary context."); -// sb.AppendLine($"{indent} /// Direct property access - no reflection, no boxing for primitives."); -// sb.AppendLine($"{indent} /// "); -// sb.AppendLine($"{indent} [MethodImpl(MethodImplOptions.AggressiveInlining)]"); -// sb.AppendLine($"{indent} public static void Serialize({classInfo.TypeNameForSignature} obj, AcBinarySerializer.BinarySerializationContext context) where TOutput : BinaryOutputBase"); -// sb.AppendLine($"{indent} {{"); -// sb.AppendLine($"{indent} var output = context.Output;"); - -// foreach (var prop in classInfo.Properties) -// { -// GenerateSerializeProperty(sb, prop, indent + " "); -// } - -// sb.AppendLine($"{indent} }}"); -// sb.AppendLine(); -// } - -// private static void GenerateSerializeProperty(StringBuilder sb, PropertyInfo prop, string indent) -// { -// var propAccess = $"obj.{prop.Name}"; - -// // Handle nullable VALUE types (Nullable) - these use .HasValue and .Value -// if (IsNullableValueTypeKind(prop.TypeKind)) -// { -// sb.AppendLine($"{indent}// {prop.Name}: {prop.TypeName} (nullable value type)"); -// sb.AppendLine($"{indent}if ({propAccess}.HasValue)"); -// sb.AppendLine($"{indent}{{"); -// GenerateSerializeValue(sb, prop.TypeKind, $"{propAccess}.Value", indent + " "); -// sb.AppendLine($"{indent}}}"); -// sb.AppendLine($"{indent}else"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.Null);"); -// sb.AppendLine($"{indent}}}"); -// return; -// } - -// sb.AppendLine($"{indent}// {prop.Name}: {prop.TypeName}"); - -// // String needs null check -// if (prop.TypeKind == PropertyTypeKind.String) -// { -// sb.AppendLine($"{indent}if ({propAccess} == null)"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.Null);"); -// sb.AppendLine($"{indent}}}"); -// sb.AppendLine($"{indent}else if ({propAccess}.Length == 0)"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.StringEmpty);"); -// sb.AppendLine($"{indent}}}"); -// sb.AppendLine($"{indent}else"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.String);"); -// sb.AppendLine($"{indent} context.WriteStringUtf8({propAccess});"); -// sb.AppendLine($"{indent}}}"); -// return; -// } - -// // Nullable reference types (Complex/Collection with ? annotation) - use == null -// if (prop.IsNullable && (prop.TypeKind == PropertyTypeKind.Complex || prop.TypeKind == PropertyTypeKind.Collection)) -// { -// sb.AppendLine($"{indent}if ({propAccess} == null)"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.Null);"); -// sb.AppendLine($"{indent}}}"); -// sb.AppendLine($"{indent}else"); -// sb.AppendLine($"{indent}{{"); -// GenerateSerializeValue(sb, prop.TypeKind, propAccess, indent + " "); -// sb.AppendLine($"{indent}}}"); -// return; -// } - -// GenerateSerializeValue(sb, prop.TypeKind, propAccess, indent); -// } - -// /// -// /// Checks if the type kind represents a nullable VALUE type (Nullable<T>), not a reference type -// /// -// private static bool IsNullableValueTypeKind(PropertyTypeKind kind) -// { -// return kind >= PropertyTypeKind.NullableInt32 && kind <= PropertyTypeKind.NullableEnum; -// } - -// private static void GenerateSerializeValue(StringBuilder sb, PropertyTypeKind typeKind, string valueExpr, string indent) -// { -// switch (typeKind) -// { -// case PropertyTypeKind.Int32: -// case PropertyTypeKind.NullableInt32: -// sb.AppendLine($"{indent}if (BinaryTypeCode.TryEncodeTinyInt({valueExpr}, out var tiny_{valueExpr.Replace(".", "_")}))"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.WriteByte(tiny_{valueExpr.Replace(".", "_")});"); -// sb.AppendLine($"{indent}}}"); -// sb.AppendLine($"{indent}else"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.Int32);"); -// sb.AppendLine($"{indent} context.WriteVarInt({valueExpr});"); -// sb.AppendLine($"{indent}}}"); -// break; - -// case PropertyTypeKind.Int64: -// case PropertyTypeKind.NullableInt64: -// sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.Int64);"); -// sb.AppendLine($"{indent}context.WriteVarLong({valueExpr});"); -// break; - -// case PropertyTypeKind.Boolean: -// case PropertyTypeKind.NullableBoolean: -// sb.AppendLine($"{indent}context.WriteByte({valueExpr} ? BinaryTypeCode.True : BinaryTypeCode.False);"); -// break; - -// case PropertyTypeKind.Double: -// case PropertyTypeKind.NullableDouble: -// sb.AppendLine($"{indent}context.WriteTypeCodeAndRaw(BinaryTypeCode.Float64, {valueExpr});"); -// break; - -// case PropertyTypeKind.Single: -// case PropertyTypeKind.NullableSingle: -// sb.AppendLine($"{indent}context.WriteTypeCodeAndRaw(BinaryTypeCode.Float32, {valueExpr});"); -// break; - -// case PropertyTypeKind.Decimal: -// case PropertyTypeKind.NullableDecimal: -// sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.Decimal);"); -// sb.AppendLine($"{indent}context.WriteDecimalBits({valueExpr});"); -// break; - -// case PropertyTypeKind.DateTime: -// case PropertyTypeKind.NullableDateTime: -// sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.DateTime);"); -// sb.AppendLine($"{indent}context.WriteDateTimeBits({valueExpr});"); -// break; - -// case PropertyTypeKind.Guid: -// case PropertyTypeKind.NullableGuid: -// sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.Guid);"); -// sb.AppendLine($"{indent}context.WriteGuidBits({valueExpr});"); -// break; - -// case PropertyTypeKind.Byte: -// case PropertyTypeKind.NullableByte: -// sb.AppendLine($"{indent}context.WriteTwoBytes(BinaryTypeCode.UInt8, {valueExpr});"); -// break; - -// case PropertyTypeKind.Int16: -// case PropertyTypeKind.NullableInt16: -// sb.AppendLine($"{indent}context.WriteTypeCodeAndRaw(BinaryTypeCode.Int16, {valueExpr});"); -// break; - -// case PropertyTypeKind.UInt16: -// case PropertyTypeKind.NullableUInt16: -// sb.AppendLine($"{indent}context.WriteTypeCodeAndRaw(BinaryTypeCode.UInt16, {valueExpr});"); -// break; - -// case PropertyTypeKind.UInt32: -// case PropertyTypeKind.NullableUInt32: -// sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.UInt32);"); -// sb.AppendLine($"{indent}context.WriteVarUInt({valueExpr});"); -// sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.UInt32);"); -// sb.AppendLine($"{indent}context.WriteVarUInt({valueExpr});"); -// break; - -// case PropertyTypeKind.UInt64: -// case PropertyTypeKind.NullableUInt64: -// sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.UInt64);"); -// sb.AppendLine($"{indent}context.WriteVarULong({valueExpr});"); -// break; - -// case PropertyTypeKind.TimeSpan: -// case PropertyTypeKind.NullableTimeSpan: -// sb.AppendLine($"{indent}context.WriteTypeCodeAndRaw(BinaryTypeCode.TimeSpan, {valueExpr}.Ticks);"); -// break; - -// case PropertyTypeKind.DateTimeOffset: -// case PropertyTypeKind.NullableDateTimeOffset: -// sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.DateTimeOffset);"); -// sb.AppendLine($"{indent}context.WriteDateTimeOffsetBits({valueExpr});"); -// break; - -// case PropertyTypeKind.Enum: -// case PropertyTypeKind.NullableEnum: -// sb.AppendLine($"{indent}context.WriteByte(BinaryTypeCode.Enum);"); -// sb.AppendLine($"{indent}var enumVal = (int){valueExpr};"); -// sb.AppendLine($"{indent}if (BinaryTypeCode.TryEncodeTinyInt(enumVal, out var tinyEnum))"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.WriteByte(tinyEnum);"); -// sb.AppendLine($"{indent}}}"); -// sb.AppendLine($"{indent}else"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.WriteByte(BinaryTypeCode.Int32);"); -// sb.AppendLine($"{indent} context.WriteVarInt(enumVal);"); -// sb.AppendLine($"{indent}}}"); -// break; - -// case PropertyTypeKind.Collection: -// case PropertyTypeKind.Complex: -// // TODO: Collections and complex types will be implemented later -// sb.AppendLine($"{indent}// TODO: Complex/Collection type - fallback to runtime serializer"); -// sb.AppendLine($"{indent}throw new NotImplementedException(\"Complex/Collection types not yet implemented in generated serializer\");"); -// break; - -// default: -// sb.AppendLine($"{indent}// Unknown type - fallback needed"); -// sb.AppendLine($"{indent}throw new NotImplementedException($\"Type {typeKind} not implemented in generated serializer\");"); -// break; -// } -// } - -// private static void GenerateDeserializeMethod(StringBuilder sb, SerializableClassInfo classInfo, string indent) -// { -// sb.AppendLine($"{indent} /// "); -// sb.AppendLine($"{indent} /// Deserializes properties into a {classInfo.ClassName} instance from the binary context."); -// sb.AppendLine($"{indent} /// Direct property access - no reflection, no boxing for primitives."); -// sb.AppendLine($"{indent} /// "); -// sb.AppendLine($"{indent} [MethodImpl(MethodImplOptions.AggressiveInlining)]"); -// sb.AppendLine($"{indent} public static void Deserialize({classInfo.TypeNameForSignature} obj, ref AcBinaryDeserializer.BinaryDeserializationContext context)"); -// sb.AppendLine($"{indent} {{"); - -// foreach (var prop in classInfo.Properties) -// { -// GenerateDeserializeProperty(sb, prop, indent + " "); -// } - -// sb.AppendLine($"{indent} }}"); -// } - -// private static void GenerateDeserializeProperty(StringBuilder sb, PropertyInfo prop, string indent) -// { -// sb.AppendLine($"{indent}// {prop.Name}: {prop.TypeName}"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} var peekCode = context.PeekByte();"); - -// // Handle Skip marker -// sb.AppendLine($"{indent} if (peekCode == BinaryTypeCode.PropertySkip)"); -// sb.AppendLine($"{indent} {{"); -// sb.AppendLine($"{indent} context.ReadByte(); // consume Skip marker"); -// sb.AppendLine($"{indent} // Property keeps default value"); -// sb.AppendLine($"{indent} }}"); - -// // Handle Null marker -// sb.AppendLine($"{indent} else if (peekCode == BinaryTypeCode.Null)"); -// sb.AppendLine($"{indent} {{"); -// sb.AppendLine($"{indent} context.ReadByte(); // consume Null marker"); -// if (prop.IsNullable || prop.TypeKind == PropertyTypeKind.String) -// { -// sb.AppendLine($"{indent} obj.{prop.Name} = default;"); -// } -// else -// { -// sb.AppendLine($"{indent} // Non-nullable property, keep default"); -// } -// sb.AppendLine($"{indent} }}"); - -// // Handle actual value -// sb.AppendLine($"{indent} else"); -// sb.AppendLine($"{indent} {{"); -// GenerateDeserializeValue(sb, prop, indent + " "); -// sb.AppendLine($"{indent} }}"); - -// sb.AppendLine($"{indent}}}"); -// } - -// private static void GenerateDeserializeValue(StringBuilder sb, PropertyInfo prop, string indent) -// { -// var propAccess = $"obj.{prop.Name}"; - -// switch (prop.TypeKind) -// { -// case PropertyTypeKind.String: -// sb.AppendLine($"{indent}if (peekCode == BinaryTypeCode.StringEmpty)"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.ReadByte();"); -// sb.AppendLine($"{indent} {propAccess} = string.Empty;"); -// sb.AppendLine($"{indent}}}"); -// sb.AppendLine($"{indent}else if (peekCode == BinaryTypeCode.String)"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.ReadByte();"); -// sb.AppendLine($"{indent} var len = (int)context.ReadVarUInt();"); -// sb.AppendLine($"{indent} {propAccess} = context.ReadStringUtf8(len);"); -// sb.AppendLine($"{indent}}}"); -// sb.AppendLine($"{indent}else if (BinaryTypeCode.IsFixStr(peekCode))"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.ReadByte();"); -// sb.AppendLine($"{indent} var len = BinaryTypeCode.DecodeFixStrLength(peekCode);"); -// sb.AppendLine($"{indent} {propAccess} = len == 0 ? string.Empty : context.ReadStringUtf8(len);"); -// sb.AppendLine($"{indent}}}"); -// break; - -// case PropertyTypeKind.Int32: -// case PropertyTypeKind.NullableInt32: -// sb.AppendLine($"{indent}if (BinaryTypeCode.IsTinyInt(peekCode))"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.ReadByte();"); -// sb.AppendLine($"{indent} {propAccess} = BinaryTypeCode.DecodeTinyInt(peekCode);"); -// sb.AppendLine($"{indent}}}"); -// sb.AppendLine($"{indent}else if (peekCode == BinaryTypeCode.Int32)"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.ReadByte();"); -// sb.AppendLine($"{indent} {propAccess} = context.ReadVarInt();"); -// sb.AppendLine($"{indent}}}"); -// break; - -// case PropertyTypeKind.Int64: -// case PropertyTypeKind.NullableInt64: -// sb.AppendLine($"{indent}if (BinaryTypeCode.IsTinyInt(peekCode))"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.ReadByte();"); -// sb.AppendLine($"{indent} {propAccess} = BinaryTypeCode.DecodeTinyInt(peekCode);"); -// sb.AppendLine($"{indent}}}"); -// sb.AppendLine($"{indent}else if (peekCode == BinaryTypeCode.Int32)"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.ReadByte();"); -// sb.AppendLine($"{indent} {propAccess} = context.ReadVarInt();"); -// sb.AppendLine($"{indent}}}"); -// sb.AppendLine($"{indent}else if (peekCode == BinaryTypeCode.Int64)"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.ReadByte();"); -// sb.AppendLine($"{indent} {propAccess} = context.ReadVarLong();"); -// sb.AppendLine($"{indent}}}"); -// break; - -// case PropertyTypeKind.Boolean: -// case PropertyTypeKind.NullableBoolean: -// sb.AppendLine($"{indent}context.ReadByte();"); -// sb.AppendLine($"{indent}{propAccess} = peekCode == BinaryTypeCode.True;"); -// break; - -// case PropertyTypeKind.Double: -// case PropertyTypeKind.NullableDouble: -// sb.AppendLine($"{indent}context.ReadByte();"); -// sb.AppendLine($"{indent}{propAccess} = context.ReadDoubleUnsafe();"); -// break; - -// case PropertyTypeKind.Single: -// case PropertyTypeKind.NullableSingle: -// sb.AppendLine($"{indent}context.ReadByte();"); -// sb.AppendLine($"{indent}{propAccess} = context.ReadSingleUnsafe();"); -// break; - -// case PropertyTypeKind.Decimal: -// case PropertyTypeKind.NullableDecimal: -// sb.AppendLine($"{indent}context.ReadByte();"); -// sb.AppendLine($"{indent}{propAccess} = context.ReadDecimalUnsafe();"); -// break; - -// case PropertyTypeKind.DateTime: -// case PropertyTypeKind.NullableDateTime: -// sb.AppendLine($"{indent}context.ReadByte();"); -// sb.AppendLine($"{indent}{propAccess} = context.ReadDateTimeUnsafe();"); -// break; - -// case PropertyTypeKind.Guid: -// case PropertyTypeKind.NullableGuid: -// sb.AppendLine($"{indent}context.ReadByte();"); -// sb.AppendLine($"{indent}{propAccess} = context.ReadGuidUnsafe();"); -// break; - -// case PropertyTypeKind.Byte: -// case PropertyTypeKind.NullableByte: -// sb.AppendLine($"{indent}if (BinaryTypeCode.IsTinyInt(peekCode))"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.ReadByte();"); -// sb.AppendLine($"{indent} {propAccess} = (byte)BinaryTypeCode.DecodeTinyInt(peekCode);"); -// sb.AppendLine($"{indent}}}"); -// sb.AppendLine($"{indent}else"); -// sb.AppendLine($"{indent}{{"); -// sb.AppendLine($"{indent} context.ReadByte();"); -// sb.AppendLine($"{indent} {propAccess} = context.ReadByte();"); -// sb.AppendLine($"{indent}}}"); -// break; - -// case PropertyTypeKind.Int16: -// case PropertyTypeKind.NullableInt16: -// sb.AppendLine($"{indent}context.ReadByte();"); -// sb.AppendLine($"{indent}{propAccess} = context.ReadInt16Unsafe();"); -// break; - -// case PropertyTypeKind.UInt16: -// case PropertyTypeKind.NullableUInt16: -// sb.AppendLine($"{indent}context.ReadByte();"); -// sb.AppendLine($"{indent}{propAccess} = context.ReadUInt16Unsafe();"); -// break; - -// case PropertyTypeKind.UInt32: -// case PropertyTypeKind.NullableUInt32: -// sb.AppendLine($"{indent}context.ReadByte();"); -// sb.AppendLine($"{indent}{propAccess} = context.ReadVarUInt();"); -// break; - -// case PropertyTypeKind.UInt64: -// case PropertyTypeKind.NullableUInt64: -// sb.AppendLine($"{indent}context.ReadByte();"); -// sb.AppendLine($"{indent}{propAccess} = context.ReadVarULong();"); -// break; - -// case PropertyTypeKind.TimeSpan: -// case PropertyTypeKind.NullableTimeSpan: -// sb.AppendLine($"{indent}context.ReadByte();"); -// sb.AppendLine($"{indent}{propAccess} = context.ReadTimeSpanUnsafe();"); -// break; - -// case PropertyTypeKind.DateTimeOffset: -// case PropertyTypeKind.NullableDateTimeOffset: -// sb.AppendLine($"{indent}context.ReadByte();"); -// sb.AppendLine($"{indent}{propAccess} = context.ReadDateTimeOffsetUnsafe();"); -// break; - -// case PropertyTypeKind.Enum: -// case PropertyTypeKind.NullableEnum: -// sb.AppendLine($"{indent}// TODO: Enum deserialization needs type info"); -// sb.AppendLine($"{indent}throw new NotImplementedException(\"Enum deserialization not yet implemented\");"); -// break; - -// case PropertyTypeKind.Collection: -// case PropertyTypeKind.Complex: -// sb.AppendLine($"{indent}// TODO: Complex/Collection types"); -// sb.AppendLine($"{indent}throw new NotImplementedException(\"Complex/Collection deserialization not yet implemented\");"); -// break; - -// default: -// sb.AppendLine($"{indent}throw new NotImplementedException($\"Type deserialization not implemented\");"); -// break; -// } -// } -//} - -///// -///// Information about a class marked with [AcBinarySerializable]. -///// -//internal sealed class SerializableClassInfo -//{ -// public string Namespace { get; } -// public string ClassName { get; } -// public string FullTypeName { get; } -// public bool IsStruct { get; } -// public bool IsNestedType { get; } -// /// -// /// The type name to use in method signatures. For nested types this includes the containing types. -// /// e.g., "OuterClass.InnerClass" -// /// -// public string TypeNameForSignature { get; } -// public List Properties { get; } - -// public SerializableClassInfo(string ns, string className, string fullTypeName, bool isStruct, bool isNestedType, string typeNameForSignature, List properties) -// { -// Namespace = ns; -// ClassName = className; -// FullTypeName = fullTypeName; -// IsStruct = isStruct; -// IsNestedType = isNestedType; -// TypeNameForSignature = typeNameForSignature; -// Properties = properties; -// } -//} - -///// -///// Information about a serializable property. -///// -//internal sealed class PropertyInfo -//{ -// public string Name { get; } -// public string TypeName { get; } -// public PropertyTypeKind TypeKind { get; } -// public bool IsNullable { get; } - -// public PropertyInfo(string name, string typeName, PropertyTypeKind typeKind, bool isNullable) -// { -// Name = name; -// TypeName = typeName; -// TypeKind = typeKind; -// IsNullable = isNullable; -// } -//} - -///// -///// Kind of property type for code generation. -///// -//internal enum PropertyTypeKind -//{ -// Unknown, -// String, -// Int32, -// Int64, -// Int16, -// Byte, -// SByte, -// UInt16, -// UInt32, -// UInt64, -// Boolean, -// Single, -// Double, -// Decimal, -// DateTime, -// DateTimeOffset, -// TimeSpan, -// DateOnly, -// TimeOnly, -// Guid, -// Enum, -// Collection, -// Complex, -// // Nullable variants -// NullableInt32, -// NullableInt64, -// NullableInt16, -// NullableByte, -// NullableSByte, -// NullableUInt16, -// NullableUInt32, -// NullableUInt64, -// NullableBoolean, -// NullableSingle, -// NullableDouble, -// NullableDecimal, -// NullableDateTime, -// NullableDateTimeOffset, -// NullableTimeSpan, -// NullableDateOnly, -// NullableTimeOnly, -// NullableGuid, -// NullableEnum -//} +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace AyCode.Core.Serializers.SourceGenerator; + +/// +/// Generates IGeneratedBinaryWriter implementations for [AcBinarySerializable] types. +/// Also generates a ModuleInitializer that auto-registers all writers at startup. +/// +[Generator] +public class AcBinarySourceGenerator : IIncrementalGenerator +{ + private const string AttributeName = "AyCode.Core.Serializers.Attributes.AcBinarySerializableAttribute"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var classDeclarations = context.SyntaxProvider + .ForAttributeWithMetadataName( + AttributeName, + predicate: static (node, _) => node is ClassDeclarationSyntax || node is StructDeclarationSyntax, + transform: static (ctx, _) => GetClassInfo(ctx)) + .Where(static info => info != null); + + context.RegisterSourceOutput(classDeclarations.Collect(), + static (spc, classes) => Execute(classes!, spc)); + } + + private static SerializableClassInfo? GetClassInfo(GeneratorAttributeSyntaxContext context) + { + if (!(context.TargetSymbol is INamedTypeSymbol typeSymbol)) + return null; + + // Skip nested types — generated writer class can't be placed inside containing type + if (typeSymbol.ContainingType != null) + return null; + + var namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace + ? string.Empty + : typeSymbol.ContainingNamespace.ToDisplayString(); + + var properties = new List(); + foreach (var member in typeSymbol.GetMembers()) + { + if (member is IPropertySymbol p && + p.DeclaredAccessibility == Accessibility.Public && + p.GetMethod != null && p.SetMethod != null && + !p.IsIndexer && !p.IsStatic) + { + var hasIgnore = p.GetAttributes().Any(a => + { + var name = a.AttributeClass?.Name ?? ""; + return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute"; + }); + if (hasIgnore) continue; + + properties.Add(new PropInfo( + p.Name, + p.Type.ToDisplayString(), + GetKind(p.Type), + p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type))); + } + } + + // IId: Id first (index 0), then alphabetical — matches runtime TypeMetadataBase ordering + var isIId = typeSymbol.AllInterfaces.Any(i => + i.IsGenericType && + i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); + + if (isIId) + properties.Sort((a, b) => + { + var aIsId = a.Name == "Id" ? 0 : 1; + var bIsId = b.Name == "Id" ? 0 : 1; + if (aIsId != bIsId) return aIsId.CompareTo(bIsId); + return string.Compare(a.Name, b.Name, StringComparison.Ordinal); + }); + else + properties.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + + return new SerializableClassInfo(namespaceName, typeSymbol.Name, typeSymbol.ToDisplayString(), properties); + } + + private static void Execute(ImmutableArray classes, SourceProductionContext context) + { + if (classes.IsDefaultOrEmpty) return; + var valid = classes.Where(c => c != null).Cast().ToList(); + if (valid.Count == 0) return; + + foreach (var ci in valid) + context.AddSource($"{ci.ClassName}_GeneratedWriter.g.cs", SourceText.From(GenWriter(ci), Encoding.UTF8)); + + context.AddSource("AcBinaryGeneratedWriters_Init.g.cs", SourceText.From(GenInit(valid), Encoding.UTF8)); + } + + private static string GenWriter(SerializableClassInfo ci) + { + var sb = new StringBuilder(2048); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine("using System.Runtime.CompilerServices;"); + sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); + sb.AppendLine(); + if (!string.IsNullOrEmpty(ci.Namespace)) + sb.AppendLine($"namespace {ci.Namespace};"); + sb.AppendLine(); + sb.AppendLine($"internal sealed class {ci.ClassName}_GeneratedWriter : IGeneratedBinaryWriter"); + sb.AppendLine("{"); + sb.AppendLine($" internal static readonly {ci.ClassName}_GeneratedWriter Instance = new();"); + sb.AppendLine(); + sb.AppendLine(" public void WriteProperties(object value, AcBinarySerializer.BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase"); + sb.AppendLine(" {"); + sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);"); + + foreach (var p in ci.Properties) + { + sb.AppendLine(); + EmitProp(sb, p, " "); + } + + sb.AppendLine(" }"); + sb.AppendLine("}"); + return sb.ToString(); + } + + private static void EmitProp(StringBuilder sb, PropInfo p, string i) + { + var a = $"obj.{p.Name}"; + + // Nullable value types always use markered path (need Null marker) + if (IsNullableVTKind(p.TypeKind)) + { + sb.AppendLine($"{i}if ({a}.HasValue)"); + sb.AppendLine($"{i}{{"); + EmitVal(sb, Underlying(p.TypeKind), $"{a}.Value", i + " "); + sb.AppendLine($"{i}}}"); + sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.PropertySkip);"); + return; + } + + // Markerless types: write raw value only, no type marker, no PropertySkip + // Matches runtime WritePropertyMarkerless — these have ExpectedTypeCode + if (IsMarkerless(p.TypeKind)) + { + EmitMarkerless(sb, p.TypeKind, a, i); + return; + } + + // Non-markerless types: write WITH type marker byte (markered path) + switch (p.TypeKind) + { + case PropertyTypeKind.String: + sb.AppendLine($"{i}AcBinarySerializer.WriteStringGenerated({a}, context);"); + break; + case PropertyTypeKind.Complex: + case PropertyTypeKind.Collection: + if (p.IsNullable) + { + sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);"); + } + else + sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);"); + break; + default: + EmitSkip(sb, p.TypeKind, a, i); + break; + } + } + + /// + /// Returns true for property types that use markerless serialization in FastMode. + /// These types have ExpectedTypeCode at runtime — no type marker byte, no PropertySkip for defaults. + /// + private static bool IsMarkerless(PropertyTypeKind k) => k switch + { + PropertyTypeKind.Int32 or PropertyTypeKind.Int64 or PropertyTypeKind.Int16 or + PropertyTypeKind.Byte or PropertyTypeKind.UInt16 or PropertyTypeKind.UInt32 or PropertyTypeKind.UInt64 or + PropertyTypeKind.Double or PropertyTypeKind.Single or PropertyTypeKind.Decimal or + PropertyTypeKind.DateTime or PropertyTypeKind.Guid or + PropertyTypeKind.TimeSpan or PropertyTypeKind.DateTimeOffset => true, + _ => false + }; + + /// + /// Emits raw value only — no type marker, no PropertySkip. + /// Matches runtime WritePropertyMarkerless exactly. + /// + private static void EmitMarkerless(StringBuilder sb, PropertyTypeKind k, string a, string i) + { + switch (k) + { + case PropertyTypeKind.Int32: sb.AppendLine($"{i}context.WriteVarInt({a});"); break; + case PropertyTypeKind.Int64: sb.AppendLine($"{i}context.WriteVarLong({a});"); break; + case PropertyTypeKind.Double: sb.AppendLine($"{i}context.WriteRaw({a});"); break; + case PropertyTypeKind.Single: sb.AppendLine($"{i}context.WriteRaw({a});"); break; + case PropertyTypeKind.Decimal: sb.AppendLine($"{i}context.WriteDecimalBits({a});"); break; + case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteDateTimeBits({a});"); break; + case PropertyTypeKind.Guid: sb.AppendLine($"{i}context.WriteGuidBits({a});"); break; + case PropertyTypeKind.Byte: sb.AppendLine($"{i}context.WriteByte({a});"); break; + case PropertyTypeKind.Int16: sb.AppendLine($"{i}context.WriteRaw({a});"); break; + case PropertyTypeKind.UInt16: sb.AppendLine($"{i}context.WriteRaw({a});"); break; + case PropertyTypeKind.UInt32: sb.AppendLine($"{i}context.WriteVarUInt({a});"); break; + case PropertyTypeKind.UInt64: sb.AppendLine($"{i}context.WriteVarULong({a});"); break; + case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}context.WriteRaw({a}.Ticks);"); break; + case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}context.WriteDateTimeOffsetBits({a});"); break; + } + } + + private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string i) + { + switch (k) + { + case PropertyTypeKind.Int32: + sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a}); }}"); + break; + case PropertyTypeKind.Int64: + sb.AppendLine($"{i}if ({a} == 0L) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong({a}); }}"); + break; + case PropertyTypeKind.Boolean: + sb.AppendLine($"{i}if (!{a}) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.True);"); + break; + case PropertyTypeKind.Double: + sb.AppendLine($"{i}if ({a} == 0.0) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Float64); context.WriteRaw({a}); }}"); + break; + case PropertyTypeKind.Single: + sb.AppendLine($"{i}if ({a} == 0f) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Float32); context.WriteRaw({a}); }}"); + break; + case PropertyTypeKind.Decimal: + sb.AppendLine($"{i}if ({a} == 0m) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits({a}); }}"); + break; + case PropertyTypeKind.DateTime: + sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits({a});"); + break; + case PropertyTypeKind.Guid: + sb.AppendLine($"{i}if ({a} == System.Guid.Empty) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits({a}); }}"); + break; + case PropertyTypeKind.Byte: + sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt8); context.WriteByte({a}); }}"); + break; + case PropertyTypeKind.Int16: + sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int16); context.WriteRaw({a}); }}"); + break; + case PropertyTypeKind.UInt16: + sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt16); context.WriteRaw({a}); }}"); + break; + case PropertyTypeKind.UInt32: + sb.AppendLine($"{i}if ({a} == 0U) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt32); context.WriteVarUInt({a}); }}"); + break; + case PropertyTypeKind.UInt64: + sb.AppendLine($"{i}if ({a} == 0UL) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt64); context.WriteVarULong({a}); }}"); + break; + case PropertyTypeKind.Enum: + var s = a.Replace(".", "_"); + sb.AppendLine($"{i}var ev_{s} = (int){a};"); + sb.AppendLine($"{i}if (ev_{s} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else if (BinaryTypeCode.TryEncodeTinyInt(ev_{s}, out var te_{s})) {{ context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(te_{s}); }}"); + sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(ev_{s}); }}"); + break; + case PropertyTypeKind.TimeSpan: + sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.TimeSpan); context.WriteRaw({a}.Ticks);"); + break; + case PropertyTypeKind.DateTimeOffset: + sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTimeOffset); context.WriteDateTimeOffsetBits({a});"); + break; + default: + sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);"); + break; + } + } + + private static void EmitVal(StringBuilder sb, PropertyTypeKind k, string a, string i) + { + switch (k) + { + case PropertyTypeKind.Int32: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a});"); break; + case PropertyTypeKind.Int64: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong({a});"); break; + case PropertyTypeKind.Boolean: sb.AppendLine($"{i}context.WriteByte({a} ? BinaryTypeCode.True : BinaryTypeCode.False);"); break; + case PropertyTypeKind.Double: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Float64); context.WriteRaw({a});"); break; + case PropertyTypeKind.Single: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Float32); context.WriteRaw({a});"); break; + case PropertyTypeKind.Decimal: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits({a});"); break; + case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits({a});"); break; + case PropertyTypeKind.Guid: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits({a});"); break; + default: EmitSkip(sb, k, a, i); break; + } + } + + private static string GenInit(List classes) + { + var sb = new StringBuilder(512); + sb.AppendLine("// "); + sb.AppendLine("using System.Runtime.CompilerServices;"); + sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); + sb.AppendLine(); + sb.AppendLine("namespace AyCode.Core.Serializers.Generated;"); + sb.AppendLine(); + sb.AppendLine("internal static class AcBinaryGeneratedWritersInit"); + sb.AppendLine("{"); + sb.AppendLine(" [ModuleInitializer]"); + sb.AppendLine(" internal static void Register()"); + sb.AppendLine(" {"); + foreach (var ci in classes) + sb.AppendLine($" AcBinarySerializer.RegisterGeneratedWriter(typeof({ci.FullTypeName}), {ci.FullTypeName}_GeneratedWriter.Instance);"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + return sb.ToString(); + } + + #region Type analysis + + private static bool IsNullableVT(ITypeSymbol t) => + t is INamedTypeSymbol n && n.IsGenericType && n.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; + + private static PropertyTypeKind GetKind(ITypeSymbol type) + { + if (type is INamedTypeSymbol n && n.IsGenericType && n.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T) + return GetKindCore(n.TypeArguments[0], true); + return GetKindCore(type, false); + } + + private static PropertyTypeKind GetKindCore(ITypeSymbol type, bool nullable) + { + switch (type.SpecialType) + { + case SpecialType.System_String: return PropertyTypeKind.String; + case SpecialType.System_Int32: return nullable ? PropertyTypeKind.NullableInt32 : PropertyTypeKind.Int32; + case SpecialType.System_Int64: return nullable ? PropertyTypeKind.NullableInt64 : PropertyTypeKind.Int64; + case SpecialType.System_Int16: return nullable ? PropertyTypeKind.NullableInt16 : PropertyTypeKind.Int16; + case SpecialType.System_Byte: return nullable ? PropertyTypeKind.NullableByte : PropertyTypeKind.Byte; + case SpecialType.System_UInt16: return nullable ? PropertyTypeKind.NullableUInt16 : PropertyTypeKind.UInt16; + case SpecialType.System_UInt32: return nullable ? PropertyTypeKind.NullableUInt32 : PropertyTypeKind.UInt32; + case SpecialType.System_UInt64: return nullable ? PropertyTypeKind.NullableUInt64 : PropertyTypeKind.UInt64; + case SpecialType.System_Boolean: return nullable ? PropertyTypeKind.NullableBoolean : PropertyTypeKind.Boolean; + case SpecialType.System_Single: return nullable ? PropertyTypeKind.NullableSingle : PropertyTypeKind.Single; + case SpecialType.System_Double: return nullable ? PropertyTypeKind.NullableDouble : PropertyTypeKind.Double; + case SpecialType.System_Decimal: return nullable ? PropertyTypeKind.NullableDecimal : PropertyTypeKind.Decimal; + case SpecialType.System_DateTime: return nullable ? PropertyTypeKind.NullableDateTime : PropertyTypeKind.DateTime; + default: break; + } + var fn = type.ToDisplayString(); + if (fn == "System.Guid") return nullable ? PropertyTypeKind.NullableGuid : PropertyTypeKind.Guid; + if (fn == "System.TimeSpan") return nullable ? PropertyTypeKind.NullableTimeSpan : PropertyTypeKind.TimeSpan; + if (fn == "System.DateTimeOffset") return nullable ? PropertyTypeKind.NullableDateTimeOffset : PropertyTypeKind.DateTimeOffset; + if (type.TypeKind == TypeKind.Enum) return nullable ? PropertyTypeKind.NullableEnum : PropertyTypeKind.Enum; + if (type is IArrayTypeSymbol) return PropertyTypeKind.Collection; + if (type is INamedTypeSymbol nt && nt.AllInterfaces.Any(iface => iface.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)) + return PropertyTypeKind.Collection; + if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct) return PropertyTypeKind.Complex; + return PropertyTypeKind.Unknown; + } + + private static bool IsNullableVTKind(PropertyTypeKind k) => k >= PropertyTypeKind.NullableInt32; + + private static PropertyTypeKind Underlying(PropertyTypeKind k) => k switch + { + PropertyTypeKind.NullableInt32 => PropertyTypeKind.Int32, PropertyTypeKind.NullableInt64 => PropertyTypeKind.Int64, + PropertyTypeKind.NullableInt16 => PropertyTypeKind.Int16, PropertyTypeKind.NullableByte => PropertyTypeKind.Byte, + PropertyTypeKind.NullableUInt16 => PropertyTypeKind.UInt16, PropertyTypeKind.NullableUInt32 => PropertyTypeKind.UInt32, + PropertyTypeKind.NullableUInt64 => PropertyTypeKind.UInt64, PropertyTypeKind.NullableBoolean => PropertyTypeKind.Boolean, + PropertyTypeKind.NullableSingle => PropertyTypeKind.Single, PropertyTypeKind.NullableDouble => PropertyTypeKind.Double, + PropertyTypeKind.NullableDecimal => PropertyTypeKind.Decimal, PropertyTypeKind.NullableDateTime => PropertyTypeKind.DateTime, + PropertyTypeKind.NullableDateTimeOffset => PropertyTypeKind.DateTimeOffset, PropertyTypeKind.NullableTimeSpan => PropertyTypeKind.TimeSpan, + PropertyTypeKind.NullableGuid => PropertyTypeKind.Guid, PropertyTypeKind.NullableEnum => PropertyTypeKind.Enum, + _ => PropertyTypeKind.Unknown + }; + + #endregion +} + +internal sealed class SerializableClassInfo +{ + public string Namespace { get; } + public string ClassName { get; } + public string FullTypeName { get; } + public List Properties { get; } + public SerializableClassInfo(string ns, string cn, string ftn, List p) + { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; } +} + +internal sealed class PropInfo +{ + public string Name { get; } + public string TypeName { get; } + public PropertyTypeKind TypeKind { get; } + public bool IsNullable { get; } + public PropInfo(string n, string tn, PropertyTypeKind tk, bool nullable) + { Name = n; TypeName = tn; TypeKind = tk; IsNullable = nullable; } +} + +internal enum PropertyTypeKind +{ + Unknown, String, Int32, Int64, Int16, Byte, UInt16, UInt32, UInt64, + Boolean, Single, Double, Decimal, DateTime, DateTimeOffset, TimeSpan, Guid, Enum, + Collection, Complex, + NullableInt32, NullableInt64, NullableInt16, NullableByte, NullableUInt16, NullableUInt32, NullableUInt64, + NullableBoolean, NullableSingle, NullableDouble, NullableDecimal, NullableDateTime, + NullableDateTimeOffset, NullableTimeSpan, NullableGuid, NullableEnum +} diff --git a/AyCode.Core.Tests/GeneratedWriters/TestOrderWriter.cs b/AyCode.Core.Tests/GeneratedWriters/TestOrderWriter.cs new file mode 100644 index 0000000..779dee3 --- /dev/null +++ b/AyCode.Core.Tests/GeneratedWriters/TestOrderWriter.cs @@ -0,0 +1,141 @@ +using System.Runtime.CompilerServices; +using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Tests.TestModels; + +namespace AyCode.Core.Tests.GeneratedWriters; + +/// +/// Hand-written generated binary writer for TestOrder. +/// Demonstrates the pattern that the source generator will produce. +/// +/// Bypasses the runtime switch/delegate property loop: +/// - Direct obj.Property access instead of Func<>.Invoke() +/// - No switch dispatch per property +/// - No boxing for value types +/// - Small method (~500B native) vs 27KB WriteObject — better ICache +/// +/// Properties are written in alphabetical order to match the runtime serializer. +/// Complex/Collection properties fall back to the runtime serializer via WriteValue. +/// +internal sealed class TestOrderWriter : IGeneratedBinaryWriter +{ + internal static readonly TestOrderWriter Instance = new(); + + public void WriteProperties( + object value, + AcBinarySerializer.BinarySerializationContext context, + int depth) + where TOutput : struct, IBinaryOutputBase + { + var obj = Unsafe.As(value); + var nextDepth = depth; + + // Properties in alphabetical order (matching runtime serializer): + + // AuditMetadata: MetadataInfo? (complex, nullable) + WriteComplexOrNull(obj.AuditMetadata, context, nextDepth); + + // Category: SharedCategory? (complex, nullable) + WriteComplexOrNull(obj.Category, context, nextDepth); + + // CreatedAt: DateTime + context.WriteByte(BinaryTypeCode.DateTime); + context.WriteDateTimeBits(obj.CreatedAt); + + // Id: int + WriteInt32OrSkip(obj.Id, context); + + // Items: List (collection) + WriteComplexOrNull(obj.Items, context, nextDepth); + + // MetadataList: List (collection) + WriteComplexOrNull(obj.MetadataList, context, nextDepth); + + // NoMergeItems: List (collection) + WriteComplexOrNull(obj.NoMergeItems, context, nextDepth); + + // OrderMetadata: MetadataInfo? (complex, nullable) + WriteComplexOrNull(obj.OrderMetadata, context, nextDepth); + + // OrderNumber: string + AcBinarySerializer.WriteStringGenerated(obj.OrderNumber, context); + + // Owner: SharedUser? (complex, nullable) + WriteComplexOrNull(obj.Owner, context, nextDepth); + + // PaidDateUtc: DateTime? (nullable) + var paidDate = obj.PaidDateUtc; + if (paidDate.HasValue) + { + context.WriteByte(BinaryTypeCode.DateTime); + context.WriteDateTimeBits(paidDate.Value); + } + else + { + context.WriteByte(BinaryTypeCode.Null); + } + + // PrimaryTag: SharedTag? (complex, nullable) + WriteComplexOrNull(obj.PrimaryTag, context, nextDepth); + + // SecondaryTag: SharedTag? (complex, nullable) + WriteComplexOrNull(obj.SecondaryTag, context, nextDepth); + + // Status: TestStatus (enum) + context.WriteByte(BinaryTypeCode.Enum); + var enumVal = (int)obj.Status; + if (BinaryTypeCode.TryEncodeTinyInt(enumVal, out var tinyEnum)) + context.WriteByte(tinyEnum); + else + { + context.WriteByte(BinaryTypeCode.Int32); + context.WriteVarInt(enumVal); + } + + // Tags: List (collection) + WriteComplexOrNull(obj.Tags, context, nextDepth); + + // TotalAmount: decimal + if (obj.TotalAmount == 0m) + context.WriteByte(BinaryTypeCode.PropertySkip); + else + { + context.WriteByte(BinaryTypeCode.Decimal); + context.WriteDecimalBits(obj.TotalAmount); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteInt32OrSkip(int value, AcBinarySerializer.BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase + { + if (value == 0) + { + context.WriteByte(BinaryTypeCode.PropertySkip); + return; + } + + if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny)) + { + context.WriteByte(BinaryTypeCode.Int32); + context.WriteByte(tiny); + return; + } + + context.WriteByte(BinaryTypeCode.Int32); + context.WriteVarInt(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteComplexOrNull(object? value, AcBinarySerializer.BinarySerializationContext context, int depth) + where TOutput : struct, IBinaryOutputBase + { + if (value == null) + { + context.WriteByte(BinaryTypeCode.PropertySkip); + return; + } + + AcBinarySerializer.WriteValueGenerated(value, value.GetType(), context, depth); + } +} diff --git a/AyCode.Core.Tests/Serialization/GeneratedSerializerIntegrationTests.cs b/AyCode.Core.Tests/Serialization/GeneratedSerializerIntegrationTests.cs index 40ac6de..b13c155 100644 --- a/AyCode.Core.Tests/Serialization/GeneratedSerializerIntegrationTests.cs +++ b/AyCode.Core.Tests/Serialization/GeneratedSerializerIntegrationTests.cs @@ -4,98 +4,37 @@ using AyCode.Core.Tests.TestModels; namespace AyCode.Core.Tests.Serialization; /// -/// Tests for Source Generator based serialization integration. +/// Tests for Source Generator based IGeneratedBinaryWriter integration. /// [TestClass] public class GeneratedSerializerIntegrationTests { [TestMethod] - public void GeneratedSerializerType_Exists_ForMarkedTypes() + public void GeneratedWriterType_Exists_ForMarkedTypes() { - // Arrange - types marked with [AcBinarySerializable] var type = typeof(GeneratedSerializerTestModel); - - // Act - find the generated serializer type directly - var generatedTypeName = $"{type.FullName}_AcBinarySerializer"; - var serializerType = type.Assembly.GetType(generatedTypeName); - - // Assert - Assert.IsNotNull(serializerType, - $"Generated serializer type '{generatedTypeName}' should exist for [AcBinarySerializable] marked type"); + var writerTypeName = $"{type.FullName}_GeneratedWriter"; + var writerType = type.Assembly.GetType(writerTypeName); + + Assert.IsNotNull(writerType, + $"Generated writer type '{writerTypeName}' should exist for [AcBinarySerializable] marked type"); } [TestMethod] - public void GeneratedSerializerType_HasCorrectMethods() + public void GeneratedWriterType_ImplementsInterface() { - // Arrange var type = typeof(SimpleGeneratedModel); - - // Act - find the generated serializer type directly - var generatedTypeName = $"{type.FullName}_AcBinarySerializer"; - var serializerType = type.Assembly.GetType(generatedTypeName); - - // Assert - Assert.IsNotNull(serializerType, $"Generated serializer type '{generatedTypeName}' should exist"); - - var serializeMethod = serializerType.GetMethod("Serialize", - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); - Assert.IsNotNull(serializeMethod, "Serialize method should exist"); - - var deserializeMethod = serializerType.GetMethod("Deserialize", - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); - Assert.IsNotNull(deserializeMethod, "Deserialize method should exist"); - - var propertyNamesField = serializerType.GetField("PropertyNames", - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); - Assert.IsNotNull(propertyNamesField, "PropertyNames field should exist"); - - var propertyNames = propertyNamesField.GetValue(null) as string[]; - Assert.IsNotNull(propertyNames, "PropertyNames should not be null"); - Assert.AreEqual(3, propertyNames.Length, "SimpleGeneratedModel has 3 properties"); - - // Verify alphabetical order - Assert.AreEqual("Age", propertyNames[0]); - Assert.AreEqual("FirstName", propertyNames[1]); - Assert.AreEqual("LastName", propertyNames[2]); + var writerTypeName = $"{type.FullName}_GeneratedWriter"; + var writerType = type.Assembly.GetType(writerTypeName); + + Assert.IsNotNull(writerType, $"Generated writer type '{writerTypeName}' should exist"); + Assert.IsTrue(typeof(IGeneratedBinaryWriter).IsAssignableFrom(writerType), + "Generated writer should implement IGeneratedBinaryWriter"); } [TestMethod] - public void GeneratedSerializerPropertyNames_MatchRuntimeOrder() + public void Serialization_WorksCorrectly_WithGeneratedWriterPresent() { - // This test verifies that the generated property order matches the runtime serializer's order - // This is critical for binary compatibility! - - var type = typeof(GeneratedSerializerTestModel); - - // Get generated property names - var generatedTypeName = $"{type.FullName}_AcBinarySerializer"; - var serializerType = type.Assembly.GetType(generatedTypeName); - Assert.IsNotNull(serializerType); - - var propertyNamesField = serializerType.GetField("PropertyNames", - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); - var generatedNames = propertyNamesField?.GetValue(null) as string[]; - Assert.IsNotNull(generatedNames); - - // Get runtime property names using the same logic as TypeMetadataBase - var runtimeProps = type.GetProperties( - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) - .Where(p => p.CanRead && p.CanWrite && !p.GetIndexParameters().Any()) - .OrderBy(p => p.Name, StringComparer.Ordinal) - .Select(p => p.Name) - .ToArray(); - - // Assert they match - CollectionAssert.AreEqual(runtimeProps, generatedNames, - "Generated property names must match runtime property order for binary compatibility"); - } - - [TestMethod] - public void Serialization_WorksCorrectly_WithGeneratedSerializerPresent() - { - // This test ensures that regular serialization still works even when - // generated serializers are present (they are not yet integrated into the hot path) - var original = new GeneratedSerializerTestModel { Id = 42, @@ -107,12 +46,10 @@ public class GeneratedSerializerIntegrationTests Price = 99.99m, BigNumber = 9999999999L }; - - // Serialize and deserialize using the regular path + var bytes = AcBinarySerializer.Serialize(original, AcBinarySerializerOptions.WithoutReferenceHandling); var deserialized = AcBinaryDeserializer.Deserialize(bytes); - - // Assert + Assert.IsNotNull(deserialized); Assert.AreEqual(original.Id, deserialized.Id); Assert.AreEqual(original.Name, deserialized.Name); @@ -125,25 +62,73 @@ public class GeneratedSerializerIntegrationTests } [TestMethod] - public void NestedType_GeneratedSerializer_IsFound() + public void GeneratedWriter_PrimitiveClass_RoundTrip() { - // Test that nested types (like QuickBenchmark.TestClassWithRepeatedValues) - // have their generated serializers properly named and discoverable - - var type = typeof(QuickBenchmark.TestClassWithRepeatedValues); - var ns = type.Namespace ?? ""; - - // For nested types, the generated class is at namespace level with just the type name - var simpleName = $"{(string.IsNullOrEmpty(ns) ? "" : ns + ".")}{type.Name}_AcBinarySerializer"; - var serializerType = type.Assembly.GetType(simpleName); - - Assert.IsNotNull(serializerType, - $"Generated serializer for nested type should be found at '{simpleName}'"); - - // Verify it has the expected methods - Assert.IsNotNull(serializerType.GetMethod("Serialize", - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)); - Assert.IsNotNull(serializerType.GetMethod("Deserialize", - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)); + var original = new PrimitiveTestClass + { + IntValue = 42, + StringValue = "TestName", + BoolValue = true, + DoubleValue = 3.14, + DateTimeValue = new DateTime(2025, 1, 5, 10, 30, 0, DateTimeKind.Utc), + GuidValue = Guid.NewGuid(), + DecimalValue = 99.99m, + LongValue = 9999999999L, + FloatValue = 1.5f, + ByteValue = 42, + ShortValue = 123, + EnumValue = TestStatus.Active, + NullableInt = 7, + NullableIntNull = null + }; + + var options = AcBinarySerializerOptions.FastMode; + var bytes = AcBinarySerializer.Serialize(original, options); + var deserialized = AcBinaryDeserializer.Deserialize(bytes, options); + + Assert.IsNotNull(deserialized); + Assert.AreEqual(original.IntValue, deserialized.IntValue); + Assert.AreEqual(original.StringValue, deserialized.StringValue); + Assert.AreEqual(original.BoolValue, deserialized.BoolValue); + Assert.AreEqual(original.DoubleValue, deserialized.DoubleValue); + Assert.AreEqual(original.DateTimeValue, deserialized.DateTimeValue); + Assert.AreEqual(original.GuidValue, deserialized.GuidValue); + Assert.AreEqual(original.DecimalValue, deserialized.DecimalValue); + Assert.AreEqual(original.LongValue, deserialized.LongValue); + Assert.AreEqual(original.NullableInt, deserialized.NullableInt); + Assert.IsNull(deserialized.NullableIntNull); + } + + [TestMethod] + public void GeneratedWriter_ComplexHierarchy_RoundTrip() + { + TestDataFactory.ResetIdCounter(); + var sharedTag = TestDataFactory.CreateTag("SharedTag"); + var sharedUser = TestDataFactory.CreateUser("shareduser"); + + var order = TestDataFactory.CreateOrder( + itemCount: 2, + palletsPerItem: 2, + measurementsPerPallet: 2, + pointsPerMeasurement: 2, + sharedTag: sharedTag, + sharedUser: sharedUser); + + var options = AcBinarySerializerOptions.FastMode; + var bytes = AcBinarySerializer.Serialize(order, options); + var deserialized = AcBinaryDeserializer.Deserialize(bytes, options); + + Assert.IsNotNull(deserialized); + Assert.AreEqual(order.Id, deserialized.Id); + Assert.AreEqual(order.OrderNumber, deserialized.OrderNumber); + Assert.AreEqual(order.Status, deserialized.Status); + Assert.AreEqual(order.TotalAmount, deserialized.TotalAmount); + Assert.AreEqual(order.Items.Count, deserialized.Items.Count); + + for (var i = 0; i < order.Items.Count; i++) + { + Assert.AreEqual(order.Items[i].Id, deserialized.Items[i].Id); + Assert.AreEqual(order.Items[i].Pallets.Count, deserialized.Items[i].Pallets.Count); + } } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 5046cd1..37b14a7 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -218,6 +218,44 @@ public static partial class AcBinarySerializer } #endif + /// + /// Registers a source-generated binary writer for the specified type. + /// Once registered, WriteObject bypasses the runtime switch/delegate property loop + /// and calls the generated writer directly — eliminating Func<>.Invoke() overhead. + /// Call once at startup (e.g., in a static constructor or module initializer). + /// + /// The type to register the writer for. + /// The generated writer instance (typically a singleton). + internal static void RegisterGeneratedWriter(IGeneratedBinaryWriter writer) + { + ArgumentNullException.ThrowIfNull(writer); + GeneratedWriterRegistry.Register(typeof(T), writer); + } + + /// + /// Registers a source-generated binary writer for the specified type. + /// + internal static void RegisterGeneratedWriter(Type type, IGeneratedBinaryWriter writer) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(writer); + GeneratedWriterRegistry.Register(type, writer); + } + + /// + /// Thread-safe registry for generated writers. Looked up once per TypeMetadataWrapper creation. + /// + internal static class GeneratedWriterRegistry + { + private static readonly ConcurrentDictionary Writers = new(); + + internal static void Register(Type type, IGeneratedBinaryWriter writer) => Writers[type] = writer; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static IGeneratedBinaryWriter? TryGet(Type type) => + Writers.TryGetValue(type, out var writer) ? writer : null; + } + /// /// Serialize object to binary with default options. /// @@ -429,6 +467,38 @@ public static partial class AcBinarySerializer #endregion + #region Generated Writer Bridge Methods + + /// + /// Bridge for generated writers to call the runtime WriteValue for complex/collection properties. + /// Generated writers handle primitives directly; complex types delegate here. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void WriteValueGenerated(object? value, Type type, BinarySerializationContext context, int depth) + where TOutput : struct, IBinaryOutputBase + { + WriteValue(value, type, context, depth); + } + + /// + /// Bridge for generated writers to call the runtime WriteString. + /// Matches WritePropertyOrSkip String case exactly: null → PropertySkip, empty → StringEmpty. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void WriteStringGenerated(string? value, BinarySerializationContext context) + where TOutput : struct, IBinaryOutputBase + { + if (string.IsNullOrEmpty(value)) + { + context.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty); + return; + } + + WriteString(value, context); + } + + #endregion + #region Value Writing private static void WriteValue(object? value, Type type, BinarySerializationContext context, int depth) @@ -1035,6 +1105,16 @@ public static partial class AcBinarySerializer var propCount = properties.Length; var hasPropertyFilter = context.HasPropertyFilter; + // Source-generated fast path: bypass the entire switch/delegate loop. + // Only when no caching features are active (no string interning, no reference handling) + // to avoid scan pass / write pass mismatch with interned strings and tracked references. + var generatedWriter = wrapper.GeneratedWriter; + if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata && !context.HasCaching) + { + generatedWriter.WriteProperties(value, context, nextDepth); + return; + } + if (!context.UseMetadata) { // Markerless loop: no extra branching per property for the common case. diff --git a/AyCode.Core/Serializers/Binaries/IGeneratedBinaryWriter.cs b/AyCode.Core/Serializers/Binaries/IGeneratedBinaryWriter.cs new file mode 100644 index 0000000..f8389ac --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/IGeneratedBinaryWriter.cs @@ -0,0 +1,29 @@ +using System.Runtime.CompilerServices; + +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Interface for source-generated binary property writers. +/// Implementations bypass the runtime switch/delegate property loop in WriteObject. +/// Each generated writer handles all properties of a specific type using direct obj.Property access. +/// +/// Performance gains over runtime path: +/// - No Func<>.Invoke() delegate calls (~5-8ns/property saved) +/// - No switch dispatch (~2-3ns/property saved) +/// - No boxing for value type properties +/// - Small per-type code (~300B) vs 27KB monolithic WriteObject — better ICache behavior +/// +internal interface IGeneratedBinaryWriter +{ + /// + /// Writes all properties of the given object to the serialization context. + /// Called from WriteObject when a generated writer is available for the type. + /// The implementation uses direct property access (obj.Id, obj.Name, etc.) instead of delegates. + /// + /// The object whose properties to write. Implementation casts to the concrete type. + /// The serialization context (owns buffer, position, options). + /// Current depth in the object graph (for nested object serialization). + /// Output strategy (ArrayBinaryOutput or BufferWriterBinaryOutput). + void WriteProperties(object value, AcBinarySerializer.BinarySerializationContext context, int depth) + where TOutput : struct, IBinaryOutputBase; +} diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index 693686c..f654c4c 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -58,6 +58,13 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// internal TypeMetadataWrapper?[]? PropertyTypeWrappers; + /// + /// Source-generated binary writer for this type. Bypasses the runtime switch/delegate loop. + /// Set via AcBinarySerializer.RegisterGeneratedWriter. Null = use runtime path. + /// Checked once per object in WriteObject (not per property). + /// + internal IGeneratedBinaryWriter? GeneratedWriter; + /// /// Options-filtered subset of metadata.ReferenceProperties for the scan pass. /// Built lazily on first scan pass call, stable during session, cleared in ResetTracking. @@ -135,6 +142,9 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat // Pre-allocate PropertyTypeWrappers — eliminates null/resize checks from hot path if (metadata.ComplexPropertyCount > 0) PropertyTypeWrappers = new TypeMetadataWrapper?[metadata.ComplexPropertyCount]; + + // Lookup generated writer from registry (once per wrapper creation, not per serialization) + GeneratedWriter = AcBinarySerializer.GeneratedWriterRegistry.TryGet(metadata.SourceType); } [MethodImpl(MethodImplOptions.AggressiveInlining)]