2143 lines
85 KiB
C#
2143 lines
85 KiB
C#
using AyCode.Core.Compression;
|
|
using AyCode.Core.Serializers.Expressions;
|
|
using System.Buffers;
|
|
using System.Collections;
|
|
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.Linq.Expressions;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
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 output strategy selection (ArrayBinaryOutput / BufferWriterBinaryOutput)
|
|
/// - Buffer-in-context: _buffer/_position owned by context for zero virtual dispatch on hot path
|
|
/// </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);
|
|
private static readonly Type ShortType = typeof(short);
|
|
private static readonly Type UShortType = typeof(ushort);
|
|
private static readonly Type UIntType = typeof(uint);
|
|
private static readonly Type ULongType = typeof(ulong);
|
|
private static readonly Type ByteType = typeof(byte);
|
|
private static readonly Type SByteType = typeof(sbyte);
|
|
private static readonly Type CharType = typeof(char);
|
|
|
|
#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(4096);
|
|
context.OutputInitialized = true;
|
|
context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);
|
|
|
|
// 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>
|
|
/// Registers a source-generated binary writer for the specified type.
|
|
/// Once registered, WriteObject bypasses the runtime switch/delegate property loop
|
|
/// and calls the generated writer directly — eliminating Func<>.Invoke() overhead.
|
|
/// Call once at startup (e.g., in a static constructor or module initializer).
|
|
/// </summary>
|
|
/// <typeparam name="T">The type to register the writer for.</typeparam>
|
|
/// <param name="writer">The generated writer instance (typically a singleton).</param>
|
|
internal static void RegisterGeneratedWriter<T>(IGeneratedBinaryWriter writer)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(writer);
|
|
GeneratedWriterRegistry.Register(typeof(T), writer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a source-generated binary writer for the specified type.
|
|
/// </summary>
|
|
internal static void RegisterGeneratedWriter(Type type, IGeneratedBinaryWriter writer)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(type);
|
|
ArgumentNullException.ThrowIfNull(writer);
|
|
GeneratedWriterRegistry.Register(type, writer);
|
|
}
|
|
|
|
#region SGen Slot Allocation
|
|
|
|
/// <summary>
|
|
/// Number of runtime wrapper slots reserved for polymorphic type cache (indices 0..RuntimeSlotCount-1).
|
|
/// SGen compile-time slots start at RuntimeSlotCount and above.
|
|
/// Easily modifiable — all code references this constant instead of literal values.
|
|
/// </summary>
|
|
internal const int RuntimeSlotCount = BinaryTypeCode.SlotCount;
|
|
|
|
/// <summary>
|
|
/// Next available wrapper slot index. Starts at RuntimeSlotCount so SGen slots
|
|
/// don't collide with runtime polymorphic slots (0..RuntimeSlotCount-1).
|
|
/// </summary>
|
|
internal static int s_nextWrapperSlot = RuntimeSlotCount + 1;
|
|
|
|
/// <summary>
|
|
/// Allocates a unique slot index for SGen wrapper cache.
|
|
/// Returns RuntimeSlotCount, RuntimeSlotCount+1, RuntimeSlotCount+2, ...
|
|
/// </summary>
|
|
internal static int AllocateWrapperSlot() => Interlocked.Increment(ref s_nextWrapperSlot) - 1;
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Thread-safe registry for generated writers. Looked up once per TypeMetadataWrapper creation.
|
|
/// </summary>
|
|
internal static class GeneratedWriterRegistry
|
|
{
|
|
private static readonly ConcurrentDictionary<Type, IGeneratedBinaryWriter> Writers = new();
|
|
|
|
internal static void Register(Type type, IGeneratedBinaryWriter writer) => Writers[type] = writer;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal static IGeneratedBinaryWriter? TryGet(Type type) =>
|
|
Writers.TryGetValue(type, out var writer) ? writer : null;
|
|
}
|
|
|
|
/// <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. Returns bytes written.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static int 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.OutputInitialized)
|
|
{
|
|
context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity);
|
|
context.OutputInitialized = true;
|
|
}
|
|
context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);
|
|
|
|
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(context._buffer, context._position), options.UseCompression);
|
|
}
|
|
|
|
// No compression - single allocation for result
|
|
return context.Output.ToArray(context._buffer, context._position);
|
|
}
|
|
finally
|
|
{
|
|
if (options.UseAsync) BinarySerializationContextPool<ArrayBinaryOutput>.ReturnAsync(context);
|
|
else BinarySerializationContextPool<ArrayBinaryOutput>.Return(context);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs only the scan pass (ScanForDuplicates) without writing.
|
|
/// For benchmarking scan pass overhead in isolation.
|
|
/// </summary>
|
|
internal static void ScanOnly<T>(T value, AcBinarySerializerOptions options)
|
|
{
|
|
if (value == null) return;
|
|
var runtimeType = value.GetType();
|
|
var context = BinarySerializationContextPool<ArrayBinaryOutput>.Get(options);
|
|
if (!context.OutputInitialized)
|
|
{
|
|
context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity);
|
|
context.OutputInitialized = true;
|
|
}
|
|
try
|
|
{
|
|
ScanForDuplicates(value, runtimeType, context);
|
|
}
|
|
finally
|
|
{
|
|
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 int 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 1;
|
|
}
|
|
|
|
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<BufferWriterBinaryOutput>.Get(options);
|
|
context.Output = new BufferWriterBinaryOutput(writer, options.BufferWriterChunkSize);
|
|
context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);
|
|
|
|
try
|
|
{
|
|
ScanForDuplicates(actualValue, runtimeType, context);
|
|
context.WriteHeader();
|
|
WriteValue(actualValue, runtimeType, context, 0);
|
|
|
|
// Apply compression if enabled
|
|
if (options.UseCompression != Lz4CompressionMode.None)
|
|
{
|
|
context.Output.Flush(context._buffer, context._position);
|
|
throw new NotSupportedException(
|
|
"Compression is not supported with IBufferWriter output. " +
|
|
"Use the byte[] overload or disable compression.");
|
|
}
|
|
|
|
var bytesWritten = context.Output.GetTotalPosition(context._position);
|
|
context.Output.Flush(context._buffer, context._position);
|
|
return bytesWritten;
|
|
}
|
|
finally
|
|
{
|
|
context.Output = default;
|
|
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.OutputInitialized)
|
|
{
|
|
context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity);
|
|
context.OutputInitialized = true;
|
|
}
|
|
context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);
|
|
|
|
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.OutputInitialized)
|
|
{
|
|
context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity);
|
|
context.OutputInitialized = true;
|
|
}
|
|
context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);
|
|
|
|
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(context._buffer, context._position), options.UseCompression);
|
|
return BinarySerializationResult.FromImmutable(compressed);
|
|
}
|
|
|
|
return context.Output.DetachResult(context._buffer, context._position);
|
|
}
|
|
finally
|
|
{
|
|
if (options.UseAsync) BinarySerializationContextPool<ArrayBinaryOutput>.ReturnAsync(context);
|
|
else BinarySerializationContextPool<ArrayBinaryOutput>.Return(context);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Generated Writer Bridge Methods
|
|
|
|
/// <summary>
|
|
/// Bridge for generated writers: writes a non-null complex/collection value directly.
|
|
/// Skips null check (caller handles it) and TryWritePrimitive (caller knows it's complex).
|
|
/// Equivalent to the runtime's WriteValueNonPrimitive path.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal static void WriteValueGenerated<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
WriteValueNonPrimitive(value, type, context, depth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bridge for generated writers to call the runtime WriteString.
|
|
/// Matches WritePropertyOrSkip String case exactly: null → PropertySkip, empty → StringEmpty.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal static void WriteStringGenerated<TOutput>(string? value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
context.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty);
|
|
return;
|
|
}
|
|
|
|
WriteString(value, context);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bridge for generated writers: writes a non-null complex OBJECT directly.
|
|
/// Skips WriteValueNonPrimitive type dispatch (is byte[]?, is IDictionary?, is IEnumerable?, GetWrapper)
|
|
/// because the SrcGen knows at compile-time that the property is a complex object.
|
|
/// Uses pre-resolved wrapper type to avoid GetWrapper dictionary lookup.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal static void WriteObjectGenerated<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
if (depth > context.MaxDepth)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Null);
|
|
return;
|
|
}
|
|
|
|
var wrapper = context.GetWrapper(type);
|
|
WriteObject(value, wrapper, context, depth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bridge for generated writers: writes a non-null complex OBJECT using slot-based wrapper lookup.
|
|
/// SGen types pass their compile-time known slot index — avoids GetWrapper dictionary lookup.
|
|
/// First call per slot per context: populates slot from GetWrapper. Subsequent calls: direct array index.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal static void WriteObjectGenerated<TOutput>(object value, Type type, int wrapperSlot, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
if (depth > context.MaxDepth)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Null);
|
|
return;
|
|
}
|
|
|
|
var wrapper = context.GetWrapper(type, wrapperSlot);
|
|
WriteObject(value, wrapper, context, depth);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Value Writing
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteValue<TOutput>(object? value, Type type, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
if (value == null)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Null);
|
|
return;
|
|
}
|
|
|
|
// Try writing as primitive first
|
|
if (TryWritePrimitive(value, type, context))
|
|
return;
|
|
|
|
WriteValueNonPrimitive(value, type, 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, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
// 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(), context))
|
|
return;
|
|
}
|
|
|
|
if (depth > context.MaxDepth)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Null);
|
|
return;
|
|
}
|
|
|
|
// Handle byte arrays specially (value-like, no reference tracking)
|
|
if (value is byte[] byteArray)
|
|
{
|
|
WriteByteArray(byteArray, context);
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache).
|
|
/// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects.
|
|
/// </summary>
|
|
private static void WriteValueNonPrimitiveWithWrapper<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
var type = wrapper.Metadata.SourceType;
|
|
|
|
// Nullable<T> where T is a value type: boxed value may be a primitive.
|
|
if (type.IsValueType)
|
|
{
|
|
if (TryWritePrimitive(value, value.GetType(), context))
|
|
return;
|
|
}
|
|
|
|
if (depth > context.MaxDepth)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Null);
|
|
return;
|
|
}
|
|
|
|
// Handle byte arrays specially (value-like, no reference tracking)
|
|
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, wrapper, context, depth);
|
|
return;
|
|
}
|
|
|
|
// Handle complex objects
|
|
WriteObject(value, wrapper, context, depth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Polymorphic variant of WriteValueNonPrimitiveWithWrapper.
|
|
/// Cold path: polymorphism is rare. Writes poly prefix for non-object types,
|
|
/// delegates to WriteObjectPolymorphic for combined poly+ref marker handling.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static void WriteValueNonPrimitiveWithWrapperPoly<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, Type polyRuntimeType)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
var type = wrapper.Metadata.SourceType;
|
|
|
|
if (type.IsValueType)
|
|
{
|
|
if (TryWritePrimitive(value, value.GetType(), context))
|
|
return;
|
|
}
|
|
|
|
if (depth > context.MaxDepth)
|
|
{
|
|
context.WritePolymorphicPrefix(polyRuntimeType);
|
|
context.WriteByte(BinaryTypeCode.Null);
|
|
return;
|
|
}
|
|
|
|
if (value is byte[] byteArray)
|
|
{
|
|
context.WritePolymorphicPrefix(polyRuntimeType);
|
|
WriteByteArray(byteArray, context);
|
|
return;
|
|
}
|
|
|
|
if (value is IDictionary dictionary)
|
|
{
|
|
context.WritePolymorphicPrefix(polyRuntimeType);
|
|
WriteDictionary(dictionary, context, depth);
|
|
return;
|
|
}
|
|
|
|
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
|
{
|
|
context.WritePolymorphicPrefix(polyRuntimeType);
|
|
WriteArray(enumerable, wrapper, context, depth);
|
|
return;
|
|
}
|
|
|
|
// Complex object — handles combined poly+ref markers
|
|
WriteObjectPolymorphic(value, wrapper, context, depth, polyRuntimeType);
|
|
}
|
|
|
|
/// <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, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
// Fast path: check TypeCode first (handles most primitives)
|
|
var typeCode = Type.GetTypeCode(type);
|
|
|
|
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:
|
|
WriteFloat64Unsafe((double)value, context);
|
|
return true;
|
|
case TypeCode.String:
|
|
WriteString((string)value, context);
|
|
return true;
|
|
case TypeCode.Single:
|
|
WriteFloat32Unsafe((float)value, context);
|
|
return true;
|
|
case TypeCode.Decimal:
|
|
WriteDecimalUnsafe((decimal)value, context);
|
|
return true;
|
|
case TypeCode.DateTime:
|
|
WriteDateTimeUnsafe((DateTime)value, context);
|
|
return true;
|
|
case TypeCode.Byte:
|
|
context.WriteByte(BinaryTypeCode.UInt8);
|
|
context.WriteByte((byte)value);
|
|
return true;
|
|
case TypeCode.Int16:
|
|
WriteInt16Unsafe((short)value, context);
|
|
return true;
|
|
case TypeCode.UInt16:
|
|
WriteUInt16Unsafe((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:
|
|
WriteCharUnsafe((char)value, context);
|
|
return true;
|
|
}
|
|
|
|
// Handle special types by reference comparison (faster than type equality)
|
|
if (ReferenceEquals(type, GuidType))
|
|
{
|
|
WriteGuidUnsafe((Guid)value, context);
|
|
return true;
|
|
}
|
|
if (ReferenceEquals(type, DateTimeOffsetType))
|
|
{
|
|
WriteDateTimeOffsetUnsafe((DateTimeOffset)value, context);
|
|
return true;
|
|
}
|
|
if (ReferenceEquals(type, TimeSpanType))
|
|
{
|
|
WriteTimeSpanUnsafe((TimeSpan)value, context);
|
|
return true;
|
|
}
|
|
if (type.IsEnum)
|
|
{
|
|
WriteEnum(value, context);
|
|
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(), context);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Optimized Primitive Writers using MemoryMarshal
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteInt32<TOutput>(int value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
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<TOutput>(long value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
if (value >= int.MinValue && value <= int.MaxValue)
|
|
{
|
|
WriteInt32((int)value, context);
|
|
return;
|
|
}
|
|
context.WriteByte(BinaryTypeCode.Int64);
|
|
context.WriteVarLong(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized float64 writer using batched write.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteFloat64Unsafe<TOutput>(double value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteTypeCodeAndRaw(BinaryTypeCode.Float64, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized float32 writer using batched write.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteFloat32Unsafe<TOutput>(float value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.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, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Decimal);
|
|
context.WriteDecimalBits(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized DateTime writer.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteDateTimeUnsafe<TOutput>(DateTime value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteByte(BinaryTypeCode.DateTime);
|
|
context.WriteDateTimeBits(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized Guid writer using direct memory copy.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteGuidUnsafe<TOutput>(Guid value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Guid);
|
|
context.WriteGuidBits(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized DateTimeOffset writer.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteDateTimeOffsetUnsafe<TOutput>(DateTimeOffset value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteByte(BinaryTypeCode.DateTimeOffset);
|
|
context.WriteDateTimeOffsetBits(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized TimeSpan writer.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteTimeSpanUnsafe<TOutput>(TimeSpan value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteTypeCodeAndRaw(BinaryTypeCode.TimeSpan, value.Ticks);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteInt16Unsafe<TOutput>(short value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteTypeCodeAndRaw(BinaryTypeCode.Int16, value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteUInt16Unsafe<TOutput>(ushort value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteTypeCodeAndRaw(BinaryTypeCode.UInt16, value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteUInt32<TOutput>(uint value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteByte(BinaryTypeCode.UInt32);
|
|
context.WriteVarUInt(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteUInt64<TOutput>(ulong value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteByte(BinaryTypeCode.UInt64);
|
|
context.WriteVarULong(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteCharUnsafe<TOutput>(char value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Char);
|
|
context.WriteRaw(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteEnum<TOutput>(object value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
// Use direct unboxing instead of Convert.ToInt32 to avoid NumberFormatInfo overhead
|
|
var intValue = GetEnumAsInt32Fast(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);
|
|
}
|
|
|
|
/// <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.
|
|
/// Uses pre-computed WriteDuplicateEntry cursor for interning (no IdentityMap lookup).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteString<TOutput>(string value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
if (value.Length == 0)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.StringEmpty);
|
|
return;
|
|
}
|
|
|
|
// Read and immediately reset — prevents flag from leaking to subsequent WriteString calls
|
|
// (e.g. from TryWritePrimitive, WriteDictionary, or when IsValidForInterningString is false)
|
|
var internEligible = context.StringInternEligible;
|
|
context.StringInternEligible = false;
|
|
|
|
if (internEligible && context.IsValidForInterningString(value.Length))
|
|
{
|
|
if (context.TryConsumeWritePlanEntry(out var planEntry))
|
|
{
|
|
ValidateWritePlanString(in planEntry, value);
|
|
if (planEntry.IsFirst)
|
|
{
|
|
// StringFirst: write interned string + cache index + data (Value holds the string)
|
|
context.WriteByte(BinaryTypeCode.StringInternFirst);
|
|
context.WriteVarUInt((uint)planEntry.CacheMapIndex);
|
|
context.WriteStringUtf8(planEntry.Value ?? value);
|
|
}
|
|
else
|
|
{
|
|
WriteStringInternRef(context, planEntry.CacheMapIndex);
|
|
}
|
|
return;
|
|
}
|
|
// No plan entry → single occurrence, fall through to FixStr/String path
|
|
#if DEBUG
|
|
context.OnStringInterned?.Invoke(null, value);
|
|
#endif
|
|
}
|
|
|
|
// FastWire: skip FixStr optimization (UTF-8 specific), write String marker + UTF-16 data
|
|
if (context.FastWire)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.String);
|
|
context.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)
|
|
context.WriteFixStrDirect(value);
|
|
return;
|
|
}
|
|
|
|
// Long strings - standard encoding
|
|
context.WriteByte(BinaryTypeCode.String);
|
|
context.WriteStringUtf8(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteByteArray<TOutput>(byte[] value, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteByte(BinaryTypeCode.ByteArray);
|
|
context.WriteVarUInt((uint)value.Length);
|
|
context.WriteBytes(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// String intern 2nd occurrence — cold path, just writes reference index.
|
|
/// </summary>
|
|
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteStringInternRef<TOutput>(BinarySerializationContext<TOutput> context, int cacheMapIndex)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteByte(BinaryTypeCode.StringInterned);
|
|
context.WriteVarUInt((uint)cacheMapIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Object ref 2nd occurrence — cold path, just writes reference index.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteObjectRef<TOutput>(BinarySerializationContext<TOutput> context, int cacheMapIndex)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.WriteByte(BinaryTypeCode.ObjectRef);
|
|
context.WriteVarUInt((uint)cacheMapIndex);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Complex Type Writers
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteObject<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
var metadata = wrapper.Metadata;
|
|
var useMetaForType = context.UseMetadata && metadata.EnableMetadataFeature;
|
|
|
|
// Only IId types with ref handling enabled go to cold path
|
|
if (context.UseTypeReferenceHandling(metadata))
|
|
{
|
|
if (useMetaForType)
|
|
WriteObjectWithRefHandlingMeta(value, wrapper, context, depth);
|
|
else
|
|
WriteObjectWithRefHandling(value, wrapper, context, depth);
|
|
return;
|
|
}
|
|
|
|
if (useMetaForType)
|
|
{
|
|
// UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking)
|
|
var isFirstMetadataOccurrence = BinarySerializationContext<TOutput>.RegisterMetadataType(wrapper);
|
|
|
|
// Marker kiírása — no ref handling, no cachedObjectCacheIndex
|
|
context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
|
|
context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence);
|
|
}
|
|
else
|
|
{
|
|
// FixObj: assign slot on first occurrence this session
|
|
if (!wrapper.PolymorphicSeen)
|
|
{
|
|
wrapper.PolymorphicSeen = true;
|
|
wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
|
|
}
|
|
if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
|
|
context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
|
|
else
|
|
context.WriteByte(BinaryTypeCode.Object);
|
|
}
|
|
|
|
WriteObjectProperties(value, wrapper, context, depth, useMetaForType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// WriteObject variant with reference handling, no metadata.
|
|
/// Cold path: only IId types with ref tracking enabled.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteObjectWithRefHandling<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
// Reference handling: consume pre-computed write plan entry from scan pass cursor
|
|
var cachedObjectCacheIndex = -1;
|
|
if (context.TryConsumeWritePlanEntry(out var planEntry))
|
|
{
|
|
ValidateWritePlanObject(in planEntry, value, wrapper);
|
|
if (planEntry.IsFirst)
|
|
{
|
|
cachedObjectCacheIndex = planEntry.CacheMapIndex;
|
|
}
|
|
else
|
|
{
|
|
WriteObjectRef(context, planEntry.CacheMapIndex);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Marker kiírása — no metadata
|
|
if (cachedObjectCacheIndex >= 0)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.ObjectRefFirst);
|
|
context.WriteVarUInt((uint)cachedObjectCacheIndex);
|
|
}
|
|
else
|
|
{
|
|
if (!wrapper.PolymorphicSeen)
|
|
{
|
|
wrapper.PolymorphicSeen = true;
|
|
wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
|
|
}
|
|
if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
|
|
context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
|
|
else
|
|
context.WriteByte(BinaryTypeCode.Object);
|
|
}
|
|
|
|
WriteObjectProperties(value, wrapper, context, depth, useMetaForType: false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// WriteObject variant with reference handling + metadata.
|
|
/// Cold path: IId types with ref tracking + UseMetadata enabled.
|
|
/// </summary>
|
|
//[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static void WriteObjectWithRefHandlingMeta<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
var isFirstMetadataOccurrence = BinarySerializationContext<TOutput>.RegisterMetadataType(wrapper);
|
|
|
|
// Reference handling: consume pre-computed write plan entry from scan pass cursor
|
|
var cachedObjectCacheIndex = -1;
|
|
if (context.TryConsumeWritePlanEntry(out var planEntry))
|
|
{
|
|
ValidateWritePlanObject(in planEntry, value, wrapper);
|
|
if (planEntry.IsFirst)
|
|
{
|
|
cachedObjectCacheIndex = planEntry.CacheMapIndex;
|
|
}
|
|
else
|
|
{
|
|
WriteObjectRef(context, planEntry.CacheMapIndex);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Marker kiírása — with metadata
|
|
if (cachedObjectCacheIndex >= 0)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);
|
|
context.WriteVarUInt((uint)cachedObjectCacheIndex);
|
|
}
|
|
else
|
|
{
|
|
context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
|
|
}
|
|
context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence);
|
|
|
|
WriteObjectProperties(value, wrapper, context, depth, useMetaForType: true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shared property writing loop — used by WriteObject, WriteObjectWithRefHandling, WriteObjectPolymorphic.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteObjectProperties<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, bool useMetaForType)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
var nextDepth = depth + 1;
|
|
|
|
if (context.UseGeneratedCode)
|
|
{
|
|
var generatedWriter = wrapper.GeneratedWriter;
|
|
if (generatedWriter != null)
|
|
{
|
|
generatedWriter.WriteProperties(value, context, nextDepth);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!useMetaForType)
|
|
{
|
|
WritePropertiesMarkerless(value, wrapper, context, nextDepth);
|
|
}
|
|
else
|
|
{
|
|
WritePropertiesWithMeta(value, wrapper, context, nextDepth);
|
|
}
|
|
}
|
|
|
|
private static void WritePropertiesWithMeta<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int nextDepth) where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
var properties = wrapper.Metadata.Properties;
|
|
var propCount = properties.Length;
|
|
var hasPropertyFilter = context.HasPropertyFilter;
|
|
|
|
for (var i = 0; i < propCount; i++)
|
|
{
|
|
var prop = properties[i];
|
|
|
|
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
|
{
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
continue;
|
|
}
|
|
|
|
WritePropertyOrSkip(value, prop, wrapper, context, nextDepth);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WritePropertiesMarkerless<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int nextDepth) where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
var properties = wrapper.Metadata.Properties;
|
|
var propCount = properties.Length;
|
|
var hasPropertyFilter = context.HasPropertyFilter;
|
|
|
|
for (var i = 0; i < propCount; i++)
|
|
{
|
|
var prop = properties[i];
|
|
|
|
if (!prop.ExpectedTypeCode.HasValue && hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
|
{
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
}
|
|
else
|
|
{
|
|
WritePropertyOrSkip(value, prop, wrapper, context, nextDepth);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Polymorphic marker writing — extracted from WriteObject to keep hot path small.
|
|
/// Cold path: polymorphism is rare, NoInlining call overhead acceptable.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static void WritePolymorphicMarker<TOutput>(BinarySerializationContext<TOutput> context, Type polyRuntimeType, int cachedObjectCacheIndex)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
if (cachedObjectCacheIndex >= 0)
|
|
{
|
|
// Combined poly + RefFirst marker (69/71)
|
|
context.WritePolymorphicPrefix(polyRuntimeType, cachedObjectCacheIndex);
|
|
}
|
|
else
|
|
{
|
|
var rtWrapper = context.GetWrapper(polyRuntimeType);
|
|
if (rtWrapper.PolymorphicSeen && rtWrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
|
|
{
|
|
// 2+ poly in this session → FixObj (1 byte)
|
|
context.WriteByte((byte)rtWrapper.PolymorphicCacheIndex);
|
|
}
|
|
else
|
|
{
|
|
// First poly in this session → ObjectWithTypeName + assigns slot
|
|
context.WritePolymorphicPrefix(polyRuntimeType);
|
|
context.WriteByte(BinaryTypeCode.Object);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Polymorphic object writing — handles combined poly+ref markers.
|
|
/// Cold path: polymorphism is rare, NoInlining acceptable.
|
|
/// Poly always implies UseMetadata=false (checked in WritePropertyOrSkip).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static void WriteObjectPolymorphic<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, Type polyRuntimeType)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
var metadata = wrapper.Metadata;
|
|
|
|
// Reference handling
|
|
var cachedObjectCacheIndex = -1;
|
|
if (context.UseTypeReferenceHandling(metadata))
|
|
{
|
|
if (context.TryConsumeWritePlanEntry(out var planEntry))
|
|
{
|
|
ValidateWritePlanObject(in planEntry, value, wrapper);
|
|
if (planEntry.IsFirst)
|
|
{
|
|
cachedObjectCacheIndex = planEntry.CacheMapIndex;
|
|
}
|
|
else
|
|
{
|
|
WriteObjectRef(context, planEntry.CacheMapIndex);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Poly marker (handles combined poly+ref)
|
|
WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex);
|
|
|
|
WriteObjectProperties(value, wrapper, context, depth, false);
|
|
}
|
|
|
|
/// <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, BinarySerializationContext<TOutput> context, int depth)
|
|
// where TOutput : struct, IBinaryOutputBase
|
|
//{
|
|
// switch (prop.AccessorType)
|
|
// {
|
|
// case PropertyAccessorType.Int32:
|
|
// WriteInt32(prop.GetInt32(obj), context);
|
|
// return;
|
|
// case PropertyAccessorType.Int64:
|
|
// WriteInt64(prop.GetInt64(obj), context);
|
|
// return;
|
|
// case PropertyAccessorType.Boolean:
|
|
// context.WriteByte(prop.GetBoolean(obj) ? BinaryTypeCode.True : BinaryTypeCode.False);
|
|
// return;
|
|
// case PropertyAccessorType.Double:
|
|
// WriteFloat64Unsafe(prop.GetDouble(obj), context);
|
|
// return;
|
|
// case PropertyAccessorType.Single:
|
|
// WriteFloat32Unsafe(prop.GetSingle(obj), context);
|
|
// return;
|
|
// case PropertyAccessorType.Decimal:
|
|
// WriteDecimalUnsafe(prop.GetDecimal(obj), context);
|
|
// return;
|
|
// case PropertyAccessorType.DateTime:
|
|
// WriteDateTimeUnsafe(prop.GetDateTime(obj), context);
|
|
// return;
|
|
// case PropertyAccessorType.Byte:
|
|
// context.WriteByte(BinaryTypeCode.UInt8);
|
|
// context.WriteByte(prop.GetByte(obj));
|
|
// return;
|
|
// case PropertyAccessorType.Int16:
|
|
// WriteInt16Unsafe(prop.GetInt16(obj), context);
|
|
// return;
|
|
// case PropertyAccessorType.UInt16:
|
|
// WriteUInt16Unsafe(prop.GetUInt16(obj), context);
|
|
// return;
|
|
// case PropertyAccessorType.UInt32:
|
|
// WriteUInt32(prop.GetUInt32(obj), context);
|
|
// return;
|
|
// case PropertyAccessorType.UInt64:
|
|
// WriteUInt64(prop.GetUInt64(obj), context);
|
|
// return;
|
|
// case PropertyAccessorType.Guid:
|
|
// WriteGuidUnsafe(prop.GetGuid(obj), context);
|
|
// return;
|
|
// case PropertyAccessorType.Enum:
|
|
// var enumValue = prop.GetEnumAsInt32(obj);
|
|
// if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny))
|
|
// {
|
|
// context.WriteByte(BinaryTypeCode.Enum);
|
|
// context.WriteByte(tiny);
|
|
// }
|
|
// else
|
|
// {
|
|
// context.WriteByte(BinaryTypeCode.Enum);
|
|
// context.WriteByte(BinaryTypeCode.Int32);
|
|
// context.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, context);
|
|
// else
|
|
// context.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.
|
|
/// Delegates to PropertyWriter bridge methods which handle UseMetadata internally:
|
|
/// UseMetadata=true: skip marker for defaults, type code + value for non-defaults.
|
|
/// UseMetadata=false (markerless): raw value only, no skip markers.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WritePropertyOrSkip<TOutput>(object obj, BinaryPropertyAccessor prop, TypeMetadataWrapper<BinarySerializeTypeMetadata> parentWrapper, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
switch (prop.AccessorType)
|
|
{
|
|
case PropertyAccessorType.Int32:
|
|
context.WriteInt32Property(prop.GetInt32(obj));
|
|
return;
|
|
case PropertyAccessorType.Int64:
|
|
context.WriteInt64Property(prop.GetInt64(obj));
|
|
return;
|
|
case PropertyAccessorType.Boolean:
|
|
context.WriteBoolProperty(prop.GetBoolean(obj));
|
|
return;
|
|
case PropertyAccessorType.Double:
|
|
context.WriteFloat64Property(prop.GetDouble(obj));
|
|
return;
|
|
case PropertyAccessorType.Single:
|
|
context.WriteFloat32Property(prop.GetSingle(obj));
|
|
return;
|
|
case PropertyAccessorType.Decimal:
|
|
context.WriteDecimalProperty(prop.GetDecimal(obj));
|
|
return;
|
|
case PropertyAccessorType.DateTime:
|
|
context.WriteDateTimeProperty(prop.GetDateTime(obj));
|
|
return;
|
|
case PropertyAccessorType.Byte:
|
|
context.WriteByteProperty(prop.GetByte(obj));
|
|
return;
|
|
case PropertyAccessorType.Int16:
|
|
context.WriteInt16Property(prop.GetInt16(obj));
|
|
return;
|
|
case PropertyAccessorType.UInt16:
|
|
context.WriteUInt16Property(prop.GetUInt16(obj));
|
|
return;
|
|
case PropertyAccessorType.UInt32:
|
|
context.WriteUInt32Property(prop.GetUInt32(obj));
|
|
return;
|
|
case PropertyAccessorType.UInt64:
|
|
context.WriteUInt64Property(prop.GetUInt64(obj));
|
|
return;
|
|
case PropertyAccessorType.Guid:
|
|
context.WriteGuidProperty(prop.GetGuid(obj));
|
|
return;
|
|
case PropertyAccessorType.Enum:
|
|
context.WriteEnumInt32Property(prop.GetEnumAsInt32(obj));
|
|
return;
|
|
case PropertyAccessorType.String:
|
|
{
|
|
// Fast path: typed getter, no boxing, no Type.GetTypeCode() call
|
|
string? value = prop.GetString(obj);
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
context.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty);
|
|
}
|
|
else
|
|
{
|
|
context.StringInternEligible = prop.UseStringPropertyInterning(context.InternBit);
|
|
WriteString(value, context);
|
|
}
|
|
return;
|
|
}
|
|
default:
|
|
{
|
|
// Object type (collection, complex object, byte[], dictionary)
|
|
// Use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism
|
|
// Set interning eligibility for string collection elements
|
|
context.StringInternEligible = prop.IsStringCollectionProperty && prop.UseStringPropertyInterning(context.InternBit);
|
|
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)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
}
|
|
else
|
|
{
|
|
var runtimeType = value.GetType();
|
|
|
|
var complexIdx = prop.ComplexPropertyIndex;
|
|
if (complexIdx >= 0)
|
|
{
|
|
var propWrapper = parentWrapper.GetPropertyTypeWrapper(complexIdx, runtimeType);
|
|
if (propWrapper == null)
|
|
{
|
|
propWrapper = context.GetWrapper(runtimeType);
|
|
parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper);
|
|
}
|
|
if (!context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType)
|
|
WriteValueNonPrimitiveWithWrapperPoly(value, propWrapper, context, depth, runtimeType);
|
|
else
|
|
WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth);
|
|
}
|
|
else
|
|
{
|
|
// Non-complex in default case (nullable value type, etc.)
|
|
if (!context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType)
|
|
context.WritePolymorphicPrefix(runtimeType);
|
|
WriteValueNonPrimitive(value, runtimeType, context, depth);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Specialized Array Writers
|
|
|
|
/// <summary>
|
|
/// Optimized array writer with specialized paths for primitive collections.
|
|
/// </summary>
|
|
private static void WriteArray<TOutput>(IEnumerable enumerable, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
context.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 T[] and List<T> — unified via ReadOnlySpan<T>
|
|
if (elementType != null)
|
|
{
|
|
if (TryWritePrimitiveCollection(enumerable, elementType, context))
|
|
return;
|
|
}
|
|
|
|
// For IList, we can write the count directly and use indexed access
|
|
if (enumerable is IList list)
|
|
{
|
|
var count = list.Count;
|
|
context.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 ICollection (HashSet<T>, Queue<T>, SortedSet<T>, etc.) — has Count, no indexer
|
|
if (enumerable is ICollection collection)
|
|
{
|
|
context.WriteVarUInt((uint)collection.Count);
|
|
foreach (var item in enumerable)
|
|
{
|
|
var itemType = item?.GetType() ?? typeof(object);
|
|
WriteValue(item, itemType, context, nextDepth);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// For other IEnumerable (rare: custom IEnumerable without ICollection), collect first
|
|
var items = new List<object?>();
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unified primitive collection writer for both T[] and List<T>.
|
|
/// Extracts ReadOnlySpan<T> from either source (zero-cost implicit conversion for arrays,
|
|
/// CollectionsMarshal.AsSpan for lists) and writes using bulk context methods with EnsureCapacity.
|
|
/// </summary>
|
|
private static bool TryWritePrimitiveCollection<TOutput>(IEnumerable enumerable, Type elementType, BinarySerializationContext<TOutput> context)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
if (ReferenceEquals(elementType, IntType))
|
|
{
|
|
ReadOnlySpan<int> span;
|
|
if (enumerable is int[] arr) span = arr;
|
|
else if (enumerable is List<int> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
context.WriteInt32ArrayOptimized(span);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, LongType))
|
|
{
|
|
ReadOnlySpan<long> span;
|
|
if (enumerable is long[] arr) span = arr;
|
|
else if (enumerable is List<long> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
context.WriteLongArrayOptimized(span);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, DoubleType))
|
|
{
|
|
ReadOnlySpan<double> span;
|
|
if (enumerable is double[] arr) span = arr;
|
|
else if (enumerable is List<double> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
context.WriteDoubleArrayBulk(span);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, FloatType))
|
|
{
|
|
ReadOnlySpan<float> span;
|
|
if (enumerable is float[] arr) span = arr;
|
|
else if (enumerable is List<float> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
context.WriteFloatArrayBulk(span);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, BoolType))
|
|
{
|
|
ReadOnlySpan<bool> span;
|
|
if (enumerable is bool[] arr) span = arr;
|
|
else if (enumerable is List<bool> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
context.WriteByte(span[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, GuidType))
|
|
{
|
|
ReadOnlySpan<Guid> span;
|
|
if (enumerable is Guid[] arr) span = arr;
|
|
else if (enumerable is List<Guid> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
context.WriteGuidArrayBulk(span);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, DecimalType))
|
|
{
|
|
ReadOnlySpan<decimal> span;
|
|
if (enumerable is decimal[] arr) span = arr;
|
|
else if (enumerable is List<decimal> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
WriteDecimalUnsafe(span[i], context);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, DateTimeType))
|
|
{
|
|
ReadOnlySpan<DateTime> span;
|
|
if (enumerable is DateTime[] arr) span = arr;
|
|
else if (enumerable is List<DateTime> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
WriteDateTimeUnsafe(span[i], context);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, StringType))
|
|
{
|
|
ReadOnlySpan<string?> span;
|
|
if (enumerable is string?[] arr) span = arr;
|
|
else if (enumerable is List<string?> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
{
|
|
var s = span[i];
|
|
if (s == null)
|
|
context.WriteByte(BinaryTypeCode.Null);
|
|
else
|
|
WriteString(s, context);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, ShortType))
|
|
{
|
|
ReadOnlySpan<short> span;
|
|
if (enumerable is short[] arr) span = arr;
|
|
else if (enumerable is List<short> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
WriteInt16Unsafe(span[i], context);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, UShortType))
|
|
{
|
|
ReadOnlySpan<ushort> span;
|
|
if (enumerable is ushort[] arr) span = arr;
|
|
else if (enumerable is List<ushort> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
WriteUInt16Unsafe(span[i], context);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, UIntType))
|
|
{
|
|
ReadOnlySpan<uint> span;
|
|
if (enumerable is uint[] arr) span = arr;
|
|
else if (enumerable is List<uint> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
WriteUInt32(span[i], context);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, ULongType))
|
|
{
|
|
ReadOnlySpan<ulong> span;
|
|
if (enumerable is ulong[] arr) span = arr;
|
|
else if (enumerable is List<ulong> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
WriteUInt64(span[i], context);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, ByteType))
|
|
{
|
|
// Note: top-level byte[] goes through WriteByteArray (BinaryTypeCode.ByteArray).
|
|
// This handles List<byte> and byte[] when reached via WriteArray (element-level encoding).
|
|
ReadOnlySpan<byte> span;
|
|
if (enumerable is byte[] arr) span = arr;
|
|
else if (enumerable is List<byte> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.UInt8);
|
|
context.WriteByte(span[i]);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, SByteType))
|
|
{
|
|
ReadOnlySpan<sbyte> span;
|
|
if (enumerable is sbyte[] arr) span = arr;
|
|
else if (enumerable is List<sbyte> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Int8);
|
|
context.WriteByte(unchecked((byte)span[i]));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, CharType))
|
|
{
|
|
ReadOnlySpan<char> span;
|
|
if (enumerable is char[] arr) span = arr;
|
|
else if (enumerable is List<char> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
WriteCharUnsafe(span[i], context);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, DateTimeOffsetType))
|
|
{
|
|
ReadOnlySpan<DateTimeOffset> span;
|
|
if (enumerable is DateTimeOffset[] arr) span = arr;
|
|
else if (enumerable is List<DateTimeOffset> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
WriteDateTimeOffsetUnsafe(span[i], context);
|
|
return true;
|
|
}
|
|
|
|
if (ReferenceEquals(elementType, TimeSpanType))
|
|
{
|
|
ReadOnlySpan<TimeSpan> span;
|
|
if (enumerable is TimeSpan[] arr) span = arr;
|
|
else if (enumerable is List<TimeSpan> list) span = CollectionsMarshal.AsSpan(list);
|
|
else return false;
|
|
|
|
context.WriteVarUInt((uint)span.Length);
|
|
for (var i = 0; i < span.Length; i++)
|
|
WriteTimeSpanUnsafe(span[i], context);
|
|
return true;
|
|
}
|
|
|
|
// Enum collections: iterate without GetWrapper/WriteValue dispatch
|
|
if (elementType.IsEnum)
|
|
{
|
|
if (enumerable is IList enumList)
|
|
{
|
|
context.WriteVarUInt((uint)enumList.Count);
|
|
for (var i = 0; i < enumList.Count; i++)
|
|
WriteEnum(enumList[i]!, context);
|
|
}
|
|
else if (enumerable is ICollection enumCol)
|
|
{
|
|
context.WriteVarUInt((uint)enumCol.Count);
|
|
foreach (var item in enumerable)
|
|
WriteEnum(item!, context);
|
|
}
|
|
else
|
|
{
|
|
var items = new List<object>();
|
|
foreach (var item in enumerable)
|
|
items.Add(item!);
|
|
context.WriteVarUInt((uint)items.Count);
|
|
for (var i = 0; i < items.Count; i++)
|
|
WriteEnum(items[i], context);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void WriteDictionary<TOutput>(IDictionary dictionary, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
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 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
|
|
|
|
#region WritePlan Debug Validation
|
|
|
|
/// <summary>
|
|
/// DEBUG ONLY: Validates that the WritePlan entry's string value matches the actual string being written.
|
|
/// Catches scan/write pass visit index misalignment immediately.
|
|
/// </summary>
|
|
[Conditional("DEBUG")]
|
|
private static void ValidateWritePlanString(in WriteDuplicateEntry planEntry, string actualValue)
|
|
{
|
|
if (planEntry.IsFirst && planEntry.Value != null && !string.Equals(planEntry.Value, actualValue, StringComparison.Ordinal))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
|
|
$"plan string=\"{planEntry.Value}\", actual=\"{actualValue}\". " +
|
|
$"Scan/write pass visit order diverged.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// DEBUG ONLY: Validates that the WritePlan entry's IId matches the actual object being written.
|
|
/// Uses the typed Id getter to extract the Id and compares against the scan pass IdentityMap.
|
|
/// </summary>
|
|
[Conditional("DEBUG")]
|
|
private static void ValidateWritePlanObject(in WriteDuplicateEntry planEntry, object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper)
|
|
{
|
|
var metadata = wrapper.Metadata;
|
|
switch (metadata.IdAccessorType)
|
|
{
|
|
case IdAccessorType.Int32:
|
|
{
|
|
var actualId = wrapper.RefIdGetterInt32!(value);
|
|
if (!wrapper.TryGetEntryInt32(actualId, out var slotIndex))
|
|
throw new InvalidOperationException(
|
|
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
|
|
$"Int32 Id={actualId} not found in IdentityMap. Scan/write pass visit order diverged.");
|
|
|
|
ref var entry = ref wrapper.GetEntryRefInt32(slotIndex);
|
|
if (entry.CacheIndex != planEntry.CacheMapIndex)
|
|
throw new InvalidOperationException(
|
|
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
|
|
$"Int32 Id={actualId} CacheIndex={entry.CacheIndex}, plan CacheMapIndex={planEntry.CacheMapIndex}.");
|
|
break;
|
|
}
|
|
case IdAccessorType.Int64:
|
|
{
|
|
var actualId = wrapper.RefIdGetterInt64!(value);
|
|
if (!wrapper.TryGetEntryInt64(actualId, out var slotIndex))
|
|
throw new InvalidOperationException(
|
|
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
|
|
$"Int64 Id={actualId} not found in IdentityMap. Scan/write pass visit order diverged.");
|
|
|
|
ref var entry = ref wrapper.GetEntryRefInt64(slotIndex);
|
|
if (entry.CacheIndex != planEntry.CacheMapIndex)
|
|
throw new InvalidOperationException(
|
|
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
|
|
$"Int64 Id={actualId} CacheIndex={entry.CacheIndex}, plan CacheMapIndex={planEntry.CacheMapIndex}.");
|
|
break;
|
|
}
|
|
case IdAccessorType.Guid:
|
|
{
|
|
var actualId = wrapper.RefIdGetterGuid!(value);
|
|
if (!wrapper.TryGetEntryGuid(actualId, out var slotIndex))
|
|
throw new InvalidOperationException(
|
|
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
|
|
$"Guid Id={actualId} not found in IdentityMap. Scan/write pass visit order diverged.");
|
|
|
|
ref var entry = ref wrapper.GetEntryRefGuid(slotIndex);
|
|
if (entry.CacheIndex != planEntry.CacheMapIndex)
|
|
throw new InvalidOperationException(
|
|
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
|
|
$"Guid Id={actualId} CacheIndex={entry.CacheIndex}, plan CacheMapIndex={planEntry.CacheMapIndex}.");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|