193 lines
7.2 KiB
C#
193 lines
7.2 KiB
C#
using System.Runtime.CompilerServices;
|
|
|
|
namespace AyCode.Core.Extensions;
|
|
|
|
/// <summary>
|
|
/// Options for AcBinarySerializer and AcBinaryDeserializer.
|
|
/// Optimized for speed and memory efficiency over raw size.
|
|
/// </summary>
|
|
public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|
{
|
|
public override AcSerializerType SerializerType { get; init; } = AcSerializerType.Binary;
|
|
|
|
/// <summary>
|
|
/// Current binary format version. Incremented when breaking changes are made.
|
|
/// </summary>
|
|
public const byte FormatVersion = 1;
|
|
|
|
/// <summary>
|
|
/// Default options instance with metadata and string interning enabled.
|
|
/// </summary>
|
|
public static readonly AcBinarySerializerOptions Default = new();
|
|
|
|
/// <summary>
|
|
/// Options optimized for maximum speed (no metadata, no interning).
|
|
/// Use when deserializer knows the exact type structure.
|
|
/// </summary>
|
|
public static readonly AcBinarySerializerOptions FastMode = new()
|
|
{
|
|
UseMetadata = false,
|
|
UseStringInterning = false,
|
|
UseReferenceHandling = false
|
|
};
|
|
|
|
/// <summary>
|
|
/// Options for shallow serialization (root level only).
|
|
/// </summary>
|
|
public static readonly AcBinarySerializerOptions ShallowCopy = new()
|
|
{
|
|
MaxDepth = 0,
|
|
UseReferenceHandling = false
|
|
};
|
|
|
|
/// <summary>
|
|
/// Whether to include metadata header with property names.
|
|
/// When enabled, property names are stored once and referenced by index.
|
|
/// Improves deserialization speed and allows schema evolution.
|
|
/// Default: true
|
|
/// </summary>
|
|
public bool UseMetadata { get; init; } = true;
|
|
|
|
/// <summary>
|
|
/// Whether to intern repeated strings.
|
|
/// When enabled, duplicate strings are stored once and referenced by index.
|
|
/// Reduces size and memory for objects with many repeated string values.
|
|
/// Default: true
|
|
/// </summary>
|
|
public bool UseStringInterning { get; init; } = true;
|
|
|
|
/// <summary>
|
|
/// Minimum string length to consider for interning.
|
|
/// Shorter strings are written inline to avoid overhead.
|
|
/// Default: 4 (strings shorter than 4 chars are not interned)
|
|
/// </summary>
|
|
public byte MinStringInternLength { get; init; } = 4;
|
|
|
|
/// <summary>
|
|
/// Initial capacity for serialization buffer.
|
|
/// Default: 4096 bytes
|
|
/// </summary>
|
|
public int InitialBufferCapacity { get; init; } = 4096;
|
|
|
|
/// <summary>
|
|
/// Creates options with specified max depth.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static AcBinarySerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth };
|
|
|
|
/// <summary>
|
|
/// Creates options without reference handling.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
|
|
|
|
/// <summary>
|
|
/// Creates options without metadata (faster but less flexible).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static AcBinarySerializerOptions WithoutMetadata() => new() { UseMetadata = false };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Binary type codes for serialization.
|
|
/// Designed for fast switch dispatch and compact storage.
|
|
/// Lower 5 bits = type code (0-31)
|
|
/// Upper 3 bits = flags (interned, reference, has-type-info)
|
|
/// </summary>
|
|
internal static class BinaryTypeCode
|
|
{
|
|
// Primitive types (0-15)
|
|
public const byte Null = 0;
|
|
public const byte True = 1;
|
|
public const byte False = 2;
|
|
public const byte Int8 = 3;
|
|
public const byte UInt8 = 4;
|
|
public const byte Int16 = 5;
|
|
public const byte UInt16 = 6;
|
|
public const byte Int32 = 7;
|
|
public const byte UInt32 = 8;
|
|
public const byte Int64 = 9;
|
|
public const byte UInt64 = 10;
|
|
public const byte Float32 = 11;
|
|
public const byte Float64 = 12;
|
|
public const byte Decimal = 13;
|
|
public const byte Char = 14;
|
|
|
|
// String types (16-19)
|
|
public const byte String = 16; // Inline UTF8 string
|
|
public const byte StringInterned = 17; // Reference to interned string by index
|
|
public const byte StringEmpty = 18; // Empty string marker
|
|
|
|
// Date/Time types (20-23)
|
|
public const byte DateTime = 20;
|
|
public const byte DateTimeOffset = 21;
|
|
public const byte TimeSpan = 22;
|
|
public const byte Guid = 23;
|
|
|
|
// Enum (24)
|
|
public const byte Enum = 24;
|
|
|
|
// Complex types (25-31)
|
|
public const byte Object = 25; // Start of object
|
|
public const byte ObjectEnd = 26; // End of object marker
|
|
public const byte ObjectRef = 27; // Reference to previously serialized object
|
|
public const byte Array = 28; // Start of array/list
|
|
public const byte Dictionary = 29; // Start of dictionary
|
|
public const byte ByteArray = 30; // Optimized byte[] storage
|
|
|
|
// Special markers (32+, for header/meta)
|
|
// Header flags byte structure (for values >= 64):
|
|
// Bit 0 (0x01): HasMetadata
|
|
// Bit 1 (0x02): HasReferenceHandling
|
|
// Values 32, 33 are legacy for backward compatibility
|
|
public const byte MetadataHeader = 32; // Binary has metadata section (legacy, implies HasReferenceHandling=true)
|
|
public const byte NoMetadataHeader = 33; // Binary has no metadata (legacy, implies HasReferenceHandling=true)
|
|
|
|
// New flag-based header markers (48+)
|
|
// Base value 48 (0x30 = 00110000) chosen to:
|
|
// - Be distinguishable from legacy values (32, 33)
|
|
// - Not conflict with flag bits in lower nibble
|
|
// - Leave room below Int32Tiny (64)
|
|
public const byte HeaderFlagsBase = 48; // Base value for flag-based headers (0x30)
|
|
public const byte HeaderFlag_Metadata = 0x01;
|
|
public const byte HeaderFlag_ReferenceHandling = 0x02;
|
|
|
|
// Compact integer variants (for VarInt optimization)
|
|
public const byte Int32Tiny = 64; // -16 to 111 stored in single byte (value = code - 64 - 16)
|
|
public const byte Int32TinyMax = 191; // Upper bound for tiny int
|
|
|
|
/// <summary>
|
|
/// Check if type code represents a reference (string or object).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool IsReference(byte code) => code == StringInterned || code == ObjectRef;
|
|
|
|
/// <summary>
|
|
/// Check if type code is a tiny int (single byte int32 encoding).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool IsTinyInt(byte code) => code >= Int32Tiny && code <= Int32TinyMax;
|
|
|
|
/// <summary>
|
|
/// Decode tiny int value from type code.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16;
|
|
|
|
/// <summary>
|
|
/// Encode small int value (-16 to 111) as type code.
|
|
/// Returns true if value fits in tiny encoding.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool TryEncodeTinyInt(int value, out byte code)
|
|
{
|
|
if (value >= -16 && value <= 111)
|
|
{
|
|
code = (byte)(value + 16 + Int32Tiny);
|
|
return true;
|
|
}
|
|
code = 0;
|
|
return false;
|
|
}
|
|
}
|