diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs index fe8b9d7..d430d7b 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs @@ -1,7 +1,7 @@ using AyCode.Core.Extensions; using AyCode.Core.Tests.TestModels; -namespace AyCode.Core.Tests.Serialization; +namespace AyCode.Core.Tests.serialization; [TestClass] public class AcBinarySerializerTests @@ -13,7 +13,7 @@ public class AcBinarySerializerTests { var result = AcBinarySerializer.Serialize(null); Assert.AreEqual(1, result.Length); - Assert.AreEqual((byte)32, result[0]); // BinaryTypeCode.Null = 32 + Assert.AreEqual((byte)0, result[0]); // BinaryTypeCode.Null = 0 } [TestMethod] @@ -70,6 +70,20 @@ public class AcBinarySerializerTests Assert.AreEqual(value, result); } + [TestMethod] + [DataRow(DateTimeKind.Unspecified)] + [DataRow(DateTimeKind.Utc)] + [DataRow(DateTimeKind.Local)] + public void Serialize_DateTime_PreservesKind(DateTimeKind kind) + { + var value = new DateTime(2024, 12, 25, 10, 30, 45, kind); + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + + Assert.AreEqual(value.Ticks, result.Ticks); + Assert.AreEqual(value.Kind, result.Kind); + } + [TestMethod] public void Serialize_Guid_RoundTrip() { diff --git a/AyCode.Core/Extensions/AcBinaryDeserializer.cs b/AyCode.Core/Extensions/AcBinaryDeserializer.cs new file mode 100644 index 0000000..68eabb6 --- /dev/null +++ b/AyCode.Core/Extensions/AcBinaryDeserializer.cs @@ -0,0 +1,1388 @@ +using System.Buffers; +using System.Collections; +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using AyCode.Core.Helpers; +using static AyCode.Core.Extensions.JsonUtilities; + +namespace AyCode.Core.Extensions; + +/// +/// Exception thrown when binary deserialization fails. +/// +public class AcBinaryDeserializationException : Exception +{ + public int Position { get; } + public Type? TargetType { get; } + + public AcBinaryDeserializationException(string message, int position = 0, Type? targetType = null, Exception? innerException = null) + : base(message, innerException) + { + Position = position; + TargetType = targetType; + } +} + +/// +/// High-performance binary deserializer matching AcBinarySerializer. +/// Features: +/// - VarInt decoding for compact integers +/// - String intern table lookup +/// - Property name table for fast property resolution +/// - Reference resolution for circular/shared references +/// - Populate/Merge mode support +/// +public static class AcBinaryDeserializer +{ + private static readonly ConcurrentDictionary TypeMetadataCache = new(); + + #region Public API + + /// + /// Deserialize binary data to object of type T. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T? Deserialize(byte[] data) => Deserialize(data.AsSpan()); + + /// + /// Deserialize binary data to object of type T. + /// + public static T? Deserialize(ReadOnlySpan data) + { + if (data.Length == 0) return default; + if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default; + + var targetType = typeof(T); + var context = new BinaryDeserializationContext(data); + + try + { + context.ReadHeader(); + var result = ReadValue(ref context, targetType, 0); + return (T?)result; + } + catch (AcBinaryDeserializationException) + { + throw; + } + catch (Exception ex) + { + throw new AcBinaryDeserializationException( + $"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}", + context.Position, targetType, ex); + } + } + + /// + /// Deserialize binary data to specified type. + /// + public static object? Deserialize(ReadOnlySpan data, Type targetType) + { + if (data.Length == 0) return null; + if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return null; + + var context = new BinaryDeserializationContext(data); + + try + { + context.ReadHeader(); + return ReadValue(ref context, targetType, 0); + } + catch (AcBinaryDeserializationException) + { + throw; + } + catch (Exception ex) + { + throw new AcBinaryDeserializationException( + $"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}", + context.Position, targetType, ex); + } + } + + /// + /// Populate existing object from binary data. + /// + public static void Populate(byte[] data, T target) where T : class + => Populate(data.AsSpan(), target); + + /// + /// Populate existing object from binary data. + /// + public static void Populate(ReadOnlySpan data, T target) where T : class + { + ArgumentNullException.ThrowIfNull(target); + if (data.Length == 0) return; + if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return; + + var targetType = target.GetType(); + var context = new BinaryDeserializationContext(data); + + try + { + context.ReadHeader(); + var typeCode = context.PeekByte(); + + if (typeCode == BinaryTypeCode.Object) + { + context.ReadByte(); // consume Object marker + PopulateObject(ref context, target, targetType, 0); + } + else if (typeCode == BinaryTypeCode.Array && target is IList targetList) + { + context.ReadByte(); // consume Array marker + PopulateList(ref context, targetList, targetType, 0); + } + else + { + throw new AcBinaryDeserializationException( + $"Cannot populate type '{targetType.Name}' from binary type code {typeCode}", + context.Position, targetType); + } + } + catch (AcBinaryDeserializationException) + { + throw; + } + catch (Exception ex) + { + throw new AcBinaryDeserializationException( + $"Failed to populate object of type '{targetType.Name}': {ex.Message}", + context.Position, targetType, ex); + } + } + + /// + /// Populate with merge semantics for IId collections. + /// + public static void PopulateMerge(ReadOnlySpan data, T target) where T : class + { + ArgumentNullException.ThrowIfNull(target); + if (data.Length == 0) return; + if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return; + + var targetType = target.GetType(); + var context = new BinaryDeserializationContext(data) { IsMergeMode = true }; + + try + { + context.ReadHeader(); + var typeCode = context.PeekByte(); + + if (typeCode == BinaryTypeCode.Object) + { + context.ReadByte(); + PopulateObjectMerge(ref context, target, targetType, 0); + } + else if (typeCode == BinaryTypeCode.Array && target is IList targetList) + { + context.ReadByte(); + PopulateListMerge(ref context, targetList, targetType, 0); + } + else + { + throw new AcBinaryDeserializationException( + $"Cannot populate type '{targetType.Name}' from binary type code {typeCode}", + context.Position, targetType); + } + } + catch (AcBinaryDeserializationException) + { + throw; + } + catch (Exception ex) + { + throw new AcBinaryDeserializationException( + $"Failed to populate/merge object of type '{targetType.Name}': {ex.Message}", + context.Position, targetType, ex); + } + } + + #endregion + + #region Value Reading + + private static object? ReadValue(ref BinaryDeserializationContext context, Type targetType, int depth) + { + if (context.IsAtEnd) return null; + + var typeCode = context.ReadByte(); + + // Handle null + if (typeCode == BinaryTypeCode.Null) return null; + + // Handle tiny int + if (BinaryTypeCode.IsTinyInt(typeCode)) + { + var intValue = BinaryTypeCode.DecodeTinyInt(typeCode); + return ConvertToTargetType(intValue, targetType); + } + + // Handle type-specific codes + switch (typeCode) + { + case BinaryTypeCode.True: + return true; + case BinaryTypeCode.False: + return false; + case BinaryTypeCode.Int8: + return (sbyte)context.ReadByte(); + case BinaryTypeCode.UInt8: + return context.ReadByte(); + case BinaryTypeCode.Int16: + return context.ReadInt16(); + case BinaryTypeCode.UInt16: + return context.ReadUInt16(); + case BinaryTypeCode.Int32: + return ReadInt32Value(ref context, targetType); + case BinaryTypeCode.UInt32: + return context.ReadVarUInt(); + case BinaryTypeCode.Int64: + return context.ReadVarLong(); + case BinaryTypeCode.UInt64: + return context.ReadVarULong(); + case BinaryTypeCode.Float32: + return context.ReadSingle(); + case BinaryTypeCode.Float64: + return context.ReadDouble(); + case BinaryTypeCode.Decimal: + return context.ReadDecimal(); + case BinaryTypeCode.Char: + return context.ReadChar(); + case BinaryTypeCode.String: + return ReadAndInternString(ref context); + case BinaryTypeCode.StringInterned: + return context.GetInternedString((int)context.ReadVarUInt()); + case BinaryTypeCode.StringEmpty: + return string.Empty; + case BinaryTypeCode.DateTime: + return context.ReadDateTime(); + case BinaryTypeCode.DateTimeOffset: + return context.ReadDateTimeOffset(); + case BinaryTypeCode.TimeSpan: + return context.ReadTimeSpan(); + case BinaryTypeCode.Guid: + return context.ReadGuid(); + case BinaryTypeCode.Enum: + return ReadEnumValue(ref context, targetType); + case BinaryTypeCode.Object: + return ReadObject(ref context, targetType, depth); + case BinaryTypeCode.ObjectRef: + var refId = context.ReadVarInt(); + return context.GetReferencedObject(refId); + case BinaryTypeCode.Array: + return ReadArray(ref context, targetType, depth); + case BinaryTypeCode.Dictionary: + return ReadDictionary(ref context, targetType, depth); + case BinaryTypeCode.ByteArray: + return ReadByteArray(ref context); + default: + throw new AcBinaryDeserializationException( + $"Unknown type code: {typeCode}", + context.Position, targetType); + } + } + + /// + /// Read a string and register it in the intern table for future references. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string ReadAndInternString(ref BinaryDeserializationContext context) + { + var length = (int)context.ReadVarUInt(); + if (length == 0) return string.Empty; + var str = context.ReadString(length); + // Register for future StringInterned references + context.RegisterInternedString(str); + return str; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object ReadInt32Value(ref BinaryDeserializationContext context, Type targetType) + { + var value = context.ReadVarInt(); + return ConvertToTargetType(value, targetType); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object ConvertToTargetType(int value, Type targetType) + { + var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType; + if (underlying.IsEnum) + return Enum.ToObject(underlying, value); + + var typeCode = Type.GetTypeCode(underlying); + return typeCode switch + { + TypeCode.Int32 => value, + TypeCode.Int64 => (long)value, + TypeCode.Int16 => (short)value, + TypeCode.Byte => (byte)value, + TypeCode.SByte => (sbyte)value, + TypeCode.UInt16 => (ushort)value, + TypeCode.UInt32 => (uint)value, + TypeCode.UInt64 => (ulong)value, + TypeCode.Double => (double)value, + TypeCode.Single => (float)value, + TypeCode.Decimal => (decimal)value, + _ => value + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string ReadStringValue(ref BinaryDeserializationContext context) + { + var length = (int)context.ReadVarUInt(); + if (length == 0) return string.Empty; + return context.ReadString(length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object ReadEnumValue(ref BinaryDeserializationContext context, Type targetType) + { + var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType; + var nextByte = context.ReadByte(); + + int intValue; + if (BinaryTypeCode.IsTinyInt(nextByte)) + { + intValue = BinaryTypeCode.DecodeTinyInt(nextByte); + } + else if (nextByte == BinaryTypeCode.Int32) + { + intValue = context.ReadVarInt(); + } + else + { + throw new AcBinaryDeserializationException( + $"Invalid enum encoding: {nextByte}", + context.Position, targetType); + } + + if (underlying.IsEnum) + return Enum.ToObject(underlying, intValue); + return intValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte[] ReadByteArray(ref BinaryDeserializationContext context) + { + var length = (int)context.ReadVarUInt(); + if (length == 0) return []; + return context.ReadBytes(length); + } + + #endregion + + #region Object Reading + + private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth) + { + // Read reference ID if present + int refId = -1; + if (context.HasReferenceHandling) + { + refId = context.ReadVarInt(); + } + + // Handle dictionary types + if (IsDictionaryType(targetType, out var keyType, out var valueType)) + { + return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth); + } + + var metadata = GetTypeMetadata(targetType); + var instance = CreateInstance(targetType, metadata); + if (instance == null) return null; + + // Register reference + if (refId > 0) + { + context.RegisterObject(refId, instance); + } + + PopulateObject(ref context, instance, metadata, depth); + return instance; + } + + private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth) + { + var metadata = GetTypeMetadata(targetType); + + // Skip ref ID if present + if (context.HasReferenceHandling) + { + var refId = context.ReadVarInt(); + if (refId > 0) + { + context.RegisterObject(refId, target); + } + } + + PopulateObject(ref context, target, metadata, depth); + } + + private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth) + { + var propertyCount = (int)context.ReadVarUInt(); + var nextDepth = depth + 1; + + for (int i = 0; i < propertyCount; i++) + { + string propertyName; + if (context.HasMetadata) + { + var propIndex = (int)context.ReadVarUInt(); + propertyName = context.GetPropertyName(propIndex); + } + else + { + var typeCode = context.ReadByte(); + if (typeCode == BinaryTypeCode.String) + { + propertyName = ReadStringValue(ref context); + } + else if (typeCode == BinaryTypeCode.StringEmpty) + { + propertyName = string.Empty; + } + else + { + throw new AcBinaryDeserializationException( + $"Expected string for property name, got: {typeCode}", + context.Position, target.GetType()); + } + } + + if (!metadata.TryGetProperty(propertyName, out var propInfo) || propInfo == null) + { + // Skip unknown property + SkipValue(ref context); + continue; + } + + var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); + propInfo.SetValue(target, value); + } + } + + private static void PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth) + { + var metadata = GetTypeMetadata(targetType); + + // Skip ref ID if present + if (context.HasReferenceHandling) + { + var refId = context.ReadVarInt(); + if (refId > 0) + { + context.RegisterObject(refId, target); + } + } + + var propertyCount = (int)context.ReadVarUInt(); + var nextDepth = depth + 1; + + for (int i = 0; i < propertyCount; i++) + { + string propertyName; + if (context.HasMetadata) + { + var propIndex = (int)context.ReadVarUInt(); + propertyName = context.GetPropertyName(propIndex); + } + else + { + var typeCode = context.ReadByte(); + if (typeCode == BinaryTypeCode.String) + { + propertyName = ReadStringValue(ref context); + } + else if (typeCode == BinaryTypeCode.StringEmpty) + { + propertyName = string.Empty; + } + else + { + throw new AcBinaryDeserializationException( + $"Expected string for property name, got: {typeCode}", + context.Position, targetType); + } + } + + if (!metadata.TryGetProperty(propertyName, out var propInfo) || propInfo == null) + { + SkipValue(ref context); + continue; + } + + var peekCode = context.PeekByte(); + + // Handle IId collection merge + if (propInfo.IsIIdCollection && peekCode == BinaryTypeCode.Array) + { + var existingCollection = propInfo.GetValue(target); + if (existingCollection is IList existingList) + { + context.ReadByte(); // consume Array marker + MergeIIdCollection(ref context, existingList, propInfo, depth); + continue; + } + } + + // Handle nested object merge + if (peekCode == BinaryTypeCode.Object && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType)) + { + var existingObj = propInfo.GetValue(target); + if (existingObj != null) + { + context.ReadByte(); // consume Object marker + PopulateObjectMerge(ref context, existingObj, propInfo.PropertyType, nextDepth); + continue; + } + } + + var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); + propInfo.SetValue(target, value); + } + } + + private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth) + { + var elementType = propInfo.ElementType!; + var idGetter = propInfo.ElementIdGetter!; + var idType = propInfo.ElementIdType!; + + var count = existingList.Count; + var acObservable = existingList as IAcObservableCollection; + acObservable?.BeginUpdate(); + + try + { + Dictionary? existingById = null; + if (count > 0) + { + existingById = new Dictionary(count); + for (var idx = 0; idx < count; idx++) + { + var item = existingList[idx]; + if (item != null) + { + var id = idGetter(item); + if (id != null && !IsDefaultValue(id, idType)) + existingById[id] = item; + } + } + } + + var arrayCount = (int)context.ReadVarUInt(); + var nextDepth = depth + 1; + var elementMetadata = GetTypeMetadata(elementType); + + for (int i = 0; i < arrayCount; i++) + { + var itemCode = context.PeekByte(); + if (itemCode != BinaryTypeCode.Object) + { + var value = ReadValue(ref context, elementType, nextDepth); + if (value != null) + existingList.Add(value); + continue; + } + + context.ReadByte(); // consume Object marker + var newItem = CreateInstance(elementType, elementMetadata); + if (newItem == null) continue; + + // Read ref ID if present + if (context.HasReferenceHandling) + { + var refId = context.ReadVarInt(); + if (refId > 0) + context.RegisterObject(refId, newItem); + } + + PopulateObject(ref context, newItem, elementMetadata, nextDepth); + + var itemId = idGetter(newItem); + if (itemId != null && !IsDefaultValue(itemId, idType) && existingById != null) + { + if (existingById.TryGetValue(itemId, out var existingItem)) + { + // Copy properties to existing item + CopyProperties(newItem, existingItem, elementMetadata); + continue; + } + } + + existingList.Add(newItem); + } + } + finally + { + acObservable?.EndUpdate(); + } + } + + private static void CopyProperties(object source, object target, BinaryDeserializeTypeMetadata metadata) + { + foreach (var prop in metadata.PropertiesArray) + { + var value = prop.GetValue(source); + if (value != null) + prop.SetValue(target, value); + } + } + + #endregion + + #region Array Reading + + private static object? ReadArray(ref BinaryDeserializationContext context, Type targetType, int depth) + { + var elementType = GetCollectionElementType(targetType); + if (elementType == null) elementType = typeof(object); + + var count = (int)context.ReadVarUInt(); + var nextDepth = depth + 1; + + if (targetType.IsArray) + { + var array = Array.CreateInstance(elementType, count); + for (int i = 0; i < count; i++) + { + var value = ReadValue(ref context, elementType, nextDepth); + array.SetValue(value, i); + } + return array; + } + + IList? list = null; + try + { + var instance = Activator.CreateInstance(targetType); + if (instance is IList l) list = l; + } + catch { /* Fallback to List */ } + + list ??= GetOrCreateListFactory(elementType)(); + + var acObservable = list as IAcObservableCollection; + acObservable?.BeginUpdate(); + + try + { + for (int i = 0; i < count; i++) + { + var value = ReadValue(ref context, elementType, nextDepth); + list.Add(value); + } + } + finally + { + acObservable?.EndUpdate(); + } + + return list; + } + + private static void PopulateList(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth) + { + var elementType = GetCollectionElementType(listType) ?? typeof(object); + + var acObservable = targetList as IAcObservableCollection; + acObservable?.BeginUpdate(); + + try + { + targetList.Clear(); + + var count = (int)context.ReadVarUInt(); + var nextDepth = depth + 1; + + for (int i = 0; i < count; i++) + { + var value = ReadValue(ref context, elementType, nextDepth); + targetList.Add(value); + } + } + finally + { + acObservable?.EndUpdate(); + } + } + + private static void PopulateListMerge(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth) + { + var elementType = GetCollectionElementType(listType) ?? typeof(object); + var (isId, idType) = GetIdInfo(elementType); + + if (!isId || idType == null) + { + // No IId, just replace + PopulateList(ref context, targetList, listType, depth); + return; + } + + // IId merge logic + var idProp = elementType.GetProperty("Id"); + if (idProp == null) + { + PopulateList(ref context, targetList, listType, depth); + return; + } + + var idGetter = CreateCompiledGetter(elementType, idProp); + var propInfo = new BinaryPropertySetterInfo( + "Items", elementType, true, elementType, idType, idGetter); + + MergeIIdCollection(ref context, targetList, propInfo, depth); + } + + #endregion + + #region Dictionary Reading + + private static object? ReadDictionary(ref BinaryDeserializationContext context, Type targetType, int depth) + { + if (!IsDictionaryType(targetType, out var keyType, out var valueType)) + { + keyType = typeof(string); + valueType = typeof(object); + } + + return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth); + } + + private static object ReadDictionaryAsObject(ref BinaryDeserializationContext context, Type keyType, Type valueType, int depth) + { + var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType); + var dict = (IDictionary)Activator.CreateInstance(dictType)!; + + var count = (int)context.ReadVarUInt(); + var nextDepth = depth + 1; + + for (int i = 0; i < count; i++) + { + var key = ReadValue(ref context, keyType, nextDepth); + var value = ReadValue(ref context, valueType, nextDepth); + if (key != null) + dict.Add(key, value); + } + + return dict; + } + + #endregion + + #region Skip Value + + private static void SkipValue(ref BinaryDeserializationContext context) + { + var typeCode = context.ReadByte(); + + if (typeCode == BinaryTypeCode.Null) return; + + if (BinaryTypeCode.IsTinyInt(typeCode)) return; + + switch (typeCode) + { + case BinaryTypeCode.True: + case BinaryTypeCode.False: + case BinaryTypeCode.StringEmpty: + return; + case BinaryTypeCode.Int8: + case BinaryTypeCode.UInt8: + context.Skip(1); + return; + case BinaryTypeCode.Int16: + case BinaryTypeCode.UInt16: + case BinaryTypeCode.Char: + context.Skip(2); + return; + case BinaryTypeCode.Int32: + context.ReadVarInt(); // Skip VarInt + return; + case BinaryTypeCode.UInt32: + context.ReadVarUInt(); + return; + case BinaryTypeCode.Float32: + context.Skip(4); + return; + case BinaryTypeCode.Int64: + context.ReadVarLong(); + return; + case BinaryTypeCode.UInt64: + context.ReadVarULong(); + return; + case BinaryTypeCode.Float64: + case BinaryTypeCode.TimeSpan: + context.Skip(8); + return; + case BinaryTypeCode.DateTime: + context.Skip(9); + return; + case BinaryTypeCode.DateTimeOffset: + context.Skip(10); + return; + case BinaryTypeCode.Guid: + case BinaryTypeCode.Decimal: + context.Skip(16); + return; + case BinaryTypeCode.String: + var strLen = (int)context.ReadVarUInt(); + context.Skip(strLen); + return; + case BinaryTypeCode.StringInterned: + context.ReadVarUInt(); + return; + case BinaryTypeCode.ByteArray: + var byteLen = (int)context.ReadVarUInt(); + context.Skip(byteLen); + return; + case BinaryTypeCode.Enum: + var enumByte = context.ReadByte(); + if (BinaryTypeCode.IsTinyInt(enumByte)) return; + if (enumByte == BinaryTypeCode.Int32) context.ReadVarInt(); + return; + case BinaryTypeCode.Object: + SkipObject(ref context); + return; + case BinaryTypeCode.ObjectRef: + context.ReadVarInt(); + return; + case BinaryTypeCode.Array: + SkipArray(ref context); + return; + case BinaryTypeCode.Dictionary: + SkipDictionary(ref context); + return; + } + } + + private static void SkipObject(ref BinaryDeserializationContext context) + { + // Skip ref ID if present + if (context.HasReferenceHandling) + { + context.ReadVarInt(); + } + + var propCount = (int)context.ReadVarUInt(); + for (int i = 0; i < propCount; i++) + { + // Skip property name + if (context.HasMetadata) + { + context.ReadVarUInt(); + } + else + { + var nameCode = context.ReadByte(); + if (nameCode == BinaryTypeCode.String) + { + var len = (int)context.ReadVarUInt(); + context.Skip(len); + } + } + // Skip value + SkipValue(ref context); + } + } + + private static void SkipArray(ref BinaryDeserializationContext context) + { + var count = (int)context.ReadVarUInt(); + for (int i = 0; i < count; i++) + { + SkipValue(ref context); + } + } + + private static void SkipDictionary(ref BinaryDeserializationContext context) + { + var count = (int)context.ReadVarUInt(); + for (int i = 0; i < count; i++) + { + SkipValue(ref context); // key + SkipValue(ref context); // value + } + } + + #endregion + + #region Type Metadata + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type) + => TypeMetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata) + { + if (metadata.CompiledConstructor != null) + return metadata.CompiledConstructor(); + + try + { + return Activator.CreateInstance(type); + } + catch (MissingMethodException ex) + { + throw new AcBinaryDeserializationException( + $"Cannot create instance of type '{type.FullName}' because it does not have a parameterless constructor.", + 0, type, ex); + } + } + + private static Func CreateCompiledGetter(Type declaringType, PropertyInfo prop) + { + var objParam = Expression.Parameter(typeof(object), "obj"); + var castExpr = Expression.Convert(objParam, declaringType); + var propAccess = Expression.Property(castExpr, prop); + var boxed = Expression.Convert(propAccess, typeof(object)); + return Expression.Lambda>(boxed, objParam).Compile(); + } + + internal sealed class BinaryDeserializeTypeMetadata + { + private readonly Dictionary _propertiesDict; + public BinaryPropertySetterInfo[] PropertiesArray { get; } + public Func? CompiledConstructor { get; } + + public BinaryDeserializeTypeMetadata(Type type) + { + var ctor = type.GetConstructor(Type.EmptyTypes); + if (ctor != null) + { + var newExpr = Expression.New(type); + var boxed = Expression.Convert(newExpr, typeof(object)); + CompiledConstructor = Expression.Lambda>(boxed).Compile(); + } + + var allProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + var propsList = new List(); + + foreach (var p in allProps) + { + if (!p.CanWrite || !p.CanRead || p.GetIndexParameters().Length != 0) continue; + if (HasJsonIgnoreAttribute(p)) continue; + propsList.Add(p); + } + + _propertiesDict = new Dictionary(propsList.Count, StringComparer.OrdinalIgnoreCase); + PropertiesArray = new BinaryPropertySetterInfo[propsList.Count]; + + for (int i = 0; i < propsList.Count; i++) + { + var prop = propsList[i]; + var propInfo = new BinaryPropertySetterInfo(prop, type); + _propertiesDict[prop.Name] = propInfo; + PropertiesArray[i] = propInfo; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetProperty(string name, out BinaryPropertySetterInfo? propInfo) + => _propertiesDict.TryGetValue(name, out propInfo); + } + + internal sealed class BinaryPropertySetterInfo + { + public readonly string Name; + public readonly Type PropertyType; + public readonly Type UnderlyingType; + public readonly bool IsIIdCollection; + public readonly Type? ElementType; + public readonly Type? ElementIdType; + public readonly Func? ElementIdGetter; + + private readonly Action _setter; + private readonly Func _getter; + + public BinaryPropertySetterInfo(PropertyInfo prop, Type declaringType) + { + Name = prop.Name; + PropertyType = prop.PropertyType; + UnderlyingType = Nullable.GetUnderlyingType(PropertyType) ?? PropertyType; + + _setter = CreateCompiledSetter(declaringType, prop); + _getter = CreateCompiledGetter(declaringType, prop); + + ElementType = GetCollectionElementType(PropertyType); + var isCollection = ElementType != null && ElementType != typeof(object) && + typeof(IEnumerable).IsAssignableFrom(PropertyType) && + !ReferenceEquals(PropertyType, StringType); + + if (isCollection && ElementType != null) + { + var idInfo = GetIdInfo(ElementType); + if (idInfo.IsId) + { + IsIIdCollection = true; + ElementIdType = idInfo.IdType; + var idProp = ElementType.GetProperty("Id"); + if (idProp != null) + ElementIdGetter = CreateCompiledGetter(ElementType, idProp); + } + } + } + + // Constructor for manual creation (merge scenarios) + public BinaryPropertySetterInfo(string name, Type propertyType, bool isIIdCollection, Type? elementType, Type? elementIdType, Func? elementIdGetter) + { + Name = name; + PropertyType = propertyType; + UnderlyingType = Nullable.GetUnderlyingType(PropertyType) ?? PropertyType; + IsIIdCollection = isIIdCollection; + ElementType = elementType; + ElementIdType = elementIdType; + ElementIdGetter = elementIdGetter; + _setter = (_, _) => { }; + _getter = _ => null; + } + + private static Action CreateCompiledSetter(Type declaringType, PropertyInfo prop) + { + var objParam = Expression.Parameter(typeof(object), "obj"); + var valueParam = Expression.Parameter(typeof(object), "value"); + var castObj = Expression.Convert(objParam, declaringType); + var castValue = Expression.Convert(valueParam, prop.PropertyType); + var propAccess = Expression.Property(castObj, prop); + var assign = Expression.Assign(propAccess, castValue); + return Expression.Lambda>(assign, objParam, valueParam).Compile(); + } + + private static Func CreateCompiledGetter(Type declaringType, PropertyInfo prop) + { + var objParam = Expression.Parameter(typeof(object), "obj"); + var castExpr = Expression.Convert(objParam, declaringType); + var propAccess = Expression.Property(castExpr, prop); + var boxed = Expression.Convert(propAccess, typeof(object)); + return Expression.Lambda>(boxed, objParam).Compile(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetValue(object target, object? value) => _setter(target, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object? GetValue(object target) => _getter(target); + } + + #endregion + + #region Deserialization Context + + internal ref struct BinaryDeserializationContext + { + private readonly ReadOnlySpan _data; + private int _position; + + // Header info + public byte FormatVersion { get; private set; } + public bool HasMetadata { get; private set; } + public bool HasReferenceHandling { get; private set; } + + // Property name table + private string[]? _propertyNames; + + // Interned strings - dynamically built during deserialization + private List? _internedStrings; + + // Reference map + private Dictionary? _references; + + public bool IsMergeMode { get; set; } + public int Position => _position; + public bool IsAtEnd => _position >= _data.Length; + + public BinaryDeserializationContext(ReadOnlySpan data) + { + _data = data; + _position = 0; + FormatVersion = 0; + HasMetadata = false; + HasReferenceHandling = true; // Assume true by default + _propertyNames = null; + _internedStrings = null; + _references = null; + IsMergeMode = false; + } + + public void ReadHeader() + { + if (_data.Length < 2) return; + + FormatVersion = ReadByte(); + var flags = ReadByte(); + + HasMetadata = flags == BinaryTypeCode.MetadataHeader; + + if (HasMetadata) + { + // Read property names + var propCount = (int)ReadVarUInt(); + if (propCount > 0) + { + _propertyNames = new string[propCount]; + for (int i = 0; i < propCount; i++) + { + var len = (int)ReadVarUInt(); + _propertyNames[i] = ReadString(len); + } + } + // Note: Interned strings are built dynamically during deserialization + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte ReadByte() + { + if (_position >= _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + return _data[_position++]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte PeekByte() + { + if (_position >= _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + return _data[_position]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Skip(int count) + { + _position += count; + if (_position > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte[] ReadBytes(int count) + { + if (_position + count > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + var result = _data.Slice(_position, count).ToArray(); + _position += count; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadVarInt() + { + var encoded = ReadVarUInt(); + // ZigZag decode + return (int)((encoded >> 1) ^ -(encoded & 1)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint ReadVarUInt() + { + uint result = 0; + int shift = 0; + + while (true) + { + var b = ReadByte(); + result |= (uint)(b & 0x7F) << shift; + if ((b & 0x80) == 0) break; + shift += 7; + if (shift > 28) + throw new AcBinaryDeserializationException("Invalid VarInt", _position); + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long ReadVarLong() + { + var encoded = ReadVarULong(); + // ZigZag decode + return (long)((encoded >> 1) ^ (0 - (encoded & 1))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong ReadVarULong() + { + ulong result = 0; + int shift = 0; + + while (true) + { + var b = ReadByte(); + result |= (ulong)(b & 0x7F) << shift; + if ((b & 0x80) == 0) break; + shift += 7; + if (shift > 63) + throw new AcBinaryDeserializationException("Invalid VarLong", _position); + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public short ReadInt16() + { + if (_position + 2 > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + var result = BitConverter.ToInt16(_data.Slice(_position, 2)); + _position += 2; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort ReadUInt16() + { + if (_position + 2 > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + var result = BitConverter.ToUInt16(_data.Slice(_position, 2)); + _position += 2; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float ReadSingle() + { + if (_position + 4 > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + var result = BitConverter.ToSingle(_data.Slice(_position, 4)); + _position += 4; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double ReadDouble() + { + if (_position + 8 > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + var result = BitConverter.ToDouble(_data.Slice(_position, 8)); + _position += 8; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public decimal ReadDecimal() + { + if (_position + 16 > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + var bits = new int[4]; + for (int i = 0; i < 4; i++) + { + bits[i] = BitConverter.ToInt32(_data.Slice(_position + i * 4, 4)); + } + _position += 16; + return new decimal(bits); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public char ReadChar() + { + if (_position + 2 > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + var result = BitConverter.ToChar(_data.Slice(_position, 2)); + _position += 2; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DateTime ReadDateTime() + { + if (_position + 9 > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + var ticks = BitConverter.ToInt64(_data.Slice(_position, 8)); + var kind = (DateTimeKind)_data[_position + 8]; + _position += 9; + return new DateTime(ticks, kind); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DateTimeOffset ReadDateTimeOffset() + { + if (_position + 10 > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + var utcTicks = BitConverter.ToInt64(_data.Slice(_position, 8)); + var offsetMinutes = BitConverter.ToInt16(_data.Slice(_position + 8, 2)); + _position += 10; + var offset = TimeSpan.FromMinutes(offsetMinutes); + // We stored UtcTicks, so we need to add offset to get local ticks + var localTicks = utcTicks + offset.Ticks; + return new DateTimeOffset(localTicks, offset); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TimeSpan ReadTimeSpan() + { + if (_position + 8 > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + var ticks = BitConverter.ToInt64(_data.Slice(_position, 8)); + _position += 8; + return new TimeSpan(ticks); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Guid ReadGuid() + { + if (_position + 16 > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + var result = new Guid(_data.Slice(_position, 16)); + _position += 16; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadString(int byteCount) + { + if (_position + byteCount > _data.Length) + throw new AcBinaryDeserializationException("Unexpected end of data", _position); + var result = Encoding.UTF8.GetString(_data.Slice(_position, byteCount)); + _position += byteCount; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string GetPropertyName(int index) + { + if (_propertyNames == null || index < 0 || index >= _propertyNames.Length) + throw new AcBinaryDeserializationException($"Invalid property name index: {index}", _position); + return _propertyNames[index]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegisterInternedString(string value) + { + _internedStrings ??= new List(16); + _internedStrings.Add(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string GetInternedString(int index) + { + if (_internedStrings == null || index < 0 || index >= _internedStrings.Count) + throw new AcBinaryDeserializationException($"Invalid interned string index: {index}. Interned strings count: {_internedStrings?.Count ?? 0}", _position); + return _internedStrings[index]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegisterObject(int refId, object obj) + { + _references ??= new Dictionary(); + _references[refId] = obj; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object? GetReferencedObject(int refId) + { + if (_references != null && _references.TryGetValue(refId, out var obj)) + return obj; + return null; + } + } + + #endregion +} diff --git a/AyCode.Core/Extensions/AcBinarySerializer.cs b/AyCode.Core/Extensions/AcBinarySerializer.cs index 16778ec..d090690 100644 --- a/AyCode.Core/Extensions/AcBinarySerializer.cs +++ b/AyCode.Core/Extensions/AcBinarySerializer.cs @@ -1,6 +1,1094 @@ -namespace AyCode.Core.Extensions; +using System.Buffers; +using System.Collections; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using static AyCode.Core.Extensions.JsonUtilities; -public class AcBinarySerializer +namespace AyCode.Core.Extensions; + +/// +/// High-performance binary serializer optimized for speed and memory efficiency. +/// Features: +/// - VarInt encoding for compact integers (MessagePack-style) +/// - String interning for repeated strings +/// - Property name table for fast lookup +/// - Reference handling for circular/shared references +/// - Optional metadata for schema evolution +/// +public static class AcBinarySerializer { - + private static readonly ConcurrentDictionary TypeMetadataCache = new(); + + #region Public API + + /// + /// Serialize object to binary with default options. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Serialize(T value) => Serialize(value, AcBinarySerializerOptions.Default); + + /// + /// Serialize object to binary with specified options. + /// + public static byte[] Serialize(T value, AcBinarySerializerOptions options) + { + if (value == null) + { + return [BinaryTypeCode.Null]; + } + + var type = value.GetType(); + + // Use context-based serialization for all types to ensure consistent format + var context = BinarySerializationContextPool.Get(options); + try + { + // Reserve space for header + context.WriteHeaderPlaceholder(); + + // Phase 1: If reference handling enabled, scan for multi-referenced objects + if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type)) + { + ScanReferences(value, context, 0); + } + + // Phase 2: If metadata enabled, collect property names + if (options.UseMetadata && !IsPrimitiveOrStringFast(type)) + { + CollectPropertyNames(value, context, 0); + } + + // Write metadata section + context.WriteMetadata(); + + // Phase 3: Write the actual data + WriteValue(value, type, context, 0); + + // Finalize and return + return context.ToArray(); + } + finally + { + BinarySerializationContextPool.Return(context); + } + } + + /// + /// Serialize to existing buffer writer (for streaming scenarios). + /// + public static void Serialize(T value, IBufferWriter writer, AcBinarySerializerOptions options) + { + var bytes = Serialize(value, options); + writer.Write(bytes); + } + + #endregion + + #region Reference Scanning + + private static void ScanReferences(object? value, BinarySerializationContext context, int depth) + { + if (value == null || depth > context.MaxDepth) return; + + var type = value.GetType(); + if (IsPrimitiveOrStringFast(type)) return; + if (!context.TrackForScanning(value)) return; + + if (value is byte[]) return; // byte arrays are value types + + if (value is IDictionary dictionary) + { + foreach (DictionaryEntry entry in dictionary) + { + if (entry.Value != null) + ScanReferences(entry.Value, context, depth + 1); + } + return; + } + + if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) + { + foreach (var item in enumerable) + { + if (item != null) + ScanReferences(item, context, depth + 1); + } + return; + } + + var metadata = GetTypeMetadata(type); + foreach (var prop in metadata.Properties) + { + var propValue = prop.GetValue(value); + if (propValue != null) + ScanReferences(propValue, context, depth + 1); + } + } + + #endregion + + #region Property Name Collection + + private static void CollectPropertyNames(object? value, BinarySerializationContext context, int depth) + { + if (value == null || depth > context.MaxDepth) return; + + var type = value.GetType(); + if (IsPrimitiveOrStringFast(type)) return; + + if (value is byte[]) return; + + if (value is IDictionary dictionary) + { + foreach (DictionaryEntry entry in dictionary) + { + if (entry.Value != null) + CollectPropertyNames(entry.Value, context, depth + 1); + } + return; + } + + if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) + { + foreach (var item in enumerable) + { + if (item != null) + CollectPropertyNames(item, context, depth + 1); + } + return; + } + + var metadata = GetTypeMetadata(type); + foreach (var prop in metadata.Properties) + { + context.RegisterPropertyName(prop.Name); + var propValue = prop.GetValue(value); + if (propValue != null) + CollectPropertyNames(propValue, context, depth + 1); + } + } + + #endregion + + #region Value Writing + + private static void WriteValue(object? value, Type type, BinarySerializationContext context, int depth) + { + if (value == null) + { + context.WriteByte(BinaryTypeCode.Null); + return; + } + + // Try writing as primitive first + if (TryWritePrimitive(value, type, context)) + return; + + if (depth > context.MaxDepth) + { + context.WriteByte(BinaryTypeCode.Null); + return; + } + + // Check for object reference + if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId)) + { + context.WriteByte(BinaryTypeCode.ObjectRef); + context.WriteVarInt(refId); + return; + } + + // Handle byte arrays specially + if (value is byte[] byteArray) + { + WriteByteArray(byteArray, context); + return; + } + + // Handle dictionaries + if (value is IDictionary dictionary) + { + WriteDictionary(dictionary, context, depth); + return; + } + + // Handle collections/arrays + if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) + { + WriteArray(enumerable, context, depth); + return; + } + + // Handle complex objects + WriteObject(value, type, context, depth); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context) + { + var underlyingType = Nullable.GetUnderlyingType(type) ?? type; + var typeCode = Type.GetTypeCode(underlyingType); + + switch (typeCode) + { + case TypeCode.Int32: + WriteInt32((int)value, context); + return true; + case TypeCode.Int64: + WriteInt64((long)value, context); + return true; + case TypeCode.Boolean: + context.WriteByte((bool)value ? BinaryTypeCode.True : BinaryTypeCode.False); + return true; + case TypeCode.Double: + WriteFloat64((double)value, context); + return true; + case TypeCode.String: + WriteString((string)value, context); + return true; + case TypeCode.Single: + WriteFloat32((float)value, context); + return true; + case TypeCode.Decimal: + WriteDecimal((decimal)value, context); + return true; + case TypeCode.DateTime: + WriteDateTime((DateTime)value, context); + return true; + case TypeCode.Byte: + context.WriteByte(BinaryTypeCode.UInt8); + context.WriteByte((byte)value); + return true; + case TypeCode.Int16: + WriteInt16((short)value, context); + return true; + case TypeCode.UInt16: + WriteUInt16((ushort)value, context); + return true; + case TypeCode.UInt32: + WriteUInt32((uint)value, context); + return true; + case TypeCode.UInt64: + WriteUInt64((ulong)value, context); + return true; + case TypeCode.SByte: + context.WriteByte(BinaryTypeCode.Int8); + context.WriteByte(unchecked((byte)(sbyte)value)); + return true; + case TypeCode.Char: + WriteChar((char)value, context); + return true; + } + + if (ReferenceEquals(underlyingType, GuidType)) + { + WriteGuid((Guid)value, context); + return true; + } + if (ReferenceEquals(underlyingType, DateTimeOffsetType)) + { + WriteDateTimeOffset((DateTimeOffset)value, context); + return true; + } + if (ReferenceEquals(underlyingType, TimeSpanType)) + { + WriteTimeSpan((TimeSpan)value, context); + return true; + } + if (underlyingType.IsEnum) + { + WriteEnum(value, context); + return true; + } + + return false; + } + + #endregion + + #region Primitive Writers + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteInt32(int value, BinarySerializationContext context) + { + if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny)) + { + context.WriteByte(tiny); + return; + } + context.WriteByte(BinaryTypeCode.Int32); + context.WriteVarInt(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteInt64(long value, BinarySerializationContext context) + { + if (value >= int.MinValue && value <= int.MaxValue) + { + WriteInt32((int)value, context); + return; + } + context.WriteByte(BinaryTypeCode.Int64); + context.WriteVarLong(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteFloat64(double value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.Float64); + Span buffer = stackalloc byte[8]; + BitConverter.TryWriteBytes(buffer, value); + context.WriteBytes(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteFloat32(float value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.Float32); + Span buffer = stackalloc byte[4]; + BitConverter.TryWriteBytes(buffer, value); + context.WriteBytes(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteDecimal(decimal value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.Decimal); + var bits = decimal.GetBits(value); + Span buffer = stackalloc byte[16]; + for (int i = 0; i < 4; i++) + { + BitConverter.TryWriteBytes(buffer[(i * 4)..], bits[i]); + } + context.WriteBytes(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteDateTime(DateTime value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.DateTime); + Span buffer = stackalloc byte[9]; + BitConverter.TryWriteBytes(buffer, value.Ticks); + buffer[8] = (byte)value.Kind; + context.WriteBytes(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteGuid(Guid value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.Guid); + Span buffer = stackalloc byte[16]; + value.TryWriteBytes(buffer); + context.WriteBytes(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteDateTimeOffset(DateTimeOffset value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.DateTimeOffset); + Span buffer = stackalloc byte[10]; + BitConverter.TryWriteBytes(buffer, value.UtcTicks); + BitConverter.TryWriteBytes(buffer[8..], (short)value.Offset.TotalMinutes); + context.WriteBytes(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteTimeSpan(TimeSpan value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.TimeSpan); + Span buffer = stackalloc byte[8]; + BitConverter.TryWriteBytes(buffer, value.Ticks); + context.WriteBytes(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteInt16(short value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.Int16); + Span buffer = stackalloc byte[2]; + BitConverter.TryWriteBytes(buffer, value); + context.WriteBytes(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteUInt16(ushort value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.UInt16); + Span buffer = stackalloc byte[2]; + BitConverter.TryWriteBytes(buffer, value); + context.WriteBytes(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteUInt32(uint value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.UInt32); + context.WriteVarUInt(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteUInt64(ulong value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.UInt64); + context.WriteVarULong(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteChar(char value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.Char); + Span buffer = stackalloc byte[2]; + BitConverter.TryWriteBytes(buffer, value); + context.WriteBytes(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteEnum(object value, BinarySerializationContext context) + { + var intValue = Convert.ToInt32(value); + if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny)) + { + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(tiny); + return; + } + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(BinaryTypeCode.Int32); + context.WriteVarInt(intValue); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteString(string value, BinarySerializationContext context) + { + if (value.Length == 0) + { + context.WriteByte(BinaryTypeCode.StringEmpty); + return; + } + + // Try string interning - but only write ref if already interned + if (context.UseStringInterning && value.Length >= context.MinStringInternLength) + { + if (context.TryGetInternedStringIndex(value, out var index)) + { + context.WriteByte(BinaryTypeCode.StringInterned); + context.WriteVarUInt((uint)index); + return; + } + // Register for future references + context.RegisterInternedString(value); + } + + // Write inline string + context.WriteByte(BinaryTypeCode.String); + var utf8Length = Encoding.UTF8.GetByteCount(value); + context.WriteVarUInt((uint)utf8Length); + context.WriteString(value, utf8Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteByteArray(byte[] value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.ByteArray); + context.WriteVarUInt((uint)value.Length); + context.WriteBytes(value); + } + + #endregion + + #region Complex Type Writers + + private static void WriteObject(object value, Type type, BinarySerializationContext context, int depth) + { + context.WriteByte(BinaryTypeCode.Object); + + // Register object reference if needed + if (context.UseReferenceHandling && context.ShouldWriteRef(value, out var refId)) + { + context.WriteVarInt(refId); + context.MarkAsWritten(value, refId); + } + else if (context.UseReferenceHandling) + { + context.WriteVarInt(-1); // No ref ID + } + + var metadata = GetTypeMetadata(type); + var nextDepth = depth + 1; + var writtenCount = 0; + + // Count non-null, non-default properties first + foreach (var prop in metadata.Properties) + { + var propValue = prop.GetValue(value); + if (propValue == null) continue; + if (IsDefaultValueFast(propValue, prop.TypeCode, prop.PropertyType)) continue; + writtenCount++; + } + + context.WriteVarUInt((uint)writtenCount); + + foreach (var prop in metadata.Properties) + { + var propValue = prop.GetValue(value); + if (propValue == null) continue; + if (IsDefaultValueFast(propValue, prop.TypeCode, prop.PropertyType)) continue; + + // Write property index or name + if (context.UseMetadata) + { + var propIndex = context.GetPropertyNameIndex(prop.Name); + context.WriteVarUInt((uint)propIndex); + } + else + { + WriteString(prop.Name, context); + } + + WriteValue(propValue, prop.PropertyType, context, nextDepth); + } + } + + private static void WriteArray(IEnumerable enumerable, BinarySerializationContext context, int depth) + { + context.WriteByte(BinaryTypeCode.Array); + var nextDepth = depth + 1; + + // For IList, we can write the count directly + if (enumerable is IList list) + { + context.WriteVarUInt((uint)list.Count); + foreach (var item in list) + { + var itemType = item?.GetType() ?? typeof(object); + WriteValue(item, itemType, context, nextDepth); + } + return; + } + + // For other IEnumerable, collect first + var items = new List(); + foreach (var item in enumerable) + { + items.Add(item); + } + + context.WriteVarUInt((uint)items.Count); + foreach (var item in items) + { + var itemType = item?.GetType() ?? typeof(object); + WriteValue(item, itemType, context, nextDepth); + } + } + + private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth) + { + context.WriteByte(BinaryTypeCode.Dictionary); + context.WriteVarUInt((uint)dictionary.Count); + var nextDepth = depth + 1; + + foreach (DictionaryEntry entry in dictionary) + { + // Write key + var keyType = entry.Key?.GetType() ?? typeof(object); + WriteValue(entry.Key, keyType, context, nextDepth); + + // Write value + var valueType = entry.Value?.GetType() ?? typeof(object); + WriteValue(entry.Value, valueType, context, nextDepth); + } + } + + #endregion + + #region VarInt Encoding (Static Methods for Direct Use) + + /// + /// Write variable-length signed integer (ZigZag encoding). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteVarInt(Span buffer, int value) + { + // ZigZag encoding + var encoded = (uint)((value << 1) ^ (value >> 31)); + return WriteVarUInt(buffer, encoded); + } + + /// + /// Write variable-length unsigned integer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteVarUInt(Span buffer, uint value) + { + var i = 0; + while (value >= 0x80) + { + buffer[i++] = (byte)(value | 0x80); + value >>= 7; + } + buffer[i++] = (byte)value; + return i; + } + + /// + /// Write variable-length signed long (ZigZag encoding). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteVarLong(Span buffer, long value) + { + var encoded = (ulong)((value << 1) ^ (value >> 63)); + return WriteVarULong(buffer, encoded); + } + + /// + /// Write variable-length unsigned long. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteVarULong(Span buffer, ulong value) + { + var i = 0; + while (value >= 0x80) + { + buffer[i++] = (byte)(value | 0x80); + value >>= 7; + } + buffer[i++] = (byte)value; + return i; + } + + #endregion + + #region Type Metadata + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BinaryTypeMetadata GetTypeMetadata(Type type) + => TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsDefaultValueFast(object value, TypeCode typeCode, Type propertyType) + { + switch (typeCode) + { + case TypeCode.Int32: return (int)value == 0; + case TypeCode.Int64: return (long)value == 0L; + case TypeCode.Double: return (double)value == 0.0; + case TypeCode.Decimal: return (decimal)value == 0m; + case TypeCode.Single: return (float)value == 0f; + case TypeCode.Byte: return (byte)value == 0; + case TypeCode.Int16: return (short)value == 0; + case TypeCode.UInt16: return (ushort)value == 0; + case TypeCode.UInt32: return (uint)value == 0; + case TypeCode.UInt64: return (ulong)value == 0; + case TypeCode.SByte: return (sbyte)value == 0; + case TypeCode.Boolean: return (bool)value == false; + case TypeCode.String: return string.IsNullOrEmpty((string)value); + } + + if (propertyType.IsEnum) return Convert.ToInt32(value) == 0; + if (ReferenceEquals(propertyType, GuidType)) return (Guid)value == Guid.Empty; + + return false; + } + + internal sealed class BinaryTypeMetadata + { + public BinaryPropertyAccessor[] Properties { get; } + + public BinaryTypeMetadata(Type type) + { + Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && + p.GetIndexParameters().Length == 0 && + !HasJsonIgnoreAttribute(p)) + .Select(p => new BinaryPropertyAccessor(p)) + .ToArray(); + } + } + + internal sealed class BinaryPropertyAccessor + { + public readonly string Name; + public readonly byte[] NameUtf8; + public readonly Type PropertyType; + public readonly TypeCode TypeCode; + private readonly Func _getter; + + public BinaryPropertyAccessor(PropertyInfo prop) + { + Name = prop.Name; + NameUtf8 = Encoding.UTF8.GetBytes(prop.Name); + PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; + TypeCode = Type.GetTypeCode(PropertyType); + _getter = CreateCompiledGetter(prop.DeclaringType!, prop); + } + + private static Func CreateCompiledGetter(Type declaringType, PropertyInfo prop) + { + var objParam = Expression.Parameter(typeof(object), "obj"); + var castExpr = Expression.Convert(objParam, declaringType); + var propAccess = Expression.Property(castExpr, prop); + var boxed = Expression.Convert(propAccess, typeof(object)); + return Expression.Lambda>(boxed, objParam).Compile(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object? GetValue(object obj) => _getter(obj); + } + + #endregion + + #region Context Pool + + private static class BinarySerializationContextPool + { + private static readonly ConcurrentQueue Pool = new(); + private const int MaxPoolSize = 16; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BinarySerializationContext Get(AcBinarySerializerOptions options) + { + if (Pool.TryDequeue(out var context)) + { + context.Reset(options); + return context; + } + return new BinarySerializationContext(options); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Return(BinarySerializationContext context) + { + if (Pool.Count < MaxPoolSize) + { + context.Clear(); + Pool.Enqueue(context); + } + else + { + context.Dispose(); + } + } + } + + #endregion + + #region Serialization Context + + internal sealed class BinarySerializationContext : IDisposable + { + private byte[] _buffer; + private int _position; + + // Reference handling + private Dictionary? _scanOccurrences; + private Dictionary? _writtenRefs; + private HashSet? _multiReferenced; + private int _nextRefId; + + // String interning + private Dictionary? _internedStrings; + private List? _internedStringList; + + // Property name table + private Dictionary? _propertyNames; + private List? _propertyNameList; + + public bool UseReferenceHandling { get; private set; } + public bool UseStringInterning { get; private set; } + public bool UseMetadata { get; private set; } + public byte MaxDepth { get; private set; } + public byte MinStringInternLength { get; private set; } + + public BinarySerializationContext(AcBinarySerializerOptions options) + { + _buffer = ArrayPool.Shared.Rent(options.InitialBufferCapacity); + Reset(options); + } + + public void Reset(AcBinarySerializerOptions options) + { + _position = 0; + _nextRefId = 1; + UseReferenceHandling = options.UseReferenceHandling; + UseStringInterning = options.UseStringInterning; + UseMetadata = options.UseMetadata; + MaxDepth = options.MaxDepth; + MinStringInternLength = options.MinStringInternLength; + } + + public void Clear() + { + _position = 0; + _nextRefId = 1; + _scanOccurrences?.Clear(); + _writtenRefs?.Clear(); + _multiReferenced?.Clear(); + _internedStrings?.Clear(); + _internedStringList?.Clear(); + _propertyNames?.Clear(); + _propertyNameList?.Clear(); + } + + public void Dispose() + { + if (_buffer != null) + { + ArrayPool.Shared.Return(_buffer); + _buffer = null!; + } + } + + #region Buffer Writing + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int additionalBytes) + { + var required = _position + additionalBytes; + if (required <= _buffer.Length) return; + + var newSize = Math.Max(_buffer.Length * 2, required); + var newBuffer = ArrayPool.Shared.Rent(newSize); + _buffer.AsSpan(0, _position).CopyTo(newBuffer); + ArrayPool.Shared.Return(_buffer); + _buffer = newBuffer; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteByte(byte value) + { + EnsureCapacity(1); + _buffer[_position++] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBytes(ReadOnlySpan data) + { + EnsureCapacity(data.Length); + data.CopyTo(_buffer.AsSpan(_position)); + _position += data.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteVarInt(int value) + { + EnsureCapacity(5); + var encoded = (uint)((value << 1) ^ (value >> 31)); + WriteVarUIntInternal(encoded); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteVarUInt(uint value) + { + EnsureCapacity(5); + WriteVarUIntInternal(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteVarUIntInternal(uint value) + { + while (value >= 0x80) + { + _buffer[_position++] = (byte)(value | 0x80); + value >>= 7; + } + _buffer[_position++] = (byte)value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteVarLong(long value) + { + EnsureCapacity(10); + var encoded = (ulong)((value << 1) ^ (value >> 63)); + WriteVarULongInternal(encoded); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteVarULong(ulong value) + { + EnsureCapacity(10); + WriteVarULongInternal(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteVarULongInternal(ulong value) + { + while (value >= 0x80) + { + _buffer[_position++] = (byte)(value | 0x80); + value >>= 7; + } + _buffer[_position++] = (byte)value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteString(string value, int utf8Length) + { + EnsureCapacity(utf8Length); + Encoding.UTF8.GetBytes(value, _buffer.AsSpan(_position, utf8Length)); + _position += utf8Length; + } + + #endregion + + #region Header and Metadata + + private int _headerPosition; + + public void WriteHeaderPlaceholder() + { + // Reserve space for: version (1) + flags (1) + EnsureCapacity(2); + _headerPosition = _position; + _position += 2; + } + + public void WriteMetadata() + { + // Write version at header position + _buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion; + + var hasPropertyNames = _propertyNameList != null && _propertyNameList.Count > 0; + + if (!UseMetadata || !hasPropertyNames) + { + _buffer[_headerPosition + 1] = BinaryTypeCode.NoMetadataHeader; + return; + } + + _buffer[_headerPosition + 1] = BinaryTypeCode.MetadataHeader; + + // Write property name count + WriteVarUInt((uint)_propertyNameList!.Count); + + // Write property names + foreach (var name in _propertyNameList) + { + var utf8Length = Encoding.UTF8.GetByteCount(name); + WriteVarUInt((uint)utf8Length); + WriteString(name, utf8Length); + } + + // NOTE: Interned strings are handled "on-the-fly" during serialization + // First occurrence is written inline, subsequent occurrences use StringInterned reference + // We don't need to write them in metadata since deserializer builds table dynamically + } + + #endregion + + #region Reference Handling + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrackForScanning(object obj) + { + _scanOccurrences ??= new Dictionary(64, ReferenceEqualityComparer.Instance); + _multiReferenced ??= new HashSet(32, ReferenceEqualityComparer.Instance); + + ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists); + if (exists) + { + count++; + _multiReferenced.Add(obj); + return false; + } + count = 1; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldWriteRef(object obj, out int refId) + { + if (_multiReferenced != null && _multiReferenced.Contains(obj)) + { + _writtenRefs ??= new Dictionary(32, ReferenceEqualityComparer.Instance); + if (!_writtenRefs.ContainsKey(obj)) + { + refId = _nextRefId++; + return true; + } + } + refId = 0; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MarkAsWritten(object obj, int refId) + { + _writtenRefs![obj] = refId; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetExistingRef(object obj, out int refId) + { + if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId)) + return true; + refId = 0; + return false; + } + + #endregion + + #region String Interning + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetInternedStringIndex(string value, out int index) + { + if (_internedStrings != null && _internedStrings.TryGetValue(value, out index)) + return true; + index = -1; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegisterInternedString(string value) + { + _internedStrings ??= new Dictionary(32, StringComparer.Ordinal); + _internedStringList ??= new List(32); + + if (!_internedStrings.ContainsKey(value)) + { + var index = _internedStringList.Count; + _internedStrings[value] = index; + _internedStringList.Add(value); + } + } + + #endregion + + #region Property Names + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegisterPropertyName(string name) + { + _propertyNames ??= new Dictionary(64, StringComparer.Ordinal); + _propertyNameList ??= new List(64); + + if (!_propertyNames.ContainsKey(name)) + { + _propertyNames[name] = _propertyNameList.Count; + _propertyNameList.Add(name); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetPropertyNameIndex(string name) + { + return _propertyNames!.TryGetValue(name, out var index) ? index : -1; + } + + #endregion + + public byte[] ToArray() + { + var result = new byte[_position]; + _buffer.AsSpan(0, _position).CopyTo(result); + return result; + } + } + + #endregion } \ No newline at end of file diff --git a/AyCode.Core/Extensions/AcBinarySerializerOptions.cs b/AyCode.Core/Extensions/AcBinarySerializerOptions.cs new file mode 100644 index 0000000..79f153c --- /dev/null +++ b/AyCode.Core/Extensions/AcBinarySerializerOptions.cs @@ -0,0 +1,179 @@ +using System.Runtime.CompilerServices; + +namespace AyCode.Core.Extensions; + +/// +/// Options for AcBinarySerializer and AcBinaryDeserializer. +/// Optimized for speed and memory efficiency over raw size. +/// +public sealed class AcBinarySerializerOptions : AcSerializerOptions +{ + public override AcSerializerType SerializerType { get; init; } = AcSerializerType.Binary; + + /// + /// Current binary format version. Incremented when breaking changes are made. + /// + public const byte FormatVersion = 1; + + /// + /// Default options instance with metadata and string interning enabled. + /// + public static readonly AcBinarySerializerOptions Default = new(); + + /// + /// Options optimized for maximum speed (no metadata, no interning). + /// Use when deserializer knows the exact type structure. + /// + public static readonly AcBinarySerializerOptions FastMode = new() + { + UseMetadata = false, + UseStringInterning = false, + UseReferenceHandling = false + }; + + /// + /// Options for shallow serialization (root level only). + /// + public static readonly AcBinarySerializerOptions ShallowCopy = new() + { + MaxDepth = 0, + UseReferenceHandling = false + }; + + /// + /// Whether to include metadata header with property names. + /// When enabled, property names are stored once and referenced by index. + /// Improves deserialization speed and allows schema evolution. + /// Default: true + /// + public bool UseMetadata { get; init; } = true; + + /// + /// Whether to intern repeated strings. + /// When enabled, duplicate strings are stored once and referenced by index. + /// Reduces size and memory for objects with many repeated string values. + /// Default: true + /// + public bool UseStringInterning { get; init; } = true; + + /// + /// Minimum string length to consider for interning. + /// Shorter strings are written inline to avoid overhead. + /// Default: 4 (strings shorter than 4 chars are not interned) + /// + public byte MinStringInternLength { get; init; } = 4; + + /// + /// Initial capacity for serialization buffer. + /// Default: 4096 bytes + /// + public int InitialBufferCapacity { get; init; } = 4096; + + /// + /// Creates options with specified max depth. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static AcBinarySerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth }; + + /// + /// Creates options without reference handling. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false }; + + /// + /// Creates options without metadata (faster but less flexible). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static AcBinarySerializerOptions WithoutMetadata() => new() { UseMetadata = false }; +} + +/// +/// Binary type codes for serialization. +/// Designed for fast switch dispatch and compact storage. +/// Lower 5 bits = type code (0-31) +/// Upper 3 bits = flags (interned, reference, has-type-info) +/// +internal static class BinaryTypeCode +{ + // Primitive types (0-15) + public const byte Null = 0; + public const byte True = 1; + public const byte False = 2; + public const byte Int8 = 3; + public const byte UInt8 = 4; + public const byte Int16 = 5; + public const byte UInt16 = 6; + public const byte Int32 = 7; + public const byte UInt32 = 8; + public const byte Int64 = 9; + public const byte UInt64 = 10; + public const byte Float32 = 11; + public const byte Float64 = 12; + public const byte Decimal = 13; + public const byte Char = 14; + + // String types (16-19) + public const byte String = 16; // Inline UTF8 string + public const byte StringInterned = 17; // Reference to interned string by index + public const byte StringEmpty = 18; // Empty string marker + + // Date/Time types (20-23) + public const byte DateTime = 20; + public const byte DateTimeOffset = 21; + public const byte TimeSpan = 22; + public const byte Guid = 23; + + // Enum (24) + public const byte Enum = 24; + + // Complex types (25-31) + public const byte Object = 25; // Start of object + public const byte ObjectEnd = 26; // End of object marker + public const byte ObjectRef = 27; // Reference to previously serialized object + public const byte Array = 28; // Start of array/list + public const byte Dictionary = 29; // Start of dictionary + public const byte ByteArray = 30; // Optimized byte[] storage + + // Special markers (32+, for header/meta) + public const byte MetadataHeader = 32; // Binary has metadata section + public const byte NoMetadataHeader = 33; // Binary has no metadata + + // Compact integer variants (for VarInt optimization) + public const byte Int32Tiny = 64; // -16 to 111 stored in single byte (value = code - 64 - 16) + public const byte Int32TinyMax = 191; // Upper bound for tiny int + + /// + /// Check if type code represents a reference (string or object). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsReference(byte code) => code == StringInterned || code == ObjectRef; + + /// + /// Check if type code is a tiny int (single byte int32 encoding). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsTinyInt(byte code) => code >= Int32Tiny && code <= Int32TinyMax; + + /// + /// Decode tiny int value from type code. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16; + + /// + /// Encode small int value (-16 to 111) as type code. + /// Returns true if value fits in tiny encoding. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryEncodeTinyInt(int value, out byte code) + { + if (value >= -16 && value <= 111) + { + code = (byte)(value + 16 + Int32Tiny); + return true; + } + code = 0; + return false; + } +} diff --git a/AyCode.Core/Extensions/AcJsonSerializerOptions.cs b/AyCode.Core/Extensions/AcJsonSerializerOptions.cs new file mode 100644 index 0000000..50e0d2d --- /dev/null +++ b/AyCode.Core/Extensions/AcJsonSerializerOptions.cs @@ -0,0 +1,54 @@ +namespace AyCode.Core.Extensions; + +public enum AcSerializerType : byte +{ + Json = 0, + Binary = 1, +} + +public abstract class AcSerializerOptions +{ + public abstract AcSerializerType SerializerType { get; init; } + /// + /// Whether to use $id/$ref reference handling for circular references. + /// Default: true + /// + public bool UseReferenceHandling { get; init; } = true; + + /// + /// Maximum depth for serialization/deserialization. + /// 0 = root level only (primitives of root object) + /// 1 = root + first level of nested objects/collections + /// byte.MaxValue (255) = effectively unlimited + /// Default: byte.MaxValue + /// + public byte MaxDepth { get; init; } = byte.MaxValue; +} + +/// +/// Options for AcJsonSerializer and AcJsonDeserializer. +/// +public sealed class AcJsonSerializerOptions : AcSerializerOptions +{ + public override AcSerializerType SerializerType { get; init; } = AcSerializerType.Json; + + /// + /// Default options instance with reference handling enabled and max depth. + /// + public static readonly AcJsonSerializerOptions Default = new(); + + /// + /// Options for shallow serialization (root level only, no references). + /// + public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, UseReferenceHandling = false }; + + /// + /// Creates options with specified max depth. + /// + public static AcJsonSerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth }; + + /// + /// Creates options without reference handling. + /// + public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false }; +} \ No newline at end of file diff --git a/AyCode.Core/Extensions/JsonUtilities.cs b/AyCode.Core/Extensions/JsonUtilities.cs index 1d0e122..7251503 100644 --- a/AyCode.Core/Extensions/JsonUtilities.cs +++ b/AyCode.Core/Extensions/JsonUtilities.cs @@ -11,47 +11,6 @@ using Newtonsoft.Json; namespace AyCode.Core.Extensions; -/// -/// Options for AcJsonSerializer and AcJsonDeserializer. -/// -public sealed class AcJsonSerializerOptions -{ - /// - /// Default options instance with reference handling enabled and max depth. - /// - public static readonly AcJsonSerializerOptions Default = new(); - - /// - /// Options for shallow serialization (root level only, no references). - /// - public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, UseReferenceHandling = false }; - - /// - /// Whether to use $id/$ref reference handling for circular references. - /// Default: true - /// - public bool UseReferenceHandling { get; init; } = true; - - /// - /// Maximum depth for serialization/deserialization. - /// 0 = root level only (primitives of root object) - /// 1 = root + first level of nested objects/collections - /// byte.MaxValue (255) = effectively unlimited - /// Default: byte.MaxValue - /// - public byte MaxDepth { get; init; } = byte.MaxValue; - - /// - /// Creates options with specified max depth. - /// - public static AcJsonSerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth }; - - /// - /// Creates options without reference handling. - /// - public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false }; -} - /// /// Cached result for IId type info lookup. /// diff --git a/AyCode.Core/Extensions/SerializeObjectExtensions.cs b/AyCode.Core/Extensions/SerializeObjectExtensions.cs index ccb90e8..cae3976 100644 --- a/AyCode.Core/Extensions/SerializeObjectExtensions.cs +++ b/AyCode.Core/Extensions/SerializeObjectExtensions.cs @@ -340,8 +340,7 @@ public static class SerializeObjectExtensions /// /// Serialize object to JSON string with default options. /// - public static string ToJson(this T source) - => AcJsonSerializer.Serialize(source); + public static string ToJson(this T source) => AcJsonSerializer.Serialize(source); /// /// Serialize object to JSON string with specified options. @@ -445,14 +444,83 @@ public static class SerializeObjectExtensions public static T MessagePackTo(this byte[] message, MessagePackSerializerOptions options) => MessagePackSerializer.Deserialize(message, options); + + public static object ToAny(this T source, AcSerializerOptions options) + { + if (options.SerializerType == AcSerializerType.Json) return ToJson(source, (AcJsonSerializerOptions)options); + return ToBinary(source, (AcBinarySerializerOptions)options); + } + + /// + /// Deserialize data (JSON string or binary byte[]) to object based on options. + /// + public static T? AnyTo(this object data, AcSerializerOptions options) + { + if (options.SerializerType == AcSerializerType.Json) + return ((string)data).JsonTo((AcJsonSerializerOptions)options); + return ((byte[])data).BinaryTo(); + } + + /// + /// Deserialize data to specified type based on options. + /// + public static object? AnyTo(this object data, Type targetType, AcSerializerOptions options) + { + if (options.SerializerType == AcSerializerType.Json) + return ((string)data).JsonTo(targetType, (AcJsonSerializerOptions)options); + return ((byte[])data).BinaryTo(targetType); + } + + /// + /// Populate existing object from data based on options. + /// + public static void AnyTo(this object data, T target, AcSerializerOptions options) where T : class + { + if (options.SerializerType == AcSerializerType.Json) + ((string)data).JsonTo(target, (AcJsonSerializerOptions)options); + else + ((byte[])data).BinaryTo(target); + } + + /// + /// Populate existing object with merge semantics based on options. + /// + public static void AnyToMerge(this object data, T target, AcSerializerOptions options) where T : class + { + if (options.SerializerType == AcSerializerType.Json) + ((string)data).JsonTo(target, (AcJsonSerializerOptions)options); // JSON always merges + else + ((byte[])data).BinaryToMerge(target); + } + + /// + /// Clone object via serialization based on options. + /// + public static T? CloneToAny(this T source, AcSerializerOptions options) where T : class + { + if (options.SerializerType == AcSerializerType.Json) + return source.CloneTo((AcJsonSerializerOptions)options); + return source.BinaryCloneTo(); + } + + /// + /// Copy object properties to target via serialization based on options. + /// + public static void CopyToAny(this T source, T target, AcSerializerOptions options) where T : class + { + if (options.SerializerType == AcSerializerType.Json) + source.CopyTo(target, (AcJsonSerializerOptions)options); + else + source.BinaryCopyTo(target); + } + #region Binary Serialization Extension Methods /// /// Serialize object to binary byte array with default options. /// Significantly faster than JSON, especially for large data in WASM. /// - public static byte[] ToBinary(this T source) - => AcBinarySerializer.Serialize(source); + public static byte[] ToBinary(this T source) => AcBinarySerializer.Serialize(source); /// /// Serialize object to binary byte array with specified options. diff --git a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs index 5e8139c..95c1ec0 100644 --- a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs +++ b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs @@ -17,7 +17,9 @@ public abstract class AcWebSignalRHubBase(IConfiguration { protected readonly List> DynamicMethodCallModels = []; protected TLogger Logger = logger; - protected IConfiguration Configuration = configuration; + protected IConfiguration Configuration = configuration; + + protected AcSerializerOptions SerializerOptions = new AcBinarySerializerOptions(); #region Connection Lifecycle diff --git a/BenchmarkSuite1/Program.cs b/BenchmarkSuite1/Program.cs index 2277d56..94f51ab 100644 --- a/BenchmarkSuite1/Program.cs +++ b/BenchmarkSuite1/Program.cs @@ -153,7 +153,7 @@ namespace BenchmarkSuite1 pointsPerMeasurement: 5); var binaryWithRef = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default); - var binaryNoRef = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.WithoutReferenceHandling); + var binaryNoRef = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.WithoutReferenceHandling()); var json = AcJsonSerializer.Serialize(order, AcJsonSerializerOptions.WithoutReferenceHandling()); var jsonBytes = Encoding.UTF8.GetByteCount(json);