AyCode.Core/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySe...

1506 lines
65 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Buffers;
using System.Collections.Concurrent;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
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 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 &lt; _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.
/// <summary>
/// Global recursion depth counter — final safety net against pathological graphs and non-IId cycles.
/// Incremented at <see cref="EnterRecursion"/> (before marker write at each object-recursion entry),
/// decremented at <see cref="ExitRecursion"/> (at WriteProperties/ScanObject body exit).
/// Checked against <see cref="AcSerializerContextBase{TMetadata,TOptions}.MaxDepth"/> (byte, default 255)
/// inside <see cref="TryHandleMaxDepth"/> which fires BEFORE any marker byte is written.
/// </summary>
private byte _recursionDepth;
/// <summary>
/// Pre-computed depth-check gate, set at <see cref="Reset"/>: <c>MaxDepthBehavior != Disable</c>.
/// Only the explicit <see cref="MaxDepthBehavior.Disable"/> opts out. <see cref="MaxDepthBehavior.Throw"/>
/// stays active even with <c>HasAllRefHandling=true</c>, because per-type <c>EnableRefHandling=false</c>
/// attribute opt-outs leave gaps that ref-handling alone cannot cover (cycle through a non-tracked type).
/// </summary>
private bool _needsDepthCheck;
/// <summary>
/// Pre-computed action when depth reaches MaxDepth. Consulted on the rare depth-limit-hit cold path only.
/// </summary>
private MaxDepthBehavior _maxDepthAction;
#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 &lt;&lt; (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>
/// <remarks>
/// The lambda's <c>t</c> parameter explicitly declares DAMs to match the
/// <see cref="MetadataFactoryDelegate"/> signature — DAMs do not auto-propagate from the
/// delegate type to lambda parameters.
/// </remarks>
protected override MetadataFactoryDelegate MetadataFactory
=> static ([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] 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;
// Pre-compute depth-check gate. Only `MaxDepthBehavior.Disable` opts out:
// - `Throw` (default): safety net. `HasAllRefHandling=true` is NOT enough on its own — per-type
// `EnableRefHandling=false` attribute opt-outs leave non-tracked types that can still form cycles.
// The safety net catches those + pathological-depth non-cyclic graphs.
// - `Truncate`: explicit developer intent for intentional shallow serialization (delta updates,
// view-model projections). Always active when set, regardless of ref-handling mode.
// - `Disable`: explicit opt-out — dev guarantees cycle-free + bounded-depth graph.
// (MaxDepth value itself is NOT an opt-out — 0 means shallow-copy semantic when paired with Truncate.)
_maxDepthAction = Options.MaxDepthBehavior;
_needsDepthCheck = _maxDepthAction != MaxDepthBehavior.Disable;
_recursionDepth = 0;
}
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;
_recursionDepth = 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();
}
/// <summary>
/// Hot wrapper: combined depth check + recursion enter. Gated by <c>_needsDepthCheck</c>.
/// On limit hit: dispatches to <see cref="OnMaxDepthHit"/> (truncate writes Null or throw) and returns
/// <c>true</c> — caller must skip marker write + recursive call. NO inc on this path.
/// On miss: increments <c>_recursionDepth</c> and returns <c>false</c> — caller proceeds with marker write
/// and recursive body. <see cref="ExitRecursion"/> at body exit balances the inc.
/// Called BEFORE any marker write — wire-correct for any marker width.
/// 2nd-occurrence ObjectRef paths (which don't actually recurse into a body) must call
/// <see cref="ExitRecursion"/> to undo the inc that this method did on the success path.
/// Single ctx field-read pattern: 2 ops merged into 1 method call vs the prior 3-method split.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryEnterRecursion(bool hasTruncatePath)
{
if (!_needsDepthCheck) return false;
if (_recursionDepth >= _maxDepth)
{
OnMaxDepthHit(hasTruncatePath);
return true;
}
_recursionDepth++;
return false;
}
/// <summary>
/// Dec <c>_recursionDepth</c> if depth tracking is active. Called at WriteProperties/ScanObject body exit
/// to balance the corresponding <see cref="TryEnterRecursion"/> call on its success path.
/// Also called on 2nd-occurrence ObjectRef paths in <c>WriteObjectFullMarker*</c> / <c>WriteObjectRefMarker*</c>
/// to undo the inc (2nd-occ writes ObjectRef without recursing — wrote no body, must dec back).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void ExitRecursion()
{
if (_needsDepthCheck) _recursionDepth--;
}
/// <summary>
/// Cold path: dispatches on <c>_maxDepthAction</c> when the depth limit is hit.
/// <list type="bullet">
/// <item><c>Throw</c>: throws <see cref="InvalidOperationException"/> (offending type name comes from the stack trace)</item>
/// <item><c>Truncate</c> + <paramref name="hasTruncatePath"/>=<c>true</c> (write pass): writes <c>Null</c> marker in place of the object — intentional shallow serialization. Wire-correct: no marker has been written yet, so no rewind needed</item>
/// <item><c>Truncate</c> + <paramref name="hasTruncatePath"/>=<c>false</c> (scan pass): no-op — caller's return skips children scan</item>
/// </list>
/// <c>NoInlining</c> keeps the throw / WriteByte body out of the hot caller (smaller i-cache footprint).
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private void OnMaxDepthHit(bool hasTruncatePath)
{
if (_maxDepthAction == MaxDepthBehavior.Throw)
throw new InvalidOperationException(
$"AcBinary: recursion depth exceeded MaxDepth={_maxDepth} (depth={_recursionDepth}, position={Position})");
// Truncate: write Null in place of the object. No rewind — check fires BEFORE any marker write.
if (hasTruncatePath) WriteByte(BinaryTypeCode.Null);
}
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
/// <summary>
/// Unchecked <c>ref byte</c> into <see cref="_buffer"/> at <paramref name="position"/> — omits the
/// JIT array bounds-check that a plain <c>_buffer[position]</c> index emits on every write.
/// </summary>
/// <remarks>
/// Safe by the buffer invariant: every write primitive first guarantees <c>position &lt; _bufferEnd</c>
/// (the <c>_position &lt; _bufferEnd</c> grow-guard or <see cref="EnsureCapacity"/>), and <c>_bufferEnd</c>
/// is "one past the last writable index in <c>_buffer</c>" ⇒ <c>_bufferEnd &lt;= _buffer.Length</c>.
/// So <c>position &lt; _buffer.Length</c> already holds — the capacity guard IS the bounds check.
/// The JIT cannot see the <c>_bufferEnd &lt;= _buffer.Length</c> relation, so <c>_buffer[position]</c>
/// would emit a second, redundant <c>cmp/jae</c> per write; this helper removes it. DEBUG builds keep
/// an explicit guard so a misbehaving <c>IBinaryOutputBase</c> surfaces loudly, not as corruption.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref byte BufferAt(int position)
{
#if DEBUG
if ((uint)position >= (uint)_buffer.Length)
throw new InvalidOperationException(
$"BufferAt({position}) out of range — buffer invariant violated " +
$"(_buffer.Length={_buffer.Length}, _bufferEnd={_bufferEnd}).");
#endif
return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), (nint)(uint)position);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacity(int additionalBytes)
{
if (_position + additionalBytes > _bufferEnd) GrowAndValidate(additionalBytes);
}
/// <summary>
/// Slow path for <see cref="EnsureCapacity"/>: invokes <c>Output.Grow</c> and revalidates that
/// the request was actually satisfied. A chunk-limited output (e.g. <c>AsyncPipeWriterOutput</c>
/// on a single-value write &gt; <c>MaxChunkSize</c> data bytes) may return WITHOUT growing the
/// active buffer to the requested size — silent under-provisioning would cause downstream
/// <c>ArgumentOutOfRangeException</c> in <c>AsSpan</c>/<c>CopyTo</c> at corrupt offsets.
/// <c>NoInlining</c> keeps the hot path of <see cref="EnsureCapacity"/> tiny and inline-friendly.
/// Related limit: see <c>BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-b7k9</c>.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowAndValidate(int additionalBytes)
{
Output.Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes);
var available = _bufferEnd - _position;
if (available < additionalBytes) ThrowGrowFailedToSatisfy(additionalBytes, available);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteByte(byte value)
{
if (_position >= _bufferEnd) GrowOne();
BufferAt(_position++) = value;
}
/// <summary>
/// Cold-path single-byte grow helper. Outlined from the hot-path 1-byte writers
/// (<see cref="WriteByte"/>, <see cref="WriteVarUInt"/> / <see cref="WriteVarULong"/> fast-paths)
/// so the inliner cost-models the hot path WITHOUT the 4-ref-arg <c>Output.Grow</c> call's
/// argument-prep IL. This keeps the per-property hot-path tight enough for AOT to inline the
/// write into the source-gen <c>WriteProperties</c> body across many call-sites — the cumulative
/// gain shows up on graph-heavy cells where every leaf serialises ~6 primitive properties.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowOne() => Output.Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
[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)
{
BufferAt(_position++) = b1;
BufferAt(_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 BufferAt(_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
{
BufferAt(_position++) = typeCode;
Unsafe.WriteUnaligned(ref BufferAt(_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) GrowOne();
BufferAt(_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)
{
BufferAt(_position++) = (byte)value;
return;
}
WriteVarUIntMultiByteUnsafe(value);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void WriteVarUIntMultiByteUnsafe(uint value)
{
while (value >= 0x80)
{
BufferAt(_position++) = (byte)(value | 0x80);
value >>= 7;
}
BufferAt(_position++) = (byte)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int VarUIntSize(uint value)
{
return value switch
{
< 0x80 => 1,
< 0x4000 => 2,
< 0x200000 => 3,
< 0x10000000 => 4,
_ => 5
};
}
[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) GrowOne();
BufferAt(_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)
{
BufferAt(_position++) = (byte)value;
return;
}
WriteVarULongMultiByteUnsafe(value);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void WriteVarULongMultiByteUnsafe(ulong value)
{
while (value >= 0x80)
{
BufferAt(_position++) = (byte)(value | 0x80);
value >>= 7;
}
BufferAt(_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 BufferAt(_position), value.Ticks);
BufferAt(_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 BufferAt(_position), value.UtcTicks);
Unsafe.WriteUnaligned(ref BufferAt(_position + 8), (short)value.Offset.TotalMinutes);
_position += 10;
}
#endregion
#region String Writes inline
/// <summary>
/// FastWire markerless string write — int32 sentinel header. Self-contained: handles all three
/// states (null / empty / content) via int32 dispatch. <c>-1</c> = null, <c>0</c> = empty,
/// <c>N &gt; 0</c> = content (followed by N×2 UTF-16 raw bytes). Saves 1 byte per content string vs
/// the markered <see cref="WriteStringWithDispatch"/> StringSmall scheme; null/empty pay +3 bytes
/// (4-byte int32 vs 1-byte marker), but null/empty are rare in typical workloads → net wire-size win.
/// Companion reader is <see cref="BinaryDeserializationContext{TInput}.ReadStringUtf16Markerless"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteStringUtf16Markerless(string? value)
{
if (value == null) { WriteRaw(-1); return; }
var charLength = value.Length;
if (charLength == 0) { WriteRaw(0); return; }
var byteLenF = charLength * 2;
EnsureCapacity(4 + byteLenF);
Unsafe.WriteUnaligned(ref BufferAt(_position), charLength);
_position += 4;
MemoryMarshal.AsBytes(value.AsSpan()).CopyTo(_buffer.AsSpan(_position, byteLenF));
_position += byteLenF;
}
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;
}
// D-2 + tight reserve: single-pass UTF-8 encode with input-bound-aware VarUInt slot.
// Replaces the prior try-ASCII-then-rewind-and-encode-UTF-8 pattern (1 scan ASCII / 3 scans
// non-ASCII) with a single GetBytes call that works identically for both content classes.
//
// Layout: [reserved N bytes for VarUInt][UTF-8 bytes...]
// 1. Compute worst-case byte count from charLength (UTF-8 max = 4 bytes/char) and the
// VarUInt size needed for that upper bound. For charLength ≤ 31, reserveSize = 1
// (since 4*31 = 124 < 128 ⇒ VarUInt(124) = 1 byte). Most short strings hit this.
// 2. EnsureCapacity for reserveSize + maxBytes.
// 3. GetBytes directly into buffer at savedPos+reserveSize → returns exact byteCount.
// 4. If actual VarUInt < reserveSize (rare), memmove encoded bytes left to compact.
// 5. WriteVarUInt at savedPos and advance.
//
// Win vs the prior fixed-5-byte reserve: short strings (the common case) skip the memmove
// entirely. For 32-char strings the reserve is 2 bytes; if actual byteCount < 128 we
// memmove a smaller distance (1 byte) than the prior fixed approach (4 bytes).
//
// Span<byte>.CopyTo is overlap-safe via Buffer.Memmove on byte arrays.
var charLength = value.Length;
// Tight UTF-8 upper bound for valid UTF-16 input: max 3 bytes per UTF-16 code unit.
var maxBytes = charLength * 3;
var reserveSize = VarUIntSize((uint)maxBytes);
EnsureCapacity(reserveSize + maxBytes);
var savedPos = _position;
var encodeStart = savedPos + reserveSize;
System.Text.Unicode.Utf8.FromUtf16(value.AsSpan(), _buffer.AsSpan(encodeStart, maxBytes), out _, out var bytesWritten, replaceInvalidSequences: false);
var actualVarUIntSize = VarUIntSize((uint)bytesWritten);
if (actualVarUIntSize < reserveSize)
{
var shift = reserveSize - actualVarUIntSize;
_buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(encodeStart - shift, bytesWritten));
}
_position = savedPos;
WriteVarUIntUnsafe((uint)bytesWritten); // advances _position by actualVarUIntSize
_position += bytesWritten;
}
/// <summary>
/// Writes a non-empty UTF-8 string in a shift-free layout with an unsigned excess slot.
/// </summary>
/// <remarks>
/// Header is fully determined before encode:
/// <list type="bullet">
/// <item><c>charLength &lt;= 31</c>: <c>[FixStr(marker carries charLength)][unsigned excess:1]</c></item>
/// <item><c>charLength &gt; 31</c>: <c>[StringLen8|StringLen16|StringLen32][charLength:1|2|4][unsigned excess:1|2|4]</c></item>
/// </list>
/// Body is UTF-8-encoded exactly once to the final destination (<c>encodeStart</c>) — no post-encode
/// body shift/copy. For the current path, <c>excess = bytesWritten - charLength</c> is expected to be
/// non-negative (ASCII=0, UTF-8=&gt;0). UTF-16 signed-negative slot usage remains on the existing FastWire
/// path for now and is intentionally not activated in this method.
///
/// <para>Caller MUST guarantee non-empty input (<c>value.Length &gt; 0</c>) — empty strings are handled by
/// the higher-level <c>WriteString</c> via the <c>StringEmpty</c> marker.</para>
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteStringWithDispatch(string value)
{
var charLength = value.Length;
#if DEBUG
System.Diagnostics.Debug.Assert(charLength > 0, "WriteStringWithDispatch expects non-empty string; empty is handled by StringEmpty marker in WriteString.");
#endif
// Overflow guard (O7G2) — predict-friendly (always false on realistic input). NoInlining throw helper.
//if ((uint)charLength > BinaryTypeCode.MaxStringCharLength) ThrowStringTooLong(charLength);
// Tight UTF-8 upper bound for valid UTF-16 input: max 3 bytes per UTF-16 code unit.
var maxBytes = charLength * 3;
var isFixStr = charLength <= BinaryTypeCode.FixStrMaxLength;
var charLengthSize = isFixStr ? 0 : charLength <= byte.MaxValue ? 1 : charLength <= ushort.MaxValue ? 2 : 4;
// IMPORTANT: the slot VALUE (excess) is not known before UTF-8 encode, but the slot SIZE is.
// We reserve the slot by width (1/2/4) from charLength, so encodeStart is final and no body shift is needed.
var slotSize = isFixStr ? 1 : BinaryTypeCode.GetUniversalStringExcessSlotSize(charLength);
var headerSize = isFixStr ? 2 : 1 + charLengthSize + slotSize;
EnsureCapacity(headerSize + maxBytes);
var headerPos = _position;
var slotPos = isFixStr ? headerPos + 1 : headerPos + 1 + charLengthSize;
var encodeStart = headerPos + headerSize;
if (isFixStr)
{
// Universal short-form string marker with unsigned excess slot.
BufferAt(headerPos) = BinaryTypeCode.EncodeFixStr(charLength);
}
else
{
// Universal long-form markers with marker-coded charLength width.
if (charLengthSize == 1)
{
BufferAt(headerPos) = BinaryTypeCode.StringLen8;
BufferAt(headerPos + 1) = unchecked((byte)charLength);
}
else if (charLengthSize == 2)
{
BufferAt(headerPos) = BinaryTypeCode.StringLen16;
Unsafe.WriteUnaligned(ref BufferAt(headerPos + 1), unchecked((ushort)charLength));
}
else
{
BufferAt(headerPos) = BinaryTypeCode.StringLen32;
Unsafe.WriteUnaligned(ref BufferAt(headerPos + 1), charLength);
}
}
var status = System.Text.Unicode.Utf8.FromUtf16(value.AsSpan(), _buffer.AsSpan(encodeStart, maxBytes), out _, out var bytesWritten, replaceInvalidSequences: false);
var excess = bytesWritten - charLength;
if (status != OperationStatus.Done) ThrowStringEncodingFailed(status);
#if DEBUG
// With status==Done, UTF-8 path mathematically implies bytesWritten >= charLength.
System.Diagnostics.Debug.Assert(excess >= 0, "WriteStringWithDispatch invariant broken: UTF-8 path produced negative excess.");
#endif
// UTF16 branch remains on the existing FastWire path for now.
// Current universal slot is unsigned (ASCII=0, UTF8>0). If UTF16-via-slot is introduced later,
// the discriminator design must be revisited (separate flag/marker or signed slot variant).
if (slotSize == 1) Unsafe.WriteUnaligned(ref BufferAt(slotPos), unchecked((byte)excess));
else if (slotSize == 2) Unsafe.WriteUnaligned(ref BufferAt(slotPos), unchecked((ushort)excess));
else Unsafe.WriteUnaligned(ref BufferAt(slotPos), excess);
_position = encodeStart + bytesWritten;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowStringEncodingFailed(OperationStatus status) =>
throw new InvalidOperationException(
$"String UTF-8 encode failed in WriteStringWithDispatch: status={status}. " +
"This indicates an unexpected encoder failure (e.g. destination sizing or invalid input state)." );
/// <summary>
/// Writes the first-occurrence body of an interned string with H2Q6 tier-marker dispatch.
/// Used by the runtime/SGen string-intern write path; subsequent occurrences use cache-index ref.
/// </summary>
/// <remarks>
/// Wire layout per tier:
/// <list type="bullet">
/// <item><c>StringInternFirstSmall</c>: <c>[marker:1][cacheIdx:VarUInt][charLen:8][utf8Len:8][bytes]</c> — utf8Len ≤ 255</item>
/// <item><c>StringInternFirstMedium</c>: <c>[marker:1][cacheIdx:VarUInt][charLen:16][utf8Len:16][bytes]</c> — utf8Len ≤ 65535</item>
/// </list>
///
/// <para>Big tier never engages — <c>MaxStringInternLength</c> is byte-typed in
/// <c>AcBinarySerializerOptions</c> (abszolút max 255 char × 4 byte/char = 1020 byte fits in Medium).</para>
///
/// <para>Tier prediction by <c>charLength</c>: ≤ 63 char → Small (worst-case 252 byte ≤ 255);
/// &gt; 63 char → Medium. Body is left-shifted by 2 bytes only when a long mostly-ASCII interning
/// string drops back into Small tier (rare).</para>
/// </remarks>
public void WriteStringInternFirstWithDispatch(string value, int cacheMapIndex)
{
// Post-encode tier choice (wire-optimal): mostly-ASCII interning string in the 64+ char band
// emits Small tier (3 byte) when bytesWritten ≤ 255, instead of Medium (5 byte). Big tier
// never engages — MaxStringInternLength byte-typed (max 255 char × 4 byte = 1020 byte fits in Medium).
var charLength = value.Length;
// Overflow guard (defensive — interning length is byte-typed so this should never trigger,
// but stays consistent with WriteStringWithDispatch and protects against future refactors).
if ((uint)charLength > BinaryTypeCode.MaxStringCharLength) ThrowStringTooLong(charLength);
var maxBytes = charLength * 4;
var cacheIdxSize = VarUIntSize((uint)cacheMapIndex);
// reserveHeader: charLength ≤ 63 → guaranteed Small (252 byte ≤ 255); else Medium-reserve.
var reserveHeader = charLength <= 63 ? 3 : 5;
EnsureCapacity(cacheIdxSize + reserveHeader + maxBytes);
var savedPos = _position;
var encodeStart = savedPos + cacheIdxSize + reserveHeader;
System.Text.Unicode.Utf8.FromUtf16(value.AsSpan(), _buffer.AsSpan(encodeStart, maxBytes), out _, out var bytesWritten, replaceInvalidSequences: false);
// Choose tier from actual bytesWritten (smallest fits)
var actualHeader = bytesWritten <= 255 ? 3 : 5;
var tierMarker = actualHeader == 3 ? BinaryTypeCode.StringInternFirstSmall : BinaryTypeCode.StringInternFirstMedium;
var shift = reserveHeader - actualHeader;
if (shift > 0)
_buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(encodeStart - shift, bytesWritten));
// Write [marker][cacheIdx VarUInt][charLen + utf8Len header][bytes]
BufferAt(savedPos) = tierMarker;
_position = savedPos + 1;
WriteVarUIntUnsafe((uint)cacheMapIndex);
if (actualHeader == 3)
{
// Pack charLen:8 | utf8Len:8 → single ushort store
var packed = (ushort)(charLength | (bytesWritten << 8));
Unsafe.WriteUnaligned(ref BufferAt(_position), packed);
_position += 2;
}
else
{
// Pack charLen:16 | utf8Len:16 → single uint store, LE
var packed = (uint)charLength | ((uint)bytesWritten << 16);
Unsafe.WriteUnaligned(ref BufferAt(_position), packed);
_position += 4;
}
_position += bytesWritten;
}
/// <summary>
/// Attempts to write <paramref name="value"/> through the string-interning protocol.
/// Reads and immediately resets <see cref="StringInternEligible"/>; when the property is
/// intern-eligible and the write-plan cursor yields an entry, emits the InternFirst /
/// InternRef wire form and returns <c>true</c>. Returns <c>false</c> when the string must
/// be written by the regular tier dispatch — the caller then invokes
/// <see cref="WriteStringWithDispatch"/>.
/// </summary>
/// <remarks>
/// Extracted from the runtime <c>WriteString</c> interning block (K9M3-style hoist) so the
/// SGen string-property emit can call it directly — no <c>WriteStringGenerated</c> /
/// <c>WriteString</c> hop. Caller guarantees non-null, non-empty content. The unconditional
/// flag reset prevents the per-property <see cref="StringInternEligible"/> from leaking into
/// subsequent string writes.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryWriteInternedString(string value)
{
// Read and immediately reset — prevents the flag leaking to subsequent string writes
// (TryWritePrimitive, WriteDictionary, or when IsValidForInterningString is false).
var internEligible = StringInternEligible;
StringInternEligible = false;
if (internEligible && IsValidForInterningString(value.Length))
{
if (TryConsumeWritePlanEntry(out var planEntry))
{
ValidateWritePlanString(in planEntry, value);
if (planEntry.IsFirst)
{
// H2Q6 v3 — StringInternFirst tier-marker dispatch (Small/Medium); charLen
// carried in header → 1-pass decode, no CountUtf8Chars Pass 1.
WriteStringInternFirstWithDispatch(planEntry.Value ?? value, planEntry.CacheMapIndex);
}
else
{
WriteStringInternRef(this, planEntry.CacheMapIndex);
}
return true;
}
// No plan entry → single occurrence, caller falls through to the tier dispatch.
#if DEBUG
OnStringInterned?.Invoke(null, value);
#endif
}
return false;
}
// ─────────────────────────────────────────────────────────────────
// V4N5 dead-code cleanup (2026-05-06): WriteFixStr, WriteFixStrDirect, WriteFixStrBytes,
// WritePreencodedPropertyName, and WriteStringUtf8Internal removed — these were unreachable
// (no core call site, no SourceGenerator template hit, no test, no reflection path).
// The hot-path string writes go through WriteStringWithDispatch (M3R7 + H2Q6 marker dispatch).
// ─────────────────────────────────────────────────────────────────
/// <summary>
/// Throw helper for the overflow guard in <see cref="WriteStringWithDispatch"/> and
/// <see cref="WriteStringInternFirstWithDispatch"/>. Marked <c>NoInlining</c> so the hot path
/// stays compact — the JIT/AOT keeps the throw-site out of the inlined caller body.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowStringTooLong(int charLength) =>
throw new InvalidOperationException(
$"String too long for binary serialization: {charLength} chars exceeds {BinaryTypeCode.MaxStringCharLength}. " +
$"This limit is dictated by the writer's worst-case 'charLength * 4' UTF-8 byte allocation; " +
$"larger inputs would silently overflow int arithmetic.");
/// <summary>
/// Throw helper for <see cref="GrowAndValidate"/>: invoked when <c>Output.Grow</c> returns
/// without satisfying the requested capacity (chunk-limited output, single value larger than
/// the per-chunk data capacity). Marked <c>NoInlining</c> so the throw-site stays out of the
/// inlined caller body. See <c>BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-b7k9</c> for fix direction.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowGrowFailedToSatisfy(int requested, int available) =>
throw new InvalidOperationException(
$"Output.Grow did not satisfy the requested capacity: {requested} bytes requested, only {available} bytes available after Grow. " +
$"This typically indicates a chunk-limited output (e.g. AsyncPipeWriterOutput) with a single serialized value exceeding the per-chunk data capacity. " +
$"See BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-b7k9.");
#endregion
#region Bulk Array Writes inline
public void WriteDoubleArrayBulk(ReadOnlySpan<double> span)
{
EnsureCapacity(span.Length * 9);
for (var i = 0; i < span.Length; i++)
{
BufferAt(_position++) = BinaryTypeCode.Float64;
Unsafe.WriteUnaligned(ref BufferAt(_position), span[i]);
_position += 8;
}
}
public void WriteFloatArrayBulk(ReadOnlySpan<float> span)
{
EnsureCapacity(span.Length * 5);
for (var i = 0; i < span.Length; i++)
{
BufferAt(_position++) = BinaryTypeCode.Float32;
Unsafe.WriteUnaligned(ref BufferAt(_position), span[i]);
_position += 4;
}
}
public void WriteGuidArrayBulk(ReadOnlySpan<Guid> span)
{
EnsureCapacity(span.Length * 17);
for (var i = 0; i < span.Length; i++)
{
BufferAt(_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 |= 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
}
}