1642 lines
64 KiB
C#
1642 lines
64 KiB
C#
using AyCode.Core.Compression;
|
|
using AyCode.Core.Helpers;
|
|
using AyCode.Core.Serializers.Expressions;
|
|
using System.Buffers;
|
|
using System.Collections;
|
|
using System.Collections.Concurrent;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using AyCode.Core.Serializers.Jsons;
|
|
using static AyCode.Core.Helpers.JsonUtilities;
|
|
|
|
namespace AyCode.Core.Serializers.Binaries;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// - Optimized buffer management with ArrayPool
|
|
/// - Zero-allocation hot paths using Span and MemoryMarshal
|
|
/// - Automatic Expression to AcExpressionNode conversion
|
|
/// - Generic TOutput for JIT devirtualization (ArrayBinaryOutput / BufferWriterBinaryOutput)
|
|
/// </summary>
|
|
public static partial class AcBinarySerializer
|
|
{
|
|
|
|
// Pre-computed UTF8 encoder for string operations
|
|
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
|
private static readonly Type StringType = typeof(string);
|
|
private static readonly Type GuidType = typeof(Guid);
|
|
private static readonly Type DateTimeOffsetType = typeof(DateTimeOffset);
|
|
private static readonly Type TimeSpanType = typeof(TimeSpan);
|
|
private static readonly Type IntType = typeof(int);
|
|
private static readonly Type LongType = typeof(long);
|
|
private static readonly Type FloatType = typeof(float);
|
|
private static readonly Type DoubleType = typeof(double);
|
|
private static readonly Type DecimalType = typeof(decimal);
|
|
private static readonly Type BoolType = typeof(bool);
|
|
private static readonly Type DateTimeType = typeof(DateTime);
|
|
|
|
#region Public API
|
|
|
|
#if DEBUG
|
|
/// <summary>
|
|
/// DEBUG ONLY: Analyzes which string properties have repeated values that would benefit from interning.
|
|
/// Returns a dictionary where key is "TypeName.PropertyName" and value is the occurrence count.
|
|
/// Only properties with count > 1 are good candidates for [StringIntern] attribute.
|
|
/// </summary>
|
|
/// <param name="value">The object graph to analyze.</param>
|
|
/// <param name="options">Serializer options (UseStringInterning should be enabled).</param>
|
|
/// <returns>Dictionary of property paths to their string occurrence counts.</returns>
|
|
public static Dictionary<string, Dictionary<string, int>> AnalyzeStringInternCandidates<T>(T value, AcBinarySerializerOptions? options = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(value);
|
|
|
|
options ??= AcBinarySerializerOptions.Default;
|
|
|
|
// For analysis, use the provided reference handling mode
|
|
var analysisOptions = new AcBinarySerializerOptions
|
|
{
|
|
UseStringInterning = StringInterningMode.All,
|
|
MinStringInternLength = options.MinStringInternLength,
|
|
MaxStringInternLength = options.MaxStringInternLength,
|
|
ReferenceHandling = options.ReferenceHandling
|
|
};
|
|
|
|
var result = new Dictionary<string, Dictionary<string, int>>();
|
|
var runtimeType = value.GetType();
|
|
|
|
// Create context without pooling (we need to set up callback)
|
|
using var context = new BinarySerializationContext<ArrayBinaryOutput>(analysisOptions);
|
|
context.Output = new ArrayBinaryOutput();
|
|
|
|
// Set up tracking callbacks
|
|
context.OnStringInterned = (propertyPath, stringValue) =>
|
|
{
|
|
propertyPath ??= "(unknown)";
|
|
|
|
if (!result.TryGetValue(stringValue, out var properties))
|
|
{
|
|
properties = new Dictionary<string, int>();
|
|
result[stringValue] = properties;
|
|
}
|
|
|
|
properties[propertyPath] = properties.GetValueOrDefault(propertyPath) + 1;
|
|
};
|
|
|
|
// Run serialization to trigger callbacks
|
|
context.WriteHeader();
|
|
WriteValue(value, runtimeType, context, 0);
|
|
|
|
return result;
|
|
}
|
|
|
|
public static StringBuilder GetAnalyzeStringInternCandidatesLog<T>(T value, AcBinarySerializerOptions? options = null)
|
|
{
|
|
var sb = new StringBuilder();
|
|
options ??= AcBinarySerializerOptions.Default;
|
|
var analysis = AnalyzeStringInternCandidates(value, options);
|
|
|
|
// Transform: stringValue → properties TO propertyPath → (stringValue, count)
|
|
var propertyStats = new Dictionary<string, List<(string StringValue, int Count, int ByteLength)>>();
|
|
|
|
foreach (var (stringValue, properties) in analysis)
|
|
{
|
|
var byteLength = Encoding.UTF8.GetByteCount(stringValue);
|
|
foreach (var (propPath, count) in properties)
|
|
{
|
|
if (!propertyStats.TryGetValue(propPath, out var list))
|
|
{
|
|
list = [];
|
|
propertyStats[propPath] = list;
|
|
}
|
|
list.Add((stringValue, count, byteLength));
|
|
}
|
|
}
|
|
|
|
var refMode = options.ReferenceHandling;
|
|
|
|
// Header
|
|
sb.AppendLine("+==============================================================================+");
|
|
sb.AppendLine($"| STRING INTERN ANALYSIS REPORT (RefMode: {refMode,-12}) |");
|
|
sb.AppendLine("+==============================================================================+");
|
|
sb.AppendLine();
|
|
|
|
// Global summary
|
|
var totalStrings = analysis.Values.Sum(p => p.Values.Sum());
|
|
var uniqueStrings = analysis.Count;
|
|
var repeatedStrings = analysis.Count(kv => kv.Value.Values.Sum() > 1);
|
|
|
|
sb.AppendLine("+-----------------------------------------------------------------------------+");
|
|
sb.AppendLine("| STRING SUMMARY |");
|
|
sb.AppendLine("+-----------------------------------------------------------------------------+");
|
|
sb.AppendLine($"| Total string occurrences: {totalStrings,-10} Unique strings: {uniqueStrings,-10} Repeated: {repeatedStrings,-8} |");
|
|
sb.AppendLine("+-----------------------------------------------------------------------------+");
|
|
sb.AppendLine();
|
|
|
|
// Property-focused table
|
|
// Calculate stats for each property first (for sorting by RepeatSum%)
|
|
var propertyStatsCalculated = propertyStats.Select(kv =>
|
|
{
|
|
var propPath = kv.Key;
|
|
var strings = kv.Value;
|
|
var total = strings.Sum(s => s.Count);
|
|
var unique = strings.Count;
|
|
var repeated = strings.Count(s => s.Count > 1);
|
|
var repeatSum = strings.Where(s => s.Count > 1).Sum(s => s.Count); // Sum of occurrences of repeated strings
|
|
var repeatSumPct = total > 0 ? repeatSum * 100.0 / total : 0;
|
|
|
|
// Calculate savings
|
|
var totalBytes = strings.Sum(s => s.Count * s.ByteLength);
|
|
var uniqueBytes = strings.Sum(s => s.ByteLength);
|
|
var indexBytes = total * 2;
|
|
var savings = totalBytes - (uniqueBytes + indexBytes);
|
|
|
|
return (propPath, strings, total, unique, repeated, repeatSum, repeatSumPct, savings);
|
|
}).ToList();
|
|
|
|
sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+");
|
|
sb.AppendLine("| Property | Total | RepSum | Unique | Repeated| RepSum % | Savings | Recommend |");
|
|
sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+");
|
|
|
|
foreach (var stat in propertyStatsCalculated.OrderByDescending(x => x.repeatSumPct))
|
|
{
|
|
var savingsStr = stat.savings > 0 ? $"+{stat.savings:N0}B" : $"{stat.savings:N0}B";
|
|
var recommend = stat.savings > 100 ? "[INTERN]" : stat.savings > 0 ? " maybe " : " skip ";
|
|
|
|
var propDisplay = stat.propPath.Length > 30 ? stat.propPath[..27] + "..." : stat.propPath;
|
|
sb.AppendLine($"| {propDisplay,-30} | {stat.total,5} | {stat.repeatSum,7} | {stat.unique,6} | {stat.repeated,7} | {stat.repeatSumPct,8:F1}% | {savingsStr,8} | {recommend,-11} |");
|
|
}
|
|
|
|
sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+");
|
|
sb.AppendLine();
|
|
|
|
// Detailed property breakdown (only for properties with significant savings)
|
|
sb.AppendLine("+-----------------------------------------------------------------------------+");
|
|
sb.AppendLine("| DETAILED BREAKDOWN (properties with savings > 100 bytes) |");
|
|
sb.AppendLine("+-----------------------------------------------------------------------------+");
|
|
|
|
foreach (var stat in propertyStatsCalculated
|
|
.Where(x => x.savings > 100)
|
|
.OrderByDescending(x => x.repeatSumPct))
|
|
{
|
|
sb.AppendLine();
|
|
sb.AppendLine($" {stat.propPath} (RepSum: {stat.repeatSum}/{stat.total} = {stat.repeatSumPct:F1}%):");
|
|
|
|
foreach (var (strVal, count, _) in stat.strings.OrderByDescending(s => s.Count).Take(10))
|
|
{
|
|
var preview = strVal.Length > 40 ? strVal[..37] + "..." : strVal;
|
|
var marker = count > 1 ? ">" : " ";
|
|
sb.AppendLine($" {marker} [{count,4}x] \"{preview}\"");
|
|
}
|
|
|
|
if (stat.strings.Count > 10)
|
|
{
|
|
sb.AppendLine($" ... and {stat.strings.Count - 10} more unique values");
|
|
}
|
|
}
|
|
|
|
sb.AppendLine();
|
|
sb.AppendLine("===============================================================================");
|
|
sb.AppendLine("Legend: Total=all occurrences, RepSum=sum of repeated string occurrences");
|
|
sb.AppendLine(" Unique=distinct values, Repeated=count of values appearing 2+ times");
|
|
sb.AppendLine(" RepSum%=percentage of occurrences that are repeated (higher=better for intern)");
|
|
sb.AppendLine(" Savings=estimated bytes saved with interning (positive=good)");
|
|
sb.AppendLine(" > = repeated string (benefits from interning)");
|
|
|
|
return sb;
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Serialize object to binary with default options.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static byte[] Serialize<T>(T value) => Serialize(value, AcBinarySerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Serialize object to an IBufferWriter with default options.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void Serialize<T>(T value, IBufferWriter<byte> writer) => Serialize(value, writer, AcBinarySerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Serialize object to binary with specified options.
|
|
/// Uses ArrayBinaryOutput for byte[] result path.
|
|
/// </summary>
|
|
public static byte[] Serialize<T>(T value, AcBinarySerializerOptions options)
|
|
{
|
|
if (value == null)
|
|
{
|
|
return [BinaryTypeCode.Null];
|
|
}
|
|
|
|
var runtimeType = value.GetType();
|
|
|
|
// Handle IQueryable types - convert to AcExpressionNode (serialize the Expression)
|
|
object actualValue = value;
|
|
if (value is IQueryable queryable)
|
|
{
|
|
actualValue = AcSerializerCommon.QueryableToNode(queryable);
|
|
runtimeType = typeof(AcExpressionNode);
|
|
}
|
|
// Handle Expression types - convert to AcExpressionNode
|
|
else if (AcSerializerCommon.IsExpressionType(runtimeType))
|
|
{
|
|
actualValue = AcExpressionConverter.ToNode((Expression)(object)value);
|
|
runtimeType = typeof(AcExpressionNode);
|
|
}
|
|
|
|
var context = BinarySerializationContextPool<ArrayBinaryOutput>.Get(options);
|
|
if (context.Output == null)
|
|
context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity);
|
|
else
|
|
context.Output.Reset();
|
|
|
|
try
|
|
{
|
|
ScanForDuplicates(actualValue, runtimeType, context);
|
|
context.WriteHeader();
|
|
WriteValue(actualValue, runtimeType, context, 0);
|
|
|
|
// Apply compression if enabled - compress directly from buffer span (1 allocation)
|
|
if (options.UseCompression != Lz4CompressionMode.None)
|
|
{
|
|
return Lz4.Compress(context.Output.AsSpan(), options.UseCompression);
|
|
}
|
|
|
|
// No compression - single allocation for result
|
|
return context.Output.ToArray();
|
|
}
|
|
finally
|
|
{
|
|
if (options.UseAsync) BinarySerializationContextPool<ArrayBinaryOutput>.ReturnAsync(context);
|
|
else BinarySerializationContextPool<ArrayBinaryOutput>.Return(context);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serialize object to an IBufferWriter for zero-copy scenarios.
|
|
/// Uses BufferWriterBinaryOutput — writes directly to the caller's buffer.
|
|
/// Note: Compression is applied if enabled in options.
|
|
/// </summary>
|
|
public static void Serialize<T>(T value, IBufferWriter<byte> writer, AcBinarySerializerOptions options)
|
|
{
|
|
if (value == null)
|
|
{
|
|
var span = writer.GetSpan(1);
|
|
span[0] = BinaryTypeCode.Null;
|
|
writer.Advance(1);
|
|
return;
|
|
}
|
|
|
|
var runtimeType = value.GetType();
|
|
|
|
// Handle IQueryable types - convert to AcExpressionNode (serialize the Expression)
|
|
object actualValue = value;
|
|
if (value is IQueryable queryable)
|
|
{
|
|
actualValue = AcSerializerCommon.QueryableToNode(queryable);
|
|
runtimeType = typeof(AcExpressionNode);
|
|
}
|
|
// Handle Expression types - convert to AcExpressionNode
|
|
else if (AcSerializerCommon.IsExpressionType(runtimeType))
|
|
{
|
|
actualValue = AcExpressionConverter.ToNode((Expression)(object)value);
|
|
runtimeType = typeof(AcExpressionNode);
|
|
}
|
|
|
|
var output = new BufferWriterBinaryOutput(writer);
|
|
var context = BinarySerializationContextPool<BufferWriterBinaryOutput>.Get(options);
|
|
context.Output = output;
|
|
|
|
try
|
|
{
|
|
ScanForDuplicates(actualValue, runtimeType, context);
|
|
context.WriteHeader();
|
|
WriteValue(actualValue, runtimeType, context, 0);
|
|
|
|
// Apply compression if enabled
|
|
if (options.UseCompression != Lz4CompressionMode.None)
|
|
{
|
|
// For compression with BufferWriter, we need to flush first then compress
|
|
// This path is less common — compression typically uses byte[] path
|
|
output.Flush();
|
|
// Compression with IBufferWriter requires intermediate buffer
|
|
// Fall back to ArrayBinaryOutput path for compression
|
|
throw new NotSupportedException(
|
|
"Compression is not supported with IBufferWriter output. " +
|
|
"Use the byte[] overload or disable compression.");
|
|
}
|
|
|
|
output.Flush();
|
|
}
|
|
finally
|
|
{
|
|
context.Output = null!;
|
|
if (options.UseAsync) BinarySerializationContextPool<BufferWriterBinaryOutput>.ReturnAsync(context);
|
|
else BinarySerializationContextPool<BufferWriterBinaryOutput>.Return(context);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the serialized size without allocating the final array.
|
|
/// Useful for pre-allocating buffers.
|
|
/// </summary>
|
|
public static int GetSerializedSize<T>(T value, AcBinarySerializerOptions options)
|
|
{
|
|
if (value == null) return 1;
|
|
|
|
var runtimeType = value.GetType();
|
|
|
|
var context = BinarySerializationContextPool<ArrayBinaryOutput>.Get(options);
|
|
if (context.Output == null)
|
|
context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity);
|
|
else
|
|
context.Output.Reset();
|
|
|
|
try
|
|
{
|
|
ScanForDuplicates(value, runtimeType, context);
|
|
context.WriteHeader();
|
|
WriteValue(value, runtimeType, context, 0);
|
|
return context.Position;
|
|
}
|
|
finally
|
|
{
|
|
if (options.UseAsync) BinarySerializationContextPool<ArrayBinaryOutput>.ReturnAsync(context);
|
|
else BinarySerializationContextPool<ArrayBinaryOutput>.Return(context);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serialize object and keep the pooled buffer for zero-copy consumers.
|
|
/// Caller must dispose the returned result to release the buffer.
|
|
/// Note: Compression is applied if enabled in options, result will be immutable (not pooled).
|
|
/// </summary>
|
|
public static BinarySerializationResult SerializeToPooledBuffer<T>(T value, AcBinarySerializerOptions options)
|
|
{
|
|
if (value == null)
|
|
{
|
|
return BinarySerializationResult.FromImmutable([BinaryTypeCode.Null]);
|
|
}
|
|
|
|
var runtimeType = value.GetType();
|
|
|
|
var context = BinarySerializationContextPool<ArrayBinaryOutput>.Get(options);
|
|
if (context.Output == null)
|
|
context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity);
|
|
else
|
|
context.Output.Reset();
|
|
|
|
try
|
|
{
|
|
ScanForDuplicates(value, runtimeType, context);
|
|
context.WriteHeader();
|
|
WriteValue(value, runtimeType, context, 0);
|
|
|
|
// If compression enabled, compress directly from buffer span (1 allocation)
|
|
if (options.UseCompression != Lz4CompressionMode.None)
|
|
{
|
|
var compressed = Lz4.Compress(context.Output.AsSpan(), options.UseCompression);
|
|
return BinarySerializationResult.FromImmutable(compressed);
|
|
}
|
|
|
|
return context.Output.DetachResult();
|
|
}
|
|
finally
|
|
{
|
|
if (options.UseAsync) BinarySerializationContextPool<ArrayBinaryOutput>.ReturnAsync(context);
|
|
else BinarySerializationContextPool<ArrayBinaryOutput>.Return(context);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Value Writing
|
|
|
|
private static void WriteValue<TOutput>(object? value, Type type, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
var output = context.Output;
|
|
if (value == null)
|
|
{
|
|
output.WriteByte(BinaryTypeCode.Null);
|
|
return;
|
|
}
|
|
|
|
// Try writing as primitive first
|
|
if (TryWritePrimitive(value, type, output, context))
|
|
return;
|
|
|
|
WriteValueNonPrimitive(value, type, output, context, depth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a non-primitive value (collection, dictionary, byte[], or complex object).
|
|
/// Skips null check and TryWritePrimitive — caller guarantees value is non-null and not a primitive type.
|
|
/// Called from WritePropertyOrSkip default case (PropertyAccessorType.Object) and WriteValue fallback.
|
|
/// </summary>
|
|
private static void WriteValueNonPrimitive<TOutput>(object value, Type type, TOutput output, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
// Nullable<T> where T is a value type: boxed value may be a primitive.
|
|
// Only Nullable<T> can be a value type in the Object accessor path.
|
|
if (type.IsValueType)
|
|
{
|
|
if (TryWritePrimitive(value, value.GetType(), output, context))
|
|
return;
|
|
}
|
|
|
|
if (depth > context.MaxDepth)
|
|
{
|
|
output.WriteByte(BinaryTypeCode.Null);
|
|
return;
|
|
}
|
|
|
|
// Handle byte arrays specially (value-like, no reference tracking)
|
|
if (value is byte[] byteArray)
|
|
{
|
|
WriteByteArray(byteArray, output);
|
|
return;
|
|
}
|
|
|
|
// Handle dictionaries
|
|
if (value is IDictionary dictionary)
|
|
{
|
|
WriteDictionary(dictionary, context, depth);
|
|
return;
|
|
}
|
|
|
|
// Get wrapper once — used by both WriteArray and WriteObject
|
|
var wrapper = context.GetWrapper(type);
|
|
|
|
// Handle collections/arrays
|
|
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
|
{
|
|
WriteArray(enumerable, wrapper, context, depth);
|
|
return;
|
|
}
|
|
|
|
// Handle complex objects with single-pass reference tracking
|
|
WriteObject(value, wrapper, context, depth, isNested: depth > 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized primitive writer using TypeCode dispatch.
|
|
/// Avoids Nullable.GetUnderlyingType in hot path by using cached type info.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool TryWritePrimitive<TOutput>(object value, Type type, TOutput output, BinarySerializationContext<TOutput> context)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
// Fast path: check TypeCode first (handles most primitives)
|
|
var typeCode = Type.GetTypeCode(type);
|
|
|
|
switch (typeCode)
|
|
{
|
|
case TypeCode.Int32:
|
|
WriteInt32((int)value, output);
|
|
return true;
|
|
case TypeCode.Int64:
|
|
WriteInt64((long)value, output);
|
|
return true;
|
|
case TypeCode.Boolean:
|
|
output.WriteByte((bool)value ? BinaryTypeCode.True : BinaryTypeCode.False);
|
|
return true;
|
|
case TypeCode.Double:
|
|
WriteFloat64Unsafe((double)value, output);
|
|
return true;
|
|
case TypeCode.String:
|
|
WriteString((string)value, output, context);
|
|
return true;
|
|
case TypeCode.Single:
|
|
WriteFloat32Unsafe((float)value, output);
|
|
return true;
|
|
case TypeCode.Decimal:
|
|
WriteDecimalUnsafe((decimal)value, output);
|
|
return true;
|
|
case TypeCode.DateTime:
|
|
WriteDateTimeUnsafe((DateTime)value, output);
|
|
return true;
|
|
case TypeCode.Byte:
|
|
output.WriteByte(BinaryTypeCode.UInt8);
|
|
output.WriteByte((byte)value);
|
|
return true;
|
|
case TypeCode.Int16:
|
|
WriteInt16Unsafe((short)value, output);
|
|
return true;
|
|
case TypeCode.UInt16:
|
|
WriteUInt16Unsafe((ushort)value, output);
|
|
return true;
|
|
case TypeCode.UInt32:
|
|
WriteUInt32((uint)value, output);
|
|
return true;
|
|
case TypeCode.UInt64:
|
|
WriteUInt64((ulong)value, output);
|
|
return true;
|
|
case TypeCode.SByte:
|
|
output.WriteByte(BinaryTypeCode.Int8);
|
|
output.WriteByte(unchecked((byte)(sbyte)value));
|
|
return true;
|
|
case TypeCode.Char:
|
|
WriteCharUnsafe((char)value, output);
|
|
return true;
|
|
}
|
|
|
|
// Handle special types by reference comparison (faster than type equality)
|
|
if (ReferenceEquals(type, GuidType))
|
|
{
|
|
WriteGuidUnsafe((Guid)value, output);
|
|
return true;
|
|
}
|
|
if (ReferenceEquals(type, DateTimeOffsetType))
|
|
{
|
|
WriteDateTimeOffsetUnsafe((DateTimeOffset)value, output);
|
|
return true;
|
|
}
|
|
if (ReferenceEquals(type, TimeSpanType))
|
|
{
|
|
WriteTimeSpanUnsafe((TimeSpan)value, output);
|
|
return true;
|
|
}
|
|
if (type.IsEnum)
|
|
{
|
|
WriteEnum(value, output);
|
|
return true;
|
|
}
|
|
|
|
// Handle nullable types - use cached check instead of GetUnderlyingType
|
|
// For nullable, value is already unwrapped when boxed, so we can use value.GetType()
|
|
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
|
{
|
|
// When boxed, nullable value types are unwrapped to their underlying type
|
|
// So we can just call TryWritePrimitive with the actual runtime type
|
|
return TryWritePrimitive(value, value.GetType(), output, context);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Optimized Primitive Writers using MemoryMarshal
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteInt32<TOutput>(int value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
|
{
|
|
output.WriteByte(tiny);
|
|
return;
|
|
}
|
|
output.WriteByte(BinaryTypeCode.Int32);
|
|
output.WriteVarInt(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteInt64<TOutput>(long value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
if (value >= int.MinValue && value <= int.MaxValue)
|
|
{
|
|
WriteInt32((int)value, output);
|
|
return;
|
|
}
|
|
output.WriteByte(BinaryTypeCode.Int64);
|
|
output.WriteVarLong(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized float64 writer using batched write.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteFloat64Unsafe<TOutput>(double value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteTypeCodeAndRaw(BinaryTypeCode.Float64, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized float32 writer using batched write.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteFloat32Unsafe<TOutput>(float value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteTypeCodeAndRaw(BinaryTypeCode.Float32, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized decimal writer using direct memory copy of bits.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteDecimalUnsafe<TOutput>(decimal value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteByte(BinaryTypeCode.Decimal);
|
|
output.WriteDecimalBits(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized DateTime writer.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteDateTimeUnsafe<TOutput>(DateTime value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteByte(BinaryTypeCode.DateTime);
|
|
output.WriteDateTimeBits(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized Guid writer using direct memory copy.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteGuidUnsafe<TOutput>(Guid value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteByte(BinaryTypeCode.Guid);
|
|
output.WriteGuidBits(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized DateTimeOffset writer.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteDateTimeOffsetUnsafe<TOutput>(DateTimeOffset value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteByte(BinaryTypeCode.DateTimeOffset);
|
|
output.WriteDateTimeOffsetBits(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized TimeSpan writer.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteTimeSpanUnsafe<TOutput>(TimeSpan value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteTypeCodeAndRaw(BinaryTypeCode.TimeSpan, value.Ticks);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteInt16Unsafe<TOutput>(short value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteTypeCodeAndRaw(BinaryTypeCode.Int16, value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteUInt16Unsafe<TOutput>(ushort value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteTypeCodeAndRaw(BinaryTypeCode.UInt16, value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteUInt32<TOutput>(uint value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteByte(BinaryTypeCode.UInt32);
|
|
output.WriteVarUInt(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteUInt64<TOutput>(ulong value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteByte(BinaryTypeCode.UInt64);
|
|
output.WriteVarULong(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteCharUnsafe<TOutput>(char value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteByte(BinaryTypeCode.Char);
|
|
output.WriteRaw(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteEnum<TOutput>(object value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
// Use direct unboxing instead of Convert.ToInt32 to avoid NumberFormatInfo overhead
|
|
var intValue = GetEnumAsInt32Fast(value);
|
|
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
|
{
|
|
output.WriteByte(BinaryTypeCode.Enum);
|
|
output.WriteByte(tiny);
|
|
return;
|
|
}
|
|
output.WriteByte(BinaryTypeCode.Enum);
|
|
output.WriteByte(BinaryTypeCode.Int32);
|
|
output.WriteVarInt(intValue);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fast enum to int conversion avoiding Convert.ToInt32 overhead.
|
|
/// Handles all common enum underlying types.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static int GetEnumAsInt32Fast(object enumValue)
|
|
{
|
|
var type = enumValue.GetType();
|
|
var underlyingType = type.GetEnumUnderlyingType();
|
|
|
|
if (ReferenceEquals(underlyingType, IntType))
|
|
return (int)enumValue;
|
|
if (ReferenceEquals(underlyingType, typeof(byte)))
|
|
return (byte)enumValue;
|
|
if (ReferenceEquals(underlyingType, typeof(sbyte)))
|
|
return (sbyte)enumValue;
|
|
if (ReferenceEquals(underlyingType, typeof(short)))
|
|
return (short)enumValue;
|
|
if (ReferenceEquals(underlyingType, typeof(ushort)))
|
|
return (ushort)enumValue;
|
|
if (ReferenceEquals(underlyingType, typeof(uint)))
|
|
return unchecked((int)(uint)enumValue);
|
|
|
|
// Fallback for rare cases (long, ulong)
|
|
return Convert.ToInt32(enumValue);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized string writer with FixStr for short strings.
|
|
/// Marker-based interning: write String marker, rewrite to StringInternFirst at end if needed.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteString<TOutput>(string value, TOutput output, BinarySerializationContext<TOutput> context)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
if (value.Length == 0)
|
|
{
|
|
output.WriteByte(BinaryTypeCode.StringEmpty);
|
|
return;
|
|
}
|
|
|
|
// String interning: only for strings within length range
|
|
// MaxStringInternLength == 0 means no max limit
|
|
if (context.UseStringInterning
|
|
&& value.Length >= context.MinStringInternLength
|
|
&& (context.MaxStringInternLength == 0 || value.Length <= context.MaxStringInternLength))
|
|
{
|
|
ref var interEntry = ref context.GetInternedStringEntry(value, out bool found);
|
|
|
|
if (found)
|
|
{
|
|
// String was seen in scan pass
|
|
if (interEntry.CacheIndex >= 0)
|
|
{
|
|
if (interEntry.IsFirstWrite)
|
|
{
|
|
// 1st serialize occurrence of a cached string - write StringInternFirst + cacheIndex + data
|
|
interEntry.IsFirstWrite = false;
|
|
output.WriteByte(BinaryTypeCode.StringInternFirst);
|
|
output.WriteVarUInt((uint)interEntry.CacheIndex);
|
|
output.WriteStringUtf8(value);
|
|
}
|
|
else
|
|
{
|
|
// 2+ serialize occurrence: write index reference
|
|
output.WriteByte(BinaryTypeCode.StringInterned);
|
|
output.WriteVarUInt((uint)interEntry.CacheIndex);
|
|
}
|
|
return;
|
|
}
|
|
// CacheIndex < 0 means string appeared only once in scan - write as plain string
|
|
}
|
|
#if DEBUG
|
|
context.OnStringInterned?.Invoke(context.CurrentPropertyPath, value);
|
|
#endif
|
|
// String not cached (single occurrence or not found) - write plain String
|
|
output.WriteByte(BinaryTypeCode.String);
|
|
output.WriteStringUtf8(value);
|
|
return;
|
|
}
|
|
|
|
// Fast path for short strings: check length first (cheap), then ASCII
|
|
// FixStr encodes type+length in single byte for strings <= 31 chars
|
|
var length = value.Length;
|
|
if (length <= BinaryTypeCode.FixStrMaxLength)
|
|
{
|
|
// For short strings, use direct ASCII copy (avoids double validation)
|
|
output.WriteFixStrDirect(value);
|
|
return;
|
|
}
|
|
|
|
// Long strings - standard encoding
|
|
output.WriteByte(BinaryTypeCode.String);
|
|
output.WriteStringUtf8(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteByteArray<TOutput>(byte[] value, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
output.WriteByte(BinaryTypeCode.ByteArray);
|
|
output.WriteVarUInt((uint)value.Length);
|
|
output.WriteBytes(value);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Complex Type Writers
|
|
|
|
private static void WriteObject<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, bool isNested = false)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
var output = context.Output;
|
|
var metadata = wrapper.Metadata;
|
|
|
|
// Wire format:
|
|
// - UseMetadata=false: [Object][props...]
|
|
// - UseMetadata=true, első: [ObjectWithMetadata][propNameHash (4b)][propCount (VarUInt)][hash0..N][props...]
|
|
// - UseMetadata=true, ismételt: [ObjectWithMetadata][propNameHash (4b)][props...]
|
|
// ObjectRef: [ObjectRef][cacheIndex]
|
|
|
|
// UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking)
|
|
var isFirstMetadataOccurrence = false;
|
|
if (context.UseMetadata)
|
|
{
|
|
isFirstMetadataOccurrence = BinarySerializationContext<TOutput>.RegisterMetadataType(wrapper);
|
|
}
|
|
|
|
// Reference handling: lookup entry from scan pass, check IsFirstWrite
|
|
var cachedObjectCacheIndex = -1; // -1 = not cached, 0+ = cache index for first write
|
|
if (context.UseTypeReferenceHandling(metadata))
|
|
{
|
|
// Lookup by Id (IId types) or by object identity hash (non-IId types)
|
|
// Both use IdAccessorType.Int32 - for non-IId, RefIdGetterInt32 returns RuntimeHelpers.GetHashCode
|
|
switch (metadata.IdAccessorType)
|
|
{
|
|
case IdAccessorType.Int32:
|
|
{
|
|
var id = wrapper.RefIdGetterInt32!(value);
|
|
// For IId: skip default Id (0). For non-IId (hash): hash is never 0 for valid objects
|
|
if ((!metadata.IsIId || id != 0) && wrapper.TryGetEntryInt32(id, out var slotIndex))
|
|
{
|
|
ref var entry = ref wrapper.GetEntryRefInt32(slotIndex);
|
|
if (entry.CacheIndex >= 0)
|
|
{
|
|
if (entry.IsFirstWrite)
|
|
{
|
|
entry.IsFirstWrite = false;
|
|
cachedObjectCacheIndex = entry.CacheIndex;
|
|
}
|
|
else
|
|
{
|
|
// 2+ occurrence → write ObjectRef
|
|
output.WriteByte(BinaryTypeCode.ObjectRef);
|
|
output.WriteVarUInt((uint)entry.CacheIndex);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case IdAccessorType.Int64:
|
|
{
|
|
var id = wrapper.RefIdGetterInt64!(value);
|
|
if (id != 0 && wrapper.TryGetEntryInt64(id, out var slotIndex))
|
|
{
|
|
ref var entry = ref wrapper.GetEntryRefInt64(slotIndex);
|
|
if (entry.CacheIndex >= 0)
|
|
{
|
|
if (entry.IsFirstWrite)
|
|
{
|
|
entry.IsFirstWrite = false;
|
|
cachedObjectCacheIndex = entry.CacheIndex;
|
|
}
|
|
else
|
|
{
|
|
output.WriteByte(BinaryTypeCode.ObjectRef);
|
|
output.WriteVarUInt((uint)entry.CacheIndex);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case IdAccessorType.Guid:
|
|
{
|
|
var id = wrapper.RefIdGetterGuid!(value);
|
|
if (id != Guid.Empty && wrapper.TryGetEntryGuid(id, out var slotIndex))
|
|
{
|
|
ref var entry = ref wrapper.GetEntryRefGuid(slotIndex);
|
|
if (entry.CacheIndex >= 0)
|
|
{
|
|
if (entry.IsFirstWrite)
|
|
{
|
|
entry.IsFirstWrite = false;
|
|
cachedObjectCacheIndex = entry.CacheIndex;
|
|
}
|
|
else
|
|
{
|
|
output.WriteByte(BinaryTypeCode.ObjectRef);
|
|
output.WriteVarUInt((uint)entry.CacheIndex);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Marker kiírása:
|
|
// - Cached object first occurrence: ObjectRefFirst/ObjectWithMetadataRefFirst + cacheIndex
|
|
// - Non-cached: Object/ObjectWithMetadata
|
|
if (context.UseMetadata)
|
|
{
|
|
if (cachedObjectCacheIndex >= 0)
|
|
{
|
|
output.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);
|
|
output.WriteVarUInt((uint)cachedObjectCacheIndex);
|
|
}
|
|
else
|
|
{
|
|
output.WriteByte(BinaryTypeCode.ObjectWithMetadata);
|
|
}
|
|
context.WriteInlineMetadata(wrapper.Metadata, output, isFirstMetadataOccurrence);
|
|
}
|
|
else
|
|
{
|
|
if (cachedObjectCacheIndex >= 0)
|
|
{
|
|
output.WriteByte(BinaryTypeCode.ObjectRefFirst);
|
|
output.WriteVarUInt((uint)cachedObjectCacheIndex);
|
|
}
|
|
else
|
|
{
|
|
output.WriteByte(BinaryTypeCode.Object);
|
|
}
|
|
}
|
|
|
|
// Write all properties (startIndex=0, including Id for IId types)
|
|
var nextDepth = depth + 1;
|
|
var properties = metadata.Properties;
|
|
var propCount = properties.Length;
|
|
var hasPropertyFilter = context.HasPropertyFilter;
|
|
|
|
if (!context.UseMetadata)
|
|
{
|
|
// Markerless loop: no extra branching per property for the common case.
|
|
// Properties with ExpectedTypeCode write raw values (no type marker, no skip).
|
|
// Properties without ExpectedTypeCode (bool, enum, string, object) use the standard path.
|
|
for (var i = 0; i < propCount; i++)
|
|
{
|
|
var prop = properties[i];
|
|
|
|
if (prop.ExpectedTypeCode.HasValue)
|
|
{
|
|
WritePropertyMarkerless(value, prop, output);
|
|
}
|
|
else if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
|
{
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
}
|
|
else
|
|
{
|
|
WritePropertyOrSkip(value, prop, output, context, nextDepth);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// UseMetadata=true loop — UNCHANGED, zero extra overhead
|
|
for (var i = 0; i < propCount; i++)
|
|
{
|
|
var prop = properties[i];
|
|
|
|
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
|
{
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
continue;
|
|
}
|
|
|
|
WritePropertyOrSkip(value, prop, output, context, nextDepth);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a property value is null or default without boxing for value types.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool IsPropertyDefaultOrNull(object obj, BinaryPropertyAccessor prop)
|
|
{
|
|
switch (prop.AccessorType)
|
|
{
|
|
case PropertyAccessorType.Int32:
|
|
return prop.GetInt32(obj) == 0;
|
|
case PropertyAccessorType.Int64:
|
|
return prop.GetInt64(obj) == 0L;
|
|
case PropertyAccessorType.Boolean:
|
|
return !prop.GetBoolean(obj);
|
|
case PropertyAccessorType.Double:
|
|
return prop.GetDouble(obj) == 0.0;
|
|
case PropertyAccessorType.Single:
|
|
return prop.GetSingle(obj) == 0f;
|
|
case PropertyAccessorType.Decimal:
|
|
return prop.GetDecimal(obj) == 0m;
|
|
case PropertyAccessorType.Byte:
|
|
return prop.GetByte(obj) == 0;
|
|
case PropertyAccessorType.Int16:
|
|
return prop.GetInt16(obj) == 0;
|
|
case PropertyAccessorType.UInt16:
|
|
return prop.GetUInt16(obj) == 0;
|
|
case PropertyAccessorType.UInt32:
|
|
return prop.GetUInt32(obj) == 0;
|
|
case PropertyAccessorType.UInt64:
|
|
return prop.GetUInt64(obj) == 0;
|
|
case PropertyAccessorType.Guid:
|
|
return prop.GetGuid(obj) == Guid.Empty;
|
|
case PropertyAccessorType.Enum:
|
|
return prop.GetEnumAsInt32(obj) == 0;
|
|
case PropertyAccessorType.DateTime:
|
|
// DateTime default is not typically skipped
|
|
return false;
|
|
default:
|
|
// Object type - use regular getter
|
|
var value = prop.GetValue(obj);
|
|
if (value == null) return true;
|
|
if (prop.PropertyTypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a property value using typed getters to avoid boxing.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WritePropertyValue<TOutput>(object obj, BinaryPropertyAccessor prop, TOutput output, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
switch (prop.AccessorType)
|
|
{
|
|
case PropertyAccessorType.Int32:
|
|
WriteInt32(prop.GetInt32(obj), output);
|
|
return;
|
|
case PropertyAccessorType.Int64:
|
|
WriteInt64(prop.GetInt64(obj), output);
|
|
return;
|
|
case PropertyAccessorType.Boolean:
|
|
output.WriteByte(prop.GetBoolean(obj) ? BinaryTypeCode.True : BinaryTypeCode.False);
|
|
return;
|
|
case PropertyAccessorType.Double:
|
|
WriteFloat64Unsafe(prop.GetDouble(obj), output);
|
|
return;
|
|
case PropertyAccessorType.Single:
|
|
WriteFloat32Unsafe(prop.GetSingle(obj), output);
|
|
return;
|
|
case PropertyAccessorType.Decimal:
|
|
WriteDecimalUnsafe(prop.GetDecimal(obj), output);
|
|
return;
|
|
case PropertyAccessorType.DateTime:
|
|
WriteDateTimeUnsafe(prop.GetDateTime(obj), output);
|
|
return;
|
|
case PropertyAccessorType.Byte:
|
|
output.WriteByte(BinaryTypeCode.UInt8);
|
|
output.WriteByte(prop.GetByte(obj));
|
|
return;
|
|
case PropertyAccessorType.Int16:
|
|
WriteInt16Unsafe(prop.GetInt16(obj), output);
|
|
return;
|
|
case PropertyAccessorType.UInt16:
|
|
WriteUInt16Unsafe(prop.GetUInt16(obj), output);
|
|
return;
|
|
case PropertyAccessorType.UInt32:
|
|
WriteUInt32(prop.GetUInt32(obj), output);
|
|
return;
|
|
case PropertyAccessorType.UInt64:
|
|
WriteUInt64(prop.GetUInt64(obj), output);
|
|
return;
|
|
case PropertyAccessorType.Guid:
|
|
WriteGuidUnsafe(prop.GetGuid(obj), output);
|
|
return;
|
|
case PropertyAccessorType.Enum:
|
|
var enumValue = prop.GetEnumAsInt32(obj);
|
|
if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny))
|
|
{
|
|
output.WriteByte(BinaryTypeCode.Enum);
|
|
output.WriteByte(tiny);
|
|
}
|
|
else
|
|
{
|
|
output.WriteByte(BinaryTypeCode.Enum);
|
|
output.WriteByte(BinaryTypeCode.Int32);
|
|
output.WriteVarInt(enumValue);
|
|
}
|
|
return;
|
|
case PropertyAccessorType.String:
|
|
{
|
|
// Fast path: typed getter, no boxing, no Type.GetTypeCode() call
|
|
var strValue = prop.GetString(obj);
|
|
if (strValue != null)
|
|
WriteString(strValue, output, context);
|
|
else
|
|
output.WriteByte(BinaryTypeCode.Null);
|
|
return;
|
|
}
|
|
default:
|
|
// Fallback to object getter for reference types
|
|
var value = prop.GetValue(obj);
|
|
WriteValue(value, prop.PropertyType, context, depth);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a property value OR a skip marker if the value is default/null.
|
|
/// Single-pass optimization: checks default + writes value in one operation.
|
|
/// Avoids double getter calls.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WritePropertyOrSkip<TOutput>(object obj, BinaryPropertyAccessor prop, TOutput output, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
//var output = context.Output;
|
|
switch (prop.AccessorType)
|
|
{
|
|
case PropertyAccessorType.Int32:
|
|
{
|
|
int value = prop.GetInt32(obj);
|
|
if (value == 0)
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteInt32(value, output);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Int64:
|
|
{
|
|
long value = prop.GetInt64(obj);
|
|
if (value == 0L)
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteInt64(value, output);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Boolean:
|
|
{
|
|
bool value = prop.GetBoolean(obj);
|
|
if (!value)
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
output.WriteByte(BinaryTypeCode.True);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Double:
|
|
{
|
|
double value = prop.GetDouble(obj);
|
|
if (value == 0.0)
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteFloat64Unsafe(value, output);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Single:
|
|
{
|
|
float value = prop.GetSingle(obj);
|
|
if (value == 0f)
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteFloat32Unsafe(value, output);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Decimal:
|
|
{
|
|
decimal value = prop.GetDecimal(obj);
|
|
if (value == 0m)
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteDecimalUnsafe(value, output);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.DateTime:
|
|
{
|
|
DateTime value = prop.GetDateTime(obj);
|
|
// DateTime always written (no default skip)
|
|
WriteDateTimeUnsafe(value, output);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Byte:
|
|
{
|
|
byte value = prop.GetByte(obj);
|
|
if (value == 0)
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
{
|
|
output.WriteByte(BinaryTypeCode.UInt8);
|
|
output.WriteByte(value);
|
|
}
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Int16:
|
|
{
|
|
short value = prop.GetInt16(obj);
|
|
if (value == 0)
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteInt16Unsafe(value, output);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.UInt16:
|
|
{
|
|
ushort value = prop.GetUInt16(obj);
|
|
if (value == 0)
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteUInt16Unsafe(value, output);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.UInt32:
|
|
{
|
|
uint value = prop.GetUInt32(obj);
|
|
if (value == 0)
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteUInt32(value, output);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.UInt64:
|
|
{
|
|
ulong value = prop.GetUInt64(obj);
|
|
if (value == 0)
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteUInt64(value, output);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Guid:
|
|
{
|
|
Guid value = prop.GetGuid(obj);
|
|
if (value == Guid.Empty)
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteGuidUnsafe(value, output);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Enum:
|
|
{
|
|
int enumValue = prop.GetEnumAsInt32(obj);
|
|
if (enumValue == 0)
|
|
{
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
}
|
|
else if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny))
|
|
{
|
|
output.WriteByte(BinaryTypeCode.Enum);
|
|
output.WriteByte(tiny);
|
|
}
|
|
else
|
|
{
|
|
output.WriteByte(BinaryTypeCode.Enum);
|
|
output.WriteByte(BinaryTypeCode.Int32);
|
|
output.WriteVarInt(enumValue);
|
|
}
|
|
return;
|
|
}
|
|
case PropertyAccessorType.String:
|
|
{
|
|
// Fast path: typed getter, no boxing, no Type.GetTypeCode() call
|
|
string? value = prop.GetString(obj);
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
output.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty);
|
|
}
|
|
else
|
|
{
|
|
#if DEBUG
|
|
context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}";
|
|
#endif
|
|
WriteString(value, output, context);
|
|
}
|
|
return;
|
|
}
|
|
default:
|
|
{
|
|
// Object type (collection, complex object, byte[], dictionary)
|
|
// TryWritePrimitive is always false for these — skip it via WriteValueNonPrimitive
|
|
var value = prop.GetValue(obj);
|
|
|
|
// SKIP marker only for null (reference types)
|
|
// Empty string, empty collections, etc. are valid values and must be written!
|
|
if (value == null)
|
|
{
|
|
output.WriteByte(BinaryTypeCode.PropertySkip);
|
|
}
|
|
else
|
|
{
|
|
#if DEBUG
|
|
context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}";
|
|
#endif
|
|
WriteValueNonPrimitive(value, prop.PropertyType, output, context, depth);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a property value without type marker byte (markerless mode, UseMetadata=false).
|
|
/// All values are written including defaults — no PropertySkip markers.
|
|
/// Only called for non-nullable value types with ExpectedTypeCode set.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WritePropertyMarkerless<TOutput>(object obj, BinaryPropertyAccessor prop, TOutput output)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
switch (prop.AccessorType)
|
|
{
|
|
case PropertyAccessorType.Int32:
|
|
output.WriteVarInt(prop.GetInt32(obj));
|
|
return;
|
|
case PropertyAccessorType.Int64:
|
|
output.WriteVarLong(prop.GetInt64(obj));
|
|
return;
|
|
case PropertyAccessorType.Double:
|
|
output.WriteRaw(prop.GetDouble(obj));
|
|
return;
|
|
case PropertyAccessorType.Single:
|
|
output.WriteRaw(prop.GetSingle(obj));
|
|
return;
|
|
case PropertyAccessorType.Decimal:
|
|
output.WriteDecimalBits(prop.GetDecimal(obj));
|
|
return;
|
|
case PropertyAccessorType.DateTime:
|
|
output.WriteDateTimeBits(prop.GetDateTime(obj));
|
|
return;
|
|
case PropertyAccessorType.Guid:
|
|
output.WriteGuidBits(prop.GetGuid(obj));
|
|
return;
|
|
case PropertyAccessorType.Byte:
|
|
output.WriteByte(prop.GetByte(obj));
|
|
return;
|
|
case PropertyAccessorType.Int16:
|
|
output.WriteRaw(prop.GetInt16(obj));
|
|
return;
|
|
case PropertyAccessorType.UInt16:
|
|
output.WriteRaw(prop.GetUInt16(obj));
|
|
return;
|
|
case PropertyAccessorType.UInt32:
|
|
output.WriteVarUInt(prop.GetUInt32(obj));
|
|
return;
|
|
case PropertyAccessorType.UInt64:
|
|
output.WriteVarULong(prop.GetUInt64(obj));
|
|
return;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Specialized Array Writers
|
|
|
|
/// <summary>
|
|
/// Optimized array writer with specialized paths for primitive arrays.
|
|
/// </summary>
|
|
private static void WriteArray<TOutput>(IEnumerable enumerable, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
var output = context.Output;
|
|
output.WriteByte(BinaryTypeCode.Array);
|
|
var nextDepth = depth + 1;
|
|
|
|
// Use pre-computed metadata — no GetWrapper or GetCollectionElementType needed
|
|
var metadata = wrapper.Metadata;
|
|
var elementType = metadata.CollectionElementType;
|
|
|
|
// Optimized path for primitive arrays
|
|
if (elementType != null && metadata.SourceType.IsArray)
|
|
{
|
|
if (TryWritePrimitiveArray(enumerable, elementType, output, context))
|
|
return;
|
|
}
|
|
|
|
// For IList, we can write the count directly
|
|
if (enumerable is IList list)
|
|
{
|
|
var count = list.Count;
|
|
output.WriteVarUInt((uint)count);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var item = list[i];
|
|
var itemType = item?.GetType() ?? typeof(object);
|
|
WriteValue(item, itemType, context, nextDepth);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// For other IEnumerable, collect first
|
|
var items = new List<object?>();
|
|
foreach (var item in enumerable)
|
|
{
|
|
items.Add(item);
|
|
}
|
|
|
|
output.WriteVarUInt((uint)items.Count);
|
|
foreach (var item in items)
|
|
{
|
|
var itemType = item?.GetType() ?? typeof(object);
|
|
WriteValue(item, itemType, context, nextDepth);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Specialized array writer for primitive arrays using bulk memory operations.
|
|
/// Non-generic to avoid JIT code duplication — virtual dispatch cost is negligible
|
|
/// because this is called once per array, not per element.
|
|
/// </summary>
|
|
private static bool TryWritePrimitiveArray<TOutput>(IEnumerable enumerable, Type elementType, TOutput output, BinarySerializationContext<TOutput> context)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
// String array needs context for interning — keep generic path
|
|
if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray)
|
|
{
|
|
output.WriteVarUInt((uint)stringArray.Length);
|
|
for (var i = 0; i < stringArray.Length; i++)
|
|
{
|
|
var s = stringArray[i];
|
|
if (s == null)
|
|
output.WriteByte(BinaryTypeCode.Null);
|
|
else
|
|
WriteString(s, output, context);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// All other primitive arrays don't need context — dispatch through base class
|
|
return TryWritePrimitiveArrayCore(enumerable, elementType, output);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Non-generic core for primitive array writes. Only compiled once by JIT (not per TOutput).
|
|
/// Virtual dispatch on BinaryOutputBase is negligible: one vtable lookup per array, not per element.
|
|
/// </summary>
|
|
private static bool TryWritePrimitiveArrayCore(IEnumerable enumerable, Type elementType, BinaryOutputBase output)
|
|
{
|
|
// Int32 array - very common case
|
|
if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray)
|
|
{
|
|
output.WriteVarUInt((uint)intArray.Length);
|
|
output.WriteInt32ArrayOptimized(intArray);
|
|
return true;
|
|
}
|
|
|
|
// Double array - bulk write as raw bytes
|
|
if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray)
|
|
{
|
|
output.WriteVarUInt((uint)doubleArray.Length);
|
|
output.WriteDoubleArrayBulk(doubleArray);
|
|
return true;
|
|
}
|
|
|
|
// Long array
|
|
if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray)
|
|
{
|
|
output.WriteVarUInt((uint)longArray.Length);
|
|
output.WriteLongArrayOptimized(longArray);
|
|
return true;
|
|
}
|
|
|
|
// Float array - bulk write as raw bytes
|
|
if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray)
|
|
{
|
|
output.WriteVarUInt((uint)floatArray.Length);
|
|
output.WriteFloatArrayBulk(floatArray);
|
|
return true;
|
|
}
|
|
|
|
// Bool array - pack as bytes
|
|
if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray)
|
|
{
|
|
output.WriteVarUInt((uint)boolArray.Length);
|
|
for (var i = 0; i < boolArray.Length; i++)
|
|
{
|
|
output.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
|
|
}
|
|
output.WriteVarUInt((uint)boolArray.Length);
|
|
return true;
|
|
}
|
|
|
|
// Guid array - bulk write
|
|
if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray)
|
|
{
|
|
output.WriteVarUInt((uint)guidArray.Length);
|
|
output.WriteGuidArrayBulk(guidArray);
|
|
return true;
|
|
}
|
|
|
|
// Decimal array
|
|
if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray)
|
|
{
|
|
output.WriteVarUInt((uint)decimalArray.Length);
|
|
for (var i = 0; i < decimalArray.Length; i++)
|
|
{
|
|
WriteDecimalUnsafe(decimalArray[i], output);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// DateTime array
|
|
if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray)
|
|
{
|
|
output.WriteVarUInt((uint)dateTimeArray.Length);
|
|
for (var i = 0; i < dateTimeArray.Length; i++)
|
|
{
|
|
WriteDateTimeUnsafe(dateTimeArray[i], output);
|
|
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void WriteDictionary<TOutput>(IDictionary dictionary, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
var output = context.Output;
|
|
output.WriteByte(BinaryTypeCode.Dictionary);
|
|
output.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 Serialization Result
|
|
|
|
// Implementation moved to AcBinarySerializer.BinarySerializationResult.cs
|
|
|
|
#endregion
|
|
|
|
#region Context Pool
|
|
|
|
// Implementation moved to AcBinarySerializer.BinarySerializationContext.cs
|
|
|
|
#endregion
|
|
|
|
#region Serialization Context
|
|
|
|
// Implementation moved to AcBinarySerializer.BinarySerializationContext.cs
|
|
|
|
#endregion
|
|
|
|
#region Type Metadata
|
|
|
|
private static Type? GetCollectionElementType(Type type)
|
|
{
|
|
if (type.IsArray)
|
|
{
|
|
return type.GetElementType();
|
|
}
|
|
|
|
if (type.IsGenericType)
|
|
{
|
|
var args = type.GetGenericArguments();
|
|
if (args.Length == 1)
|
|
{
|
|
return args[0];
|
|
}
|
|
}
|
|
|
|
foreach (var iface in type.GetInterfaces())
|
|
{
|
|
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
|
{
|
|
return iface.GetGenericArguments()[0];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
// Type metadata helpers moved to AcBinarySerializer.BinarySerializeTypeMetadata.cs
|
|
|
|
#endregion
|
|
}
|