diff --git a/.gitignore b/.gitignore index f4faab088a..b8b277c7c3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ MODULE.bazel MODULE.bazel.lock .DS_Store **/.DS_Store +csharp/**/bin +csharp/**/obj diff --git a/csharp/.csharpierrc.yaml b/csharp/.csharpierrc.yaml new file mode 100644 index 0000000000..ef96c2d0a0 --- /dev/null +++ b/csharp/.csharpierrc.yaml @@ -0,0 +1 @@ +printWidth: 160 diff --git a/csharp/Fury.Testing/Fury.Testing.csproj b/csharp/Fury.Testing/Fury.Testing.csproj new file mode 100644 index 0000000000..353da2b7d7 --- /dev/null +++ b/csharp/Fury.Testing/Fury.Testing.csproj @@ -0,0 +1,33 @@ + + + + + net8.0 + enable + enable + 12 + false + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/csharp/Fury.Testing/MetaStringTest.cs b/csharp/Fury.Testing/MetaStringTest.cs new file mode 100644 index 0000000000..1316e75239 --- /dev/null +++ b/csharp/Fury.Testing/MetaStringTest.cs @@ -0,0 +1,253 @@ +using System.Text; +using Bogus; +using Fury.Context; +using Fury.Meta; + +namespace Fury.Testing; + +public sealed class MetaStringTest +{ + public static readonly IEnumerable Lengths = Enumerable.Range(0, 9).Select(i => new object[] { i }); + + private static readonly string LowerSpecialChars = Enumerable + .Range(0, 1 << AbstractLowerSpecialEncoding.BitsPerChar) + .Select(i => (AbstractLowerSpecialEncoding.TryDecodeByte((byte)i, out var c), c)) + .Where(t => t.Item1) + .Aggregate(new StringBuilder(), (builder, t) => builder.Append(t.c)) + .ToString(); + + private static readonly string AllToLowerSpecialChars = Enumerable + .Range(0, 1 << AbstractLowerSpecialEncoding.BitsPerChar) + .Select(i => (AbstractLowerSpecialEncoding.TryDecodeByte((byte)i, out var c), c)) + .Where(t => t.Item1 && t.c != AllToLowerSpecialEncoding.UpperCaseFlag) + .Aggregate(new StringBuilder(), (builder, t) => builder.Append(t.c).Append(char.ToUpperInvariant(t.c))) + .ToString(); + + private static readonly string TypeNameLowerUpperDigitSpecialChars = Enumerable + .Range(0, 1 << LowerUpperDigitSpecialEncoding.BitsPerChar) + .Select(i => (MetaStringStorage.NameEncoding.LowerUpperDigit.TryDecodeByte((byte)i, out var c), c)) + .Where(t => t.Item1 && char.IsLetterOrDigit(t.c)) + .Aggregate(new StringBuilder(), (builder, t) => builder.Append(t.c)) + .ToString(); + + private static readonly string NamespaceLowerUpperDigitSpecialChars = Enumerable + .Range(0, 1 << LowerUpperDigitSpecialEncoding.BitsPerChar) + .Select(i => (MetaStringStorage.NamespaceEncoding.LowerUpperDigit.TryDecodeByte((byte)i, out var c), c)) + .Where(t => t.Item1 && char.IsLetterOrDigit(t.c)) + .Aggregate(new StringBuilder(), (builder, t) => builder.Append(t.c)) + .ToString(); + + [Theory] + [MemberData(nameof(Lengths))] + public void LowerSpecialEncoding_InputString_ShouldReturnTheSame(int length) + { + var faker = new Faker(); + var stubString = faker.Random.String2(length, LowerSpecialChars); + + var bufferLength = LowerSpecialEncoding.Instance.GetByteCount(stubString); + Span buffer = stackalloc byte[bufferLength]; + LowerSpecialEncoding.Instance.GetBytes(stubString, buffer); + var output = LowerSpecialEncoding.Instance.GetString(buffer); + + Assert.Equal(stubString, output); + } + + [Theory] + [MemberData(nameof(Lengths))] + public void LowerSpecialEncoding_InputSeparatedBytes_ShouldReturnConcatenatedString(int length) + { + var faker = new Faker(); + var stubString = faker.Random.String2(length, LowerSpecialChars); + + var bufferLength = LowerSpecialEncoding.Instance.GetByteCount(stubString); + Span bytes = stackalloc byte[bufferLength]; + Span chars = stackalloc char[stubString.Length]; + LowerSpecialEncoding.Instance.GetBytes(stubString, bytes); + var decoder = LowerSpecialEncoding.Instance.GetDecoder(); + var emptyChars = chars; + for (var i = 0; i < bytes.Length; i++) + { + var slicedBytes = bytes.Slice(i, 1); + decoder.Convert(slicedBytes, emptyChars, i == bytes.Length - 1, out _, out var charsUsed, out _); + emptyChars = emptyChars.Slice(charsUsed); + } + + var output = chars.ToString(); + + Assert.Equal(stubString, output); + } + + [Theory] + [MemberData(nameof(Lengths))] + public void FirstToLowerSpecialEncoding_InputString_ShouldReturnTheSame(int length) + { + var faker = new Faker(); + var stubString = faker.Random.String2(length, LowerSpecialChars); + if (stubString.Length > 0 && char.IsLower(stubString[0])) + { + Span stubSpan = stackalloc char[stubString.Length]; + stubString.AsSpan().CopyTo(stubSpan); + stubSpan[0] = char.ToUpperInvariant(stubSpan[0]); + stubString = stubSpan.ToString(); + } + + var bufferLength = FirstToLowerSpecialEncoding.Instance.GetByteCount(stubString); + Span buffer = stackalloc byte[bufferLength]; + FirstToLowerSpecialEncoding.Instance.GetBytes(stubString, buffer); + var output = FirstToLowerSpecialEncoding.Instance.GetString(buffer); + + Assert.Equal(stubString, output); + } + + [Theory] + [MemberData(nameof(Lengths))] + public void FirstToLowerSpecialEncoding_InputSeparatedBytes_ShouldReturnConcatenatedString(int length) + { + var faker = new Faker(); + var stubString = faker.Random.String2(length, LowerSpecialChars); + if (stubString.Length > 0 && char.IsLower(stubString[0])) + { + Span stubSpan = stackalloc char[stubString.Length]; + stubString.AsSpan().CopyTo(stubSpan); + stubSpan[0] = char.ToUpperInvariant(stubSpan[0]); + stubString = stubSpan.ToString(); + } + + var bufferLength = FirstToLowerSpecialEncoding.Instance.GetByteCount(stubString); + Span bytes = stackalloc byte[bufferLength]; + Span chars = stackalloc char[stubString.Length]; + FirstToLowerSpecialEncoding.Instance.GetBytes(stubString, bytes); + var decoder = FirstToLowerSpecialEncoding.Instance.GetDecoder(); + var emptyChars = chars; + for (var i = 0; i < bytes.Length; i++) + { + var slicedBytes = bytes.Slice(i, 1); + decoder.Convert(slicedBytes, emptyChars, i == bytes.Length - 1, out _, out var charsUsed, out _); + emptyChars = emptyChars.Slice(charsUsed); + } + + var output = chars.ToString(); + + Assert.Equal(stubString, output); + } + + [Theory] + [MemberData(nameof(Lengths))] + public void AllToLowerSpecialEncoding_InputString_ShouldReturnTheSame(int length) + { + var faker = new Faker(); + var stubString = faker.Random.String2(length, AllToLowerSpecialChars); + + var bufferLength = AllToLowerSpecialEncoding.Instance.GetByteCount(stubString); + Span buffer = stackalloc byte[bufferLength]; + AllToLowerSpecialEncoding.Instance.GetBytes(stubString, buffer); + var output = AllToLowerSpecialEncoding.Instance.GetString(buffer); + + Assert.Equal(stubString, output); + } + + [Theory] + [MemberData(nameof(Lengths))] + public void AllToLowerSpecialEncoding_InputSeparatedBytes_ShouldReturnConcatenatedString(int length) + { + var faker = new Faker(); + var stubString = faker.Random.String2(length, AllToLowerSpecialChars); + + var bufferLength = AllToLowerSpecialEncoding.Instance.GetByteCount(stubString); + Span bytes = stackalloc byte[bufferLength]; + Span chars = stackalloc char[stubString.Length]; + AllToLowerSpecialEncoding.Instance.GetBytes(stubString, bytes); + var decoder = AllToLowerSpecialEncoding.Instance.GetDecoder(); + var emptyChars = chars; + for (var i = 0; i < bytes.Length; i++) + { + var slicedBytes = bytes.Slice(i, 1); + decoder.Convert(slicedBytes, emptyChars, i == bytes.Length - 1, out _, out var charsUsed, out _); + emptyChars = emptyChars.Slice(charsUsed); + } + + var output = chars.ToString(); + + Assert.Equal(stubString, output); + } + + [Theory] + [MemberData(nameof(Lengths))] + public void TypeNameLowerUpperDigitSpecialEncoding_InputString_ShouldReturnTheSame(int length) + { + var faker = new Faker(); + var stubString = faker.Random.String2(length, TypeNameLowerUpperDigitSpecialChars); + + var bufferLength = MetaStringStorage.NameEncoding.LowerUpperDigit.GetByteCount(stubString); + Span buffer = stackalloc byte[bufferLength]; + MetaStringStorage.NameEncoding.LowerUpperDigit.GetBytes(stubString, buffer); + var output = MetaStringStorage.NameEncoding.LowerUpperDigit.GetString(buffer); + + Assert.Equal(stubString, output); + } + + [Theory] + [MemberData(nameof(Lengths))] + public void TypeNameLowerUpperDigitSpecialEncoding_InputSeparatedBytes_ShouldReturnConcatenatedString(int length) + { + var faker = new Faker(); + var stubString = faker.Random.String2(length, TypeNameLowerUpperDigitSpecialChars); + + var bufferLength = MetaStringStorage.NameEncoding.LowerUpperDigit.GetByteCount(stubString); + Span bytes = stackalloc byte[bufferLength]; + Span chars = stackalloc char[stubString.Length]; + MetaStringStorage.NameEncoding.LowerUpperDigit.GetBytes(stubString, bytes); + var decoder = MetaStringStorage.NameEncoding.LowerUpperDigit.GetDecoder(); + var emptyChars = chars; + for (var i = 0; i < bytes.Length; i++) + { + var slicedBytes = bytes.Slice(i, 1); + decoder.Convert(slicedBytes, emptyChars, i == bytes.Length - 1, out _, out var charsUsed, out _); + emptyChars = emptyChars.Slice(charsUsed); + } + + var output = chars.ToString(); + + Assert.Equal(stubString, output); + } + + [Theory] + [MemberData(nameof(Lengths))] + public void NamespaceLowerUpperDigitSpecialEncoding_InputString_ShouldReturnTheSame(int length) + { + var faker = new Faker(); + var stubString = faker.Random.String2(length, NamespaceLowerUpperDigitSpecialChars); + + var bufferLength = MetaStringStorage.NamespaceEncoding.LowerUpperDigit.GetByteCount(stubString); + Span buffer = stackalloc byte[bufferLength]; + MetaStringStorage.NamespaceEncoding.LowerUpperDigit.GetBytes(stubString, buffer); + var output = MetaStringStorage.NamespaceEncoding.LowerUpperDigit.GetString(buffer); + + Assert.Equal(stubString, output); + } + + [Theory] + [MemberData(nameof(Lengths))] + public void NamespaceLowerUpperDigitSpecialEncoding_InputSeparatedBytes_ShouldReturnConcatenatedString(int length) + { + var faker = new Faker(); + var stubString = faker.Random.String2(length, NamespaceLowerUpperDigitSpecialChars); + + var bufferLength = MetaStringStorage.NamespaceEncoding.LowerUpperDigit.GetByteCount(stubString); + Span bytes = stackalloc byte[bufferLength]; + Span chars = stackalloc char[stubString.Length]; + MetaStringStorage.NamespaceEncoding.LowerUpperDigit.GetBytes(stubString, bytes); + var decoder = MetaStringStorage.NamespaceEncoding.LowerUpperDigit.GetDecoder(); + var emptyChars = chars; + for (var i = 0; i < bytes.Length; i++) + { + var slicedBytes = bytes.Slice(i, 1); + decoder.Convert(slicedBytes, emptyChars, i == bytes.Length - 1, out _, out var charsUsed, out _); + emptyChars = emptyChars.Slice(charsUsed); + } + + var output = chars.ToString(); + + Assert.Equal(stubString, output); + } +} diff --git a/csharp/Fury.slnx b/csharp/Fury.slnx new file mode 100644 index 0000000000..9379444e0a --- /dev/null +++ b/csharp/Fury.slnx @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/csharp/Fury/Backports/ArrayBufferWriter.cs b/csharp/Fury/Backports/ArrayBufferWriter.cs new file mode 100644 index 0000000000..0a8c83941f --- /dev/null +++ b/csharp/Fury/Backports/ArrayBufferWriter.cs @@ -0,0 +1,254 @@ +#if NETSTANDARD2_0 +using System.Diagnostics; +using Fury; + +namespace System.Buffers; + +/// +/// Represents a heap-based, array-backed output sink into which data can be written. +/// +internal sealed class ArrayBufferWriter : IBufferWriter +{ + // Copy of Array.MaxLength. + // Used by projects targeting .NET Framework. + private const int ArrayMaxLength = 0x7FFFFFC7; + + private const int DefaultInitialBufferSize = 256; + + private T[] _buffer; + private int _index; + + /// + /// Creates an instance of an , in which data can be written to, + /// with the default initial capacity. + /// + public ArrayBufferWriter() + { + _buffer = Array.Empty(); + _index = 0; + } + + /// + /// Creates an instance of an , in which data can be written to, + /// with an initial capacity specified. + /// + /// The minimum capacity with which to initialize the underlying buffer. + /// + /// Thrown when is not positive (i.e. less than or equal to 0). + /// + public ArrayBufferWriter(int initialCapacity) + { + if (initialCapacity <= 0) + { + ThrowHelper.ThrowArgumentException(null, nameof(initialCapacity)); + } + + _buffer = new T[initialCapacity]; + _index = 0; + } + + /// + /// Returns the data written to the underlying buffer so far, as a . + /// + public ReadOnlyMemory WrittenMemory => _buffer.AsMemory(0, _index); + + /// + /// Returns the data written to the underlying buffer so far, as a . + /// + public ReadOnlySpan WrittenSpan => _buffer.AsSpan(0, _index); + + /// + /// Returns the amount of data written to the underlying buffer so far. + /// + public int WrittenCount => _index; + + /// + /// Returns the total amount of space within the underlying buffer. + /// + public int Capacity => _buffer.Length; + + /// + /// Returns the amount of space available that can still be written into without forcing the underlying buffer to grow. + /// + public int FreeCapacity => _buffer.Length - _index; + + /// + /// Clears the data written to the underlying buffer. + /// + /// + /// + /// You must reset or clear the before trying to re-use it. + /// + /// + /// The method is faster since it only sets to zero the writer's index + /// while the method additionally zeroes the content of the underlying buffer. + /// + /// + /// + public void Clear() + { + Debug.Assert(_buffer.Length >= _index); + _buffer.AsSpan(0, _index).Clear(); + _index = 0; + } + + /// + /// Resets the data written to the underlying buffer without zeroing its content. + /// + /// + /// + /// You must reset or clear the before trying to re-use it. + /// + /// + /// If you reset the writer using the method, the underlying buffer will not be cleared. + /// + /// + /// + public void ResetWrittenCount() => _index = 0; + + /// + /// Notifies that amount of data was written to the output / + /// + /// + /// Thrown when is negative. + /// + /// + /// Thrown when attempting to advance past the end of the underlying buffer. + /// + /// + /// You must request a new buffer after calling Advance to continue writing more data and cannot write to a previously acquired buffer. + /// + public void Advance(int count) + { + if (count < 0) + { + ThrowHelper.ThrowArgumentException(null, nameof(count)); + } + + if (_index > _buffer.Length - count) + { + ThrowInvalidOperationException_AdvancedTooFar(_buffer.Length); + } + + _index += count; + } + + /// + /// Returns a to write to that is at least the requested length (specified by ). + /// If no is provided (or it's equal to 0), some non-empty buffer is returned. + /// + /// + /// Thrown when is negative. + /// + /// + /// + /// This will never return an empty . + /// + /// + /// There is no guarantee that successive calls will return the same buffer or the same-sized buffer. + /// + /// + /// You must request a new buffer after calling Advance to continue writing more data and cannot write to a previously acquired buffer. + /// + /// + /// If you reset the writer using the method, this method may return a non-cleared . + /// + /// + /// If you clear the writer using the method, this method will return a with its content zeroed. + /// + /// + public Memory GetMemory(int sizeHint = 0) + { + CheckAndResizeBuffer(sizeHint); + Debug.Assert(_buffer.Length > _index); + return _buffer.AsMemory(_index); + } + + /// + /// Returns a to write to that is at least the requested length (specified by ). + /// If no is provided (or it's equal to 0), some non-empty buffer is returned. + /// + /// + /// Thrown when is negative. + /// + /// + /// + /// This will never return an empty . + /// + /// + /// There is no guarantee that successive calls will return the same buffer or the same-sized buffer. + /// + /// + /// You must request a new buffer after calling Advance to continue writing more data and cannot write to a previously acquired buffer. + /// + /// + /// If you reset the writer using the method, this method may return a non-cleared . + /// + /// + /// If you clear the writer using the method, this method will return a with its content zeroed. + /// + /// + public Span GetSpan(int sizeHint = 0) + { + CheckAndResizeBuffer(sizeHint); + Debug.Assert(_buffer.Length > _index); + return _buffer.AsSpan(_index); + } + + private void CheckAndResizeBuffer(int sizeHint) + { + if (sizeHint < 0) + { + ThrowHelper.ThrowArgumentException(null, nameof(sizeHint)); + } + + if (sizeHint == 0) + { + sizeHint = 1; + } + + if (sizeHint > FreeCapacity) + { + int currentLength = _buffer.Length; + + // Attempt to grow by the larger of the sizeHint and double the current size. + int growBy = Math.Max(sizeHint, currentLength); + + if (currentLength == 0) + { + growBy = Math.Max(growBy, DefaultInitialBufferSize); + } + + int newSize = currentLength + growBy; + + if ((uint)newSize > int.MaxValue) + { + // Attempt to grow to ArrayMaxLength. + uint needed = (uint)(currentLength - FreeCapacity + sizeHint); + Debug.Assert(needed > currentLength); + + if (needed > ArrayMaxLength) + { + ThrowOutOfMemoryException(needed); + } + + newSize = ArrayMaxLength; + } + + Array.Resize(ref _buffer, newSize); + } + + Debug.Assert(FreeCapacity > 0 && FreeCapacity >= sizeHint); + } + + private static void ThrowInvalidOperationException_AdvancedTooFar(int capacity) + { + throw new InvalidOperationException($"Cannot advance past the end of the underlying buffer which has a capacity of {capacity}."); + } + + private static void ThrowOutOfMemoryException(uint capacity) + { + throw new OutOfMemoryException($"Cannot allocate an array of {capacity} elements."); + } +} +#endif diff --git a/csharp/Fury/Backports/BitOperations.cs b/csharp/Fury/Backports/BitOperations.cs new file mode 100644 index 0000000000..89577ba955 --- /dev/null +++ b/csharp/Fury/Backports/BitOperations.cs @@ -0,0 +1,21 @@ +#if !NET8_0_OR_GREATER +using System.Runtime.CompilerServices; + +// ReSharper disable once CheckNamespace +namespace System.Numerics; + +internal static class BitOperations +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong RotateLeft(ulong value, int offset) => (value << offset) | (value >> (64 - offset)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RotateLeft(uint value, int offset) => (value << offset) | (value >> (32 - offset)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong RotateRight(ulong value, int offset) => (value >> offset) | (value << (64 - offset)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RotateRight(uint value, int offset) => (value >> offset) | (value << (32 - offset)); +} +#endif diff --git a/csharp/Fury/Backports/EncodingExtensions.cs b/csharp/Fury/Backports/EncodingExtensions.cs new file mode 100644 index 0000000000..b51a7f654a --- /dev/null +++ b/csharp/Fury/Backports/EncodingExtensions.cs @@ -0,0 +1,64 @@ +#if !NET8_0_OR_GREATER +using System; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Fury; + +internal static class EncodingExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void Convert( + this Encoder encoder, + ReadOnlySpan chars, + Span bytes, + bool flush, + out int charsUsed, + out int bytesUsed, + out bool completed + ) + { + fixed (char* pChars = chars) + fixed (byte* pBytes = bytes) + { + encoder.Convert( + pChars, + chars.Length, + pBytes, + bytes.Length, + flush, + out charsUsed, + out bytesUsed, + out completed + ); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void Convert( + this Decoder decoder, + ReadOnlySpan bytes, + Span chars, + bool flush, + out int bytesUsed, + out int charsUsed, + out bool completed + ) + { + fixed (byte* pBytes = bytes) + fixed (char* pChars = chars) + { + decoder.Convert( + pBytes, + bytes.Length, + pChars, + chars.Length, + flush, + out bytesUsed, + out charsUsed, + out completed + ); + } + } +} +#endif diff --git a/csharp/Fury/Backports/Lock.cs b/csharp/Fury/Backports/Lock.cs new file mode 100644 index 0000000000..a7baefa777 --- /dev/null +++ b/csharp/Fury/Backports/Lock.cs @@ -0,0 +1,5 @@ +#if !NET9_0_OR_GREATER +namespace System.Threading; + +internal sealed class Lock; +#endif diff --git a/csharp/Fury/Backports/MethodInfoExtensions.cs b/csharp/Fury/Backports/MethodInfoExtensions.cs new file mode 100644 index 0000000000..46ab35150b --- /dev/null +++ b/csharp/Fury/Backports/MethodInfoExtensions.cs @@ -0,0 +1,13 @@ +#if !NET5_0_OR_GREATER +using System; +using System.Reflection; + +namespace Fury; + +internal static class MethodInfoExtensions +{ + + /// Creates a delegate of the given type 'T' from this method. + public static T CreateDelegate(this MethodInfo methodInfo) where T : Delegate => (T)methodInfo.CreateDelegate(typeof(T)); +} +#endif diff --git a/csharp/Fury/Backports/NotReturnAttributes.cs b/csharp/Fury/Backports/NotReturnAttributes.cs new file mode 100644 index 0000000000..8b12c14c81 --- /dev/null +++ b/csharp/Fury/Backports/NotReturnAttributes.cs @@ -0,0 +1,7 @@ +#if !NET8_0_OR_GREATER +// ReSharper disable once CheckNamespace +namespace System.Diagnostics.CodeAnalysis; + +[AttributeUsage(AttributeTargets.Method, Inherited = false)] +internal class DoesNotReturnAttribute : Attribute; +#endif diff --git a/csharp/Fury/Backports/NullableAttributes.cs b/csharp/Fury/Backports/NullableAttributes.cs new file mode 100644 index 0000000000..63ee28754b --- /dev/null +++ b/csharp/Fury/Backports/NullableAttributes.cs @@ -0,0 +1,64 @@ +#if NETSTANDARD +// ReSharper disable once CheckNamespace +namespace System.Diagnostics.CodeAnalysis; + +/// Specifies that when a method returns , the parameter will not be even if the corresponding type allows it. +[AttributeUsage(AttributeTargets.Parameter)] +internal sealed class NotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// The return value condition. If the method returns this value, the associated parameter will not be . + public NotNullWhenAttribute(bool returnValue) => this.ReturnValue = returnValue; + + /// Gets the return value condition. + /// The return value condition. If the method returns this value, the associated parameter will not be . + public bool ReturnValue { get; } +} + +/// Specifies that the method will not return if the associated parameter is passed the specified value. +[AttributeUsage(AttributeTargets.Parameter)] +internal sealed class DoesNotReturnIfAttribute : Attribute +{ + /// Initializes a new instance of the class with the specified parameter value. + /// The condition parameter value. Code after the method is considered unreachable by diagnostics if the argument to the associated parameter matches this value. + public DoesNotReturnIfAttribute(bool parameterValue) => this.ParameterValue = parameterValue; + + /// Gets the condition parameter value. + /// The condition parameter value. Code after the method is considered unreachable by diagnostics if the argument to the associated parameter matches this value. + public bool ParameterValue { get; } +} + +/// Specifies that the method or property will ensure that the listed field and property members have not-null values. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +internal sealed class MemberNotNullAttribute : Attribute +{ + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } +} + +/// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +internal sealed class MaybeNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} +#endif diff --git a/csharp/Fury/Backports/SequenceReader.cs b/csharp/Fury/Backports/SequenceReader.cs new file mode 100644 index 0000000000..3658556b23 --- /dev/null +++ b/csharp/Fury/Backports/SequenceReader.cs @@ -0,0 +1,463 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Copied and modified from System.Buffers.SequenceReader + +#if NETSTANDARD2_0 +using System.Diagnostics; +using System.Runtime.CompilerServices; + +// ReSharper disable once CheckNamespace +namespace System.Buffers; + +public ref struct SequenceReader + where T : unmanaged, IEquatable +{ + private SequencePosition _currentPosition; + private SequencePosition _nextPosition; + private bool _moreData; + private readonly long _length; + + /// + /// Create a over the given . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SequenceReader(ReadOnlySequence sequence) + { + CurrentSpanIndex = 0; + Consumed = 0; + Sequence = sequence; + _currentPosition = sequence.Start; + _length = -1; + + CurrentSpan = sequence.First.Span; + _nextPosition = sequence.GetPosition(CurrentSpan.Length); + _moreData = CurrentSpan.Length > 0; + + if (!_moreData && !sequence.IsSingleSegment) + { + _moreData = true; + GetNextSpan(); + } + } + + /// + /// True when there is no more data in the . + /// + public readonly bool End => !_moreData; + + /// + /// The underlying for the reader. + /// + public ReadOnlySequence Sequence { get; } + + /// + /// Gets the unread portion of the . + /// + /// + /// The unread portion of the . + /// + public readonly ReadOnlySequence UnreadSequence => Sequence.Slice(Position); + + /// + /// The current position in the . + /// + public readonly SequencePosition Position => Sequence.GetPosition(CurrentSpanIndex, _currentPosition); + + /// + /// The current segment in the as a span. + /// + public ReadOnlySpan CurrentSpan { get; private set; } + + /// + /// The index in the . + /// + public int CurrentSpanIndex { get; private set; } + + /// + /// The unread portion of the . + /// + public readonly ReadOnlySpan UnreadSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => CurrentSpan.Slice(CurrentSpanIndex); + } + + /// + /// The total number of 's processed by the reader. + /// + public long Consumed { get; private set; } + + /// + /// Remaining 's in the reader's . + /// + public readonly long Remaining => Length - Consumed; + + /// + /// Count of in the reader's . + /// + public readonly long Length + { + get + { + if (_length < 0) + { + // Cast-away readonly to initialize lazy field + Unsafe.AsRef(in _length) = Sequence.Length; + } + return _length; + } + } + + /// + /// Peeks at the next value without advancing the reader. + /// + /// The next value or default if at the end. + /// False if at the end of the reader. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool TryPeek(out T value) + { + if (_moreData) + { + value = CurrentSpan[CurrentSpanIndex]; + return true; + } + else + { + value = default; + return false; + } + } + + /// + /// Peeks at the next value at specific offset without advancing the reader. + /// + /// The offset from current position. + /// The next value, or the default value if at the end of the reader. + /// true if the reader is not at its end and the peek operation succeeded; false if at the end of the reader. + public readonly bool TryPeek(long offset, out T value) + { + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + // If we've got data and offset is not out of bounds + if (!_moreData || Remaining <= offset) + { + value = default; + return false; + } + + // Sum CurrentSpanIndex + offset could overflow as is but the value of offset should be very large + // because we check Remaining <= offset above so to overflow we should have a ReadOnlySequence close to 8 exabytes + Debug.Assert(CurrentSpanIndex + offset >= 0); + + // If offset doesn't fall inside current segment move to next until we find correct one + if ((CurrentSpanIndex + offset) <= CurrentSpan.Length - 1) + { + Debug.Assert(offset <= int.MaxValue); + + value = CurrentSpan[CurrentSpanIndex + (int)offset]; + return true; + } + else + { + long remainingOffset = offset - (CurrentSpan.Length - CurrentSpanIndex); + SequencePosition nextPosition = _nextPosition; + ReadOnlyMemory currentMemory; + + while (Sequence.TryGet(ref nextPosition, out currentMemory, advance: true)) + { + // Skip empty segment + if (currentMemory.Length > 0) + { + if (remainingOffset >= currentMemory.Length) + { + // Subtract current non consumed data + remainingOffset -= currentMemory.Length; + } + else + { + break; + } + } + } + + value = currentMemory.Span[(int)remainingOffset]; + return true; + } + } + + /// + /// Read the next value and advance the reader. + /// + /// The next value or default if at the end. + /// False if at the end of the reader. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryRead(out T value) + { + if (End) + { + value = default; + return false; + } + + value = CurrentSpan[CurrentSpanIndex]; + CurrentSpanIndex++; + Consumed++; + + if (CurrentSpanIndex >= CurrentSpan.Length) + { + GetNextSpan(); + } + + return true; + } + + /// + /// Move the reader back the specified number of items. + /// + /// + /// Thrown if trying to rewind a negative amount or more than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Rewind(long count) + { + if ((ulong)count > (ulong)Consumed) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (count == 0) + { + return; + } + + Consumed -= count; + + if (CurrentSpanIndex >= count) + { + CurrentSpanIndex -= (int)count; + _moreData = true; + } + else + { + // Current segment doesn't have enough data, scan backward through segments + RetreatToPreviousSpan(Consumed); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void RetreatToPreviousSpan(long consumed) + { + ResetReader(); + Advance(consumed); + } + + private void ResetReader() + { + CurrentSpanIndex = 0; + Consumed = 0; + _currentPosition = Sequence.Start; + _nextPosition = _currentPosition; + + if (Sequence.TryGet(ref _nextPosition, out ReadOnlyMemory memory, advance: true)) + { + _moreData = true; + + if (memory.Length == 0) + { + CurrentSpan = default; + // No data in the first span, move to one with data + GetNextSpan(); + } + else + { + CurrentSpan = memory.Span; + } + } + else + { + // No data in any spans and at end of sequence + _moreData = false; + CurrentSpan = default; + } + } + + /// + /// Get the next segment with available data, if any. + /// + private void GetNextSpan() + { + if (!Sequence.IsSingleSegment) + { + SequencePosition previousNextPosition = _nextPosition; + while (Sequence.TryGet(ref _nextPosition, out ReadOnlyMemory memory, advance: true)) + { + _currentPosition = previousNextPosition; + if (memory.Length > 0) + { + CurrentSpan = memory.Span; + CurrentSpanIndex = 0; + return; + } + else + { + CurrentSpan = default; + CurrentSpanIndex = 0; + previousNextPosition = _nextPosition; + } + } + } + _moreData = false; + } + + /// + /// Move the reader ahead the specified number of items. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(long count) + { + const long TooBigOrNegative = unchecked((long)0xFFFFFFFF80000000); + if ((count & TooBigOrNegative) == 0 && CurrentSpan.Length - CurrentSpanIndex > (int)count) + { + CurrentSpanIndex += (int)count; + Consumed += count; + } + else + { + // Can't satisfy from the current span + AdvanceToNextSpan(count); + } + } + + /// + /// Unchecked helper to avoid unnecessary checks where you know count is valid. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AdvanceCurrentSpan(long count) + { + Debug.Assert(count >= 0); + + Consumed += count; + CurrentSpanIndex += (int)count; + if (CurrentSpanIndex >= CurrentSpan.Length) + GetNextSpan(); + } + + /// + /// Only call this helper if you know that you are advancing in the current span + /// with valid count and there is no need to fetch the next one. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AdvanceWithinSpan(long count) + { + Debug.Assert(count >= 0); + + Consumed += count; + CurrentSpanIndex += (int)count; + + Debug.Assert(CurrentSpanIndex < CurrentSpan.Length); + } + + private void AdvanceToNextSpan(long count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + Consumed += count; + while (_moreData) + { + int remaining = CurrentSpan.Length - CurrentSpanIndex; + + if (remaining > count) + { + CurrentSpanIndex += (int)count; + count = 0; + break; + } + + // As there may not be any further segments we need to + // push the current index to the end of the span. + CurrentSpanIndex += remaining; + count -= remaining; + Debug.Assert(count >= 0); + + GetNextSpan(); + + if (count == 0) + { + break; + } + } + + if (count != 0) + { + // Not enough data left- adjust for where we actually ended and throw + Consumed -= count; + throw new ArgumentOutOfRangeException(nameof(count)); + } + } + + /// + /// Copies data from the current to the given span if there + /// is enough data to fill it. + /// + /// + /// This API is used to copy a fixed amount of data out of the sequence if possible. It does not advance + /// the reader. To look ahead for a specific stream of data can be used. + /// + /// Destination span to copy to. + /// True if there is enough data to completely fill the span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool TryCopyTo(Span destination) + { + // This API doesn't advance to facilitate conditional advancement based on the data returned. + // We don't provide an advance option to allow easier utilizing of stack allocated destination spans. + // (Because we can make this method readonly we can guarantee that we won't capture the span.) + + ReadOnlySpan firstSpan = UnreadSpan; + if (firstSpan.Length >= destination.Length) + { + firstSpan.Slice(0, destination.Length).CopyTo(destination); + return true; + } + + // Not enough in the current span to satisfy the request, fall through to the slow path + return TryCopyMultisegment(destination); + } + + internal readonly bool TryCopyMultisegment(Span destination) + { + // If we don't have enough to fill the requested buffer, return false + if (Remaining < destination.Length) + return false; + + ReadOnlySpan firstSpan = UnreadSpan; + Debug.Assert(firstSpan.Length < destination.Length); + firstSpan.CopyTo(destination); + int copied = firstSpan.Length; + + SequencePosition next = _nextPosition; + while (Sequence.TryGet(ref next, out ReadOnlyMemory nextSegment, true)) + { + if (nextSegment.Length > 0) + { + ReadOnlySpan nextSpan = nextSegment.Span; + int toCopy = Math.Min(nextSpan.Length, destination.Length - copied); + nextSpan.Slice(0, toCopy).CopyTo(destination.Slice(copied)); + copied += toCopy; + if (copied >= destination.Length) + { + break; + } + } + } + + return true; + } +} +#endif diff --git a/csharp/Fury/Backports/SpanAction.cs b/csharp/Fury/Backports/SpanAction.cs new file mode 100644 index 0000000000..15c878cde2 --- /dev/null +++ b/csharp/Fury/Backports/SpanAction.cs @@ -0,0 +1,5 @@ +#if !NET5_0_OR_GREATER && !NETSTANDARD2_1 +// ReSharper disable once CheckNamespace +namespace System.Buffers; +internal delegate void SpanAction(Span span, TArg arg); +#endif diff --git a/csharp/Fury/Buffers/ObjectPool.cs b/csharp/Fury/Buffers/ObjectPool.cs new file mode 100644 index 0000000000..4a97778f13 --- /dev/null +++ b/csharp/Fury/Buffers/ObjectPool.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; + +namespace Fury.Buffers; + +// Copy and modify from Microsoft.Extensions.ObjectPool.DefaultObjectPool + +/// +/// The type to pool objects for. +/// +/// +/// This implementation keeps a cache of retained objects. +/// This means that if objects are returned when the pool has already reached +/// "maximumRetained" objects they will be available to be Garbage Collected. +/// +internal sealed class ObjectPool : IDisposable + where T : class +{ + private readonly Func _factory; + + private readonly int _maxCapacity; + private int _numItems; + + private readonly ConcurrentQueue _items = new(); + private T? _fastItem; + + /// + /// Creates an instance of . + /// + public ObjectPool(Func factory) + : this(factory, Environment.ProcessorCount * 2) { } + + /// + /// Creates an instance of . + /// + /// + /// The factory method to create new instances of . + /// + /// + /// The maximum number of objects to retain in the pool. + /// + public ObjectPool(Func factory, int maximumRetained) + { + _factory = factory; + _maxCapacity = maximumRetained - 1; // -1 to account for _fastItem + } + + public void Return(T obj) + { + if (_fastItem == null && Interlocked.CompareExchange(ref _fastItem, obj, null) == null) + { + return; + } + + if (Interlocked.Increment(ref _numItems) <= _maxCapacity) + { + _items.Enqueue(obj); + } + + // no room, clean up the count and drop the object on the floor + Interlocked.Decrement(ref _numItems); + } + + public T Rent() + { + var item = _fastItem; + if (item != null && Interlocked.CompareExchange(ref _fastItem, null, item) == item) + { + return item; + } + + if (_items.TryDequeue(out item)) + { + Interlocked.Decrement(ref _numItems); + return item; + } + + return _factory(); + } + + public void Dispose() + { + if (_fastItem is IDisposable disposableFastItem) + { + disposableFastItem.Dispose(); + } + + while (_items.TryDequeue(out var item)) + { + if (item is IDisposable disposableItem) + { + disposableItem.Dispose(); + } + } + } +} diff --git a/csharp/Fury/Buffers/UnmanagedToByteArrayMemoryManager.cs b/csharp/Fury/Buffers/UnmanagedToByteArrayMemoryManager.cs new file mode 100644 index 0000000000..7c073dfc21 --- /dev/null +++ b/csharp/Fury/Buffers/UnmanagedToByteArrayMemoryManager.cs @@ -0,0 +1,26 @@ +using System; +using System.Buffers; +using System.Runtime.InteropServices; + +namespace Fury.Buffers; + +internal sealed class UnmanagedToByteArrayMemoryManager(TElement[] array) : MemoryManager + where TElement : unmanaged +{ + protected override void Dispose(bool disposing) { } + + public override Span GetSpan() + { + return MemoryMarshal.AsBytes(array.AsSpan()); + } + + public override MemoryHandle Pin(int elementIndex = 0) + { + throw new InvalidOperationException(); + } + + public override void Unpin() + { + throw new InvalidOperationException(); + } +} diff --git a/csharp/Fury/Buffers/UnmanagedToByteListMemoryManager.cs b/csharp/Fury/Buffers/UnmanagedToByteListMemoryManager.cs new file mode 100644 index 0000000000..899b32fecb --- /dev/null +++ b/csharp/Fury/Buffers/UnmanagedToByteListMemoryManager.cs @@ -0,0 +1,31 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Fury.Buffers; + +#if NET5_0_OR_GREATER +internal sealed class UnmanagedToByteListMemoryManager : MemoryManager + where TElement : unmanaged +{ + public List? List; + + protected override void Dispose(bool disposing) { } + + public override Span GetSpan() + { + return MemoryMarshal.AsBytes(CollectionsMarshal.AsSpan(List)); + } + + public override MemoryHandle Pin(int elementIndex = 0) + { + throw new InvalidOperationException(); + } + + public override void Unpin() + { + throw new InvalidOperationException(); + } +} +#endif diff --git a/csharp/Fury/Collections/AutoIncrementIdDictionary.cs b/csharp/Fury/Collections/AutoIncrementIdDictionary.cs new file mode 100644 index 0000000000..a5ea2d7143 --- /dev/null +++ b/csharp/Fury/Collections/AutoIncrementIdDictionary.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Fury.Collections; + +internal sealed class AutoIncrementIdDictionary + where TValue : notnull +{ + private readonly Dictionary _valueToId = new(); + private readonly SpannableList _idToValue = []; + + public int this[TValue key] => _valueToId[key]; + + public TValue this[int id] + { + get + { + if (id < 0 || id >= _idToValue.Count) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(id)); + } + return _idToValue[id]; + } + set + { + if (id < 0 || id >= _idToValue.Count) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(id)); + } + + if (_valueToId.ContainsKey(value)) + { + ThrowArgumentException_ValueAlreadyExists(value); + } + _idToValue[id] = value; + } + } + + [DoesNotReturn] + private void ThrowArgumentException_ValueAlreadyExists(TValue value) + { + ThrowHelper.ThrowArgumentException( + $"The value '{value}' already exists in the {nameof(AutoIncrementIdDictionary)}.", + nameof(value) + ); + } + + public int GetOrAdd(in TValue value, out bool exists) + { + var nextId = _idToValue.Count; + var id = _valueToId.GetOrAdd(value, nextId, out exists); + if (!exists) + { + _idToValue.Add(value); + } + + return id; + } + + public bool Remove(TValue item) + { + ThrowHelper.ThrowNotSupportedException(); + return false; + } + + public void Clear() + { + _valueToId.Clear(); + _idToValue.Clear(); + } + + public bool ContainsKey(TValue key) + { + return _valueToId.ContainsKey(key); + } + + public bool ContainsKey(int key) + { + return key >= 0 && key < _idToValue.Count; + } + + public void CopyTo(TValue[] array, int arrayIndex) + { + _idToValue.CopyTo(array, arrayIndex); + } + + public bool TryGetValue(TValue key, out int value) + { + return _valueToId.TryGetValue(key, out value); + } + + public bool TryGetValue(int key, out TValue value) + { + if (ContainsKey(key)) + { + value = _idToValue[key]; + return true; + } + + value = default!; + return false; + } + + public ref TValue GetValueRefOrNullRef(int key) + { + if (ContainsKey(key)) + { + var values = _idToValue.AsSpan(); + return ref values[key]; + } + + return ref Unsafe.NullRef(); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public ref struct Enumerator(AutoIncrementIdDictionary idDictionary) + { + private int _index = -1; + private readonly Span _entries = idDictionary._idToValue.AsSpan(); + + public bool MoveNext() + { + return ++_index < _entries.Length; + } + + public void Reset() + { + _index = -1; + } + + public KeyValuePair Current => new(_index, _entries[_index]); + } +} diff --git a/csharp/Fury/Collections/DictionaryExtensions.cs b/csharp/Fury/Collections/DictionaryExtensions.cs new file mode 100644 index 0000000000..4483f63dd3 --- /dev/null +++ b/csharp/Fury/Collections/DictionaryExtensions.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Fury.Collections; + +internal static class DictionaryExtensions +{ + public static TValue GetOrAdd( + this Dictionary dictionary, + TKey key, + TValue value, + out bool exists + ) + where TKey : notnull + where TValue : notnull + { +#if NET8_0_OR_GREATER + ref var existingValue = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out exists); +#else + exists = dictionary.TryGetValue(key, out var existingValue); +#endif + if (exists) + { + return existingValue!; + } +#if NET8_0_OR_GREATER + existingValue = value; +#else + dictionary[key] = value; +#endif + return value; + } + + public static TValue GetOrAdd( + this Dictionary dictionary, + TKey key, + Func factory, + in TArg arg, + out bool exists + ) + where TKey : notnull + where TValue : notnull + { +#if NET8_0_OR_GREATER + ref var existingValue = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out exists); +#else + exists = dictionary.TryGetValue(key, out var existingValue); +#endif + if (!exists) + { + existingValue = factory(key, arg); +#if !NET8_0_OR_GREATER + dictionary[key] = existingValue; +#endif + } + return existingValue!; + } + + public static TValue GetOrAdd( + this Dictionary dictionary, + TKey key, + Func factory, + out bool exists + ) + where TKey : notnull + where TValue : notnull + { +#if NET8_0_OR_GREATER + ref var existingValue = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out exists); +#else + exists = dictionary.TryGetValue(key, out var existingValue); +#endif + if (!exists) + { + existingValue = factory(key); +#if !NET8_0_OR_GREATER + dictionary[key] = existingValue; +#endif + } + return existingValue!; + } + + public static TValue AddAndModify( + this Dictionary dictionary, + TKey key, + Func modifier, + in TState state, + out bool exists + ) + where TKey : notnull + where TValue : notnull + { +#if NET8_0_OR_GREATER + ref var existingValue = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out exists); + existingValue = modifier(key, existingValue, state); +#else + exists = dictionary.TryGetValue(key, out var existingValue); + existingValue = modifier(key, existingValue, state); + dictionary[key] = existingValue; +#endif + return existingValue; + } +} diff --git a/csharp/Fury/Collections/EnumerableExtensions.cs b/csharp/Fury/Collections/EnumerableExtensions.cs new file mode 100644 index 0000000000..03f6cb38be --- /dev/null +++ b/csharp/Fury/Collections/EnumerableExtensions.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using JetBrains.Annotations; +#if NET8_0_OR_GREATER +using System.Collections.Immutable; +using System.Runtime.InteropServices; +#endif + +namespace Fury.Collections; + +internal static class EnumerableExtensions +{ +#if !NET8_0_OR_GREATER + public static bool TryGetNonEnumeratedCount([NoEnumeration] this IEnumerable enumerable, out int count) + { + switch (enumerable) + { + case ICollection typedCollection: + count = typedCollection.Count; + return true; + case ICollection collection: + count = collection.Count; + return true; + case IReadOnlyCollection readOnlyCollection: + count = readOnlyCollection.Count; + return true; + default: + count = 0; + return false; + } + } +#endif + public static bool TryGetSpan([NoEnumeration] this IEnumerable enumerable, out Span span) + { + switch (enumerable) + { + case T[] elements: + span = elements; + return true; +#if NET8_0_OR_GREATER + case List elements: + span = CollectionsMarshal.AsSpan(elements); + return true; + case ImmutableArray elements: + span = ImmutableCollectionsMarshal.AsArray(elements); + return true; +#endif + case PooledList elements: + span = elements.AsSpan(); + return true; + default: + span = Span.Empty; + return false; + } + } +} diff --git a/csharp/Fury/Collections/PooledList.cs b/csharp/Fury/Collections/PooledList.cs new file mode 100644 index 0000000000..6a36b584fb --- /dev/null +++ b/csharp/Fury/Collections/PooledList.cs @@ -0,0 +1,217 @@ +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Fury.Helpers; + +namespace Fury.Collections; + +/// +/// A list that uses pooled arrays to reduce allocations. +/// +internal sealed class PooledList : IList, IDisposable +{ + private static readonly bool NeedClear = TypeHelper.IsReferenceOrContainsReferences; + + private readonly ArrayPool _pool = ArrayPool.Shared; + private TElement[] _elements = []; + public int Count { get; private set; } + + public PooledList() { } + + public PooledList(int capacity) + { + _elements = _pool.Rent(capacity); + } + + public PooledList(IEnumerable enumerable) + { + AddRange(enumerable); + } + + private void EnsureCapacity(int capacity) + { + if (_elements.Length < capacity) + { + var newElements = _pool.Rent(capacity); + _elements.CopyTo(newElements, 0); + ClearElementsIfNeeded(); + _pool.Return(_elements); + _elements = newElements; + } + } + + public Enumerator GetEnumerator() => new(this); + + public void Add(TElement item) + { + EnsureCapacity(Count + 1); + _elements[Count++] = item; + } + + public void AddRange(IEnumerable elements) + { + if (elements.TryGetSpan(out var span)) + { + AddRange(span); + return; + } + + if (elements.TryGetNonEnumeratedCount(out var count)) + { + EnsureCapacity(Count + count); + } + foreach (var element in elements) + { + Add(element); + } + } + + public void AddRange(Span elements) + { + EnsureCapacity(Count + elements.Length); + elements.CopyTo(_elements.AsSpan(Count)); + Count += elements.Length; + } + + public void Clear() + { + ClearElementsIfNeeded(); + Count = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ClearElementsIfNeeded() + { + if (NeedClear) + { + Array.Clear(_elements, 0, _elements.Length); + } + } + + public bool Contains(TElement item) => Array.IndexOf(_elements, item) != -1; + + public void CopyTo(TElement[] array, int arrayIndex) => _elements.CopyTo(array, arrayIndex); + + public bool Remove(TElement item) + { + var index = Array.IndexOf(_elements, item); + if (index == -1) + { + return false; + } + + RemoveAt(index); + return true; + } + + public bool IsReadOnly => _elements.IsReadOnly; + + public int IndexOf(TElement item) => Array.IndexOf(_elements, item); + + public void Insert(int index, TElement item) + { + if (index < 0 || index > Count) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(index), index); + } + + var length = _elements.Length; + if (Count == length) + { + var newLength = Math.Max(length * 2, StaticConfigs.BuiltInListDefaultCapacity); + var newElements = _pool.Rent(newLength); + _elements.CopyTo(newElements, 0); + Array.Copy(_elements, 0, newElements, 0, index); + newElements[index] = item; + Array.Copy(_elements, index, newElements, index + 1, Count - index); + ClearElementsIfNeeded(); + _pool.Return(_elements); + _elements = newElements; + } + else + { + Array.Copy(_elements, index, _elements, index + 1, Count - index); + _elements[index] = item; + } + Count++; + } + + public void RemoveAt(int index) + { + ThrowIfOutOfRange(index, nameof(index)); + + if (index < Count - 1) + { + Array.Copy(_elements, index + 1, _elements, index, Count - index - 1); + } + Count--; + _elements[Count] = default!; + } + + public TElement this[int index] + { + get + { + ThrowIfOutOfRange(index, nameof(index)); + return _elements[index]; + } + set + { + ThrowIfOutOfRange(index, nameof(index)); + _elements[index] = value; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ThrowIfOutOfRange(int index, string paramName) + { + if (index < 0 || index >= Count) + { + ThrowHelper.ThrowArgumentOutOfRangeException(paramName, index); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator(PooledList list) : IEnumerator + { + private int _count = list.Count; + private int _current = 0; + + public bool MoveNext() + { + return _current++ < _count; + } + + public void Reset() + { + _count = list.Count; + _current = 0; + } + + public TElement Current => list._elements[_current]; + + object IEnumerator.Current => Current!; + + public void Dispose() { } + } + + public void Dispose() + { + if (_elements.Length <= 0) + { + return; + } + + ClearElementsIfNeeded(); + _pool.Return(_elements); + _elements = []; + } + + public Span AsSpan() => _elements.AsSpan(0, Count); +} diff --git a/csharp/Fury/Collections/SpannableList.cs b/csharp/Fury/Collections/SpannableList.cs new file mode 100644 index 0000000000..14fa9b2c61 --- /dev/null +++ b/csharp/Fury/Collections/SpannableList.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using Fury.Helpers; + +namespace Fury.Collections; + +internal sealed class SpannableList : IList, IReadOnlyList +{ + private static readonly bool NeedsClear = TypeHelper.IsReferenceOrContainsReferences; + + private T[] _items = []; + + public int Count { get; private set; } + public bool IsReadOnly => false; + + public SpannableList() { } + + public SpannableList(int capacity) + { + _items = new T[capacity]; + } + + public Enumerator GetEnumerator() => new(this); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private void EnsureCapacity(int requiredCapacity) + { + if (requiredCapacity <= _items.Length) + { + return; + } + + var newCapacity = Math.Max(_items.Length * 2, requiredCapacity); + Array.Resize(ref _items, newCapacity); + } + + void ICollection.Add(T item) => Add(in item); + + public void Add(in T item) + { + EnsureCapacity(Count + 1); + _items[Count++] = item; + } + + public void Clear() + { + if (NeedsClear) + { + Array.Clear(_items, 0, Count); + } + Count = 0; + } + + public bool Contains(T item) => IndexOf(item) != -1; + + public void CopyTo(T[] array, int arrayIndex) + { + Array.Copy(_items, 0, array, arrayIndex, Count); + } + + bool ICollection.Remove(T item) => Remove(in item); + + public bool Remove(in T item) + { + var index = IndexOf(item); + if (index == -1) + { + return false; + } + + RemoveAt(index); + return true; + } + + int IList.IndexOf(T item) => IndexOf(item); + + public int IndexOf(in T item) + { + for (var i = 0; i < Count; i++) + { + if (EqualityComparer.Default.Equals(_items[i], item)) + { + return i; + } + } + + return -1; + } + + void IList.Insert(int index, T item) => Insert(index, in item); + + public void Insert(int index, in T item) + { + EnsureCapacity(Count + 1); + Array.Copy(_items, index, _items, index + 1, Count - index); + _items[index] = item; + Count++; + } + + public void RemoveAt(int index) + { + if (NeedsClear) + { + _items[index] = default!; + } + + Count--; + if (index < Count) + { + Array.Copy(_items, index + 1, _items, index, Count - index); + } + } + + public ref T this[int index] + { + get + { + if (index < 0 || index >= Count) + { + ThrowHelper.ThrowIndexOutOfRangeException(); + } + + return ref _items[index]; + } + } + + T IReadOnlyList.this[int index] => this[index]; + + T IList.this[int index] + { + get => this[index]; + set => this[index] = value; + } + + public Span AsSpan() => new(_items, 0, Count); + + public struct Enumerator() : IEnumerator + { + private int _index = -1; + private readonly SpannableList _list; + T IEnumerator.Current => _list[_index]; + public ref T Current => ref _list[_index]; + + internal Enumerator(SpannableList list) + : this() + { + _list = list; + } + + public bool MoveNext() + { + _index++; + return _index < _list.Count; + } + + public void Reset() + { + _index = -1; + } + + object? IEnumerator.Current => Current; + + public void Dispose() + { + } + } +} diff --git a/csharp/Fury/Config.cs b/csharp/Fury/Config.cs new file mode 100644 index 0000000000..82ac17dbc5 --- /dev/null +++ b/csharp/Fury/Config.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using Fury.Serialization; + +namespace Fury; + +public sealed record FuryConfig(ITypeRegistrationProvider RegistrationProvider, TimeSpan LockTimeOut); + +public sealed record SerializationConfig( + bool ReferenceTracking, + IEnumerable PreferredStringEncodings, + bool WriteUtf16ByteCountForUtf8Encoding +) +{ + public static readonly SerializationConfig Default = new(false, [StringEncoding.UTF8], false); +}; + +public sealed record DeserializationConfig(bool ReadUtf16ByteCountForUtf8Encoding) +{ + public static readonly DeserializationConfig Default = new(false); +}; diff --git a/csharp/Fury/Context/BatchReader.cs b/csharp/Fury/Context/BatchReader.cs new file mode 100644 index 0000000000..c7710de0c0 --- /dev/null +++ b/csharp/Fury/Context/BatchReader.cs @@ -0,0 +1,278 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO.Pipelines; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Fury.Context; + +/// +/// The result of read operation. +/// +/// +/// Indicates if the deserialization operation was successful. +/// If true, the property will contain the deserialized value and +/// the bytes will be consumed. +/// Otherwise, the property will be the default value of and +/// the bytes will be examined. +/// +/// +/// The deserialized value. If is false, this will be the default value of . +/// +/// +/// The type of the deserialized value. +/// +public readonly struct ReadValueResult +{ + /// + /// Indicates if the deserialization operation was successful. + /// If true, the property will contain the deserialized value and + /// the bytes will be consumed. + /// Otherwise, the property will be the default value of and + /// the bytes will be examined. + /// + public bool IsSuccess { get; } + + /// + /// The deserialized value. If is false, this will be the default value of . + /// + public TValue Value { get; } + + public ReadValueResult() + { + IsSuccess = false; + Value = default!; + } + + public ReadValueResult(in TValue value) + { + IsSuccess = true; + Value = value; + } + + /// + /// Creates a successful with the provided value. + /// + /// + /// + public static ReadValueResult FromValue(in TValue value) + { + return new ReadValueResult(in value); + } + + /// + /// A failed instance with the default value of . + /// + public static ReadValueResult Failed { get; } = new(); +} + +// /// +// /// +// /// The state of read operation. +// /// Some read operations may read part of the data even if they fail, this can be used to resume the read operation. +// /// +// /// +// /// The type of the deserialized value. +// /// +// /// +// /// The type of the reading state. +// /// +// public readonly record struct ReadValueResult( +// bool IsSuccess, +// in TValue? Value, +// in TState? ReadingState +// ) +// { +// /// +// /// Creates a successful with the provided value. +// /// +// /// +// /// The deserialized value. +// /// +// /// +// /// A successful instance with the provided value. +// /// +// public static ReadValueResult Success(in TValue? value) +// { +// return new ReadValueResult(true, in value, default); +// } +// +// /// +// /// A failed instance with the default value of . +// /// +// /// +// /// The minimum required bytes to complete the current read operation. +// /// This will be 0 if the write operation is successful. +// /// +// /// +// /// The state of read operation. +// /// +// /// +// /// A failed instance with the default value of +// /// and the provided reading state. +// /// +// public static ReadValueResult Failure(int minRequiredBytes, in TState? readingState) => +// new(false, default, in readingState); +// } +// +// /// +// /// Result of read byte sequence operation. +// /// +// public readonly struct ReadBytesResult +// { +// private readonly ResultFlags _flags; +// public ReadOnlySequence Buffer { get; } +// +// /// +// /// Indicates if the of is greater than or equal to +// /// the provided "sizeHint". +// /// +// public bool IsSuccess => (_flags & ResultFlags.IsSuccess) != 0; +// +// /// +// /// Indicates if all remaining data has been returned and there is no more data to read. +// /// +// public bool IsCompleted => (_flags & ResultFlags.IsCompleted) != 0; +// +// internal ReadBytesResult(bool isSuccess, bool isCompleted, in ReadOnlySequence buffer) +// { +// _flags = ResultFlags.None; +// if (isSuccess) +// { +// _flags |= ResultFlags.IsSuccess; +// } +// if (isCompleted) +// { +// _flags |= ResultFlags.IsCompleted; +// } +// Buffer = buffer; +// } +// +// [Flags] +// private enum ResultFlags +// { +// None = 0, +// IsSuccess = 1, +// IsCompleted = 2, +// } +// } + +internal sealed class BatchReader +{ + private PipeReader _innerReader = null!; + private ReadOnlySequence _currentBuffer; + private ReadOnlySequence _uncomsumedBuffer; + private ReadOnlySequence _unexaminedBuffer; + + internal int Version { get; private set; } + + private bool _isInnerReaderCompleted; + private bool _isLastReadCanceled; + + internal void Reset() + { + _innerReader = null!; + } + + [MemberNotNull(nameof(_innerReader))] + internal void Initialize(PipeReader reader) + { + _innerReader = reader; + _currentBuffer = ReadOnlySequence.Empty; + _uncomsumedBuffer = ReadOnlySequence.Empty; + _unexaminedBuffer = ReadOnlySequence.Empty; + _isInnerReaderCompleted = false; + Version = 0; + } + + public void AdvanceTo(SequencePosition consumed) + { + Version++; + _uncomsumedBuffer = _uncomsumedBuffer.Slice(consumed); + if (_uncomsumedBuffer.Length < _unexaminedBuffer.Length) + { + _unexaminedBuffer = _uncomsumedBuffer; + } + } + + public void AdvanceTo(SequencePosition consumed, SequencePosition examined) + { + Version++; + _uncomsumedBuffer = _uncomsumedBuffer.Slice(consumed); + _unexaminedBuffer = _unexaminedBuffer.Slice(examined); + } + + private void Flush() + { + // Check if the AdvanceTo call is necessary to reduce virtual calls. + var consumed = _uncomsumedBuffer.Start; + var examined = _unexaminedBuffer.Start; + var start = _currentBuffer.Start; + if (!consumed.Equals(start) || !examined.Equals(start)) + { + _innerReader.AdvanceTo(consumed, examined); + _currentBuffer = _uncomsumedBuffer; + } + } + + public ReadResult Read(int sizeHint = 0) + { + if (sizeHint < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(sizeHint), sizeHint); + } + + if (sizeHint > _uncomsumedBuffer.Length) + { + Flush(); + if (_innerReader.TryRead(out var innerResult)) + { + PopulateNewData(in innerResult); + } + } + + return new ReadResult(_uncomsumedBuffer, _isInnerReaderCompleted, _isLastReadCanceled); + } + + public async ValueTask ReadAsync(int sizeHint, CancellationToken cancellationToken = default) + { + if (sizeHint < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(sizeHint), sizeHint); + } + + if (sizeHint is 0) + { + if (_unexaminedBuffer.IsEmpty) + { + Flush(); + var innerResult = await _innerReader.ReadAsync(cancellationToken); + PopulateNewData(in innerResult); + } + } + else if (sizeHint > _uncomsumedBuffer.Length || _unexaminedBuffer.IsEmpty) + { + Flush(); + var innerResult = await _innerReader.ReadAtLeastAsync(sizeHint, cancellationToken); + PopulateNewData(in innerResult); + } + + return new ReadResult(_uncomsumedBuffer, _isInnerReaderCompleted, _isLastReadCanceled); + } + + private void PopulateNewData(in ReadResult result) + { + Version++; + var examined = _uncomsumedBuffer.Length - _unexaminedBuffer.Length; + _currentBuffer = result.Buffer; + _uncomsumedBuffer = result.Buffer; + _unexaminedBuffer = result.Buffer.Slice(examined); + _isInnerReaderCompleted = result.IsCompleted; + _isLastReadCanceled = result.IsCanceled; + } +} diff --git a/csharp/Fury/Context/BatchWriter.cs b/csharp/Fury/Context/BatchWriter.cs new file mode 100644 index 0000000000..252c0e822d --- /dev/null +++ b/csharp/Fury/Context/BatchWriter.cs @@ -0,0 +1,93 @@ +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.IO.Pipelines; +using JetBrains.Annotations; + +namespace Fury.Context; + +// This is used to reduce the virtual call overhead of the PipeWriter + +internal sealed class BatchWriter : IBufferWriter, IDisposable +{ + private PipeWriter _innerWriter = null!; + private Memory _cachedMemory; + + public int Version { get; private set; } + internal int Consumed { get; private set; } + internal Memory Buffer => _cachedMemory; + + public Memory UnconsumedBuffer => _cachedMemory.Slice(Consumed); + public Memory UnflushedConsumedBuffer => _cachedMemory.Slice(0, Consumed); + + [MemberNotNull(nameof(_innerWriter))] + internal void Initialize(PipeWriter writer) + { + _innerWriter = writer; + Consumed = 0; + Version = 0; + } + + internal void Reset() + { + _innerWriter = null!; + Consumed = 0; + Version = 0; + } + + public void Flush() + { + if (Consumed > 0) + { + _innerWriter.Advance(Consumed); + Consumed = 0; + } + _cachedMemory = Memory.Empty; + Version++; + } + + public void Advance(int bytes) + { + if (bytes + Consumed > _cachedMemory.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException_AttemptedToAdvanceFurtherThanBufferLength( + nameof(bytes), + _cachedMemory.Length, + bytes + ); + } + + Consumed += bytes; + Version++; + } + + [MustUseReturnValue] + public Memory GetMemory(int sizeHint = 0) + { + var result = UnconsumedBuffer; + if (result.Length < sizeHint) + { + if (Consumed > 0) + { + _innerWriter.Advance(Consumed); + Consumed = 0; + } + _cachedMemory = _innerWriter.GetMemory(sizeHint); + Version++; + result = UnconsumedBuffer; + } + + return result; + } + + [MustUseReturnValue] + public Span GetSpan(int sizeHint = 0) + { + return GetMemory(sizeHint).Span; + } + + public void Dispose() + { + Flush(); + } +} diff --git a/csharp/Fury/Context/DeserializationReader.cs b/csharp/Fury/Context/DeserializationReader.cs new file mode 100644 index 0000000000..6392451fd9 --- /dev/null +++ b/csharp/Fury/Context/DeserializationReader.cs @@ -0,0 +1,825 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO.Pipelines; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Fury.Collections; +using Fury.Helpers; +using Fury.Meta; +using Fury.Serialization; +using Fury.Serialization.Meta; +using JetBrains.Annotations; + +namespace Fury.Context; + +public sealed class DeserializationReader +{ + private sealed class Frame + { + public object? Value; + public TypeRegistration? Registration; + public RefMetadata? RefMetadata; + public IDeserializer? Deserializer; + + public void Reset() + { + Debug.Assert(Registration is not null || Deserializer is null); + if (Registration is not null && Deserializer is not null) + { + Registration.ReturnDeserializer(Deserializer); + } + Value = null; + Registration = null; + RefMetadata = null; + Deserializer = null; + } + } + + public TypeRegistry TypeRegistry { get; } + internal MetaStringStorage MetaStringStorage { get; } + + public DeserializationConfig Config { get; private set; } = DeserializationConfig.Default; + private readonly BatchReader _innerReader = new(); + + private readonly HeaderDeserializer _headerDeserializer = new(); + private readonly ReferenceMetaDeserializer _referenceMetaDeserializer = new(); + private readonly TypeMetaDeserializer _typeMetaDeserializer; + + internal AutoIncrementIdDictionary MetaStringContext { get; } = new(); + private readonly FrameStack _frameStack = new(); + + internal DeserializationReader(TypeRegistry registry, MetaStringStorage metaStringStorage) + { + TypeRegistry = registry; + MetaStringStorage = metaStringStorage; + _typeMetaDeserializer = new TypeMetaDeserializer(); + _typeMetaDeserializer.Initialize(TypeRegistry, MetaStringStorage, MetaStringContext); + } + + internal void Reset() + { + _innerReader.Reset(); + _headerDeserializer.Reset(); + ResetCurrent(); + foreach (var frame in _frameStack.Frames) + { + frame.Reset(); + } + _frameStack.Reset(); + } + + private void ResetCurrent() + { + _referenceMetaDeserializer.ResetCurrent(); + _typeMetaDeserializer.ResetCurrent(); + } + + internal void Initialize(PipeReader pipeReader, DeserializationConfig config) + { + Config = config; + _innerReader.Initialize(pipeReader); + } + + private void OnCurrentDeserializationCompleted(bool isSuccess) + { + if (isSuccess) + { + ResetCurrent(); + _frameStack.PopFrame().Reset(); + } + else + { + _frameStack.MoveLast(); + } + } + + internal ValueTask> ReadHeader(bool isAsync, CancellationToken cancellationToken) + { + return _headerDeserializer.Read(this, isAsync, cancellationToken); + } + + [MustUseReturnValue] + public ReadValueResult Read(TypeRegistration? registrationHint = null) + { + var task = Read(registrationHint, ObjectMetaOption.ReferenceMeta | ObjectMetaOption.TypeMeta, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + [MustUseReturnValue] + public ValueTask> ReadAsync(TypeRegistration? registrationHint = null, CancellationToken cancellationToken = default) + { + return Read(registrationHint, ObjectMetaOption.ReferenceMeta | ObjectMetaOption.TypeMeta, true, cancellationToken); + } + + [MustUseReturnValue] + internal async ValueTask> Read( + TypeRegistration? registrationHint, + ObjectMetaOption metaOption, + bool isAsync, + CancellationToken cancellationToken + ) + { + _frameStack.MoveNext(); + var currentFrame = _frameStack.CurrentFrame; + var isSuccess = false; + try + { + if ((metaOption & ObjectMetaOption.ReferenceMeta) != 0) + { + isSuccess = await ReadRefMeta(currentFrame, isAsync, cancellationToken); + if (!isSuccess) + { + return ReadValueResult.Failed; + } + } + else + { + // If reading RefFlag is not required, it should be equivalent to RefFlag.NotNullValue. + currentFrame.RefMetadata = new RefMetadata(RefFlag.NotNullValue); + } + + var valueResult = await ReadCommon(currentFrame, metaOption, registrationHint, isAsync, cancellationToken); + isSuccess = valueResult.IsSuccess; + return valueResult; + } + finally + { + OnCurrentDeserializationCompleted(isSuccess); + } + } + + [MustUseReturnValue] + public ReadValueResult ReadNullable(TypeRegistration? registrationHint = null) + where TTarget : struct + { + var task = ReadNullable(registrationHint, ObjectMetaOption.ReferenceMeta | ObjectMetaOption.TypeMeta, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + [MustUseReturnValue] + public async ValueTask> ReadNullableAsync( + TypeRegistration? registrationHint = null, + CancellationToken cancellationToken = default + ) + where TTarget : struct + { + return await ReadNullable(registrationHint, ObjectMetaOption.ReferenceMeta | ObjectMetaOption.TypeMeta, true, cancellationToken); + } + + [MustUseReturnValue] + internal async ValueTask> ReadNullable( + TypeRegistration? registrationHint, + ObjectMetaOption metaOption, + bool isAsync, + CancellationToken cancellationToken + ) + where TTarget : struct + { + _frameStack.MoveNext(); + var currentFrame = _frameStack.CurrentFrame; + var isSuccess = false; + try + { + var refMetaResult = await ReadRefMeta(currentFrame, isAsync, cancellationToken); + if (!refMetaResult) + { + return ReadValueResult.Failed; + } + + if (currentFrame.RefMetadata is { RefFlag: RefFlag.Null }) + { + return ReadValueResult.FromValue(null); + } + + var valueResult = await ReadCommon(currentFrame, metaOption, registrationHint, isAsync, cancellationToken); + isSuccess = valueResult.IsSuccess; + if (!isSuccess) + { + return ReadValueResult.Failed; + } + + return ReadValueResult.FromValue(valueResult.Value); + } + finally + { + OnCurrentDeserializationCompleted(isSuccess); + } + } + + private async ValueTask ReadRefMeta(Frame currentFrame, bool isAsync, CancellationToken cancellationToken) + { + if (currentFrame.RefMetadata is not null) + { + return true; + } + var readResult = await _referenceMetaDeserializer.Read(this, isAsync, cancellationToken); + if (!readResult.IsSuccess) + { + return false; + } + currentFrame.RefMetadata = readResult.Value; + return true; + } + + private async ValueTask> ReadCommon( + Frame currentFrame, + ObjectMetaOption metaOption, + TypeRegistration? registrationHint, + bool isAsync, + CancellationToken cancellationToken + ) + { + if (currentFrame.RefMetadata is not { } refMeta) + { + ThrowHelper.ThrowArgumentException(nameof(metaOption)); + return ReadValueResult.Failed; + } + + if (refMeta is { RefFlag: RefFlag.Null }) + { + // Maybe we should throw an exception here for value types + return ReadValueResult.FromValue(default); + } + + if (refMeta is { RefFlag: RefFlag.Ref, RefId: var refId }) + { + _referenceMetaDeserializer.GetReadValue(refId, out var readValue); + return ReadValueResult.FromValue((TTarget)readValue); + } + + if (refMeta is { RefFlag: RefFlag.RefValue }) + { + if (!await ReadTypeMeta(currentFrame, metaOption, typeof(TTarget), registrationHint, isAsync, cancellationToken)) + { + return ReadValueResult.Failed; + } + + if (!await ReadReferenceable(currentFrame, isAsync, cancellationToken)) + { + return ReadValueResult.Failed; + } + + return ReadValueResult.FromValue((TTarget?)currentFrame.Value); + } + + if (refMeta is { RefFlag: RefFlag.NotNullValue }) + { + if (!await ReadTypeMeta(currentFrame, metaOption, typeof(TTarget), registrationHint, isAsync, cancellationToken)) + { + return ReadValueResult.Failed; + } + + return (await ReadUnreferenceable(currentFrame, isAsync, cancellationToken))!; + } + + ThrowBadDeserializationInputExceptionException_InvalidRefFlag(refMeta.RefFlag); + return ReadValueResult.Failed; + } + + [DoesNotReturn] + private static void ThrowBadDeserializationInputExceptionException_InvalidRefFlag(RefFlag refFlag) + { + throw new BadDeserializationInputException($"Invalid RefFlag: {refFlag}"); + } + + [DoesNotReturn] + private static void ThrowArgumentNullException_RegistrationHintIsNull([InvokerParameterName] string paramName) + { + throw new ArgumentNullException(paramName, $"When type meta is not read, the {paramName} must not be null."); + } + + [DoesNotReturn] + private static void ThrowArgumentException_RegistrationHintDoesNotMatch( + Type declaredType, + TypeRegistration registrationHint, + [InvokerParameterName] string paramName + ) + { + throw new ArgumentException( + $"Provided registration hint's target type `{registrationHint.TargetType}` cannot be assigned to `{declaredType}`.", + paramName + ); + } + + private async ValueTask ReadTypeMeta( + Frame currentFrame, + ObjectMetaOption metaOption, + Type declaredType, + TypeRegistration? registrationHint, + bool isAsync, + CancellationToken cancellationToken + ) + { + if ((metaOption & ObjectMetaOption.TypeMeta) != 0) + { + var typeMetaResult = await _typeMetaDeserializer.Read(this, declaredType, registrationHint, isAsync, cancellationToken); + if (!typeMetaResult.IsSuccess) + { + return false; + } + currentFrame.Registration = typeMetaResult.Value; + } + else + { + if (registrationHint is null) + { + ThrowArgumentNullException_RegistrationHintIsNull(nameof(registrationHint)); + } + + if (!declaredType.IsAssignableFrom(registrationHint.TargetType)) + { + ThrowArgumentException_RegistrationHintDoesNotMatch(declaredType, registrationHint, nameof(registrationHint)); + } + + currentFrame.Registration = registrationHint; + } + + return true; + } + + private async ValueTask ReadReferenceable(Frame currentFrame, bool isAsync, CancellationToken cancellationToken) + { + if (currentFrame.Value is not null) + { + return true; + } + + Debug.Assert(currentFrame.Registration is not null); + var deserializer = currentFrame.Deserializer ?? currentFrame.Registration.RentDeserializer(); + + var createResult = ReadValueResult.Failed; + try + { + if (currentFrame.RefMetadata is { RefFlag: RefFlag.RefValue, RefId: var refId }) + { + // Associate the deserializer with the reference ID before deserialization. + // So that we can use it to get partial deserialization results when circular references occur. + _referenceMetaDeserializer.AddInProgressDeserializer(refId, deserializer); + } + if (isAsync) + { + createResult = await deserializer.DeserializeAsync(this, cancellationToken); + } + else + { + // ReSharper disable once MethodHasAsyncOverloadWithCancellation + createResult = deserializer.Deserialize(this); + } + + return createResult.IsSuccess; + } + finally + { + if (createResult.IsSuccess) + { + currentFrame.Value = createResult.Value; + + if (currentFrame.RefMetadata is { RefFlag: RefFlag.RefValue, RefId: var refId }) + { + // If no circular reference occurs, the ReferenceableObject of deserializer will not be called. + // To make the result referenceable, we need to associate it with the reference ID here. + _referenceMetaDeserializer.AddReadValue(refId, createResult.Value); + } + Debug.Assert(createResult.Value is not null); + } + } + } + + private async ValueTask> ReadUnreferenceable(Frame currentFrame, bool isAsync, CancellationToken cancellationToken) + { + Debug.Assert(currentFrame.Registration is not null); + var deserializer = currentFrame.Deserializer ?? currentFrame.Registration.RentDeserializer(); + if (deserializer is not IDeserializer typedDeserializer) + { + ReadValueResult untypedResult; + if (isAsync) + { + untypedResult = await deserializer.DeserializeAsync(this, cancellationToken); + } + else + { + // ReSharper disable once MethodHasAsyncOverloadWithCancellation + untypedResult = deserializer.Deserialize(this); + } + + if (!untypedResult.IsSuccess) + { + return ReadValueResult.Failed; + } + return ReadValueResult.FromValue((TTarget)untypedResult.Value); + } + + // If the declared type matches the deserializer type, + // we can use the typed deserializer for better performance. + + ReadValueResult result; + if (isAsync) + { + result = await typedDeserializer.DeserializeAsync(this, cancellationToken); + } + else + { + // ReSharper disable once MethodHasAsyncOverloadWithCancellation + result = typedDeserializer.Deserialize(this); + } + + return result; + } + + public ReadResult Read(int sizeHint = 0) + { + return _innerReader.Read(sizeHint); + } + + public ValueTask ReadAsync(CancellationToken cancellationToken = default) + { + return _innerReader.ReadAsync(0, cancellationToken); + } + + public ValueTask ReadAsync(int sizeHint = 0, CancellationToken cancellationToken = default) + { + return _innerReader.ReadAsync(sizeHint, cancellationToken); + } + + internal async ValueTask Read(int sizeHint, bool isAsync, CancellationToken cancellationToken) + { + if (isAsync) + { + return await ReadAsync(sizeHint, cancellationToken); + } + + // ReSharper disable once MethodHasAsyncOverloadWithCancellation + return Read(sizeHint); + } + + public void AdvanceTo(SequencePosition consumed) + { + _innerReader.AdvanceTo(consumed); + } + + public void AdvanceTo(SequencePosition consumed, SequencePosition examined) + { + _innerReader.AdvanceTo(consumed, examined); + } + + #region Read Methods + + private ReadValueResult ReadUnmanagedCommon(in ReadResult sequenceResult) + where T : unmanaged + { + var size = Unsafe.SizeOf(); + var buffer = sequenceResult.Buffer; + var bufferLength = buffer.Length; + if (bufferLength < size) + { + AdvanceTo(buffer.Start, buffer.End); + return ReadValueResult.Failed; + } + if (bufferLength > size) + { + buffer = buffer.Slice(size); + } + T value = default; + var destination = MemoryMarshal.AsBytes(SpanHelper.CreateSpan(ref value, 1)); + buffer.CopyTo(destination); + AdvanceTo(buffer.End); + return ReadValueResult.FromValue(in value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ReadValueResult ReadUnmanagedAs(int size) + where T : unmanaged + { + var sequenceResult = Read(size); + return ReadUnmanagedCommon(in sequenceResult); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal async ValueTask> ReadUnmanagedAsAsync(int size, CancellationToken cancellationToken) + where T : unmanaged + { + var sequenceResult = await ReadAsync(size, cancellationToken); + return ReadUnmanagedCommon(in sequenceResult); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ValueTask> ReadUnmanagedAs(int size, bool isAsync, CancellationToken cancellationToken) + where T : unmanaged + { + if (isAsync) + { + return ReadUnmanagedAsAsync(size, cancellationToken); + } + + return new ValueTask>(ReadUnmanagedAs(size)); + } + + public int ReadBytes(scoped Span destination) + { + var readResult = Read(destination.Length); + var buffer = readResult.Buffer; + var (consumed, consumedLength) = buffer.CopyUpTo(destination); + AdvanceTo(consumed); + return consumedLength; + } + + public async ValueTask ReadBytesAsync(Memory destination, CancellationToken cancellationToken = default) + { + var readResult = await ReadAsync(destination.Length, cancellationToken); + var buffer = readResult.Buffer; + var (consumed, consumedLength) = buffer.CopyUpTo(destination.Span); + AdvanceTo(consumed); + return consumedLength; + } + + public ReadValueResult ReadUInt8() => ReadUnmanagedAs(sizeof(byte)); + + public ReadValueResult ReadInt8() => ReadUnmanagedAs(sizeof(sbyte)); + + public ReadValueResult ReadUInt16() => ReadUnmanagedAs(sizeof(ushort)); + + public ReadValueResult ReadInt16() => ReadUnmanagedAs(sizeof(short)); + + public ReadValueResult ReadUInt32() => ReadUnmanagedAs(sizeof(uint)); + + public ReadValueResult ReadInt32() => ReadUnmanagedAs(sizeof(int)); + + public ReadValueResult ReadUInt64() => ReadUnmanagedAs(sizeof(ulong)); + + public ReadValueResult ReadInt64() => ReadUnmanagedAs(sizeof(long)); + + public ReadValueResult ReadFloat32() => ReadUnmanagedAs(sizeof(float)); + + public ReadValueResult ReadFloat64() => ReadUnmanagedAs(sizeof(double)); + + public ReadValueResult ReadBoolean() => ReadUnmanagedAs(sizeof(bool)); + + public async ValueTask> ReadUInt8Async(CancellationToken cancellationToken = default) => + await ReadUnmanagedAsAsync(sizeof(byte), cancellationToken); + + public async ValueTask> ReadInt8Async(CancellationToken cancellationToken = default) => + await ReadUnmanagedAsAsync(sizeof(sbyte), cancellationToken); + + public async ValueTask> ReadUInt16Async(CancellationToken cancellationToken = default) => + await ReadUnmanagedAsAsync(sizeof(ushort), cancellationToken); + + public async ValueTask> ReadInt16Async(CancellationToken cancellationToken = default) => + await ReadUnmanagedAsAsync(sizeof(short), cancellationToken); + + public async ValueTask> ReadUInt32Async(CancellationToken cancellationToken = default) => + await ReadUnmanagedAsAsync(sizeof(uint), cancellationToken); + + public async ValueTask> ReadInt32Async(CancellationToken cancellationToken = default) => + await ReadUnmanagedAsAsync(sizeof(int), cancellationToken); + + public async ValueTask> ReadUInt64Async(CancellationToken cancellationToken = default) => + await ReadUnmanagedAsAsync(sizeof(ulong), cancellationToken); + + public async ValueTask> ReadInt64Async(CancellationToken cancellationToken = default) => + await ReadUnmanagedAsAsync(sizeof(long), cancellationToken); + + public async ValueTask> ReadFloat32Async(CancellationToken cancellationToken = default) => + await ReadUnmanagedAsAsync(sizeof(float), cancellationToken); + + public async ValueTask> ReadFloat64Async(CancellationToken cancellationToken = default) => + await ReadUnmanagedAsAsync(sizeof(double), cancellationToken); + + public async ValueTask> ReadBooleanAsync(CancellationToken cancellationToken = default) => + await ReadUnmanagedAsAsync(sizeof(bool), cancellationToken); + + public ValueTask> ReadUInt8(bool isAsync, CancellationToken cancellationToken = default) + { + return isAsync ? ReadUInt8Async(cancellationToken) : new ValueTask>(ReadUInt8()); + } + + public ValueTask> ReadInt8(bool isAsync, CancellationToken cancellationToken = default) + { + return isAsync ? ReadInt8Async(cancellationToken) : new ValueTask>(ReadInt8()); + } + + public ValueTask> ReadUInt16(bool isAsync, CancellationToken cancellationToken = default) + { + return isAsync ? ReadUInt16Async(cancellationToken) : new ValueTask>(ReadUInt16()); + } + + public ValueTask> ReadInt16(bool isAsync, CancellationToken cancellationToken = default) + { + return isAsync ? ReadInt16Async(cancellationToken) : new ValueTask>(ReadInt16()); + } + + public ValueTask> ReadUInt32(bool isAsync, CancellationToken cancellationToken = default) + { + return isAsync ? ReadUInt32Async(cancellationToken) : new ValueTask>(ReadUInt32()); + } + + public ValueTask> ReadInt32(bool isAsync, CancellationToken cancellationToken = default) + { + return isAsync ? ReadInt32Async(cancellationToken) : new ValueTask>(ReadInt32()); + } + + public ValueTask> ReadUInt64(bool isAsync, CancellationToken cancellationToken = default) + { + return isAsync ? ReadUInt64Async(cancellationToken) : new ValueTask>(ReadUInt64()); + } + + public ValueTask> ReadInt64(bool isAsync, CancellationToken cancellationToken = default) + { + return isAsync ? ReadInt64Async(cancellationToken) : new ValueTask>(ReadInt64()); + } + + public ValueTask> ReadFloat32(bool isAsync, CancellationToken cancellationToken = default) + { + return isAsync ? ReadFloat32Async(cancellationToken) : new ValueTask>(ReadFloat32()); + } + + public ValueTask> ReadFloat64(bool isAsync, CancellationToken cancellationToken = default) + { + return isAsync ? ReadFloat64Async(cancellationToken) : new ValueTask>(ReadFloat64()); + } + + public ValueTask> ReadBoolean(bool isAsync, CancellationToken cancellationToken = default) + { + return isAsync ? ReadBooleanAsync(cancellationToken) : new ValueTask>(ReadBoolean()); + } + + private const int MaxBytesOfVarInt32 = 5; + + public ReadValueResult Read7BitEncodedUint() + { + var task = Read7BitEncodedUint(false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public ReadValueResult Read7BitEncodedInt() + { + var uintResult = Read7BitEncodedUint(); + if (!uintResult.IsSuccess) + { + return ReadValueResult.Failed; + } + var value = (int)BitOperations.RotateRight(uintResult.Value, 1); + return ReadValueResult.FromValue(value); + } + + public ValueTask> Read7BitEncodedUintAsync(CancellationToken cancellationToken = default) + { + return Read7BitEncodedUint(true, cancellationToken); + } + + public async ValueTask> Read7BitEncodedIntAsync(CancellationToken cancellationToken = default) + { + var uintResult = await Read7BitEncodedUintAsync(cancellationToken); + if (!uintResult.IsSuccess) + { + return ReadValueResult.Failed; + } + var value = (int)BitOperations.RotateRight(uintResult.Value, 1); + return ReadValueResult.FromValue(value); + } + + internal async ValueTask> Read7BitEncodedUint(bool isAsync, CancellationToken cancellationToken) + { + uint value = 0; + var reader = new SequenceReader(ReadOnlySequence.Empty); + + while (reader.Consumed < MaxBytesOfVarInt32) + { + var consumed = (int)reader.Consumed; + if (!reader.TryRead(out var currentByte)) + { + if (reader.Consumed > 0) + { + AdvanceTo(reader.Sequence.Start, reader.Position); + } + + // We do not check if the sequenceResult is success because varint32 may be less than 5 bytes. + var bytesResult = await Read(MaxBytesOfVarInt32 - consumed, isAsync, cancellationToken); + reader = new SequenceReader(bytesResult.Buffer); + Debug.Assert(reader.Length >= consumed); + reader.Advance(consumed); + + if (!reader.TryRead(out currentByte)) + { + return ReadValueResult.Failed; + } + } + + if (consumed < MaxBytesOfVarInt32 - 1) + { + value |= ((uint)currentByte & 0x7F) << (consumed * 7); + if ((currentByte & 0x80) == 0) + { + AdvanceTo(reader.Position); + break; + } + } + else + { + if (currentByte > 0b_1111u) + { + ThrowBadDeserializationInputException_VarInt32Overflow(); + } + + value |= (uint)currentByte << ((MaxBytesOfVarInt32 - 1) * 7); + AdvanceTo(reader.Position); + } + } + return ReadValueResult.FromValue(in value); + } + + [DoesNotReturn] + private static void ThrowBadDeserializationInputException_VarInt32Overflow() + { + throw new InvalidOperationException("VarInt32 overflow."); + } + + private const int MaxBytesOfVarInt64 = 9; + + public ReadValueResult Read7BitEncodedUlong() + { + var task = Read7BitEncodedUlong(false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public ReadValueResult Read7BitEncodedLong() + { + var ulongResult = Read7BitEncodedUlong(); + if (!ulongResult.IsSuccess) + { + return ReadValueResult.Failed; + } + var value = (long)BitOperations.RotateRight(ulongResult.Value, 1); + return ReadValueResult.FromValue(value); + } + + public ValueTask> Read7BitEncodedUlongAsync(CancellationToken cancellationToken = default) + { + return Read7BitEncodedUlong(true, cancellationToken); + } + + public async ValueTask> Read7BitEncodedLongAsync(CancellationToken cancellationToken = default) + { + var ulongResult = await Read7BitEncodedUlongAsync(cancellationToken); + if (!ulongResult.IsSuccess) + { + return ReadValueResult.Failed; + } + var value = (long)BitOperations.RotateRight(ulongResult.Value, 1); + return ReadValueResult.FromValue(value); + } + + internal async ValueTask> Read7BitEncodedUlong(bool isAsync, CancellationToken cancellationToken = default) + { + ulong value = 0; + var reader = new SequenceReader(ReadOnlySequence.Empty); + + while (reader.Consumed < MaxBytesOfVarInt64) + { + var consumed = (int)reader.Consumed; + if (!reader.TryRead(out var currentByte)) + { + if (reader.Consumed > 0) + { + AdvanceTo(reader.Sequence.Start, reader.Position); + } + + // We do not check if the sequenceResult is success because varint64 may be less than 9 bytes. + var bytesResult = await Read(MaxBytesOfVarInt64 - consumed, isAsync, cancellationToken); + reader = new SequenceReader(bytesResult.Buffer); + Debug.Assert(reader.Length >= consumed); + reader.Advance(consumed); + + if (!reader.TryRead(out currentByte)) + { + return ReadValueResult.Failed; + } + } + + if (consumed < MaxBytesOfVarInt64 - 1) + { + value |= ((ulong)currentByte & 0x7F) << (consumed * 7); + if ((currentByte & 0x80) == 0) + { + AdvanceTo(reader.Position); + break; + } + } + else + { + value |= (ulong)currentByte << ((MaxBytesOfVarInt64 - 1) * 7); + AdvanceTo(reader.Position); + } + } + return ReadValueResult.FromValue(in value); + } + #endregion +} diff --git a/csharp/Fury/Context/FrameStack.cs b/csharp/Fury/Context/FrameStack.cs new file mode 100644 index 0000000000..dd5b3b4be0 --- /dev/null +++ b/csharp/Fury/Context/FrameStack.cs @@ -0,0 +1,48 @@ +using System; +using Fury.Collections; + +namespace Fury.Context; + +internal sealed class FrameStack + where TFrame : class, new() +{ + private readonly SpannableList _frames = []; + + private int _frameCount; + private int _currentFrameIndex = -1; + + /// + /// Get the current frame. When resuming serialization or deserialization, the current frame may not be the last frame. + /// + public TFrame CurrentFrame => _frames[_currentFrameIndex]; + public bool IsCurrentTheLastFrame => _currentFrameIndex == _frameCount - 1; + + public void MoveNext() + { + _currentFrameIndex++; + _frameCount = Math.Max(_frameCount, _currentFrameIndex + 1); + if (_frames.Count < _frameCount) + { + _frames.Add(new TFrame()); + } + } + + public void MoveLast() + { + _currentFrameIndex--; + } + + public TFrame PopFrame() + { + _frameCount--; + return _frames[_currentFrameIndex--]; + } + + public void Reset() + { + _currentFrameIndex = -1; + _frameCount = 0; + } + + public ReadOnlySpan Frames => _frames.AsSpan().Slice(_frameCount); +} diff --git a/csharp/Fury/Context/MetaStringStorage.cs b/csharp/Fury/Context/MetaStringStorage.cs new file mode 100644 index 0000000000..6e36217a25 --- /dev/null +++ b/csharp/Fury/Context/MetaStringStorage.cs @@ -0,0 +1,212 @@ +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Runtime.InteropServices; +using Fury.Helpers; +using Fury.Meta; + +namespace Fury.Context; + +internal sealed class MetaStringStorage +{ + private const char NamespaceSpecialChar1 = '.'; + private const char NamespaceSpecialChar2 = '_'; + private const char NameSpecialChar1 = '$'; + private const char NameSpecialChar2 = '_'; + private const char FieldSpecialChar1 = '$'; + private const char FieldSpecialChar2 = '_'; + + public static MetaString EmptyNamespaceMetaString { get; } = + new(string.Empty, MetaString.Encoding.Utf8, NamespaceSpecialChar1, NamespaceSpecialChar2, []); + public static MetaString EmptyNameMetaString { get; } = + new(string.Empty, MetaString.Encoding.Utf8, NameSpecialChar1, NameSpecialChar2, []); + public static MetaString EmptyFieldMetaString { get; } = + new(string.Empty, MetaString.Encoding.Utf8, FieldSpecialChar1, FieldSpecialChar2, []); + + private static readonly MetaString.Encoding[] CandidateNamespaceEncodings = + [ + MetaString.Encoding.Utf8, + MetaString.Encoding.AllToLowerSpecial, + MetaString.Encoding.LowerUpperDigitSpecial, + ]; + private static readonly MetaString.Encoding[] CandidateNameEncodings = + [ + MetaString.Encoding.Utf8, + MetaString.Encoding.LowerUpperDigitSpecial, + MetaString.Encoding.FirstToLowerSpecial, + MetaString.Encoding.AllToLowerSpecial, + ]; + private static readonly MetaString.Encoding[] CandidateFieldEncodings = + [ + MetaString.Encoding.Utf8, + MetaString.Encoding.LowerUpperDigitSpecial, + MetaString.Encoding.AllToLowerSpecial, + ]; + + public static readonly HybridMetaStringEncoding NamespaceEncoding = new( + NamespaceSpecialChar1, + NamespaceSpecialChar2, + CandidateNamespaceEncodings + ); + + public static readonly HybridMetaStringEncoding NameEncoding = new( + NameSpecialChar1, + NameSpecialChar2, + CandidateNameEncodings + ); + public static readonly HybridMetaStringEncoding FieldEncoding = new( + FieldSpecialChar1, + FieldSpecialChar2, + CandidateFieldEncodings + ); + + private readonly ConcurrentDictionary _namespaceMetaStrings = new(); + private readonly ConcurrentDictionary _nameMetaStrings = new(); + private readonly ConcurrentDictionary _fieldMetaStrings = new(); + + private readonly ConcurrentDictionary _hashCodeToNamespaceMetaString = new(); + private readonly ConcurrentDictionary _hashCodeToNameMetaString = new(); + private readonly ConcurrentDictionary _hashCodeToFieldMetaString = new(); + + [Pure] + public static MetaString GetEmptyMetaString(EncodingPolicy policy) + { + return policy switch + { + EncodingPolicy.Namespace => EmptyNamespaceMetaString, + EncodingPolicy.Name => EmptyNameMetaString, + EncodingPolicy.Field => EmptyFieldMetaString, + _ => ThrowHelper.ThrowUnreachableException(), + }; + } + + public MetaString GetMetaString(string? chars, EncodingPolicy policy) + { + if (chars is null) + { + return GetEmptyMetaString(policy); + } + var hybridEncoding = GetHybridEncoding(policy); + var metaStrings = GetMetaStrings(policy); + var encoding = hybridEncoding.SelectEncoding(chars); + var metaString = metaStrings.GetOrAdd( + chars, + str => + { + var bytes = encoding.GetBytes(str); + return new MetaString( + str, + encoding.Encoding, + hybridEncoding.SpecialChar1, + hybridEncoding.SpecialChar2, + bytes + ); + } + ); + return metaString; + } + + public MetaString GetMetaString( + ulong hashCode, + in ReadOnlySequence bytesSequence, + EncodingPolicy policy, + ref MetaStringFactory? cache + ) + { + Debug.Assert(bytesSequence.Length > MetaString.SmallStringThreshold); + cache ??= new MetaStringFactory(); + var metaStringFactory = cache.GetMetaStringFactory(in bytesSequence, policy); + var hashCodeToMetaString = GetHashCodeToMetaString(policy); + var metaString = hashCodeToMetaString.GetOrAdd(hashCode, metaStringFactory); + if (metaString.HashCode != hashCode) + { + hashCodeToMetaString.TryRemove(hashCode, out _); + ThrowHelper.ThrowBadDeserializationInputException_BadMetaStringHashCodeOrBytes(); + } + + return metaString; + } + + private static HybridMetaStringEncoding GetHybridEncoding(EncodingPolicy policy) + { + return policy switch + { + EncodingPolicy.Namespace => NamespaceEncoding, + EncodingPolicy.Name => NameEncoding, + EncodingPolicy.Field => FieldEncoding, + _ => ThrowHelper.ThrowUnreachableException(), + }; + } + + private ConcurrentDictionary GetMetaStrings(EncodingPolicy policy) + { + return policy switch + { + EncodingPolicy.Namespace => _namespaceMetaStrings, + EncodingPolicy.Name => _nameMetaStrings, + EncodingPolicy.Field => _fieldMetaStrings, + _ => ThrowHelper.ThrowUnreachableException>(), + }; + } + + private ConcurrentDictionary GetHashCodeToMetaString(EncodingPolicy policy) + { + return policy switch + { + EncodingPolicy.Namespace => _hashCodeToNamespaceMetaString, + EncodingPolicy.Name => _hashCodeToNameMetaString, + EncodingPolicy.Field => _hashCodeToFieldMetaString, + _ => ThrowHelper.ThrowUnreachableException>(), + }; + } + + public enum EncodingPolicy + { + Namespace, + Name, + Field, + } + + // A delegate cache to avoid allocations on every call to ConcurrentDictionary.GetOrAdd + public sealed class MetaStringFactory + { + private ReadOnlySequence _bytes; + private EncodingPolicy _policy; + + private readonly Func _cachedMetaStringFactory; + + public MetaStringFactory() + { + // Cache the factory delegate to avoid allocations on every call to ConcurrentDictionary.GetOrAdd + _cachedMetaStringFactory = CreateMetaString; + } + + public Func GetMetaStringFactory(in ReadOnlySequence bytes, EncodingPolicy policy) + { + _bytes = bytes; + _policy = policy; + return _cachedMetaStringFactory; + } + + private MetaString CreateMetaString(ulong hashCode) + { + var bytes = _bytes.ToArray(); + + var metaEncoding = MetaString.GetEncodingFromHashCode(hashCode); + var hybridEncoding = GetHybridEncoding(_policy); + var encoding = hybridEncoding.GetEncoding(metaEncoding); + var charCount = encoding.GetCharCount(bytes); + var str = StringHelper.Create(charCount, (encoding, bytes), DecodeBytes); + return new MetaString(str, metaEncoding, hybridEncoding.SpecialChar1, hybridEncoding.SpecialChar2, bytes); + } + + private static void DecodeBytes(Span chars, (MetaStringEncoding, byte[]) state) + { + var (encoding, bytes) = state; + var charsWritten = encoding.GetChars(bytes, chars); + Debug.Assert(charsWritten == chars.Length); + } + } +} diff --git a/csharp/Fury/Context/ObjectMetaOption.cs b/csharp/Fury/Context/ObjectMetaOption.cs new file mode 100644 index 0000000000..c92155c4c0 --- /dev/null +++ b/csharp/Fury/Context/ObjectMetaOption.cs @@ -0,0 +1,10 @@ +using System; + +namespace Fury.Context; + +[Flags] +internal enum ObjectMetaOption +{ + ReferenceMeta = 1, + TypeMeta = 2, +} diff --git a/csharp/Fury/Context/SerializationWriter.cs b/csharp/Fury/Context/SerializationWriter.cs new file mode 100644 index 0000000000..54e17148d6 --- /dev/null +++ b/csharp/Fury/Context/SerializationWriter.cs @@ -0,0 +1,350 @@ +using System; +using System.Diagnostics; +using System.IO.Pipelines; +using Fury.Collections; +using Fury.Meta; +using Fury.Serialization; +using Fury.Serialization.Meta; +using JetBrains.Annotations; + +namespace Fury.Context; + +public sealed class SerializationWriter : IDisposable +{ + private sealed class Frame + { + public bool NeedNotifyWriteValueCompleted; + public TypeRegistration? Registration; + public ISerializer? Serializer; + + public void Reset() + { + Debug.Assert(Registration is not null || Serializer is null); + if (Registration is not null && Serializer is not null) + { + Registration.ReturnSerializer(Serializer); + } + NeedNotifyWriteValueCompleted = false; + Registration = null; + Serializer = null; + } + } + + public SerializationConfig Config { get; private set; } = SerializationConfig.Default; + private readonly BatchWriter _innerWriter = new(); + + public TypeRegistry TypeRegistry { get; } + + private readonly HeaderSerializer _headerSerializer = new(); + private readonly ReferenceMetaSerializer _referenceMetaSerializer = new(); + private readonly TypeMetaSerializer _typeMetaSerializer; + + internal AutoIncrementIdDictionary MetaStringContext { get; } = new(); + private readonly FrameStack _frameStack = new(); + + public SerializationWriterRef ByrefWriter => new(this, _innerWriter); + + internal SerializationWriter(TypeRegistry registry) + { + TypeRegistry = registry; + _typeMetaSerializer = new TypeMetaSerializer(); + _typeMetaSerializer.Initialize(MetaStringContext); + } + + internal void Reset() + { + _innerWriter.Reset(); + _headerSerializer.Reset(); + ResetCurrent(); + foreach (var frame in _frameStack.Frames) + { + frame.Reset(); + } + _frameStack.Reset(); + } + + private void ResetCurrent() + { + _referenceMetaSerializer.ResetCurrent(); + _typeMetaSerializer.Reset(); + } + + internal void Initialize(PipeWriter pipeWriter, SerializationConfig config) + { + Config = config; + _innerWriter.Initialize(pipeWriter); + _referenceMetaSerializer.Initialize(config.ReferenceTracking); + } + + public void Dispose() + { + _innerWriter.Dispose(); + TypeRegistry.Dispose(); + } + + private void OnCurrentSerializationCompleted(bool isSuccess) + { + if (isSuccess) + { + ResetCurrent(); + _frameStack.PopFrame().Reset(); + } + else + { + _frameStack.MoveLast(); + } + } + + internal bool WriteHeader(bool rootObjectIsNull) + { + var writerRef = ByrefWriter; + return _headerSerializer.Write(ref writerRef, rootObjectIsNull); + } + + [MustUseReturnValue] + public bool Write(in TTarget? value, TypeRegistration? registrationHint = null) + { + return Write(in value, ObjectMetaOption.ReferenceMeta | ObjectMetaOption.TypeMeta, registrationHint); + } + + [MustUseReturnValue] + internal bool Write(in TTarget? value, ObjectMetaOption metaOption, TypeRegistration? registrationHint = null) + { + _frameStack.MoveNext(); + var currentFrame = _frameStack.CurrentFrame; + var needWriteMeta = _frameStack.IsCurrentTheLastFrame; + var isSuccess = false; + try + { + var writer = ByrefWriter; + if (needWriteMeta) + { + isSuccess = WriteMeta(currentFrame, ref writer, in value, metaOption, registrationHint, out var needWriteValue); + if (!isSuccess) + { + return false; + } + if (!needWriteValue) + { + return true; + } + } + isSuccess = WriteValue(currentFrame, in value); + } + finally + { + OnCurrentSerializationCompleted(isSuccess); + } + return isSuccess; + } + + [MustUseReturnValue] + public bool WriteNullable(in TTarget? value, TypeRegistration? registrationHint = null) + where TTarget : struct + { + return WriteNullable(in value, ObjectMetaOption.ReferenceMeta | ObjectMetaOption.TypeMeta, registrationHint); + } + + [MustUseReturnValue] + internal bool WriteNullable(in TTarget? value, ObjectMetaOption metaOption, TypeRegistration? registrationHint = null) + where TTarget : struct + { + _frameStack.MoveNext(); + var currentFrame = _frameStack.CurrentFrame; + var needWriteMeta = _frameStack.IsCurrentTheLastFrame; + var isSuccess = false; + try + { + var writerRef = ByrefWriter; + if (needWriteMeta) + { + isSuccess = WriteMeta(currentFrame, ref writerRef, in value, metaOption, registrationHint, out var needWriteValue); + if (!isSuccess) + { + return false; + } + if (!needWriteValue) + { + return true; + } + } +#if NET7_0_OR_GREATER + ref readonly var valueRef = ref Nullable.GetValueRefOrDefaultRef(in value); + isSuccess = WriteValue(currentFrame, in valueRef); +#else + isSuccess = WriteValue(currentFrame, value!.Value); +#endif + } + finally + { + OnCurrentSerializationCompleted(isSuccess); + } + return isSuccess; + } + + private bool WriteMeta( + Frame currentFrame, + ref SerializationWriterRef writerRef, + in TTarget? value, + ObjectMetaOption metaOption, + TypeRegistration? registrationHint, + out bool needWriteValue + ) + { + if ((metaOption & ObjectMetaOption.ReferenceMeta) != 0) + { + if (!WriteRefMeta(currentFrame, ref writerRef, in value, out needWriteValue)) + { + return false; + } + } + else + { + needWriteValue = true; + } + PopulateTypeRegistrationToCurrentFrame(in value, registrationHint); + if ((metaOption & ObjectMetaOption.TypeMeta) != 0) + { + if (!WriteTypeMeta(ref writerRef)) + { + return false; + } + } + + return true; + } + + private bool WriteRefMeta(Frame currentFrame, ref SerializationWriterRef writerRef, in TTarget? value, out bool needWriteValue) + { + var writeMetaSuccess = _referenceMetaSerializer.Write(ref writerRef, in value, out var writtenFlag); + if (!writeMetaSuccess) + { + needWriteValue = false; + return false; + } + + needWriteValue = writtenFlag is RefFlag.RefValue or RefFlag.NotNullValue; + currentFrame.NeedNotifyWriteValueCompleted = writtenFlag is RefFlag.RefValue; + return true; + } + + private void PopulateTypeRegistrationToCurrentFrame(in TTarget value, TypeRegistration? registrationHint) + { + var currentFrame = _frameStack.CurrentFrame; + if (currentFrame.Registration is null) + { + var desiredType = value!.GetType(); + if (registrationHint?.TargetType != desiredType) + { + Debug.WriteLine("Type registration hint does not match the actual type."); + registrationHint = null; + } + currentFrame.Registration = registrationHint ?? TypeRegistry.GetTypeRegistration(desiredType); + } + Debug.Assert(currentFrame.Registration.TargetType == value!.GetType()); + } + + private bool WriteTypeMeta(ref SerializationWriterRef writerRef) + { + var currentFrame = _frameStack.CurrentFrame; + Debug.Assert(currentFrame is { Registration: not null }); + return _typeMetaSerializer.Write(ref writerRef, currentFrame.Registration!); + } + + [MustUseReturnValue] + private bool WriteValue(Frame currentFrame, in TTarget value) + { + Debug.Assert(currentFrame.Registration is not null); + currentFrame.Serializer ??= currentFrame.Registration!.RentSerializer(); + + bool success; + + var serializer = currentFrame.Serializer; + if (serializer is ISerializer typedSerializer) + { + success = typedSerializer.Serialize(this, in value); + } + else + { + success = serializer.Serialize(this, value!); + } + + if (success && currentFrame.NeedNotifyWriteValueCompleted) + { + _referenceMetaSerializer.HandleWriteValueCompleted(in value); + } + + return success; + } + + #region Write Methods + + [MustUseReturnValue] + internal bool WriteUnmanaged(T value) + where T : unmanaged => ByrefWriter.WriteUnmanaged(value); + + /// + [MustUseReturnValue] + public int WriteBytes(scoped ReadOnlySpan bytes) => ByrefWriter.WriteBytes(bytes); + + /// + [MustUseReturnValue] + public bool WriteUInt8(byte value) => ByrefWriter.WriteUInt8(value); + + /// + [MustUseReturnValue] + public bool WriteInt8(sbyte value) => ByrefWriter.WriteInt8(value); + + /// + [MustUseReturnValue] + public bool WriteUInt16(ushort value) => ByrefWriter.WriteUInt16(value); + + /// + [MustUseReturnValue] + public bool WriteInt16(short value) => ByrefWriter.WriteInt16(value); + + /// + [MustUseReturnValue] + public bool WriteUInt32(uint value) => ByrefWriter.WriteUInt32(value); + + /// + [MustUseReturnValue] + public bool WriteInt32(int value) => ByrefWriter.WriteInt32(value); + + /// + [MustUseReturnValue] + public bool WriteInt64(ulong value) => ByrefWriter.WriteInt64(value); + + /// + [MustUseReturnValue] + public bool WriteUInt64(long value) => ByrefWriter.WriteUInt64(value); + + /// + [MustUseReturnValue] + public bool WriteFloat32(float value) => ByrefWriter.WriteFloat32(value); + + /// + [MustUseReturnValue] + public bool WriteFloat64(double value) => ByrefWriter.WriteFloat64(value); + + /// + [MustUseReturnValue] + public bool WriteBool(bool value) => ByrefWriter.WriteBool(value); + + /// + [MustUseReturnValue] + public bool Write7BitEncodedInt32(int value) => ByrefWriter.Write7BitEncodedInt32(value); + + /// + [MustUseReturnValue] + public bool Write7BitEncodedUInt32(uint value) => ByrefWriter.Write7BitEncodedUInt32(value); + + /// + [MustUseReturnValue] + public bool Write7BitEncodedInt64(long value) => ByrefWriter.Write7BitEncodedInt64(value); + + /// + [MustUseReturnValue] + public bool Write7BitEncodedUInt64(ulong value) => ByrefWriter.Write7BitEncodedUInt64(value); + #endregion +} diff --git a/csharp/Fury/Context/SerializationWriterRef.cs b/csharp/Fury/Context/SerializationWriterRef.cs new file mode 100644 index 0000000000..a9d524a177 --- /dev/null +++ b/csharp/Fury/Context/SerializationWriterRef.cs @@ -0,0 +1,402 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Fury.Helpers; +using Fury.Serialization.Meta; +using JetBrains.Annotations; + +namespace Fury.Context; + +/// +/// A struct that provides a fast way to write data to a . +/// It caches the span internally and reduces the potential overhead of virtual calls and type cast from . +/// +public ref struct SerializationWriterRef +{ + private Span _buffer = Span.Empty; + private int Consumed => _batchWriter.Consumed; + private int _version; + + private readonly BatchWriter _batchWriter; + public SerializationWriter InnerWriter { get; } + + public SerializationConfig Config => InnerWriter.Config; + public TypeRegistry TypeRegistry => InnerWriter.TypeRegistry; + + internal SerializationWriterRef(SerializationWriter innerWriter, BatchWriter batchWriter) + { + InnerWriter = innerWriter; + _batchWriter = batchWriter; + _version = _batchWriter.Version; + } + + [MustUseReturnValue] + public bool Write(in TTarget? value, TypeRegistration? registrationHint = null) + { + _version--; // make sure the version is out of date + return InnerWriter.Write(in value, registrationHint); + } + + [MustUseReturnValue] + public bool Write(in TTarget? value, TypeRegistration? registrationHint = null) + where TTarget : struct + { + _version--; // make sure the version is out of date + return InnerWriter.WriteNullable(in value, registrationHint); + } + + [MustUseReturnValue] + internal bool Write(in TTarget? value, ObjectMetaOption metaOption, TypeRegistration? registrationHint = null) + { + _version--; // make sure the version is out of date + return InnerWriter.Write(in value, metaOption, registrationHint); + } + + [MustUseReturnValue] + internal bool Write(in TTarget? value, ObjectMetaOption metaOption, TypeRegistration? registrationHint = null) + where TTarget : struct + { + _version--; // make sure the version is out of date + return InnerWriter.WriteNullable(in value, metaOption, registrationHint); + } + + public void Advance(int count) + { + if (_version != _batchWriter.Version) + { + ThrowInvalidOperationException_VersionMismatch(); + } + _batchWriter.Advance(count); + _version = _batchWriter.Version; + } + + [MustUseReturnValue] + public Span GetSpan(int sizeHint = 0) + { + if (_version != _batchWriter.Version) + { + SyncToInnerWriter(); + } + var result = _buffer.Slice(Consumed); + if (result.Length < sizeHint) + { + result = _batchWriter.GetSpan(sizeHint); + SyncToInnerWriter(); + } + + return result; + } + + private void SyncToInnerWriter() + { + _buffer = _batchWriter.Buffer.Span; + _version = _batchWriter.Version; + } + + [DoesNotReturn] + private static void ThrowInvalidOperationException_VersionMismatch() + { + throw new InvalidOperationException( + $"The {nameof(SerializationWriterRef)} is out of date. Call {nameof(GetSpan)} again to write data." + ); + } + + #region Write Methods + + /// + /// Writes the given bytes to the writer. + /// + /// + /// The byte span to write. + /// + /// + /// The number of bytes written. + /// + [MustUseReturnValue] + public int WriteBytes(scoped ReadOnlySpan bytes) + { + var destination = GetSpan(bytes.Length); + var consumed = bytes.CopyUpTo(destination); + Advance(consumed); + return consumed; + } + + [MustUseReturnValue] + internal bool WriteUnmanaged(T value) + where T : unmanaged + { + var size = Unsafe.SizeOf(); + var buffer = GetSpan(size); + if (buffer.Length < size) + { + return false; + } +#if NET8_0_OR_GREATER + MemoryMarshal.Write(buffer, in value); +#else + MemoryMarshal.Write(buffer, ref value); +#endif + Advance(size); + + return true; + } + + [MustUseReturnValue] + public bool WriteUInt8(byte value) => WriteUnmanaged(value); + + [MustUseReturnValue] + public bool WriteInt8(sbyte value) => WriteUnmanaged(value); + + [MustUseReturnValue] + public bool WriteUInt16(ushort value) => WriteUnmanaged(value); + + [MustUseReturnValue] + public bool WriteInt16(short value) => WriteUnmanaged(value); + + [MustUseReturnValue] + public bool WriteUInt32(uint value) => WriteUnmanaged(value); + + [MustUseReturnValue] + public bool WriteInt32(int value) => WriteUnmanaged(value); + + [MustUseReturnValue] + public bool WriteInt64(ulong value) => WriteUnmanaged(value); + + [MustUseReturnValue] + public bool WriteUInt64(long value) => WriteUnmanaged(value); + + [MustUseReturnValue] + public bool WriteFloat32(float value) => WriteUnmanaged(value); + + [MustUseReturnValue] + public bool WriteFloat64(double value) => WriteUnmanaged(value); + + [MustUseReturnValue] + public bool WriteBool(bool value) => WriteUnmanaged(value); + + private bool TryGetSpan(int sizeHint, out Span span) + { + span = GetSpan(sizeHint); + return span.Length >= sizeHint; + } + + [MustUseReturnValue] + public bool Write7BitEncodedInt32(int value) + { + var zigzag = BitOperations.RotateLeft((uint)value, 1); + return Write7BitEncodedUInt32(zigzag); + } + + [MustUseReturnValue] + public bool Write7BitEncodedUInt32(uint value) + { + Span buffer; + switch (value) + { + case < 1u << 7: + return WriteUInt8((byte)value); + case < 1u << 14: + { + const int size = 2; + if (!TryGetSpan(size, out buffer)) + { + return false; + } + buffer[0] = (byte)(value | ~0x7Fu); + buffer[1] = (byte)(value >>> 7); + Advance(size); + return true; + } + case < 1u << 21: + { + const int size = 3; + if (!TryGetSpan(size, out buffer)) + { + return false; + } + buffer[0] = (byte)(value | ~0x7Fu); + buffer[1] = (byte)((value >>> 7) | ~0x7Fu); + buffer[2] = (byte)(value >>> 14); + Advance(size); + return true; + } + case < 1u << 28: + { + const int size = 4; + if (!TryGetSpan(size, out buffer)) + { + return false; + } + buffer[0] = (byte)(value | ~0x7Fu); + buffer[1] = (byte)((value >>> 7) | ~0x7Fu); + buffer[2] = (byte)((value >>> 14) | ~0x7Fu); + buffer[3] = (byte)(value >>> 21); + Advance(size); + return true; + } + default: + { + const int size = 5; + if (!TryGetSpan(size, out buffer)) + { + return false; + } + buffer[0] = (byte)(value | ~0x7Fu); + buffer[1] = (byte)((value >>> 7) | ~0x7Fu); + buffer[2] = (byte)((value >>> 14) | ~0x7Fu); + buffer[3] = (byte)((value >>> 21) | ~0x7Fu); + buffer[4] = (byte)(value >>> 28); + Advance(size); + return true; + } + } + } + + [MustUseReturnValue] + public bool Write7BitEncodedInt64(long value) + { + var zigzag = BitOperations.RotateLeft((ulong)value, 1); + return Write7BitEncodedUInt64(zigzag); + } + + [MustUseReturnValue] + public bool Write7BitEncodedUInt64(ulong value) + { + switch (value) + { + case < 1ul << 7: + return WriteUInt8((byte)value); + case < 1ul << 14: + { + const int size = 2; + if (!TryGetSpan(size, out var buffer)) + { + return false; + } + buffer[0] = (byte)(value | ~0x7Fu); + buffer[1] = (byte)(value >>> 7); + Advance(size); + return true; + } + case < 1ul << 21: + { + const int size = 3; + if (!TryGetSpan(size, out var buffer)) + { + return false; + } + buffer[0] = (byte)(value | ~0x7Fu); + buffer[1] = (byte)((value >>> 7) | ~0x7Fu); + buffer[2] = (byte)(value >>> 14); + Advance(size); + return true; + } + case < 1ul << 28: + { + const int size = 4; + if (!TryGetSpan(size, out var buffer)) + { + return false; + } + buffer[0] = (byte)(value | ~0x7Fu); + buffer[1] = (byte)((value >>> 7) | ~0x7Fu); + buffer[2] = (byte)((value >>> 14) | ~0x7Fu); + buffer[3] = (byte)(value >>> 21); + Advance(size); + return true; + } + case < 1ul << 35: + { + const int size = 5; + if (!TryGetSpan(size, out var buffer)) + { + return false; + } + buffer[0] = (byte)(value | ~0x7Fu); + buffer[1] = (byte)((value >>> 7) | ~0x7Fu); + buffer[2] = (byte)((value >>> 14) | ~0x7Fu); + buffer[3] = (byte)((value >>> 21) | ~0x7Fu); + buffer[4] = (byte)(value >>> 28); + Advance(size); + return true; + } + case < 1ul << 42: + { + const int size = 6; + if (!TryGetSpan(size, out var buffer)) + { + return false; + } + buffer[0] = (byte)(value | ~0x7Fu); + buffer[1] = (byte)((value >>> 7) | ~0x7Fu); + buffer[2] = (byte)((value >>> 14) | ~0x7Fu); + buffer[3] = (byte)((value >>> 21) | ~0x7Fu); + buffer[4] = (byte)((value >>> 28) | ~0x7Fu); + buffer[5] = (byte)(value >>> 35); + Advance(size); + return true; + } + case < 1ul << 49: + { + const int size = 7; + if (!TryGetSpan(size, out var buffer)) + { + return false; + } + buffer[0] = (byte)(value | ~0x7Fu); + buffer[1] = (byte)((value >>> 7) | ~0x7Fu); + buffer[2] = (byte)((value >>> 14) | ~0x7Fu); + buffer[3] = (byte)((value >>> 21) | ~0x7Fu); + buffer[4] = (byte)((value >>> 28) | ~0x7Fu); + buffer[5] = (byte)((value >>> 35) | ~0x7Fu); + buffer[6] = (byte)(value >>> 42); + Advance(size); + return true; + } + case < 1ul << 56: + { + const int size = 8; + if (!TryGetSpan(size, out var buffer)) + { + return false; + } + buffer[0] = (byte)(value | ~0x7Fu); + buffer[1] = (byte)((value >>> 7) | ~0x7Fu); + buffer[2] = (byte)((value >>> 14) | ~0x7Fu); + buffer[3] = (byte)((value >>> 21) | ~0x7Fu); + buffer[4] = (byte)((value >>> 28) | ~0x7Fu); + buffer[5] = (byte)((value >>> 35) | ~0x7Fu); + buffer[6] = (byte)((value >>> 42) | ~0x7Fu); + buffer[7] = (byte)(value >>> 49); + Advance(size); + return true; + } + case < 1ul << 63: + { + const int size = 9; + if (!TryGetSpan(size, out var buffer)) + { + return false; + } + buffer[0] = (byte)(value | ~0x7Fu); + buffer[1] = (byte)((value >>> 7) | ~0x7Fu); + buffer[2] = (byte)((value >>> 14) | ~0x7Fu); + buffer[3] = (byte)((value >>> 21) | ~0x7Fu); + buffer[4] = (byte)((value >>> 28) | ~0x7Fu); + buffer[5] = (byte)((value >>> 35) | ~0x7Fu); + buffer[6] = (byte)((value >>> 42) | ~0x7Fu); + buffer[7] = (byte)((value >>> 49) | ~0x7Fu); + buffer[8] = (byte)(value >>> 56); + Advance(size); + return true; + } + default: + ThrowHelper.ThrowUnreachableException(); + return false; + } + } + + #endregion +} diff --git a/csharp/Fury/Context/TypeRegistration.cs b/csharp/Fury/Context/TypeRegistration.cs new file mode 100644 index 0000000000..ac6e86eeab --- /dev/null +++ b/csharp/Fury/Context/TypeRegistration.cs @@ -0,0 +1,117 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Fury.Buffers; +using Fury.Meta; +using Fury.Serialization; + +namespace Fury.Context; + +public sealed class TypeRegistration +{ + private readonly ObjectPool? _serializerPool; + private readonly ObjectPool? _deserializerPool; + + public Func? SerializerFactory { get; } + + public Func? DeserializerFactory { get; } + + internal MetaString? NamespaceMetaString { get; } + internal MetaString? NameMetaString { get; } + + public string? Namespace => NamespaceMetaString?.Value; + public string? Name => NameMetaString?.Value; + + public Type TargetType { get; } + public TypeKind? TypeKind { get; } + public int? Id { get; } + internal InternalTypeKind InternalTypeKind { get; } + + internal TypeRegistration( + Type targetType, + InternalTypeKind internalTypeKind, + MetaString? ns, + MetaString? name, + int? id, + Func? serializerFactory, + Func? deserializerFactory + ) + { + TargetType = targetType; + + SerializerFactory = serializerFactory; + DeserializerFactory = deserializerFactory; + if (serializerFactory is not null) + { + _serializerPool = new ObjectPool(serializerFactory); + } + + if (deserializerFactory is not null) + { + _deserializerPool = new ObjectPool(deserializerFactory); + } + + NamespaceMetaString = ns; + NameMetaString = name; + + Id = id; + + InternalTypeKind = internalTypeKind; + if (internalTypeKind.TryToBePublic(out var typeKind)) + { + TypeKind = typeKind; + } + else + { + TypeKind = null; + } + } + + internal ISerializer RentSerializer() + { + if (_serializerPool is null) + { + ThrowInvalidOperationException_NoSerializerPool(); + } + return _serializerPool!.Rent(); + } + + internal void ReturnSerializer(ISerializer serializer) + { + if (_serializerPool is null) + { + ThrowInvalidOperationException_NoSerializerPool(); + } + _serializerPool!.Return(serializer); + } + + internal IDeserializer RentDeserializer() + { + if (_deserializerPool is null) + { + ThrowInvalidOperationException_NoDeserializerPool(); + } + return _deserializerPool!.Rent(); + } + + internal void ReturnDeserializer(IDeserializer deserializer) + { + if (_deserializerPool is null) + { + ThrowInvalidOperationException_NoDeserializerPool(); + } + _deserializerPool!.Return(deserializer); + } + + [DoesNotReturn] + private void ThrowInvalidOperationException_NoSerializerPool() + { + throw new InvalidOperationException($"Can not get serializer for type '{TargetType}'."); + } + + [DoesNotReturn] + private void ThrowInvalidOperationException_NoDeserializerPool() + { + throw new InvalidOperationException($"Can not get deserializer for type '{TargetType}'."); + } +} diff --git a/csharp/Fury/Context/TypeRegistry.cs b/csharp/Fury/Context/TypeRegistry.cs new file mode 100644 index 0000000000..447abd0975 --- /dev/null +++ b/csharp/Fury/Context/TypeRegistry.cs @@ -0,0 +1,472 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Fury.Collections; +using Fury.Helpers; +using Fury.Meta; +using Fury.Serialization; +using JetBrains.Annotations; + +namespace Fury.Context; + +internal readonly struct TypeRegistrationCreateInfo(Type targetType) +{ + public Type TargetType { get; } = targetType; + public string? Namespace { get; init; } = null; + public string? Name { get; init; } = null; + public InternalTypeKind? TypeKind { get; init; } + public int? Id { get; init; } + public Func? SerializerFactory { get; init; } + public Func? DeserializerFactory { get; init; } + + internal bool CustomSerialization { get; init; } = false; +} + +[MustDisposeResource] +public sealed class TypeRegistry : IDisposable +{ + private readonly TimeSpan _timeout; + + private readonly MetaStringStorage _metaStringStorage; + + private readonly Dictionary _typeToRegistrations = new(); + private readonly Dictionary<(TypeKind TypeKind, Type DeclaredType), TypeRegistration> _declaredTypeToRegistrations = new(); + private readonly Dictionary<(string? Namespace, string Name), TypeRegistration> _nameToRegistrations = new(); + private readonly Dictionary _idToRegistrations = new(); + private int _idGenerator; + + private readonly ReaderWriterLockSlim _registrationLock = new(LockRecursionPolicy.SupportsRecursion); + private readonly ReaderWriterLockSlim _declaredTypeLock = new(LockRecursionPolicy.SupportsRecursion); + + private readonly ITypeRegistrationProvider _registrationProvider; + + internal TypeRegistry(MetaStringStorage metaStringStorage, ITypeRegistrationProvider provider, TimeSpan timeout) + { + _metaStringStorage = metaStringStorage; + _registrationProvider = provider; + _timeout = timeout; + + Initialize(); + } + + public void Dispose() + { + _registrationLock.Dispose(); + _declaredTypeLock.Dispose(); + } + + private void Initialize() + { + RegisterPrimitive(InternalTypeKind.Bool, TypeKind.BoolArray); + RegisterPrimitive(InternalTypeKind.Int8, TypeKind.Int8Array); + RegisterPrimitive(InternalTypeKind.Int8, TypeKind.Int8Array); + RegisterPrimitive(InternalTypeKind.Int16, TypeKind.Int16Array); + RegisterPrimitive(InternalTypeKind.Int16, TypeKind.Int16Array); + RegisterPrimitive(InternalTypeKind.Int32, TypeKind.Int32Array); + RegisterPrimitive(InternalTypeKind.Int32, TypeKind.Int32Array); + RegisterPrimitive(InternalTypeKind.Int64, TypeKind.Int64Array); + RegisterPrimitive(InternalTypeKind.Int64, TypeKind.Int64Array); +#if NET5_0_OR_GREATER + // Technically, this is not a primitive type, but we register it here for convenience. + RegisterPrimitive(InternalTypeKind.Float16, TypeKind.Float16Array); +#endif + RegisterPrimitive(InternalTypeKind.Float32, TypeKind.Float32Array); + RegisterPrimitive(InternalTypeKind.Float64, TypeKind.Float64Array); + + RegisterGeneral(InternalTypeKind.String, () => new StringSerializer(), () => new StringDeserializer()); + RegisterGeneral(InternalTypeKind.Duration, () => new StandardTimeSpanSerializer(), () => new StandardTimeSpanDeserializer()); +#if NET6_0_OR_GREATER + RegisterGeneral(InternalTypeKind.LocalDate, () => new StandardDateOnlySerializer(), () => new StandardDateOnlyDeserializer()); +#endif + RegisterGeneral(InternalTypeKind.Timestamp, () => StandardDateTimeSerializer.Instance, () => StandardDateTimeDeserializer.Instance); + return; + + void RegisterPrimitive(InternalTypeKind typeKind, TypeKind arrayTypeKind) + where T : unmanaged + { + var createInfo = new TypeRegistrationCreateInfo(typeof(T)) + { + TypeKind = typeKind, + SerializerFactory = () => PrimitiveSerializer.Instance, + DeserializerFactory = () => PrimitiveDeserializer.Instance, + }; + var registration = Register(createInfo); + + Register(typeof(T[]), arrayTypeKind, () => new PrimitiveArraySerializer(), () => new PrimitiveArrayDeserializer()); +#if NET8_0_OR_GREATER + Register(typeof(List), TypeKind.List, () => new PrimitiveListSerializer(registration), () => new PrimitiveListDeserializer(registration)); +#else + Register(typeof(List), TypeKind.List, () => new ListSerializer(registration), () => new ListDeserializer(registration)); +#endif + RegisterCollections(registration); + } + + void RegisterGeneral(InternalTypeKind typeKind, Func serializerFactory, Func deserializerFactory) + { + var createInfo = new TypeRegistrationCreateInfo(typeof(T)) + { + TypeKind = typeKind, + SerializerFactory = serializerFactory, + DeserializerFactory = deserializerFactory, + }; + var registration = Register(createInfo); + + Register(typeof(T[]), TypeKind.List, () => new ArraySerializer(registration), () => new ArrayDeserializer(registration)); + Register(typeof(List), TypeKind.List, () => new ListSerializer(registration), () => new ListDeserializer(registration)); + RegisterCollections(registration); + } + + void RegisterCollections(TypeRegistration elementRegistration) + { + Register( + typeof(HashSet), + TypeKind.Set, + () => new HashSetSerializer(elementRegistration), + () => new HashSetDeserializer(elementRegistration) + ); + } + } + + #region Public Register Methods + + public TypeRegistration Register(Type targetType, Func? serializerFactory, Func? deserializerFactory) + { + // We need lock here to ensure that the auto-generated id is unique. + if (!_registrationLock.TryEnterReadLock(_timeout)) + { + ThrowTimeoutException_RegisterTypeTimeout(); + } + + try + { + while (_idToRegistrations.ContainsKey(_idGenerator)) + { + _idGenerator++; + } + var createInfo = new TypeRegistrationCreateInfo(targetType) + { + Id = _idGenerator++, + SerializerFactory = serializerFactory, + DeserializerFactory = deserializerFactory, + }; + return Register(createInfo); + } + finally + { + _registrationLock.ExitReadLock(); + } + } + + public TypeRegistration Register( + Type targetType, + string? @namespace, + string name, + Func serializerFactory, + Func deserializerFactory + ) + { + var createInfo = new TypeRegistrationCreateInfo(targetType) + { + Namespace = @namespace, + Name = name, + SerializerFactory = serializerFactory, + DeserializerFactory = deserializerFactory, + }; + return Register(createInfo); + } + + public TypeRegistration Register(Type targetType, TypeKind targetTypeKind, Func serializerFactory, Func deserializerFactory) + { + var createInfo = new TypeRegistrationCreateInfo(targetType) + { + TypeKind = targetTypeKind.ToInternal(), + SerializerFactory = serializerFactory, + DeserializerFactory = deserializerFactory, + }; + return Register(createInfo); + } + + public TypeRegistration Register(Type targetType, int id, Func serializerFactory, Func deserializerFactory) + { + var createInfo = new TypeRegistrationCreateInfo(targetType) + { + Id = id, + SerializerFactory = serializerFactory, + DeserializerFactory = deserializerFactory, + }; + return Register(createInfo); + } + + public void Register(Type declaredType, TypeRegistration registration) + { + if (!_declaredTypeLock.TryEnterWriteLock(_timeout)) + { + ThrowTimeoutException_RegisterTypeTimeout(); + } + + try + { + if (registration.TypeKind is not { } typeKind) + { + ThrowArgumentException_NoTypeKindRegistered(nameof(registration), registration); + return; + } + if (_declaredTypeToRegistrations.TryGetValue((typeKind, declaredType), out var existingRegistration)) + { + ThrowArgumentException_DuplicateTypeKindDeclaredType($"{nameof(declaredType)}, {nameof(registration)}", declaredType, existingRegistration); + } + + _declaredTypeToRegistrations.Add((typeKind, declaredType), registration); + } + finally + { + _declaredTypeLock.ExitWriteLock(); + } + } + + #endregion + + private TypeRegistration Register(TypeRegistrationCreateInfo createInfo) + { + if (!_registrationLock.TryEnterWriteLock(_timeout)) + { + ThrowTimeoutException_RegisterTypeTimeout(); + } + + try + { + var registration = _typeToRegistrations.GetOrAdd( + createInfo.TargetType, + static (_, tuple) => tuple.Register.CreateTypeRegistration(tuple.CreateInfo), + (Register: this, CreateInfo: createInfo), + out var exists + ); + + if (exists) + { + ThrowInvalidOperationException_DuplicateRegistration(registration); + } + + if (createInfo.TypeKind is not null) + { + Register(registration.TargetType, registration); + } + + if (createInfo.Id is { } id) + { + var registered = _idToRegistrations.GetOrAdd(id, registration, out exists); + if (exists) + { + Debug.Assert(registered.Id == createInfo.Id); + _typeToRegistrations.Remove(createInfo.TargetType); + ThrowInvalidOperationException_DuplicateTypeId(registered); + } + } + if (createInfo.Name is not null) + { + var registered = _nameToRegistrations.GetOrAdd((createInfo.Namespace, createInfo.Name), registration, out exists); + if (exists) + { + Debug.Assert(registered.Name == createInfo.Name); + Debug.Assert(registered.Namespace == createInfo.Namespace); + _typeToRegistrations.Remove(createInfo.TargetType); + ThrowInvalidOperationException_DuplicateTypeName(registered); + } + } + + return registration; + } + finally + { + _registrationLock.ExitWriteLock(); + } + } + + private TypeRegistration CreateTypeRegistration(in TypeRegistrationCreateInfo createInfo) + { + var targetType = createInfo.TargetType; + var serializerFactory = createInfo.SerializerFactory; + var deserializerFactory = createInfo.DeserializerFactory; + + if (serializerFactory is null && deserializerFactory is null) + { + ThrowInvalidOperationException_NoSerializationProviderSupport(targetType); + } + + MetaString? namespaceMetaString = null; + MetaString? nameMetaString = null; + if (createInfo.Namespace is { } ns) + { + namespaceMetaString = _metaStringStorage.GetMetaString(ns, MetaStringStorage.EncodingPolicy.Namespace); + } + + var isNamed = false; + if (createInfo.Name is { } name) + { + nameMetaString = _metaStringStorage.GetMetaString(name, MetaStringStorage.EncodingPolicy.Name); + isNamed = true; + } + if (createInfo.TypeKind is not { } typeKind) + { + // Other prefixes, such as "Polymorphic" or "Compatible", depend on configuration and object being serialized. + // We can't determine them here, so we'll just use the "Struct" and "Ext" and handle them in the serialization code. + if (targetType.IsEnum) + { + typeKind = isNamed ? InternalTypeKind.NamedEnum : InternalTypeKind.Enum; + } + else if (createInfo.CustomSerialization) + { + typeKind = isNamed ? InternalTypeKind.NamedExt : InternalTypeKind.Ext; + } + else + { + typeKind = isNamed ? InternalTypeKind.NamedStruct : InternalTypeKind.Struct; + } + } + var newRegistration = new TypeRegistration( + targetType, + typeKind, + namespaceMetaString, + nameMetaString, + createInfo.Id, + serializerFactory, + deserializerFactory + ); + + return newRegistration; + } + + private static void ThrowInvalidOperationException_NoSerializationProviderSupport(Type targetType) + { + throw new InvalidOperationException($"Type `{targetType}` is not supported by either built-in or custom serialization provider."); + } + + public TypeRegistration GetTypeRegistration(Type type) + { + if (!_registrationLock.TryEnterUpgradeableReadLock(_timeout)) + { + ThrowTimeoutException_RegisterTypeTimeout(); + } + + try + { + if (!_typeToRegistrations.TryGetValue(type, out var registration)) + { + registration = _registrationProvider.RegisterType(this, type); + } + return registration; + } + finally + { + _registrationLock.ExitUpgradeableReadLock(); + } + } + + public TypeRegistration GetTypeRegistration(string ns, string name) + { + if (!_registrationLock.TryEnterUpgradeableReadLock(_timeout)) + { + ThrowTimeoutException_RegisterTypeTimeout(); + } + + try + { + if (!_nameToRegistrations.TryGetValue((ns, name), out var registration)) + { + registration = _registrationProvider.GetTypeRegistration(this, ns, name); + } + return registration; + } + finally + { + _registrationLock.ExitUpgradeableReadLock(); + } + } + + public TypeRegistration GetTypeRegistration(TypeKind typeKind, Type declaredType) + { + if (!_registrationLock.TryEnterUpgradeableReadLock(_timeout)) + { + ThrowTimeoutException_RegisterTypeTimeout(); + } + + try + { + if (!_declaredTypeToRegistrations.TryGetValue((typeKind, declaredType), out var registration)) + { + registration = _registrationProvider.GetTypeRegistration(this, typeKind, declaredType); + } + return registration; + } + finally + { + _registrationLock.ExitUpgradeableReadLock(); + } + } + + public TypeRegistration GetTypeRegistration(int id) + { + if (!_registrationLock.TryEnterUpgradeableReadLock(_timeout)) + { + ThrowTimeoutException_RegisterTypeTimeout(); + } + + try + { + if (!_idToRegistrations.TryGetValue(id, out var registration)) + { + registration = _registrationProvider.GetTypeRegistration(this, id); + } + + return registration; + } + finally + { + _registrationLock.ExitUpgradeableReadLock(); + } + } + + [DoesNotReturn] + private static void ThrowTimeoutException_RegisterTypeTimeout() + { + throw new TimeoutException("It took too long to register the type."); + } + + [DoesNotReturn] + private static void ThrowInvalidOperationException_DuplicateRegistration(TypeRegistration registration) + { + throw new InvalidOperationException($"Type `{registration.TargetType}` is already registered."); + } + + [DoesNotReturn] + private static void ThrowInvalidOperationException_DuplicateTypeName(TypeRegistration registration) + { + var fullName = StringHelper.ToFullName(registration.Namespace, registration.Name); + throw new InvalidOperationException($"Type name `{fullName}` is already registered for type `{registration.TargetType}`."); + } + + [DoesNotReturn] + private static void ThrowInvalidOperationException_DuplicateTypeId(TypeRegistration existent) + { + throw new InvalidOperationException($"Type id `{existent.Id}` is already registered for type `{existent.TargetType}`."); + } + + [DoesNotReturn] + private static void ThrowArgumentException_DuplicateTypeKindDeclaredType( + [InvokerParameterName] string parameterName, + Type declaredType, + TypeRegistration registration + ) + { + var typeKind = registration.TypeKind; + throw new ArgumentException($"Declared type `{declaredType}` and type kind `{typeKind}` are already registered.", parameterName); + } + + [DoesNotReturn] + private static void ThrowArgumentException_NoTypeKindRegistered([InvokerParameterName] string parameterName, TypeRegistration registration) + { + throw new ArgumentException($"Type `{registration.TargetType}` was not registered with a {nameof(TypeKind)}", parameterName); + } +} diff --git a/csharp/Fury/Development/Macros.cs b/csharp/Fury/Development/Macros.cs new file mode 100644 index 0000000000..ee7b748b95 --- /dev/null +++ b/csharp/Fury/Development/Macros.cs @@ -0,0 +1,53 @@ +#if DEBUG +using System.Collections.Generic; +using Fury.Context; +using JetBrains.Annotations; + +namespace Fury; + +/// +/// Macros for rider templates. +/// +internal static class Macros +{ + // These code are used in development time to generate similar code. + // They should not be depended on at runtime. + + [UsedImplicitly] + [SourceTemplate, Macro(Target = nameof(TTarget), Expression = "guessExpectedType()")] + internal static void GetRegistrationIfPossible(this TypeRegistration? registration) + { + /*$ + if ($registration$ is null && TypeHelper<$TTarget$>.IsSealed) + { + $registration$ = context.TypeRegistry.GetOrRegisterType(typeof($TTarget$)); + } + */ + } + + [UsedImplicitly] + [SourceTemplate] + internal static void GetValueRefOrAddDefault( + this IDictionary dictionary, + TKey key, + TValue value, + TValue newValue + ) + { + /*$ +#if NET8_0_OR_GREATER + ref var $value$ = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, $key$, out var exists); +#else + var exists = dictionary.TryGetValue($key$, out var $value$); +#endif + if (!exists) + { + $value$ = $newValue$; +#if !NET8_0_OR_GREATER + dictionary.Add($key$, $value$); +#endif + } + */ + } +} +#endif diff --git a/csharp/Fury/Exceptions/Backports/UnreachableException.cs b/csharp/Fury/Exceptions/Backports/UnreachableException.cs new file mode 100644 index 0000000000..00238bf8b7 --- /dev/null +++ b/csharp/Fury/Exceptions/Backports/UnreachableException.cs @@ -0,0 +1,28 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +#if !NET8_0_OR_GREATER +// ReSharper disable once CheckNamespace +namespace System.Diagnostics +{ + internal sealed class UnreachableException(string? message = null) : Exception(message); +} +#endif + +namespace Fury +{ + internal static partial class ThrowHelper + { + [DoesNotReturn] + public static void ThrowUnreachableException(string? message = null) + { + throw new UnreachableException(message); + } + + [DoesNotReturn] + public static TReturn ThrowUnreachableException(string? message = null) + { + throw new UnreachableException(message); + } + } +} diff --git a/csharp/Fury/Exceptions/BadDeserializationInputException.cs b/csharp/Fury/Exceptions/BadDeserializationInputException.cs new file mode 100644 index 0000000000..c64a903e18 --- /dev/null +++ b/csharp/Fury/Exceptions/BadDeserializationInputException.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Fury.Meta; + +namespace Fury; + +public class BadDeserializationInputException(string? message = null, Exception? innerException = null) : Exception(message, innerException); + +internal static partial class ThrowHelper +{ + [DoesNotReturn] + public static void ThrowBadDeserializationInputException_UnrecognizedMetaStringCodePoint(byte codePoint) + { + throw new BadDeserializationInputException($"Unrecognized MetaString code point: {codePoint}"); + } + + [DoesNotReturn] + public static void ThrowBadDeserializationInputException_UpperCaseFlagCannotAppearConsecutively() + { + throw new BadDeserializationInputException( + $"The '{AllToLowerSpecialEncoding.UpperCaseFlag}' cannot appear consecutively" + ); + } + + [DoesNotReturn] + public static void ThrowBadDeserializationInputException_UnknownMetaStringId(int id) + { + throw new BadDeserializationInputException($"Unknown MetaString ID: {id}"); + } + + [DoesNotReturn] + public static void ThrowBadDeserializationInputException_InvalidMagicNumber() + { + throw new BadDeserializationInputException("Invalid magic number."); + } + + [DoesNotReturn] + public static void ThrowBadDeserializationInputException_NotCrossLanguage() + { + throw new BadDeserializationInputException("Not cross language."); + } + + [DoesNotReturn] + public static void ThrowBadDeserializationInputException_NotLittleEndian() + { + throw new BadDeserializationInputException("Not little endian."); + } + + [DoesNotReturn] + public static void ThrowBadDeserializationInputException_BadMetaStringHashCodeOrBytes() + { + throw new BadDeserializationInputException("The bytes of meta string do not match the prefixed hash code."); + } +} diff --git a/csharp/Fury/Exceptions/BadSerializationInputException.cs b/csharp/Fury/Exceptions/BadSerializationInputException.cs new file mode 100644 index 0000000000..1971b13e6f --- /dev/null +++ b/csharp/Fury/Exceptions/BadSerializationInputException.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Fury; + +public sealed class BadSerializationInputException(string? message = null) : Exception(message); + +internal static partial class ThrowHelper +{ + [DoesNotReturn] + public static void ThrowBadSerializationInputException(string? message = null) + { + throw new BadSerializationInputException(message); + } + + [DoesNotReturn] + public static TReturn ThrowBadSerializationInputException(string? message = null) + { + throw new BadSerializationInputException(message); + } + + [DoesNotReturn] + public static void ThrowBadSerializationInputException_UnsupportedMetaStringChar(char c) + { + throw new BadSerializationInputException($"Unsupported MetaString character: '{c}'"); + } + + [DoesNotReturn] + public static void ThrowBadSerializationInputException_UnregisteredType(Type type) + { + throw new BadSerializationInputException($"Type '{type.FullName}' is not registered."); + } + + [DoesNotReturn] + public static void ThrowBadSerializationInputException_CircularDependencyDetected() + { + throw new BadSerializationInputException("Circular dependency detected."); + } + + [DoesNotReturn] + public static void ThrowBadSerializationInputException_NoSerializerFactoryProvider(Type targetType) + { + throw new BadSerializationInputException( + $"Can not find an appropriate serializer factory provider for type '{targetType.FullName}'." + ); + } + + [DoesNotReturn] + public static void ThrowBadSerializationInputException_AttemptedToSerializeNullValueWhenResuming() + { + throw new BadSerializationInputException("Attempted to serialize a null value when resuming."); + } + + [DoesNotReturn] + public static void ThrowBadSerializationInputException_ObjectWithBuiltInSerializerAndCustomDeserializer( + Type targetType + ) + { + throw new BadSerializationInputException( + "Attempted to serialize an object of type '{targetType.FullName}' with a built-in serializer and a custom deserializer." + ); + } +} diff --git a/csharp/Fury/Exceptions/CircularDependencyException.cs b/csharp/Fury/Exceptions/CircularDependencyException.cs new file mode 100644 index 0000000000..9e95f8fbc9 --- /dev/null +++ b/csharp/Fury/Exceptions/CircularDependencyException.cs @@ -0,0 +1,13 @@ +using System; + +namespace Fury; + +public class CircularDependencyException(string? message = null) : Exception(message); + +internal static partial class ThrowHelper +{ + public static void ThrowCircularDependencyException(string? message = null) + { + throw new CircularDependencyException(message); + } +} diff --git a/csharp/Fury/Exceptions/FailedToResumeException.cs b/csharp/Fury/Exceptions/FailedToResumeException.cs new file mode 100644 index 0000000000..d59828a8d0 --- /dev/null +++ b/csharp/Fury/Exceptions/FailedToResumeException.cs @@ -0,0 +1,16 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Fury; + +public sealed class FailedToResumeException(string? message = null, Exception? innerException = null) + : Exception(message, innerException); + +internal static partial class ThrowHelper +{ + [DoesNotReturn] + public static void ThrowFailedToResumeException_ExceptionWasThrownDuringResuming(Exception innerException) + { + throw new FailedToResumeException("An exception was thrown during resuming.", innerException); + } +} diff --git a/csharp/Fury/Exceptions/ThrowHelper.ArgumentException.cs b/csharp/Fury/Exceptions/ThrowHelper.ArgumentException.cs new file mode 100644 index 0000000000..938ac9a0f3 --- /dev/null +++ b/csharp/Fury/Exceptions/ThrowHelper.ArgumentException.cs @@ -0,0 +1,14 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Fury; + +internal static partial class ThrowHelper +{ + + [DoesNotReturn] + public static void ThrowArgumentException_InsufficientSpaceInTheOutputBuffer(string? paramName = null) + { + throw new ArgumentException("Insufficient space in the output buffer.", paramName); + } +} diff --git a/csharp/Fury/Exceptions/ThrowHelper.ArgumentOutOfRangeException.cs b/csharp/Fury/Exceptions/ThrowHelper.ArgumentOutOfRangeException.cs new file mode 100644 index 0000000000..ae0d8f90b4 --- /dev/null +++ b/csharp/Fury/Exceptions/ThrowHelper.ArgumentOutOfRangeException.cs @@ -0,0 +1,22 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; + +namespace Fury; + +internal static partial class ThrowHelper +{ + + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeException_AttemptedToAdvanceFurtherThanBufferLength( + string paramName, + int bufferLength, + int advanceLength + ) + { + throw new ArgumentOutOfRangeException( + paramName, + $"Attempted to advance further than the buffer length. Buffer length: {bufferLength}, Advance length: {advanceLength}" + ); + } +} diff --git a/csharp/Fury/Exceptions/ThrowHelper.InvalidCastException.cs b/csharp/Fury/Exceptions/ThrowHelper.InvalidCastException.cs new file mode 100644 index 0000000000..e87d1e5794 --- /dev/null +++ b/csharp/Fury/Exceptions/ThrowHelper.InvalidCastException.cs @@ -0,0 +1,13 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Fury; + +internal static partial class ThrowHelper +{ + [DoesNotReturn] + public static void ThrowInvalidCastException_CannotCastFromTo(Type fromType, Type toType) + { + throw new InvalidCastException($"Cannot cast from {fromType} to {toType}."); + } +} diff --git a/csharp/Fury/Exceptions/ThrowHelper.NotSupportedException.cs b/csharp/Fury/Exceptions/ThrowHelper.NotSupportedException.cs new file mode 100644 index 0000000000..f8f3b98ed8 --- /dev/null +++ b/csharp/Fury/Exceptions/ThrowHelper.NotSupportedException.cs @@ -0,0 +1,32 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Fury.Meta; + +namespace Fury; + +internal static partial class ThrowHelper +{ + [DoesNotReturn] + public static void ThrowNotSupportedException(string? message = null) + { + throw new NotSupportedException(message); + } + + [DoesNotReturn] + public static TReturn ThrowNotSupportedException(string? message = null) + { + throw new NotSupportedException(message); + } + + [DoesNotReturn] + public static TReturn ThrowNotSupportedException_EncoderNotSupportedForThisEncoding(string? encodingName) + { + throw new NotSupportedException($"The encoder is not supported for the encoding '{encodingName}'."); + } + + [DoesNotReturn] + public static void ThrowNotSupportedException_SearchTypeByNamespaceAndName() + { + throw new NotSupportedException("Searching for types by namespace and name is not supported yet."); + } +} diff --git a/csharp/Fury/Exceptions/ThrowHelper.OutOfMemoryException.cs b/csharp/Fury/Exceptions/ThrowHelper.OutOfMemoryException.cs new file mode 100644 index 0000000000..a7d57f0083 --- /dev/null +++ b/csharp/Fury/Exceptions/ThrowHelper.OutOfMemoryException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Fury; + +internal static partial class ThrowHelper +{ + public static void ThrowOutOfMemoryException_BufferMaximumSizeExceeded(uint needed) + { + throw new OutOfMemoryException($"Cannot allocate a buffer of size {needed}."); + } +} diff --git a/csharp/Fury/Exceptions/ThrowHelper.cs b/csharp/Fury/Exceptions/ThrowHelper.cs new file mode 100644 index 0000000000..f138a20148 --- /dev/null +++ b/csharp/Fury/Exceptions/ThrowHelper.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; + +namespace Fury; + +internal static partial class ThrowHelper +{ + [DoesNotReturn] + public static void ThrowInvalidOperationException(string? message = null) + { + throw new InvalidOperationException(message); + } + + [DoesNotReturn] + public static void ThrowArgumentException(string? message = null, string? paramName = null) + { + throw new ArgumentException(message, paramName); + } + + [DoesNotReturn] + public static void ThrowArgumentNullException([InvokerParameterName] string? paramName = null) + { + throw new ArgumentNullException(paramName); + } + + public static void ThrowArgumentNullExceptionIfNull(in T value, [InvokerParameterName] string? paramName = null) + { + if (value is null) + { + throw new ArgumentNullException(paramName); + } + } + + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeException( + string paramName, + object? actualValue = null, + string? message = null + ) + { + throw new ArgumentOutOfRangeException(paramName, actualValue, message); + } + + public static void ThrowArgumentOutOfRangeExceptionIfNegative( + int value, + [InvokerParameterName]string paramName, + string? message = null + ) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(paramName, value, message); + } + } + + [DoesNotReturn] + public static void ThrowIndexOutOfRangeException() + { + throw new IndexOutOfRangeException(); + } +} diff --git a/csharp/Fury/Fury.cs b/csharp/Fury/Fury.cs new file mode 100644 index 0000000000..a004bb24aa --- /dev/null +++ b/csharp/Fury/Fury.cs @@ -0,0 +1,426 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Fury.Buffers; +using Fury.Context; +using Fury.Meta; +using Fury.Serialization; +using Fury.Serialization.Meta; +using JetBrains.Annotations; + +namespace Fury; + +[MustDisposeResource] +public sealed class Fury : IDisposable +{ + private const int MaxRetainedPoolSize = 16; + + private readonly MetaStringStorage _metaStringStorage = new(); + private readonly TypeRegistry _typeRegistry; + private readonly ObjectPool _writerPool; + private readonly ObjectPool _readerPool; + private readonly ObjectPool _headerSerializerPool = new( + () => new HeaderSerializer(), + MaxRetainedPoolSize + ); + private readonly ObjectPool _headerDeserializerPool = new( + () => new HeaderDeserializer(), + MaxRetainedPoolSize + ); + + public Fury(FuryConfig config) + { + _typeRegistry = new TypeRegistry(_metaStringStorage, config.RegistrationProvider, config.LockTimeOut); + _writerPool = new ObjectPool( + () => new SerializationWriter(_typeRegistry), + MaxRetainedPoolSize + ); + _readerPool = new ObjectPool( + () => new DeserializationReader(_typeRegistry, _metaStringStorage), + MaxRetainedPoolSize + ); + } + + public void Dispose() + { + _typeRegistry.Dispose(); + _writerPool.Dispose(); + _readerPool.Dispose(); + _headerSerializerPool.Dispose(); + _headerDeserializerPool.Dispose(); + } + + public SerializationResult Serialize( + PipeWriter writer, + in T? value, + SerializationConfig config, + TypeRegistration? registrationHint = null + ) + where T : notnull + { + var serializationWriter = _writerPool.Rent(); + serializationWriter.Initialize(writer, config); + var uncompletedResult = SerializationResult.FromUncompleted(serializationWriter, registrationHint); + return ContinueSerialize(uncompletedResult, in value); + } + + public SerializationResult Serialize( + PipeWriter writer, + in T? value, + SerializationConfig config, + TypeRegistration? registrationHint = null + ) + where T : struct + { + var serializationWriter = _writerPool.Rent(); + serializationWriter.Initialize(writer, config); + var uncompletedResult = SerializationResult.FromUncompleted(serializationWriter, registrationHint); + return ContinueSerialize(uncompletedResult, in value); + } + + // To avoid unnecessary copying, we let the caller provide the value again rather than + // storing it in the SerializationResult. + + public SerializationResult ContinueSerialize(SerializationResult uncompletedResult, in T? value) + where T : notnull + { + if (uncompletedResult.IsCompleted) + { + ThrowInvalidOperationException_SerializationCompleted(); + } + + var completedOrFailed = false; + var writer = uncompletedResult.Writer; + Debug.Assert(writer is not null); + try + { + if (!writer.WriteHeader(value is null)) + { + return uncompletedResult; + } + + if (value is not null && !writer.Write(in value, uncompletedResult.RootTypeRegistrationHint)) + { + return uncompletedResult; + } + + completedOrFailed = true; + return SerializationResult.Completed; + } + catch (Exception) + { + completedOrFailed = true; + throw; + } + finally + { + if (completedOrFailed) + { + writer.Reset(); + _writerPool.Return(writer); + } + } + } + + public SerializationResult ContinueSerialize(SerializationResult uncompletedResult, in T? value) + where T : struct + { + if (uncompletedResult.IsCompleted) + { + ThrowInvalidOperationException_SerializationCompleted(); + } + + var completedOrFailed = false; + var writer = uncompletedResult.Writer; + Debug.Assert(writer is not null); + try + { + if (!writer.WriteHeader(value is null)) + { + return uncompletedResult; + } + + if (value is not null && !writer.Write(value.Value, uncompletedResult.RootTypeRegistrationHint)) + { + return uncompletedResult; + } + + completedOrFailed = true; + return SerializationResult.Completed; + } + catch (Exception) + { + completedOrFailed = true; + throw; + } + finally + { + if (completedOrFailed) + { + writer.Reset(); + _writerPool.Return(writer); + } + } + } + + public DeserializationResult Deserialize( + PipeReader reader, + DeserializationConfig config, + TypeRegistration? registrationHint = null + ) + where T : notnull + { + var serializationReader = _readerPool.Rent(); + serializationReader.Initialize(reader, config); + var uncompletedResult = DeserializationResult.FromUncompleted(serializationReader, registrationHint); + var task = Deserialize(uncompletedResult, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public DeserializationResult DeserializeNullable( + PipeReader reader, + DeserializationConfig config, + TypeRegistration? registrationHint = null + ) + where T : struct + { + var serializationReader = _readerPool.Rent(); + serializationReader.Initialize(reader, config); + var uncompletedResult = DeserializationResult.FromUncompleted(serializationReader, registrationHint); + var task = DeserializeNullable(uncompletedResult, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public ValueTask> DeserializeAsync( + PipeReader reader, + DeserializationConfig config, + TypeRegistration? registrationHint = null, + CancellationToken cancellationToken = default + ) + where T : notnull + { + var serializationReader = _readerPool.Rent(); + serializationReader.Initialize(reader, config); + var uncompletedResult = DeserializationResult.FromUncompleted(serializationReader, registrationHint); + return Deserialize(uncompletedResult, true, cancellationToken); + } + + public ValueTask> DeserializeNullableAsync( + PipeReader reader, + DeserializationConfig config, + TypeRegistration? registrationHint = null, + CancellationToken cancellationToken = default + ) + where T : struct + { + var serializationReader = _readerPool.Rent(); + serializationReader.Initialize(reader, config); + var uncompletedResult = DeserializationResult.FromUncompleted(serializationReader, registrationHint); + return DeserializeNullable(uncompletedResult, true, cancellationToken); + } + + public DeserializationResult ContinueDeserialize(DeserializationResult uncompletedResult) + where T : notnull + { + var task = Deserialize(uncompletedResult, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public DeserializationResult ContinueDeserializeNullable(DeserializationResult uncompletedResult) + where T : struct + { + var task = DeserializeNullable(uncompletedResult, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public ValueTask> ContinueDeserializeAsync( + DeserializationResult uncompletedResult, + CancellationToken cancellationToken + ) + where T : notnull + { + return Deserialize(uncompletedResult, true, cancellationToken); + } + + public ValueTask> ContinueDeserializeNullableAsync( + DeserializationResult uncompletedResult, + CancellationToken cancellationToken + ) + where T : struct + { + return DeserializeNullable(uncompletedResult, true, cancellationToken); + } + + private async ValueTask> Deserialize( + DeserializationResult uncompletedResult, + bool isAsync, + CancellationToken cancellationToken + ) + where T : notnull + { + if (uncompletedResult.IsCompleted) + { + ThrowInvalidOperationException_DeserializationCompleted(); + } + + var completedOrFailed = false; + var reader = uncompletedResult.Reader; + Debug.Assert(reader is not null); + try + { + var headerResult = await reader.ReadHeader(isAsync, cancellationToken); + if (!headerResult.IsSuccess) + { + return uncompletedResult; + } + + var rootObjectIsNull = headerResult.Value; + if (rootObjectIsNull) + { + return DeserializationResult.FromValue(default); + } + var deserializationResult = await reader.Read( + uncompletedResult.RootTypeRegistrationHint, + ObjectMetaOption.ReferenceMeta | ObjectMetaOption.TypeMeta, + isAsync, + cancellationToken + ); + if (!deserializationResult.IsSuccess) + { + return uncompletedResult; + } + + completedOrFailed = true; + return DeserializationResult.FromValue(deserializationResult.Value); + } + catch (Exception) + { + completedOrFailed = true; + throw; + } + finally + { + if (completedOrFailed) + { + reader.Reset(); + _readerPool.Return(reader); + } + } + } + + private async ValueTask> DeserializeNullable( + DeserializationResult uncompletedResult, + bool isAsync, + CancellationToken cancellationToken + ) + where T : struct + { + if (uncompletedResult.IsCompleted) + { + ThrowInvalidOperationException_DeserializationCompleted(); + } + + var completedOrFailed = false; + var reader = uncompletedResult.Reader; + Debug.Assert(reader is not null); + try + { + var headerResult = await reader.ReadHeader(isAsync, cancellationToken); + if (!headerResult.IsSuccess) + { + return uncompletedResult; + } + + var rootObjectIsNull = headerResult.Value; + if (rootObjectIsNull) + { + return DeserializationResult.FromValue(null); + } + var deserializationResult = await reader.ReadNullable( + uncompletedResult.RootTypeRegistrationHint, + ObjectMetaOption.ReferenceMeta | ObjectMetaOption.TypeMeta, + isAsync, + cancellationToken + ); + if (!deserializationResult.IsSuccess) + { + return uncompletedResult; + } + + completedOrFailed = true; + return DeserializationResult.FromValue(deserializationResult.Value); + } + catch (Exception) + { + completedOrFailed = true; + throw; + } + finally + { + if (completedOrFailed) + { + reader.Reset(); + _readerPool.Return(reader); + } + } + } + + [DoesNotReturn] + private void ThrowInvalidOperationException_SerializationCompleted() + { + throw new InvalidOperationException("Serialization is already completed."); + } + + [DoesNotReturn] + private void ThrowInvalidOperationException_DeserializationCompleted() + { + throw new InvalidOperationException("Deserialization is already completed."); + } + + #region Register methods + + /// + public TypeRegistration Register( + Type targetType, + Func? serializerFactory, + Func? deserializerFactory + ) => _typeRegistry.Register(targetType, serializerFactory, deserializerFactory); + + /// + public TypeRegistration Register( + Type targetType, + string? @namespace, + string name, + Func serializerFactory, + Func deserializerFactory + ) => _typeRegistry.Register(targetType, @namespace, name, serializerFactory, deserializerFactory); + + /// + public TypeRegistration Register( + Type targetType, + TypeKind targetTypeKind, + Func serializerFactory, + Func deserializerFactory + ) => _typeRegistry.Register(targetType, targetTypeKind, serializerFactory, deserializerFactory); + + /// + public TypeRegistration Register( + Type targetType, + int id, + Func serializerFactory, + Func deserializerFactory + ) => _typeRegistry.Register(targetType, id, serializerFactory, deserializerFactory); + + /// + public void Register(Type declaredType, TypeRegistration registration) => + _typeRegistry.Register(declaredType, registration); + + #endregion +} diff --git a/csharp/Fury/Fury.csproj b/csharp/Fury/Fury.csproj new file mode 100644 index 0000000000..9d906ce24c --- /dev/null +++ b/csharp/Fury/Fury.csproj @@ -0,0 +1,18 @@ + + + + net8.0;net9.0;netstandard2.0;netstandard2.1 + 13 + enable + true + Debug;Release;ReleaseAot + AnyCPU + + + + + + + + + diff --git a/csharp/Fury/Global.cs b/csharp/Fury/Global.cs new file mode 100644 index 0000000000..80752dd862 --- /dev/null +++ b/csharp/Fury/Global.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +// ReSharper disable once CheckNamespace +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +[assembly: InternalsVisibleTo("Fury.Testing")] + + +#if !NET8_0_OR_GREATER +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + internal class IsExternalInit; +} +#endif diff --git a/csharp/Fury/Helpers/BitHelper.cs b/csharp/Fury/Helpers/BitHelper.cs new file mode 100644 index 0000000000..0e4ef0e410 --- /dev/null +++ b/csharp/Fury/Helpers/BitHelper.cs @@ -0,0 +1,85 @@ +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; + +#if NET6_0_OR_GREATER +using System.Runtime.Intrinsics.X86; +#endif + +namespace Fury.Helpers; + +internal static class BitHelper +{ + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetBitMask32(int bitsCount) => (1 << bitsCount) - 1; + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetBitMaskU32(int bitsCount) => (1u << bitsCount) - 1; + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long GetBitMask64(int bitsCount) => (1L << bitsCount) - 1; + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong GetBitMaskU64(int bitsCount) => (1uL << bitsCount) - 1; + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ClearLowBits(byte value, int lowBitsCount) => (byte)(value & ~GetBitMask32(lowBitsCount)); + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long ClearLowBits(long value, int lowBitsCount) => value & ~GetBitMask64(lowBitsCount); + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ClearLowBits(ulong value, int lowBitsCount) => value & ~(ulong)GetBitMask64(lowBitsCount); + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ClearHighBits(byte value, int highBitsCount) => (byte)(value & GetBitMask32(8 - highBitsCount)); + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte KeepLowBits(byte value, int lowBitsCount) => (byte)(value & GetBitMask32(lowBitsCount)); + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong KeepLowBits(ulong value, int lowBitsCount) => value & (ulong)GetBitMask64(lowBitsCount); + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte KeepHighBits(byte value, int highBitsCount) => (byte)(value & ~GetBitMask32(8 - highBitsCount)); + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadBits(byte b1, int bitOffset, int bitCount) + { + + return (byte)((b1 >>> (8 - bitCount - bitOffset)) & GetBitMask32(bitCount)); + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadBits(byte b1, byte b2, int bitOffset, int bitCount) + { + var byteFromB1 = b1 << (bitOffset + bitCount - 8); + var byteFromB2 = b2 >>> (8 * 2 - bitCount - bitOffset); + return (byte)((byteFromB1 | byteFromB2) & GetBitMask32(bitCount)); + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong BitFieldExtract(ulong value, byte bitOffset, byte bitCount) + { +#if NET6_0_OR_GREATER + if (Bmi1.X64.IsSupported) + { + return Bmi1.X64.BitFieldExtract(value, bitOffset, bitCount); + } +#endif + return (value >>> bitOffset) & GetBitMaskU64(bitCount); + } +} diff --git a/csharp/Fury/Helpers/HashHelper.cs b/csharp/Fury/Helpers/HashHelper.cs new file mode 100644 index 0000000000..868bd6a049 --- /dev/null +++ b/csharp/Fury/Helpers/HashHelper.cs @@ -0,0 +1,168 @@ +using System; +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Fury.Helpers; + +internal static class HashHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong FinalizationMix(ulong k) + { + k ^= k >> 33; + k *= 0xff51afd7ed558ccd; + k ^= k >> 33; + k *= 0xc4ceb9fe1a85ec53; + k ^= k >> 33; + return k; + } + + public static void MurmurHash3_x64_128(ReadOnlySpan key, uint seed, out ulong out1, out ulong out2) + { + const int blockSize = sizeof(ulong) * 2; + + const ulong c1 = 0x87c37b91114253d5; + const ulong c2 = 0x4cf5ad432745937f; + + var length = key.Length; + + ulong h1 = seed; + ulong h2 = seed; + + ulong k1; + ulong k2; + + var blocks = MemoryMarshal.Cast(key); + var nBlocks = length / blockSize; + for (var i = 0; i < nBlocks; i++) + { + k1 = blocks[i * 2]; + k2 = blocks[i * 2 + 1]; + + k1 *= c1; + k1 = BitOperations.RotateLeft(k1, 31); + k1 *= c2; + h1 ^= k1; + + h1 = BitOperations.RotateLeft(h1, 27); + h1 += h2; + h1 = h1 * 5 + 0x52dce729; + + k2 *= c2; + k2 = BitOperations.RotateLeft(k2, 33); + k2 *= c1; + h2 ^= k2; + + h2 = BitOperations.RotateLeft(h2, 31); + h2 += h1; + h2 = h2 * 5 + 0x38495ab5; + } + + var tail = key.Slice(nBlocks * blockSize); + + k1 = 0; + k2 = 0; + + switch (length & 15) + { + case 15: + k2 ^= (ulong)tail[14] << 48; + goto case 14; + case 14: + k2 ^= (ulong)tail[13] << 40; + goto case 13; + case 13: + k2 ^= (ulong)tail[12] << 32; + goto case 12; + case 12: + k2 ^= (ulong)tail[11] << 24; + goto case 11; + case 11: + k2 ^= (ulong)tail[10] << 16; + goto case 10; + case 10: + k2 ^= (ulong)tail[9] << 8; + goto case 9; + case 9: + k2 ^= tail[8]; + k2 *= c2; + k2 = BitOperations.RotateLeft(k2, 33); + k2 *= c1; + h2 ^= k2; + goto case 8; + case 8: + k1 ^= (ulong)tail[7] << 56; + goto case 7; + case 7: + k1 ^= (ulong)tail[6] << 48; + goto case 6; + case 6: + k1 ^= (ulong)tail[5] << 40; + goto case 5; + case 5: + k1 ^= (ulong)tail[4] << 32; + goto case 4; + case 4: + k1 ^= (ulong)tail[3] << 24; + goto case 3; + case 3: + k1 ^= (ulong)tail[2] << 16; + goto case 2; + case 2: + k1 ^= (ulong)tail[1] << 8; + goto case 1; + case 1: + k1 ^= tail[0]; + k1 *= c1; + k1 = BitOperations.RotateLeft(k1, 31); + k1 *= c2; + h1 ^= k1; + break; + } + + h1 ^= (ulong)length; + h2 ^= (ulong)length; + + h1 += h2; + h2 += h1; + + h1 = FinalizationMix(h1); + h2 = FinalizationMix(h2); + + h1 += h2; + h2 += h1; + + out1 = h1; + out2 = h2; + } + + public static void MurmurHash3_x64_128(ReadOnlySequence key, uint seed, out ulong out1, out ulong out2) + { + var length = (int)key.Length; + if (length == 0) + { + MurmurHash3_x64_128(ReadOnlySpan.Empty, seed, out out1, out out2); + return; + } + + if (key.IsSingleSegment) + { + MurmurHash3_x64_128(key.First.Span, seed, out out1, out out2); + return; + } + + // Maybe a ReadOnlySequence specialised version would be faster than copying to an array? + var buffer = ArrayPool.Shared.Rent(length); + try + { + key.CopyTo(buffer); + MurmurHash3_x64_128(buffer.AsSpan(0, length), seed, out out1, out out2); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +} diff --git a/csharp/Fury/Helpers/ReferenceHelper.cs b/csharp/Fury/Helpers/ReferenceHelper.cs new file mode 100644 index 0000000000..a54411a1b5 --- /dev/null +++ b/csharp/Fury/Helpers/ReferenceHelper.cs @@ -0,0 +1,54 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Fury.Helpers; + +internal static class ReferenceHelper +{ + public static readonly MethodInfo UnboxMethod = typeof(Unsafe).GetMethod(nameof(Unsafe.Unbox))!; + + public static ref T UnboxOrGetNullRef(object value) + { + if (value is not T) + { + ThrowHelper.ThrowArgumentNullExceptionIfNull(in value, nameof(value)); + } + + if (ReferenceHelper.Unbox is null) + { + return ref Unsafe.NullRef(); + } + + return ref ReferenceHelper.Unbox(value); + } + + public static ref T UnboxOrGetInputRef(ref object value) + { + if (value is not T) + { + ThrowHelper.ThrowArgumentNullExceptionIfNull(in value, nameof(value)); + } + + if (ReferenceHelper.Unbox is null) + { + return ref Unsafe.As(ref value); + } + + return ref ReferenceHelper.Unbox(value); + } +} + +file static class ReferenceHelper +{ + internal delegate ref T UnboxDelegate(object box); + + internal static readonly UnboxDelegate? Unbox; + + static ReferenceHelper() + { + if (typeof(T).IsValueType) + { + Unbox = ReferenceHelper.UnboxMethod.MakeGenericMethod(typeof(T)).CreateDelegate(); + } + } +} diff --git a/csharp/Fury/Helpers/SpanHelper.cs b/csharp/Fury/Helpers/SpanHelper.cs new file mode 100644 index 0000000000..ecc2bbb8ec --- /dev/null +++ b/csharp/Fury/Helpers/SpanHelper.cs @@ -0,0 +1,59 @@ +using System; +using System.Buffers; +using System.Runtime.InteropServices; + +namespace Fury.Helpers; + +internal static class SpanHelper +{ + public static Span CreateSpan(ref T reference, int length) + where T : unmanaged + { +#if NETSTANDARD2_0 + unsafe + { + fixed (T* p = &reference) + { + return new Span(p, length); + } + } +#else + return MemoryMarshal.CreateSpan(ref reference, length); +#endif + } + + public static int CopyUpTo(this Span source, Span destination) + { + if (source.Length > destination.Length) + { + source = source.Slice(0, destination.Length); + } + + source.CopyTo(destination); + return source.Length; + } + + public static int CopyUpTo(this ReadOnlySpan source, Span destination) + { + if (source.Length > destination.Length) + { + source = source.Slice(0, destination.Length); + } + + source.CopyTo(destination); + return source.Length; + } + + public static (SequencePosition Consumed, int Length) CopyUpTo(this ReadOnlySequence source, Span destination) + { + var sourceLength = (int)source.Length; + if (sourceLength > destination.Length) + { + source = source.Slice(0, destination.Length); + sourceLength = destination.Length; + } + + source.CopyTo(destination); + return (source.End, sourceLength); + } +} diff --git a/csharp/Fury/Helpers/StringHelper.cs b/csharp/Fury/Helpers/StringHelper.cs new file mode 100644 index 0000000000..fcd0376d4e --- /dev/null +++ b/csharp/Fury/Helpers/StringHelper.cs @@ -0,0 +1,50 @@ +using System; +using System.Buffers; + +namespace Fury.Helpers; + +internal static class StringHelper +{ + public static string Create(int length, in TState state, SpanAction action) + { + if (length == 0) + { + return string.Empty; + } +#if NET5_0_OR_GREATER || NETSTANDARD2_1 + return string.Create(length, state, action); +#else + var result = new string(' ', length); + unsafe + { + fixed (char* pChar = result) + { + var chars = new Span(pChar, result.Length); + action(chars, state); + } + } + + return result; +#endif + } + + public static string ToFullName(string? ns, string? name) + { + name = ToStringOrNull(name); + if (string.IsNullOrWhiteSpace(ns)) + { + return name; + } + return ns + "." + name; + } + + public static bool AreStringsEqualOrEmpty(string? str1, string? str2) + { + return string.IsNullOrEmpty(str1) && string.IsNullOrEmpty(str2) || str1 == str2; + } + + public static string ToStringOrNull(in T obj) + { + return obj?.ToString() ?? "null"; + } +} diff --git a/csharp/Fury/Helpers/TypeHelper.cs b/csharp/Fury/Helpers/TypeHelper.cs new file mode 100644 index 0000000000..c7129cd009 --- /dev/null +++ b/csharp/Fury/Helpers/TypeHelper.cs @@ -0,0 +1,114 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Fury.Helpers; + +internal static class TypeHelper +{ + public static readonly bool IsSealed = typeof(T).IsSealed; + public static readonly bool IsReferenceOrContainsReferences = TypeHelper.IsReferenceOrContainsReferences(); +} + +internal static class TypeHelper +{ + public static bool GetGenericBaseTypeArguments( + Type targetType, + Type genericBaseType, + [NotNullWhen(true)] out Type[]? argument + ) + { + Debug.Assert(genericBaseType.IsGenericType); + genericBaseType = genericBaseType.GetGenericTypeDefinition(); + + if (genericBaseType.IsInterface) + { + foreach (var @interface in targetType.GetInterfaces()) + { + if (@interface.IsGenericType && @interface.GetGenericTypeDefinition() == genericBaseType) + { + argument = @interface.GenericTypeArguments; + return true; + } + } + } + else + { + var baseType = targetType; + while (baseType is not null) + { + if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == genericBaseType) + { + argument = baseType.GenericTypeArguments; + return true; + } + baseType = targetType.BaseType; + } + } + + argument = null; + return false; + } + + public static bool TryGetUnderlyingElementType( + Type arrayType, + [NotNullWhen(true)] out Type? elementType, + out int rank + ) + { + // TODO: Multi-dimensional arrays are not supported yet. + rank = 0; + var currentType = arrayType; + while (currentType.IsArray) + { + elementType = currentType.GetElementType(); + if (elementType is null) + { + return false; + } + currentType = elementType; + rank++; + } + elementType = currentType; + return true; + } + + public static bool IsReferenceOrContainsReferences(Type type) + { + if (!type.IsValueType) + { + return true; + } + + if (type.IsPrimitive || type.IsEnum) + { + return false; + } + + foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + { + if (IsReferenceOrContainsReferences(field.FieldType)) + { + return true; + } + } + + return false; + } + + public static bool IsReferenceOrContainsReferences() + { +#if NET8_0_OR_GREATER + return RuntimeHelpers.IsReferenceOrContainsReferences(); +#else + return IsReferenceOrContainsReferences(typeof(T)); +#endif + } + + public static bool IsNullable(Type type) + { + return Nullable.GetUnderlyingType(type) is not null; + } +} diff --git a/csharp/Fury/Meta/AbstractLowerSpecialEncoding.cs b/csharp/Fury/Meta/AbstractLowerSpecialEncoding.cs new file mode 100644 index 0000000000..1011ed5de5 --- /dev/null +++ b/csharp/Fury/Meta/AbstractLowerSpecialEncoding.cs @@ -0,0 +1,60 @@ +using System.Text; + +namespace Fury.Meta; + +internal abstract class AbstractLowerSpecialEncoding(MetaString.Encoding encoding) : MetaStringEncoding(encoding) +{ + internal const int BitsPerChar = 5; + + public sealed override Encoder GetEncoder() => + ThrowHelper.ThrowNotSupportedException_EncoderNotSupportedForThisEncoding(GetType().Name); + + internal static bool TryEncodeChar(char c, out byte b) + { + var (success, encoded) = c switch + { + >= 'a' and <= 'z' => (true, (byte)(c - 'a')), + '.' => (true, NumberOfEnglishLetters), + '_' => (true, NumberOfEnglishLetters + 1), + '$' => (true, NumberOfEnglishLetters + 2), + '|' => (true, NumberOfEnglishLetters + 3), + _ => (false, default) + }; + b = (byte)encoded; + return success; + } + + internal static byte EncodeChar(char c) + { + if (!TryEncodeChar(c, out var b)) + { + ThrowHelper.ThrowBadSerializationInputException_UnsupportedMetaStringChar(c); + } + + return b; + } + + internal static bool TryDecodeByte(byte b, out char c) + { + (var success, c) = b switch + { + < NumberOfEnglishLetters => (true, (char)(b + 'a')), + NumberOfEnglishLetters => (true, '.'), + NumberOfEnglishLetters + 1 => (true, '_'), + NumberOfEnglishLetters + 2 => (true, '$'), + NumberOfEnglishLetters + 3 => (true, '|'), + _ => (false, default) + }; + return success; + } + + internal static char DecodeByte(byte b) + { + if (!TryDecodeByte(b, out var c)) + { + ThrowHelper.ThrowBadDeserializationInputException_UnrecognizedMetaStringCodePoint(b); + } + + return c; + } +} diff --git a/csharp/Fury/Meta/AllToLowerSpecialDecoder.cs b/csharp/Fury/Meta/AllToLowerSpecialDecoder.cs new file mode 100644 index 0000000000..45375426b5 --- /dev/null +++ b/csharp/Fury/Meta/AllToLowerSpecialDecoder.cs @@ -0,0 +1,51 @@ +using System; + +namespace Fury.Meta; + +internal sealed class AllToLowerSpecialDecoder : MetaStringDecoder +{ + internal bool WasLastCharUpperCaseFlag; + + public override void Convert( + ReadOnlySpan bytes, + Span chars, + bool flush, + out int bytesUsed, + out int charsUsed, + out bool completed + ) + { + MustFlush = flush; + AllToLowerSpecialEncoding.GetChars(bytes, chars, this, out bytesUsed, out charsUsed); + completed = bytesUsed == bytes.Length && !HasLeftoverData; + if (flush) + { + Reset(); + } + } + + public override int GetCharCount(ReadOnlySpan bytes, bool flush) + { + MustFlush = flush; + var charCount = AllToLowerSpecialEncoding.GetCharCount(bytes, this); + if (flush) + { + Reset(); + } + return charCount; + } + + public override int GetChars(ReadOnlySpan bytes, Span chars, bool flush) + { + Convert(bytes, chars, flush, out _, out var charsUsed, out _); + return charsUsed; + } + + public override void Reset() + { + MustFlush = false; + LeftoverBits = 0; + LeftoverBitCount = 0; + HasState = false; + } +} diff --git a/csharp/Fury/Meta/AllToLowerSpecialEncoding.cs b/csharp/Fury/Meta/AllToLowerSpecialEncoding.cs new file mode 100644 index 0000000000..35cabc888f --- /dev/null +++ b/csharp/Fury/Meta/AllToLowerSpecialEncoding.cs @@ -0,0 +1,260 @@ +using System; +using System.Text; + +namespace Fury.Meta; + +internal sealed class AllToLowerSpecialEncoding() : AbstractLowerSpecialEncoding(MetaString.Encoding.AllToLowerSpecial) +{ + public static readonly AllToLowerSpecialEncoding Instance = new(); + + internal const char UpperCaseFlag = '|'; + private static readonly byte EncodedUpperCaseFlag = EncodeChar(UpperCaseFlag); + + private static readonly AllToLowerSpecialDecoder SharedDecoder = new(); + + public override int GetByteCount(ReadOnlySpan chars) + { + var upperCount = 0; + foreach (var c in chars) + { + if (char.IsUpper(c)) + { + upperCount++; + } + } + + var bitCount = GetBitCount(chars.Length, upperCount); + return bitCount / BitsOfByte + 1; + } + + public static int GetBitCount(int charCount, int upperCount) + { + return (charCount + upperCount) * BitsPerChar; + } + + public override int GetCharCount(ReadOnlySpan bytes) + { + var charCount = SharedDecoder.GetCharCount(bytes, true); + SharedDecoder.Reset(); + return charCount; + } + + public override int GetMaxByteCount(int charCount) + { + return charCount * BitsPerChar * 2 / BitsOfByte + 1; + } + + public override int GetMaxCharCount(int byteCount) + { + return LowerSpecialEncoding.Instance.GetMaxCharCount(byteCount); + } + + public override int GetBytes(ReadOnlySpan chars, Span bytes) + { + var bitsWriter = new BitsWriter(bytes); + var charsReader = new CharsReader(chars); + + bitsWriter.Advance(1); + while (charsReader.TryReadChar(out var c)) + { + if (char.IsUpper(c)) + { + if (bitsWriter.TryWriteBits(BitsPerChar, EncodedUpperCaseFlag)) + { + bitsWriter.Advance(BitsPerChar); + } + else + { + break; + } + c = char.ToLowerInvariant(c); + } + + var charByte = EncodeChar(c); + + if (bitsWriter.TryWriteBits(BitsPerChar, charByte)) + { + charsReader.Advance(); + bitsWriter.Advance(BitsPerChar); + } + else + { + break; + } + } + + if (charsReader.CharsUsed < chars.Length) + { + ThrowHelper.ThrowArgumentException_InsufficientSpaceInTheOutputBuffer(nameof(bytes)); + } + + if (bitsWriter.UnusedBitCountInLastUsedByte >= BitsPerChar) + { + bitsWriter[0] = true; + } + + return bitsWriter.BytesUsed; + } + + public override int GetChars(ReadOnlySpan bytes, Span chars) + { + GetChars(bytes, chars, SharedDecoder, out _, out var charsUsed); + SharedDecoder.Reset(); + return charsUsed; + } + + private static bool TryWriteChar( + ref CharsWriter writer, + byte charByte, + AllToLowerSpecialDecoder decoder, + out bool writtenChar + ) + { + if (charByte == EncodedUpperCaseFlag) + { + if (decoder.WasLastCharUpperCaseFlag) + { + ThrowHelper.ThrowBadDeserializationInputException_UpperCaseFlagCannotAppearConsecutively(); + } + writtenChar = false; + return true; + } + + var decodedChar = DecodeByte(charByte); + if (decoder.WasLastCharUpperCaseFlag) + { + decodedChar = char.ToUpperInvariant(decodedChar); + } + + writtenChar = writer.TryWriteChar(decodedChar); + return writtenChar; + } + + internal static void GetChars( + ReadOnlySpan bytes, + Span chars, + AllToLowerSpecialDecoder decoder, + out int bytesUsed, + out int charsUsed + ) + { + var bitsReader = new BitsReader(bytes); + var charsWriter = new CharsWriter(chars); + if (!decoder.HasState) + { + decoder.HasState = true; + if (bitsReader.TryReadBits(1, out var stripLastCharFlag)) + { + bitsReader.Advance(1); + decoder.StripLastChar = stripLastCharFlag != 0; + } + } + else + { + if (TryReadLeftOver(decoder, ref bitsReader, BitsPerChar, out var charByte, out var bitsUsedFromBitsReader)) + { + if (TryWriteChar(ref charsWriter, charByte, decoder, out var writtenChar)) + { + decoder.WasLastCharUpperCaseFlag = charByte == EncodedUpperCaseFlag; + bitsReader.Advance(bitsUsedFromBitsReader); + if (writtenChar) + { + charsWriter.Advance(); + } + + if (TryReadLeftOver(decoder, ref bitsReader, BitsPerChar, out charByte, out bitsUsedFromBitsReader)) + { + if (TryWriteChar(ref charsWriter, charByte, decoder, out writtenChar)) + { + decoder.WasLastCharUpperCaseFlag = charByte == EncodedUpperCaseFlag; + bitsReader.Advance(bitsUsedFromBitsReader); + if (writtenChar) + { + charsWriter.Advance(); + } + } + } + } + } + } + + while (bitsReader.TryReadBits(BitsPerChar, out var charByte)) + { + if (bitsReader.GetRemainingCount(BitsPerChar) == 1 && decoder is { MustFlush: true, StripLastChar: true }) + { + break; + } + if (TryWriteChar(ref charsWriter, charByte, decoder, out var writtenChar)) + { + decoder.WasLastCharUpperCaseFlag = charByte == EncodedUpperCaseFlag; + bitsReader.Advance(BitsPerChar); + if (writtenChar) + { + charsWriter.Advance(); + } + } + else + { + break; + } + } + + decoder.SetLeftoverData(bitsReader.UnusedBitsInLastUsedByte, bitsReader.UnusedBitCountInLastUsedByte); + + bytesUsed = bitsReader.BytesUsed; + charsUsed = charsWriter.CharsUsed; + } + + internal static int GetCharCount(ReadOnlySpan bytes, AllToLowerSpecialDecoder decoder) + { + var bitsReader = new BitsReader(bytes); + var charCount = 0; + if (!decoder.HasState) + { + decoder.HasState = true; + if (bitsReader.TryReadBits(1, out var stripLastCharFlag)) + { + bitsReader.Advance(1); + decoder.StripLastChar = stripLastCharFlag != 0; + } + } + else + { + if (TryReadLeftOver(decoder, ref bitsReader, BitsPerChar, out var charByte, out var bitsUsedFromBitsReader)) + { + bitsReader.Advance(bitsUsedFromBitsReader); + if (charByte != EncodedUpperCaseFlag) + { + charCount++; + } + + if (TryReadLeftOver(decoder, ref bitsReader, BitsPerChar, out charByte, out bitsUsedFromBitsReader)) + { + bitsReader.Advance(bitsUsedFromBitsReader); + if (charByte != EncodedUpperCaseFlag) + { + charCount++; + } + } + } + } + + while (bitsReader.TryReadBits(BitsPerChar, out var charByte)) + { + if (bitsReader.GetRemainingCount(BitsPerChar) == 1 && decoder is { MustFlush: true, StripLastChar: true }) + { + break; + } + bitsReader.Advance(BitsPerChar); + if (charByte != EncodedUpperCaseFlag) + { + charCount++; + } + } + + decoder.SetLeftoverData(bitsReader.UnusedBitsInLastUsedByte, bitsReader.UnusedBitCountInLastUsedByte); + return charCount; + } + + public override Decoder GetDecoder() => new AllToLowerSpecialDecoder(); +} diff --git a/csharp/Fury/Meta/BitsReader.cs b/csharp/Fury/Meta/BitsReader.cs new file mode 100644 index 0000000000..799b4f6f18 --- /dev/null +++ b/csharp/Fury/Meta/BitsReader.cs @@ -0,0 +1,88 @@ +using System; +using Fury.Helpers; + +namespace Fury.Meta; + +internal ref struct BitsReader(ReadOnlySpan bytes) +{ + private const int BitsOfByte = sizeof(byte) * 8; + + private readonly ReadOnlySpan _bytes = bytes; + + private int _currentBitIndex; + private int CurrentByteIndex => _currentBitIndex / BitsOfByte; + + internal int BytesUsed => (_currentBitIndex + BitsOfByte - 1) / BitsOfByte; + internal int UnusedBitCountInLastUsedByte => (BitsOfByte - _currentBitIndex % BitsOfByte) % BitsOfByte; + + internal byte UnusedBitsInLastUsedByte + { + get + { + var unusedBitCountInLastUsedByte = UnusedBitCountInLastUsedByte; + if (unusedBitCountInLastUsedByte == 0) + { + return 0; + } + + var currentByte = _bytes[CurrentByteIndex]; + return BitHelper.KeepLowBits(currentByte, unusedBitCountInLastUsedByte); + } + } + + internal bool HasNext(int bitCount) => _currentBitIndex + bitCount <= _bytes.Length * BitsOfByte; + + internal int GetRemainingCount(int bitCount) => (_bytes.Length * BitsOfByte - _currentBitIndex) / bitCount; + + internal bool TryReadBits(int bitCount, out byte bits) + { + if (!HasNext(bitCount)) + { + bits = default; + return false; + } + var currentByteIndex = CurrentByteIndex; + if (currentByteIndex >= _bytes.Length) + { + bits = default; + return false; + } + + var bitOffsetInCurrentByte = _currentBitIndex % BitsOfByte; + var bitsLeftInCurrentByte = BitsOfByte - bitOffsetInCurrentByte; + if (bitsLeftInCurrentByte >= bitCount) + { + bits = BitHelper.ReadBits(_bytes[currentByteIndex], bitOffsetInCurrentByte, bitCount); + return true; + } + + if (currentByteIndex + 1 >= _bytes.Length) + { + bits = default; + return false; + } + + bits = BitHelper.ReadBits( + _bytes[currentByteIndex], + _bytes[currentByteIndex + 1], + bitOffsetInCurrentByte, + bitCount + ); + return true; + } + + internal void Advance(int bitCount) + { + _currentBitIndex += bitCount; + } + + internal bool this[int bitIndex] + { + get + { + var byteIndex = bitIndex / BitsOfByte; + var bitOffset = bitIndex % BitsOfByte; + return (_bytes[byteIndex] & (1 << (BitsOfByte - bitOffset - 1))) != 0; + } + } +} diff --git a/csharp/Fury/Meta/BitsWriter.cs b/csharp/Fury/Meta/BitsWriter.cs new file mode 100644 index 0000000000..d342e6bdd6 --- /dev/null +++ b/csharp/Fury/Meta/BitsWriter.cs @@ -0,0 +1,136 @@ +using System; +using Fury.Helpers; + +namespace Fury.Meta; + +public ref struct BitsWriter(Span bytes) +{ + private const int BitsOfByte = sizeof(byte) * 8; + + private readonly Span _bytes = bytes; + + private int _currentBitIndex; + private int CurrentByteIndex => _currentBitIndex / BitsOfByte; + + internal int BytesUsed => (_currentBitIndex + BitsOfByte - 1) / BitsOfByte; + internal int UnusedBitCountInLastUsedByte => (BitsOfByte - _currentBitIndex % BitsOfByte) % BitsOfByte; + + internal byte UnusedBitInLastUsedByte + { + get + { + var unusedBitCountInLastUsedByte = UnusedBitCountInLastUsedByte; + if (unusedBitCountInLastUsedByte == 0) + { + return 0; + } + + var currentByte = _bytes[CurrentByteIndex]; + return BitHelper.KeepLowBits(currentByte, unusedBitCountInLastUsedByte); + } + } + + internal bool HasNext(int bitCount) => _currentBitIndex + bitCount <= _bytes.Length * BitsOfByte; + + internal bool TryReadBits(int bitCount, out byte bits) + { + if (!HasNext(bitCount)) + { + bits = default; + return false; + } + var currentByteIndex = CurrentByteIndex; + if (currentByteIndex >= _bytes.Length) + { + bits = default; + return false; + } + + var bitOffsetInCurrentByte = _currentBitIndex % BitsOfByte; + var bitsLeftInCurrentByte = BitsOfByte - bitOffsetInCurrentByte; + if (bitsLeftInCurrentByte >= bitCount) + { + bits = BitHelper.ReadBits(_bytes[currentByteIndex], bitOffsetInCurrentByte, bitCount); + return true; + } + + if (currentByteIndex + 1 >= _bytes.Length) + { + bits = default; + return false; + } + + bits = BitHelper.ReadBits( + _bytes[currentByteIndex], + _bytes[currentByteIndex + 1], + bitOffsetInCurrentByte, + bitCount + ); + return true; + } + + internal bool TryWriteBits(int bitCount, byte bits) + { + if (!HasNext(bitCount)) + { + return false; + } + bits = (byte)(bits & BitHelper.GetBitMask32(bitCount)); + var currentByteIndex = CurrentByteIndex; + if (currentByteIndex >= _bytes.Length) + { + return false; + } + + var bitOffsetInCurrentByte = _currentBitIndex % BitsOfByte; + var bitsLeftInCurrentByte = BitsOfByte - bitOffsetInCurrentByte; + byte currentByte; + if (bitsLeftInCurrentByte >= bitCount) + { + currentByte = BitHelper.ClearLowBits(_bytes[currentByteIndex], bitsLeftInCurrentByte); + _bytes[currentByteIndex] = (byte)(currentByte | (bits << (bitsLeftInCurrentByte - bitCount))); + return true; + } + + if (currentByteIndex + 1 >= _bytes.Length) + { + return false; + } + + var bitsToWriteInCurrentByte = bits >>> (bitCount - bitsLeftInCurrentByte); + var bitsToWriteInNextByte = bits & BitHelper.GetBitMask32(bitCount - bitsLeftInCurrentByte); + currentByte = BitHelper.ClearLowBits(_bytes[currentByteIndex], bitsLeftInCurrentByte); + _bytes[currentByteIndex] = (byte)(currentByte | bitsToWriteInCurrentByte); + _bytes[currentByteIndex + 1] = (byte)(bitsToWriteInNextByte << (BitsOfByte - bitCount + bitsLeftInCurrentByte)); + + return true; + } + + internal void Advance(int bitCount) + { + _currentBitIndex += bitCount; + } + + internal bool this[int bitIndex] + { + get + { + var byteIndex = bitIndex / BitsOfByte; + var bitOffset = bitIndex % BitsOfByte; + return (_bytes[byteIndex] & (1 << (BitsOfByte - bitOffset - 1))) != 0; + } + set + { + var byteIndex = bitIndex / BitsOfByte; + var bitOffset = bitIndex % BitsOfByte; + if (value) + { + _bytes[byteIndex] |= (byte)(1 << (BitsOfByte - bitOffset - 1)); + } + else + { + _bytes[byteIndex] &= (byte)~(1 << (BitsOfByte - bitOffset - 1)); + } + } + } +} diff --git a/csharp/Fury/Meta/CharsReader.cs b/csharp/Fury/Meta/CharsReader.cs new file mode 100644 index 0000000000..a709e5dd25 --- /dev/null +++ b/csharp/Fury/Meta/CharsReader.cs @@ -0,0 +1,29 @@ +using System; + +namespace Fury.Meta; + +public ref struct CharsReader(ReadOnlySpan chars) +{ + private readonly ReadOnlySpan _chars = chars; + + private int _currentIndex; + + internal int CharsUsed => _currentIndex; + + internal bool TryReadChar(out char c) + { + if (_currentIndex >= _chars.Length) + { + c = default; + return false; + } + + c = _chars[_currentIndex]; + return true; + } + + internal void Advance() + { + _currentIndex++; + } +} diff --git a/csharp/Fury/Meta/CharsWriter.cs b/csharp/Fury/Meta/CharsWriter.cs new file mode 100644 index 0000000000..a36cf5847e --- /dev/null +++ b/csharp/Fury/Meta/CharsWriter.cs @@ -0,0 +1,40 @@ +using System; + +namespace Fury.Meta; + +internal ref struct CharsWriter(Span chars) +{ + private readonly Span _chars = chars; + + private int _currentIndex; + + internal int CharsUsed => _currentIndex; + + internal bool TryReadChar(out char c) + { + if (_currentIndex >= _chars.Length) + { + c = default; + return false; + } + + c = _chars[_currentIndex]; + return true; + } + + internal bool TryWriteChar(char c) + { + if (_currentIndex >= _chars.Length) + { + return false; + } + + _chars[_currentIndex] = c; + return true; + } + + internal void Advance() + { + _currentIndex++; + } +} diff --git a/csharp/Fury/Meta/CompatibleMode.cs b/csharp/Fury/Meta/CompatibleMode.cs new file mode 100644 index 0000000000..1371a8fc45 --- /dev/null +++ b/csharp/Fury/Meta/CompatibleMode.cs @@ -0,0 +1,18 @@ +namespace Fury.Meta; + +/// +/// Type forward/backward compatibility config. +/// +public enum CompatibleMode +{ + /// + /// Class schema must be consistent between serialization peer and deserialization peer. + /// + SchemaConsistent, + + /// + /// Class schema can be different between serialization peer and deserialization peer. They can + /// add/delete fields independently. + /// + Compatible +} diff --git a/csharp/Fury/Meta/FirstToLowerSpecialDecoder.cs b/csharp/Fury/Meta/FirstToLowerSpecialDecoder.cs new file mode 100644 index 0000000000..eea031c919 --- /dev/null +++ b/csharp/Fury/Meta/FirstToLowerSpecialDecoder.cs @@ -0,0 +1,49 @@ +using System; + +namespace Fury.Meta; + +internal sealed class FirstToLowerSpecialDecoder : MetaStringDecoder +{ + internal bool WrittenFirstChar { get; set; } + + public override void Convert( + ReadOnlySpan bytes, + Span chars, + bool flush, + out int bytesUsed, + out int charsUsed, + out bool completed + ) + { + MustFlush = flush; + FirstToLowerSpecialEncoding.GetChars(bytes, chars, this, out bytesUsed, out charsUsed); + completed = bytesUsed == bytes.Length && !HasLeftoverData; + if (flush) + { + Reset(); + } + } + + public override int GetCharCount(ReadOnlySpan bytes, bool flush) + { + MustFlush = flush; + var charCount = MetaStringEncoding.GetCharCount(bytes, AbstractLowerSpecialEncoding.BitsPerChar, this); + if (flush) + { + Reset(); + } + return charCount; + } + + public override int GetChars(ReadOnlySpan bytes, Span chars, bool flush) + { + Convert(bytes, chars, flush, out _, out var charsUsed, out _); + return charsUsed; + } + + public override void Reset() + { + WrittenFirstChar = false; + base.Reset(); + } +} diff --git a/csharp/Fury/Meta/FirstToLowerSpecialEncoding.cs b/csharp/Fury/Meta/FirstToLowerSpecialEncoding.cs new file mode 100644 index 0000000000..2b1ed8eaa6 --- /dev/null +++ b/csharp/Fury/Meta/FirstToLowerSpecialEncoding.cs @@ -0,0 +1,160 @@ +using System; +using System.Text; + +namespace Fury.Meta; + +internal sealed class FirstToLowerSpecialEncoding() + : AbstractLowerSpecialEncoding(MetaString.Encoding.FirstToLowerSpecial) +{ + public static readonly FirstToLowerSpecialEncoding Instance = new(); + + private static readonly FirstToLowerSpecialDecoder SharedDecoder = new(); + + public override int GetByteCount(ReadOnlySpan chars) + { + return LowerSpecialEncoding.Instance.GetByteCount(chars); + } + + public override int GetCharCount(ReadOnlySpan bytes) + { + return LowerSpecialEncoding.Instance.GetCharCount(bytes); + } + + public override int GetMaxByteCount(int charCount) + { + return LowerSpecialEncoding.Instance.GetMaxByteCount(charCount); + } + + public override int GetMaxCharCount(int byteCount) + { + return LowerSpecialEncoding.Instance.GetMaxCharCount(byteCount); + } + + public override int GetBytes(ReadOnlySpan chars, Span bytes) + { + var bitsWriter = new BitsWriter(bytes); + var charsReader = new CharsReader(chars); + + bitsWriter.Advance(1); + var writtenFirstCharBits = false; + while (charsReader.TryReadChar(out var c)) + { + if (!writtenFirstCharBits) + { + c = char.ToLowerInvariant(c); + writtenFirstCharBits = true; + } + + var charByte = EncodeChar(c); + + if (bitsWriter.TryWriteBits(BitsPerChar, charByte)) + { + charsReader.Advance(); + bitsWriter.Advance(BitsPerChar); + } + else + { + break; + } + } + + if (charsReader.CharsUsed < chars.Length) + { + ThrowHelper.ThrowArgumentException_InsufficientSpaceInTheOutputBuffer(nameof(bytes)); + } + + if (bitsWriter.UnusedBitCountInLastUsedByte >= BitsPerChar) + { + bitsWriter[0] = true; + } + + return bitsWriter.BytesUsed; + } + + public override int GetChars(ReadOnlySpan bytes, Span chars) + { + SharedDecoder.Convert(bytes, chars, true, out _, out var charsUsed, out _); + SharedDecoder.Reset(); + return charsUsed; + } + + private static bool TryWriteChar(ref CharsWriter writer, byte charByte, FirstToLowerSpecialDecoder decoder) + { + var decodedChar = DecodeByte(charByte); + if (!decoder.WrittenFirstChar) + { + decodedChar = char.ToUpperInvariant(decodedChar); + } + + return writer.TryWriteChar(decodedChar); + } + + internal static void GetChars( + ReadOnlySpan bytes, + Span chars, + FirstToLowerSpecialDecoder decoder, + out int bytesUsed, + out int charsUsed + ) + { + var bitsReader = new BitsReader(bytes); + var charsWriter = new CharsWriter(chars); + if (!decoder.HasState) + { + decoder.HasState = true; + if (bitsReader.TryReadBits(1, out var stripLastCharFlag)) + { + bitsReader.Advance(1); + decoder.StripLastChar = stripLastCharFlag != 0; + } + } + else + { + if (TryReadLeftOver(decoder, ref bitsReader, BitsPerChar, out var charByte, out var bitsUsedFromBitsReader)) + { + if (TryWriteChar(ref charsWriter, charByte, decoder)) + { + decoder.WrittenFirstChar = true; + bitsReader.Advance(bitsUsedFromBitsReader); + charsWriter.Advance(); + + if (TryReadLeftOver(decoder, ref bitsReader, BitsPerChar, out charByte, out bitsUsedFromBitsReader)) + { + if (TryWriteChar(ref charsWriter, charByte, decoder)) + { + decoder.WrittenFirstChar = true; + bitsReader.Advance(bitsUsedFromBitsReader); + charsWriter.Advance(); + } + } + } + } + } + + while (bitsReader.TryReadBits(BitsPerChar, out var charByte)) + { + if (bitsReader.GetRemainingCount(BitsPerChar) == 1 && decoder is { MustFlush: true, StripLastChar: true }) + { + break; + } + + if (TryWriteChar(ref charsWriter, charByte, decoder)) + { + decoder.WrittenFirstChar = true; + bitsReader.Advance(BitsPerChar); + charsWriter.Advance(); + } + else + { + break; + } + } + + decoder.SetLeftoverData(bitsReader.UnusedBitsInLastUsedByte, bitsReader.UnusedBitCountInLastUsedByte); + + bytesUsed = bitsReader.BytesUsed; + charsUsed = charsWriter.CharsUsed; + } + + public override Decoder GetDecoder() => new FirstToLowerSpecialDecoder(); +} diff --git a/csharp/Fury/Meta/HeaderFlag.cs b/csharp/Fury/Meta/HeaderFlag.cs new file mode 100644 index 0000000000..9cf1d4be2d --- /dev/null +++ b/csharp/Fury/Meta/HeaderFlag.cs @@ -0,0 +1,4 @@ +using System; + +namespace Fury.Meta; + diff --git a/csharp/Fury/Meta/HybridMetaStringEncoding.cs b/csharp/Fury/Meta/HybridMetaStringEncoding.cs new file mode 100644 index 0000000000..63e28f7296 --- /dev/null +++ b/csharp/Fury/Meta/HybridMetaStringEncoding.cs @@ -0,0 +1,116 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Fury.Meta; + +internal sealed class HybridMetaStringEncoding(char specialChar1, char specialChar2, MetaString.Encoding[] candidateEncodings) +{ + public LowerUpperDigitSpecialEncoding LowerUpperDigit { get; } = new(specialChar1, specialChar2); + public char SpecialChar1 { get; } = specialChar1; + public char SpecialChar2 { get; } = specialChar2; + + private MetaString.Encoding[] _candidateEncodings = candidateEncodings; + + public MetaStringEncoding GetEncoding(MetaString.Encoding encoding) + { + var result = encoding switch + { + MetaString.Encoding.LowerSpecial => LowerSpecialEncoding.Instance, + MetaString.Encoding.FirstToLowerSpecial => FirstToLowerSpecialEncoding.Instance, + MetaString.Encoding.AllToLowerSpecial => AllToLowerSpecialEncoding.Instance, + MetaString.Encoding.LowerUpperDigitSpecial => LowerUpperDigit, + MetaString.Encoding.Utf8 => Utf8Encoding.Instance, + _ => ThrowHelper.ThrowUnreachableException(), + }; + + return result; + } + + private MetaString GetMetaString(string chars, MetaString.Encoding encoding) + { + var e = GetEncoding(encoding); + var byteCount = e.GetByteCount(chars); + var bytes = new byte[byteCount]; + e.GetBytes(chars.AsSpan(), bytes); + return new MetaString(chars, encoding, SpecialChar1, SpecialChar2, bytes); + } + + public MetaStringEncoding SelectEncoding(string chars) + { + var statistics = GetStatistics(chars); + if (statistics.LowerSpecialCompatible && _candidateEncodings.Contains(MetaString.Encoding.LowerSpecial)) + { + return LowerSpecialEncoding.Instance; + } + + if (statistics.LowerUpperDigitCompatible) + { + if (statistics.DigitCount == 0) + { + if ( + statistics.UpperCount == 1 + && char.IsUpper(chars[0]) + && _candidateEncodings.Contains(MetaString.Encoding.FirstToLowerSpecial) + ) + { + return FirstToLowerSpecialEncoding.Instance; + } + + var bitCountWithAllToLower = AllToLowerSpecialEncoding.GetBitCount(chars.Length, statistics.UpperCount); + var bitCountWithLowerUpperDigit = LowerUpperDigitSpecialEncoding.GetBitCount(chars.Length); + if ( + bitCountWithAllToLower < bitCountWithLowerUpperDigit + && _candidateEncodings.Contains(MetaString.Encoding.AllToLowerSpecial) + ) + { + return AllToLowerSpecialEncoding.Instance; + } + } + + if (_candidateEncodings.Contains(MetaString.Encoding.LowerUpperDigitSpecial)) + { + return LowerUpperDigit; + } + } + return Utf8Encoding.Instance; + } + + private CharStatistics GetStatistics(string chars) + { + var digitCount = 0; + var upperCount = 0; + var lowerSpecialCompatible = true; + var lowerUpperDigitCompatible = true; + foreach (var c in chars) + { + if (lowerSpecialCompatible) + { + lowerSpecialCompatible = AbstractLowerSpecialEncoding.TryEncodeChar(c, out _); + } + + if (lowerUpperDigitCompatible) + { + lowerUpperDigitCompatible = LowerUpperDigit.TryEncodeChar(c, out _); + } + + if (char.IsDigit(c)) + { + digitCount++; + } + else if (char.IsUpper(c)) + { + upperCount++; + } + } + + return new CharStatistics(digitCount, upperCount, lowerSpecialCompatible, lowerUpperDigitCompatible); + } + + private record struct CharStatistics( + int DigitCount, + int UpperCount, + bool LowerSpecialCompatible, + bool LowerUpperDigitCompatible + ); +} diff --git a/csharp/Fury/Meta/Language.cs b/csharp/Fury/Meta/Language.cs new file mode 100644 index 0000000000..26349f6bde --- /dev/null +++ b/csharp/Fury/Meta/Language.cs @@ -0,0 +1,14 @@ +namespace Fury.Meta; + +public enum Language : byte +{ + Xlang, + Java, + Python, + Cpp, + Go, + Javascript, + Rust, + Dart, + Csharp, +} diff --git a/csharp/Fury/Meta/LowerSpecialDecoder.cs b/csharp/Fury/Meta/LowerSpecialDecoder.cs new file mode 100644 index 0000000000..49067d184a --- /dev/null +++ b/csharp/Fury/Meta/LowerSpecialDecoder.cs @@ -0,0 +1,41 @@ +using System; + +namespace Fury.Meta; + +internal sealed class LowerSpecialDecoder : MetaStringDecoder +{ + public override void Convert( + ReadOnlySpan bytes, + Span chars, + bool flush, + out int bytesUsed, + out int charsUsed, + out bool completed + ) + { + MustFlush = flush; + LowerSpecialEncoding.GetChars(bytes, chars, this, out bytesUsed, out charsUsed); + completed = bytesUsed == bytes.Length && !HasLeftoverData; + if (flush) + { + Reset(); + } + } + + public override int GetCharCount(ReadOnlySpan bytes, bool flush) + { + MustFlush = flush; + var charCount = MetaStringEncoding.GetCharCount(bytes, AbstractLowerSpecialEncoding.BitsPerChar, this); + if (flush) + { + Reset(); + } + return charCount; + } + + public override int GetChars(ReadOnlySpan bytes, Span chars, bool flush) + { + Convert(bytes, chars, flush, out _, out var charsUsed, out _); + return charsUsed; + } +} diff --git a/csharp/Fury/Meta/LowerSpecialEncoding.cs b/csharp/Fury/Meta/LowerSpecialEncoding.cs new file mode 100644 index 0000000000..1822fd3129 --- /dev/null +++ b/csharp/Fury/Meta/LowerSpecialEncoding.cs @@ -0,0 +1,146 @@ +using System; +using System.Text; + +namespace Fury.Meta; + +internal sealed class LowerSpecialEncoding() : AbstractLowerSpecialEncoding(MetaString.Encoding.LowerSpecial) +{ + public static readonly LowerSpecialEncoding Instance = new(); + + private static readonly LowerSpecialDecoder SharedDecoder = new(); + + public override int GetByteCount(ReadOnlySpan chars) + { + return GetMaxByteCount(chars.Length); + } + + public override int GetCharCount(ReadOnlySpan bytes) + { + if (bytes.Length == 0) + { + return 0; + } + + var firstByte = bytes[0]; + var stripLastChar = (firstByte & StripLastCharFlagMask) != 0; + return GetMaxCharCount(bytes.Length) - (stripLastChar ? 1 : 0); + } + + public override int GetMaxByteCount(int charCount) + { + return charCount * BitsPerChar / BitsOfByte + 1; + } + + public override int GetMaxCharCount(int byteCount) + { + return (byteCount * BitsOfByte - 1) / BitsPerChar; + } + + public override int GetBytes(ReadOnlySpan chars, Span bytes) + { + var bitsWriter = new BitsWriter(bytes); + var charsReader = new CharsReader(chars); + + bitsWriter.Advance(1); + while (charsReader.TryReadChar(out var c)) + { + var charByte = EncodeChar(c); + + if (bitsWriter.TryWriteBits(BitsPerChar, charByte)) + { + charsReader.Advance(); + bitsWriter.Advance(BitsPerChar); + } + else + { + break; + } + } + + if (charsReader.CharsUsed < chars.Length) + { + ThrowHelper.ThrowArgumentException_InsufficientSpaceInTheOutputBuffer(nameof(bytes)); + } + + if (bitsWriter.UnusedBitCountInLastUsedByte >= BitsPerChar) + { + bitsWriter[0] = true; + } + + return bitsWriter.BytesUsed; + } + + public override int GetChars(ReadOnlySpan bytes, Span chars) + { + SharedDecoder.Convert(bytes, chars, true, out _, out var charsUsed, out _); + SharedDecoder.Reset(); + return charsUsed; + } + + internal static void GetChars( + ReadOnlySpan bytes, + Span chars, + LowerSpecialDecoder decoder, + out int bytesUsed, + out int charsUsed + ) + { + var bitsReader = new BitsReader(bytes); + var charsWriter = new CharsWriter(chars); + if (!decoder.HasState) + { + decoder.HasState = true; + if (bitsReader.TryReadBits(1, out var stripLastCharFlag)) + { + bitsReader.Advance(1); + decoder.StripLastChar = stripLastCharFlag != 0; + } + } + else + { + if (TryReadLeftOver(decoder, ref bitsReader, BitsPerChar, out var charByte, out var bitsUsedFromBitsReader)) + { + var decodedChar = DecodeByte(charByte); + if (charsWriter.TryWriteChar(decodedChar)) + { + bitsReader.Advance(bitsUsedFromBitsReader); + charsWriter.Advance(); + + if (TryReadLeftOver(decoder, ref bitsReader, BitsPerChar, out charByte, out bitsUsedFromBitsReader)) + { + if (charsWriter.TryWriteChar(decodedChar)) + { + bitsReader.Advance(bitsUsedFromBitsReader); + charsWriter.Advance(); + } + } + } + } + } + + while (bitsReader.TryReadBits(BitsPerChar, out var charByte)) + { + if (bitsReader.GetRemainingCount(BitsPerChar) == 1 && decoder is { MustFlush: true, StripLastChar: true }) + { + break; + } + var decodedChar = DecodeByte(charByte); + if (charsWriter.TryWriteChar(decodedChar)) + { + bitsReader.Advance(BitsPerChar); + charsWriter.Advance(); + } + else + { + break; + } + } + + decoder.SetLeftoverData(bitsReader.UnusedBitsInLastUsedByte, bitsReader.UnusedBitCountInLastUsedByte); + + bytesUsed = bitsReader.BytesUsed; + charsUsed = charsWriter.CharsUsed; + } + + public override Decoder GetDecoder() => new LowerSpecialDecoder(); +} diff --git a/csharp/Fury/Meta/LowerUpperDigitSpecialDecoder.cs b/csharp/Fury/Meta/LowerUpperDigitSpecialDecoder.cs new file mode 100644 index 0000000000..ee95c7c1a7 --- /dev/null +++ b/csharp/Fury/Meta/LowerUpperDigitSpecialDecoder.cs @@ -0,0 +1,41 @@ +using System; + +namespace Fury.Meta; + +internal sealed class LowerUpperDigitSpecialDecoder(LowerUpperDigitSpecialEncoding encoding) : MetaStringDecoder +{ + public override void Convert( + ReadOnlySpan bytes, + Span chars, + bool flush, + out int bytesUsed, + out int charsUsed, + out bool completed + ) + { + MustFlush = flush; + encoding.GetChars(bytes, chars, this, out bytesUsed, out charsUsed); + completed = bytesUsed == bytes.Length && !HasLeftoverData; + if (flush) + { + Reset(); + } + } + + public override int GetCharCount(ReadOnlySpan bytes, bool flush) + { + MustFlush = flush; + var charCount = encoding.GetCharCount(bytes); + if (flush) + { + Reset(); + } + return charCount; + } + + public override int GetChars(ReadOnlySpan bytes, Span chars, bool flush) + { + Convert(bytes, chars, flush, out _, out var charsUsed, out _); + return charsUsed; + } +} diff --git a/csharp/Fury/Meta/LowerUpperDigitSpecialEncoding.cs b/csharp/Fury/Meta/LowerUpperDigitSpecialEncoding.cs new file mode 100644 index 0000000000..de81e59198 --- /dev/null +++ b/csharp/Fury/Meta/LowerUpperDigitSpecialEncoding.cs @@ -0,0 +1,257 @@ +using System; +using System.Text; + +namespace Fury.Meta; + +internal sealed class LowerUpperDigitSpecialEncoding(char specialChar1, char specialChar2) + : MetaStringEncoding(MetaString.Encoding.LowerUpperDigitSpecial) +{ + internal const int BitsPerChar = 6; + private const int UnusedBitsPerChar = BitsOfByte - BitsPerChar; + private const int MaxRepresentableChar = (1 << BitsPerChar) - 1; + + public override int GetByteCount(ReadOnlySpan chars) + { + return GetMaxByteCount(chars.Length); + } + + public static int GetBitCount(int charCount) + { + return charCount * BitsPerChar; + } + + public override int GetCharCount(ReadOnlySpan bytes) + { + if (bytes.Length == 0) + { + return 0; + } + + var firstByte = bytes[0]; + var stripLastChar = (firstByte & StripLastCharFlagMask) != 0; + return GetMaxCharCount(bytes.Length) - (stripLastChar ? 1 : 0); + } + + public override int GetMaxByteCount(int charCount) + { + return GetBitCount(charCount) / BitsOfByte + 1; + } + + public override int GetMaxCharCount(int byteCount) + { + return (byteCount * BitsOfByte - 1) / BitsPerChar; + } + + public override int GetBytes(ReadOnlySpan chars, Span bytes) + { + var bitsWriter = new BitsWriter(bytes); + var charsReader = new CharsReader(chars); + + bitsWriter.Advance(1); + while (charsReader.TryReadChar(out var c)) + { + var charByte = EncodeChar(c); + + if (bitsWriter.TryWriteBits(BitsPerChar, charByte)) + { + charsReader.Advance(); + bitsWriter.Advance(BitsPerChar); + } + else + { + break; + } + } + + if (charsReader.CharsUsed < chars.Length) + { + ThrowHelper.ThrowArgumentException_InsufficientSpaceInTheOutputBuffer(nameof(bytes)); + } + + if (bitsWriter.UnusedBitCountInLastUsedByte >= BitsPerChar) + { + bitsWriter[0] = true; + } + + return bitsWriter.BytesUsed; + } + + public override int GetChars(ReadOnlySpan bytes, Span chars) + { + const byte bitMask = MaxRepresentableChar; + + var charCount = GetCharCount(bytes); + if (chars.Length < charCount) + { + ThrowHelper.ThrowArgumentException(paramName: nameof(chars)); + } + for (var i = 0; i < charCount; i++) + { + var currentBit = i * BitsPerChar + 1; + var byteIndex = currentBit / BitsOfByte; + var bitOffset = currentBit % BitsOfByte; + + byte charByte; + if (bitOffset <= UnusedBitsPerChar) + { + // bitOffset locations read locations + // x _ _ _ _ _ _ _ x x x x x x _ _ + // _ x _ _ _ _ _ _ _ x x x x x x _ + // _ _ x _ _ _ _ _ _ _ x x x x x x + + charByte = (byte)((bytes[byteIndex] >>> (UnusedBitsPerChar - bitOffset)) & bitMask); + } + else + { + // bitOffset locations read locations + // _ _ _ x _ _ _ _ _ _ _ x x x x x | x _ _ _ _ _ _ _ + // _ _ _ _ x _ _ _ _ _ _ _ x x x x | x x _ _ _ _ _ _ + // _ _ _ _ _ x _ _ _ _ _ _ _ x x x | x x x _ _ _ _ _ + // _ _ _ _ _ _ x _ _ _ _ _ _ _ x x | x x x x _ _ _ _ + // _ _ _ _ _ _ _ x _ _ _ _ _ _ _ x | x x x x x _ _ _ + + charByte = (byte)( + ( + bytes[byteIndex] << (bitOffset - UnusedBitsPerChar) + | bytes[byteIndex + 1] >>> (BitsOfByte + UnusedBitsPerChar - bitOffset) + ) & bitMask + ); + } + + if (!TryDecodeByte(charByte, out var c)) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(bytes)); + } + chars[i] = c; + } + + return charCount; + } + + internal void GetChars( + ReadOnlySpan bytes, + Span chars, + LowerUpperDigitSpecialDecoder decoder, + out int bytesUsed, + out int charsUsed + ) + { + var bitsReader = new BitsReader(bytes); + var charsWriter = new CharsWriter(chars); + if (!decoder.HasState) + { + decoder.HasState = true; + if (bitsReader.TryReadBits(1, out var stripLastCharFlag)) + { + bitsReader.Advance(1); + decoder.StripLastChar = stripLastCharFlag != 0; + } + } + else + { + if (TryReadLeftOver(decoder, ref bitsReader, BitsPerChar, out var charByte, out var bitsUsedFromBitsReader)) + { + var decodedChar = DecodeByte(charByte); + if (charsWriter.TryWriteChar(decodedChar)) + { + bitsReader.Advance(bitsUsedFromBitsReader); + charsWriter.Advance(); + + if (TryReadLeftOver(decoder, ref bitsReader, BitsPerChar, out charByte, out bitsUsedFromBitsReader)) + { + if (charsWriter.TryWriteChar(decodedChar)) + { + bitsReader.Advance(bitsUsedFromBitsReader); + charsWriter.Advance(); + } + } + } + } + } + + while (bitsReader.TryReadBits(BitsPerChar, out var charByte)) + { + if (bitsReader.GetRemainingCount(BitsPerChar) == 1 && decoder is { MustFlush: true, StripLastChar: true }) + { + break; + } + var decodedChar = DecodeByte(charByte); + if (charsWriter.TryWriteChar(decodedChar)) + { + bitsReader.Advance(BitsPerChar); + charsWriter.Advance(); + } + else + { + break; + } + } + + decoder.SetLeftoverData(bitsReader.UnusedBitsInLastUsedByte, bitsReader.UnusedBitCountInLastUsedByte); + + bytesUsed = bitsReader.BytesUsed; + charsUsed = charsWriter.CharsUsed; + } + + internal bool TryEncodeChar(char c, out byte b) + { + var success = true; + if (c == specialChar1) + { + b = MaxRepresentableChar - 1; + } + else if (c == specialChar2) + { + b = MaxRepresentableChar; + } + else + { + (success, b) = c switch + { + >= 'a' and <= 'z' => (true, (byte)(c - 'a')), + >= 'A' and <= 'Z' => (true, (byte)(c - 'A' + NumberOfEnglishLetters)), + >= '0' and <= '9' => (true, (byte)(c - '0' + NumberOfEnglishLetters * 2)), + _ => (false, default), + }; + } + + return success; + } + + private byte EncodeChar(char c) + { + if (!TryEncodeChar(c, out var b)) + { + ThrowHelper.ThrowBadSerializationInputException_UnsupportedMetaStringChar(c); + } + + return b; + } + + internal bool TryDecodeByte(byte b, out char c) + { + (var success, c) = b switch + { + < NumberOfEnglishLetters => (true, (char)(b + 'a')), + < NumberOfEnglishLetters * 2 => (true, (char)(b - NumberOfEnglishLetters + 'A')), + < NumberOfEnglishLetters * 2 + 10 => (true, (char)(b - NumberOfEnglishLetters * 2 + '0')), + MaxRepresentableChar - 1 => (true, specialChar1), + MaxRepresentableChar => (true, specialChar2), + _ => (false, default), + }; + + return success; + } + + private char DecodeByte(byte b) + { + if (!TryDecodeByte(b, out var c)) + { + ThrowHelper.ThrowBadDeserializationInputException_UnrecognizedMetaStringCodePoint(b); + } + + return c; + } + + public override Decoder GetDecoder() => new LowerUpperDigitSpecialDecoder(this); +} diff --git a/csharp/Fury/Meta/MetaString.cs b/csharp/Fury/Meta/MetaString.cs new file mode 100644 index 0000000000..91734f31bc --- /dev/null +++ b/csharp/Fury/Meta/MetaString.cs @@ -0,0 +1,137 @@ +using System; +using System.Buffers; +using System.Diagnostics.Contracts; +using System.Runtime.InteropServices; +using Fury.Helpers; + +namespace Fury.Meta; + +internal sealed class MetaString : IEquatable +{ + public const int SmallStringThreshold = sizeof(long) * 2; + private const int EncodingBitCount = 8; + + public ulong HashCode { get; } + public ulong HashCodeWithoutEncoding => BitHelper.ClearLowBits(HashCode, EncodingBitCount); + public string Value { get; } + public Encoding MetaEncoding { get; } + public char SpecialChar1 { get; } + public char SpecialChar2 { get; } + private readonly byte[] _bytes; + public ReadOnlySpan Bytes => new(_bytes); + public bool IsSmallString => _bytes.Length <= SmallStringThreshold; + + public MetaString(string value, Encoding metaEncoding, char specialChar1, char specialChar2, byte[] bytes) + { + Value = value; + MetaEncoding = metaEncoding; + SpecialChar1 = specialChar1; + SpecialChar2 = specialChar2; + _bytes = bytes; + HashCode = GetHashCode(_bytes, metaEncoding); + } + + [Pure] + public static Encoding GetEncodingFromHashCode(ulong hashCode) + { + return (Encoding)BitHelper.KeepLowBits(hashCode, EncodingBitCount); + } + + [Pure] + public static ulong GetHashCode(ReadOnlySpan bytes, Encoding metaEncoding) + { + HashHelper.MurmurHash3_x64_128(bytes, 47, out var hash, out _); + return GetHashCode(hash, metaEncoding); + } + + [Pure] + public static ulong GetHashCode(int length, ulong v1, ulong v2, Encoding metaEncoding) + { + Span bytes = stackalloc byte[SmallStringThreshold]; + var ulongSpan = MemoryMarshal.Cast(bytes); + ulongSpan[0] = v1; + ulongSpan[1] = v2; + bytes = bytes.Slice(0, length); + HashHelper.MurmurHash3_x64_128(bytes, 47, out var hash, out _); + return GetHashCode(hash, metaEncoding); + } + + [Pure] + public static ulong GetHashCode(ReadOnlySequence bytes, Encoding metaEncoding) + { + HashHelper.MurmurHash3_x64_128(bytes, 47, out var hash, out _); + return GetHashCode(hash, metaEncoding); + } + + [Pure] + public static ulong GetHashCodeWithoutEncoding(ReadOnlySequence bytes) + { + HashHelper.MurmurHash3_x64_128(bytes, 47, out var hash, out _); + return GetHashCodeWithoutEncoding(hash); + } + + [Pure] + private static ulong GetHashCode(ulong hash, Encoding metaEncoding) + { + if (hash == 0) + { + // Ensure hash is never 0 + // Last byte is reserved for header + hash += 0x100; + } + hash = BitHelper.ClearLowBits(hash, EncodingBitCount); + var header = (byte)metaEncoding; + hash |= header; + return hash; + } + + [Pure] + private static ulong GetHashCodeWithoutEncoding(ulong hash) + { + if (hash == 0) + { + // Ensure hash is never 0 + // Last byte is reserved for header + hash += 0x100; + } + hash = BitHelper.ClearLowBits(hash, EncodingBitCount); + return hash; + } + + [Pure] + public override int GetHashCode() => HashCode.GetHashCode(); + + [Pure] + public bool Equals(MetaString? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Value == other.Value + && MetaEncoding == other.MetaEncoding + && SpecialChar1 == other.SpecialChar1 + && SpecialChar2 == other.SpecialChar2; + } + + [Pure] + public override bool Equals(object? obj) + { + return ReferenceEquals(this, obj) || obj is MetaString other && Equals(other); + } + + public enum Encoding : byte + { + Utf8 = 0, + LowerSpecial = 1, + LowerUpperDigitSpecial = 2, + FirstToLowerSpecial = 3, + AllToLowerSpecial = 4, + } +} diff --git a/csharp/Fury/Meta/MetaStringBytes.cs b/csharp/Fury/Meta/MetaStringBytes.cs new file mode 100644 index 0000000000..8f5b2e76a2 --- /dev/null +++ b/csharp/Fury/Meta/MetaStringBytes.cs @@ -0,0 +1,22 @@ +using System; + +namespace Fury.Meta; + +internal sealed class MetaStringBytes +{ + private const byte EncodingMask = 0xff; + + private readonly byte[] _bytes; + private readonly long _hashCode; + private MetaString.Encoding _encoding; + + public long HashCode => _hashCode; + public ReadOnlySpan Bytes => _bytes; + + public MetaStringBytes(byte[] bytes, long hashCode) + { + _bytes = bytes; + _hashCode = hashCode; + _encoding = (MetaString.Encoding)(hashCode & EncodingMask); + } +} diff --git a/csharp/Fury/Meta/MetaStringDecoder.cs b/csharp/Fury/Meta/MetaStringDecoder.cs new file mode 100644 index 0000000000..56918298bb --- /dev/null +++ b/csharp/Fury/Meta/MetaStringDecoder.cs @@ -0,0 +1,175 @@ +using System; +using System.Buffers; +using System.Text; + +namespace Fury.Meta; + +// The StripLastChar flag need to be set in the first byte of the encoded data, +// so that wo can not implement a dotnet-style encoder. +// However, implementing a decoder is possible. + +internal abstract class MetaStringDecoder : Decoder +{ + internal bool StripLastChar { get; set; } + internal bool HasState { get; set; } + protected byte LeftoverBits { get; set; } + protected int LeftoverBitCount { get; set; } + internal bool MustFlush { get; private protected set; } + + internal bool HasLeftoverData => LeftoverBitCount > 0; + + internal void SetLeftoverData(byte bits, int bitCount) + { + LeftoverBits = (byte)(bits & ((1 << bitCount) - 1)); + LeftoverBitCount = bitCount; + } + + internal (byte bits, int bitCount) GetLeftoverData() + { + return (LeftoverBits, LeftoverBitCount); + } + + public override void Reset() + { + LeftoverBits = 0; + LeftoverBitCount = 0; + HasState = false; + StripLastChar = false; + MustFlush = false; + } + +#if !NET8_0_OR_GREATER + public abstract void Convert( + ReadOnlySpan bytes, + Span chars, + bool flush, + out int bytesUsed, + out int charsUsed, + out bool completed + ); + + public abstract int GetCharCount(ReadOnlySpan bytes, bool flush); + + public abstract int GetChars(ReadOnlySpan bytes, Span chars, bool flush); +#endif + + public sealed override unsafe void Convert( + byte* bytes, + int byteCount, + char* chars, + int charCount, + bool flush, + out int bytesUsed, + out int charsUsed, + out bool completed + ) + { + var byteSpan = new ReadOnlySpan(bytes, byteCount); + var charSpan = new Span(chars, charCount); + Convert(byteSpan, charSpan, flush, out bytesUsed, out charsUsed, out completed); + } + + public sealed override void Convert( + byte[] bytes, + int byteIndex, + int byteCount, + char[] chars, + int charIndex, + int charCount, + bool flush, + out int bytesUsed, + out int charsUsed, + out bool completed + ) + { + var byteSpan = new ReadOnlySpan(bytes, byteIndex, byteCount); + var charSpan = new Span(chars, charIndex, charCount); + Convert(byteSpan, charSpan, flush, out bytesUsed, out charsUsed, out completed); + } + + public void Convert( + ReadOnlySequence bytes, + Span chars, + bool flush, + out int bytesUsed, + out int charsUsed, + out bool completed + ) + { + var reader = new SequenceReader(bytes); + bytesUsed = 0; + charsUsed = 0; + var unwrittenChars = chars; + completed = reader.End; + while (!reader.End && unwrittenChars.Length > 0) + { + var currentSpan = reader.UnreadSpan; + var currentFlush = flush && reader.Remaining == currentSpan.Length; + Convert( + currentSpan, + chars, + currentFlush, + out var currentBytesUsed, + out var currentCharsUsed, + out completed + ); + bytesUsed += currentBytesUsed; + charsUsed += currentCharsUsed; + unwrittenChars = chars.Slice(charsUsed); + reader.Advance(currentBytesUsed); + } + } + + public sealed override unsafe int GetCharCount(byte* bytes, int count, bool flush) + { + return GetCharCount(new ReadOnlySpan(bytes, count), flush); + } + + public sealed override int GetCharCount(byte[] bytes, int index, int count) + { + return GetCharCount(bytes, index, count, true); + } + + public sealed override int GetCharCount(byte[] bytes, int index, int count, bool flush) + { + return GetCharCount(bytes.AsSpan(index, count), flush); + } + + public int GetCharCount(ReadOnlySequence bytes, bool flush) + { + var reader = new SequenceReader(bytes); + var charCount = 0; + while (!reader.End) + { + var currentSpan = reader.UnreadSpan; + var currentFlush = flush && reader.Remaining == currentSpan.Length; + charCount += GetCharCount(currentSpan, currentFlush); + reader.Advance(currentSpan.Length); + } + return charCount; + } + + public sealed override unsafe int GetChars(byte* bytes, int byteCount, char* chars, int charCount, bool flush) + { + var byteSpan = new ReadOnlySpan(bytes, byteCount); + var charSpan = new Span(chars, charCount); + return GetChars(byteSpan, charSpan, flush); + } + + public sealed override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) + { + return GetChars(bytes, byteIndex, byteCount, chars, charIndex, true); + } + + public sealed override int GetChars( + byte[] bytes, + int byteIndex, + int byteCount, + char[] chars, + int charIndex, + bool flush + ) + { + return GetChars(bytes.AsSpan(byteIndex, byteCount), chars.AsSpan(charIndex), flush); + } +} diff --git a/csharp/Fury/Meta/MetaStringEncoding.cs b/csharp/Fury/Meta/MetaStringEncoding.cs new file mode 100644 index 0000000000..f1de044af7 --- /dev/null +++ b/csharp/Fury/Meta/MetaStringEncoding.cs @@ -0,0 +1,259 @@ +using System; +using System.Text; +using Fury.Helpers; + +namespace Fury.Meta; + +internal abstract class MetaStringEncoding(MetaString.Encoding encoding) : Encoding +{ + protected const int BitsOfByte = sizeof(byte) * 8; + protected const int NumberOfEnglishLetters = 26; + protected const int StripLastCharFlagMask = 1 << (BitsOfByte - 1); + + public MetaString.Encoding Encoding { get; } = encoding; + + protected static void WriteByte(byte input, ref byte b1, int bitOffset, int bitsPerChar) + { + var unusedBitsPerChar = BitsOfByte - bitsPerChar; + b1 |= (byte)(input << (unusedBitsPerChar - bitOffset)); + } + + protected static void WriteByte(byte input, ref byte b1, ref byte b2, int bitOffset, int bitsPerChar) + { + var unusedBitsPerChar = BitsOfByte - bitsPerChar; + b1 |= (byte)(input >>> (bitOffset - unusedBitsPerChar)); + b2 |= (byte)(input << (BitsOfByte + unusedBitsPerChar - bitOffset)); + } + + protected static byte ReadByte(byte b1, int bitOffset, int bitsPerChar) + { + var bitMask = (1 << bitsPerChar) - 1; + var unusedBitsPerChar = BitsOfByte - bitsPerChar; + return (byte)((b1 >>> (unusedBitsPerChar - bitOffset)) & bitMask); + } + + protected static byte ReadByte(byte b1, byte b2, int bitOffset, int bitsPerChar) + { + var bitMask = (1 << bitsPerChar) - 1; + var unusedBitsPerChar = BitsOfByte - bitsPerChar; + return (byte)( + (b1 << (bitOffset - unusedBitsPerChar) | b2 >>> (BitsOfByte + unusedBitsPerChar - bitOffset)) & bitMask + ); + } + + protected static bool TryReadLeftOver( + MetaStringDecoder decoder, + ref BitsReader bitsReader, + int bitsPerChar, + out byte bits, + out int bitsUsedFromBitsReader + ) + { + if (!decoder.HasLeftoverData) + { + bits = default; + bitsUsedFromBitsReader = default; + return false; + } + var (leftOverBits, leftOverBitsCount) = decoder.GetLeftoverData(); + if (leftOverBitsCount >= bitsPerChar) + { + leftOverBitsCount -= bitsPerChar; + bits = BitHelper.KeepLowBits((byte)(leftOverBits >>> leftOverBitsCount), bitsPerChar); + leftOverBits = BitHelper.KeepLowBits(leftOverBits, leftOverBitsCount); + decoder.SetLeftoverData(leftOverBits, leftOverBitsCount); + bitsUsedFromBitsReader = 0; + return true; + } + + bitsUsedFromBitsReader = bitsPerChar - leftOverBitsCount; + if (!bitsReader.TryReadBits(bitsUsedFromBitsReader, out var bitsFromNextByte)) + { + bits = default; + bitsUsedFromBitsReader = 0; + return false; + } + + var bitsFromLeftOver = leftOverBits << bitsUsedFromBitsReader; + bits = BitHelper.KeepLowBits((byte)(bitsFromLeftOver | bitsFromNextByte), bitsPerChar); + decoder.SetLeftoverData(0, 0); + return true; + } + + internal static int GetCharCount(ReadOnlySpan bytes, int bitsPerChar, MetaStringDecoder decoder) + { + if (bytes.Length == 0) + { + return 0; + } + + var charCount = 0; + var currentBit = 0; + if (!decoder.HasState) + { + decoder.StripLastChar = (bytes[0] & StripLastCharFlagMask) != 0; + currentBit = 1; + } + + if (decoder.HasLeftoverData) + { + var (_, bitCount) = decoder.GetLeftoverData(); + currentBit += bitsPerChar - bitCount; + charCount++; + } + + var bitsAvailable = bytes.Length * BitsOfByte - currentBit; + var charsAvailable = bitsAvailable / bitsPerChar; + charCount += charsAvailable; + var leftOverBitCount = bitsAvailable % bitsPerChar; + decoder.SetLeftoverData(default, leftOverBitCount); + if (decoder is { MustFlush: true, StripLastChar: true }) + { + charCount--; + } + + return charCount; + } + +#if !NET8_0_OR_GREATER + public abstract int GetByteCount(ReadOnlySpan chars); + public abstract int GetCharCount(ReadOnlySpan bytes); + public abstract int GetBytes(ReadOnlySpan chars, Span bytes); + public abstract int GetChars(ReadOnlySpan bytes, Span chars); +#endif + + public +#if NET8_0_OR_GREATER + sealed override +#endif + bool TryGetBytes(ReadOnlySpan chars, Span bytes, out int bytesWritten) + { + var byteCount = GetByteCount(chars); + if (bytes.Length < byteCount) + { + bytesWritten = 0; + return false; + } + + bytesWritten = GetBytes(chars, bytes); + return true; + } + + public +#if NET8_0_OR_GREATER + sealed override +#endif + bool TryGetChars(ReadOnlySpan bytes, Span chars, out int charsWritten) + { + var charCount = GetCharCount(bytes); + if (chars.Length < charCount) + { + charsWritten = 0; + return false; + } + + charsWritten = GetChars(bytes, chars); + return true; + } + + public sealed override unsafe int GetByteCount(char* chars, int count) => + GetByteCount(new ReadOnlySpan(chars, count)); + + public sealed override int GetByteCount(char[] chars) => GetByteCount(chars.AsSpan()); + + public sealed override int GetByteCount(char[] chars, int index, int count) => + GetByteCount(chars.AsSpan(index, count)); + + public sealed override int GetByteCount(string s) => GetByteCount(s.AsSpan()); + + public sealed override unsafe int GetBytes(char* chars, int charCount, byte* bytes, int byteCount) => + GetBytes(new ReadOnlySpan(chars, charCount), new Span(bytes, byteCount)); + + public sealed override byte[] GetBytes(char[] chars) => GetBytes(chars, 0, chars.Length); + + public sealed override byte[] GetBytes(char[] chars, int index, int count) + { + var span = chars.AsSpan().Slice(index, count); + var byteCount = GetByteCount(chars); + var bytes = new byte[byteCount]; + GetBytes(span, bytes); + return bytes; + } + + public sealed override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) => + GetBytes(chars.AsSpan(charIndex, charCount), bytes.AsSpan(byteIndex)); + + public sealed override byte[] GetBytes(string s) + { + var span = s.AsSpan(); + var byteCount = GetByteCount(span); + var bytes = new byte[byteCount]; + GetBytes(span, bytes); + return bytes; + } + + public sealed override int GetBytes(string s, int charIndex, int charCount, byte[] bytes, int byteIndex) + { + var span = s.AsSpan().Slice(charIndex, charCount); + var byteCount = GetByteCount(span); + if (bytes.Length - byteIndex < byteCount) + { + ThrowHelper.ThrowArgumentException(paramName: nameof(bytes)); + } + return GetBytes(span, bytes.AsSpan(byteIndex)); + } + + public sealed override unsafe int GetCharCount(byte* bytes, int count) => + GetCharCount(new ReadOnlySpan(bytes, count)); + + public sealed override int GetCharCount(byte[] bytes) => GetCharCount(bytes.AsSpan()); + + public sealed override int GetCharCount(byte[] bytes, int index, int count) => + GetCharCount(bytes.AsSpan(index, count)); + + public sealed override unsafe int GetChars(byte* bytes, int byteCount, char* chars, int charCount) + { + var byteSpan = new ReadOnlySpan(bytes, byteCount); + var charSpan = new Span(chars, charCount); + return GetChars(byteSpan, charSpan); + } + + public sealed override char[] GetChars(byte[] bytes) + { + var charCount = GetCharCount(bytes); + var chars = new char[charCount]; + GetChars(bytes, chars); + return chars; + } + + public sealed override char[] GetChars(byte[] bytes, int index, int count) + { + var span = bytes.AsSpan(index, count); + var charCount = GetCharCount(span); + var chars = new char[charCount]; + GetChars(span, chars); + return chars; + } + + public sealed override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) + { + var byteSpan = bytes.AsSpan(byteIndex, byteCount); + var charSpan = chars.AsSpan(charIndex); + return GetChars(byteSpan, charSpan); + } + +#if !NET8_0_OR_GREATER + public string GetString(ReadOnlySpan bytes) + { + var charCount = GetCharCount(bytes); + Span chars = stackalloc char[charCount]; + GetChars(bytes, chars); + return chars.ToString(); + } +#endif + + public sealed override string GetString(byte[] bytes) => GetString(bytes.AsSpan()); + + public sealed override string GetString(byte[] bytes, int index, int count) => + GetString(bytes.AsSpan().Slice(index, count)); +} diff --git a/csharp/Fury/Meta/Metadatas.cs b/csharp/Fury/Meta/Metadatas.cs new file mode 100644 index 0000000000..8db44215c6 --- /dev/null +++ b/csharp/Fury/Meta/Metadatas.cs @@ -0,0 +1,42 @@ +namespace Fury.Meta; + +internal readonly record struct TypeMetadata(InternalTypeKind Kind, int Id) +{ + private const int TypeKindBits = 8; + private const uint TypeKindMask = (1u << TypeKindBits) - 1; + + public static TypeMetadata FromUint(uint value) + { + var kind = (InternalTypeKind)(value & TypeKindMask); + var id = (int)(value >>> TypeKindBits); + return new TypeMetadata(kind, id); + } + + public uint ToUint() + { + return (uint)Id << TypeKindBits | (uint)Kind; + } +} + +internal record struct RefMetadata(RefFlag RefFlag, int RefId = 0); + +internal enum RefFlag : sbyte +{ + Null = -3, + + /// + /// This flag indicates that object is a not-null value. + /// We don't use another byte to indicate REF, so that we can save one byte. + /// + Ref = -2, + + /// + /// this flag indicates that the object is a non-null value. + /// + NotNullValue = -1, + + /// + /// this flag indicates that the object is a referencable and first write. + /// + RefValue = 0, +} diff --git a/csharp/Fury/Meta/TypeKind.cs b/csharp/Fury/Meta/TypeKind.cs new file mode 100644 index 0000000000..7c3450828f --- /dev/null +++ b/csharp/Fury/Meta/TypeKind.cs @@ -0,0 +1,395 @@ +using System; + +namespace Fury.Meta; + +public enum TypeKind : byte +{ + /// + String = 12, + + /// + List = 27, + + /// + Set = 28, + + /// + Map = 29, + + /// + Duration = 30, + + /// + Timestamp = 31, + + /// + LocalDate = 32, + + /// + Binary = 34, + + /// + Array = 35, + + /// + BoolArray = 36, + + /// + Int8Array = 37, + + /// + Int16Array = 38, + + /// + Int32Array = 39, + + /// + Int64Array = 40, + + /// + Float16Array = 41, + + /// + Float32Array = 42, + + /// + Float64Array = 43, +} + +internal static class TypeKindHelper +{ + public static InternalTypeKind ToInternal(this TypeKind typeKind) + { + return (InternalTypeKind)(byte)typeKind; + } + + public static TypeKind SelectListTypeKind(Type elementType) + { + var typeCode = Type.GetTypeCode(elementType); + return typeCode switch + { + TypeCode.Boolean => TypeKind.BoolArray, + TypeCode.SByte => TypeKind.Int8Array, + TypeCode.Byte => TypeKind.Int8Array, + TypeCode.Int16 => TypeKind.Int16Array, + TypeCode.UInt16 => TypeKind.Int16Array, + TypeCode.Int32 => TypeKind.Int32Array, + TypeCode.UInt32 => TypeKind.Int32Array, + TypeCode.Int64 => TypeKind.Int64Array, + TypeCode.UInt64 => TypeKind.Int64Array, +#if NET5_0_OR_GREATER + _ when elementType == typeof(Half) => TypeKind.Float16Array, +#endif + TypeCode.Single => TypeKind.Float32Array, + TypeCode.Double => TypeKind.Float64Array, + _ => TypeKind.List + }; + } +} + +/// +/// Represents various data types used in the system. +/// +internal enum InternalTypeKind : byte +{ + /// + /// bool: a boolean value (true or false). + /// + Bool = 1, + + /// + /// int8: an 8-bit signed integer. + /// + Int8 = 2, + + /// + /// int16: a 16-bit signed integer. + /// + Int16 = 3, + + /// + /// int32: a 32-bit signed integer. + /// + Int32 = 4, + + /// + /// var_int32: a 32-bit signed integer which uses fury var_int32 encoding. + /// + VarInt32 = 5, + + /// + /// int64: a 64-bit signed integer. + /// + Int64 = 6, + + /// + /// var_int64: a 64-bit signed integer which uses fury PVL encoding. + /// + VarInt64 = 7, + + /// + /// sli_int64: a 64-bit signed integer which uses fury SLI encoding. + /// + SliInt64 = 8, + + /// + /// float16: a 16-bit floating point number. + /// + Float16 = 9, + + /// + /// float32: a 32-bit floating point number. + /// + Float32 = 10, + + /// + /// float64: a 64-bit floating point number including NaN and Infinity. + /// + Float64 = 11, + + /// + /// string: a text string encoded using Latin1/UTF16/UTF-8 encoding. + /// + String = 12, + + /// + /// enum: a data type consisting of a set of named values. + /// + Enum = 13, + + /// + /// named_enum: an enum whose value will be serialized as the registered name. + /// + NamedEnum = 14, + + /// + /// A morphic(sealed) type serialized by Fury Struct serializer. i.e. it doesn't have subclasses. + /// We can save dynamic serializer dispatch if target type is morphic(sealed). + /// + Struct = 15, + + /// + /// A morphic(sealed) type serialized by Fury compatible Struct serializer. + /// + CompatibleStruct = 16, + + /// + /// A whose type mapping will be encoded as a name. + /// + NamedStruct = 17, + + /// + /// A whose type mapping will be encoded as a name. + /// + NamedCompatibleStruct = 18, + + /// + /// A type which will be serialized by a customized serializer. + /// + Ext = 19, + + /// + /// An type whose type mapping will be encoded as a name. + /// + NamedExt = 20, + + /// + /// A sequence of objects. + /// + List = 21, + + /// + /// An unordered set of unique elements. + /// + Set = 22, + + /// + /// A map of key-value pairs. Mutable types such as `list/map/set/array/tensor/arrow` are not + /// allowed as key of map. + /// + Map = 23, + + /// + /// An absolute length of time, independent of any calendar/timezone, as a count of nanoseconds. + /// + Duration = 24, + + /// + /// A point in time, independent of any calendar/timezone, as a count of nanoseconds. The count is + /// relative to an epoch at UTC midnight on January 1, 1970. + /// + Timestamp = 25, + + /// + /// A naive date without timezone. The count is days relative to an epoch at UTC midnight on Jan 1, + /// 1970. + /// + LocalDate = 26, + + /// + /// Exact decimal value represented as an integer value in two's complement. + /// + Decimal = 27, + + /// + /// A variable-length array of bytes. + /// + Binary = 28, + + /// + /// A multidimensional array where every sub-array can have different sizes but all have the same + /// type. Only numeric components allowed. Other arrays will be taken as List. The implementation + /// should support interoperability between array and list. + /// + Array = 29, + + /// + /// One dimensional bool array. + /// + BoolArray = 30, + + /// + /// One dimensional int8 array. + /// + Int8Array = 31, + + /// + /// One dimensional int16 array. + /// + Int16Array = 32, + + /// + /// One dimensional int32 array. + /// + Int32Array = 33, + + /// + /// One dimensional int64 array. + /// + Int64Array = 34, + + /// + /// One dimensional half_float_16 array. + /// + Float16Array = 35, + + /// + /// One dimensional float32 array. + /// + Float32Array = 36, + + /// + /// One dimensional float64 array. + /// + Float64Array = 37, + + /// + /// An (arrow record batch) object. + /// + ArrowRecordBatch = 38, + + /// + /// An (arrow table) object. + /// + ArrowTable = 39, +} + +internal static class InternalTypeKindExtensions +{ + private const InternalTypeKind InvalidInternalTypeKind = 0; + private const TypeKind InvalidTypeKind = 0; + + public static bool IsStructType(this InternalTypeKind typeKind) + { + return typeKind switch + { + InternalTypeKind.Struct => true, + InternalTypeKind.CompatibleStruct => true, + InternalTypeKind.NamedStruct => true, + InternalTypeKind.NamedCompatibleStruct => true, + _ => false, + }; + } + + public static bool IsNamed(this InternalTypeKind typeKind) + { + return typeKind switch + { + InternalTypeKind.NamedEnum => true, + InternalTypeKind.NamedStruct => true, + InternalTypeKind.NamedCompatibleStruct => true, + InternalTypeKind.NamedExt => true, + _ => false, + }; + } + + public static bool IsCompatible(this InternalTypeKind typeKind) + { + return typeKind switch + { + InternalTypeKind.CompatibleStruct => true, + InternalTypeKind.NamedCompatibleStruct => true, + _ => false, + }; + } + + public static bool IsEnum(this InternalTypeKind typeKind) + { + return typeKind switch + { + InternalTypeKind.Enum => true, + InternalTypeKind.NamedEnum => true, + _ => false, + }; + } + + public static bool IsCustomSerialization(this InternalTypeKind typeKind) + { + return typeKind switch + { + InternalTypeKind.Ext => true, + InternalTypeKind.NamedExt => true, + _ => false, + }; + } + + public static bool TryToBeNamed(this InternalTypeKind typeKind, out InternalTypeKind namedTypeKind) + { + namedTypeKind = typeKind switch + { + InternalTypeKind.Enum => InternalTypeKind.NamedEnum, + InternalTypeKind.Struct => InternalTypeKind.NamedStruct, + InternalTypeKind.CompatibleStruct => InternalTypeKind.NamedCompatibleStruct, + InternalTypeKind.Ext => InternalTypeKind.NamedExt, + _ => InvalidInternalTypeKind, + }; + return namedTypeKind != InvalidInternalTypeKind; + } + + public static bool TryToBePublic(this InternalTypeKind internalTypeKind, out TypeKind typeKind) + { + typeKind = internalTypeKind switch + { + InternalTypeKind.String => TypeKind.String, + InternalTypeKind.List => TypeKind.List, + InternalTypeKind.Set => TypeKind.Set, + InternalTypeKind.Map => TypeKind.Map, + InternalTypeKind.Duration => TypeKind.Duration, + InternalTypeKind.Timestamp => TypeKind.Timestamp, + InternalTypeKind.LocalDate => TypeKind.LocalDate, + InternalTypeKind.Binary => TypeKind.Binary, + InternalTypeKind.Array => TypeKind.Array, + InternalTypeKind.BoolArray => TypeKind.BoolArray, + InternalTypeKind.Int8Array => TypeKind.Int8Array, + InternalTypeKind.Int16Array => TypeKind.Int16Array, + InternalTypeKind.Int32Array => TypeKind.Int32Array, + InternalTypeKind.Int64Array => TypeKind.Int64Array, + InternalTypeKind.Float16Array => TypeKind.Float16Array, + InternalTypeKind.Float32Array => TypeKind.Float32Array, + InternalTypeKind.Float64Array => TypeKind.Float64Array, + _ => InvalidTypeKind, + }; + + return typeKind != InvalidTypeKind; + } +} diff --git a/csharp/Fury/Meta/Utf8Encoding.cs b/csharp/Fury/Meta/Utf8Encoding.cs new file mode 100644 index 0000000000..af02643ca8 --- /dev/null +++ b/csharp/Fury/Meta/Utf8Encoding.cs @@ -0,0 +1,52 @@ +using System; +using System.Text; + +namespace Fury.Meta; + +internal sealed class Utf8Encoding() : MetaStringEncoding(MetaString.Encoding.Utf8) +{ + public static readonly Utf8Encoding Instance = new(); + + public override int GetMaxByteCount(int charCount) => UTF8.GetMaxByteCount(charCount); + + public override int GetMaxCharCount(int byteCount) => UTF8.GetMaxCharCount(byteCount); + + public override unsafe int GetByteCount(ReadOnlySpan chars) + { + fixed (char* p = chars) + { + return UTF8.GetByteCount(p, chars.Length); + } + } + + public override unsafe int GetCharCount(ReadOnlySpan bytes) + { + fixed (byte* p = bytes) + { + return UTF8.GetCharCount(p, bytes.Length); + } + } + + public override unsafe int GetBytes(ReadOnlySpan chars, Span bytes) + { + fixed (char* pChars = chars) + fixed (byte* pBytes = bytes) + { + return UTF8.GetBytes(pChars, chars.Length, pBytes, bytes.Length); + } + } + + public override unsafe int GetChars(ReadOnlySpan bytes, Span chars) + { + fixed (byte* pBytes = bytes) + fixed (char* pChars = chars) + { + return UTF8.GetChars(pBytes, bytes.Length, pChars, chars.Length); + } + } + + public override Encoder GetEncoder() + { + return UTF8.GetEncoder(); + } +} diff --git a/csharp/Fury/Serialization/AbstractSerializer.cs b/csharp/Fury/Serialization/AbstractSerializer.cs new file mode 100644 index 0000000000..5144e273f0 --- /dev/null +++ b/csharp/Fury/Serialization/AbstractSerializer.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Fury.Context; +using Fury.Helpers; + +namespace Fury.Serialization; + +public abstract class AbstractSerializer : ISerializer + where TTarget : notnull +{ + public abstract bool Serialize(SerializationWriter writer, in TTarget value); + + public virtual bool Serialize(SerializationWriter writer, object value) + { + ref var valueRef = ref ReferenceHelper.UnboxOrGetNullRef(value); + + bool completed; + if (Unsafe.IsNullRef(ref valueRef)) + { + completed = Serialize(writer, (TTarget)value); + } + else + { + completed = Serialize(writer, in valueRef); + } + + return completed; + } + + public abstract void Reset(); + + public virtual void Dispose() { } +} + +public abstract class AbstractDeserializer : IDeserializer + where TTarget : notnull +{ + public abstract object ReferenceableObject { get; } + + public abstract ReadValueResult Deserialize(DeserializationReader reader); + public abstract ValueTask> DeserializeAsync( + DeserializationReader reader, + CancellationToken cancellationToken = default + ); + + ReadValueResult IDeserializer.Deserialize(DeserializationReader reader) + { + var typedResult = Deserialize(reader); + if (!typedResult.IsSuccess) + { + return ReadValueResult.Failed; + } + return ReadValueResult.FromValue(typedResult.Value); + } + + async ValueTask> IDeserializer.DeserializeAsync( + DeserializationReader reader, + CancellationToken cancellationToken + ) + { + var typedResult = await DeserializeAsync(reader, cancellationToken); + if (!typedResult.IsSuccess) + { + return ReadValueResult.Failed; + } + return ReadValueResult.FromValue(typedResult.Value); + } + + public abstract void Reset(); + + public virtual void Dispose() { } + + [DoesNotReturn] + private protected static void ThrowInvalidOperationException_ObjectNotCreated() + { + throw new InvalidOperationException("Attempted to get the deserialized object before it was created."); + } + + private protected static object ThrowInvalidOperationException_AcyclicType() + { + throw new InvalidOperationException( + $"Attempted to get a referenceable object of type {typeof(TTarget).Name}. " + + $"This type should not contain circular references." + ); + } +} diff --git a/csharp/Fury/Serialization/CollectionSerializers.cs b/csharp/Fury/Serialization/CollectionSerializers.cs new file mode 100644 index 0000000000..88d950a654 --- /dev/null +++ b/csharp/Fury/Serialization/CollectionSerializers.cs @@ -0,0 +1,840 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Fury.Context; +using Fury.Helpers; +using Fury.Serialization.Meta; + +namespace Fury.Serialization; + +[Flags] +internal enum CollectionHeaderFlags : byte +{ + TrackingRef = 0b1, + HasNull = 0b10, + NotDeclElementType = 0b100, + NotSameType = 0b1000, +} + +// IReadOnlyCollection is not inherited from ICollection, so we use IEnumerable instead. + +public abstract class CollectionSerializer(TypeRegistration? elementRegistration = null) : AbstractSerializer + where TCollection : notnull +{ + private bool _hasWrittenHeader; + private bool _hasInitializedTypeMetaSerializer; + private CollectionHeaderFlags _writtenCollectionFlags; + + /// + /// Only used when elements are same type but not declared type. + /// + private TypeRegistration? _elementRegistration = elementRegistration; + private readonly bool _shouldResetElementRegistration = elementRegistration is null; + private TypeMetaSerializer _elementTypeMetaSerializer = new(); + private bool _hasWrittenCount; + + public override void Reset() + { + _hasWrittenHeader = false; + _writtenCollectionFlags = default; + _hasWrittenCount = false; + + if (_shouldResetElementRegistration) + { + _elementRegistration = null; + } + _hasInitializedTypeMetaSerializer = false; + } + + public sealed override bool Serialize(SerializationWriter writer, in TCollection value) + { + if (_elementRegistration is null && typeof(TElement).IsSealed) + { + _elementRegistration = writer.TypeRegistry.GetTypeRegistration(typeof(TElement)); + } + + var writerRef = writer.ByrefWriter; + WriteCount(ref writerRef, in value); + if (!_hasWrittenCount) + { + return false; + } + WriteElementsHeader(ref writerRef, in value, out var needWriteTypeMeta); + if (!_hasWrittenHeader) + { + return false; + } + + if (needWriteTypeMeta) + { + if (!WriteTypeMeta(ref writerRef)) + { + return false; + } + } + + return WriteElements(ref writerRef, in value); + } + + private void WriteElementsHeader(ref SerializationWriterRef writerRef, in TCollection collection, out bool needWriteTypeMeta) + { + needWriteTypeMeta = false; + if (typeof(TElement).IsValueType) + { + // For value types, all elements are the same as the declared type. + if (TypeHelper.IsNullable(typeof(TElement))) + { + // If the element type is nullable, we need to check if there are any null elements. + WriteNullabilityHeader(ref writerRef, in collection); + } + else + { + // If the element type is not nullable, we can write a header without any flags. + WriteHeaderFlags(ref writerRef, default); + } + + _elementRegistration = writerRef.TypeRegistry.GetTypeRegistration(typeof(TElement)); + } + var config = writerRef.Config; + if (typeof(TElement).IsSealed) + { + // For sealed reference types, all elements are the same as the declared type. + if (config.ReferenceTracking) + { + // RefFlag contains the nullability information, so we don't need to write HasNull flag here. + WriteHeaderFlags(ref writerRef, CollectionHeaderFlags.TrackingRef); + } + else + { + // If ReferenceTracking is disabled, we need to check if there are any null elements to determine if we need to write the HasNull flag. + WriteNullabilityHeader(ref writerRef, in collection); + } + _elementRegistration = writerRef.TypeRegistry.GetTypeRegistration(typeof(TElement)); + } + else + { + if (config.ReferenceTracking) + { + var flags = CollectionHeaderFlags.TrackingRef; + var checkResult = CheckElementsState(in collection, CollectionCheckOptions.TypeConsistency); + if (checkResult.ElementType is { } elementType) + { + _elementRegistration = writerRef.TypeRegistry.GetTypeRegistration(elementType); + if (elementType != typeof(TElement)) + { + flags |= CollectionHeaderFlags.NotDeclElementType; + needWriteTypeMeta = true; + } + } + else + { + // ElementType is null, which means elements are not the same type or all null. + flags |= CollectionHeaderFlags.NotSameType | CollectionHeaderFlags.NotDeclElementType; + } + + WriteHeaderFlags(ref writerRef, flags); + } + } + } + + private void WriteNullabilityHeader(ref SerializationWriterRef writerRef, in TCollection collection) + { + var checkResult = CheckElementsState(in collection, CollectionCheckOptions.Nullability); + var flags = checkResult.HasNull ? CollectionHeaderFlags.HasNull : default; + WriteHeaderFlags(ref writerRef, flags); + } + + /// + /// Depending on the , this method will check if the elements in the collection + /// contain null values or if they are of the same type. + /// + /// + /// The collection to check. + /// + /// + /// Which checks to perform. + /// + /// + /// A indicating the result of the checks. + /// + /// + protected virtual CollectionCheckResult CheckElementsState(in TCollection collection, CollectionCheckOptions options) + { + if (collection is not IEnumerable enumerable) + { + ThrowNotSupportedException_TCollectionNotSupported(); + return default; + } + + return CheckElementsState(enumerable.GetEnumerator(), options); + } + + /// + /// A default implementation of . + /// + /// + /// An enumerator for the collection to check. + /// + /// + /// Which checks to perform. + /// + /// + /// A indicating the result of the checks. + /// + protected CollectionCheckResult CheckElementsState(in TEnumerator enumerator, CollectionCheckOptions options) + where TEnumerator : IEnumerator + { + // We create this separate method to avoid boxing the enumerator. + + if ((options & CollectionCheckOptions.Nullability) != 0) + { + if ((options & CollectionCheckOptions.TypeConsistency) != 0) + { + return CheckElementsNullabilityAndTypeConsistency(enumerator); + } + + return CheckElementsNullability(enumerator); + } + + if ((options & CollectionCheckOptions.TypeConsistency) != 0) + { + return CheckElementsTypeConsistency(enumerator); + } + + Debug.Fail("Invalid options"); + return new CollectionCheckResult(true, null); + } + + private static CollectionCheckResult CheckElementsNullability(TEnumerator enumerator) + where TEnumerator : IEnumerator + { + if (typeof(TElement).IsValueType && !TypeHelper.IsNullable(typeof(TElement))) + { + return CollectionCheckResult.FromNullability(false); + } + + while (enumerator.MoveNext()) + { + if (enumerator.Current is null) + { + return CollectionCheckResult.FromNullability(true); + } + } + + return CollectionCheckResult.FromNullability(false); + } + + private static CollectionCheckResult CheckElementsTypeConsistency(TEnumerator enumerator) + where TEnumerator : IEnumerator + { + if (typeof(TElement).IsSealed) + { + return CollectionCheckResult.FromElementType(typeof(TElement)); + } + Type? elementType = null; + while (enumerator.MoveNext()) + { + var element = enumerator.Current; + if (element is null) + { + continue; + } + if (elementType is null) + { + elementType = element.GetType(); + } + else if (elementType != element.GetType()) + { + return CollectionCheckResult.FromElementType(null); + } + } + + return CollectionCheckResult.FromElementType(elementType ?? typeof(void)); + } + + private static CollectionCheckResult CheckElementsNullabilityAndTypeConsistency(TEnumerator enumerator) + where TEnumerator : IEnumerator + { + var hasNull = false; + var hasDifferentType = false; + if (typeof(TElement).IsSealed) + { + return CheckElementsNullability(enumerator); + } + + Type? elementType = null; + while (enumerator.MoveNext()) + { + var element = enumerator.Current; + if (element is null) + { + hasNull = true; + } + else + { + if (elementType is null) + { + elementType = element.GetType(); + } + else if (elementType != element.GetType()) + { + hasDifferentType = true; + } + } + + if (hasNull && hasDifferentType) + { + break; + } + } + + return new CollectionCheckResult(hasNull, hasDifferentType ? null : elementType); + } + + private void WriteHeaderFlags(ref SerializationWriterRef writerRef, CollectionHeaderFlags flags) + { + if (_hasWrittenHeader) + { + Debug.Assert(_writtenCollectionFlags == flags); + return; + } + + _hasWrittenHeader = writerRef.WriteUInt8((byte)flags); + _writtenCollectionFlags = flags; + } + + private bool WriteTypeMeta(ref SerializationWriterRef writerRef) + { + if (!_hasInitializedTypeMetaSerializer) + { + _elementTypeMetaSerializer.Initialize(writerRef.InnerWriter.MetaStringContext); + _hasInitializedTypeMetaSerializer = true; + } + return _elementTypeMetaSerializer.Write(ref writerRef, _elementRegistration!); + } + + private void WriteCount(ref SerializationWriterRef writerRef, in TCollection collection) + { + if (_hasWrittenCount) + { + return; + } + + var count = GetCount(in collection); + _hasWrittenCount = writerRef.Write7BitEncodedUInt32((uint)count); + } + + protected bool WriteElement(ref SerializationWriterRef writerRef, in TElement element) + { + ObjectMetaOption metaOption = default; + if ((_writtenCollectionFlags & (CollectionHeaderFlags.TrackingRef | CollectionHeaderFlags.HasNull)) != 0) + { + metaOption |= ObjectMetaOption.ReferenceMeta; + } + if ((_writtenCollectionFlags & CollectionHeaderFlags.NotSameType) != 0) + { + metaOption |= ObjectMetaOption.TypeMeta; + } + return writerRef.Write(element, metaOption, _elementRegistration); + } + + protected abstract bool WriteElements(ref SerializationWriterRef writerRef, in TCollection collection); + + protected abstract int GetCount(in TCollection collection); + + private void ThrowNotSupportedException_TCollectionNotSupported([CallerMemberName] string methodName = "") + { + throw new NotSupportedException($"The default implementation of {methodName} is not supported for {typeof(TCollection).Name}."); + } + + [Flags] + protected enum CollectionCheckOptions + { + Nullability = 0b1, + TypeConsistency = 0b10, + } + + protected readonly record struct CollectionCheckResult(bool HasNull, Type? ElementType) + { + public static CollectionCheckResult AllNullElements => new(true, null); + + public static CollectionCheckResult FromNullability(bool hasNull) => new(hasNull, null); + + public static CollectionCheckResult FromElementType(Type? elementType) => new(true, elementType); + } +} + +public abstract class CollectionDeserializer(TypeRegistration? elementRegistration = null) : AbstractDeserializer + where TCollection : notnull +{ + private bool _hasReadCount; + private CollectionHeaderFlags? _headerFlags; + private bool _hasInitializedTypeMetaDeserializer; + private readonly TypeMetaDeserializer _elementTypeMetaDeserializer = new(); + + protected TCollection? Collection; + private TypeRegistration? _elementRegistration = elementRegistration; + private readonly bool _shouldResetElementRegistration = elementRegistration is null; + + public override object ReferenceableObject + { + get + { + if (typeof(TCollection).IsValueType) + { + ThrowNotSupportedException_ValueTypeNotSupported(); + } + + return Collection!; + } + } + + public override void Reset() + { + _hasReadCount = false; + _headerFlags = null; + _hasInitializedTypeMetaDeserializer = false; + Collection = default; + if (_shouldResetElementRegistration) + { + _elementRegistration = null; + } + } + + public sealed override ReadValueResult Deserialize(DeserializationReader reader) + { + var task = Deserialize(reader, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public sealed override ValueTask> DeserializeAsync(DeserializationReader reader, CancellationToken cancellationToken = default) + { + return Deserialize(reader, true, cancellationToken); + } + + private async ValueTask> Deserialize(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken = default) + { + if (_elementRegistration is null && typeof(TElement).IsSealed) + { + _elementRegistration = reader.TypeRegistry.GetTypeRegistration(typeof(TElement)); + } + + if (!_hasReadCount) + { + var countResult = await reader.Read7BitEncodedUint(isAsync, cancellationToken); + if (!countResult.IsSuccess) + { + return ReadValueResult.Failed; + } + + _hasReadCount = true; + var count = (int)countResult.Value; + CreateCollection(count); + } + Debug.Assert(Collection is not null); + + if (!await ReadHeader(reader, isAsync, cancellationToken)) + { + return ReadValueResult.Failed; + } + + bool fillSuccess; + if (isAsync) + { + fillSuccess = await ReadElementsAsync(reader, cancellationToken); + } + else + { + // ReSharper disable once MethodHasAsyncOverloadWithCancellation + fillSuccess = ReadElements(reader); + } + + if (!fillSuccess) + { + return ReadValueResult.Failed; + } + return ReadValueResult.FromValue(Collection!); + } + + private async ValueTask ReadHeader(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (!await ReadHeaderFlags(reader, isAsync, cancellationToken)) + { + return false; + } + + if ((_headerFlags!.Value & CollectionHeaderFlags.NotSameType) == 0) + { + if (!await ReadTypeMeta(reader, isAsync, cancellationToken)) + { + return false; + } + } + + return true; + } + + private async ValueTask ReadHeaderFlags(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_headerFlags is not null) + { + return true; + } + + var readFlagsResult = await reader.ReadUInt8(isAsync, cancellationToken); + if (!readFlagsResult.IsSuccess) + { + return false; + } + + _headerFlags = (CollectionHeaderFlags)readFlagsResult.Value; + return true; + } + + private async ValueTask ReadTypeMeta(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (!_hasInitializedTypeMetaDeserializer) + { + _elementTypeMetaDeserializer.Initialize(reader.TypeRegistry, reader.MetaStringStorage, reader.MetaStringContext); + _hasInitializedTypeMetaDeserializer = true; + } + + var typeMetaResult = await _elementTypeMetaDeserializer.Read(reader, typeof(TElement), _elementRegistration, isAsync, cancellationToken); + if (!typeMetaResult.IsSuccess) + { + return false; + } + + _elementRegistration = typeMetaResult.Value; + return true; + } + + protected ReadValueResult ReadElement(DeserializationReader reader) + { + var task = ReadElement(reader, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + protected ValueTask> ReadElementAsync(DeserializationReader reader, CancellationToken cancellationToken = default) + { + return ReadElement(reader, true, cancellationToken); + } + + private protected async ValueTask> ReadElement(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_headerFlags is not { } headerFlags) + { + ThrowInvalidOperationException_HeaderNotRead(); + return ReadValueResult.Failed; + } + + ObjectMetaOption metaOption = default; + if ((headerFlags & (CollectionHeaderFlags.TrackingRef | CollectionHeaderFlags.HasNull)) != 0) + { + metaOption |= ObjectMetaOption.ReferenceMeta; + } + if ((headerFlags & CollectionHeaderFlags.NotSameType) != 0) + { + metaOption |= ObjectMetaOption.TypeMeta; + } + return await reader.Read(_elementRegistration, metaOption, isAsync, cancellationToken); + } + + protected abstract bool ReadElements(DeserializationReader reader); + + protected abstract ValueTask ReadElementsAsync(DeserializationReader reader, CancellationToken cancellationToken); + + [MemberNotNull(nameof(Collection))] + protected abstract void CreateCollection(int count); + + [DoesNotReturn] + private static void ThrowInvalidOperationException_HeaderNotRead() + { + throw new InvalidOperationException( + $"Header not read yet. Call {nameof(ReadElement)} in {nameof(ReadElements)} " + $"or {nameof(ReadElementAsync)} in {nameof(ReadElementsAsync)}." + ); + } + + [DoesNotReturn] + private static void ThrowNotSupportedException_ValueTypeNotSupported([CallerMemberName] string memberName = "") + { + throw new NotSupportedException( + $"{memberName}'s default implementation is not supported when {nameof(TCollection)} is a value type: {typeof(TCollection).Name}." + ); + } +} + +#region Built-in + +internal sealed class ListSerializer(TypeRegistration? elementRegistration) : CollectionSerializer>(elementRegistration) +{ + private int _currentIndex; + + public override void Reset() + { + base.Reset(); + _currentIndex = 0; + } + + protected override int GetCount(in List list) => list.Count; + + protected override bool WriteElements(ref SerializationWriterRef writerRef, in List collection) + { +#if NET5_0_OR_GREATER + var elements = CollectionsMarshal.AsSpan(collection); + for (; _currentIndex < elements.Length; _currentIndex++) + { + if (!WriteElement(ref writerRef, in elements[_currentIndex])) + { + return false; + } + } +#else + for (; _currentIndex < collection.Count; _currentIndex++) + { + if (!WriteElement(ref writerRef, collection[_currentIndex])) + { + return false; + } + } +#endif + return true; + } + + protected override CollectionCheckResult CheckElementsState(in List collection, CollectionCheckOptions options) + { + return base.CheckElementsState(collection.GetEnumerator(), options); + } +} + +internal sealed class ListDeserializer(TypeRegistration? elementRegistration) : CollectionDeserializer>(elementRegistration) +{ + private int _count; + private int _currentIndex; + public override object ReferenceableObject => Collection!; + + public override void Reset() + { + base.Reset(); + _count = 0; + _currentIndex = 0; + } + + protected override void CreateCollection(int count) + { + _count = count; + Collection = new List(count); + } + + protected override bool ReadElements(DeserializationReader reader) + { +#if NET8_0_OR_GREATER + CollectionsMarshal.SetCount(Collection!, _count); + var elements = CollectionsMarshal.AsSpan(Collection); +#else + var elements = Collection!; +#endif + for (; _currentIndex < _count; _currentIndex++) + { + var readResult = ReadElement(reader); + if (!readResult.IsSuccess) + { + return false; + } +#if NET8_0_OR_GREATER + elements[_currentIndex] = readResult.Value; +#else + elements.Add(readResult.Value); +#endif + } + + return true; + } + + protected override async ValueTask ReadElementsAsync(DeserializationReader reader, CancellationToken cancellationToken) + { + for (; _currentIndex < _count; _currentIndex++) + { + var readResult = await ReadElementAsync(reader, cancellationToken); + if (!readResult.IsSuccess) + { + return false; + } + + Collection!.Add(readResult.Value); + } + + return true; + } +} + +internal sealed class ArraySerializer(TypeRegistration? elementRegistration) : CollectionSerializer(elementRegistration) +{ + private int _currentIndex; + + public override void Reset() + { + base.Reset(); + _currentIndex = 0; + } + + protected override int GetCount(in TElement[] list) => list.Length; + + protected override bool WriteElements(ref SerializationWriterRef writerRef, in TElement[] collection) + { + for (; _currentIndex < collection.Length; _currentIndex++) + { + if (!WriteElement(ref writerRef, in collection[_currentIndex])) + { + return false; + } + } + + return true; + } +} + +internal sealed class ArrayDeserializer(TypeRegistration? elementRegistration) : CollectionDeserializer(elementRegistration) +{ + private int _currentIndex; + + public override object ReferenceableObject => Collection!; + + public override void Reset() + { + base.Reset(); + _currentIndex = 0; + } + + protected override void CreateCollection(int count) => Collection = new TElement[count]; + + protected override bool ReadElements(DeserializationReader reader) + { + var task = ReadElements(reader, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + protected override ValueTask ReadElementsAsync(DeserializationReader reader, CancellationToken cancellationToken) + { + return ReadElements(reader, true, cancellationToken); + } + + private async ValueTask ReadElements(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + for (; _currentIndex < Collection!.Length; _currentIndex++) + { + var readResult = await ReadElement(reader, isAsync, cancellationToken); + if (!readResult.IsSuccess) + { + return false; + } + + Collection[_currentIndex] = readResult.Value; + } + return true; + } +} + +internal sealed class HashSetSerializer(TypeRegistration? elementRegistration) + : CollectionSerializer>(elementRegistration) +{ + private bool _hasGottenEnumerator; + private HashSet.Enumerator _enumerator; + + public override void Reset() + { + base.Reset(); + _hasGottenEnumerator = false; + } + + protected override int GetCount(in HashSet set) => set.Count; + + protected override bool WriteElements(ref SerializationWriterRef writerRef, in HashSet collection) + { + var moveNextSuccess = true; + if (!_hasGottenEnumerator) + { + _enumerator = collection.GetEnumerator(); + _hasGottenEnumerator = true; + moveNextSuccess = _enumerator.MoveNext(); + } + + while (moveNextSuccess) + { + if (!WriteElement(ref writerRef, _enumerator.Current)) + { + return false; + } + moveNextSuccess = _enumerator.MoveNext(); + } + + return true; + } + + protected override CollectionCheckResult CheckElementsState(in HashSet collection, CollectionCheckOptions options) + { + return base.CheckElementsState(collection.GetEnumerator(), options); + } +} + +internal sealed class HashSetDeserializer(TypeRegistration? elementRegistration) + : CollectionDeserializer>(elementRegistration) +{ + private int _count; + + public override object ReferenceableObject => Collection!; + + public override void Reset() + { + base.Reset(); + _count = 0; + } + + protected override void CreateCollection(int count) + { + _count = count; +#if NETSTANDARD2_0 + Collection = []; +#else + Collection = new HashSet(count); +#endif + } + + protected override bool ReadElements(DeserializationReader reader) + { + var task = ReadElements(reader, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + protected override ValueTask ReadElementsAsync(DeserializationReader reader, CancellationToken cancellationToken) + { + return ReadElements(reader, true, cancellationToken); + } + + private async ValueTask ReadElements(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + while (Collection!.Count < _count) + { + var readResult = await ReadElement(reader, isAsync, cancellationToken); + if (!readResult.IsSuccess) + { + return false; + } + + Collection.Add(readResult.Value); + } + + return true; + } +} + +#endregion diff --git a/csharp/Fury/Serialization/DictionarySerializer.cs b/csharp/Fury/Serialization/DictionarySerializer.cs new file mode 100644 index 0000000000..21abec52e8 --- /dev/null +++ b/csharp/Fury/Serialization/DictionarySerializer.cs @@ -0,0 +1,9 @@ +using Fury.Context; + +namespace Fury.Serialization; + +public abstract class DictionarySerializer(TypeRegistration keyRegistration, TypeRegistration valueRegistration) : AbstractSerializer +where TDictionary : notnull +{ + +} diff --git a/csharp/Fury/Serialization/EnumSerializer.cs b/csharp/Fury/Serialization/EnumSerializer.cs new file mode 100644 index 0000000000..ff6799744b --- /dev/null +++ b/csharp/Fury/Serialization/EnumSerializer.cs @@ -0,0 +1,117 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Fury.Context; + +namespace Fury.Serialization; + +internal sealed class EnumSerializer : AbstractSerializer + where TEnum : unmanaged, Enum +{ + private static readonly int Size = Unsafe.SizeOf(); + + public override bool Serialize(SerializationWriter writer, in TEnum value) + { + // TODO: Serialize by name + + var v = value; + var underlyingValue64 = Size switch + { + sizeof(byte) => Unsafe.As(ref v), + sizeof(ushort) => Unsafe.As(ref v), + sizeof(uint) => Unsafe.As(ref v), + sizeof(ulong) => Unsafe.As(ref v), + _ => ThrowHelper.ThrowUnreachableException(), + }; + + if (underlyingValue64 > uint.MaxValue) + { + ThrowNotSupportedException_TooLong(); + } + return writer.Write7BitEncodedUInt32((uint)underlyingValue64); + } + + public override void Reset() { } + + [DoesNotReturn] + private static void ThrowNotSupportedException_TooLong() + { + throw new NotSupportedException( + $"Cannot serialize ${typeof(TEnum).Name} with value greater than {uint.MaxValue}" + ); + } +} + +internal sealed class EnumDeserializer : AbstractDeserializer + where TEnum : unmanaged, Enum +{ + private static readonly TypeCode UnderlyingTypeCode = Type.GetTypeCode(Enum.GetUnderlyingType(typeof(TEnum))); + + public override object ReferenceableObject => ThrowInvalidOperationException_AcyclicType(); + + public override ReadValueResult Deserialize(DeserializationReader reader) + { + var task = CreateAndFillInstance(reader, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public override async ValueTask> DeserializeAsync( + DeserializationReader reader, + CancellationToken cancellationToken = default + ) + { + return await CreateAndFillInstance(reader, true, cancellationToken); + } + + private static async ValueTask> CreateAndFillInstance( + DeserializationReader reader, + bool isAsync, + CancellationToken cancellationToken = default + ) + { + TEnum e; + var enumValueResult = await reader.Read7BitEncodedUint(isAsync, cancellationToken); + if (!enumValueResult.IsSuccess) + { + return ReadValueResult.Failed; + } + + var value = enumValueResult.Value; + + switch (UnderlyingTypeCode) + { + case TypeCode.Byte: + case TypeCode.SByte: + var byteValue = (byte)value; + e = Unsafe.As(ref byteValue); + break; + case TypeCode.UInt16: + case TypeCode.Int16: + var shortValue = (ushort)value; + e = Unsafe.As(ref shortValue); + break; + case TypeCode.UInt32: + case TypeCode.Int32: + e = Unsafe.As(ref value); + break; + + case TypeCode.UInt64: + case TypeCode.Int64: + var longValue = (ulong)value; + e = Unsafe.As(ref longValue); + break; + default: + e = default; + ThrowHelper.ThrowUnreachableException(); + break; + } + + return ReadValueResult.FromValue(e); + } + + public override void Reset() { } +} diff --git a/csharp/Fury/Serialization/ISerializationProvider.cs b/csharp/Fury/Serialization/ISerializationProvider.cs new file mode 100644 index 0000000000..8a1a23844b --- /dev/null +++ b/csharp/Fury/Serialization/ISerializationProvider.cs @@ -0,0 +1,20 @@ +using System; +using Fury.Context; +using Fury.Meta; + +namespace Fury.Serialization; + +// For specific types, such as generics, it may be difficult to determine the exact type +// at the time of writing the code, so we need a mechanism to allow users to dynamically +// provide type registration information. + +public interface ITypeRegistrationProvider +{ + TypeRegistration RegisterType(TypeRegistry registry, Type targetType); + + TypeRegistration GetTypeRegistration(TypeRegistry registry, TypeKind targetTypeKind, Type declaredType); + + TypeRegistration GetTypeRegistration(TypeRegistry registry, string? @namespace, string name); + + TypeRegistration GetTypeRegistration(TypeRegistry registry, int id); +} diff --git a/csharp/Fury/Serialization/ISerializer.cs b/csharp/Fury/Serialization/ISerializer.cs new file mode 100644 index 0000000000..e81f0e5249 --- /dev/null +++ b/csharp/Fury/Serialization/ISerializer.cs @@ -0,0 +1,169 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Fury.Context; + +namespace Fury.Serialization; + +// This interface is used to support polymorphism. +public interface ISerializer : IDisposable +{ + bool Serialize(SerializationWriter writer, object value); + + void Reset(); +} + +// It is very common that the data is not all available at once, so we need to read it asynchronously. +public interface IDeserializer : IDisposable +{ + /// + /// The object which is being deserialized. + /// + /// + /// + /// This property is used for circular dependency scenarios. + /// + /// + /// It will be called when circular dependency is detected. + /// For each deserialization process, this property can be called at most once. + /// + /// + /// The returned object should be the same as the returned value + /// of or . + /// + /// + public object ReferenceableObject { get; } + + // /// + // /// Try to create an instance of the object which will be deserialized. + // /// + // /// + // /// The reader which contains the state of the deserialization process. + // /// + // /// + // /// if the instance is created completely; otherwise, . + // /// + // /// + // ReadValueResult CreateInstance(DeserializationReader reader); + + // /// + // /// Try to read the serialized data and populate the given object. + // /// + // /// + // /// The reader which contains the state of the deserialization process. + // /// + // /// + // /// if the object is deserialized completely; otherwise, . + // /// + // /// + // bool FillInstance(DeserializationReader reader); + + ReadValueResult Deserialize(DeserializationReader reader); + + // /// + // /// Create an instance of the object which will be deserialized. + // /// + // /// + // /// The reader which contains the state of the deserialization process. + // /// + // /// + // /// The token to monitor for cancellation requests. + // /// + // /// + // /// An instance of the object which is not deserialized yet. + // /// + // /// + // /// + // /// This method is used to solve the circular reference problem. + // /// When deserializing an object which may be referenced by itself or its child objects, + // /// we need to create an instance before reading its fields. + // /// So that we can reference it before it is fully deserialized. + // /// + // /// + // /// You can read some necessary data from the reader to create the instance, e.g. the length of an array. + // /// + // /// + // /// If the object certainly does not have circular references, you can return a fully deserialized object + // /// and keep the method empty.
+ // /// Be careful that the default implementation of + // /// in use this method to create an instance.
+ // /// If you want to do all the deserialization here, it is recommended to override + // /// and call it in this method. + // ///
+ // ///
+ // /// + // ValueTask> CreateInstanceAsync(DeserializationReader reader, + // CancellationToken cancellationToken = default); + + // /// + // /// Read the serialized data and populate the given object. + // /// + // /// + // /// The reader which contains the state of the deserialization process. + // /// + // /// + // /// The token to monitor for cancellation requests. + // /// + // /// + // ValueTask FillInstanceAsync(DeserializationReader reader, + // CancellationToken cancellationToken = default); + + ValueTask> DeserializeAsync( + DeserializationReader reader, + CancellationToken cancellationToken = default + ); + + void Reset(); +} + +public interface ISerializer : ISerializer +{ + bool Serialize(SerializationWriter writer, in TTarget value); +} + +public interface IDeserializer : IDeserializer +{ + /// + /// Read the serialized data and create an instance of the object. + /// + /// + /// The reader which contains the state of the deserialization process. + /// + /// + /// The result of the read operation. + /// If is , + /// the deserialized value will be in . + /// Otherwise, the default value of will + /// be in . + /// + /// + /// This method is designed to avoid boxing and unboxing. + /// + /// + new ReadValueResult Deserialize(DeserializationReader reader); + + /// + /// Read the serialized data and create an instance of the object. + /// + /// + /// The reader which contains the state of the deserialization process. + /// + /// + /// The token to monitor for cancellation requests. + /// + /// + /// The result of the read operation. + /// If is , + /// the deserialized value will be in . + /// Otherwise, the default value of will + /// be in . + /// + /// + /// This method is designed to avoid boxing and unboxing. + /// + /// + new ValueTask> DeserializeAsync( + DeserializationReader reader, + CancellationToken cancellationToken = default + ); +} diff --git a/csharp/Fury/Serialization/Meta/FuryObjectSerializer.cs b/csharp/Fury/Serialization/Meta/FuryObjectSerializer.cs new file mode 100644 index 0000000000..ae77d58481 --- /dev/null +++ b/csharp/Fury/Serialization/Meta/FuryObjectSerializer.cs @@ -0,0 +1,6 @@ +namespace Fury.Serialization.Meta; + +internal sealed class FuryObjectSerializer +{ + +} diff --git a/csharp/Fury/Serialization/Meta/HeaderSerializer.cs b/csharp/Fury/Serialization/Meta/HeaderSerializer.cs new file mode 100644 index 0000000000..9dc28171a1 --- /dev/null +++ b/csharp/Fury/Serialization/Meta/HeaderSerializer.cs @@ -0,0 +1,163 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Fury.Context; +using Fury.Meta; + +namespace Fury.Serialization.Meta; + +[Flags] +file enum HeaderFlag : byte +{ + NullRootObject = 1, + LittleEndian = 1 << 1, + CrossLanguage = 1 << 2, + OutOfBand = 1 << 3, +} + +file static class HeaderHelper +{ + public const short MagicNumber = 0x62D4; +} + +internal sealed class HeaderSerializer +{ + private bool _hasWrittenMagicNumber; + private bool _hasWrittenHeaderFlag; + private bool _hasWrittenLanguage; + + public void Reset() + { + _hasWrittenMagicNumber = false; + _hasWrittenHeaderFlag = false; + _hasWrittenLanguage = false; + } + + public bool Write(ref SerializationWriterRef writerRef, bool rootObjectIsNull) + { + if (!_hasWrittenMagicNumber) + { + _hasWrittenMagicNumber = writerRef.WriteInt16(HeaderHelper.MagicNumber); + if (!_hasWrittenMagicNumber) + { + return false; + } + } + + if (!_hasWrittenHeaderFlag) + { + var flag = HeaderFlag.LittleEndian | HeaderFlag.CrossLanguage; + if (rootObjectIsNull) + { + flag |= HeaderFlag.NullRootObject; + } + + _hasWrittenHeaderFlag = writerRef.WriteUInt8((byte)flag); + if (!_hasWrittenMagicNumber) + { + return false; + } + } + + if (!_hasWrittenLanguage) + { + _hasWrittenLanguage = writerRef.WriteUInt8((byte)Language.Csharp); + } + + return _hasWrittenLanguage; + } +} + +internal sealed class HeaderDeserializer +{ + private bool _hasReadMagicNumber; + private bool _hasReadHeaderFlag; + private bool _rootObjectIsNull; + + public void Reset() + { + _hasReadMagicNumber = false; + _hasReadHeaderFlag = false; + _rootObjectIsNull = false; + } + + public async ValueTask> Read( + DeserializationReader reader, + bool isAsync, + CancellationToken cancellationToken + ) + { + if (!_hasReadMagicNumber) + { + var magicNumberResult = await reader.ReadInt16(isAsync, cancellationToken); + if (!magicNumberResult.IsSuccess) + { + return ReadValueResult.Failed; + } + + _hasReadMagicNumber = true; + if (magicNumberResult.Value is not HeaderHelper.MagicNumber) + { + ThrowBadDeserializationInputException_MagicNumberMismatch(); + } + } + + if (!_hasReadHeaderFlag) + { + var headerFlagResult = await reader.ReadUInt8(isAsync, cancellationToken); + if (!headerFlagResult.IsSuccess) + { + return ReadValueResult.Failed; + } + + _hasReadHeaderFlag = true; + var flag = (HeaderFlag)headerFlagResult.Value; + if ((flag & HeaderFlag.LittleEndian) == 0) + { + ThrowBadDeserializationInputException_BigEndianUnsupported(); + } + if ((flag & HeaderFlag.CrossLanguage) == 0) + { + ThrowBadDeserializationInputException_NonCrossLanguageUnsupported(); + } + + if ((flag & HeaderFlag.OutOfBand) != 0) + { + ThrowBadDeserializationInputException_OutOfBandUnsupported(); + } + _rootObjectIsNull = (flag & HeaderFlag.NullRootObject) != 0; + } + + return ReadValueResult.FromValue(_rootObjectIsNull); + } + + [DoesNotReturn] + private void ThrowBadDeserializationInputException_MagicNumberMismatch() + { + throw new BadDeserializationInputException( + $"The fury xlang serialization must start with magic number {HeaderHelper.MagicNumber:X}. " + + $"Please check whether the serialization is based on the xlang protocol and the data didn't corrupt." + ); + } + + [DoesNotReturn] + private void ThrowBadDeserializationInputException_BigEndianUnsupported() + { + throw new BadSerializationInputException("Non-Little-Endian format detected. Only Little-Endian is supported."); + } + + [DoesNotReturn] + private void ThrowBadDeserializationInputException_NonCrossLanguageUnsupported() + { + throw new BadSerializationInputException( + "Non-Cross-Language format detected. Only Cross-Language is supported." + ); + } + + [DoesNotReturn] + private void ThrowBadDeserializationInputException_OutOfBandUnsupported() + { + throw new BadSerializationInputException("Out-Of-Band format detected. Only In-Band is supported."); + } +} diff --git a/csharp/Fury/Serialization/Meta/MetaStringSerializer.cs b/csharp/Fury/Serialization/Meta/MetaStringSerializer.cs new file mode 100644 index 0000000000..9258681815 --- /dev/null +++ b/csharp/Fury/Serialization/Meta/MetaStringSerializer.cs @@ -0,0 +1,350 @@ +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Fury.Collections; +using Fury.Context; +using Fury.Meta; +using JetBrains.Annotations; + +namespace Fury.Serialization.Meta; + +internal sealed class MetaStringSerializer +{ + private int? _cachedMetaStringId; + private bool _shouldWriteId; + private bool _hasWrittenHeader; + private bool _hasWrittenHashCodeOrEncoding; + private int _writtenBytesCount; + private AutoIncrementIdDictionary _metaStringContext = null!; + + public void Reset() + { + _cachedMetaStringId = null; + _shouldWriteId = false; + _hasWrittenHeader = false; + _hasWrittenHashCodeOrEncoding = false; + _writtenBytesCount = 0; + } + + public void Initialize(AutoIncrementIdDictionary metaStringContext) + { + _metaStringContext = metaStringContext; + } + + [MustUseReturnValue] + public bool Write(ref SerializationWriterRef writerRef, MetaString metaString) + { + _cachedMetaStringId ??= _metaStringContext.GetOrAdd(metaString, out _shouldWriteId); + if (_shouldWriteId) + { + var header = MetaStringHeader.FromId(_cachedMetaStringId.Value); + WriteHeader(ref writerRef, header.Value); + return _hasWrittenHeader; + } + else + { + var length = metaString.Bytes.Length; + var header = MetaStringHeader.FromLength(length); + WriteHeader(ref writerRef, header.Value); + if (!_hasWrittenHeader) + { + return false; + } + if (metaString.IsSmallString) + { + WriteEncoding(ref writerRef, metaString.MetaEncoding); + } + else + { + WriteHashCode(ref writerRef, metaString.HashCode); + } + + if (!_hasWrittenHashCodeOrEncoding) + { + return false; + } + + return WriteMetaStringBytes(ref writerRef, metaString); + } + } + + private void WriteHeader(ref SerializationWriterRef writerRef, uint header) + { + if (_hasWrittenHeader) + { + return; + } + + _hasWrittenHeader = writerRef.Write7BitEncodedUInt32(header); + } + + private void WriteEncoding(ref SerializationWriterRef writerRef, MetaString.Encoding encoding) + { + if (_hasWrittenHashCodeOrEncoding) + { + return; + } + + _hasWrittenHashCodeOrEncoding = writerRef.WriteUInt8((byte)encoding); + } + + private void WriteHashCode(ref SerializationWriterRef writerRef, ulong hashCode) + { + if (_hasWrittenHashCodeOrEncoding) + { + return; + } + + _hasWrittenHashCodeOrEncoding = writerRef.WriteInt64(hashCode); + } + + private bool WriteMetaStringBytes(ref SerializationWriterRef writerRef, MetaString metaString) + { + var bytes = metaString.Bytes; + if (_writtenBytesCount == bytes.Length) + { + return true; + } + + var unwrittenBytes = bytes.Slice(_writtenBytesCount); + var writtenBytes = writerRef.WriteBytes(unwrittenBytes); + _writtenBytesCount += writtenBytes; + Debug.Assert(_writtenBytesCount <= bytes.Length); + return _writtenBytesCount == bytes.Length; + } +} + +internal struct MetaStringDeserializer(MetaStringStorage.EncodingPolicy encodingPolicy) +{ + private MetaStringStorage _sharedMetaStringStorage; + private MetaStringHeader? _header; + private ulong? _hashCode; + private MetaString.Encoding? _metaEncoding; + private MetaString? _metaString; + + private MetaStringStorage.MetaStringFactory? _cache; + private AutoIncrementIdDictionary _metaStringContext; + + public void Reset() + { + _header = null; + _hashCode = null; + _metaString = null; + } + + public void Initialize(MetaStringStorage metaStringStorage, AutoIncrementIdDictionary metaStringContext) + { + _sharedMetaStringStorage = metaStringStorage; + _metaStringContext = metaStringContext; + Reset(); + } + + public async ValueTask> Read(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_metaString is not null) + { + return ReadValueResult.FromValue(_metaString); + } + + await ReadHeader(reader, isAsync, cancellationToken); + if (_header is null) + { + return ReadValueResult.Failed; + } + + await ReadMetaString(reader, isAsync, cancellationToken); + if (_metaString is null) + { + return ReadValueResult.Failed; + } + + return ReadValueResult.FromValue(_metaString); + } + + private async ValueTask ReadHeader(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_header is not null) + { + return; + } + + ReadValueResult uintResult; + if (isAsync) + { + uintResult = await reader.Read7BitEncodedUintAsync(cancellationToken); + } + else + { + // ReSharper disable once MethodHasAsyncOverloadWithCancellation + uintResult = reader.Read7BitEncodedUint(); + } + + if (!uintResult.IsSuccess) + { + return; + } + + var header = new MetaStringHeader(uintResult.Value); + _header = header; + } + + private async ValueTask ReadMetaString(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_metaString is not null) + { + return; + } + + Debug.Assert(_header is not null); + var header = _header.Value; + if (header.IsId) + { + _metaString = GetMetaStringById(); + } + else + { + await ReadMetaStringBytes(reader, isAsync, cancellationToken); + } + } + + private MetaString GetMetaStringById() + { + var id = _header!.Value.Id; + if (!_metaStringContext.TryGetValue(id, out var metaString)) + { + ThrowHelper.ThrowBadDeserializationInputException_UnknownMetaStringId(id); + } + + return metaString; + } + + private async ValueTask ReadMetaStringBytes(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + var length = _header!.Value.Length; + ulong hashCode = 0; + MetaString.Encoding metaEncoding = default; + if (length > MetaString.SmallStringThreshold) + { + // big meta string + + await ReadHashCode(reader, isAsync, cancellationToken); + if (!_hashCode.HasValue) + { + return; + } + hashCode = _hashCode.Value; + } + else + { + // small meta string + + await ReadMetaEncoding(reader, isAsync, cancellationToken); + if (!_metaEncoding.HasValue) + { + return; + } + metaEncoding = _metaEncoding.Value; + } + + // Maybe we can use the hash code to get the meta string and skip reading the bytes + // if a meta string can be found by the hash code. + + var bytesResult = await reader.Read(length, isAsync, cancellationToken); + var buffer = bytesResult.Buffer; + var bufferLength = buffer.Length; + if (bufferLength < length) + { + reader.AdvanceTo(buffer.Start, buffer.End); + return; + } + + if (bufferLength > length) + { + buffer = buffer.Slice(0, length); + } + + if (length <= MetaString.SmallStringThreshold) + { + hashCode = MetaString.GetHashCode(buffer, metaEncoding); + } + + _metaString = _sharedMetaStringStorage.GetMetaString(hashCode, in buffer, encodingPolicy, ref _cache); + + reader.AdvanceTo(buffer.End); + } + + private async ValueTask ReadHashCode(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_hashCode is not null) + { + return; + } + + ReadValueResult ulongResult; + if (isAsync) + { + ulongResult = await reader.ReadUInt64Async(cancellationToken); + } + else + { + // ReSharper disable once MethodHasAsyncOverloadWithCancellation + ulongResult = reader.ReadUInt64(); + } + + if (ulongResult.IsSuccess) + { + _hashCode = ulongResult.Value; + } + } + + private async ValueTask ReadMetaEncoding(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_metaEncoding is not null) + { + return; + } + + ReadValueResult byteResult; + if (isAsync) + { + byteResult = await reader.ReadUInt8Async(cancellationToken); + } + else + { + // ReSharper disable once MethodHasAsyncOverloadWithCancellation + byteResult = reader.ReadUInt8(); + } + + if (byteResult.IsSuccess) + { + _metaEncoding = (MetaString.Encoding)byteResult.Value; + } + } +} + +internal readonly struct MetaStringHeader(uint value) +{ + public uint Value { get; } = value; + public bool IsId => (Value & 1) == 1; + public int Length + { + get + { + Debug.Assert(!IsId); + return (int)(Value >> 1); + } + } + + public int Id + { + get + { + Debug.Assert(IsId); + return (int)(Value >> 1) - 1; + } + } + + public static MetaStringHeader FromLength(int length) => new((uint)length << 1); + + public static MetaStringHeader FromId(int id) => new((uint)(id + 1) << 1 | 1); +} diff --git a/csharp/Fury/Serialization/Meta/ReferenceMetaSerializer.cs b/csharp/Fury/Serialization/Meta/ReferenceMetaSerializer.cs new file mode 100644 index 0000000000..95c54dbed3 --- /dev/null +++ b/csharp/Fury/Serialization/Meta/ReferenceMetaSerializer.cs @@ -0,0 +1,242 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Fury.Collections; +using Fury.Context; +using Fury.Meta; + +namespace Fury.Serialization.Meta; + +internal sealed class ReferenceMetaSerializer +{ + private bool _referenceTracking; + + private readonly HashSet _objectsBeingSerialized = []; + private readonly AutoIncrementIdDictionary _writtenRefIds = new(); + private bool _hasWrittenRefId; + private bool _hasWrittenRefFlag; + private RefMetadata? _cachedRefMetadata; + + public void Reset() + { + ResetCurrent(); + + _objectsBeingSerialized.Clear(); + _writtenRefIds.Clear(); + } + + public void Initialize(bool referenceTracking) + { + _referenceTracking = referenceTracking; + } + + public void ResetCurrent() + { + _hasWrittenRefId = false; + _hasWrittenRefFlag = false; + _cachedRefMetadata = null; + } + + public bool Write(ref SerializationWriterRef writerRef, in TTarget? value, out RefFlag writtenFlag) + { + if (value is null) + { + writtenFlag = RefFlag.Null; + WriteRefFlag(ref writerRef, writtenFlag); + return _hasWrittenRefFlag; + } + + if (typeof(TTarget).IsValueType) + { + // Objects declared as ValueType are not possible to be referenced + writtenFlag = RefFlag.NotNullValue; + + WriteRefFlag(ref writerRef, writtenFlag); + return _hasWrittenRefFlag; + } + + if (_referenceTracking) + { + if (_cachedRefMetadata is null) + { + var id = _writtenRefIds.GetOrAdd(value, out var exists); + var flag = exists ? RefFlag.Ref : RefFlag.RefValue; + _cachedRefMetadata = new RefMetadata(flag, id); + } + writtenFlag = _cachedRefMetadata.Value.RefFlag; + var refId = _cachedRefMetadata.Value.RefId; + WriteRefFlag(ref writerRef, writtenFlag); + if (!_hasWrittenRefFlag) + { + return false; + } + if (writtenFlag is RefFlag.Ref) + { + // A referenceable object has been recorded + if (_hasWrittenRefId) + { + // This should not happen, but if it does, nothing will be written. + Debug.Fail($"Redundant call to {nameof(Write)}."); + return true; + } + _hasWrittenRefId = writerRef.Write7BitEncodedUInt32((uint)refId); + return _hasWrittenRefId; + } + + // A new referenceable object + Debug.Assert(writtenFlag is RefFlag.RefValue); + return true; + } + + // Add the object to the set to mark it as being serialized. + // When reference tracking is disabled, the same object can be serialized multiple times, + // so we need a mechanism to detect circular dependencies. + + if (!_objectsBeingSerialized.Add(value)) + { + ThrowBadSerializationInputException_CircularDependencyDetected(); + } + + writtenFlag = RefFlag.NotNullValue; + WriteRefFlag(ref writerRef, writtenFlag); + return _hasWrittenRefFlag; + } + + private void WriteRefFlag(ref SerializationWriterRef writerRef, RefFlag flag) + { + if (_hasWrittenRefFlag) + { + return; + } + + _hasWrittenRefFlag = writerRef.WriteUInt64((sbyte)flag); + } + + public void HandleWriteValueCompleted(in TValue value) + { + if (!_referenceTracking && !typeof(TValue).IsValueType) + { + // Remove the object from the set to mark it as completed. + _objectsBeingSerialized.Remove(value!); + } + } + + [DoesNotReturn] + private static void ThrowBadSerializationInputException_CircularDependencyDetected() + { + throw new BadSerializationInputException("Circular dependency detected."); + } +} + +internal sealed class ReferenceMetaDeserializer +{ + private readonly AutoIncrementIdDictionary _readValues = new(); + private readonly AutoIncrementIdDictionary _inProgressDeserializers = new(); + private RefFlag? _refFlag; + private int? _refId; + + public void Reset() + { + ResetCurrent(); + _readValues.Clear(); + _inProgressDeserializers.Clear(); + } + + public void ResetCurrent() + { + _refFlag = null; + _refId = null; + } + + private async ValueTask ReadRefFlag(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_refFlag is not null) + { + return; + } + + var sbyteResult = await reader.ReadInt8(isAsync, cancellationToken); + if (!sbyteResult.IsSuccess) + { + return; + } + + _refFlag = (RefFlag)sbyteResult.Value; + } + + private async ValueTask ReadRefId(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_refId is not null) + { + return; + } + var uintResult = await reader.Read7BitEncodedUint(isAsync, cancellationToken); + + if (!uintResult.IsSuccess) + { + return; + } + _refId = (int)uintResult.Value; + } + + public async ValueTask> Read( + DeserializationReader reader, + bool isAsync, + CancellationToken cancellationToken + ) + { + await ReadRefFlag(reader, isAsync, cancellationToken); + switch (_refFlag) + { + case null: + return ReadValueResult.Failed; + case RefFlag.Null: + case RefFlag.NotNullValue: + case RefFlag.RefValue: + return ReadValueResult.FromValue(new RefMetadata(_refFlag.Value)); + case RefFlag.Ref: + await ReadRefId(reader, isAsync, cancellationToken); + if (_refId is not { } refId) + { + return ReadValueResult.Failed; + } + return ReadValueResult.FromValue(new RefMetadata(_refFlag.Value, refId)); + default: + return ThrowHelper.ThrowUnreachableException>(); + } + } + + public void GetReadValue(int refId, out object value) + { + if (_readValues.TryGetValue(refId, out value)) + { + return; + } + + if (!_inProgressDeserializers.TryGetValue(refId, out var deserializer)) + { + ThrowBadDeserializationInputException_ReferencedObjectNotFound(refId); + } + + value = deserializer.ReferenceableObject; + _readValues[refId] = value; + } + + public void AddReadValue(int refId, object value) + { + _readValues[refId] = value; + } + + public void AddInProgressDeserializer(int refId, IDeserializer deserializer) + { + _inProgressDeserializers[refId] = deserializer; + } + + [DoesNotReturn] + private static void ThrowBadDeserializationInputException_ReferencedObjectNotFound(int refId) + { + throw new BadDeserializationInputException($"Referenced object not found for ref kind '{refId}'."); + } +} diff --git a/csharp/Fury/Serialization/Meta/TypeMetaSerializer.cs b/csharp/Fury/Serialization/Meta/TypeMetaSerializer.cs new file mode 100644 index 0000000000..493e1cca1a --- /dev/null +++ b/csharp/Fury/Serialization/Meta/TypeMetaSerializer.cs @@ -0,0 +1,219 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Fury.Collections; +using Fury.Context; +using Fury.Helpers; +using Fury.Meta; + +namespace Fury.Serialization.Meta; + +internal sealed class TypeMetaSerializer +{ + private readonly MetaStringSerializer _nameMetaStringSerializer = new(); + private readonly MetaStringSerializer _namespaceMetaStringSerializer = new(); + + private bool _hasWrittenTypeKind; + + public void Reset() + { + _hasWrittenTypeKind = false; + _nameMetaStringSerializer.Reset(); + _namespaceMetaStringSerializer.Reset(); + } + + public void Initialize(AutoIncrementIdDictionary metaStringContext) + { + _nameMetaStringSerializer.Initialize(metaStringContext); + _namespaceMetaStringSerializer.Initialize(metaStringContext); + } + + public bool Write(ref SerializationWriterRef writerRef, TypeRegistration registration) + { + var typeKind = registration.InternalTypeKind; + WriteTypeKind(ref writerRef, typeKind); + if (!_hasWrittenTypeKind) + { + return false; + } + if (typeKind.IsNamed()) + { + if (!_namespaceMetaStringSerializer.Write(ref writerRef, registration.NamespaceMetaString!)) + { + return false; + } + + if (!_nameMetaStringSerializer.Write(ref writerRef, registration.NameMetaString!)) + { + return false; + } + } + return true; + } + + private void WriteTypeKind(ref SerializationWriterRef writerRef, InternalTypeKind typeKind) + { + if (_hasWrittenTypeKind) + { + return; + } + + _hasWrittenTypeKind = writerRef.Write7BitEncodedUInt32((uint)typeKind); + } +} + +internal sealed class TypeMetaDeserializer +{ + private MetaStringDeserializer _nameMetaStringDeserializer = new(MetaStringStorage.EncodingPolicy.Name); + private MetaStringDeserializer _namespaceMetaStringDeserializer = new(MetaStringStorage.EncodingPolicy.Namespace); + + private TypeRegistry _registry = null!; + + private TypeMetadata? _typeMetadata; + private MetaString? _namespaceMetaString; + private MetaString? _nameMetaString; + private TypeRegistration? _registration; + + public void Reset() + { + ResetCurrent(); + } + + public void Initialize(TypeRegistry registry, MetaStringStorage metaStringStorage, AutoIncrementIdDictionary metaStringContext) + { + _registry = registry; + _nameMetaStringDeserializer.Initialize(metaStringStorage, metaStringContext); + _namespaceMetaStringDeserializer.Initialize(metaStringStorage, metaStringContext); + Reset(); + } + + public void ResetCurrent() + { + _typeMetadata = null; + _namespaceMetaString = null; + _nameMetaString = null; + _registration = null; + _nameMetaStringDeserializer.Reset(); + _namespaceMetaStringDeserializer.Reset(); + } + + public async ValueTask> Read( + DeserializationReader reader, + Type declaredType, + TypeRegistration? registrationHint, + bool isAsync, + CancellationToken cancellationToken + ) + { + await ReadTypeMeta(reader, isAsync, cancellationToken); + if (_typeMetadata is not var (internalTypeKind, typeId)) + { + return ReadValueResult.Failed; + } + + if (internalTypeKind.TryToBePublic(out var typeKind)) + { + if (registrationHint is not null && registrationHint.TypeKind == typeKind && declaredType.IsAssignableFrom(registrationHint.TargetType)) + { + _registration = registrationHint; + } + else + { + _registration = _registry.GetTypeRegistration(typeKind, declaredType); + } + } + else + { + if (internalTypeKind.IsNamed()) + { + await ReadNamespaceMetaString(reader, isAsync, cancellationToken); + if (_namespaceMetaString is not { } namespaceMetaString) + { + return ReadValueResult.Failed; + } + await ReadNameMetaString(reader, isAsync, cancellationToken); + if (_nameMetaString is not { } nameMetaString) + { + return ReadValueResult.Failed; + } + + if ( + registrationHint is not null + && registrationHint.InternalTypeKind == internalTypeKind + && StringHelper.AreStringsEqualOrEmpty(registrationHint.Name, nameMetaString.Value) + && StringHelper.AreStringsEqualOrEmpty(registrationHint.Namespace, namespaceMetaString.Value) + && declaredType.IsAssignableFrom(registrationHint.TargetType) + ) + { + _registration = registrationHint; + } + else + { + _registration = _registry.GetTypeRegistration(namespaceMetaString.Value, nameMetaString.Value); + } + } + else + { + if ( + registrationHint is not null + && registrationHint.InternalTypeKind == internalTypeKind + && registrationHint.Id == typeId + && declaredType.IsAssignableFrom(registrationHint.TargetType) + ) + { + _registration = registrationHint; + } + else + { + _registration = _registry.GetTypeRegistration(typeId); + } + } + } + + return ReadValueResult.FromValue(_registration); + } + + private async ValueTask ReadTypeMeta(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_typeMetadata is not null) + { + return; + } + + var varIntResult = await reader.Read7BitEncodedUint(isAsync, cancellationToken); + if (!varIntResult.IsSuccess) + { + return; + } + + _typeMetadata = TypeMetadata.FromUint(varIntResult.Value); + } + + private async ValueTask ReadNamespaceMetaString(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_namespaceMetaString is not null) + { + return; + } + + var metaStringResult = await _namespaceMetaStringDeserializer.Read(reader, isAsync, cancellationToken); + if (metaStringResult.IsSuccess) + { + _namespaceMetaString = metaStringResult.Value; + } + } + + private async ValueTask ReadNameMetaString(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_nameMetaString is not null) + { + return; + } + + var metaStringResult = await _nameMetaStringDeserializer.Read(reader, isAsync, cancellationToken); + if (metaStringResult.IsSuccess) + { + _nameMetaString = metaStringResult.Value; + } + } +} diff --git a/csharp/Fury/Serialization/NotSupportedSerializer.cs b/csharp/Fury/Serialization/NotSupportedSerializer.cs new file mode 100644 index 0000000000..d37344eacb --- /dev/null +++ b/csharp/Fury/Serialization/NotSupportedSerializer.cs @@ -0,0 +1,75 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Fury.Context; + +namespace Fury.Serialization; + +public sealed class NotSupportedSerializer(Type targetType) : ISerializer +{ + public void Dispose() + { + ThrowNotSupportedException(); + } + + public bool Serialize(SerializationWriter writer, object value) + { + ThrowNotSupportedException(); + return false; + } + + public void Reset() + { + ThrowNotSupportedException(); + } + + [DoesNotReturn] + private void ThrowNotSupportedException() + { + throw new NotSupportedException($"Serialization of type {targetType} is not supported."); + } +} + +public sealed class NotSupportedDeserializer(Type targetType) : IDeserializer +{ + public void Dispose() + { + ThrowNotSupportedException(); + } + + public object ReferenceableObject + { + get + { + ThrowNotSupportedException(); + return null!; + } + } + + public ReadValueResult Deserialize(DeserializationReader reader) + { + ThrowNotSupportedException(); + return default; + } + + public ValueTask> DeserializeAsync( + DeserializationReader reader, + CancellationToken cancellationToken = default + ) + { + ThrowNotSupportedException(); + return default; + } + + public void Reset() + { + ThrowNotSupportedException(); + } + + [DoesNotReturn] + private void ThrowNotSupportedException() + { + throw new NotSupportedException($"Deserialization of type {targetType} is not supported."); + } +} diff --git a/csharp/Fury/Serialization/PrimitiveCollectionSerializers.cs b/csharp/Fury/Serialization/PrimitiveCollectionSerializers.cs new file mode 100644 index 0000000000..8287963d8f --- /dev/null +++ b/csharp/Fury/Serialization/PrimitiveCollectionSerializers.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Fury.Buffers; +using Fury.Context; +using Fury.Helpers; + +namespace Fury.Serialization; + +// For primitive arrays, the length is the byte size of the array rather than the number of elements. + +internal sealed class PrimitiveArraySerializer : AbstractSerializer + where TElement : unmanaged +{ + private bool _hasWrittenLength; + private int _writtenByteCount; + + public override void Reset() + { + _hasWrittenLength = false; + _writtenByteCount = 0; + } + + public override bool Serialize(SerializationWriter writer, in TElement[] value) + { + var writerRef = writer.ByrefWriter; + var bytes = MemoryMarshal.AsBytes(value.AsSpan()).Slice(_writtenByteCount); + var byteCount = bytes.Length; + if (_hasWrittenLength) + { + _hasWrittenLength = writerRef.Write7BitEncodedUInt32((uint)byteCount); + if (_hasWrittenLength) + { + return false; + } + } + + var buffer = writerRef.GetSpan(byteCount); + var consumed = bytes.CopyUpTo(buffer); + _writtenByteCount += consumed; + writerRef.Advance(consumed); + Debug.Assert(_writtenByteCount <= byteCount); + return _writtenByteCount == byteCount; + } +} + +internal sealed class PrimitiveArrayDeserializer : AbstractDeserializer + where TElement : unmanaged +{ + private static readonly int ElementSize = Unsafe.SizeOf(); + + private TElement[]? _array; + private int _readByteCount; + + public override void Reset() + { + _array = null; + _readByteCount = 0; + } + + public override object ReferenceableObject => ThrowInvalidOperationException_AcyclicType(); + + public override ReadValueResult Deserialize(DeserializationReader reader) + { + var task = Deserialize(reader, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public override ValueTask> DeserializeAsync(DeserializationReader reader, CancellationToken cancellationToken = default) + { + return Deserialize(reader, true, cancellationToken); + } + + private async ValueTask> Deserialize(DeserializationReader reader, bool isAsync, CancellationToken cancellationToken) + { + if (_array is null) + { + var byteCountResult = await reader.Read7BitEncodedUint(isAsync, cancellationToken); + if (!byteCountResult.IsSuccess) + { + return ReadValueResult.Failed; + } + + var byteCount = (int)byteCountResult.Value; + if (byteCount % ElementSize != 0) + { + ThrowBadDeserializationInputException_InvalidByteCount(byteCount); + return ReadValueResult.Failed; + } + + _array = new TElement[byteCount / ElementSize]; + } + + var totalByteCount = _array.Length * ElementSize; + + if (isAsync) + { + var memoryManager = new UnmanagedToByteArrayMemoryManager(_array); + var destination = memoryManager.Memory.Slice(_readByteCount); + _readByteCount += await reader.ReadBytesAsync(destination, cancellationToken); + } + else + { + var destination = MemoryMarshal.AsBytes(_array.AsSpan()).Slice(_readByteCount); + _readByteCount += reader.ReadBytes(destination); + } + Debug.Assert(_readByteCount <= totalByteCount); + + if (_readByteCount != totalByteCount) + { + return ReadValueResult.Failed; + } + + return ReadValueResult.FromValue(_array); + } + + [DoesNotReturn] + private static void ThrowBadDeserializationInputException_InvalidByteCount(int byteCount) + { + throw new BadDeserializationInputException($"Invalid byte count: {byteCount}. Expected a multiple of {ElementSize}."); + } +} + +#if NET5_0_OR_GREATER +internal sealed class PrimitiveListSerializer(TypeRegistration elementRegistration) + : CollectionSerializer>(elementRegistration) + where TElement : unmanaged +{ + private int _writtenByteCount; + + public override void Reset() + { + base.Reset(); + _writtenByteCount = 0; + } + + protected override int GetCount(in List collection) + { + return collection.Count; + } + + protected override bool WriteElements(ref SerializationWriterRef writerRef, in List collection) + { + var bytes = MemoryMarshal.AsBytes(CollectionsMarshal.AsSpan(collection)); + _writtenByteCount += writerRef.WriteBytes(bytes.Slice(_writtenByteCount)); + return _writtenByteCount == bytes.Length; + } + + protected override CollectionCheckResult CheckElementsState(in List collection, CollectionCheckOptions options) + { + return new CollectionCheckResult(false, typeof(TElement)); + } +} +#endif + +#if NET8_0_OR_GREATER +internal sealed class PrimitiveListDeserializer(TypeRegistration elementRegistration) + : CollectionDeserializer>(elementRegistration) + where TElement : unmanaged +{ + private static readonly int ElementSize = Unsafe.SizeOf(); + + private int _readByteCount; + private int _totalByteCount; + + private readonly UnmanagedToByteListMemoryManager _listMemoryManager = new(); + private Memory ByteMemory => _listMemoryManager.Memory; + + public override void Reset() + { + base.Reset(); + _readByteCount = 0; + _totalByteCount = 0; + } + + protected override void CreateCollection(int count) + { + _totalByteCount = count * ElementSize; + Collection = new List(count); + CollectionsMarshal.SetCount(Collection, count); + _listMemoryManager.List = Collection; + } + + protected override bool ReadElements(DeserializationReader reader) + { + var destination = MemoryMarshal.AsBytes(CollectionsMarshal.AsSpan(Collection)).Slice(_readByteCount); + _readByteCount += reader.ReadBytes(destination); + return _readByteCount == _totalByteCount; + } + + protected override async ValueTask ReadElementsAsync(DeserializationReader reader, CancellationToken cancellationToken) + { + var destination = ByteMemory.Slice(_readByteCount); + _readByteCount += await reader.ReadBytesAsync(destination, cancellationToken); + return _readByteCount == _totalByteCount; + } +} +#endif diff --git a/csharp/Fury/Serialization/PrimitiveSerializers.cs b/csharp/Fury/Serialization/PrimitiveSerializers.cs new file mode 100644 index 0000000000..5efa3962aa --- /dev/null +++ b/csharp/Fury/Serialization/PrimitiveSerializers.cs @@ -0,0 +1,45 @@ +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Fury.Context; + +namespace Fury.Serialization; + +internal sealed class PrimitiveSerializer : AbstractSerializer + where T : unmanaged +{ + public static PrimitiveSerializer Instance { get; } = new(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Serialize(SerializationWriter writer, in T value) + { + return writer.WriteUnmanaged(value); + } + + public override void Reset() { } +} + +internal sealed class PrimitiveDeserializer : AbstractDeserializer + where T : unmanaged +{ + public static PrimitiveDeserializer Instance { get; } = new(); + + private static readonly int Size = Unsafe.SizeOf(); + + public override object ReferenceableObject => ThrowInvalidOperationException_AcyclicType(); + + public override ReadValueResult Deserialize(DeserializationReader reader) + { + return reader.ReadUnmanagedAs(Size); + } + + public override async ValueTask> DeserializeAsync( + DeserializationReader reader, + CancellationToken cancellationToken = default + ) + { + return await reader.ReadUnmanagedAsAsync(Size, cancellationToken); + } + + public override void Reset() { } +} diff --git a/csharp/Fury/Serialization/Providers/ArraySerializationProvider.cs b/csharp/Fury/Serialization/Providers/ArraySerializationProvider.cs new file mode 100644 index 0000000000..e880af112f --- /dev/null +++ b/csharp/Fury/Serialization/Providers/ArraySerializationProvider.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Reflection; +using Fury.Context; +using Fury.Meta; + +namespace Fury.Serialization; + +internal static class ArraySerializationProvider +{ + private static readonly MethodInfo CreateArraySerializerMethod = typeof(ArraySerializationProvider).GetMethod( + nameof(CreateArraySerializer), + BindingFlags.NonPublic | BindingFlags.Static + )!; + + private static readonly MethodInfo CreateArrayDeserializerMethod = typeof(ArraySerializationProvider).GetMethod( + nameof(CreateArrayDeserializer), + BindingFlags.NonPublic | BindingFlags.Static + )!; + + public static bool TryGetType(TypeKind targetTypeKind, Type declaredType, [NotNullWhen(true)] out Type? targetType) + { + targetType = null; + if (!TryGetElementType(targetTypeKind, out var candidateElementTypes)) + { + return false; + } + + var arrayType = candidateElementTypes.Item1.MakeArrayType(); + if (!declaredType.IsAssignableFrom(arrayType)) + { + arrayType = candidateElementTypes.Item2?.MakeArrayType(); + if (!declaredType.IsAssignableFrom(arrayType)) + { + return false; + } + } + + targetType = arrayType; + return true; + } + + public static bool TryGetTypeKind(Type targetType, out TypeKind targetTypeKind) + { + if (!targetType.IsArray || targetType.GetArrayRank() > 1) + { + // Variable bound arrays are not supported yet. + targetTypeKind = default; + return false; + } + + if (targetType == typeof(byte[])) + { + targetTypeKind = TypeKind.Int8Array; + } + else if (targetType == typeof(sbyte[])) + { + targetTypeKind = TypeKind.Int8Array; + } + else if (targetType == typeof(short[])) + { + targetTypeKind = TypeKind.Int16Array; + } + else if (targetType == typeof(ushort[])) + { + targetTypeKind = TypeKind.Int16Array; + } + else if (targetType == typeof(int[])) + { + targetTypeKind = TypeKind.Int32Array; + } + else if (targetType == typeof(uint[])) + { + targetTypeKind = TypeKind.Int32Array; + } + else if (targetType == typeof(long[])) + { + targetTypeKind = TypeKind.Int64Array; + } + else if (targetType == typeof(ulong[])) + { + targetTypeKind = TypeKind.Int64Array; + } + else if (targetType == typeof(float[])) + { + targetTypeKind = TypeKind.Float32Array; + } + else if (targetType == typeof(double[])) + { + targetTypeKind = TypeKind.Float64Array; + } + else if (targetType == typeof(bool[])) + { + targetTypeKind = TypeKind.BoolArray; + } + else + { + var elementType = targetType.GetElementType(); + if (elementType is { IsArray: true }) + { + while (elementType is { IsArray: true }) + { + elementType = elementType.GetElementType(); + } + + if (elementType is { IsPrimitive: true }) + { + switch (Type.GetTypeCode(elementType)) + { + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.UInt16: + case TypeCode.Int32: + case TypeCode.UInt32: + case TypeCode.Int64: + case TypeCode.UInt64: + case TypeCode.Single: + case TypeCode.Double: + case TypeCode.Boolean: + targetTypeKind = TypeKind.Array; + return true; + } + } + } + + targetTypeKind = default; + return false; + } + + return true; + } + + [Pure] + private static bool TryGetElementType(Type targetType, [NotNullWhen(true)] out Type? candidateElementTypes) + { + if (!targetType.IsArray) + { + candidateElementTypes = null; + return false; + } + + candidateElementTypes = targetType.GetElementType(); + return candidateElementTypes is { IsGenericParameter: false }; + } + + [Pure] + private static bool TryGetElementType(TypeKind typeKind, out (Type, Type?) candidateElementTypes) + { + // TODO: Add support for TypeKind.Array + candidateElementTypes = typeKind switch + { + TypeKind.Int8Array => (typeof(byte), typeof(sbyte)), + TypeKind.Int16Array => (typeof(short), typeof(ushort)), + TypeKind.Int32Array => (typeof(int), typeof(uint)), + TypeKind.Int64Array => (typeof(long), typeof(ulong)), +#if NET8_0_OR_GREATER + TypeKind.Float16Array => (typeof(Half), null), +#endif + TypeKind.Float32Array => (typeof(float), null), + TypeKind.Float64Array => (typeof(double), null), + TypeKind.BoolArray => (typeof(bool), null), + _ => default, + }; + return candidateElementTypes is not (null, null); + } + + public static bool TryGetSerializerFactory(TypeRegistry registry, Type targetType, [NotNullWhen(true)] out Func? serializerFactory) + { + if (!TryGetElementType(targetType, out var elementType)) + { + serializerFactory = null; + return false; + } + + Func createMethod = + (Func) + CreateArraySerializerMethod.MakeGenericMethod(elementType).CreateDelegate(typeof(Func)); + + if (elementType.IsSealed) + { + var elementRegistration = registry.GetTypeRegistration(elementType); + serializerFactory = () => createMethod(elementRegistration); + } + else + { + serializerFactory = () => createMethod(null); + } + + return true; + } + + private static ISerializer CreateArraySerializer(TypeRegistration? elementRegistration) + where TElement : notnull + { + return new ArraySerializer(elementRegistration); + } + + private static bool TryGetDeserializerFactoryCommon( + TypeRegistry registry, + Type elementType, + [NotNullWhen(true)] out Func? deserializerFactory + ) + { + var createMethod = + (Func) + CreateArrayDeserializerMethod.MakeGenericMethod(elementType).CreateDelegate(typeof(Func)); + + if (elementType.IsSealed) + { + var elementRegistration = registry.GetTypeRegistration(elementType); + deserializerFactory = () => createMethod(elementRegistration); + } + else + { + deserializerFactory = () => createMethod(null); + } + + return true; + } + + public static bool TryGetDeserializerFactory(TypeRegistry registry, Type targetType, [NotNullWhen(true)] out Func? deserializerFactory) + { + if (!TryGetElementType(targetType, out var elementType)) + { + deserializerFactory = null; + return false; + } + + return TryGetDeserializerFactoryCommon(registry, elementType, out deserializerFactory); + } + + private static IDeserializer CreateArrayDeserializer(TypeRegistration? elementRegistration) + where TElement : notnull + { + return new ArrayDeserializer(elementRegistration); + } +} + +internal static class ArrayTypeRegistrationProvider +{ + // Supported types: + // CustomType[] + + // Unsupported types: + // any array with more than 1 dimension, e.g. CustomType[,] + // PrimitiveType[] (supported by builtin serializers and deserializers directly) + + private static readonly MethodInfo CreateArraySerializerMethod = typeof(ArraySerializationProvider).GetMethod( + nameof(CreateArraySerializer), + BindingFlags.NonPublic | BindingFlags.Static + )!; + + private static readonly MethodInfo CreateArrayDeserializerMethod = typeof(ArraySerializationProvider).GetMethod( + nameof(CreateArrayDeserializer), + BindingFlags.NonPublic | BindingFlags.Static + )!; + + public static bool TryRegisterType(TypeRegistry registry, Type targetType, [NotNullWhen(true)] out TypeRegistration? registration) + { + if (!TryGetElementType(targetType, out var elementType)) + { + registration = null; + return false; + } + return TryRegisterTypeCommon(registry, elementType, out registration); + } + + private static bool TryRegisterTypeCommon(TypeRegistry registry, Type elementType, [NotNullWhen(true)] out TypeRegistration? registration) + { + var serializerFactory = CreateArraySerializerMethod.MakeGenericMethod(elementType).CreateDelegate>(); + var deserializerFactory = CreateArrayDeserializerMethod.MakeGenericMethod(elementType).CreateDelegate>(); + + registration = registry.Register(elementType.MakeArrayType(), TypeKind.List, serializerFactory, deserializerFactory); + return true; + } + + private static bool TryGetElementType(Type type, [NotNullWhen(true)] out Type? elementType) + { + if (!type.IsArray) + { + elementType = null; + return false; + } + + if (type.GetArrayRank() > 1) + { + // Variable bound arrays are not supported yet. + elementType = null; + return false; + } + + elementType = type.GetElementType(); + return elementType is not null; + } + + private static bool TryGetElementTypeByDeclaredType(Type declaredType, [NotNullWhen(true)] out Type? elementType) + { + if (declaredType.IsArray) + { + if (declaredType.GetArrayRank() > 1) + { + // Variable bound arrays are not supported yet. + elementType = null; + return false; + } + + elementType = declaredType.GetElementType(); + return elementType is not null; + } + + var interfaces = declaredType.GetInterfaces(); + var genericEnumerableInterfaces = interfaces.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)).ToList(); + if (genericEnumerableInterfaces.Count > 1) + { + // Ambiguous type + elementType = null; + return false; + } + + if (genericEnumerableInterfaces.Count == 0) + { + var enumerableInterface = interfaces.FirstOrDefault(t => t == typeof(IEnumerable)); + if (enumerableInterface is not null) + { + elementType = typeof(object); + return true; + } + + elementType = null; + return false; + } + + elementType = genericEnumerableInterfaces[0].GenericTypeArguments[0]; + return true; + } + + private static bool TryMakeGenericCreateMethod( + Type elementType, + MethodInfo createMethod, + MethodInfo nullableCreateMethod, + [NotNullWhen(true)] out TDelegate? factory + ) + where TDelegate : Delegate + { + MethodInfo method; + if (Nullable.GetUnderlyingType(elementType) is { } underlyingType) + { +#if NET5_0_OR_GREATER + if (underlyingType.IsPrimitive || underlyingType == typeof(Half)) +#else + if (underlyingType.IsPrimitive) +#endif + { + // Fury does not support nullable primitive types + factory = null; + return false; + } + elementType = underlyingType; + method = createMethod; + } + else + { + method = nullableCreateMethod; + } + + factory = method.MakeGenericMethod(elementType).CreateDelegate(); + return true; + } + + private static ISerializer CreateArraySerializer(TypeRegistration? elementRegistration) + where TElement : notnull + { + return new ArraySerializer(elementRegistration); + } + + private static IDeserializer CreateArrayDeserializer(TypeRegistration? elementRegistration) + where TElement : notnull + { + return new ArrayDeserializer(elementRegistration); + } +} diff --git a/csharp/Fury/Serialization/Providers/BuiltInTypeRegistrationProvider.cs b/csharp/Fury/Serialization/Providers/BuiltInTypeRegistrationProvider.cs new file mode 100644 index 0000000000..714c19beec --- /dev/null +++ b/csharp/Fury/Serialization/Providers/BuiltInTypeRegistrationProvider.cs @@ -0,0 +1,118 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Fury.Context; +using Fury.Meta; + +namespace Fury.Serialization; + +public sealed class BuiltInTypeRegistrationProvider : ITypeRegistrationProvider +{ + public TypeRegistration RegisterType(TypeRegistry registry, Type targetType) + { + if (!TryRegisterType(registry, targetType, out var registration)) + { + ThrowNotSupportedException_TypeNotSupported(targetType); + } + + return registration; + } + + public TypeRegistration GetTypeRegistration(TypeRegistry registry, TypeKind targetTypeKind, Type declaredType) + { + if (!TryGetTypeRegistration(registry, targetTypeKind, declaredType, out var registration)) + { + ThrowNotSupportedException_DeclaredTypeNotSupported(declaredType, targetTypeKind); + } + + return registration; + } + + public TypeRegistration GetTypeRegistration(TypeRegistry registry, int id) + { + ThrowNotSupportedException_IdNotSupported(id); + return null; + } + + public TypeRegistration GetTypeRegistration(TypeRegistry registry, string? @namespace, string name) + { + if (!TryGetTypeRegistration(registry, @namespace, name, out var registration)) + { + ThrowNotSupportedException_NameNotSupported(@namespace, name); + } + + return registration; + } + + public static bool TryRegisterType(TypeRegistry registry, Type targetType, [NotNullWhen(true)] out TypeRegistration? registration) + { + if (EnumTypeRegistrationProvider.TryRegisterType(registry, targetType, out registration)) + { + return true; + } + + if (ArrayTypeRegistrationProvider.TryRegisterType(registry, targetType, out registration)) + { + return true; + } + + if (CollectionTypeRegistrationProvider.TryRegisterType(registry, targetType, out registration)) + { + return true; + } + + return false; + } + + public static bool TryGetTypeRegistration( + TypeRegistry registry, + TypeKind targetTypeKind, + Type declaredType, + [NotNullWhen(true)] out TypeRegistration? registration + ) + { + if (CollectionTypeRegistrationProvider.TryGetTypeRegistration(registry, targetTypeKind, declaredType, out registration)) + { + return true; + } + + return false; + } + + public static bool TryGetTypeRegistration(TypeRegistry registry, string? @namespace, string name, [NotNullWhen(true)] out TypeRegistration? registration) + { + if (EnumTypeRegistrationProvider.TryGetTypeRegistration(registry, @namespace, name, out registration)) + { + return true; + } + + return false; + } + + [DoesNotReturn] + private static void ThrowNotSupportedException_TypeNotSupported(Type targetType) + { + throw new NotSupportedException($"Type `{targetType}` is not supported by built-in type registration provider."); + } + + [DoesNotReturn] + private static void ThrowNotSupportedException_DeclaredTypeNotSupported(Type declaredType, TypeKind typeKind) + { + throw new NotSupportedException( + $"The exact type can not be determined with declared type `{declaredType}` and type kind `{typeKind}` by built-in type registration provider." + ); + } + + [DoesNotReturn] + private static void ThrowNotSupportedException_IdNotSupported(int id) + { + throw new NotSupportedException($"The type whose id is '{id}' is not supported by built-in type registration provider."); + } + + [DoesNotReturn] + private static void ThrowNotSupportedException_NameNotSupported(string? @namespace, string name) + { + throw new NotSupportedException( + $"The type whose namespace is `{@namespace}` and name is `{name}` is not supported by built-in type registration provider." + ); + } +} diff --git a/csharp/Fury/Serialization/Providers/CollectionTypeRegistrationProvider.cs b/csharp/Fury/Serialization/Providers/CollectionTypeRegistrationProvider.cs new file mode 100644 index 0000000000..cf3c029e59 --- /dev/null +++ b/csharp/Fury/Serialization/Providers/CollectionTypeRegistrationProvider.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Fury.Context; +using Fury.Helpers; +using Fury.Meta; + +namespace Fury.Serialization; + +internal static class CollectionTypeRegistrationProvider +{ + private static readonly MethodInfo CreateListSerializerMethodInfo = + typeof(CollectionTypeRegistrationProvider).GetMethod( + nameof(CreateListSerializer), + BindingFlags.NonPublic | BindingFlags.Static + )!; + + private static readonly MethodInfo CreateListDeserializerMethodInfo = + typeof(CollectionTypeRegistrationProvider).GetMethod( + nameof(CreateListDeserializer), + BindingFlags.NonPublic | BindingFlags.Static + )!; + + /// + /// + /// + /// Supported types: + /// + /// + /// + /// + public static bool TryRegisterType( + TypeRegistry registry, + Type targetType, + [NotNullWhen(true)] out TypeRegistration? registration + ) + { + if (!targetType.IsGenericType) + { + registration = null; + return false; + } + if (targetType.GetGenericTypeDefinition() != typeof(List<>)) + { + registration = null; + return false; + } + + var elementType = targetType.GetGenericArguments()[0]; + var createSerializer = CreateListSerializerMethodInfo + .MakeGenericMethod(elementType) + .CreateDelegate>(); + var createDeserializer = CreateListDeserializerMethodInfo + .MakeGenericMethod(elementType) + .CreateDelegate>(); + Func serializerFactory; + Func deserializerFactory; + if (elementType.IsSealed) + { + var elementRegistration = registry.GetTypeRegistration(elementType); + serializerFactory = () => createSerializer(elementRegistration); + deserializerFactory = () => createDeserializer(elementRegistration); + } + else + { + serializerFactory = () => createSerializer(null); + deserializerFactory = () => createDeserializer(null); + } + + var typeKind = TypeKindHelper.SelectListTypeKind(elementType); + registration = registry.Register(targetType, typeKind, serializerFactory, deserializerFactory); + return true; + } + + /// + /// + /// + /// Supported types: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static bool TryGetTypeRegistration( + TypeRegistry registry, + TypeKind targetTypeKind, + Type declaredType, + [NotNullWhen(true)] out TypeRegistration? registration + ) + { + if (!TryGetElementType(declaredType, out var elementType)) + { + registration = null; + return false; + } + + var listType = targetTypeKind switch + { + TypeKind.BoolArray when elementType is null || elementType == typeof(bool) => typeof(List), + TypeKind.Int8Array when elementType is null || elementType == typeof(byte) => typeof(List), + TypeKind.Int8Array when elementType == typeof(sbyte) => typeof(List), + TypeKind.Int16Array when elementType is null || elementType == typeof(short) => typeof(List), + TypeKind.Int16Array when elementType == typeof(ushort) => typeof(List), + TypeKind.Int32Array when elementType is null || elementType == typeof(int) => typeof(List), + TypeKind.Int32Array when elementType == typeof(uint) => typeof(List), + TypeKind.Int64Array when elementType is null || elementType == typeof(long) => typeof(List), + TypeKind.Int64Array when elementType == typeof(ulong) => typeof(List), +#if NET5_0_OR_GREATER + TypeKind.Float16Array when elementType is null || elementType == typeof(Half) => typeof(List), +#endif + TypeKind.Float32Array when elementType is null || elementType == typeof(float) => typeof(List), + TypeKind.Float64Array when elementType is null || elementType == typeof(double) => typeof(List), + TypeKind.List when elementType is not null => typeof(List<>).MakeGenericType(elementType), + _ => null, + }; + + if (listType is null) + { + registration = null; + return false; + } + + registration = registry.GetTypeRegistration(listType); + return true; + } + + private static bool TryGetElementType(Type declaredType, out Type? elementType) + { + if (TypeHelper.GetGenericBaseTypeArguments(declaredType, typeof(List<>), out var argumentTypes)) + { + elementType = argumentTypes[0]; + return true; + } + + if (TypeHelper.GetGenericBaseTypeArguments(declaredType, typeof(IList<>), out argumentTypes)) + { + elementType = argumentTypes[0]; + return true; + } + + if (TypeHelper.GetGenericBaseTypeArguments(declaredType, typeof(ICollection<>), out argumentTypes)) + { + elementType = argumentTypes[0]; + return true; + } + + if (TypeHelper.GetGenericBaseTypeArguments(declaredType, typeof(IEnumerable<>), out argumentTypes)) + { + elementType = argumentTypes[0]; + return true; + } + + if (TypeHelper.GetGenericBaseTypeArguments(declaredType, typeof(IReadOnlyList<>), out argumentTypes)) + { + elementType = argumentTypes[0]; + return true; + } + + if (TypeHelper.GetGenericBaseTypeArguments(declaredType, typeof(IReadOnlyCollection<>), out argumentTypes)) + { + elementType = argumentTypes[0]; + return true; + } + + if ( + typeof(IList).IsAssignableFrom(declaredType) + || typeof(ICollection).IsAssignableFrom(declaredType) + || typeof(IEnumerable).IsAssignableFrom(declaredType) + || declaredType == typeof(object) + ) + { + elementType = null; + return true; + } + + elementType = null; + return false; + } + + private static ISerializer CreateListSerializer(TypeRegistration? elementRegistration) + { + return new ListSerializer(elementRegistration); + } + + private static IDeserializer CreateListDeserializer(TypeRegistration? elementRegistration) + { + return new ListDeserializer(elementRegistration); + } +} diff --git a/csharp/Fury/Serialization/Providers/EnumTypeRegistrationProvider.cs b/csharp/Fury/Serialization/Providers/EnumTypeRegistrationProvider.cs new file mode 100644 index 0000000000..a7f114500f --- /dev/null +++ b/csharp/Fury/Serialization/Providers/EnumTypeRegistrationProvider.cs @@ -0,0 +1,70 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Fury.Context; + +namespace Fury.Serialization; + +internal static class EnumTypeRegistrationProvider +{ + private static MethodInfo CreateEnumSerializerMethod { get; } = + typeof(EnumTypeRegistrationProvider).GetMethod( + nameof(CreateEnumSerializer), + BindingFlags.NonPublic | BindingFlags.Static + )!; + + private static MethodInfo CreateEnumDeserializerMethod { get; } = + typeof(EnumTypeRegistrationProvider).GetMethod( + nameof(CreateEnumDeserializer), + BindingFlags.NonPublic | BindingFlags.Static + )!; + + private static ISerializer CreateEnumSerializer() + where TEnum : unmanaged, Enum + { + return new EnumSerializer(); + } + + private static IDeserializer CreateEnumDeserializer() + where TEnum : unmanaged, Enum + { + return new EnumDeserializer(); + } + + private static bool CanHandle(Type targetType) + { + return targetType.IsEnum; + } + + public static bool TryRegisterType( + TypeRegistry registry, + Type targetType, + [NotNullWhen(true)] out TypeRegistration? registration + ) + { + if (!CanHandle(targetType)) + { + registration = null; + return false; + } + var method = CreateEnumSerializerMethod.MakeGenericMethod(targetType); + var serializerFactory = (Func)method.CreateDelegate(typeof(Func)); + method = CreateEnumDeserializerMethod.MakeGenericMethod(targetType); + var deserializerFactory = (Func)method.CreateDelegate(typeof(Func)); + + registration = registry.Register(targetType, serializerFactory, deserializerFactory); + return true; + } + + public static bool TryGetTypeRegistration( + TypeRegistry registry, + string? @namespace, + string name, + [NotNullWhen(true)] out TypeRegistration? registration + ) + { + // TODO: Implement by-name serialization for enums + registration = null; + return false; + } +} diff --git a/csharp/Fury/Serialization/StringSerializer.cs b/csharp/Fury/Serialization/StringSerializer.cs new file mode 100644 index 0000000000..2ff7215e1d --- /dev/null +++ b/csharp/Fury/Serialization/StringSerializer.cs @@ -0,0 +1,295 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Fury.Context; + +namespace Fury.Serialization; + +public enum StringEncoding : byte +{ + Latin1 = 0, + + // ReSharper disable once InconsistentNaming + UTF16 = 1, + + // ReSharper disable once InconsistentNaming + UTF8 = 2, +} + +file static class StringSerializationHelper +{ + private const int EncodingBitCount = 2; + private const int EncodingMask = (1 << EncodingBitCount) - 1; + + internal static readonly Encoding Latin1 = Encoding.GetEncoding( + "ISO-8859-1", + EncoderFallback.ExceptionFallback, + DecoderFallback.ExceptionFallback + ); + + public static uint GetHeader(int length, StringEncoding encoding) + { + return (uint)((length << EncodingBitCount) | (byte)encoding); + } + + public static (int Length, StringEncoding encoding) GetLengthAndEncoding(uint header) + { + var encoding = (StringEncoding)(header & EncodingMask); + var length = (int)(header >> EncodingBitCount); + return (length, encoding); + } +} + +internal sealed class StringSerializer : AbstractSerializer +{ + private static readonly Encoding Latin1Encoding = StringSerializationHelper.Latin1; + private static readonly Encoding Utf16Encoding = Encoding.Unicode; + private static readonly Encoding Utf8Encoding = Encoding.UTF8; + + private readonly Encoder _latin1Encoder = Latin1Encoding.GetEncoder(); + private readonly Encoder _utf16Encoder = Utf16Encoding.GetEncoder(); + private readonly Encoder _utf8Encoder = Utf8Encoding.GetEncoder(); + + private Encoding? _encoding; + private Encoder? _encoder; + private StringEncoding _selectedStringEncoding; + private bool _hasWrittenHeader; + private bool _hasWrittenUtf16ByteCount; + private int _charsUsed; + private int _byteCount; + + public override void Reset() + { + _encoding = null; + _encoder = null; + _latin1Encoder.Reset(); + _utf16Encoder.Reset(); + _utf8Encoder.Reset(); + + _hasWrittenHeader = false; + _hasWrittenUtf16ByteCount = false; + _charsUsed = 0; + _byteCount = 0; + } + + public override bool Serialize(SerializationWriter writer, in string value) + { + var config = writer.Config; + var writerRef = writer.ByrefWriter; + if (_encoding is null) + { + // If no preferred encoding is set, we default to UTF8 + _encoding = Utf8Encoding; + _selectedStringEncoding = StringEncoding.UTF8; + foreach (var preferredEncoding in config.PreferredStringEncodings) + { + (_encoding, _encoder, _selectedStringEncoding) = preferredEncoding switch + { + StringEncoding.Latin1 => (Latin1Encoding, _latin1Encoder, StringEncoding.Latin1), + StringEncoding.UTF16 => (Utf16Encoding, _utf16Encoder, StringEncoding.UTF16), + _ => (Utf8Encoding, _utf8Encoder, StringEncoding.UTF8), + }; + try + { + _byteCount = _encoding.GetByteCount(value); + _encoder = _encoding.GetEncoder(); + } + catch (EncoderFallbackException) { } + } + } + + WriteHeader(ref writerRef, value); + WriteUtf8ByteCount(ref writerRef); + WriteStringBytes(ref writerRef, value); + + return _charsUsed == value.Length; + } + + private void WriteHeader(ref SerializationWriterRef writerRef, string value) + { + if (_hasWrittenHeader) + { + return; + } + + var config = writerRef.Config; + int length; + if (_selectedStringEncoding is StringEncoding.UTF8 && config.WriteUtf16ByteCountForUtf8Encoding) + { + // When WriteUtf16ByteCountForUtf8Encoding is true, + // length contained in the header represents the byte length of the UTF-16 string. + // This is redundant with the byte count written after the header, + // but we can use this to create a string without allocating a temporary buffer. + length = value.Length * sizeof(char); + } + else + { + // When WriteUtf16ByteCountForUtf8Encoding is false, + // length contained in the header represents the byte length of the selected encoding. + length = _byteCount; + } + var header = StringSerializationHelper.GetHeader(length, _selectedStringEncoding); + _hasWrittenHeader = writerRef.Write7BitEncodedUInt32(header); + } + + private void WriteUtf8ByteCount(ref SerializationWriterRef writerRef) + { + if (_hasWrittenUtf16ByteCount) + { + return; + } + + if (_selectedStringEncoding is StringEncoding.UTF8 && writerRef.Config.WriteUtf16ByteCountForUtf8Encoding) + { + // When WriteUtf16ByteCountForUtf8Encoding is true, + // the true byte length of the UTF-8 string is written as Int32 after the header. + _hasWrittenUtf16ByteCount = writerRef.WriteInt32(_byteCount); + } + else + { + _hasWrittenUtf16ByteCount = true; + } + } + + private void WriteStringBytes(ref SerializationWriterRef writerRef, string value) + { + while (_charsUsed < value.Length) + { + var charSpan = value.AsSpan().Slice(_charsUsed); + var buffer = writerRef.GetSpan(); + if (buffer.Length == 0) + { + return; + } + + _encoder!.Convert(charSpan, buffer, true, out var currentCharsUsed, out var currentBytesUsed, out _); + + _charsUsed += currentCharsUsed; + writerRef.Advance(currentBytesUsed); + } + } +} + +internal sealed class StringDeserializer : AbstractDeserializer +{ + private readonly Decoder _latin1Decoder = StringSerializationHelper.Latin1.GetDecoder(); + private readonly Decoder _utf16Decoder = Encoding.Unicode.GetDecoder(); + private readonly Decoder _utf8Decoder = Encoding.UTF8.GetDecoder(); + + private bool _hasReadHeader; + private StringEncoding _encoding; + private int _byteCount; + private int? _charCount; + + private int _bytesUsed; + private readonly ArrayBufferWriter _charBuffer = new(); + + public override object ReferenceableObject => ThrowInvalidOperationException_AcyclicType(); + + public override void Reset() + { + _hasReadHeader = false; + _bytesUsed = 0; + _charBuffer.Clear(); + } + + private Decoder SelectDecoder(StringEncoding encoding) + { + return encoding switch + { + StringEncoding.Latin1 => _latin1Decoder, + StringEncoding.UTF16 => _utf16Decoder, + _ => _utf8Decoder, + }; + } + + public override ReadValueResult Deserialize(DeserializationReader reader) + { + var task = CreateAndFillInstance(reader, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public override ValueTask> DeserializeAsync( + DeserializationReader reader, + CancellationToken cancellationToken = default + ) + { + return CreateAndFillInstance(reader, true, cancellationToken); + } + + private async ValueTask> CreateAndFillInstance( + DeserializationReader reader, + bool isAsync, + CancellationToken cancellationToken + ) + { + var config = reader.Config; + if (!_hasReadHeader) + { + var headerResult = await reader.Read7BitEncodedUint(isAsync, cancellationToken); + if (!headerResult.IsSuccess) + { + return ReadValueResult.Failed; + } + + (_byteCount, _encoding) = StringSerializationHelper.GetLengthAndEncoding(headerResult.Value); + _hasReadHeader = true; + } + + if (config.ReadUtf16ByteCountForUtf8Encoding && _encoding is StringEncoding.UTF8) + { + if (_charCount is null) + { + _charCount = _byteCount / sizeof(char); + _charBuffer.GetSpan(_charCount.Value); + var utf8ByteCountResult = await reader.ReadInt32(isAsync, cancellationToken); + if (!utf8ByteCountResult.IsSuccess) + { + return ReadValueResult.Failed; + } + _byteCount = utf8ByteCountResult.Value; + } + } + + var decoder = SelectDecoder(_encoding); + while (_bytesUsed < _byteCount) + { + var requiredLength = _byteCount - _bytesUsed; + var readResult = await reader.Read(requiredLength, isAsync, cancellationToken); + var buffer = readResult.Buffer; + if (buffer.Length == 0) + { + return ReadValueResult.Failed; + } + + if (buffer.Length > requiredLength) + { + buffer = buffer.Slice(0, requiredLength); + } + + var bufferReader = new SequenceReader(buffer); + while (!bufferReader.End) + { + var unwrittenChars = _charBuffer.GetSpan(); + var unreadBytes = bufferReader.UnreadSpan; + var flush = bufferReader.Remaining == unreadBytes.Length; + decoder.Convert(unreadBytes, unwrittenChars, flush, out var bytesUsed, out var charsUsed, out _); + _bytesUsed += bytesUsed; + _charBuffer.Advance(charsUsed); + bufferReader.Advance(bytesUsed); + } + } + + if (_bytesUsed != _byteCount) + { + return ReadValueResult.Failed; + } + + var str = _charBuffer.WrittenSpan.ToString(); + return ReadValueResult.FromValue(str); + } +} diff --git a/csharp/Fury/Serialization/TimeSerializers.cs b/csharp/Fury/Serialization/TimeSerializers.cs new file mode 100644 index 0000000000..379c1fd319 --- /dev/null +++ b/csharp/Fury/Serialization/TimeSerializers.cs @@ -0,0 +1,361 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Fury.Context; + +namespace Fury.Serialization; + +public abstract class TimeSpanSerializer : AbstractSerializer + where TTimeSpan : notnull +{ + private bool _hasWrittenSecond; + private bool _hasWrittenNanosecond; + + public sealed override void Reset() + { + _hasWrittenSecond = false; + _hasWrittenNanosecond = false; + } + + public sealed override bool Serialize(SerializationWriter writer, in TTimeSpan value) + { + var (seconds, nanoseconds) = GetSecondsAndNanoseconds(value); + var writerRef = writer.ByrefWriter; + if (!_hasWrittenSecond) + { + _hasWrittenSecond = writerRef.WriteUInt64(seconds); + if (!_hasWrittenSecond) + { + return false; + } + } + + if (!_hasWrittenNanosecond) + { + _hasWrittenNanosecond = writerRef.WriteInt32(nanoseconds); + if (!_hasWrittenNanosecond) + { + return false; + } + } + + return true; + } + + protected abstract (long Seconds, int Nanoseconds) GetSecondsAndNanoseconds(in TTimeSpan value); +} + +public abstract class TimeSpanDeserializer : AbstractDeserializer + where TTimeSpan : notnull +{ + public sealed override object ReferenceableObject => ThrowInvalidOperationException_AcyclicType(); + + private long? _seconds; + private int? _nanoseconds; + + public sealed override void Reset() + { + _seconds = null; + _nanoseconds = null; + } + + public sealed override ReadValueResult Deserialize(DeserializationReader reader) + { + var task = Deserialize(reader, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public sealed override ValueTask> DeserializeAsync( + DeserializationReader reader, + CancellationToken cancellationToken = default + ) + { + return Deserialize(reader, true, cancellationToken); + } + + private async ValueTask> Deserialize( + DeserializationReader reader, + bool isAsync, + CancellationToken cancellationToken + ) + { + if (_seconds is null) + { + var secondResult = await reader.ReadInt64(isAsync, cancellationToken); + if (!secondResult.IsSuccess) + { + return ReadValueResult.Failed; + } + + _seconds = secondResult.Value; + } + + if (_nanoseconds is null) + { + var nanosecondResult = await reader.ReadInt32(isAsync, cancellationToken); + if (!nanosecondResult.IsSuccess) + { + return ReadValueResult.Failed; + } + + _nanoseconds = nanosecondResult.Value; + } + + return ReadValueResult.FromValue(CreateTimeSpan(_seconds.Value, _nanoseconds.Value)); + } + + protected abstract TTimeSpan CreateTimeSpan(long seconds, int nanoseconds); +} + +file static class TimeSpanHelper +{ + // Must be the same as TimeSpan.NanosecondsPerTick + public const long NanosecondsPerTick = 100; +} + +internal sealed class StandardTimeSpanSerializer : TimeSpanSerializer +{ + protected override (long Seconds, int Nanoseconds) GetSecondsAndNanoseconds(in TimeSpan value) + { + var seconds = value.Ticks / TimeSpan.TicksPerSecond; + var nanoseconds = value.Ticks % TimeSpan.TicksPerSecond * TimeSpanHelper.NanosecondsPerTick; + return (seconds, (int)nanoseconds); + } +} + +internal sealed class StandardTimeSpanDeserializer : TimeSpanDeserializer +{ + protected override TimeSpan CreateTimeSpan(long seconds, int nanoseconds) + { + var ticks = seconds * TimeSpan.TicksPerSecond + nanoseconds / TimeSpanHelper.NanosecondsPerTick; + return new TimeSpan(ticks); + } +} + +public abstract class DateOnlySerializer : AbstractSerializer + where TDate : notnull +{ + private bool _hasWrittenYear; + private bool _hasWrittenMonth; + private bool _hasWrittenDay; + + public sealed override void Reset() + { + _hasWrittenYear = false; + _hasWrittenMonth = false; + _hasWrittenDay = false; + } + + public override bool Serialize(SerializationWriter writer, in TDate value) + { + var (year, month, day) = GetDateParts(value); + var writerRef = writer.ByrefWriter; + if (!_hasWrittenYear) + { + _hasWrittenYear = writerRef.WriteInt32(year); + if (!_hasWrittenYear) + { + return false; + } + } + + if (!_hasWrittenMonth) + { + _hasWrittenMonth = writerRef.WriteUInt8(month); + if (!_hasWrittenMonth) + { + return false; + } + } + + if (!_hasWrittenDay) + { + _hasWrittenDay = writerRef.WriteUInt8(day); + if (!_hasWrittenDay) + { + return false; + } + } + + return true; + } + + protected abstract (int Year, byte Month, byte Day) GetDateParts(in TDate value); +} + +public abstract class DateOnlyDeserializer : AbstractDeserializer + where TDate : notnull +{ + public sealed override object ReferenceableObject => ThrowInvalidOperationException_AcyclicType(); + + private int? _year; + private byte? _month; + private byte? _day; + + public sealed override void Reset() + { + _year = null; + _month = null; + _day = null; + } + + public sealed override ReadValueResult Deserialize(DeserializationReader reader) + { + var task = Deserialize(reader, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public sealed override ValueTask> DeserializeAsync( + DeserializationReader reader, + CancellationToken cancellationToken = default + ) + { + return Deserialize(reader, true, cancellationToken); + } + + private async ValueTask> Deserialize( + DeserializationReader reader, + bool isAsync, + CancellationToken cancellationToken + ) + { + if (_year is null) + { + var yearResult = await reader.ReadInt32(isAsync, cancellationToken); + if (!yearResult.IsSuccess) + { + return ReadValueResult.Failed; + } + + _year = yearResult.Value; + } + + if (_month is null) + { + var monthResult = await reader.ReadUInt8(isAsync, cancellationToken); + if (!monthResult.IsSuccess) + { + return ReadValueResult.Failed; + } + + _month = monthResult.Value; + } + + if (_day is null) + { + var dayResult = await reader.ReadUInt8(isAsync, cancellationToken); + if (!dayResult.IsSuccess) + { + return ReadValueResult.Failed; + } + + _day = dayResult.Value; + } + + return ReadValueResult.FromValue(CreateDate(_year.Value, _month.Value, _day.Value)); + } + + protected abstract TDate CreateDate(int year, byte month, byte day); +} + +#if NET6_0_OR_GREATER +internal sealed class StandardDateOnlySerializer : DateOnlySerializer +{ + protected override (int Year, byte Month, byte Day) GetDateParts(in DateOnly value) + { + var year = value.Year; + var month = (byte)value.Month; + var day = (byte)value.Day; + return (year, month, day); + } +} + +internal sealed class StandardDateOnlyDeserializer : DateOnlyDeserializer +{ + protected override DateOnly CreateDate(int year, byte month, byte day) + { + return new DateOnly(year, month, day); + } +} +#endif + +public abstract class DateTimeSerializer : AbstractSerializer + where TDateTime : notnull +{ + public sealed override void Reset() { } + + public sealed override bool Serialize(SerializationWriter writer, in TDateTime value) + { + var millisecond = GetMillisecond(value); + return writer.WriteUInt64(millisecond); + } + + protected abstract long GetMillisecond(in TDateTime value); +} + +public abstract class DateTimeDeserializer : AbstractDeserializer + where TDateTime : notnull +{ + public sealed override object ReferenceableObject => ThrowInvalidOperationException_AcyclicType(); + + public sealed override void Reset() { } + + public sealed override ReadValueResult Deserialize(DeserializationReader reader) + { + var task = Deserialize(reader, false, CancellationToken.None); + Debug.Assert(task.IsCompleted); + return task.Result; + } + + public sealed override ValueTask> DeserializeAsync( + DeserializationReader reader, + CancellationToken cancellationToken = default + ) + { + return Deserialize(reader, true, cancellationToken); + } + + private async ValueTask> Deserialize( + DeserializationReader reader, + bool isAsync, + CancellationToken cancellationToken + ) + { + var millisecondResult = await reader.ReadInt64(isAsync, cancellationToken); + if (!millisecondResult.IsSuccess) + { + return ReadValueResult.Failed; + } + + return ReadValueResult.FromValue(CreateDateTime(millisecondResult.Value)); + } + + protected abstract TDateTime CreateDateTime(long millisecond); +} + +internal sealed class StandardDateTimeSerializer : DateTimeSerializer +{ + public static readonly StandardDateTimeSerializer Instance = new StandardDateTimeSerializer(); + + private StandardDateTimeSerializer() { } + + protected override long GetMillisecond(in DateTime value) + { + return new DateTimeOffset(value.ToUniversalTime()).ToUnixTimeMilliseconds(); + } +} + +internal sealed class StandardDateTimeDeserializer : DateTimeDeserializer +{ + public static readonly StandardDateTimeDeserializer Instance = new StandardDateTimeDeserializer(); + + private StandardDateTimeDeserializer() { } + + protected override DateTime CreateDateTime(long millisecond) + { + return DateTimeOffset.FromUnixTimeMilliseconds(millisecond).UtcDateTime; + } +} diff --git a/csharp/Fury/SerializationResult.cs b/csharp/Fury/SerializationResult.cs new file mode 100644 index 0000000000..0f513f09b2 --- /dev/null +++ b/csharp/Fury/SerializationResult.cs @@ -0,0 +1,59 @@ +using Fury.Context; + +namespace Fury; + +public readonly struct SerializationResult +{ + public bool IsCompleted { get; private init; } + + internal SerializationWriter? Writer { get; private init; } + internal TypeRegistration? RootTypeRegistrationHint { get; private init; } + + internal static SerializationResult Completed { get; } = new() { IsCompleted = true }; + + internal static SerializationResult FromUncompleted( + SerializationWriter writer, + TypeRegistration? rootTypeRegistrationHint + ) + { + return new SerializationResult + { + IsCompleted = false, + Writer = writer, + RootTypeRegistrationHint = rootTypeRegistrationHint, + }; + } +} + +public readonly struct DeserializationResult +{ + public bool IsCompleted { get; private init; } + public T? Value { get; init; } + internal DeserializationReader? Reader { get; private init; } + internal TypeRegistration? RootTypeRegistrationHint { get; private init; } + + internal static DeserializationResult FromValue(in T? value) + { + return new DeserializationResult + { + IsCompleted = true, + Reader = null, + Value = value, + RootTypeRegistrationHint = null, + }; + } + + internal static DeserializationResult FromUncompleted( + DeserializationReader reader, + TypeRegistration? rootTypeRegistrationHint + ) + { + return new DeserializationResult + { + IsCompleted = false, + Reader = reader, + RootTypeRegistrationHint = rootTypeRegistrationHint, + Value = default, + }; + } +} diff --git a/csharp/Fury/StaticConfigs.cs b/csharp/Fury/StaticConfigs.cs new file mode 100644 index 0000000000..f0665d97a5 --- /dev/null +++ b/csharp/Fury/StaticConfigs.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; + +namespace Fury; + +internal static class StaticConfigs +{ + private const int StackAllocLimit = 256; + public const int CharStackAllocLimit = StackAllocLimit / sizeof(char); + + public const int BuiltInListDefaultCapacity = 16; + public const int BuiltInBufferDefaultCapacity = 256; +} diff --git a/csharp/global.json b/csharp/global.json new file mode 100644 index 0000000000..dad2db5efd --- /dev/null +++ b/csharp/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file