2510 lines
109 KiB
C#
2510 lines
109 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.Diagnostics.CodeAnalysis;
|
||
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(options.BufferWriterChunkSize);
|
||
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);
|
||
|
||
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 = System.Text.Encoding.UTF8.GetByteCount(stringValue.AsSpan());
|
||
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>
|
||
/// <remarks>
|
||
/// NativeAOT contract: <typeparamref name="T"/> is annotated with DAMs <c>PublicProperties</c>
|
||
/// so the trimmer preserves <see cref="System.Reflection.PropertyInfo"/> metadata required by the
|
||
/// runtime reflection path (compiled getters fall back to <c>PropertyInfo.GetValue</c> when
|
||
/// <see cref="System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported"/> is false).
|
||
/// SGen path is unaffected — annotation is uniform with <see cref="AcBinaryDeserializer.Deserialize{T}(byte[])"/>.
|
||
/// </remarks>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public static byte[] Serialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value)
|
||
=> Serialize(value, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Serialize object to an IBufferWriter with default options. Returns bytes written.
|
||
/// </summary>
|
||
/// <inheritdoc cref="Serialize{T}(T)" path="/remarks"/>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public static int Serialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] 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<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, AcBinarySerializerOptions options)
|
||
{
|
||
if (value == null) return [BinaryTypeCode.Null];
|
||
|
||
var runtimeType = value.GetType();
|
||
var context = AcquireArrayOutputContext(options);
|
||
|
||
try
|
||
{
|
||
// SGen fast path: skip IQueryable/Expression check + WriteValue dispatch chain.
|
||
// If root type has a GeneratedWriter it cannot be IQueryable/Expression/primitive/collection.
|
||
if (options.UseGeneratedCode)
|
||
{
|
||
var wrapper = context.GetWrapper(runtimeType);
|
||
if (wrapper.GeneratedWriter != null)
|
||
{
|
||
ScanForDuplicates(value, runtimeType, context);
|
||
context.WriteHeader();
|
||
WriteObject(value, wrapper, context);
|
||
|
||
if (options.UseCompression != Lz4CompressionMode.None)
|
||
return Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression);
|
||
return context.Output.ToArray(context._buffer, context._position);
|
||
}
|
||
}
|
||
|
||
// Full path: IQueryable/Expression conversion, primitive/collection dispatch
|
||
var actualValue = ConvertExpressionValue(value, ref runtimeType);
|
||
ScanForDuplicates(actualValue, runtimeType, context);
|
||
context.WriteHeader();
|
||
WriteValue(actualValue, runtimeType, context);
|
||
|
||
if (options.UseCompression != Lz4CompressionMode.None)
|
||
return Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression);
|
||
return context.Output.ToArray(context._buffer, context._position);
|
||
}
|
||
finally
|
||
{
|
||
ReturnContext(context, options);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Non-generic <c>Type</c>-based <see cref="Serialize{T}(T, AcBinarySerializerOptions)"/>. For
|
||
/// runtime-typed scenarios (plugin frameworks, ASP.NET ModelBinding, MVC formatters). The
|
||
/// <paramref name="type"/> parameter is the declared-type hint; the body uses
|
||
/// <c>value.GetType()</c> for the runtime polymorphism path, identical to the generic version.
|
||
/// </summary>
|
||
public static byte[] Serialize(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, AcBinarySerializerOptions options)
|
||
{
|
||
if (value == null) return [BinaryTypeCode.Null];
|
||
|
||
var runtimeType = value.GetType();
|
||
var context = AcquireArrayOutputContext(options);
|
||
|
||
try
|
||
{
|
||
if (options.UseGeneratedCode)
|
||
{
|
||
var wrapper = context.GetWrapper(runtimeType);
|
||
if (wrapper.GeneratedWriter != null)
|
||
{
|
||
ScanForDuplicates(value, runtimeType, context);
|
||
context.WriteHeader();
|
||
WriteObject(value, wrapper, context);
|
||
|
||
if (options.UseCompression != Lz4CompressionMode.None)
|
||
return Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression);
|
||
return context.Output.ToArray(context._buffer, context._position);
|
||
}
|
||
}
|
||
|
||
var actualValue = ConvertExpressionValue(value, ref runtimeType);
|
||
ScanForDuplicates(actualValue, runtimeType, context);
|
||
context.WriteHeader();
|
||
WriteValue(actualValue, runtimeType, context);
|
||
|
||
if (options.UseCompression != Lz4CompressionMode.None)
|
||
return Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression);
|
||
return context.Output.ToArray(context._buffer, context._position);
|
||
}
|
||
finally
|
||
{
|
||
ReturnContext(context, options);
|
||
}
|
||
}
|
||
|
||
/// <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 = AcquireArrayOutputContext(options);
|
||
try
|
||
{
|
||
ScanForDuplicates(value, runtimeType, context);
|
||
}
|
||
finally
|
||
{
|
||
ReturnContext(context, options);
|
||
}
|
||
}
|
||
|
||
/// <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<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] 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();
|
||
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
|
||
{
|
||
// SGen fast path: skip IQueryable/Expression check + WriteValue dispatch chain.
|
||
// If root type has a GeneratedWriter it cannot be IQueryable/Expression/primitive/collection.
|
||
if (options.UseGeneratedCode)
|
||
{
|
||
var wrapper = context.GetWrapper(runtimeType);
|
||
if (wrapper.GeneratedWriter != null)
|
||
{
|
||
ScanForDuplicates(value, runtimeType, context);
|
||
context.WriteHeader();
|
||
WriteObject(value, wrapper, context);
|
||
|
||
if (options.UseCompression != Lz4CompressionMode.None)
|
||
ThrowCompressionNotSupportedWithBufferWriter(context);
|
||
|
||
var bytesWritten = context.Output.GetTotalPosition(context._position);
|
||
context.Output.Flush(context._buffer, context._position);
|
||
return bytesWritten;
|
||
}
|
||
}
|
||
|
||
// Full path: IQueryable/Expression conversion, primitive/collection dispatch
|
||
var actualValue = ConvertExpressionValue(value, ref runtimeType);
|
||
ScanForDuplicates(actualValue, runtimeType, context);
|
||
context.WriteHeader();
|
||
WriteValue(actualValue, runtimeType, context);
|
||
|
||
if (options.UseCompression != Lz4CompressionMode.None)
|
||
ThrowCompressionNotSupportedWithBufferWriter(context);
|
||
|
||
var totalBytesWritten = context.Output.GetTotalPosition(context._position);
|
||
context.Output.Flush(context._buffer, context._position);
|
||
return totalBytesWritten;
|
||
}
|
||
finally
|
||
{
|
||
context.Output = default;
|
||
ReturnContext(context, options);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Non-generic <c>Type</c>-based counterpart to
|
||
/// <see cref="Serialize{T}(T, IBufferWriter{byte}, AcBinarySerializerOptions)"/>. Body identical
|
||
/// — <paramref name="type"/> is a declared-type hint only; runtime polymorphism uses <c>value.GetType()</c>.
|
||
/// </summary>
|
||
public static int Serialize(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, 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();
|
||
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
|
||
{
|
||
if (options.UseGeneratedCode)
|
||
{
|
||
var wrapper = context.GetWrapper(runtimeType);
|
||
if (wrapper.GeneratedWriter != null)
|
||
{
|
||
ScanForDuplicates(value, runtimeType, context);
|
||
context.WriteHeader();
|
||
WriteObject(value, wrapper, context);
|
||
|
||
if (options.UseCompression != Lz4CompressionMode.None)
|
||
ThrowCompressionNotSupportedWithBufferWriter(context);
|
||
|
||
var bytesWritten = context.Output.GetTotalPosition(context._position);
|
||
context.Output.Flush(context._buffer, context._position);
|
||
return bytesWritten;
|
||
}
|
||
}
|
||
|
||
var actualValue = ConvertExpressionValue(value, ref runtimeType);
|
||
ScanForDuplicates(actualValue, runtimeType, context);
|
||
context.WriteHeader();
|
||
WriteValue(actualValue, runtimeType, context);
|
||
|
||
if (options.UseCompression != Lz4CompressionMode.None)
|
||
ThrowCompressionNotSupportedWithBufferWriter(context);
|
||
|
||
var totalBytesWritten = context.Output.GetTotalPosition(context._position);
|
||
context.Output.Flush(context._buffer, context._position);
|
||
return totalBytesWritten;
|
||
}
|
||
finally
|
||
{
|
||
context.Output = default;
|
||
ReturnContext(context, options);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Serialize to a <see cref="System.IO.Pipelines.Pipe"/> as a chunked stream — pure AcBinary
|
||
/// bytes are written via <see cref="AsyncPipeWriterOutput"/> in raw mode (no per-chunk header).
|
||
/// The output is byte-compatible with <see cref="Serialize{T}(T, AcBinarySerializerOptions)"/>'s
|
||
/// <c>byte[]</c> result; a consumer drains <c>pipe.Reader</c> in its own reader-task and pushes
|
||
/// bytes via <see cref="AsyncPipeReaderInput.Feed"/>, then calls
|
||
/// <see cref="AcBinaryDeserializer.Deserialize{T}(AsyncPipeReaderInput, AcBinarySerializerOptions)"/>
|
||
/// — no extra parser, no special transport adapter.
|
||
///
|
||
/// <para><b>Why <see cref="System.IO.Pipelines.Pipe"/> instead of <see cref="System.IO.Pipelines.PipeWriter"/>?</b>
|
||
/// <c>Pipe.Writer</c> is always the BCL <c>PipeWriterImpl</c>, which is parallel-capable
|
||
/// (no <c>_tailMemory</c> reset race like <c>StreamPipeWriter</c>). This overload exposes the
|
||
/// <paramref name="flushPolicy"/> + <paramref name="flushTimeout"/> tuning safely.</para>
|
||
/// </summary>
|
||
/// <param name="value">The value to serialize; <c>null</c> writes a single null marker.</param>
|
||
/// <param name="pipe">Target pipe — caller drains <c>pipe.Reader</c> elsewhere.</param>
|
||
/// <param name="options">Serializer options (type wrappers, reference handling, interning, etc.).</param>
|
||
/// <param name="flushPolicy">
|
||
/// Per-chunk flush synchronization — see <see cref="FlushPolicy"/> for the three trade-off
|
||
/// points. <see cref="FlushPolicy.PerChunk"/>: strictly bounded ~chunk × 1 peak memory, no
|
||
/// producer/flush parallelism. <see cref="FlushPolicy.DoubleBuffered"/> (default): ~chunk × 2
|
||
/// peak memory, max producer/flush parallelism. <see cref="FlushPolicy.Coalesced"/>: up to
|
||
/// PauseWriterThreshold (~64 KB), highest throughput on bounded payloads.
|
||
/// </param>
|
||
/// <param name="flushTimeout">
|
||
/// Per-flush timeout. <c>null</c> → wait forever. Positive value: throws
|
||
/// <see cref="TimeoutException"/> on stuck consumers.
|
||
/// </param>
|
||
/// <returns>Total serialized bytes written.</returns>
|
||
public static int SerializeChunked<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, System.IO.Pipelines.Pipe pipe, AcBinarySerializerOptions options, FlushPolicy flushPolicy = FlushPolicy.DoubleBuffered, TimeSpan? flushTimeout = null)
|
||
{
|
||
if (pipe is null) throw new ArgumentNullException(nameof(pipe));
|
||
return SerializeToPipeWriterCore(value, pipe.Writer, options, flushPolicy, flushTimeout, multiMessage: false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Serialize to any <see cref="System.IO.Pipelines.PipeWriter"/> as a chunked stream — pure
|
||
/// AcBinary bytes, no per-chunk header. The output is byte-compatible with
|
||
/// <see cref="Serialize{T}(T, AcBinarySerializerOptions)"/>'s <c>byte[]</c> result.
|
||
///
|
||
/// <para><b>Flush strategy auto-selected by writer type</b>: <c>StreamPipeWriter</c>
|
||
/// (<c>PipeWriter.Create(stream)</c> — NamedPipe / FileStream / NetworkStream / etc.) runs
|
||
/// sequentially per chunk because the BCL impl resets <c>_tailMemory</c> on flush completion
|
||
/// (race-incompatible with parallel send). Other PipeWriter implementations (Kestrel transport,
|
||
/// custom impls) run with the safe <see cref="FlushPolicy.DoubleBuffered"/> default — max parallelism, zero-alloc.</para>
|
||
///
|
||
/// <para><b>Need runtime tuning of the flush strategy?</b> Build a
|
||
/// <see cref="System.IO.Pipelines.Pipe"/> instance and use
|
||
/// <see cref="SerializeChunked{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
|
||
/// — only Pipe-based writers can guarantee parallel-capable flush behavior.</para>
|
||
///
|
||
/// <para><b>Need a multiplexed wire format with per-chunk frame headers?</b> See
|
||
/// <see cref="SerializeChunkedFramed{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions)"/>.</para>
|
||
/// </summary>
|
||
/// <param name="value">The value to serialize; <c>null</c> writes a single null marker.</param>
|
||
/// <param name="pipeWriter">Target pipe writer.</param>
|
||
/// <param name="options">Serializer options (type wrappers, reference handling, interning, etc.).</param>
|
||
/// <returns>Total serialized bytes written.</returns>
|
||
public static int SerializeChunked<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options)
|
||
=> SerializeToPipeWriterCore(value, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: false);
|
||
|
||
/// <summary>
|
||
/// Non-generic <c>Type</c>-based counterpart to
|
||
/// <see cref="SerializeChunked{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions)"/>.
|
||
/// For runtime-typed scenarios (MVC formatters, plugin frameworks).
|
||
/// </summary>
|
||
public static int SerializeChunked(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options)
|
||
=> SerializeToPipeWriterCore<object?>(value, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: false);
|
||
|
||
/// <summary>
|
||
/// Serialize a value into a chunked stream where each chunk carries a self-describing
|
||
/// frame header — <c>[201][UINT16 size][data]</c> per chunk, with a final <c>[202]</c>
|
||
/// end-of-stream marker. The frame headers let the receiver detect chunk boundaries
|
||
/// incrementally without knowing the total payload size up front, and let multiple
|
||
/// independent messages share a single transport with reliable separation.
|
||
///
|
||
/// <para><b>Use this when</b> building a multiplexed wire protocol where several logical
|
||
/// messages are interleaved on one stream, when the receiver needs to start deserializing
|
||
/// as bytes arrive (pipeline parallelism — serialize / network / deserialize overlap), or
|
||
/// when the upper layer needs to dispatch each chunk independently. Typical scenarios:
|
||
/// real-time RPC, custom Hub-style protocols, event stream multiplexing.</para>
|
||
///
|
||
/// <para><b>Concrete example</b>: SignalR's <c>BinaryProtocolMode.AsyncSegment</c> uses
|
||
/// this exact wire format to interleave many HubMessages over a single connection.</para>
|
||
///
|
||
/// <para><b>Need a simpler streaming output without per-chunk metadata?</b> Use
|
||
/// <see cref="SerializeChunked{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
|
||
/// — bit-compatible with <see cref="Serialize{T}(T, AcBinarySerializerOptions)"/>'s
|
||
/// <c>byte[]</c> output, no extra parser needed on the receive side.</para>
|
||
/// </summary>
|
||
/// <param name="value">The value to serialize; <c>null</c> writes a single null marker.</param>
|
||
/// <param name="pipe">Target pipe — caller drains <c>pipe.Reader</c> elsewhere.</param>
|
||
/// <param name="options">Serializer options.</param>
|
||
/// <param name="flushPolicy">See <see cref="SerializeChunked{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>.</param>
|
||
/// <param name="flushTimeout">See <see cref="SerializeChunked{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>.</param>
|
||
/// <returns>Total serialized data bytes (excluding framing overhead).</returns>
|
||
public static int SerializeChunkedFramed<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, System.IO.Pipelines.Pipe pipe, AcBinarySerializerOptions options, FlushPolicy flushPolicy = FlushPolicy.DoubleBuffered, TimeSpan? flushTimeout = null)
|
||
{
|
||
if (pipe is null) throw new ArgumentNullException(nameof(pipe));
|
||
return SerializeToPipeWriterCore(value, pipe.Writer, options, flushPolicy, flushTimeout, multiMessage: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Serialize to any <see cref="System.IO.Pipelines.PipeWriter"/> with per-chunk frame headers
|
||
/// (multiplexed wire format). See
|
||
/// <see cref="SerializeChunkedFramed{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
|
||
/// for the wire format details and use-cases.
|
||
///
|
||
/// <para><b>Flush strategy auto-selected by writer type</b> — see
|
||
/// <see cref="SerializeChunked{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions)"/>.</para>
|
||
/// </summary>
|
||
public static int SerializeChunkedFramed<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options)
|
||
=> SerializeToPipeWriterCore(value, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: true);
|
||
|
||
/// <summary>
|
||
/// Non-generic <c>Type</c>-based counterpart to
|
||
/// <see cref="SerializeChunkedFramed{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions)"/>.
|
||
/// </summary>
|
||
public static int SerializeChunkedFramed(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options)
|
||
=> SerializeToPipeWriterCore<object?>(value, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: true);
|
||
|
||
/// <summary>
|
||
/// Internal flush-tunable framed PipeWriter overload — used by <c>AyCode.Services</c>
|
||
/// (SignalR hub protocol) on Kestrel transport output, which is parallel-capable. External
|
||
/// callers should use the <see cref="System.IO.Pipelines.Pipe"/> overload to safely tune
|
||
/// <paramref name="flushPolicy"/> on a guaranteed parallel-capable writer.
|
||
/// </summary>
|
||
internal static int SerializeChunkedFramed<T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout)
|
||
=> SerializeToPipeWriterCore(value, pipeWriter, options, flushPolicy, flushTimeout, multiMessage: true);
|
||
|
||
/// <summary>
|
||
/// Internal legacy alias for <see cref="SerializeChunkedFramed{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
|
||
/// — kept until the SignalR hub protocol (<c>AcBinaryHubProtocol.cs</c>) is migrated to the
|
||
/// new name in a separate, isolated step. Identical behavior to <c>SerializeChunkedFramed</c>
|
||
/// (framed wire format with <c>[201][UINT16][data]</c> per chunk + <c>[202]</c> end marker).
|
||
/// </summary>
|
||
internal static int Serialize<T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout)
|
||
=> SerializeToPipeWriterCore(value, pipeWriter, options, flushPolicy, flushTimeout, multiMessage: true);
|
||
|
||
/// <summary>
|
||
/// Common pipe-output serialization core. Same loop for both raw (<see cref="SerializeChunked{T}"/>)
|
||
/// and framed (<see cref="SerializeChunkedFramed{T}"/>) modes — the only difference flows through
|
||
/// <paramref name="multiMessage"/> into the <see cref="AsyncPipeWriterOutput"/> ctor.
|
||
/// </summary>
|
||
private static int SerializeToPipeWriterCore<T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout, bool multiMessage)
|
||
{
|
||
if (value == null)
|
||
{
|
||
if (!multiMessage)
|
||
{
|
||
// Raw single-message mode: null is just a [BinaryTypeCode.Null] byte on the wire.
|
||
// No chunking needed, no [201]/[202] framing.
|
||
var span = pipeWriter.GetSpan(1);
|
||
span[0] = BinaryTypeCode.Null;
|
||
pipeWriter.Advance(1);
|
||
return 1;
|
||
}
|
||
|
||
// Framed mode (multiMessage=true): null still needs to flow through AsyncPipeWriterOutput
|
||
// so the wire is [201][UINT16=1][BinaryTypeCode.Null][202] — well-formed chunked frame the
|
||
// receiver can parse. Bypassing AsyncPipeWriterOutput here would emit a bare [Null] byte that
|
||
// breaks framing for any chunked-stream consumer (e.g. AcBinaryHubProtocol AsyncSegment).
|
||
var nullOutput = new AsyncPipeWriterOutput(pipeWriter, options.BufferWriterChunkSize, multiMessage: true, flushPolicy, flushTimeout);
|
||
nullOutput.Initialize(out var nullBuf, out var nullPos, out _);
|
||
nullBuf[nullPos++] = BinaryTypeCode.Null;
|
||
nullOutput.Flush(nullBuf, nullPos);
|
||
return 1;
|
||
}
|
||
|
||
var runtimeType = value.GetType();
|
||
var context = BinarySerializationContextPool<AsyncPipeWriterOutput>.Get(options);
|
||
|
||
context.Output = new AsyncPipeWriterOutput(pipeWriter, options.BufferWriterChunkSize, multiMessage, flushPolicy, flushTimeout);
|
||
context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);
|
||
|
||
try
|
||
{
|
||
if (options.UseGeneratedCode)
|
||
{
|
||
var wrapper = context.GetWrapper(runtimeType);
|
||
if (wrapper.GeneratedWriter != null)
|
||
{
|
||
ScanForDuplicates(value, runtimeType, context);
|
||
context.WriteHeader();
|
||
WriteObject(value, wrapper, context);
|
||
|
||
if (options.UseCompression != Compression.Lz4CompressionMode.None)
|
||
ThrowCompressionNotSupportedWithPipeWriter(context);
|
||
|
||
var bytesWritten = context.Output.GetTotalPosition(context._position);
|
||
context.Output.Flush(context._buffer, context._position);
|
||
return bytesWritten;
|
||
}
|
||
}
|
||
|
||
var actualValue = ConvertExpressionValue(value, ref runtimeType);
|
||
ScanForDuplicates(actualValue, runtimeType, context);
|
||
context.WriteHeader();
|
||
WriteValue(actualValue, runtimeType, context);
|
||
|
||
if (options.UseCompression != Compression.Lz4CompressionMode.None)
|
||
ThrowCompressionNotSupportedWithPipeWriter(context);
|
||
|
||
var totalBytesWritten = context.Output.GetTotalPosition(context._position);
|
||
context.Output.Flush(context._buffer, context._position);
|
||
return totalBytesWritten;
|
||
}
|
||
finally
|
||
{
|
||
context.Output = default;
|
||
ReturnContext(context, options);
|
||
}
|
||
}
|
||
|
||
/// <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 = AcquireArrayOutputContext(options);
|
||
|
||
try
|
||
{
|
||
ScanForDuplicates(value, runtimeType, context);
|
||
context.WriteHeader();
|
||
WriteValue(value, runtimeType, context);
|
||
return context.Position;
|
||
}
|
||
finally
|
||
{
|
||
ReturnContext(context, options);
|
||
}
|
||
}
|
||
|
||
/// <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 = AcquireArrayOutputContext(options);
|
||
|
||
try
|
||
{
|
||
ScanForDuplicates(value, runtimeType, context);
|
||
context.WriteHeader();
|
||
WriteValue(value, runtimeType, context);
|
||
|
||
// 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
|
||
{
|
||
ReturnContext(context, options);
|
||
}
|
||
}
|
||
|
||
#region Entry Point Helpers
|
||
|
||
/// <summary>
|
||
/// Acquires a pooled ArrayBinaryOutput context and initializes the output buffer.
|
||
/// Reuses pooled ArrayBinaryOutput instance when available.
|
||
/// AggressiveInlining: JIT must see buffer init in caller scope for register allocation.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static BinarySerializationContext<ArrayBinaryOutput> AcquireArrayOutputContext(AcBinarySerializerOptions options)
|
||
{
|
||
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);
|
||
return context;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Returns a serialization context to its pool. Uses async return when UseAsync is enabled.
|
||
/// </summary>
|
||
private static void ReturnContext<TOutput>(BinarySerializationContext<TOutput> context, AcBinarySerializerOptions options)
|
||
where TOutput : struct, IBinaryOutputBase
|
||
{
|
||
if (options.UseAsync) BinarySerializationContextPool<TOutput>.ReturnAsync(context);
|
||
else BinarySerializationContextPool<TOutput>.Return(context);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Converts IQueryable/Expression values to AcExpressionNode for serialization.
|
||
/// Returns the converted value (or original if no conversion needed).
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||
private static object ConvertExpressionValue<T>(T value, ref Type runtimeType)
|
||
{
|
||
if (value is IQueryable queryable)
|
||
{
|
||
runtimeType = typeof(AcExpressionNode);
|
||
return AcSerializerCommon.QueryableToNode(queryable);
|
||
}
|
||
if (AcSerializerCommon.IsExpressionType(runtimeType))
|
||
{
|
||
runtimeType = typeof(AcExpressionNode);
|
||
return AcExpressionConverter.ToNode((Expression)(object)value!);
|
||
}
|
||
return value!;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Flushes output and throws NotSupportedException for compression with IBufferWriter.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||
private static void ThrowCompressionNotSupportedWithBufferWriter(BinarySerializationContext<BufferWriterBinaryOutput> context)
|
||
{
|
||
context.Output.Flush(context._buffer, context._position);
|
||
throw new NotSupportedException(
|
||
"Compression is not supported with IBufferWriter output. " +
|
||
"Use the byte[] overload or disable compression.");
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||
private static void ThrowCompressionNotSupportedWithPipeWriter(BinarySerializationContext<AsyncPipeWriterOutput> context)
|
||
{
|
||
context.Output.Flush(context._buffer, context._position);
|
||
throw new NotSupportedException(
|
||
"Compression is not supported with PipeWriter output. " +
|
||
"Use the byte[] overload or disable compression.");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#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)
|
||
where TOutput : struct, IBinaryOutputBase
|
||
{
|
||
WriteValueNonPrimitive(value, type, context);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Bridge for generated writers to call the runtime WriteString.
|
||
/// <para>FastWire mode: markerless wire — delegates to <see cref="BinarySerializationContext{TOutput}.WriteStringUtf16Markerless"/>
|
||
/// which handles all three states (null / empty / content) via int32 sentinel header.</para>
|
||
/// <para>Compact mode: existing markerful path — null → <c>PropertySkip</c>, empty → <c>StringEmpty</c>,
|
||
/// content → <see cref="WriteString{TOutput}"/> with marker dispatch.</para>
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
internal static void WriteStringGenerated<TOutput>(string? value, BinarySerializationContext<TOutput> context)
|
||
where TOutput : struct, IBinaryOutputBase
|
||
{
|
||
if (context.FastWire)
|
||
{
|
||
context.WriteStringUtf16Markerless(value);
|
||
return;
|
||
}
|
||
|
||
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)
|
||
where TOutput : struct, IBinaryOutputBase
|
||
{
|
||
var wrapper = context.GetWrapper(type);
|
||
WriteObject(value, wrapper, context);
|
||
}
|
||
|
||
/// <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)
|
||
where TOutput : struct, IBinaryOutputBase
|
||
{
|
||
var wrapper = context.GetWrapper(type, wrapperSlot);
|
||
WriteObject(value, wrapper, context);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Value Writing
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void WriteValue<TOutput>(object? value, Type type, BinarySerializationContext<TOutput> context)
|
||
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);
|
||
}
|
||
|
||
/// <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)
|
||
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;
|
||
}
|
||
|
||
// 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);
|
||
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);
|
||
return;
|
||
}
|
||
|
||
// Handle complex objects with single-pass reference tracking
|
||
WriteObject(value, wrapper, context);
|
||
}
|
||
|
||
/// <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)
|
||
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;
|
||
}
|
||
|
||
// 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);
|
||
return;
|
||
}
|
||
|
||
// Handle collections/arrays
|
||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||
{
|
||
WriteArray(enumerable, wrapper, context);
|
||
return;
|
||
}
|
||
|
||
// Handle complex objects
|
||
WriteObject(value, wrapper, context);
|
||
}
|
||
|
||
/// <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, Type polyRuntimeType)
|
||
where TOutput : struct, IBinaryOutputBase
|
||
{
|
||
var type = wrapper.Metadata.SourceType;
|
||
|
||
if (type.IsValueType)
|
||
{
|
||
if (TryWritePrimitive(value, value.GetType(), context))
|
||
return;
|
||
}
|
||
|
||
if (value is byte[] byteArray)
|
||
{
|
||
context.WritePolymorphicPrefix(polyRuntimeType);
|
||
WriteByteArray(byteArray, context);
|
||
return;
|
||
}
|
||
|
||
if (value is IDictionary dictionary)
|
||
{
|
||
context.WritePolymorphicPrefix(polyRuntimeType);
|
||
WriteDictionary(dictionary, context);
|
||
return;
|
||
}
|
||
|
||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||
{
|
||
context.WritePolymorphicPrefix(polyRuntimeType);
|
||
WriteArray(enumerable, wrapper, context);
|
||
return;
|
||
}
|
||
|
||
// Complex object — handles combined poly+ref markers
|
||
WriteObjectPolymorphic(value, wrapper, context, 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)
|
||
{
|
||
// H2Q6 v3 wire format — StringFirst with tier-marker dispatch (Small/Medium):
|
||
// [StringInternFirstSmall][cacheIdx:VarUInt][charLen:8][utf8Len:8][bytes] if utf8Len ≤ 255
|
||
// [StringInternFirstMedium][cacheIdx:VarUInt][charLen:16][utf8Len:16][bytes] if utf8Len ≤ 65535
|
||
// 1-pass decode: charLen carried in header, no CountUtf8Chars Pass 1.
|
||
context.WriteStringInternFirstWithDispatch(planEntry.Value ?? value, planEntry.CacheMapIndex);
|
||
}
|
||
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
|
||
}
|
||
|
||
// Marker-dispatch (ACCORE-BIN-T-M3R7): WriteStringWithDispatch encodes UTF-8 once, detects
|
||
// ASCII via bytesWritten == charLength, and emits the optimal wire marker:
|
||
// • bytesWritten ≤ 31 + ASCII → FixStrAscii (single byte marker + length, ASCII payload)
|
||
// • bytesWritten ≤ 31 + UTF-8 → FixStr (single byte marker + length, UTF-8 payload)
|
||
// • bytesWritten > 31 + ASCII → StringAscii (marker + VarUInt length + ASCII payload)
|
||
// • bytesWritten > 31 + UTF-8 → String (marker + VarUInt length + UTF-8 payload)
|
||
// Reader dispatches on the ASCII marker to skip UTF-8 decode (byte→char widen only).
|
||
// FastWire path is handled inside WriteStringWithDispatch (no marker dispatch — UTF-16 raw).
|
||
context.WriteStringWithDispatch(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)
|
||
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);
|
||
else
|
||
WriteObjectWithRefHandling(value, wrapper, context);
|
||
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, 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)
|
||
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, 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)
|
||
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, 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, bool useMetaForType)
|
||
where TOutput : struct, IBinaryOutputBase
|
||
{
|
||
if (context.UseGeneratedCode)
|
||
{
|
||
var generatedWriter = wrapper.GeneratedWriter;
|
||
if (generatedWriter != null)
|
||
{
|
||
// SGen path handles its own RecursionDepth inc/dec via generated emit (gated by NeedsDepthCheck)
|
||
generatedWriter.WriteProperties(value, context);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Runtime path: global recursion depth safety net — gated by context.NeedsDepthCheck
|
||
// (pre-computed at Reset(): !HasAllRefHandling && MaxDepth > 0 && MaxDepthBehavior != Disable).
|
||
// Local-cached flag: 1 ctx field-read, register-resident — re-used at inc and dec.
|
||
var needsDepthCheck = context.NeedsDepthCheck;
|
||
if (needsDepthCheck)
|
||
{
|
||
if (context.RecursionDepth >= context.MaxDepth)
|
||
throw new InvalidOperationException($"AcBinary serialize: recursion depth exceeded MaxDepth={context.MaxDepth} at type '{wrapper.Metadata.SourceType.FullName}' (depth={context.RecursionDepth}, position={context.Position})");
|
||
|
||
context.RecursionDepth++;
|
||
}
|
||
|
||
if (!useMetaForType)
|
||
{
|
||
WritePropertiesMarkerless(value, wrapper, context);
|
||
}
|
||
else
|
||
{
|
||
WritePropertiesWithMeta(value, wrapper, context);
|
||
}
|
||
|
||
if (needsDepthCheck) context.RecursionDepth--;
|
||
}
|
||
|
||
private static void WritePropertiesWithMeta<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context) where TOutput : struct, IBinaryOutputBase
|
||
{
|
||
var properties = wrapper.Metadata.Properties;
|
||
var propCount = properties.Length;
|
||
// Combined runtime+attribute gate: if the type opted out of the filter feature via
|
||
// [AcBinarySerializable(EnablePropertyFilterFeature = false)], the runtime PropertyFilter is
|
||
// ignored for this type — short-circuit eliminates the per-property `ShouldSerializeProperty`
|
||
// call entirely. Mirrors the SGen-side gate in `EmitProp`.
|
||
var hasPropertyFilter = context.HasPropertyFilter && wrapper.Metadata.EnablePropertyFilterFeature;
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void WritePropertiesMarkerless<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context) where TOutput : struct, IBinaryOutputBase
|
||
{
|
||
var properties = wrapper.Metadata.Properties;
|
||
var propCount = properties.Length;
|
||
// Combined runtime+attribute gate — see WritePropertiesWithMeta for the rationale.
|
||
var hasPropertyFilter = context.HasPropertyFilter && wrapper.Metadata.EnablePropertyFilterFeature;
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <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, 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, 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>
|
||
/// <remarks>
|
||
/// NativeAOT note: the polymorphism path uses <c>value.GetType()</c> to obtain the runtime type,
|
||
/// which inherently loses the DAMs annotation chain. Polymorphic concrete types must therefore
|
||
/// be rooted by the consumer (either via <c>[AcBinarySerializable]</c> + SGen, or
|
||
/// <c><TrimmerRootAssembly></c>). The IL2072 suppression below is bounded to this
|
||
/// well-known trimmer blind spot — runtime polymorphism is a fundamental limitation, not a bug.
|
||
/// </remarks>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2072",
|
||
Justification = "Polymorphism via obj.GetType() is a documented trimmer blind spot. Consumers must root "
|
||
+ "polymorphic concrete types via [AcBinarySerializable] (SGen) or TrimmerRootAssembly.")]
|
||
private static void WritePropertyOrSkip<TOutput>(object obj, BinaryPropertyAccessor prop, TypeMetadataWrapper<BinarySerializeTypeMetadata> parentWrapper, BinarySerializationContext<TOutput> context)
|
||
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.
|
||
// FastWire: markerless int32 sentinel via `WriteStringUtf16Markerless` — wire-symmetric
|
||
// with `WriteStringGenerated` (SGen) so cross-mode interop holds. Compact: existing markered.
|
||
string? value = prop.GetString(obj);
|
||
if (context.FastWire)
|
||
{
|
||
context.WriteStringUtf16Markerless(value);
|
||
return;
|
||
}
|
||
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, runtimeType);
|
||
else
|
||
WriteValueNonPrimitiveWithWrapper(value, propWrapper, context);
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
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)
|
||
where TOutput : struct, IBinaryOutputBase
|
||
{
|
||
context.WriteByte(BinaryTypeCode.Array);
|
||
|
||
// 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);
|
||
}
|
||
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);
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <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)
|
||
where TOutput : struct, IBinaryOutputBase
|
||
{
|
||
context.WriteByte(BinaryTypeCode.Dictionary);
|
||
context.WriteVarUInt((uint)dictionary.Count);
|
||
|
||
foreach (DictionaryEntry entry in dictionary)
|
||
{
|
||
// Write key
|
||
var keyType = entry.Key?.GetType() ?? typeof(object);
|
||
WriteValue(entry.Key, keyType, context);
|
||
|
||
// Write value
|
||
var valueType = entry.Value?.GetType() ?? typeof(object);
|
||
WriteValue(entry.Value, valueType, context);
|
||
}
|
||
}
|
||
|
||
#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
|
||
}
|