diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index 14372d0..f291741 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -169,20 +169,21 @@ public static partial class AcBinaryDeserializer { var context = state.Context; var target = state.Target; - var peekCode = context.PeekByte(); - // Nincs megfelelő target property → skip + // Nincs megfelelő target property → skip (SkipValue reads its own marker byte) if (propInfo == null) { SkipValue(context, state.Metadata); return; } - // Skip marker - property has default/null value - if (peekCode == BinaryTypeCode.PropertySkip) - { - context.ReadByte(); // consume Skip marker + // Read marker once — eliminates redundant PeekByte + ReadByte boundary checks. + // All branches below receive the already-consumed typeCode. + var typeCode = context.ReadByte(); + // Skip marker - property has default/null value + if (typeCode == BinaryTypeCode.PropertySkip) + { // Populate mode: overwrite with default (existing object may have non-default values) // Deserialize mode: skip write (new object already has defaults from CreateInstance) if (!state.SkipDefaultWrite) @@ -193,9 +194,8 @@ public static partial class AcBinaryDeserializer } // Null values - always set - if (peekCode == BinaryTypeCode.Null) + if (typeCode == BinaryTypeCode.Null) { - context.ReadByte(); // consume Null marker propInfo.SetValue(target, null); return; } @@ -203,13 +203,11 @@ public static partial class AcBinaryDeserializer var nextDepth = state.NextDepth; // Handle collections - if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection) + if (typeCode == BinaryTypeCode.Array && propInfo.IsCollection) { var existingCollection = propInfo.GetValue(target); if (existingCollection is IList existingList) { - context.ReadByte(); // consume Array marker - // Merge mode with IId collection: use merge logic if (state.IsMergeMode && propInfo.IsIIdCollection) { @@ -225,12 +223,13 @@ public static partial class AcBinaryDeserializer } // Handle nested complex objects - reuse existing if available - if ((peekCode == BinaryTypeCode.Object || peekCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType) + if ((typeCode == BinaryTypeCode.Object || typeCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType) { var existingObj = propInfo.GetValue(target); if (existingObj != null) { - // ReadValue kezeli mindkét markert + // Marker already consumed → rewind so ReadValue can read it + context._position--; var nestedValue = ReadValue(context, propInfo.PropertyType, nextDepth); if (nestedValue != null) { @@ -246,25 +245,28 @@ public static partial class AcBinaryDeserializer } // Default: read value and set (for primitives, strings, new objects) - var positionBeforeRead = context.Position; + var positionBeforeRead = context.Position - 1; // marker already consumed try { - // Use typed setters for primitives and strings to avoid ReadValue dispatch + // Use typed setters for primitives and strings to avoid ReadValue dispatch. + // typeCode is already consumed — TryReadAndSetTypedValue skips its internal ReadByte. if (propInfo.AccessorType != PropertyAccessorType.Object && - TryReadAndSetTypedValue(context, target, propInfo, peekCode)) + TryReadAndSetTypedValue(context, target, propInfo, typeCode)) return; // Complex property with Object marker: use pre-cached wrapper to skip GetWrapper lookup var complexIdx = propInfo.ComplexPropertyIndex; - if (complexIdx >= 0 && peekCode == BinaryTypeCode.Object) + if (complexIdx >= 0 && typeCode == BinaryTypeCode.Object) { - context.ReadByte(); // consume Object marker + // Marker already consumed — go straight to ReadObjectCoreWithWrapper var propWrapper = ResolvePropertyWrapper(state.ParentWrapper, complexIdx, propInfo.PropertyType, context); var value = ReadObjectCoreWithWrapper(context, propWrapper, nextDepth, cacheIndex: -1); propInfo.SetValue(target, value); } else { + // Marker already consumed → rewind so ReadValue can read it + context._position--; var value = ReadValue(context, propInfo.PropertyType, nextDepth); propInfo.SetValue(target, value); } @@ -275,7 +277,7 @@ public static partial class AcBinaryDeserializer throw new AcBinaryDeserializationException( $"Type mismatch for property '{propInfo.Name}' (index {propertyIndex}) on '{targetType.Name}'. " + $"Expected type: '{propInfo.PropertyType.FullName}'. " + - $"PeekCode before read: {peekCode} (0x{peekCode:X2}). " + + $"TypeCode read: {typeCode} (0x{typeCode:X2}). " + $"Position before read: {positionBeforeRead}, current: {context.Position}. " + $"Depth: {state.Depth}. " + $"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " + diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 3dc462b..9f0c8fe 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -608,194 +608,173 @@ public static partial class AcBinaryDeserializer /// /// Tries to read and set a primitive value directly using typed setters to avoid boxing. + /// The type code marker byte is already consumed by the caller. /// Returns true if handled, false if should fall back to generic path. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryReadAndSetTypedValue(BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo, byte peekCode) + private static bool TryReadAndSetTypedValue(BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo, byte typeCode) where TInput : struct, IBinaryInputBase { // Only handle if we have a typed setter if (propInfo.AccessorType == PropertyAccessorType.Object) return false; - // Handle based on property setter type and incoming data type + // Handle based on property setter type and incoming data type. + // The marker byte (typeCode) is already consumed — no ReadByte() needed. switch (propInfo.AccessorType) { case PropertyAccessorType.Int32: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetInt32(target, BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.Int32) + if (typeCode == BinaryTypeCode.Int32) { - context.ReadByte(); propInfo.SetInt32(target, context.ReadVarInt()); return true; } break; case PropertyAccessorType.Int64: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetInt64(target, BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetInt64(target, BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.Int32) + if (typeCode == BinaryTypeCode.Int32) { - context.ReadByte(); propInfo.SetInt64(target, context.ReadVarInt()); return true; } - if (peekCode == BinaryTypeCode.Int64) + if (typeCode == BinaryTypeCode.Int64) { - context.ReadByte(); propInfo.SetInt64(target, context.ReadVarLong()); return true; } break; case PropertyAccessorType.Boolean: - if (peekCode == BinaryTypeCode.True) + if (typeCode == BinaryTypeCode.True) { - context.ReadByte(); propInfo.SetBoolean(target, true); return true; } - if (peekCode == BinaryTypeCode.False) + if (typeCode == BinaryTypeCode.False) { - context.ReadByte(); propInfo.SetBoolean(target, false); return true; } break; case PropertyAccessorType.Double: - if (peekCode == BinaryTypeCode.Float64) + if (typeCode == BinaryTypeCode.Float64) { - context.ReadByte(); propInfo.SetDouble(target, context.ReadDoubleUnsafe()); return true; } break; case PropertyAccessorType.Single: - if (peekCode == BinaryTypeCode.Float32) + if (typeCode == BinaryTypeCode.Float32) { - context.ReadByte(); propInfo.SetSingle(target, context.ReadSingleUnsafe()); return true; } break; case PropertyAccessorType.Decimal: - if (peekCode == BinaryTypeCode.Decimal) + if (typeCode == BinaryTypeCode.Decimal) { - context.ReadByte(); propInfo.SetDecimal(target, context.ReadDecimalUnsafe()); return true; } break; case PropertyAccessorType.DateTime: - if (peekCode == BinaryTypeCode.DateTime) + if (typeCode == BinaryTypeCode.DateTime) { - context.ReadByte(); propInfo.SetDateTime(target, context.ReadDateTimeUnsafe()); return true; } break; case PropertyAccessorType.Guid: - if (peekCode == BinaryTypeCode.Guid) + if (typeCode == BinaryTypeCode.Guid) { - context.ReadByte(); propInfo.SetGuid(target, context.ReadGuidUnsafe()); return true; } break; case PropertyAccessorType.Byte: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetByte(target, (byte)BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetByte(target, (byte)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.UInt8) + if (typeCode == BinaryTypeCode.UInt8) { - context.ReadByte(); propInfo.SetByte(target, context.ReadByte()); return true; } break; case PropertyAccessorType.Int16: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetInt16(target, (short)BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetInt16(target, (short)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.Int16) + if (typeCode == BinaryTypeCode.Int16) { - context.ReadByte(); propInfo.SetInt16(target, context.ReadInt16Unsafe()); return true; } break; case PropertyAccessorType.UInt16: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetUInt16(target, (ushort)BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetUInt16(target, (ushort)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.UInt16) + if (typeCode == BinaryTypeCode.UInt16) { - context.ReadByte(); propInfo.SetUInt16(target, context.ReadUInt16Unsafe()); return true; } break; case PropertyAccessorType.UInt32: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetUInt32(target, (uint)BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetUInt32(target, (uint)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.UInt32) + if (typeCode == BinaryTypeCode.UInt32) { - context.ReadByte(); propInfo.SetUInt32(target, context.ReadVarUInt()); return true; } break; case PropertyAccessorType.UInt64: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetUInt64(target, (ulong)BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetUInt64(target, (ulong)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.UInt64) + if (typeCode == BinaryTypeCode.UInt64) { - context.ReadByte(); propInfo.SetUInt64(target, context.ReadVarULong()); return true; } break; case PropertyAccessorType.Enum: - if (peekCode == BinaryTypeCode.Enum) + if (typeCode == BinaryTypeCode.Enum) { - context.ReadByte(); var enumByte = context.ReadByte(); int enumValue; if (BinaryTypeCode.IsTinyInt(enumByte)) @@ -808,43 +787,37 @@ public static partial class AcBinaryDeserializer return true; } // Enum can also be encoded as TinyInt directly - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetEnumAsInt32(target, BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetEnumAsInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } break; case PropertyAccessorType.String: - if (BinaryTypeCode.IsFixStr(peekCode)) + if (BinaryTypeCode.IsFixStr(typeCode)) { - context.ReadByte(); - var length = BinaryTypeCode.DecodeFixStrLength(peekCode); + var length = BinaryTypeCode.DecodeFixStrLength(typeCode); propInfo.SetValue(target, length == 0 ? string.Empty : context.ReadStringUtf8(length)); return true; } - if (peekCode == BinaryTypeCode.String) + if (typeCode == BinaryTypeCode.String) { - context.ReadByte(); propInfo.SetValue(target, ReadPlainString(context)); return true; } - if (peekCode == BinaryTypeCode.StringEmpty) + if (typeCode == BinaryTypeCode.StringEmpty) { - context.ReadByte(); propInfo.SetValue(target, string.Empty); return true; } - if (peekCode == BinaryTypeCode.StringInterned) + if (typeCode == BinaryTypeCode.StringInterned) { - context.ReadByte(); propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt())); return true; } - if (peekCode == BinaryTypeCode.StringInternFirst) + if (typeCode == BinaryTypeCode.StringInternFirst) { - context.ReadByte(); propInfo.SetValue(target, ReadAndRegisterInternedString(context)); return true; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index f1f0a27..ca66ad8 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -133,6 +133,7 @@ public static partial class AcBinarySerializer /// public bool HasCaching => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None; public bool UseMetadata => Options.UseMetadata; + public bool UseGeneratedCode => Options.UseGeneratedCode; public byte MinStringInternLength => Options.MinStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength; public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 803a1b2..1429b62 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -1109,13 +1109,16 @@ public static partial class AcBinarySerializer // 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) + if (context.UseGeneratedCode) { - generatedWriter.WriteProperties(value, context, nextDepth); - return; + 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/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index f52489b..a41a6df 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -84,6 +84,8 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// public bool UseMetadata { get; set; } = false; + public bool UseGeneratedCode { get; set; } = true; + /// /// When true, checks for duplicate property name hashes during serialization (UseMetadata mode). /// Throws exception if FNV-1a hash collision is detected between property names of the same type.