1130 lines
42 KiB
C#
1130 lines
42 KiB
C#
using System;
|
|
using System.Buffers;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using static AyCode.Core.Helpers.JsonUtilities;
|
|
|
|
namespace AyCode.Core.Serializers.Binaries;
|
|
|
|
public static partial class AcBinarySerializer
|
|
{
|
|
private static class BinarySerializationContextPool<TOutput> where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
private static readonly ConcurrentQueue<BinarySerializationContext<TOutput>> Pool = new();
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static BinarySerializationContext<TOutput> Get(AcBinarySerializerOptions options)
|
|
{
|
|
if (Pool.TryDequeue(out var context))
|
|
{
|
|
context.Reset(options);
|
|
return context;
|
|
}
|
|
|
|
return new BinarySerializationContext<TOutput>(options);
|
|
}
|
|
|
|
public static void ReturnAsync(BinarySerializationContext<TOutput> context)
|
|
{
|
|
// 🔥 FIRE-AND-FORGET: cleanup háttérben
|
|
ThreadPool.UnsafeQueueUserWorkItem(Return, context, preferLocal: true);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void Return(BinarySerializationContext<TOutput> context)
|
|
{
|
|
if (Pool.Count < context.Options.MaxContextPoolSize)
|
|
{
|
|
context.Clear();
|
|
Pool.Enqueue(context);
|
|
}
|
|
else
|
|
{
|
|
context.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Binary serialization context. Generic on TOutput for output strategy selection.
|
|
/// Owns _buffer/_position for zero virtual dispatch on the hot path.
|
|
/// All write operations (WriteByte, WriteVarUInt, etc.) are inline methods here.
|
|
/// TOutput Output handles only cold-path buffer management (Grow/Initialize) and finalization.
|
|
/// </summary>
|
|
internal sealed partial class BinarySerializationContext<TOutput>
|
|
: SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable
|
|
where TOutput : struct, IBinaryOutputBase
|
|
{
|
|
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
|
|
|
private const int PropertyIndexBufferMaxCache = 512;
|
|
private const int PropertyStateBufferMaxCache = 512;
|
|
|
|
/// <summary>
|
|
/// Output target — handles only Grow (cold path) and finalization (AsSpan/ToArray/Flush).
|
|
/// </summary>
|
|
public TOutput Output;
|
|
|
|
/// <summary>
|
|
/// True if Output has been assigned (struct can't be null-checked).
|
|
/// </summary>
|
|
public bool OutputInitialized;
|
|
|
|
#region Buffer State — owned by context for zero virtual dispatch
|
|
|
|
/// <summary>Current writable buffer (from ArrayPool or IBufferWriter chunk).</summary>
|
|
internal byte[] _buffer = null!;
|
|
|
|
/// <summary>Current write position within _buffer.</summary>
|
|
internal int _position;
|
|
|
|
/// <summary>One past the last writable index in _buffer. Write must satisfy _position < _bufferEnd.</summary>
|
|
internal int _bufferEnd;
|
|
|
|
#endregion
|
|
|
|
private IdentityMap<string, InternEntry>? _stringInternMap;
|
|
private int _nextCacheIndex;
|
|
public int NextFirstIndex; // Next first occurrence index for scan pass. Direct access for performance.
|
|
|
|
#region WriteDuplicateEntry — scan pass output for write pass cursor
|
|
|
|
private WriteDuplicateEntry[]? _writePlan;
|
|
private int _writePlanCount;
|
|
|
|
/// <summary>Unified scan visit counter. Increments on every IId object and internable string visit.</summary>
|
|
public int ScanVisitIndex;
|
|
|
|
/// <summary>Write plan entry count for write pass cursor.</summary>
|
|
internal int WritePlanCount => _writePlanCount;
|
|
|
|
/// <summary>Write plan array for write pass cursor. Sorted by VisitIndex after scan pass.</summary>
|
|
internal WriteDuplicateEntry[]? WritePlan => _writePlan;
|
|
|
|
/// <summary>
|
|
/// Adds a pre-computed write instruction for a duplicate string or IId object reference.
|
|
/// </summary>
|
|
public void AddWriteDuplicateEntry(int visitIndex, int cacheMapIndex, bool isFirst, string? value)
|
|
{
|
|
if (_writePlan == null)
|
|
{
|
|
_writePlan = ArrayPool<WriteDuplicateEntry>.Shared.Rent(16);
|
|
}
|
|
else if (_writePlanCount >= _writePlan.Length)
|
|
{
|
|
var newArray = ArrayPool<WriteDuplicateEntry>.Shared.Rent(_writePlan.Length * 2);
|
|
_writePlan.AsSpan(0, _writePlanCount).CopyTo(newArray);
|
|
ArrayPool<WriteDuplicateEntry>.Shared.Return(_writePlan, clearArray: true);
|
|
_writePlan = newArray;
|
|
}
|
|
|
|
ref var entry = ref _writePlan[_writePlanCount++];
|
|
entry.VisitIndex = visitIndex;
|
|
entry.CacheMapIndex = cacheMapIndex;
|
|
entry.IsFirst = isFirst;
|
|
entry.Value = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sorts write plan by VisitIndex for sequential cursor consumption in write pass.
|
|
/// Called once after scan pass completes.
|
|
/// </summary>
|
|
internal void SortWritePlan()
|
|
{
|
|
if (_writePlanCount > 1)
|
|
_writePlan.AsSpan(0, _writePlanCount).Sort(static (a, b) => a.VisitIndex.CompareTo(b.VisitIndex));
|
|
|
|
_nextWritePlanVisitIndex = _writePlanCount > 0 ? _writePlan![0].VisitIndex : int.MaxValue;
|
|
}
|
|
|
|
/// <summary>Write pass cursor index into sorted _writePlan array.</summary>
|
|
internal int WritePlanCursor;
|
|
|
|
/// <summary>Write pass visit counter. Mirrors ScanVisitIndex ordering.</summary>
|
|
internal int WriteVisitIndex;
|
|
|
|
/// <summary>
|
|
/// Pre-cached VisitIndex of the next write plan entry, or int.MaxValue when exhausted.
|
|
/// </summary>
|
|
private int _nextWritePlanVisitIndex = int.MaxValue;
|
|
|
|
/// <summary>
|
|
/// Set per-property in WritePropertyOrSkip before calling WriteString.
|
|
/// Controls whether the current string property participates in the cursor-based interning.
|
|
/// Must mirror scan pass's prop.UseStringPropertyInterning() check.
|
|
/// </summary>
|
|
internal bool StringInternEligible;
|
|
|
|
/// <summary>
|
|
/// Next polymorphic type cache index. Assigned sequentially on first polymorphic write per runtime type.
|
|
/// Used together with TypeMetadataWrapper.PolymorphicSeen/PolymorphicCacheIndex.
|
|
/// </summary>
|
|
internal int _nextTypeSlot;
|
|
|
|
/// <summary>
|
|
/// Tries to consume the next write plan entry at the current WriteVisitIndex.
|
|
/// Returns true if the entry matches (duplicate exists at this visit point).
|
|
/// Always increments WriteVisitIndex.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal bool TryConsumeWritePlanEntry(out WriteDuplicateEntry entry)
|
|
{
|
|
var visitIndex = WriteVisitIndex++;
|
|
if (visitIndex == _nextWritePlanVisitIndex)
|
|
{
|
|
entry = _writePlan![WritePlanCursor++];
|
|
_nextWritePlanVisitIndex = WritePlanCursor < _writePlanCount
|
|
? _writePlan[WritePlanCursor].VisitIndex
|
|
: int.MaxValue;
|
|
return true;
|
|
}
|
|
entry = default;
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Next cache index reference for scan pass. Direct ref access for TryTrack methods.
|
|
/// </summary>
|
|
public ref int NextCacheIndexRef
|
|
{
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
get => ref _nextCacheIndex;
|
|
}
|
|
|
|
private int[]? _propertyIndexBuffer;
|
|
private byte[]? _propertyStateBuffer;
|
|
|
|
/// <summary>
|
|
/// Current property being serialized. Set in WriteObject property loops.
|
|
/// Used by WriteString for per-property interning control (UseStringPropertyInterning).
|
|
/// null when writing non-property values (arrays, dictionaries, root value).
|
|
/// </summary>
|
|
//internal BinaryPropertyAccessorBase? CurrentProperty;
|
|
|
|
#if DEBUG
|
|
/// <summary>
|
|
/// DEBUG ONLY: Callback invoked when a string is registered for interning.
|
|
/// Parameters: (propertyPath, stringValue)
|
|
/// Use this to analyze which properties have repeated string values.
|
|
/// </summary>
|
|
internal Action<string?, string>? OnStringInterned;
|
|
#endif
|
|
|
|
// These properties delegate to Options for convenience
|
|
internal bool HasStringInterning { get; private set; }
|
|
internal bool UseStringInterning => HasStringInterning;
|
|
|
|
/// <summary>
|
|
/// Pre-computed <c>1 << (int)Options.UseStringInterning</c>.
|
|
/// Used by sgen scan (per-object intern check) and sgen write (per-property StringInternEligible).
|
|
/// Avoids repeated field chain traversal (context.Options.UseStringInterning) + shift per call site.
|
|
/// Value: None=1, Attribute=2, All=4.
|
|
/// </summary>
|
|
public int InternBit { get; private set; }
|
|
|
|
public bool IsValidForInterningString(int strLength)
|
|
{
|
|
return strLength >= MinStringInternLength && (MaxStringInternLength == 0 || strLength <= MaxStringInternLength);
|
|
}
|
|
|
|
/// <summary>
|
|
/// True if we have interning/ref tracking (cache count needed in header).
|
|
/// </summary>
|
|
public bool HasCaching => HasStringInterning || HasRefHandling;
|
|
public bool UseMetadata => Options.UseMetadata;
|
|
public bool UseGeneratedCode => Options.UseGeneratedCode;
|
|
|
|
/// <summary>
|
|
/// True when generated writers can bypass WriteObject entirely and write markers + properties inline.
|
|
/// Requires: no UseMetadata (no inline metadata tracking).
|
|
/// PropertyFilter is handled by generated code's per-property filter checks.
|
|
/// Reference handling is safe because generated code inlines TryConsumeWritePlanEntry for IId types.
|
|
/// </summary>
|
|
public bool IsDirectObjectWrite => !UseMetadata;
|
|
public bool FastWire { get; private set; }
|
|
public byte MinStringInternLength => Options.MinStringInternLength;
|
|
public byte MaxStringInternLength => Options.MaxStringInternLength;
|
|
public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter;
|
|
|
|
/// <summary>
|
|
/// Cached check for PropertyFilter != null. Set in Reset() to avoid property getter in hot loop.
|
|
/// </summary>
|
|
public bool HasPropertyFilter { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Current output position (total bytes written so far).
|
|
/// Cold path — uses virtual dispatch through Output.GetTotalPosition.
|
|
/// </summary>
|
|
public int Position
|
|
{
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
get => Output.GetTotalPosition(_position);
|
|
}
|
|
|
|
public BinarySerializationContext(AcBinarySerializerOptions options)
|
|
{
|
|
Reset(options);
|
|
InitializeWrapperSlots(Volatile.Read(ref s_nextWrapperSlot));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Factory for creating BinarySerializeTypeMetadata instances.
|
|
/// </summary>
|
|
protected override Func<Type, BinarySerializeTypeMetadata> MetadataFactory
|
|
=> static t => new BinarySerializeTypeMetadata(t, HasJsonIgnoreAttribute);
|
|
|
|
public override void Reset(AcBinarySerializerOptions options)
|
|
{
|
|
// IMPORTANT: base.Reset sets Options first, so derived code can use Options-derived properties
|
|
base.Reset(options);
|
|
HasPropertyFilter = Options.PropertyFilter != null;
|
|
InternBit = 1 << (int)Options.UseStringInterning;
|
|
HasStringInterning = Options.UseStringInterning != StringInterningMode.None;
|
|
FastWire = Options.WireMode == WireMode.Fast;
|
|
}
|
|
|
|
public override void Clear()
|
|
{
|
|
// Reset output buffer (large buffers → return to pool, rent halved)
|
|
if (OutputInitialized)
|
|
{
|
|
Output.Reset();
|
|
}
|
|
|
|
_stringInternMap?.Reset();
|
|
|
|
_nextCacheIndex = 0;
|
|
NextFirstIndex = 0;
|
|
ScanVisitIndex = 0;
|
|
WritePlanCursor = 0;
|
|
WriteVisitIndex = 0;
|
|
_nextWritePlanVisitIndex = int.MaxValue;
|
|
StringInternEligible = false;
|
|
_nextTypeSlot = 0;
|
|
|
|
// Clear write plan string references to avoid GC pinning, keep array if small enough
|
|
if (_writePlan != null)
|
|
{
|
|
_writePlan.AsSpan(0, _writePlanCount).Clear();
|
|
if (_writePlan.Length > PropertyIndexBufferMaxCache)
|
|
{
|
|
ArrayPool<WriteDuplicateEntry>.Shared.Return(_writePlan);
|
|
_writePlan = null;
|
|
}
|
|
}
|
|
_writePlanCount = 0;
|
|
|
|
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
|
|
{
|
|
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
|
|
_propertyIndexBuffer = null;
|
|
}
|
|
|
|
if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache)
|
|
{
|
|
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
|
_propertyStateBuffer = null;
|
|
}
|
|
|
|
// Clear wrapper tracking - returns IdentityMap arrays to pool
|
|
base.Clear();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_propertyIndexBuffer != null)
|
|
{
|
|
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
|
|
_propertyIndexBuffer = null;
|
|
}
|
|
|
|
if (_propertyStateBuffer != null)
|
|
{
|
|
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
|
_propertyStateBuffer = null;
|
|
}
|
|
|
|
// Dispose the output if it implements IDisposable (e.g. ArrayBinaryOutput returns buffer to pool)
|
|
if (Output is IDisposable disposableOutput)
|
|
disposableOutput.Dispose();
|
|
}
|
|
|
|
#region Write Methods — inline, zero virtual dispatch
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void EnsureCapacity(int additionalBytes)
|
|
{
|
|
if (_position + additionalBytes > _bufferEnd)
|
|
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures the buffer has enough space for the specified number of bytes.
|
|
/// Called before property writes to avoid mid-object Grow() calls.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void ReserveCapacity(int bytes)
|
|
{
|
|
EnsureCapacity(bytes);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteByte(byte value)
|
|
{
|
|
if (_position >= _bufferEnd)
|
|
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
|
|
_buffer[_position++] = value;
|
|
}
|
|
|
|
/// <summary>Writes a single byte without capacity check. Caller must ensure buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteByteUnsafe(byte value)
|
|
{
|
|
_buffer[_position++] = value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteTwoBytes(byte b1, byte b2)
|
|
{
|
|
EnsureCapacity(2);
|
|
WriteTwoBytesUnsafe(b1, b2);
|
|
}
|
|
|
|
/// <summary>Writes two bytes without capacity check. Caller must ensure buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteTwoBytesUnsafe(byte b1, byte b2)
|
|
{
|
|
_buffer[_position++] = b1;
|
|
_buffer[_position++] = b2;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteBytes(ReadOnlySpan<byte> data)
|
|
{
|
|
EnsureCapacity(data.Length);
|
|
WriteBytesUnsafe(data);
|
|
}
|
|
|
|
/// <summary>Writes a span of bytes without capacity check. Caller must ensure buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteBytesUnsafe(ReadOnlySpan<byte> data)
|
|
{
|
|
data.CopyTo(_buffer.AsSpan(_position));
|
|
_position += data.Length;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteRaw<T>(T value) where T : unmanaged
|
|
{
|
|
EnsureCapacity(Unsafe.SizeOf<T>());
|
|
WriteRawUnsafe(value);
|
|
}
|
|
|
|
/// <summary>Writes an unmanaged value without capacity check. Caller must ensure buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteRawUnsafe<T>(T value) where T : unmanaged
|
|
{
|
|
Unsafe.WriteUnaligned(ref _buffer[_position], value);
|
|
_position += Unsafe.SizeOf<T>();
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged
|
|
{
|
|
EnsureCapacity(1 + Unsafe.SizeOf<T>());
|
|
WriteTypeCodeAndRawUnsafe(typeCode, value);
|
|
}
|
|
|
|
/// <summary>Writes a type code + unmanaged value without capacity check. Caller must ensure buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteTypeCodeAndRawUnsafe<T>(byte typeCode, T value) where T : unmanaged
|
|
{
|
|
_buffer[_position++] = typeCode;
|
|
Unsafe.WriteUnaligned(ref _buffer[_position], value);
|
|
_position += Unsafe.SizeOf<T>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region VarInt Encoding — inline
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteVarUInt(uint value)
|
|
{
|
|
//if (FastWire) { WriteRaw(value); return; }
|
|
if (value < 0x80)
|
|
{
|
|
if (_position >= _bufferEnd)
|
|
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
|
|
_buffer[_position++] = (byte)value;
|
|
return;
|
|
}
|
|
EnsureCapacity(5);
|
|
WriteVarUIntMultiByteUnsafe(value);
|
|
}
|
|
|
|
/// <summary>Writes a VarUInt without capacity check. Caller must ensure at least 5 bytes of buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteVarUIntUnsafe(uint value)
|
|
{
|
|
if (value < 0x80)
|
|
{
|
|
_buffer[_position++] = (byte)value;
|
|
return;
|
|
}
|
|
WriteVarUIntMultiByteUnsafe(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private void WriteVarUIntMultiByteUnsafe(uint value)
|
|
{
|
|
while (value >= 0x80)
|
|
{
|
|
_buffer[_position++] = (byte)(value | 0x80);
|
|
value >>= 7;
|
|
}
|
|
_buffer[_position++] = (byte)value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteVarInt(int value)
|
|
{
|
|
//if (FastWire) { WriteRaw(value); return; }
|
|
var encoded = (uint)((value << 1) ^ (value >> 31));
|
|
WriteVarUInt(encoded);
|
|
}
|
|
|
|
/// <summary>Writes a zigzag-encoded VarInt without capacity check. Caller must ensure at least 5 bytes of buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteVarIntUnsafe(int value)
|
|
{
|
|
var encoded = (uint)((value << 1) ^ (value >> 31));
|
|
WriteVarUIntUnsafe(encoded);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteVarULong(ulong value)
|
|
{
|
|
//if (FastWire) { WriteRaw(value); return; }
|
|
if (value < 0x80)
|
|
{
|
|
if (_position >= _bufferEnd)
|
|
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
|
|
_buffer[_position++] = (byte)value;
|
|
return;
|
|
}
|
|
EnsureCapacity(10);
|
|
WriteVarULongMultiByteUnsafe(value);
|
|
}
|
|
|
|
/// <summary>Writes a VarULong without capacity check. Caller must ensure at least 10 bytes of buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteVarULongUnsafe(ulong value)
|
|
{
|
|
if (value < 0x80)
|
|
{
|
|
_buffer[_position++] = (byte)value;
|
|
return;
|
|
}
|
|
WriteVarULongMultiByteUnsafe(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private void WriteVarULongMultiByteUnsafe(ulong value)
|
|
{
|
|
while (value >= 0x80)
|
|
{
|
|
_buffer[_position++] = (byte)(value | 0x80);
|
|
value >>= 7;
|
|
}
|
|
_buffer[_position++] = (byte)value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteVarLong(long value)
|
|
{
|
|
//if (FastWire) { WriteRaw(value); return; }
|
|
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
|
WriteVarULong(encoded);
|
|
}
|
|
|
|
/// <summary>Writes a zigzag-encoded VarLong without capacity check. Caller must ensure at least 10 bytes of buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteVarLongUnsafe(long value)
|
|
{
|
|
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
|
WriteVarULongUnsafe(encoded);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Specialized Types — inline
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteDecimalBits(decimal value)
|
|
{
|
|
EnsureCapacity(16);
|
|
WriteDecimalBitsUnsafe(value);
|
|
}
|
|
|
|
/// <summary>Writes 16-byte decimal bits without capacity check. Caller must ensure buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteDecimalBitsUnsafe(decimal value)
|
|
{
|
|
Span<int> bits = stackalloc int[4];
|
|
decimal.TryGetBits(value, bits, out _);
|
|
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
|
|
_position += 16;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteDateTimeBits(DateTime value)
|
|
{
|
|
EnsureCapacity(9);
|
|
WriteDateTimeBitsUnsafe(value);
|
|
}
|
|
|
|
/// <summary>Writes 9-byte DateTime (Ticks + Kind) without capacity check. Caller must ensure buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteDateTimeBitsUnsafe(DateTime value)
|
|
{
|
|
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
|
|
_buffer[_position + 8] = (byte)value.Kind;
|
|
_position += 9;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteGuidBits(Guid value)
|
|
{
|
|
EnsureCapacity(16);
|
|
WriteGuidBitsUnsafe(value);
|
|
}
|
|
|
|
/// <summary>Writes 16-byte Guid without capacity check. Caller must ensure buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteGuidBitsUnsafe(Guid value)
|
|
{
|
|
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
|
|
_position += 16;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteDateTimeOffsetBits(DateTimeOffset value)
|
|
{
|
|
EnsureCapacity(10);
|
|
WriteDateTimeOffsetBitsUnsafe(value);
|
|
}
|
|
|
|
/// <summary>Writes 10-byte DateTimeOffset (UtcTicks + Offset) without capacity check. Caller must ensure buffer space.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteDateTimeOffsetBitsUnsafe(DateTimeOffset value)
|
|
{
|
|
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
|
|
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
|
|
_position += 10;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region String Writes — inline
|
|
|
|
public void WriteStringUtf8(string value)
|
|
{
|
|
if (FastWire)
|
|
{
|
|
// UTF-16: char count (VarUInt) + raw char data (zero-encoding memcopy)
|
|
var charLen = value.Length;
|
|
var byteLen = charLen * 2;
|
|
WriteVarUInt((uint)charLen);
|
|
EnsureCapacity(byteLen);
|
|
MemoryMarshal.AsBytes(value.AsSpan()).CopyTo(_buffer.AsSpan(_position, byteLen));
|
|
_position += byteLen;
|
|
return;
|
|
}
|
|
|
|
var charLength = value.Length;
|
|
|
|
// Speculative ASCII fast path: assume byteCount == charLength
|
|
// Single-pass Ascii.FromUtf16 (scan+copy combined) instead of
|
|
// Ascii.IsValid (scan) + Ascii.FromUtf16 (scan+copy) = double traversal
|
|
var savedPosition = _position;
|
|
|
|
WriteVarUInt((uint)charLength);
|
|
EnsureCapacity(charLength);
|
|
|
|
if (Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, charLength), out _) == OperationStatus.Done)
|
|
{
|
|
_position += charLength;
|
|
return;
|
|
}
|
|
|
|
// Non-ASCII fallback: rewind VarUInt, encode with UTF-8
|
|
// FromUtf16 fails fast on first non-ASCII char, so speculative cost is minimal
|
|
_position = savedPosition;
|
|
var byteCount = Utf8NoBom.GetByteCount(value);
|
|
|
|
WriteVarUInt((uint)byteCount);
|
|
EnsureCapacity(byteCount);
|
|
|
|
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
|
|
_position += byteCount;
|
|
}
|
|
|
|
public void WriteFixStr(string value)
|
|
{
|
|
var length = value.Length;
|
|
EnsureCapacity(1 + length);
|
|
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
|
|
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
|
|
_position += length;
|
|
}
|
|
|
|
public void WriteFixStrDirect(string value)
|
|
{
|
|
var length = value.Length;
|
|
EnsureCapacity(1 + length);
|
|
|
|
var destSpan = _buffer.AsSpan(_position + 1, length);
|
|
var status = Ascii.FromUtf16(value.AsSpan(), destSpan, out var bytesWritten);
|
|
|
|
if (status == OperationStatus.Done && bytesWritten == length)
|
|
{
|
|
_buffer[_position] = BinaryTypeCode.EncodeFixStr(length);
|
|
_position += 1 + length;
|
|
}
|
|
else
|
|
{
|
|
_buffer[_position++] = BinaryTypeCode.String;
|
|
WriteStringUtf8Internal(value);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteFixStrBytes(ReadOnlySpan<byte> utf8Bytes)
|
|
{
|
|
var length = utf8Bytes.Length;
|
|
EnsureCapacity(1 + length);
|
|
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
|
|
utf8Bytes.CopyTo(_buffer.AsSpan(_position, length));
|
|
_position += length;
|
|
}
|
|
|
|
public void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name)
|
|
{
|
|
WriteByte(BinaryTypeCode.String);
|
|
WriteVarUInt((uint)utf8Name.Length);
|
|
WriteBytes(utf8Name);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void WriteStringUtf8Internal(string value)
|
|
{
|
|
var byteCount = Utf8NoBom.GetByteCount(value);
|
|
WriteVarUInt((uint)byteCount);
|
|
EnsureCapacity(byteCount);
|
|
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
|
|
_position += byteCount;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Bulk Array Writes — inline
|
|
|
|
public void WriteDoubleArrayBulk(ReadOnlySpan<double> span)
|
|
{
|
|
EnsureCapacity(span.Length * 9);
|
|
for (var i = 0; i < span.Length; i++)
|
|
{
|
|
_buffer[_position++] = BinaryTypeCode.Float64;
|
|
Unsafe.WriteUnaligned(ref _buffer[_position], span[i]);
|
|
_position += 8;
|
|
}
|
|
}
|
|
|
|
public void WriteFloatArrayBulk(ReadOnlySpan<float> span)
|
|
{
|
|
EnsureCapacity(span.Length * 5);
|
|
for (var i = 0; i < span.Length; i++)
|
|
{
|
|
_buffer[_position++] = BinaryTypeCode.Float32;
|
|
Unsafe.WriteUnaligned(ref _buffer[_position], span[i]);
|
|
_position += 4;
|
|
}
|
|
}
|
|
|
|
public void WriteGuidArrayBulk(ReadOnlySpan<Guid> span)
|
|
{
|
|
EnsureCapacity(span.Length * 17);
|
|
for (var i = 0; i < span.Length; i++)
|
|
{
|
|
_buffer[_position++] = BinaryTypeCode.Guid;
|
|
span[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
|
_position += 16;
|
|
}
|
|
}
|
|
|
|
public void WriteInt32ArrayOptimized(ReadOnlySpan<int> span)
|
|
{
|
|
for (var i = 0; i < span.Length; i++)
|
|
{
|
|
var value = span[i];
|
|
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
|
{
|
|
WriteByte(tiny);
|
|
}
|
|
else
|
|
{
|
|
WriteByte(BinaryTypeCode.Int32);
|
|
WriteVarInt(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void WriteLongArrayOptimized(ReadOnlySpan<long> span)
|
|
{
|
|
for (var i = 0; i < span.Length; i++)
|
|
{
|
|
var value = span[i];
|
|
if (value >= int.MinValue && value <= int.MaxValue)
|
|
{
|
|
var intValue = (int)value;
|
|
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
|
{
|
|
WriteByte(tiny);
|
|
}
|
|
else
|
|
{
|
|
WriteByte(BinaryTypeCode.Int32);
|
|
WriteVarInt(intValue);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
WriteByte(BinaryTypeCode.Int64);
|
|
WriteVarLong(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void WriteBytesSimd(ReadOnlySpan<byte> source)
|
|
{
|
|
EnsureCapacity(source.Length);
|
|
var destination = _buffer.AsSpan(_position, source.Length);
|
|
|
|
if (Vector.IsHardwareAccelerated && source.Length >= Vector<byte>.Count * 2)
|
|
{
|
|
var vectorSize = Vector<byte>.Count;
|
|
var i = 0;
|
|
var length = source.Length;
|
|
var vectorCount = length / vectorSize;
|
|
for (var v = 0; v < vectorCount; v++)
|
|
{
|
|
var vec = new Vector<byte>(source.Slice(i, vectorSize));
|
|
vec.CopyTo(destination.Slice(i, vectorSize));
|
|
i += vectorSize;
|
|
}
|
|
if (i < length)
|
|
source.Slice(i).CopyTo(destination.Slice(i));
|
|
}
|
|
else
|
|
{
|
|
source.CopyTo(destination);
|
|
}
|
|
|
|
_position += source.Length;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region String Interning
|
|
|
|
/// <summary>
|
|
/// Serialize pass: looks up interned string state.
|
|
/// Returns the entry ref for caller to check IsFirstWrite and update it.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public ref InternEntry GetInternedStringEntry(string value, out bool found)
|
|
{
|
|
if (_stringInternMap == null)
|
|
{
|
|
found = false;
|
|
return ref Unsafe.NullRef<InternEntry>();
|
|
}
|
|
|
|
if (_stringInternMap.TryAdd(value, out var slotIndex))
|
|
{
|
|
// Not in map (shouldn't happen after scan pass for cached strings)
|
|
found = false;
|
|
return ref _stringInternMap.GetValueRef(slotIndex);
|
|
}
|
|
|
|
found = true;
|
|
return ref _stringInternMap.GetValueRef(slotIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scan pass: tracks a string for interning. Assigns CacheIndex immediately on 2nd occurrence.
|
|
/// Builds WriteDuplicateEntry for all duplicate occurrences.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void ScanInternString(string value)
|
|
{
|
|
var visitIndex = ScanVisitIndex++;
|
|
|
|
_stringInternMap ??= new IdentityMap<string, InternEntry>();
|
|
|
|
if (!_stringInternMap.TryAdd(value, out var slotIndex))
|
|
{
|
|
// 2+ occurrence: assign CacheIndex immediately
|
|
ref var entry = ref _stringInternMap.GetValueRef(slotIndex);
|
|
if (entry.CacheIndex == -1)
|
|
{
|
|
// 2nd occurrence: assign CacheIndex + add StringFirst entry at first visit position
|
|
entry.CacheIndex = _nextCacheIndex++;
|
|
entry.IsFirstWrite = true;
|
|
AddWriteDuplicateEntry(entry.FirstIndex, entry.CacheIndex, isFirst: true, value);
|
|
}
|
|
// 2nd+ occurrence: add StringRef entry at current position
|
|
AddWriteDuplicateEntry(visitIndex, entry.CacheIndex, isFirst: false, value: null);
|
|
return;
|
|
}
|
|
|
|
// 1st occurrence: store scan visit index, CacheIndex = -1 (not cached yet)
|
|
ref var newEntry = ref _stringInternMap.GetValueRef(slotIndex);
|
|
newEntry.FirstIndex = visitIndex;
|
|
newEntry.CacheIndex = -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if there are any interned strings that occurred more than once.
|
|
/// </summary>
|
|
public bool HasInternedStrings => _stringInternMap != null && _stringInternMap.Count > 0;
|
|
|
|
/// <summary>
|
|
/// Gets the count of cached values (string intern + object ref that occurred more than once).
|
|
/// </summary>
|
|
public int GetCacheCount() => _nextCacheIndex;
|
|
|
|
#endregion
|
|
|
|
#region Polymorphic Type Prefix
|
|
|
|
/// <summary>
|
|
/// Writes a polymorphic type prefix when the runtime type differs from the declared property type.
|
|
/// <para>
|
|
/// When <paramref name="cachedObjectCacheIndex"/> is -1 (default): PREFIX markers.
|
|
/// An inner Object/Array/Dict marker follows.
|
|
/// First type occurrence: ObjectWithTypeName (68) + typename
|
|
/// Cached type: ObjectWithTypeIndex (70) + typeIndex
|
|
/// </para>
|
|
/// <para>
|
|
/// When <paramref name="cachedObjectCacheIndex"/> >= 0: COMBINED markers.
|
|
/// Object body follows directly (no inner Object/ObjectRefFirst marker).
|
|
/// First type occurrence: ObjectWithTypeNameRefFirst (69) + typename + refCacheIndex
|
|
/// Cached type: ObjectWithTypeIndexRefFirst (71) + typeIndex + refCacheIndex
|
|
/// </para>
|
|
/// </summary>
|
|
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WritePolymorphicPrefix(Type runtimeType, int cachedObjectCacheIndex = -1)
|
|
{
|
|
var rtWrapper = GetWrapper(runtimeType);
|
|
if (!rtWrapper.PolymorphicSeen)
|
|
{
|
|
rtWrapper.PolymorphicSeen = true;
|
|
rtWrapper.PolymorphicCacheIndex = _nextTypeSlot++;
|
|
if (cachedObjectCacheIndex >= 0)
|
|
{
|
|
WriteByte(BinaryTypeCode.ObjectWithTypeNameRefFirst);
|
|
WriteStringUtf8(runtimeType.FullName!);
|
|
WriteVarUInt((uint)cachedObjectCacheIndex);
|
|
}
|
|
else
|
|
{
|
|
WriteByte(BinaryTypeCode.ObjectWithTypeName);
|
|
WriteStringUtf8(runtimeType.FullName!);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (cachedObjectCacheIndex >= 0)
|
|
{
|
|
WriteByte(BinaryTypeCode.ObjectWithTypeIndexRefFirst);
|
|
WriteVarUInt((uint)rtWrapper.PolymorphicCacheIndex);
|
|
WriteVarUInt((uint)cachedObjectCacheIndex);
|
|
}
|
|
else
|
|
{
|
|
WriteByte(BinaryTypeCode.ObjectWithTypeIndex);
|
|
WriteVarUInt((uint)rtWrapper.PolymorphicCacheIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region UseMetadata Type Tracking
|
|
|
|
/// <summary>
|
|
/// Registers a type for UseMetadata first/repeated tracking.
|
|
/// Returns true on first occurrence (caller should write inline property hashes),
|
|
/// false on repeated (caller writes only typeNameHash).
|
|
/// Used by both runtime and SGen paths via wrapper.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool RegisterMetadataType(TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper)
|
|
{
|
|
if (wrapper.MetadataSeen)
|
|
return false;
|
|
|
|
wrapper.MetadataSeen = true;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inline metadata kiírása az ObjectWithMetadata marker után.
|
|
/// Első előfordulás: [propNameHash (4b)][propCount (VarUInt)][hash0 (4b)][hash1 (4b)]...
|
|
/// Ismételt: [propNameHash (4b)]
|
|
/// </summary>
|
|
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteInlineMetadata(BinarySerializeTypeMetadata metadata, bool isFirstOccurrence)
|
|
{
|
|
WriteRaw(metadata.PropNameHash);
|
|
|
|
if (isFirstOccurrence)
|
|
{
|
|
var hashes = metadata.MetadataPropertyHashes;
|
|
WriteVarUInt((uint)hashes.Length);
|
|
for (var i = 0; i < hashes.Length; i++)
|
|
{
|
|
WriteRaw(hashes[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Property State Buffer
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public byte[] RentPropertyStateBuffer(int size)
|
|
{
|
|
if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= size)
|
|
{
|
|
return _propertyStateBuffer;
|
|
}
|
|
|
|
if (_propertyStateBuffer != null)
|
|
{
|
|
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
|
}
|
|
|
|
_propertyStateBuffer = ArrayPool<byte>.Shared.Rent(size);
|
|
return _propertyStateBuffer;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void ReturnPropertyStateBuffer(byte[] buffer)
|
|
{
|
|
// Buffer stays cached for reuse.
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Property Filtering
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool ShouldSerializeProperty(object instance, BinaryPropertyAccessor property)
|
|
{
|
|
if (PropertyFilter == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var context = new BinaryPropertyFilterContext(
|
|
instance,
|
|
property.DeclaringType,
|
|
property.Name,
|
|
property.PropertyType,
|
|
property.DynamicGetter);
|
|
|
|
return PropertyFilter(context);
|
|
}
|
|
|
|
public bool CheckDuplicatePropName => Options.CheckDuplicatePropName;
|
|
|
|
#endregion
|
|
|
|
#region Header
|
|
|
|
/// <summary>
|
|
/// Writes the binary header directly. Call AFTER ScanForDuplicates (cacheCount is known).
|
|
/// No placeholder, no shift — single forward write.
|
|
/// Layout: [version (1b)][flags (1b)][cacheCount (VarUInt, if caching)]
|
|
/// </summary>
|
|
public void WriteHeader()
|
|
{
|
|
var flags = BinaryTypeCode.HeaderFlagsBase;
|
|
if (UseMetadata)
|
|
flags |= BinaryTypeCode.HeaderFlag_Metadata;
|
|
if (ReferenceHandling == ReferenceHandlingMode.OnlyId)
|
|
flags |= BinaryTypeCode.HeaderFlag_RefHandling_OnlyId;
|
|
else if (ReferenceHandling == ReferenceHandlingMode.All)
|
|
flags |= (byte)(BinaryTypeCode.HeaderFlag_RefHandling_OnlyId | BinaryTypeCode.HeaderFlag_RefHandling_All);
|
|
if (HasCaching)
|
|
flags |= BinaryTypeCode.HeaderFlag_HasCacheCount;
|
|
|
|
WriteByte(AcBinarySerializerOptions.FormatVersion);
|
|
WriteByte(flags);
|
|
|
|
if (HasCaching)
|
|
{
|
|
WriteVarUInt((uint)GetCacheCount());
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void ClearAndTrimIfNeeded<TKey, TValue>(Dictionary<TKey, TValue>? dict, int maxCapacity)
|
|
where TKey : notnull
|
|
{
|
|
if (dict == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
dict.Clear();
|
|
if (dict.EnsureCapacity(0) > maxCapacity)
|
|
{
|
|
dict.TrimExcess();
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void ClearAndTrimIfNeeded<T>(HashSet<T>? set, int maxCapacity)
|
|
{
|
|
if (set == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
set.Clear();
|
|
if (set.EnsureCapacity(0) > maxCapacity)
|
|
{
|
|
set.TrimExcess();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|