From 754bc9f00be79d924ebadbd86ec3c088bc022c88 Mon Sep 17 00:00:00 2001 From: sakno Date: Thu, 5 Dec 2024 20:49:17 +0200 Subject: [PATCH] Release 5.16.0 --- CHANGELOG.md | 30 ++- README.md | 34 ++- src/Directory.Packages.props | 10 +- .../DotNext.Benchmarks.csproj | 2 +- src/DotNext.IO/Buffers/Binary/Leb128Reader.cs | 20 ++ src/DotNext.IO/Buffers/BufferWriter.cs | 140 +++++++++-- src/DotNext.IO/Buffers/IBufferReader.cs | 5 +- src/DotNext.IO/Buffers/ReadOnlySpanFunc.cs | 2 +- src/DotNext.IO/Buffers/SevenBitEncodedInt.cs | 83 ------- src/DotNext.IO/DotNext.IO.csproj | 2 +- .../IO/FileBufferingWriter.Options.cs | 4 +- src/DotNext.IO/IO/FileReader.Binary.cs | 4 +- src/DotNext.IO/IO/FileWriter.Binary.cs | 8 +- src/DotNext.IO/IO/IAsyncBinaryReader.cs | 2 +- .../IO/Pipelines/PipeExtensions.Readers.cs | 4 +- .../IO/Pipelines/PipeReaderWriter.cs | 2 +- ...m.Utils.cs => RandomAccessStream.Utils.cs} | 2 +- src/DotNext.IO/IO/RandomAccessStream.cs | 158 +++++++++++++ src/DotNext.IO/IO/Segment.cs | 6 +- src/DotNext.IO/IO/SequenceReader.cs | 4 +- src/DotNext.IO/IO/SparseStream.cs | 12 +- src/DotNext.IO/IO/StreamBinaryAccessor.cs | 2 +- src/DotNext.IO/IO/StreamExtensions.Readers.cs | 4 +- src/DotNext.IO/IO/StreamExtensions.Writers.cs | 2 +- src/DotNext.IO/IO/UnbufferedFileStream.cs | 114 +-------- src/DotNext.IO/Text/EncodingContext.cs | 2 + .../DotNext.Metaprogramming.csproj | 2 +- .../Buffers/Binary/Leb128Tests.cs | 39 ++++ .../Buffers/BufferWriterSlimTests.cs | 120 ++++++++++ .../Buffers/SpanReaderWriterTests.cs | 75 ++++++ src/DotNext.Tests/DotNext.Tests.csproj | 2 +- src/DotNext.Tests/Numerics/BitVectorTests.cs | 13 ++ .../Threading/AsyncCounterTests.cs | 12 + .../Threading/AsyncExclusiveLockTests.cs | 23 ++ .../Threading/AsyncReaderWriterLockTests.cs | 14 +- .../LinkedCancellationTokenSourceTests.cs | 33 +++ src/DotNext.Tests/Threading/LockTests.cs | 12 + .../DotNext.Threading.csproj | 2 +- .../Runtime/Caching/RandomAccessCache.cs | 10 +- .../Threading/AsyncAutoResetEvent.cs | 30 ++- .../Threading/AsyncCountdownEvent.cs | 2 +- .../Threading/AsyncCounter.cs | 4 +- .../Threading/AsyncEventHub.cs | 4 +- .../Threading/AsyncExclusiveLock.cs | 40 ++-- .../Threading/AsyncManualResetEvent.cs | 24 +- .../Threading/AsyncReaderWriterLock.cs | 102 ++++++-- .../Threading/AsyncSharedLock.cs | 6 +- .../Threading/AsyncTrigger.cs | 4 +- .../Threading/Channels/IChannelWriter.cs | 2 +- .../LinkedCancellationTokenSource.cs | 14 +- .../Threading/LinkedTokenSourceFactory.cs | 91 +++++++- .../Threading/QueuedSynchronizer.cs | 103 ++++++++- src/DotNext.Threading/Threading/Scheduler.cs | 20 +- src/DotNext.Unsafe/DotNext.Unsafe.csproj | 2 +- .../InteropServices/IUnmanagedMemory.cs | 2 +- src/DotNext/Buffers/Binary/Leb128.cs | 218 ++++++++++++++++++ src/DotNext/Buffers/BufferWriterSlim.cs | 32 +++ src/DotNext/Buffers/ByteBuffer.cs | 77 ++++++- src/DotNext/Buffers/CharBuffer.cs | 4 +- src/DotNext/Buffers/MemoryOwner.cs | 8 - src/DotNext/Buffers/SpanWriter.cs | 15 +- .../Buffers/Text/Base64Decoder.Unicode.cs | 17 +- .../Buffers/Text/Base64Decoder.Utf8.cs | 17 +- .../Buffers/Text/Base64Encoder.Unicode.cs | 8 +- .../Buffers/Text/Base64Encoder.Utf8.cs | 8 +- .../Collections/Generic/Collection.Buffer.cs | 5 +- src/DotNext/DotNext.csproj | 4 +- src/DotNext/Numerics/Number.BitVector.cs | 40 +++- src/DotNext/Numerics/Number.cs | 4 +- src/DotNext/Threading/Lock.cs | 68 ++++++ src/DotNext/Threading/Timeout.cs | 22 +- .../DotNext.AspNetCore.Cluster.csproj | 2 +- .../Http/HttpPeerController.Disconnect.cs | 3 +- .../Http/HttpPeerController.ForwardJoin.cs | 3 +- .../HyParView/Http/HttpPeerController.Join.cs | 3 +- .../Http/HttpPeerController.Neighbor.cs | 3 +- .../Http/HttpPeerController.Shuffle.cs | 6 +- .../DotNext.Net.Cluster.csproj | 2 +- .../InMemoryClusterConfigurationStorage.cs | 3 +- .../PersistentClusterConfigurationStorage.cs | 3 +- .../Net/EndPointFormatter.cs | 3 +- 81 files changed, 1580 insertions(+), 463 deletions(-) create mode 100644 src/DotNext.IO/Buffers/Binary/Leb128Reader.cs delete mode 100644 src/DotNext.IO/Buffers/SevenBitEncodedInt.cs rename src/DotNext.IO/IO/{UnbufferedFileStream.Utils.cs => RandomAccessStream.Utils.cs} (97%) create mode 100644 src/DotNext.IO/IO/RandomAccessStream.cs create mode 100644 src/DotNext.Tests/Buffers/Binary/Leb128Tests.cs create mode 100644 src/DotNext/Buffers/Binary/Leb128.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index cf5ceb475a..299b45c8af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,35 @@ Release Notes ==== -# 10-16-2023 +# 12-05-2024 +DotNext 5.16.0 +* Added [LEB128](https://en.wikipedia.org/wiki/LEB128) encoder and decoder as a public API. See `DotNext.Buffers.Binary.Leb128` type for more information +* Added `SlideToEnd` method to `SpanWriter` type +* Added `IsBitSet` and `SetBit` generic methods to `Number` type +* Added `DetachOrCopyBuffer` to `BufferWriterSlim` type + +DotNext.Metaprogramming 5.16.0 +* Updated dependencies + +DotNext.Unsafe 5.16.0 +* Updated dependencies + +DotNext.Threading 5.16.0 +* Async locks with synchronous acquisition methods now throw [LockRecursionException](https://learn.microsoft.com/en-us/dotnet/api/system.threading.lockrecursionexception) if the current thread tries to acquire the lock synchronously and recursively. +* Added support of cancellation token to synchronous acquisition methods of `AsyncExclusiveLock` and `AsyncReaderWriterLock` classes +* Introduced `LinkTo` method overload that supports multiple cancellation tokens + +DotNext.IO 5.16.0 +* Introduced `RandomAccessStream` class that represents [Stream](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream) wrapper over the underlying data storage that supports random access pattern +* Added extension method for `SpanWriter` that provides length-prefixed string encoding + +DotNext.Net.Cluster 5.16.0 +* Updated dependencies + +DotNext.AspNetCore.Cluster 5.16.0 +* Updated dependencies + +# 10-16-2024 DotNext.Threading 5.15.0 * Added support of synchronous lock acquisition to `AsyncExclusiveLock`, `AsyncReaderWriterLock`, `AsyncManualResetEvent`, `AsyncAutoResetEvent` so the users can easily migrate step-by-step from monitors and other synchronization primitives to async-friendly primitives * Fixed random `InvalidOperationException` caused by `RandomAccessCache` diff --git a/README.md b/README.md index 7b86a0f135..b8eb924e5b 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,34 @@ All these things are implemented in 100% managed code on top of existing .NET AP * [NuGet Packages](https://www.nuget.org/profiles/rvsakno) # What's new -Release Date: 10-16-2024 +Release Date: 12-05-2024 -DotNext.Threading 5.15.0 -* Added support of synchronous lock acquisition to `AsyncExclusiveLock`, `AsyncReaderWriterLock`, `AsyncManualResetEvent`, `AsyncAutoResetEvent` so the users can easily migrate step-by-step from monitors and other synchronization primitives to async-friendly primitives -* Fixed random `InvalidOperationException` caused by `RandomAccessCache` -* Added synchronous methods to `RandomAccessCache` to support [251](https://github.com/dotnet/dotNext/issues/251) feature request +DotNext 5.16.0 +* Added [LEB128](https://en.wikipedia.org/wiki/LEB128) encoder and decoder as a public API. See `DotNext.Buffers.Binary.Leb128` type for more information +* Added `SlideToEnd` method to `SpanWriter` type +* Added `IsBitSet` and `SetBit` generic methods to `Number` type +* Added `DetachOrCopyBuffer` to `BufferWriterSlim` type + +DotNext.Metaprogramming 5.16.0 +* Updated dependencies + +DotNext.Unsafe 5.16.0 +* Updated dependencies + +DotNext.Threading 5.16.0 +* Async locks with synchronous acquisition methods now throw [LockRecursionException](https://learn.microsoft.com/en-us/dotnet/api/system.threading.lockrecursionexception) if the current thread tries to acquire the lock synchronously and recursively. +* Added support of cancellation token to synchronous acquisition methods of `AsyncExclusiveLock` and `AsyncReaderWriterLock` classes +* Introduced `LinkTo` method overload that supports multiple cancellation tokens + +DotNext.IO 5.16.0 +* Introduced `RandomAccessStream` class that represents [Stream](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream) wrapper over the underlying data storage that supports random access pattern +* Added extension method for `SpanWriter` that provides length-prefixed string encoding + +DotNext.Net.Cluster 5.16.0 +* Updated dependencies + +DotNext.AspNetCore.Cluster 5.16.0 +* Updated dependencies Changelog for previous versions located [here](./CHANGELOG.md). @@ -62,7 +84,7 @@ The libraries are versioned according to [Semantic Versioning 2.0](https://semve | 1.x | .NET Standard 2.0 | :x: | | 2.x | .NET Standard 2.1 | :x: | | 3.x | .NET Standard 2.1, .NET 5 | :x: | -| 4.x | .NET 6 | :white_check_mark: | +| 4.x | .NET 6 | :x: | | 5.x | .NET 8 | :heavy_check_mark: | :x: - unsupported, :white_check_mark: - bug and security fixes only, :heavy_check_mark: - active development diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 33f98518b5..8a6086688a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -19,7 +19,7 @@ - + @@ -27,14 +27,14 @@ - + - + - - + + \ No newline at end of file diff --git a/src/DotNext.Benchmarks/DotNext.Benchmarks.csproj b/src/DotNext.Benchmarks/DotNext.Benchmarks.csproj index 31c8f313f5..347a50835f 100644 --- a/src/DotNext.Benchmarks/DotNext.Benchmarks.csproj +++ b/src/DotNext.Benchmarks/DotNext.Benchmarks.csproj @@ -8,7 +8,7 @@ DotNext DotNext.Program false - 5.11.0 + 5.16.0 .NET Foundation and Contributors .NEXT Family of Libraries Various benchmarks demonstrating performance aspects of .NEXT extensions diff --git a/src/DotNext.IO/Buffers/Binary/Leb128Reader.cs b/src/DotNext.IO/Buffers/Binary/Leb128Reader.cs new file mode 100644 index 0000000000..139d17e32d --- /dev/null +++ b/src/DotNext.IO/Buffers/Binary/Leb128Reader.cs @@ -0,0 +1,20 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DotNext.Buffers.Binary; + +[StructLayout(LayoutKind.Auto)] +internal struct Leb128Reader() : IBufferReader, ISupplier + where T : struct, IBinaryInteger +{ + private Leb128 decoder; + private bool incompleted = true; + + readonly int IBufferReader.RemainingBytes => Unsafe.BitCast(incompleted); + + void IReadOnlySpanConsumer.Invoke(ReadOnlySpan source) + => incompleted = decoder.Append(MemoryMarshal.GetReference(source)); + + readonly T ISupplier.Invoke() => decoder.Value; +} \ No newline at end of file diff --git a/src/DotNext.IO/Buffers/BufferWriter.cs b/src/DotNext.IO/Buffers/BufferWriter.cs index 3ea22c8a5f..4f03f734fe 100644 --- a/src/DotNext.IO/Buffers/BufferWriter.cs +++ b/src/DotNext.IO/Buffers/BufferWriter.cs @@ -7,6 +7,7 @@ namespace DotNext.Buffers; using EncodingContext = DotNext.Text.EncodingContext; using LengthFormat = IO.LengthFormat; +using SevenBitEncodedInt = Binary.Leb128; /// /// Represents extension methods for writing typed data into buffer. @@ -47,28 +48,18 @@ internal static unsafe int WriteLength(this ref SpanWriter destination, in { LengthFormat.LittleEndian => &ByteBuffer.WriteLittleEndian, LengthFormat.BigEndian => &ByteBuffer.WriteBigEndian, - LengthFormat.Compressed => &Write7BitEncodedInt, + LengthFormat.Compressed => &ByteBuffer.WriteLeb128, _ => throw new ArgumentOutOfRangeException(nameof(lengthFormat)), }; return writer(ref destination, value); - - static int Write7BitEncodedInt(ref SpanWriter writer, int value) - { - foreach (var b in new SevenBitEncodedInt(value)) - { - writer.Add() = b; - } - - return writer.WrittenCount; - } } internal static int WriteLength(this IBufferWriter buffer, int length, LengthFormat lengthFormat) { - var writer = new SpanWriter(buffer.GetSpan(SevenBitEncodedInt.MaxSize)); - buffer.Advance(writer.WriteLength(length, lengthFormat)); - return writer.WrittenCount; + var bytesWritten = WriteLength(buffer.GetSpan(SevenBitEncodedInt.MaxSizeInBytes), length, lengthFormat); + buffer.Advance(bytesWritten); + return bytesWritten; } internal static int WriteLength(Span buffer, int length, LengthFormat lengthFormat) @@ -77,26 +68,131 @@ internal static int WriteLength(Span buffer, int length, LengthFormat leng return writer.WriteLength(length, lengthFormat); } + private static int WriteLength(this ref BufferWriterSlim buffer, int length, LengthFormat lengthFormat) + { + var bytesWritten = WriteLength(buffer.GetSpan(SevenBitEncodedInt.MaxSizeInBytes), length, lengthFormat); + buffer.Advance(bytesWritten); + return bytesWritten; + } + /// /// Encodes string using the specified encoding. /// /// The buffer writer. - /// The sequence of characters. + /// The sequence of characters. /// The encoding context. /// String length encoding format; or to prevent encoding of string length. /// The number of written bytes. - public static long Encode(this IBufferWriter writer, ReadOnlySpan value, in EncodingContext context, LengthFormat? lengthFormat = null) + public static long Encode(this IBufferWriter writer, ReadOnlySpan chars, in EncodingContext context, LengthFormat? lengthFormat = null) { - long result = lengthFormat.HasValue - ? writer.WriteLength(context.Encoding.GetByteCount(value), lengthFormat.GetValueOrDefault()) + var result = lengthFormat.HasValue + ? writer.WriteLength(context.Encoding.GetByteCount(chars), lengthFormat.GetValueOrDefault()) : 0L; - context.GetEncoder().Convert(value, writer, true, out var bytesWritten, out _); + context.GetEncoder().Convert(chars, writer, true, out var bytesWritten, out _); + result += bytesWritten; + + return result; + } + + /// + /// Encodes string using the specified encoding. + /// + /// The buffer writer. + /// The sequence of characters. + /// The encoding context. + /// String length encoding format; or to prevent encoding of string length. + /// The number of written bytes. + public static int Encode(this ref SpanWriter writer, scoped ReadOnlySpan chars, in EncodingContext context, LengthFormat? lengthFormat = null) + { + var result = lengthFormat.HasValue + ? writer.WriteLength(context.Encoding.GetByteCount(chars), lengthFormat.GetValueOrDefault()) + : 0; + + var bytesWritten = context.TryGetEncoder() is { } encoder + ? encoder.GetBytes(chars, writer.RemainingSpan, flush: true) + : context.Encoding.GetBytes(chars, writer.RemainingSpan); result += bytesWritten; + writer.Advance(bytesWritten); return result; } + /// + /// Writes a sequence of bytes prefixed with the length. + /// + /// The buffer writer. + /// A sequence of bytes to be written. + /// A format of the buffer length to be written. + /// A number of bytes written. + public static int Write(this ref SpanWriter writer, scoped ReadOnlySpan value, LengthFormat lengthFormat) + { + var result = writer.WriteLength(value.Length, lengthFormat); + result += writer.Write(value); + + return result; + } + + /// + /// Encodes string using the specified encoding. + /// + /// The buffer writer. + /// The sequence of characters. + /// The encoding context. + /// String length encoding format; or to prevent encoding of string length. + /// The number of written bytes. + public static int Encode(this ref BufferWriterSlim writer, scoped ReadOnlySpan chars, in EncodingContext context, + LengthFormat? lengthFormat = null) + { + Span buffer; + int byteCount, result; + if (lengthFormat.HasValue) + { + byteCount = context.Encoding.GetByteCount(chars); + result = writer.WriteLength(byteCount, lengthFormat.GetValueOrDefault()); + + buffer = writer.GetSpan(byteCount); + byteCount = context.TryGetEncoder() is { } encoder + ? encoder.GetBytes(chars, buffer, flush: true) + : context.Encoding.GetBytes(chars, buffer); + + result += byteCount; + writer.Advance(byteCount); + } + else + { + result = 0; + var encoder = context.GetEncoder(); + byteCount = context.Encoding.GetMaxByteCount(1); + for (int charsUsed, bytesWritten; !chars.IsEmpty; chars = chars.Slice(charsUsed), result += bytesWritten) + { + buffer = writer.GetSpan(byteCount); + var maxChars = buffer.Length / byteCount; + + encoder.Convert(chars, buffer, chars.Length <= maxChars, out charsUsed, out bytesWritten, out _); + writer.Advance(bytesWritten); + } + } + + return result; + } + + /// + /// Writes a sequence of bytes prefixed with the length. + /// + /// The buffer writer. + /// A sequence of bytes to be written. + /// A format of the buffer length to be written. + /// A number of bytes written. + public static int Write(this ref BufferWriterSlim writer, scoped ReadOnlySpan value, LengthFormat lengthFormat) + { + var result = writer.WriteLength(value.Length, lengthFormat); + writer.Write(value); + result += value.Length; + + return result; + } + private static bool TryFormat(IBufferWriter writer, T value, Span buffer, in EncodingContext context, LengthFormat? lengthFormat, ReadOnlySpan format, IFormatProvider? provider, out long bytesWritten) where T : notnull, ISpanFormattable { @@ -167,12 +263,12 @@ public static int Format(this IBufferWriter writer, T value, LengthForm { null => 0, LengthFormat.BigEndian or LengthFormat.LittleEndian => sizeof(int), - LengthFormat.Compressed => SevenBitEncodedInt.MaxSize, + LengthFormat.Compressed => SevenBitEncodedInt.MaxSizeInBytes, _ => throw new ArgumentOutOfRangeException(nameof(lengthFormat)), }; int bytesWritten; - for (int bufferSize = 0, actualLengthSize; ; bufferSize = bufferSize <= MaxBufferSize ? bufferSize << 1 : throw new InsufficientMemoryException()) + for (int bufferSize = 0; ; bufferSize = bufferSize <= MaxBufferSize ? bufferSize << 1 : throw new InsufficientMemoryException()) { var buffer = writer.GetSpan(bufferSize); @@ -182,7 +278,7 @@ public static int Format(this IBufferWriter writer, T value, LengthForm continue; } - actualLengthSize = lengthFormat.HasValue + var actualLengthSize = lengthFormat.HasValue ? WriteLength(buffer.Slice(0, expectedLengthSize), bytesWritten, lengthFormat.GetValueOrDefault()) : 0; diff --git a/src/DotNext.IO/Buffers/IBufferReader.cs b/src/DotNext.IO/Buffers/IBufferReader.cs index 8396a7677d..8ec00e50c5 100644 --- a/src/DotNext.IO/Buffers/IBufferReader.cs +++ b/src/DotNext.IO/Buffers/IBufferReader.cs @@ -252,7 +252,6 @@ TResult ISupplier.Invoke() } [StructLayout(LayoutKind.Auto)] - internal struct SkippingReader(long length) : IBufferReader { readonly int IBufferReader.RemainingBytes => int.CreateSaturating(length); @@ -261,7 +260,7 @@ void IReadOnlySpanConsumer.Invoke(ReadOnlySpan source) => length -= source.Length; } -internal struct BufferReader(TReader reader) : IBufferReader, ISupplier +internal struct ProxyReader(TReader reader) : IBufferReader, ISupplier where TReader : struct, IBufferReader { int IBufferReader.RemainingBytes => reader.RemainingBytes; @@ -273,5 +272,5 @@ void IReadOnlySpanConsumer.Invoke(ReadOnlySpan source) readonly TReader ISupplier.Invoke() => reader; - public static implicit operator BufferReader(TReader reader) => new(reader); + public static implicit operator ProxyReader(TReader reader) => new(reader); } \ No newline at end of file diff --git a/src/DotNext.IO/Buffers/ReadOnlySpanFunc.cs b/src/DotNext.IO/Buffers/ReadOnlySpanFunc.cs index 23f9944d5f..8a8335168c 100644 --- a/src/DotNext.IO/Buffers/ReadOnlySpanFunc.cs +++ b/src/DotNext.IO/Buffers/ReadOnlySpanFunc.cs @@ -10,4 +10,4 @@ namespace DotNext.Buffers; /// A read-only span of objects. /// A state object. /// The value returned by the delegate. -public delegate TResult ReadOnlySpanFunc(ReadOnlySpan span, TArg arg); \ No newline at end of file +public delegate TResult ReadOnlySpanFunc(ReadOnlySpan span, TArg arg); \ No newline at end of file diff --git a/src/DotNext.IO/Buffers/SevenBitEncodedInt.cs b/src/DotNext.IO/Buffers/SevenBitEncodedInt.cs deleted file mode 100644 index 14d0aebce6..0000000000 --- a/src/DotNext.IO/Buffers/SevenBitEncodedInt.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace DotNext.Buffers; - -internal struct SevenBitEncodedInt -{ - internal const int MaxSize = 5; - - private uint value; - private byte shift; - - internal SevenBitEncodedInt(int value) => this.value = (uint)value; - - internal bool Append(byte b) - { - if (shift is MaxSize * 7) - throw new InvalidDataException(); - - value |= (b & 0x7FU) << shift; - shift += 7; - return (b & 0x80U) is not 0U; - } - - internal readonly int Value => (int)value; - - internal readonly int CopyTo(Span buffer) - { - var writer = new SpanWriter(buffer); - foreach (var b in this) - { - writer.Add(b); - } - - return writer.WrittenCount; - } - - public readonly Enumerator GetEnumerator() => new(value); - - internal struct Enumerator - { - private uint value; - private byte current; - private bool completed; - - internal Enumerator(uint value) => this.value = value; - - public readonly byte Current => current; - - public bool MoveNext() - { - if (completed) - return false; - - if (value >= 0x80U) - { - current = (byte)(value | 0x80U); - value >>= 7; - } - else - { - current = (byte)value; - completed = true; - } - - return true; - } - } - - [StructLayout(LayoutKind.Auto)] - internal struct Reader : IBufferReader, ISupplier - { - private SevenBitEncodedInt value; - private bool completed; - - readonly int IBufferReader.RemainingBytes => Unsafe.BitCast(!completed); - - void IReadOnlySpanConsumer.Invoke(ReadOnlySpan source) - => completed = value.Append(MemoryMarshal.GetReference(source)) is false; - - readonly int ISupplier.Invoke() => value.Value; - } -} \ No newline at end of file diff --git a/src/DotNext.IO/DotNext.IO.csproj b/src/DotNext.IO/DotNext.IO.csproj index 9a1bb78e55..bc774d4d75 100644 --- a/src/DotNext.IO/DotNext.IO.csproj +++ b/src/DotNext.IO/DotNext.IO.csproj @@ -11,7 +11,7 @@ .NET Foundation and Contributors .NEXT Family of Libraries - 5.14.0 + 5.16.0 DotNext.IO MIT diff --git a/src/DotNext.IO/IO/FileBufferingWriter.Options.cs b/src/DotNext.IO/IO/FileBufferingWriter.Options.cs index b2ed09162b..1109c62fe8 100644 --- a/src/DotNext.IO/IO/FileBufferingWriter.Options.cs +++ b/src/DotNext.IO/IO/FileBufferingWriter.Options.cs @@ -40,8 +40,6 @@ internal BackingFileProvider(in Options options) { } - internal bool IsAsynchronous => (options & FileOptions.Asynchronous) != 0; - internal SafeFileHandle CreateBackingFileHandle(int preallocationSize, out string fileName) => File.OpenHandle(fileName = temporary ? Path.Combine(path, Path.GetRandomFileName()) : path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read, options, preallocationSize); } @@ -65,7 +63,7 @@ public readonly struct Options /// public int MemoryThreshold { - get => memoryThreshold == 0 ? DefaultMemoryThreshold : memoryThreshold; + get => memoryThreshold is 0 ? DefaultMemoryThreshold : memoryThreshold; init => memoryThreshold = value > 0 ? value : throw new ArgumentOutOfRangeException(nameof(value)); } diff --git a/src/DotNext.IO/IO/FileReader.Binary.cs b/src/DotNext.IO/IO/FileReader.Binary.cs index 174847e8cd..aec2a275e8 100644 --- a/src/DotNext.IO/IO/FileReader.Binary.cs +++ b/src/DotNext.IO/IO/FileReader.Binary.cs @@ -121,13 +121,13 @@ public ValueTask ReadBigEndianAsync(CancellationToken token = default) /// ValueTask IAsyncBinaryReader.ReadAsync(TReader reader, CancellationToken token) - => ReadAsync>(reader, token); + => ReadAsync>(reader, token); private ValueTask ReadLengthAsync(LengthFormat lengthFormat, CancellationToken token) => lengthFormat switch { LengthFormat.LittleEndian => ReadLittleEndianAsync(token), LengthFormat.BigEndian => ReadBigEndianAsync(token), - LengthFormat.Compressed => ReadAsync(new(), token), + LengthFormat.Compressed => ReadAsync>(new(), token), _ => ValueTask.FromException(new ArgumentOutOfRangeException(nameof(lengthFormat))), }; diff --git a/src/DotNext.IO/IO/FileWriter.Binary.cs b/src/DotNext.IO/IO/FileWriter.Binary.cs index 9d9cd599d4..49311bbebd 100644 --- a/src/DotNext.IO/IO/FileWriter.Binary.cs +++ b/src/DotNext.IO/IO/FileWriter.Binary.cs @@ -142,7 +142,7 @@ private int WriteLength(int length, LengthFormat lengthFormat) /// The operation has been canceled. public async ValueTask WriteAsync(ReadOnlyMemory input, LengthFormat lengthFormat, CancellationToken token = default) { - if (FreeCapacity < SevenBitEncodedInt.MaxSize) + if (FreeCapacity < Leb128.MaxSizeInBytes) await FlushAsync(token).ConfigureAwait(false); WriteLength(input.Length, lengthFormat); @@ -164,7 +164,7 @@ public async ValueTask EncodeAsync(ReadOnlyMemory chars, EncodingCon long result; if (lengthFormat.HasValue) { - if (FreeCapacity < SevenBitEncodedInt.MaxSize) + if (FreeCapacity < Leb128.MaxSizeInBytes) await FlushAsync(token).ConfigureAwait(false); result = WriteLength(context.Encoding.GetByteCount(chars.Span), lengthFormat.GetValueOrDefault()); @@ -256,7 +256,7 @@ private bool TryFormat(T value, LengthFormat? lengthFormat, ReadOnlySpan 0, LengthFormat.BigEndian or LengthFormat.LittleEndian => sizeof(int), - LengthFormat.Compressed => SevenBitEncodedInt.MaxSize, + LengthFormat.Compressed => Leb128.MaxSizeInBytes, _ => throw new ArgumentOutOfRangeException(nameof(lengthFormat)), }; @@ -289,7 +289,7 @@ private async ValueTask FormatSlowAsync(T value, LengthFormat? lengthFor if (!TryFormat(value, lengthFormat, format, provider, out var bytesWritten)) { const int maxBufferSize = int.MaxValue / 2; - for (var bufferSize = MaxBufferSize + SevenBitEncodedInt.MaxSize; ; bufferSize = bufferSize <= maxBufferSize ? bufferSize << 1 : throw new InsufficientMemoryException()) + for (var bufferSize = MaxBufferSize + Leb128.MaxSizeInBytes; ; bufferSize = bufferSize <= maxBufferSize ? bufferSize << 1 : throw new InsufficientMemoryException()) { using var buffer = allocator.AllocateAtLeast(bufferSize); if (value.TryFormat(buffer.Span, out bytesWritten, format, provider)) diff --git a/src/DotNext.IO/IO/IAsyncBinaryReader.cs b/src/DotNext.IO/IO/IAsyncBinaryReader.cs index e889c5f214..938f29a4e1 100644 --- a/src/DotNext.IO/IO/IAsyncBinaryReader.cs +++ b/src/DotNext.IO/IO/IAsyncBinaryReader.cs @@ -99,7 +99,7 @@ private async ValueTask ReadAsync(TReader reader, Can { LengthFormat.LittleEndian => ReadLittleEndianAsync(token), LengthFormat.BigEndian => ReadBigEndianAsync(token), - LengthFormat.Compressed => ReadAsync(new(), token), + LengthFormat.Compressed => ReadAsync>(new(), token), _ => ValueTask.FromException(new ArgumentOutOfRangeException(nameof(lengthFormat))), }; diff --git a/src/DotNext.IO/IO/Pipelines/PipeExtensions.Readers.cs b/src/DotNext.IO/IO/Pipelines/PipeExtensions.Readers.cs index 9f79680bd0..d6ea850af1 100644 --- a/src/DotNext.IO/IO/Pipelines/PipeExtensions.Readers.cs +++ b/src/DotNext.IO/IO/Pipelines/PipeExtensions.Readers.cs @@ -154,7 +154,7 @@ public static ValueTask ReadBigEndianAsync(this PipeReader reader, Cancell { LengthFormat.LittleEndian => reader.ReadLittleEndianAsync(token), LengthFormat.BigEndian => reader.ReadBigEndianAsync(token), - LengthFormat.Compressed => ReadAsync(reader, new(), token), + LengthFormat.Compressed => ReadAsync>(reader, new(), token), _ => ValueTask.FromException(new ArgumentOutOfRangeException(nameof(lengthFormat))), }; @@ -177,7 +177,7 @@ public static async ValueTask> DecodeAsync(this PipeReader rea MemoryOwner result; if (length > 0) { - result = allocator.AllocateAtLeast(context.Encoding.GetMaxCharCount(length)); + result = allocator.AllocateAtLeast(context.Encoding.GetMaxCharCount(length)); result.TryResize(await ReadAsync(reader, new(in context, length, result.Memory), token).ConfigureAwait(false)); } else diff --git a/src/DotNext.IO/IO/Pipelines/PipeReaderWriter.cs b/src/DotNext.IO/IO/Pipelines/PipeReaderWriter.cs index 12565e3d48..58d5233671 100644 --- a/src/DotNext.IO/IO/Pipelines/PipeReaderWriter.cs +++ b/src/DotNext.IO/IO/Pipelines/PipeReaderWriter.cs @@ -27,7 +27,7 @@ ValueTask IAsyncBinaryReader.ReadAsync(Memory output, CancellationToken to => reader.ReadExactlyAsync(output, token); ValueTask IAsyncBinaryReader.ReadAsync(TReader parser, CancellationToken token) - => PipeExtensions.ReadAsync>(reader, parser, token); + => PipeExtensions.ReadAsync>(reader, parser, token); ValueTask IAsyncBinaryReader.SkipAsync(long length, CancellationToken token) => reader.SkipAsync(length, token); diff --git a/src/DotNext.IO/IO/UnbufferedFileStream.Utils.cs b/src/DotNext.IO/IO/RandomAccessStream.Utils.cs similarity index 97% rename from src/DotNext.IO/IO/UnbufferedFileStream.Utils.cs rename to src/DotNext.IO/IO/RandomAccessStream.Utils.cs index 9a2f24f08f..30a72953df 100644 --- a/src/DotNext.IO/IO/UnbufferedFileStream.Utils.cs +++ b/src/DotNext.IO/IO/RandomAccessStream.Utils.cs @@ -3,7 +3,7 @@ namespace DotNext.IO; -internal partial class UnbufferedFileStream : IValueTaskSource, IValueTaskSource +public partial class RandomAccessStream : IValueTaskSource, IValueTaskSource { private ManualResetValueTaskSourceCore source; private int bytesWritten; diff --git a/src/DotNext.IO/IO/RandomAccessStream.cs b/src/DotNext.IO/IO/RandomAccessStream.cs new file mode 100644 index 0000000000..eb6b256f63 --- /dev/null +++ b/src/DotNext.IO/IO/RandomAccessStream.cs @@ -0,0 +1,158 @@ +using System.Runtime.CompilerServices; + +namespace DotNext.IO; + +/// +/// Represents a stream over the storage that supports random access. +/// +public abstract partial class RandomAccessStream : Stream, IFlushable +{ + private long position; + + /// + public sealed override long Position + { + get => position; + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value); + + position = value; + } + } + + private void Advance(int count) => position += count; + + /// + /// Writes the bytes at the specified offset. + /// + /// The buffer to write. + /// The offset within the underlying data storage. + protected abstract void Write(ReadOnlySpan buffer, long offset); + + /// + /// Writes the bytes at the specified offset. + /// + /// The buffer to write. + /// The offset within the underlying data storage. + /// The token that can be used to cancel the operation. + /// The task representing asynchronous operation. + protected abstract ValueTask WriteAsync(ReadOnlyMemory buffer, long offset, CancellationToken token); + + /// + public sealed override void Write(byte[] buffer, int offset, int count) + { + ValidateBufferArguments(buffer, offset, count); + + Write(new ReadOnlySpan(buffer, offset, count)); + } + + /// + public sealed override void Write(ReadOnlySpan buffer) + { + Write(buffer, position); + Advance(buffer.Length); + } + + /// + public sealed override void WriteByte(byte value) + => Write(new ReadOnlySpan(in value)); + + /// + public sealed override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken token = default) + => SubmitWrite(WriteAsync(buffer, position, token), buffer.Length); + + /// + public sealed override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token) + { + ValidateBufferArguments(buffer, offset, count); + + await WriteAsync(buffer, position, token).ConfigureAwait(false); + Advance(count); + } + + /// + /// Reads bytes to the specified buffer. + /// + /// The buffer to be modified. + /// The offset within the underlying data storage. + /// The number of bytes read. + protected abstract int Read(Span buffer, long offset); + + /// + /// Reads bytes to the specified buffer. + /// + /// The buffer to be modified. + /// The offset within the underlying data storage. + /// The token that can be used to cancel the operation. + /// The number of bytes read. + protected abstract ValueTask ReadAsync(Memory buffer, long offset, CancellationToken token); + + /// + public sealed override int Read(byte[] buffer, int offset, int count) + { + ValidateBufferArguments(buffer, offset, count); + + return Read(buffer.AsSpan(offset, count)); + } + + /// + public sealed override int Read(Span buffer) + { + var bytesRead = Read(buffer, position); + Advance(bytesRead); + return bytesRead; + } + + /// + public sealed override int ReadByte() + { + Unsafe.SkipInit(out byte result); + + return Read(new Span(ref result)) is not 0 ? result : -1; + } + + /// + public sealed override ValueTask ReadAsync(Memory buffer, CancellationToken token = default) + => SubmitRead(ReadAsync(buffer, position, token)); + + /// + public sealed override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token) + { + ValidateBufferArguments(buffer, offset, count); + + var bytesRead = await ReadAsync(buffer, position, token).ConfigureAwait(false); + Advance(bytesRead); + return bytesRead; + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + var newPosition = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => position + offset, + SeekOrigin.End => Length + offset, + _ => throw new ArgumentOutOfRangeException(nameof(origin)), + }; + + return position = newPosition >= 0L + ? newPosition + : throw new IOException(); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + readCallback = writeCallback = null; // help GC + readTask = default; + writeTask = default; + source = default; + } + + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/src/DotNext.IO/IO/Segment.cs b/src/DotNext.IO/IO/Segment.cs index 217a2b0d65..69d56401a2 100644 --- a/src/DotNext.IO/IO/Segment.cs +++ b/src/DotNext.IO/IO/Segment.cs @@ -4,12 +4,8 @@ namespace DotNext.IO; [StructLayout(LayoutKind.Auto)] -internal readonly record struct Segment +internal readonly record struct Segment(int Length, long Offset) { - internal int Length { get; init; } - - internal long Offset { get; init; } - private long End => Length + Offset; public static Segment operator >>(in Segment segment, int length) diff --git a/src/DotNext.IO/IO/SequenceReader.cs b/src/DotNext.IO/IO/SequenceReader.cs index 2ff2be3fdd..7969dc918d 100644 --- a/src/DotNext.IO/IO/SequenceReader.cs +++ b/src/DotNext.IO/IO/SequenceReader.cs @@ -271,8 +271,8 @@ public MemoryOwner ReadBlock(LengthFormat lengthFormat, MemoryAllocator(ref parser); + var parser = new Leb128Reader(); + return Read>(ref parser); } private int ReadLength(LengthFormat lengthFormat) => lengthFormat switch diff --git a/src/DotNext.IO/IO/SparseStream.cs b/src/DotNext.IO/IO/SparseStream.cs index f74bf86450..eaa21bcbc2 100644 --- a/src/DotNext.IO/IO/SparseStream.cs +++ b/src/DotNext.IO/IO/SparseStream.cs @@ -82,7 +82,7 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation } /// - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token = default) + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token) => ReadAsync(buffer.AsMemory(offset, count), token).AsTask(); /// @@ -95,7 +95,7 @@ public override void CopyTo(Stream destination, int bufferSize) } /// - public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken token = default) + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken token) { ValidateCopyToArguments(destination, bufferSize); @@ -119,15 +119,15 @@ public override long Position set => throw new NotSupportedException(); } - /// + /// public override void Flush() { if (streamAvailable) enumerator.Current.Flush(); } - /// - public override Task FlushAsync(CancellationToken token = default) + /// + public override Task FlushAsync(CancellationToken token) => streamAvailable ? enumerator.Current.FlushAsync(token) : Task.CompletedTask; /// @@ -149,7 +149,7 @@ public override Task FlushAsync(CancellationToken token = default) public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(); /// - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token = default) => Task.FromException(new NotSupportedException()); + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token) => Task.FromException(new NotSupportedException()); /// public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) diff --git a/src/DotNext.IO/IO/StreamBinaryAccessor.cs b/src/DotNext.IO/IO/StreamBinaryAccessor.cs index 17524b78f5..386b66fbc3 100644 --- a/src/DotNext.IO/IO/StreamBinaryAccessor.cs +++ b/src/DotNext.IO/IO/StreamBinaryAccessor.cs @@ -28,7 +28,7 @@ ValueTask IAsyncBinaryReader.ReadAsync(CancellationToken token) => stream.ReadAsync(buffer, token); ValueTask IAsyncBinaryReader.ReadAsync(TReader reader, CancellationToken token) - => StreamExtensions.ReadAsync>(stream, reader, buffer, token); + => StreamExtensions.ReadAsync>(stream, reader, buffer, token); ValueTask IAsyncBinaryReader.ReadAsync(Memory output, CancellationToken token) => stream.ReadExactlyAsync(output, token); diff --git a/src/DotNext.IO/IO/StreamExtensions.Readers.cs b/src/DotNext.IO/IO/StreamExtensions.Readers.cs index d06f9f8f23..eef6e8b1b7 100644 --- a/src/DotNext.IO/IO/StreamExtensions.Readers.cs +++ b/src/DotNext.IO/IO/StreamExtensions.Readers.cs @@ -118,7 +118,7 @@ private static unsafe ValueTask ReadLengthAsync(this Stream stream, LengthF return reader(stream, buffer, token); static ValueTask Read7BitEncodedIntAsync(Stream stream, Memory buffer, CancellationToken token) - => ReadAsync(stream, new(), buffer, token); + => ReadAsync>(stream, new(), buffer, token); } /// @@ -136,7 +136,7 @@ public static async ValueTask> ReadBlockAsync(this Stream stre { MemoryOwner result; int length; - using (result = allocator.AllocateExactly(SevenBitEncodedInt.MaxSize)) + using (result = allocator.AllocateExactly(Leb128.MaxSizeInBytes)) { length = await stream.ReadLengthAsync(lengthFormat, result.Memory, token).ConfigureAwait(false); } diff --git a/src/DotNext.IO/IO/StreamExtensions.Writers.cs b/src/DotNext.IO/IO/StreamExtensions.Writers.cs index 8abb3280ad..f34e6fee71 100644 --- a/src/DotNext.IO/IO/StreamExtensions.Writers.cs +++ b/src/DotNext.IO/IO/StreamExtensions.Writers.cs @@ -191,7 +191,7 @@ public static async ValueTask FormatAsync(this Stream stream, T value, L Memory bufferForLength; if (lengthFormat.HasValue) { - bufferForLength = buffer.Slice(0, SevenBitEncodedInt.MaxSize); + bufferForLength = buffer.Slice(0, Leb128.MaxSizeInBytes); buffer = buffer.Slice(bufferForLength.Length); } else diff --git a/src/DotNext.IO/IO/UnbufferedFileStream.cs b/src/DotNext.IO/IO/UnbufferedFileStream.cs index 1b6abeed67..8b8bcc4cbd 100644 --- a/src/DotNext.IO/IO/UnbufferedFileStream.cs +++ b/src/DotNext.IO/IO/UnbufferedFileStream.cs @@ -3,10 +3,9 @@ namespace DotNext.IO; -internal sealed partial class UnbufferedFileStream(SafeFileHandle handle, FileAccess access) : Stream, IFlushable +internal sealed class UnbufferedFileStream(SafeFileHandle handle, FileAccess access) : RandomAccessStream { private static readonly Action FlushToDiskAction = RandomAccess.FlushToDisk; - private long position; public override bool CanRead => access.HasFlag(FileAccess.Read); @@ -34,119 +33,28 @@ private static bool CheckSeekable(SafeFileHandle handle) public override long Length => RandomAccess.GetLength(handle); - public override long Position - { - get => position; - set - { - ArgumentOutOfRangeException.ThrowIfNegative(position); - - position = value; - } - } - public override void Flush() => RandomAccess.FlushToDisk(handle); public override Task FlushAsync(CancellationToken token) => Task.Run(FlushToDiskAction.Bind(handle), token); - - public override int Read(byte[] buffer, int offset, int count) - { - ValidateBufferArguments(buffer, offset, count); - - return Read(new Span(buffer, offset, count)); - } - - public override int Read(Span buffer) - { - var bytesRead = RandomAccess.Read(handle, buffer, position); - Advance(bytesRead); - return bytesRead; - } - - public override int ReadByte() - { - Unsafe.SkipInit(out byte result); - - return Read(new Span(ref result)) is not 0 ? result : -1; - } - - public override ValueTask ReadAsync(Memory buffer, CancellationToken token) - => SubmitRead(RandomAccess.ReadAsync(handle, buffer, position, token)); - - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token) - { - ValidateBufferArguments(buffer, offset, count); - - var bytesRead = await RandomAccess.ReadAsync(handle, buffer.AsMemory(offset, count), position, token).ConfigureAwait(false); - Advance(bytesRead); - return bytesRead; - } - - public override long Seek(long offset, SeekOrigin origin) - { - var newPosition = origin switch - { - SeekOrigin.Begin => offset, - SeekOrigin.Current => position + offset, - SeekOrigin.End => Length + offset, - _ => throw new ArgumentOutOfRangeException(nameof(origin)), - }; - - return position = newPosition >= 0L - ? newPosition - : throw new IOException(); - } - + public override void SetLength(long value) { ArgumentOutOfRangeException.ThrowIfNegative(value); RandomAccess.SetLength(handle, value); - - if (position > value) - position = value; - } - - public override void Write(byte[] buffer, int offset, int count) - { - ValidateBufferArguments(buffer, offset, count); - - Write(new ReadOnlySpan(buffer, offset, count)); - } - - public override void Write(ReadOnlySpan buffer) - { - RandomAccess.Write(handle, buffer, position); - Advance(buffer.Length); + Position = long.Clamp(Position, 0L, value); } - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken token) - => SubmitWrite(RandomAccess.WriteAsync(handle, buffer, position, token), buffer.Length); - - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token) - { - ValidateBufferArguments(buffer, offset, count); - - await RandomAccess.WriteAsync(handle, new ReadOnlyMemory(buffer, offset, count), position, token).ConfigureAwait(false); - Advance(count); - } + protected override void Write(ReadOnlySpan buffer, long offset) + => RandomAccess.Write(handle, buffer, offset); - public override void WriteByte(byte value) - => Write(new ReadOnlySpan(ref value)); + protected override ValueTask WriteAsync(ReadOnlyMemory buffer, long offset, CancellationToken token) + => RandomAccess.WriteAsync(handle, buffer, offset, token); - private void Advance(int count) => position += count; + protected override int Read(Span buffer, long offset) + => RandomAccess.Read(handle, buffer, offset); - protected override void Dispose(bool disposing) - { - if (disposing) - { - readCallback = writeCallback = null; // help GC - readTask = default; - writeTask = default; - source = default; - } - - base.Dispose(disposing); - } + protected override ValueTask ReadAsync(Memory buffer, long offset, CancellationToken token) + => RandomAccess.ReadAsync(handle, buffer, offset, token); } \ No newline at end of file diff --git a/src/DotNext.IO/Text/EncodingContext.cs b/src/DotNext.IO/Text/EncodingContext.cs index 693e06b55a..eede903007 100644 --- a/src/DotNext.IO/Text/EncodingContext.cs +++ b/src/DotNext.IO/Text/EncodingContext.cs @@ -44,6 +44,8 @@ public readonly struct EncodingContext(Encoding encoding, bool reuseEncoder) : I internal Encoder GetEncoder() => encoder ?? Encoding.GetEncoder(); + internal Encoder? TryGetEncoder() => encoder; + /// /// Creates encoding context. /// diff --git a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj index c295a09d78..0bd13365ec 100644 --- a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj +++ b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj @@ -8,7 +8,7 @@ true false nullablePublicOnly - 5.14.0 + 5.16.0 .NET Foundation .NEXT Family of Libraries diff --git a/src/DotNext.Tests/Buffers/Binary/Leb128Tests.cs b/src/DotNext.Tests/Buffers/Binary/Leb128Tests.cs new file mode 100644 index 0000000000..9316f8f224 --- /dev/null +++ b/src/DotNext.Tests/Buffers/Binary/Leb128Tests.cs @@ -0,0 +1,39 @@ +using System.Numerics; + +namespace DotNext.Buffers.Binary; + +public sealed class Leb128Tests : Test +{ + private static void EncodeDecode(ReadOnlySpan values) + where T : struct, IBinaryInteger + { + Span buffer = stackalloc byte[Leb128.MaxSizeInBytes]; + + foreach (var expected in values) + { + True(Leb128.TryGetBytes(expected, buffer, out var bytesWritten)); + True(Leb128.TryParse(buffer, out var actual, out var bytesConsumed)); + Equal(bytesWritten, bytesConsumed); + Equal(expected, actual); + } + } + + [Fact] + public static void EncodeDecodeInt32() => EncodeDecode([0, int.MaxValue, int.MinValue, 0x80, -1]); + + [Fact] + public static void EncodeDecodeInt64() => EncodeDecode([0L, long.MaxValue, long.MinValue, 0x80L, -1L]); + + [Fact] + public static void EncodeDecodeInt128() => EncodeDecode([0, Int128.MaxValue, Int128.MinValue, 0x80, Int128.NegativeOne]); + + [Fact] + public static void EncodeDecodeUInt32() => EncodeDecode([uint.MinValue, uint.MaxValue, 0x80U]); + + [Fact] + public static void EncodeDecodeEmptyBuffer() + { + False(Leb128.TryGetBytes(42, Span.Empty, out _)); + False(Leb128.TryParse(ReadOnlySpan.Empty, out _, out _)); + } +} \ No newline at end of file diff --git a/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs b/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs index 8dac05303d..ce8975f6ba 100644 --- a/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs +++ b/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs @@ -5,6 +5,10 @@ namespace DotNext.Buffers; +using Binary; +using IO; +using static DotNext.Text.EncodingExtensions; + public sealed class BufferWriterSlimTests : Test { [Fact] @@ -163,6 +167,31 @@ public static void EscapeBuffer() owner.Dispose(); } + [Fact] + public static void DetachOrCopyBuffer() + { + using var writer = new BufferWriterSlim(stackalloc int[2]); + writer.Add(10); + writer.Add(20); + + using (var buffer = writer.DetachOrCopyBuffer()) + { + Equal([10, 20], buffer.Span); + } + + True(writer.WrittenCount is 0); + + // overflow + writer.Add(10); + writer.Add(20); + writer.Add(30); + + using (var buffer = writer.DetachOrCopyBuffer()) + { + Equal([10, 20, 30], buffer.Span); + } + } + [Fact] public static void FormatValues() { @@ -335,4 +364,95 @@ public static void ReadWriteBigInteger() Equal(expected, new BigInteger(writer.WrittenSpan)); } + + private static void EncodeDecode(ReadOnlySpan values) + where T : struct, IBinaryInteger + { + Span buffer = stackalloc byte[Leb128.MaxSizeInBytes]; + var writer = new BufferWriterSlim(buffer); + var reader = new SpanReader(buffer); + + foreach (var expected in values) + { + writer.Clear(reuseBuffer: true); + reader.Reset(); + + True(writer.WriteLeb128(expected) > 0); + Equal(expected, reader.ReadLeb128()); + } + } + + [Fact] + public static void EncodeDecodeInt32() => EncodeDecode([0, int.MaxValue, int.MinValue, 0x80, -1]); + + [Fact] + public static void EncodeDecodeInt64() => EncodeDecode([0L, long.MaxValue, long.MinValue, 0x80L, -1L]); + + [Fact] + public static void EncodeDecodeUInt32() => EncodeDecode([uint.MinValue, uint.MaxValue, 0x80U]); + + [InlineData(LengthFormat.BigEndian)] + [InlineData(LengthFormat.LittleEndian)] + [InlineData(LengthFormat.Compressed)] + [Theory] + public static void WriteLengthPrefixedBytes(LengthFormat format) + { + ReadOnlySpan expected = [1, 2, 3]; + + var writer = new BufferWriterSlim(); + True(writer.Write(expected, format) > 0); + + using var buffer = writer.DetachOrCopyBuffer(); + var reader = IAsyncBinaryReader.Create(buffer.Memory); + using var actual = reader.ReadBlock(format); + Equal(expected, actual.Span); + } + + [Theory] + [InlineData("UTF-8", null)] + [InlineData("UTF-8", LengthFormat.LittleEndian)] + [InlineData("UTF-8", LengthFormat.BigEndian)] + [InlineData("UTF-8", LengthFormat.Compressed)] + [InlineData("UTF-16LE", null)] + [InlineData("UTF-16LE", LengthFormat.LittleEndian)] + [InlineData("UTF-16LE", LengthFormat.BigEndian)] + [InlineData("UTF-16LE", LengthFormat.Compressed)] + [InlineData("UTF-16BE", null)] + [InlineData("UTF-16BE", LengthFormat.LittleEndian)] + [InlineData("UTF-16BE", LengthFormat.BigEndian)] + [InlineData("UTF-16BE", LengthFormat.Compressed)] + [InlineData("UTF-32LE", null)] + [InlineData("UTF-32LE", LengthFormat.LittleEndian)] + [InlineData("UTF-32LE", LengthFormat.BigEndian)] + [InlineData("UTF-32LE", LengthFormat.Compressed)] + [InlineData("UTF-32BE", null)] + [InlineData("UTF-32BE", LengthFormat.LittleEndian)] + [InlineData("UTF-32BE", LengthFormat.BigEndian)] + [InlineData("UTF-32BE", LengthFormat.Compressed)] + public static void EncodeDecodeString(string encodingName, LengthFormat? format) + { + var encoding = Encoding.GetEncoding(encodingName); + const string expected = "Hello, world!&*(@&*(fghjwgfwffgw Привет, мир!"; + var writer = new BufferWriterSlim(); + + True(writer.Encode(expected, encoding, format) > 0); + + using var buffer = writer.DetachOrCopyBuffer(); + MemoryOwner actual; + if (format.HasValue) + { + var reader = IAsyncBinaryReader.Create(buffer.Memory); + actual = reader.Decode(encoding, format.GetValueOrDefault()); + Equal(expected, actual.Span); + } + else + { + actual = encoding.GetChars(buffer.Span); + } + + using (actual) + { + Equal(expected, actual.Span); + } + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Buffers/SpanReaderWriterTests.cs b/src/DotNext.Tests/Buffers/SpanReaderWriterTests.cs index 797104c594..c246053efe 100644 --- a/src/DotNext.Tests/Buffers/SpanReaderWriterTests.cs +++ b/src/DotNext.Tests/Buffers/SpanReaderWriterTests.cs @@ -1,5 +1,8 @@ using System.Numerics; +using System.Runtime.InteropServices; using System.Text; +using DotNext.IO; +using DotNext.Text; namespace DotNext.Buffers; @@ -181,6 +184,20 @@ public static void ReadToEnd() Equal(new[] { 20, 30 }, reader.ReadToEnd().ToArray()); } + [Fact] + public static void SlideToEnd() + { + Span expected = stackalloc byte[3]; + var writer = new SpanWriter(expected); + writer.Add() = 10; + + var remaining = writer.SlideToEnd(); + True(expected[1..] == remaining); + + Random.Shared.NextBytes(remaining); + Equal(expected, writer.WrittenSpan); + } + [Fact] public static void ReadWritePrimitives() { @@ -400,4 +417,62 @@ public static void WriteStringBuilder(int stringLength) writer.Write(builder); Equal(builder.ToString(), writer.WrittenSpan); } + + [InlineData(LengthFormat.BigEndian)] + [InlineData(LengthFormat.LittleEndian)] + [InlineData(LengthFormat.Compressed)] + [Theory] + public static void EncodeString(LengthFormat format) + { + ReadOnlySpan expected = ['a', 'b', 'c']; + var buffer = new byte[16]; + + var writer = new SpanWriter(buffer); + True(writer.Encode(expected, Encoding.UTF8, format) > 0); + + var reader = IAsyncBinaryReader.Create(buffer.AsMemory(0, writer.WrittenCount)); + + using var actual = reader.Decode(Encoding.UTF8, lengthFormat: format); + Equal(expected, actual.Span); + } + + [InlineData(LengthFormat.BigEndian)] + [InlineData(LengthFormat.LittleEndian)] + [InlineData(LengthFormat.Compressed)] + [Theory] + public static void WriteLengthPrefixedBytes(LengthFormat format) + { + ReadOnlySpan expected = [1, 2, 3]; + var buffer = new byte[expected.Length + 5]; + + var writer = new SpanWriter(buffer); + True(writer.Write(expected, format) > 0); + + var reader = IAsyncBinaryReader.Create(buffer.AsMemory(0, writer.WrittenCount)); + using var actual = reader.ReadBlock(format); + Equal(expected, actual.Span); + } + + private static void EncodeDecodeLeb128(ReadOnlySpan values) + where T : struct, IBinaryInteger + { + Span buffer = stackalloc byte[Leb128.MaxSizeInBytes]; + var writer = new SpanWriter(buffer); + var reader = new SpanReader(buffer); + + foreach (var expected in values) + { + writer.Reset(); + reader.Reset(); + + True(writer.WriteLeb128(expected) > 0); + Equal(expected, reader.ReadLeb128()); + } + } + + [Fact] + public static void EncodeDecodeInt32() => EncodeDecodeLeb128([0, int.MaxValue, int.MinValue, 0x80, -1]); + + [Fact] + public static void EncodeDecodeInt64() => EncodeDecodeLeb128([0L, long.MaxValue, long.MinValue, 0x80L, -1L]); } \ No newline at end of file diff --git a/src/DotNext.Tests/DotNext.Tests.csproj b/src/DotNext.Tests/DotNext.Tests.csproj index c1d8c4265c..41f7a72a8e 100644 --- a/src/DotNext.Tests/DotNext.Tests.csproj +++ b/src/DotNext.Tests/DotNext.Tests.csproj @@ -6,7 +6,7 @@ latest true false - 5.11.0 + 5.16.0 false .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/DotNext.Tests/Numerics/BitVectorTests.cs b/src/DotNext.Tests/Numerics/BitVectorTests.cs index 16a964e89c..175c094e04 100644 --- a/src/DotNext.Tests/Numerics/BitVectorTests.cs +++ b/src/DotNext.Tests/Numerics/BitVectorTests.cs @@ -152,4 +152,17 @@ public static void Int64ToBits() Number.GetBits(value, buffer); True(buffer[62]); } + + [Fact] + public static void SingleBitManipulation() + { + var value = 0.SetBit(1, true); + Equal(2, value); + + True(value.IsBitSet(1)); + False(value.IsBitSet(0)); + + value = value.SetBit(1, false); + False(value.IsBitSet(1)); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Threading/AsyncCounterTests.cs b/src/DotNext.Tests/Threading/AsyncCounterTests.cs index 0af4e8a7c1..f5095fef84 100644 --- a/src/DotNext.Tests/Threading/AsyncCounterTests.cs +++ b/src/DotNext.Tests/Threading/AsyncCounterTests.cs @@ -45,4 +45,16 @@ public static void CounterOverflow() Throws(counter.Increment); } + + [Fact] + public static void DecrementSynchronously() + { + using (var counter = new AsyncCounter()) + { + False(counter.TryDecrement()); + + counter.Increment(); + True(counter.TryDecrement()); + } + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs b/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs index d8ab311d26..371deb794c 100644 --- a/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs +++ b/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs @@ -214,4 +214,27 @@ public static async Task DisposedWhenSynchronousLockAcquired() l.Dispose(); await ThrowsAsync(Func.Constant(t)); } + + [Fact] + public static async Task CancelSynchronousLock() + { + using var l = new AsyncExclusiveLock(); + using var cts = new CancellationTokenSource(); + True(l.TryAcquire()); + + var t = Task.Factory.StartNew(() => l.TryAcquire(DefaultTimeout, cts.Token), TaskCreationOptions.LongRunning); + await cts.CancelAsync(); + + False(await t); + } + + [Fact] + public static void ReentrantLock() + { + using var l = new AsyncExclusiveLock(); + True(l.TryAcquire()); + False(l.TryAcquire()); + + Throws(() => l.TryAcquire(DefaultTimeout)); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Threading/AsyncReaderWriterLockTests.cs b/src/DotNext.Tests/Threading/AsyncReaderWriterLockTests.cs index 2be071ae14..7507e1e218 100644 --- a/src/DotNext.Tests/Threading/AsyncReaderWriterLockTests.cs +++ b/src/DotNext.Tests/Threading/AsyncReaderWriterLockTests.cs @@ -226,12 +226,10 @@ public static async Task AcquireReadWriteLockSynchronously() { using var l = new AsyncReaderWriterLock(); True(l.TryEnterReadLock(DefaultTimeout)); - True(l.TryEnterReadLock(DefaultTimeout)); - Equal(2L, l.CurrentReadCount); + Equal(1L, l.CurrentReadCount); var t = Task.Factory.StartNew(() => l.TryEnterWriteLock(DefaultTimeout), TaskCreationOptions.LongRunning); - l.Release(); l.Release(); True(await t); @@ -256,4 +254,14 @@ public static async Task ResumeMultipleReadersSynchronously() bool TryEnterReadLock() => l.TryEnterReadLock(DefaultTimeout); } + + [Fact] + public static void ReentrantLock() + { + using var l = new AsyncReaderWriterLock(); + True(l.TryEnterReadLock()); + + Throws(() => l.TryEnterReadLock(DefaultTimeout)); + Throws(() => l.TryEnterWriteLock(DefaultTimeout)); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Threading/LinkedCancellationTokenSourceTests.cs b/src/DotNext.Tests/Threading/LinkedCancellationTokenSourceTests.cs index aa0a86645b..60bf3cf3b0 100644 --- a/src/DotNext.Tests/Threading/LinkedCancellationTokenSourceTests.cs +++ b/src/DotNext.Tests/Threading/LinkedCancellationTokenSourceTests.cs @@ -45,4 +45,37 @@ public static async Task DirectCancellation() Equal(linked.CancellationOrigin, linked.Token); } } + + [Fact] + public static async Task CancellationWithTimeout() + { + using var source1 = new CancellationTokenSource(); + var token = new CancellationToken(canceled: false); + using var cts = token.LinkTo(DefaultTimeout, source1.Token); + NotNull(cts); + source1.Cancel(); + + await token.WaitAsync(); + } + + [Fact] + public static async Task ConcurrentCancellation() + { + using var source1 = new CancellationTokenSource(); + using var source2 = new CancellationTokenSource(); + using var source3 = new CancellationTokenSource(); + var token = source3.Token; + + using var cts = token.LinkTo([source1.Token, source2.Token]); + NotNull(cts); + var task1 = source1.CancelAsync(); + var task2 = source2.CancelAsync(); + var task3 = source3.CancelAsync(); + + await token.WaitAsync(); + + Contains(cts.CancellationOrigin, new[] { source1.Token, source2.Token, source3.Token }); + + await Task.WhenAll(task1, task2, task3); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Threading/LockTests.cs b/src/DotNext.Tests/Threading/LockTests.cs index 938f1fe853..a336124ed4 100644 --- a/src/DotNext.Tests/Threading/LockTests.cs +++ b/src/DotNext.Tests/Threading/LockTests.cs @@ -47,4 +47,16 @@ public static void SemaphoreLock() holder.Dispose(); Equal(3, sem.CurrentCount); } + + [Fact] + public static async Task InterruptibleLock() + { + using var cts = new CancellationTokenSource(); + var obj = new object(); + + Monitor.Enter(obj); + var task = Task.Factory.StartNew(() => Lock.TryEnterMonitor(obj, System.Threading.Timeout.InfiniteTimeSpan, cts.Token), TaskCreationOptions.LongRunning); + await cts.CancelAsync(); + False(await task); + } } \ No newline at end of file diff --git a/src/DotNext.Threading/DotNext.Threading.csproj b/src/DotNext.Threading/DotNext.Threading.csproj index 7269a17fd9..9988c2b44c 100644 --- a/src/DotNext.Threading/DotNext.Threading.csproj +++ b/src/DotNext.Threading/DotNext.Threading.csproj @@ -7,7 +7,7 @@ true true nullablePublicOnly - 5.15.0 + 5.16.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.cs b/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.cs index 6d4d77fcc6..0e13d071bd 100644 --- a/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.cs +++ b/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.cs @@ -227,7 +227,7 @@ public bool TryRead(TKey key, out ReadSession session) /// Tries to invalidate cache record associated with the provided key synchronously. /// /// The key of the cache record to be removed. - /// + /// The time to wait for the cache lock. /// The session that can be used to read the removed cache record. /// if the record associated with exists; otherwise, . /// The internal lock cannot be acquired in timely manner. @@ -308,6 +308,14 @@ public async ValueTask InvalidateAsync(TKey key, CancellationToken token = return true; } + /// + /// Invalidates the cache record associated with the specified key. + /// + /// The key of the cache record to be removed. + /// The time to wait for the cache lock. + /// if the cache record associated with is removed successfully; otherwise, . + /// The internal lock cannot be acquired in timely manner. + /// The cache is disposed. public bool Invalidate(TKey key, TimeSpan timeout) { var keyComparerCopy = KeyComparer; diff --git a/src/DotNext.Threading/Threading/AsyncAutoResetEvent.cs b/src/DotNext.Threading/Threading/AsyncAutoResetEvent.cs index 8867c7ca21..66f1095df1 100644 --- a/src/DotNext.Threading/Threading/AsyncAutoResetEvent.cs +++ b/src/DotNext.Threading/Threading/AsyncAutoResetEvent.cs @@ -23,7 +23,7 @@ internal StateManager(bool initialState) readonly bool ILockManager.IsLockAllowed => Value; - void ILockManager.AcquireLock() => Value = false; + void ILockManager.AcquireLock(bool synchronously) => Value = false; } private ValueTaskPool> pool; @@ -79,7 +79,7 @@ public bool Reset() ObjectDisposedException.ThrowIf(IsDisposed, this); Monitor.Enter(SyncRoot); - var result = TryAcquire(ref manager); + var result = TryAcquire(ref manager, synchronously: true); Monitor.Exit(SyncRoot); return result; @@ -173,20 +173,26 @@ public bool Wait(TimeSpan timeout) private bool Wait(Timeout timeout) { bool result; - lock (SyncRoot) + if (result = timeout.TryGetRemainingTime(out var remainingTime) && Monitor.TryEnter(SyncRoot, remainingTime)) { - if (TryAcquire(ref manager)) - { - result = true; - } - else if (timeout.TryGetRemainingTime(out var remainingTime) && Monitor.Wait(SyncRoot, remainingTime)) + try { - result = true; - manager.Value = false; + if (TryAcquire(ref manager, synchronously: true)) + { + // nothing to do + } + else if (timeout.TryGetRemainingTime(out remainingTime) && Monitor.Wait(SyncRoot, remainingTime)) + { + manager.Value = false; + } + else + { + result = false; + } } - else + finally { - result = false; + Monitor.Exit(SyncRoot); } } diff --git a/src/DotNext.Threading/Threading/AsyncCountdownEvent.cs b/src/DotNext.Threading/Threading/AsyncCountdownEvent.cs index a13b5dc61e..56e7a33ba9 100644 --- a/src/DotNext.Threading/Threading/AsyncCountdownEvent.cs +++ b/src/DotNext.Threading/Threading/AsyncCountdownEvent.cs @@ -33,7 +33,7 @@ internal void IncrementInitial(long value) internal bool Decrement(long value = 1L) => (Current = Math.Max(0L, Current - value)) is 0L; - readonly void ILockManager.AcquireLock() + readonly void ILockManager.AcquireLock(bool synchronously) { // nothing to do here } diff --git a/src/DotNext.Threading/Threading/AsyncCounter.cs b/src/DotNext.Threading/Threading/AsyncCounter.cs index 11cee7635a..05d666b641 100644 --- a/src/DotNext.Threading/Threading/AsyncCounter.cs +++ b/src/DotNext.Threading/Threading/AsyncCounter.cs @@ -36,7 +36,7 @@ internal bool TryReset() readonly bool ILockManager.IsLockAllowed => Value > 0L; - void ILockManager.AcquireLock() => Decrement(); + void ILockManager.AcquireLock(bool synchronously) => Decrement(); } private ValueTaskPool> pool; @@ -203,7 +203,7 @@ public bool TryDecrement() ObjectDisposedException.ThrowIf(IsDisposed, this); Monitor.Enter(SyncRoot); - var result = TryAcquire(ref manager); + var result = TryAcquire(ref manager, synchronously: true); Monitor.Exit(SyncRoot); return result; diff --git a/src/DotNext.Threading/Threading/AsyncEventHub.cs b/src/DotNext.Threading/Threading/AsyncEventHub.cs index 81152a698a..6190f5455f 100644 --- a/src/DotNext.Threading/Threading/AsyncEventHub.cs +++ b/src/DotNext.Threading/Threading/AsyncEventHub.cs @@ -562,7 +562,7 @@ internal WaitAllManager(AsyncEventHub stateHolder, in UInt128 mask) bool ILockManager.IsLockAllowed => (state.Value & mask) == mask; - void ILockManager.AcquireLock() + void ILockManager.AcquireLock(bool synchronously) { // no need to reset events } @@ -595,7 +595,7 @@ internal ICollection? Events bool ILockManager.IsLockAllowed => (state.Value & mask) != UInt128.Zero; - void ILockManager.AcquireLock() + void ILockManager.AcquireLock(bool synchronously) { if (Events is { } collection) FillIndices(state.Value & mask, collection); diff --git a/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs b/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs index a272c06d90..2485169cf3 100644 --- a/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs +++ b/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs @@ -16,23 +16,22 @@ public class AsyncExclusiveLock : QueuedSynchronizer, IAsyncDisposable [StructLayout(LayoutKind.Auto)] private struct LockManager : ILockManager { - private bool state; + // null - not acquired, Sentinel.Instance - acquired asynchronously, Thread - acquired synchronously + private object? state; - internal readonly bool Value => state; + internal readonly bool Value => state is not null; - internal readonly bool VolatileRead() => Volatile.Read(in state); + internal readonly bool VolatileRead() => Volatile.Read(in state) is not null; - public readonly bool IsLockAllowed => !state; + public readonly bool IsLockAllowed => state is null; - public void AcquireLock() - { - state = true; - } + public void AcquireLock(bool synchronously) + => state = synchronously ? Thread.CurrentThread : Sentinel.Instance; - internal void ExitLock() - { - state = false; - } + internal void ExitLock() => state = null; + + readonly bool ILockManager.IsLockHeldByCurrentThread + => ReferenceEquals(state, Thread.CurrentThread); } private ValueTaskPool> pool; @@ -92,7 +91,7 @@ public bool TryAcquire() private bool TryAcquireCore() { Monitor.Enter(SyncRoot); - var result = TryAcquire(ref manager); + var result = TryAcquire(ref manager, synchronously: true); Monitor.Exit(SyncRoot); return result; @@ -102,14 +101,21 @@ private bool TryAcquireCore() /// Tries to acquire the lock synchronously. /// /// The interval to wait for the lock. - /// if the lock is acquired in timely manner; otherwise, . + /// The token that can be used to cancel the operation. + /// if the lock is acquired in timely manner; if canceled or timed out. /// is negative. /// This object has been disposed. + /// The lock is already acquired by the current thread. [UnsupportedOSPlatform("browser")] - public bool TryAcquire(TimeSpan timeout) + public bool TryAcquire(TimeSpan timeout, CancellationToken token = default) { ObjectDisposedException.ThrowIf(IsDisposed, this); - return timeout == TimeSpan.Zero ? TryAcquireCore() : TryAcquire(new Timeout(timeout), ref manager); + + return timeout == TimeSpan.Zero + ? TryAcquireCore() + : token.CanBeCanceled + ? TryAcquire(new Timeout(timeout), ref manager, token) + : TryAcquire(new Timeout(timeout), ref manager); } /// @@ -222,7 +228,7 @@ public ValueTask StealAsync(object? reason = null, CancellationToken token = def // skip dead node if (RemoveAndSignal(current, out var resumable)) { - manager.AcquireLock(); + manager.AcquireLock(synchronously: false); return resumable ? current : null; } } diff --git a/src/DotNext.Threading/Threading/AsyncManualResetEvent.cs b/src/DotNext.Threading/Threading/AsyncManualResetEvent.cs index 2de454feb5..3ba345ee20 100644 --- a/src/DotNext.Threading/Threading/AsyncManualResetEvent.cs +++ b/src/DotNext.Threading/Threading/AsyncManualResetEvent.cs @@ -31,7 +31,7 @@ internal bool TryReset() readonly bool ILockManager.IsLockAllowed => Value; - readonly void ILockManager.AcquireLock() + readonly void ILockManager.AcquireLock(bool synchronously) { // nothing to do here } @@ -173,11 +173,25 @@ public bool Wait(TimeSpan timeout) [UnsupportedOSPlatform("browser")] private bool Wait(Timeout timeout) { - lock (SyncRoot) + bool result; + if (timeout.TryGetRemainingTime(out var remainingTime) && Monitor.TryEnter(SyncRoot, remainingTime)) + { + try + { + result = TryAcquire(ref manager, synchronously: true) || + timeout.TryGetRemainingTime(out remainingTime) + && Monitor.Wait(SyncRoot, remainingTime); + } + finally + { + Monitor.Exit(SyncRoot); + } + } + else { - return TryAcquire(ref manager) || - timeout.TryGetRemainingTime(out var remainingTime) - && Monitor.Wait(SyncRoot, remainingTime); + result = false; } + + return result; } } \ No newline at end of file diff --git a/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs b/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs index b739393701..583f2d5e6b 100644 --- a/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs +++ b/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs @@ -36,13 +36,14 @@ private enum LockType : byte // describes internal state of reader/writer lock [StructLayout(LayoutKind.Auto)] - internal struct State + internal struct State : IDisposable { private ulong version; // version of write lock // number of acquired read locks private long readLocks; // volatile private bool writeLock; + private ThreadLocal? lockOwnerState; internal readonly bool WriteLock => Volatile.Read(ref Unsafe.AsRef(in writeLock)); @@ -65,6 +66,7 @@ internal bool ExitLock() readLocks--; } + IsLockHelpByCurrentThread = false; return result; } @@ -76,16 +78,47 @@ internal bool ExitLock() internal readonly bool IsUpgradeToWriteLockAllowed => writeLock is false && readLocks is 1L; - internal void AcquireWriteLock() + internal void AcquireWriteLock(bool synchronously) { readLocks = 0L; writeLock = true; version++; + + if (synchronously) + IsLockHelpByCurrentThread = true; } internal readonly bool IsReadLockAllowed => !writeLock; - internal void AcquireReadLock() => readLocks++; + internal void AcquireReadLock(bool synchronously) + { + readLocks++; + + if (synchronously) + IsLockHelpByCurrentThread = true; + } + + public bool IsLockHelpByCurrentThread + { + readonly get => lockOwnerState?.Value ?? false; + set + { + if (lockOwnerState is not null) + { + lockOwnerState.Value = value; + } + else if (value) + { + lockOwnerState = new() { Value = true }; + } + } + } + + public void Dispose() + { + lockOwnerState?.Dispose(); + lockOwnerState = null; + } } [StructLayout(LayoutKind.Auto)] @@ -96,8 +129,10 @@ private struct ReadLockManager : ILockManager readonly bool ILockManager.IsLockAllowed => state.IsReadLockAllowed; - void ILockManager.AcquireLock() - => state.AcquireReadLock(); + void ILockManager.AcquireLock(bool synchronously) + => state.AcquireReadLock(synchronously); + + readonly bool ILockManager.IsLockHeldByCurrentThread => state.IsLockHelpByCurrentThread; static void ILockManager.InitializeNode(WaitNode node) => node.Type = LockType.Read; @@ -111,8 +146,10 @@ private struct WriteLockManager : ILockManager readonly bool ILockManager.IsLockAllowed => state.IsWriteLockAllowed; - void ILockManager.AcquireLock() - => state.AcquireWriteLock(); + void ILockManager.AcquireLock(bool synchronously) + => state.AcquireWriteLock(synchronously); + + readonly bool ILockManager.IsLockHeldByCurrentThread => state.IsLockHelpByCurrentThread; static void ILockManager.InitializeNode(WaitNode node) => node.Type = LockType.Exclusive; @@ -126,11 +163,13 @@ private struct UpgradeManager : ILockManager readonly bool ILockManager.IsLockAllowed => state.IsUpgradeToWriteLockAllowed; - void ILockManager.AcquireLock() - => state.AcquireWriteLock(); + void ILockManager.AcquireLock(bool synchronously) + => state.AcquireWriteLock(synchronously: false); static void ILockManager.InitializeNode(WaitNode node) => node.Type = LockType.Upgrade; + + readonly bool ILockManager.IsLockHeldByCurrentThread => false; } /// @@ -294,19 +333,25 @@ public bool TryEnterReadLock() /// Tries to obtain reader lock synchronously. /// /// The time to wait. - /// if reader lock is acquired in timely manner; otherwise, . + /// The token that can be used to cancel the operation. + /// if reader lock is acquired in timely manner; if timed out or canceled. /// is negative. /// This object has been disposed. + /// The lock is already acquired by the current thread. [UnsupportedOSPlatform("browser")] - public bool TryEnterReadLock(TimeSpan timeout) + public bool TryEnterReadLock(TimeSpan timeout, CancellationToken token = default) { ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); - return TryEnter(timeout); + return TryEnter(timeout, token); } - private bool TryEnter(TimeSpan timeout) + private bool TryEnter(TimeSpan timeout, CancellationToken token) where TLockManager : struct, ILockManager - => timeout == TimeSpan.Zero ? TryEnter() : TryAcquire(new Timeout(timeout), ref GetLockManager()); + => timeout == TimeSpan.Zero + ? TryEnter() + : token.CanBeCanceled + ? TryAcquire(new Timeout(timeout), ref GetLockManager(), token) + : TryAcquire(new Timeout(timeout), ref GetLockManager()); /// /// Tries to enter the lock in read mode asynchronously, with an optional time-out. @@ -358,7 +403,7 @@ public bool TryEnterWriteLock(in LockStamp stamp) ObjectDisposedException.ThrowIf(IsDisposed, this); Monitor.Enter(SyncRoot); - var result = stamp.IsValid(in state) && TryAcquire(ref GetLockManager()); + var result = stamp.IsValid(in state) && TryAcquire(ref GetLockManager(), synchronously: true); Monitor.Exit(SyncRoot); return result; @@ -379,14 +424,16 @@ public bool TryEnterWriteLock() /// Tries to obtain writer lock synchronously. /// /// The time to wait. - /// if writer lock is acquired in timely manner; otherwise, . + /// The token that can be used to cancel the operation. + /// if writer lock is acquired in timely manner; if timed out or canceled. /// is negative. /// This object has been disposed. + /// The lock is already acquired by the current thread. [UnsupportedOSPlatform("browser")] - public bool TryEnterWriteLock(TimeSpan timeout) + public bool TryEnterWriteLock(TimeSpan timeout, CancellationToken token = default) { ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); - return TryEnter(timeout); + return TryEnter(timeout, token); } /// @@ -443,7 +490,7 @@ private bool TryEnter() where TLockManager : struct, ILockManager { Monitor.Enter(SyncRoot); - var result = TryAcquire(ref GetLockManager()); + var result = TryAcquire(ref GetLockManager(), synchronously: true); Monitor.Exit(SyncRoot); return result; @@ -551,7 +598,7 @@ public ValueTask StealWriteLockAsync(object? reason = null, CancellationToken to if (!RemoveAndSignal(current, out var resumable)) continue; - state.AcquireWriteLock(); + state.AcquireWriteLock(synchronously: false); if (resumable) detachedQueue.Add(current); @@ -564,7 +611,7 @@ public ValueTask StealWriteLockAsync(object? reason = null, CancellationToken to if (!RemoveAndSignal(current, out resumable)) continue; - state.AcquireWriteLock(); + state.AcquireWriteLock(synchronously: false); if (resumable) detachedQueue.Add(current); @@ -574,7 +621,7 @@ public ValueTask StealWriteLockAsync(object? reason = null, CancellationToken to goto exit; if (RemoveAndSignal(current, out resumable)) - state.AcquireReadLock(); + state.AcquireReadLock(synchronously: false); if (resumable) detachedQueue.Add(current); @@ -659,4 +706,15 @@ public void DowngradeFromWriteLock() } private protected sealed override bool IsReadyToDispose => state.IsWriteLockAllowed && WaitQueueHead is null; + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + state.Dispose(); + } + + base.Dispose(disposing); + } } \ No newline at end of file diff --git a/src/DotNext.Threading/Threading/AsyncSharedLock.cs b/src/DotNext.Threading/Threading/AsyncSharedLock.cs index 380b4ae4ad..d171f50ec6 100644 --- a/src/DotNext.Threading/Threading/AsyncSharedLock.cs +++ b/src/DotNext.Threading/Threading/AsyncSharedLock.cs @@ -66,7 +66,7 @@ private struct WeakLockManager : ILockManager readonly bool ILockManager.IsLockAllowed => state.IsWeakLockAllowed; - void ILockManager.AcquireLock() + void ILockManager.AcquireLock(bool synchronously) => state.AcquireWeakLock(); static void ILockManager.InitializeNode(WaitNode node) @@ -81,7 +81,7 @@ private struct StrongLockManager : ILockManager readonly bool ILockManager.IsLockAllowed => state.IsStrongLockAllowed; - void ILockManager.AcquireLock() + void ILockManager.AcquireLock(bool synchronously) => state.AcquireStrongLock(); static void ILockManager.InitializeNode(WaitNode node) @@ -154,7 +154,7 @@ private bool TryAcquire() ObjectDisposedException.ThrowIf(IsDisposed, this); Monitor.Enter(SyncRoot); - var result = TryAcquire(ref GetLockManager()); + var result = TryAcquire(ref GetLockManager(), synchronously: true); Monitor.Exit(SyncRoot); return result; diff --git a/src/DotNext.Threading/Threading/AsyncTrigger.cs b/src/DotNext.Threading/Threading/AsyncTrigger.cs index afbbfeeba2..278881d08e 100644 --- a/src/DotNext.Threading/Threading/AsyncTrigger.cs +++ b/src/DotNext.Threading/Threading/AsyncTrigger.cs @@ -19,7 +19,7 @@ public class AsyncTrigger : QueuedSynchronizer, IAsyncEvent { bool ILockManager.IsLockAllowed => false; - void ILockManager.AcquireLock() + void ILockManager.AcquireLock(bool synchronously) { // nothing to do here } @@ -304,7 +304,7 @@ private struct ConditionalLockManager : ILockManager Condition.Invoke(); - readonly void ILockManager.AcquireLock() + readonly void ILockManager.AcquireLock(bool synchronously) { } } diff --git a/src/DotNext.Threading/Threading/Channels/IChannelWriter.cs b/src/DotNext.Threading/Threading/Channels/IChannelWriter.cs index 248854beed..164401cfd9 100644 --- a/src/DotNext.Threading/Threading/Channels/IChannelWriter.cs +++ b/src/DotNext.Threading/Threading/Channels/IChannelWriter.cs @@ -4,7 +4,7 @@ namespace DotNext.Threading.Channels; using IO; -internal interface IChannelWriter : IChannel +internal interface IChannelWriter : IChannel { private const string InputTypeMeterAttribute = "dotnext.persistentchannel.input"; diff --git a/src/DotNext.Threading/Threading/LinkedCancellationTokenSource.cs b/src/DotNext.Threading/Threading/LinkedCancellationTokenSource.cs index 6aca9ee868..6499bee7cd 100644 --- a/src/DotNext.Threading/Threading/LinkedCancellationTokenSource.cs +++ b/src/DotNext.Threading/Threading/LinkedCancellationTokenSource.cs @@ -13,12 +13,14 @@ namespace DotNext.Threading; /// public abstract class LinkedCancellationTokenSource : CancellationTokenSource { - private protected static readonly Action CancellationCallback; + private Atomic.Boolean status; - static LinkedCancellationTokenSource() + private protected LinkedCancellationTokenSource() => CancellationOrigin = Token; + + private protected CancellationTokenRegistration Attach(CancellationToken token) { - CancellationCallback = OnCanceled; - + return token.UnsafeRegister(OnCanceled, this); + static void OnCanceled(object? source, CancellationToken token) { Debug.Assert(source is LinkedCancellationTokenSource); @@ -27,10 +29,6 @@ static void OnCanceled(object? source, CancellationToken token) } } - private Atomic.Boolean status; - - private protected LinkedCancellationTokenSource() => CancellationOrigin = Token; - private void Cancel(CancellationToken token) { if (status.FalseToTrue()) diff --git a/src/DotNext.Threading/Threading/LinkedTokenSourceFactory.cs b/src/DotNext.Threading/Threading/LinkedTokenSourceFactory.cs index 3c9c0b45fc..7254fb083b 100644 --- a/src/DotNext.Threading/Threading/LinkedTokenSourceFactory.cs +++ b/src/DotNext.Threading/Threading/LinkedTokenSourceFactory.cs @@ -1,3 +1,5 @@ +using System.Buffers; +using DotNext.Buffers; using Debug = System.Diagnostics.Debug; namespace DotNext.Threading; @@ -8,7 +10,7 @@ namespace DotNext.Threading; public static class LinkedTokenSourceFactory { /// - /// Links two cancellation tokens. + /// Links two cancellation tokens together. /// /// The first cancellation token. Can be modified by this method. /// The second cancellation token. @@ -34,6 +36,36 @@ public static class LinkedTokenSourceFactory return result; } + /// + /// Links multiple cancellation tokens together. + /// + /// The first cancellation token. Can be modified by this method. + /// A list of cancellation tokens to link together. + /// The linked token source; or if or are not cancelable. + public static LinkedCancellationTokenSource? LinkTo(this ref CancellationToken first, ReadOnlySpan tokens) // TODO: Add params + { + LinkedCancellationTokenSource? result; + if (tokens.IsEmpty) + { + result = null; + } + else + { + result = new MultipleLinkedCancellationTokenSource(tokens, out var isEmpty, first); + if (isEmpty) + { + result.Dispose(); + result = null; + } + else + { + first = result.Token; + } + } + + return result; + } + /// /// Links cancellation token with the timeout. /// @@ -109,19 +141,68 @@ internal Linked2CancellationTokenSource(in CancellationToken token1, in Cancella Debug.Assert(token1.CanBeCanceled); Debug.Assert(token2.CanBeCanceled); - registration1 = token1.UnsafeRegister(CancellationCallback, this); - registration2 = token2.UnsafeRegister(CancellationCallback, this); + registration1 = Attach(token1); + registration2 = Attach(token2); } protected override void Dispose(bool disposing) { if (disposing) { - registration1.Dispose(); - registration2.Dispose(); + registration1.Unregister(); + registration2.Unregister(); } base.Dispose(disposing); } } + + private sealed class MultipleLinkedCancellationTokenSource : LinkedCancellationTokenSource + { + private MemoryOwner registrations; + + internal MultipleLinkedCancellationTokenSource(ReadOnlySpan tokens, out bool isEmpty, CancellationToken first) + { + Debug.Assert(!tokens.IsEmpty); + + var writer = new BufferWriterSlim(tokens.Length); + try + { + foreach (var token in tokens) + { + if (token != first && token.CanBeCanceled) + { + writer.Add(Attach(token)); + } + } + + if (first.CanBeCanceled && writer.WrittenCount > 0) + { + writer.Add(Attach(first)); + } + + registrations = writer.DetachOrCopyBuffer(); + isEmpty = registrations.IsEmpty; + } + finally + { + writer.Dispose(); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + foreach (ref readonly var registration in registrations.Span) + { + registration.Unregister(); + } + + registrations.Dispose(); + } + + base.Dispose(disposing); + } + } } \ No newline at end of file diff --git a/src/DotNext.Threading/Threading/QueuedSynchronizer.cs b/src/DotNext.Threading/Threading/QueuedSynchronizer.cs index 8d2c43e821..28b04ae088 100644 --- a/src/DotNext.Threading/Threading/QueuedSynchronizer.cs +++ b/src/DotNext.Threading/Threading/QueuedSynchronizer.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -158,7 +159,7 @@ private protected TNode EnqueueNode(ref ValueTaskPool => EnqueueNode>(ref pool, new(flags)); - private protected bool TryAcquire(ref TLockManager manager) + private protected bool TryAcquire(ref TLockManager manager, [ConstantExpected] bool synchronously) where TLockManager : struct, ILockManager { Debug.Assert(Monitor.IsEntered(SyncRoot)); @@ -166,7 +167,7 @@ private protected bool TryAcquire(ref TLockManager manager) if (TLockManager.RequiresEmptyQueue && WaitQueueHead is not null || !manager.IsLockAllowed) return false; - manager.AcquireLock(); + manager.AcquireLock(synchronously); return true; } @@ -174,18 +175,94 @@ private protected bool TryAcquire(ref TLockManager manager) private protected bool TryAcquire(Timeout timeout, ref TLockManager manager) where TLockManager : struct, ILockManager { - lock (SyncRoot) + bool result; + if (timeout.TryGetRemainingTime(out var remainingTime) && Monitor.TryEnter(SyncRoot, remainingTime)) { - while (!TryAcquireOrThrow(ref manager)) + try { - if (timeout.TryGetRemainingTime(out var remainingTime) && Monitor.Wait(SyncRoot, remainingTime)) - continue; + if (manager.IsLockHeldByCurrentThread) + throw new LockRecursionException(); - return false; + while (!(result = TryAcquireOrThrow(ref manager))) + { + if (timeout.TryGetRemainingTime(out remainingTime) && Monitor.Wait(SyncRoot, remainingTime)) + continue; + + break; + } + } + finally + { + Monitor.Exit(SyncRoot); } } + else + { + result = false; + } - return true; + return result; + } + + [UnsupportedOSPlatform("browser")] + private protected bool TryAcquire(Timeout timeout, ref TLockManager manager, CancellationToken token) + where TLockManager : struct, ILockManager + { + Debug.Assert(token.CanBeCanceled); + + var result = false; + var entered = false; + var registration = token.UnsafeRegister(Interrupt, Thread.CurrentThread); + try + { + if (entered = timeout.TryGetRemainingTime(out var remainingTime) && Monitor.TryEnter(SyncRoot, remainingTime)) + { + if (manager.IsLockHeldByCurrentThread) + throw new LockRecursionException(); + + while (!(result = TryAcquireOrThrow(ref manager))) + { + if (timeout.TryGetRemainingTime(out remainingTime) && Monitor.Wait(SyncRoot, remainingTime)) + continue; + + goto exit; + } + } + } + catch (ThreadInterruptedException) when (token.IsCancellationRequested) + { + // nothing to do + } + finally + { + if (entered) + Monitor.Exit(SyncRoot); + + registration.Dispose(); + } + + // make sure that the interruption was not called on this thread concurrently with registration.Dispose() + if (token.IsCancellationRequested) + { + try + { + Thread.Sleep(0); // reset interrupted state + } + catch (ThreadInterruptedException) + { + // suspend exception + } + } + + exit: + return result; + + static void Interrupt(object? thread) + { + Debug.Assert(thread is Thread); + + Unsafe.As(thread).Interrupt(); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -193,7 +270,7 @@ private bool TryAcquireOrThrow(ref TLockManager manager) where TLockManager : struct, ILockManager { ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); - return TryAcquire(ref manager); + return TryAcquire(ref manager, synchronously: true); } private T AcquireAsync(ref ValueTaskPool> pool, ref TLockManager manager, TInitializer initializer, TOptions options) @@ -226,7 +303,7 @@ private T AcquireAsync(ref Value ? Interrupt(options.InterruptionReason) : null; - task = TryAcquire(ref manager) + task = TryAcquire(ref manager, synchronously: false) ? TNode.SuccessfulTask : TNode.TimedOutTask; } @@ -253,7 +330,7 @@ private T AcquireAsync(ref Value ? Interrupt(options.InterruptionReason) : null; - if (TryAcquire(ref manager)) + if (TryAcquire(ref manager, synchronously: false)) { task = TNode.SuccessfulTask; break; @@ -582,7 +659,9 @@ private protected interface ILockManager { bool IsLockAllowed { get; } - void AcquireLock(); + void AcquireLock(bool synchronously); + + bool IsLockHeldByCurrentThread => false; static virtual bool RequiresEmptyQueue => true; } diff --git a/src/DotNext.Threading/Threading/Scheduler.cs b/src/DotNext.Threading/Threading/Scheduler.cs index 4fe9bf7cb8..5d6da4a7a8 100644 --- a/src/DotNext.Threading/Threading/Scheduler.cs +++ b/src/DotNext.Threading/Threading/Scheduler.cs @@ -21,13 +21,11 @@ public static partial class Scheduler public static DelayedTask ScheduleAsync(Func callback, TArgs args, TimeSpan delay, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(callback); + Timeout.Validate(delay); - return delay.Ticks switch - { - < 0L and not Timeout.InfiniteTicks => throw new ArgumentOutOfRangeException(nameof(delay)), - 0L => new ImmediateTask(callback, args, token), - _ => DelayedTaskStateMachine.Start(callback, args, delay, token), - }; + return delay.Ticks is 0L + ? new ImmediateTask(callback, args, token) + : DelayedTaskStateMachine.Start(callback, args, delay, token); } /// @@ -45,12 +43,10 @@ public static DelayedTask ScheduleAsync(Func ScheduleAsync(Func> callback, TArgs args, TimeSpan delay, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(callback); + Timeout.Validate(delay); - return delay.Ticks switch - { - < 0L and not Timeout.InfiniteTicks or > Timeout.MaxTimeoutParameterTicks => throw new ArgumentOutOfRangeException(nameof(delay)), - 0L => new ImmediateTask(callback, args, token), - _ => DelayedTaskStateMachine.Start(callback, args, delay, token), - }; + return delay.Ticks is 0L + ? new ImmediateTask(callback, args, token) + : DelayedTaskStateMachine.Start(callback, args, delay, token); } } \ No newline at end of file diff --git a/src/DotNext.Unsafe/DotNext.Unsafe.csproj b/src/DotNext.Unsafe/DotNext.Unsafe.csproj index 2cbe4dd42f..6bdc10e192 100644 --- a/src/DotNext.Unsafe/DotNext.Unsafe.csproj +++ b/src/DotNext.Unsafe/DotNext.Unsafe.csproj @@ -7,7 +7,7 @@ enable true true - 5.14.0 + 5.16.0 nullablePublicOnly .NET Foundation and Contributors diff --git a/src/DotNext.Unsafe/Runtime/InteropServices/IUnmanagedMemory.cs b/src/DotNext.Unsafe/Runtime/InteropServices/IUnmanagedMemory.cs index 78bfc7fddd..865471c0b6 100644 --- a/src/DotNext.Unsafe/Runtime/InteropServices/IUnmanagedMemory.cs +++ b/src/DotNext.Unsafe/Runtime/InteropServices/IUnmanagedMemory.cs @@ -90,7 +90,7 @@ public interface IUnmanagedMemory : IUnmanagedMemory, IMemoryOwner, ISuppl /// Resizes a block of memory represented by this instance. /// /// - /// This method is dangerous becase it invalidates all buffers returned by property. + /// This method is dangerous because it invalidates all buffers returned by property. /// /// The new number of elements in the unmanaged array. /// The underlying unmanaged memory is released. diff --git a/src/DotNext/Buffers/Binary/Leb128.cs b/src/DotNext/Buffers/Binary/Leb128.cs new file mode 100644 index 0000000000..8afd60b497 --- /dev/null +++ b/src/DotNext/Buffers/Binary/Leb128.cs @@ -0,0 +1,218 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DotNext.Buffers.Binary; + +using Numerics; + +/// +/// Represents encoder and decoder for 7-bit encoded integers. +/// +/// The type of the integer. +/// LEB128 encoding +[StructLayout(LayoutKind.Auto)] +public struct Leb128 : ISupplier, IResettable + where T : struct, IBinaryInteger +{ + /// + /// Maximum size of encoded , in bytes. + /// + public static int MaxSizeInBytes { get; } + + private static readonly int MaxSizeInBits; + private const byte BitMask = 0x7F; + private const byte CarryBit = BitMask + 1; + + static Leb128() + { + var bitCount = Number.GetMaxByteCount() * 8; + bitCount = Math.DivRem(bitCount, 7, out var remainder); + bitCount += Unsafe.BitCast(remainder is not 0); + + MaxSizeInBytes = bitCount; + MaxSizeInBits = bitCount * 7; + } + + private ushort shift; + private T value; + + /// + /// Decodes an octet. + /// + /// The byte that represents a part of 7-bit encoded integer. + /// if the decoder expects more data to decode; if the last octet detected. + /// The maximum number of octets reached. + public bool Append(byte b) + { + if (shift == MaxSizeInBits) + ThrowInvalidDataException(); + + value |= (T.CreateTruncating(b) & T.CreateTruncating(BitMask)) << shift; + shift += 7; + + var nextOctetExpected = (b & CarryBit) is not 0; + const byte signBit = 0x40; + + // return back sign bit for signed integers + if (Number.IsSigned() && !nextOctetExpected && shift < MaxSizeInBits && (b & signBit) is not 0) + value |= T.AllBitsSet << shift; + + return nextOctetExpected; + + [DoesNotReturn] + [StackTraceHidden] + static void ThrowInvalidDataException() + => throw new InvalidDataException(); + } + + /// + /// Resets the decoder. + /// + public void Reset() => Value = default; + + /// + /// Gets a value represented by the encoded. + /// + public T Value + { + readonly get => value; + set + { + shift = 0; + this.value = value; + } + } + + /// + readonly T ISupplier.Invoke() => value; + + /// + /// Gets an enumerator over encoded octets. + /// + /// + public readonly Enumerator GetEnumerator() => new(value); + + /// + /// Tries to encode the value by using LEB128 binary format. + /// + /// The value to encode. + /// The output buffer. + /// The number of bytes written. + /// if has enough space to save the encoded value; otherwise, . + public static bool TryGetBytes(T value, Span buffer, out int bytesWritten) + { + bytesWritten = 0; + var index = 0; + foreach (var octet in new Leb128 { Value = value }) + { + if ((uint)index >= (uint)buffer.Length) + return false; + + buffer[index++] = octet; + } + + bytesWritten = index; + return true; + } + + /// + /// Decodes LEB128-encoded integer. + /// + /// The input buffer containing LEB128 octets. + /// The decoded value. + /// The number of bytes consumed from . + /// if operation is successful; otherwise, . + public static bool TryParse(ReadOnlySpan buffer, out T result, out int bytesConsumed) + { + bytesConsumed = 0; + var decoder = new Leb128(); + var successful = false; + + foreach (var octet in buffer) + { + bytesConsumed += 1; + if (successful = !decoder.Append(octet)) + break; + } + + result = decoder.Value; + return successful; + } + + /// + /// Represents an enumerator that produces 7-bit encoded integer as a sequence of octets. + /// + [StructLayout(LayoutKind.Auto)] + public struct Enumerator + { + private T value; + private byte current; + private bool completed; + + internal Enumerator(T value) => this.value = value; + + /// + /// The current octet. + /// + public readonly byte Current => current; + + /// + /// Moves to the next octet. + /// + /// if one more octet is produced; otherwise, . + public bool MoveNext() + { + if (completed) + return false; + + if (Number.IsSigned()) + { + MoveNextSigned(); + } + else + { + MoveNextUnsigned(); + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void MoveNextSigned() + { + var sevenBits = value & T.CreateTruncating(BitMask); + value >>= 7; + + var octet = byte.CreateTruncating(sevenBits); + if (value == -T.CreateTruncating((octet >>> 6) & 1)) + { + completed = true; + } + else + { + octet = (byte)(octet | CarryBit); + } + + current = octet; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void MoveNextUnsigned() + { + var allBitsSet = T.CreateTruncating(BitMask); + if (value > allBitsSet) + { + current = byte.CreateTruncating(value | ~allBitsSet); + value >>>= 7; + } + else + { + current = byte.CreateTruncating(value); + completed = true; + } + } + } +} \ No newline at end of file diff --git a/src/DotNext/Buffers/BufferWriterSlim.cs b/src/DotNext/Buffers/BufferWriterSlim.cs index cd3b00983a..30c8950f24 100644 --- a/src/DotNext/Buffers/BufferWriterSlim.cs +++ b/src/DotNext/Buffers/BufferWriterSlim.cs @@ -302,6 +302,38 @@ public bool TryDetachBuffer(out MemoryOwner owner) return true; } + /// + /// Detaches or copies the underlying buffer with written content from this writer. + /// + /// Detached or copied buffer. + public MemoryOwner DetachOrCopyBuffer() + { + MemoryOwner result; + + if (position is 0) + { + result = default; + } + else + { + if (NoOverflow) + { + result = allocator.AllocateExactly(position); + initialBuffer.CopyTo(result.Span); + } + else + { + result = extraBuffer; + extraBuffer = default; + } + + result.Truncate(position); + position = 0; + } + + return result; + } + /// /// Clears the data written to the underlying buffer. /// diff --git a/src/DotNext/Buffers/ByteBuffer.cs b/src/DotNext/Buffers/ByteBuffer.cs index 71abe26927..672755fcfc 100644 --- a/src/DotNext/Buffers/ByteBuffer.cs +++ b/src/DotNext/Buffers/ByteBuffer.cs @@ -42,7 +42,7 @@ public static int WriteLittleEndian(this IBufferWriter writer, T value) for (var destination = writer.GetSpan(); !value.TryWriteLittleEndian(destination, out length); destination = writer.GetSpan(length)) { length = destination.Length; - length = length <= MaxBufferSize ? length << 1 : throw new InsufficientMemoryException(); + length = length <= MaxBufferSize ? length << 1 : throw new InsufficientMemoryException(ExceptionMessages.NotEnoughMemory); } writer.Advance(length); @@ -64,7 +64,7 @@ public static int WriteBigEndian(this IBufferWriter writer, T value) for (var destination = writer.GetSpan(); !value.TryWriteBigEndian(destination, out length); destination = writer.GetSpan(length)) { length = destination.Length; - length = length <= MaxBufferSize ? length << 1 : throw new InsufficientMemoryException(); + length = length <= MaxBufferSize ? length << 1 : throw new InsufficientMemoryException(ExceptionMessages.NotEnoughMemory); } writer.Advance(length); @@ -84,7 +84,7 @@ public static int Write(this IBufferWriter writer, in BigInteger value, bo { var buffer = writer.GetSpan(value.GetByteCount(isUnsigned)); if (!value.TryWriteBytes(buffer, out var bytesWritten, isUnsigned, isBigEndian)) - throw new InsufficientMemoryException(); + throw new InsufficientMemoryException(ExceptionMessages.NotEnoughMemory); writer.Advance(bytesWritten); return bytesWritten; @@ -108,7 +108,7 @@ public static int Format(this IBufferWriter writer, T value, ReadOnlySp for (int sizeHint; !value.TryFormat(buffer, out bytesWritten, format, provider); buffer = writer.GetSpan(sizeHint)) { sizeHint = buffer.Length; - sizeHint = sizeHint <= MaxBufferSize ? sizeHint << 1 : throw new InsufficientMemoryException(); + sizeHint = sizeHint <= MaxBufferSize ? sizeHint << 1 : throw new InsufficientMemoryException(ExceptionMessages.NotEnoughMemory); } writer.Advance(bytesWritten); @@ -143,7 +143,7 @@ public static int WriteLittleEndian(this ref BufferWriterSlim writer, T for (var destination = writer.InternalGetSpan(sizeHint: 0); !value.TryWriteLittleEndian(destination, out length); destination = writer.InternalGetSpan(length)) { length = destination.Length; - length = length <= MaxBufferSize ? length << 1 : throw new InsufficientMemoryException(); + length = length <= MaxBufferSize ? length << 1 : throw new InsufficientMemoryException(ExceptionMessages.NotEnoughMemory); } writer.Advance(length); @@ -165,7 +165,7 @@ public static int WriteBigEndian(this ref BufferWriterSlim writer, T va for (var destination = writer.InternalGetSpan(sizeHint: 0); !value.TryWriteBigEndian(destination, out length); destination = writer.InternalGetSpan(length)) { length = destination.Length; - length = length <= MaxBufferSize ? length << 1 : throw new InsufficientMemoryException(); + length = length <= MaxBufferSize ? length << 1 : throw new InsufficientMemoryException(ExceptionMessages.NotEnoughMemory); } writer.Advance(length); @@ -185,7 +185,7 @@ public static int Write(this ref BufferWriterSlim writer, in BigInteger va { var buffer = writer.InternalGetSpan(value.GetByteCount(isUnsigned)); if (!value.TryWriteBytes(buffer, out var bytesWritten, isUnsigned, isBigEndian)) - throw new InsufficientMemoryException(); + throw new InsufficientMemoryException(ExceptionMessages.NotEnoughMemory); writer.Advance(bytesWritten); return bytesWritten; @@ -209,7 +209,7 @@ public static int Format(this ref BufferWriterSlim writer, T value, Rea for (int sizeHint; !value.TryFormat(buffer, out bytesWritten, format, provider); buffer = writer.InternalGetSpan(sizeHint)) { sizeHint = buffer.Length; - sizeHint = sizeHint <= MaxBufferSize ? sizeHint << 1 : throw new InsufficientMemoryException(); + sizeHint = sizeHint <= MaxBufferSize ? sizeHint << 1 : throw new InsufficientMemoryException(ExceptionMessages.NotEnoughMemory); } writer.Advance(bytesWritten); @@ -258,7 +258,7 @@ public static int WriteLittleEndian(this ref SpanWriter writer, T value where T : notnull, IBinaryInteger { if (!value.TryWriteLittleEndian(writer.RemainingSpan, out var bytesWritten)) - throw new InsufficientMemoryException(); + throw new InsufficientMemoryException(ExceptionMessages.NotEnoughMemory); writer.Advance(bytesWritten); return bytesWritten; @@ -276,7 +276,7 @@ public static int WriteBigEndian(this ref SpanWriter writer, T value) where T : notnull, IBinaryInteger { if (!value.TryWriteBigEndian(writer.RemainingSpan, out var bytesWritten)) - throw new InsufficientMemoryException(); + throw new InsufficientMemoryException(ExceptionMessages.NotEnoughMemory); writer.Advance(bytesWritten); return bytesWritten; @@ -323,6 +323,63 @@ public static bool TryFormat(this ref SpanWriter writer, T value, ReadO return result; } + + /// + /// Writes 32-bit integer in a compressed format. + /// + /// The buffer writer. + /// The integer to be written. + /// A number of bytes written to the buffer. + public static int WriteLeb128(this ref SpanWriter writer, T value) + where T : struct, IBinaryInteger + { + var count = 0; + foreach (var b in new Leb128 { Value = value }) + { + writer.Add() = b; + count += 1; + } + + return count; + } + + /// + /// Writes 32-bit integer in a compressed format. + /// + /// The buffer writer. + /// The integer to be written. + /// A number of bytes written to the buffer. + public static int WriteLeb128(this ref BufferWriterSlim writer, T value) + where T : struct, IBinaryInteger + { + var count = 0; + foreach (var b in new Leb128 { Value = value }) + { + writer.Add() = b; + count += 1; + } + + return count; + } + + /// + /// Decodes an integer encoded as 7-bit octets. + /// + /// The buffer reader. + /// The integer type. + /// The decoded integer. + public static T ReadLeb128(this ref SpanReader reader) + where T : struct, IBinaryInteger + { + var decoder = new Leb128(); + byte octet; + do + { + octet = reader.Read(); + } while (decoder.Append(octet)); + + return decoder.Value; + } /// /// Restores a value from a sequence of bytes. diff --git a/src/DotNext/Buffers/CharBuffer.cs b/src/DotNext/Buffers/CharBuffer.cs index b12b1484fd..41e66cc64b 100644 --- a/src/DotNext/Buffers/CharBuffer.cs +++ b/src/DotNext/Buffers/CharBuffer.cs @@ -65,7 +65,7 @@ public static int Format(this IBufferWriter writer, CompositeFormat format const int maxBufferSize = int.MaxValue / 2; int bufferSize; - for (bufferSize = 0; ; bufferSize = bufferSize <= maxBufferSize ? bufferSize << 1 : throw new InsufficientMemoryException()) + for (bufferSize = 0; ; bufferSize = bufferSize <= maxBufferSize ? bufferSize << 1 : throw new InsufficientMemoryException(ExceptionMessages.NotEnoughMemory)) { var buffer = writer.GetSpan(bufferSize); if (buffer.TryWrite(provider, format, out bufferSize, args)) @@ -203,7 +203,7 @@ public static int Format(this ref BufferWriterSlim writer, CompositeFormat const int maxBufferSize = int.MaxValue / 2; int bufferSize; - for (bufferSize = 0; ; bufferSize = bufferSize <= maxBufferSize ? bufferSize << 1 : throw new InsufficientMemoryException()) + for (bufferSize = 0; ; bufferSize = bufferSize <= maxBufferSize ? bufferSize << 1 : throw new InsufficientMemoryException(ExceptionMessages.NotEnoughMemory)) { var buffer = writer.InternalGetSpan(bufferSize); if (buffer.TryWrite(provider, format, out bufferSize, args)) diff --git a/src/DotNext/Buffers/MemoryOwner.cs b/src/DotNext/Buffers/MemoryOwner.cs index 9601ed8dee..66a2fa3189 100644 --- a/src/DotNext/Buffers/MemoryOwner.cs +++ b/src/DotNext/Buffers/MemoryOwner.cs @@ -21,14 +21,6 @@ public struct MemoryOwner : IMemoryOwner, ISupplier>, ISupplier< private readonly T[]? array; // not null only if owner is ArrayPool or null private int length; - private MemoryOwner(IMemoryOwner owner, int? length) - { - Debug.Assert(length.GetValueOrDefault() >= 0); - - this.owner = (this.length = length ?? owner.Memory.Length) > 0 ? owner : null; - array = null; - } - internal MemoryOwner(ArrayPool? pool, T[] array, int length) { Debug.Assert(length > 0); diff --git a/src/DotNext/Buffers/SpanWriter.cs b/src/DotNext/Buffers/SpanWriter.cs index bf4cadb324..d3b05ae1a9 100644 --- a/src/DotNext/Buffers/SpanWriter.cs +++ b/src/DotNext/Buffers/SpanWriter.cs @@ -120,7 +120,7 @@ public void Rewind(int count) public void Reset() => position = 0; /// - /// Gets the span over written elements. + /// Gets the span overwritten elements. /// /// The segment of underlying span containing written elements. public readonly Span WrittenSpan => MemoryMarshal.CreateSpan(ref reference, position); @@ -164,7 +164,7 @@ public int Write(scoped ReadOnlySpan input) /// /// The item to place. /// - /// if item has beem placed successfully; + /// if item has been placed successfully; /// if remaining space in the underlying span is not enough to place the item. /// public bool TryAdd(T item) @@ -246,6 +246,17 @@ public Span Slide(int count) return result; } + /// + /// Obtains the tail of the remaining buffer and advances to its end. + /// + /// The tail of the remaining buffer. + public Span SlideToEnd() + { + var result = RemainingSpan; + position = length; + return result; + } + /// /// Writes a portion of data. /// diff --git a/src/DotNext/Buffers/Text/Base64Decoder.Unicode.cs b/src/DotNext/Buffers/Text/Base64Decoder.Unicode.cs index 4feb099345..3fdef681d4 100644 --- a/src/DotNext/Buffers/Text/Base64Decoder.Unicode.cs +++ b/src/DotNext/Buffers/Text/Base64Decoder.Unicode.cs @@ -98,21 +98,12 @@ public MemoryOwner DecodeFromUtf16(scoped ReadOnlySpan chars, Memory goto bad_data; var bytes = new BufferWriterSlim(GetMaxDecodedLength(chars.Length), allocator); - if (!DecodeFromUtf16Buffered(chars, ref bytes)) - { - bytes.Dispose(); - goto bad_data; - } - - if (!bytes.TryDetachBuffer(out var result)) - { - result = bytes.WrittenSpan.Copy(allocator); - bytes.Dispose(); - } + if (DecodeFromUtf16Buffered(chars, ref bytes)) + return bytes.DetachOrCopyBuffer(); - return result; + bytes.Dispose(); - bad_data: + bad_data: throw new FormatException(ExceptionMessages.MalformedBase64); } diff --git a/src/DotNext/Buffers/Text/Base64Decoder.Utf8.cs b/src/DotNext/Buffers/Text/Base64Decoder.Utf8.cs index 5b97f2a25d..931f8bc3cc 100644 --- a/src/DotNext/Buffers/Text/Base64Decoder.Utf8.cs +++ b/src/DotNext/Buffers/Text/Base64Decoder.Utf8.cs @@ -94,19 +94,10 @@ public MemoryOwner DecodeFromUtf8(ReadOnlySpan chars, MemoryAllocato goto bad_data; var bytes = new BufferWriterSlim(GetMaxDecodedLength(chars.Length), allocator); - if (!DecodeFromUtf8Buffered(chars, ref bytes)) - { - bytes.Dispose(); - goto bad_data; - } - - if (!bytes.TryDetachBuffer(out var result)) - { - result = bytes.WrittenSpan.Copy(allocator); - bytes.Dispose(); - } - - return result; + if (DecodeFromUtf8Buffered(chars, ref bytes)) + return bytes.DetachOrCopyBuffer(); + + bytes.Dispose(); bad_data: throw new FormatException(ExceptionMessages.MalformedBase64); diff --git a/src/DotNext/Buffers/Text/Base64Encoder.Unicode.cs b/src/DotNext/Buffers/Text/Base64Encoder.Unicode.cs index a5228d6b00..9ffee01729 100644 --- a/src/DotNext/Buffers/Text/Base64Encoder.Unicode.cs +++ b/src/DotNext/Buffers/Text/Base64Encoder.Unicode.cs @@ -68,13 +68,7 @@ public MemoryOwner EncodeToUtf16(ReadOnlySpan bytes, MemoryAllocator var writer = new BufferWriterSlim(GetMaxEncodedLength(bytes.Length), allocator); EncodeToUtf16Buffered(bytes, ref writer, flush); - if (!writer.TryDetachBuffer(out var result)) - { - result = writer.WrittenSpan.Copy(allocator); - writer.Dispose(); - } - - return result; + return writer.DetachOrCopyBuffer(); } /// diff --git a/src/DotNext/Buffers/Text/Base64Encoder.Utf8.cs b/src/DotNext/Buffers/Text/Base64Encoder.Utf8.cs index 10593d9593..c6e7070b0d 100644 --- a/src/DotNext/Buffers/Text/Base64Encoder.Utf8.cs +++ b/src/DotNext/Buffers/Text/Base64Encoder.Utf8.cs @@ -65,13 +65,7 @@ public MemoryOwner EncodeToUtf8(ReadOnlySpan bytes, MemoryAllocator< var writer = new BufferWriterSlim(GetMaxEncodedLength(bytes.Length), allocator); EncodeToUtf8Buffered(bytes, ref writer, flush); - if (!writer.TryDetachBuffer(out var result)) - { - result = writer.WrittenSpan.Copy(allocator); - writer.Dispose(); - } - - return result; + return writer.DetachOrCopyBuffer(); } /// diff --git a/src/DotNext/Collections/Generic/Collection.Buffer.cs b/src/DotNext/Collections/Generic/Collection.Buffer.cs index f26b5b9db8..387b0a9e6b 100644 --- a/src/DotNext/Collections/Generic/Collection.Buffer.cs +++ b/src/DotNext/Collections/Generic/Collection.Buffer.cs @@ -52,10 +52,7 @@ static MemoryOwner CopySlow(IEnumerable enumerable, int sizeHint, MemoryAl foreach (var item in enumerable) writer.Add(item); - if (!writer.TryDetachBuffer(out MemoryOwner result)) - result = writer.WrittenSpan.Copy(allocator); - - return result; + return writer.DetachOrCopyBuffer(); } static int GetSize(IEnumerable enumerable, int sizeHint) diff --git a/src/DotNext/DotNext.csproj b/src/DotNext/DotNext.csproj index e468552165..878d7bfff5 100644 --- a/src/DotNext/DotNext.csproj +++ b/src/DotNext/DotNext.csproj @@ -11,14 +11,14 @@ .NET Foundation and Contributors .NEXT Family of Libraries - 5.14.0 + 5.16.0 DotNext MIT https://dotnet.github.io/dotNext/ https://github.com/dotnet/dotNext.git git - extensions;performance;hashcode;randomstring;valuetype;delegate + extensions;performance;randomstring;delegate;utf8;leb128;hex Copyright © .NET Foundation and Contributors true Provides various extensions of .NET Base Class Library diff --git a/src/DotNext/Numerics/Number.BitVector.cs b/src/DotNext/Numerics/Number.BitVector.cs index 1c178c2e55..9c7a4f578f 100644 --- a/src/DotNext/Numerics/Number.BitVector.cs +++ b/src/DotNext/Numerics/Number.BitVector.cs @@ -8,6 +8,42 @@ namespace DotNext.Numerics; public static partial class Number { + /// + /// Gets a value indicating that the specified bit is set. + /// + /// The number to inspect. + /// The position of the bit within . + /// The type of the number. + /// if the bit at is set; otherwise, . + /// is negative. + public static bool IsBitSet(this T number, int position) + where T : struct, IBinaryNumber, IShiftOperators + { + ArgumentOutOfRangeException.ThrowIfNegative(position); + + return (number & (T.One << position)) != T.Zero; + } + + /// + /// Sets the bit at the specified position. + /// + /// The number to modify. + /// The position of the bit to set. + /// The bit value. + /// The type of the number. + /// A modified number. + /// is negative. + public static T SetBit(this T number, int position, bool value) + where T : struct, IBinaryNumber, IShiftOperators + { + ArgumentOutOfRangeException.ThrowIfNegative(position); + + var bit = T.One << position; + return value + ? number | bit + : number & ~bit; + } + /// /// Converts bit vector to a value of type . /// @@ -15,7 +51,7 @@ public static partial class Number /// A vector of bits. /// A value of type restored from the vector of bits. public static TResult FromBits(this ReadOnlySpan bits) - where TResult : struct, IBinaryInteger + where TResult : struct, IBinaryNumber, IShiftOperators { var result = TResult.Zero; @@ -36,7 +72,7 @@ public static TResult FromBits(this ReadOnlySpan bits) /// A buffer to be modified. /// has not enough length. public static unsafe void GetBits(this T value, Span bits) - where T : unmanaged, IBinaryInteger + where T : unmanaged, IBinaryNumber, IShiftOperators { var sizeInBits = sizeof(T) * 8; ArgumentOutOfRangeException.ThrowIfLessThan((uint)bits.Length, (uint)sizeInBits, nameof(bits)); diff --git a/src/DotNext/Numerics/Number.cs b/src/DotNext/Numerics/Number.cs index e549daa838..e2385f4a58 100644 --- a/src/DotNext/Numerics/Number.cs +++ b/src/DotNext/Numerics/Number.cs @@ -109,7 +109,7 @@ public static float Normalize(this int value) /// if is a prime number; otherwise, . /// is negative or zero. public static bool IsPrime(T value) - where T : struct, IBinaryInteger, ISignedNumber + where T : struct, IBinaryNumber, ISignedNumber, IShiftOperators { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); @@ -183,7 +183,7 @@ static T Sqrt(T value) /// There is no prime number that is greater than and less than . [EditorBrowsable(EditorBrowsableState.Never)] public static T GetPrime(T lowerBound, ReadOnlySpan cachedPrimes = default) - where T : struct, IBinaryInteger, ISignedNumber, IMinMaxValue + where T : struct, IBinaryNumber, ISignedNumber, IMinMaxValue, IShiftOperators { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(lowerBound); diff --git a/src/DotNext/Threading/Lock.cs b/src/DotNext/Threading/Lock.cs index acd7b3078f..c50386a750 100644 --- a/src/DotNext/Threading/Lock.cs +++ b/src/DotNext/Threading/Lock.cs @@ -332,4 +332,72 @@ private readonly bool Equals(in Lock other) /// , if both are not the same; otherwise, . public static bool operator !=(in Lock first, in Lock second) => !first.Equals(in second); + + /// + /// Tries to acquire an exclusive lock on the specified object with cancellation support. + /// + /// The object on which to acquire the lock. + /// Time to wait for the lock. + /// The token that can be used to cancel the operation. + /// + /// to throw if is canceled during the lock acquisition; + /// to return . + /// + /// if the monitor acquired successfully; if timeout occurred or canceled. + /// is . + /// is invalid. + /// interrupts lock acquisition and is . + public static bool TryEnterMonitor(object obj, TimeSpan timeout, CancellationToken token, [ConstantExpected] bool throwOnCancellation = false) + { + ArgumentNullException.ThrowIfNull(obj); + Timeout.Validate(timeout); + + var result = false; + if (token.CanBeCanceled) + { + var registration = token.UnsafeRegister(Interrupt, Thread.CurrentThread); + try + { + result = System.Threading.Monitor.TryEnter(obj, timeout); + } + catch (ThreadInterruptedException e) when (token.IsCancellationRequested) + { + if (throwOnCancellation) + throw new OperationCanceledException(e.Message, e, token); + + goto exit; + } + finally + { + registration.Dispose(); + } + + // make sure that the interruption was not called on this thread concurrently with registration.Dispose() + if (token.IsCancellationRequested) + { + try + { + Thread.Sleep(0); // reset interrupted state + } + catch (ThreadInterruptedException) + { + // suspend exception + } + } + + static void Interrupt(object? thread) + { + Debug.Assert(thread is Thread); + + As(thread).Interrupt(); + } + } + else + { + result = System.Threading.Monitor.TryEnter(obj, timeout); + } + + exit: + return result; + } } \ No newline at end of file diff --git a/src/DotNext/Threading/Timeout.cs b/src/DotNext/Threading/Timeout.cs index 83a21fa4d0..98fab8c60f 100644 --- a/src/DotNext/Threading/Timeout.cs +++ b/src/DotNext/Threading/Timeout.cs @@ -1,4 +1,7 @@ -using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace DotNext.Threading; @@ -185,4 +188,21 @@ public bool TryGetRemainingTime(TimeProvider provider, out TimeSpan remainingTim /// Timeout control object. /// The original timeout value. public static implicit operator TimeSpan(in Timeout timeout) => timeout.Value; + + /// + /// Validates the timeout. + /// + /// The timeout value. + /// The name of the timeout parameter passed by the caller. + /// is negative and not ; or greater than . + public static void Validate(TimeSpan timeout, [CallerArgumentExpression(nameof(timeout))] string? parameterName = null) + { + if (timeout.Ticks is < 0L and not InfiniteTicks or > MaxTimeoutParameterTicks) + Throw(parameterName); + + [DoesNotReturn] + [StackTraceHidden] + static void Throw(string? parameterName) + => throw new ArgumentOutOfRangeException(parameterName); + } } \ No newline at end of file diff --git a/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj b/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj index 4a3be274e7..d13e488391 100644 --- a/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj +++ b/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj @@ -8,7 +8,7 @@ true true nullablePublicOnly - 5.14.0 + 5.16.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Disconnect.cs b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Disconnect.cs index 8133e623ba..e5c6dfbb65 100644 --- a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Disconnect.cs +++ b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Disconnect.cs @@ -70,8 +70,7 @@ private MemoryOwner SerializeDisconnectRequest(bool isAlive) writer.WriteEndPoint(localNode); writer.Add(Unsafe.BitCast(isAlive)); - if (!writer.TryDetachBuffer(out result)) - result = writer.WrittenSpan.Copy(allocator); + result = writer.DetachOrCopyBuffer(); } finally { diff --git a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.ForwardJoin.cs b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.ForwardJoin.cs index 0b7201faa3..9d75d2a76f 100644 --- a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.ForwardJoin.cs +++ b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.ForwardJoin.cs @@ -72,8 +72,7 @@ private MemoryOwner SerializeForwardJoinRequest(EndPoint joinedPeer, int t writer.WriteEndPoint(joinedPeer); writer.WriteLittleEndian(timeToLive); - if (!writer.TryDetachBuffer(out result)) - result = writer.WrittenSpan.Copy(allocator); + result = writer.DetachOrCopyBuffer(); } finally { diff --git a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Join.cs b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Join.cs index e1edbc69b5..679a023a0e 100644 --- a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Join.cs +++ b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Join.cs @@ -66,8 +66,7 @@ private MemoryOwner SerializeJoinRequest() { writer.WriteEndPoint(localNode); - if (!writer.TryDetachBuffer(out result)) - result = writer.WrittenSpan.Copy(allocator); + result = writer.DetachOrCopyBuffer(); } finally { diff --git a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Neighbor.cs b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Neighbor.cs index 9761a6cbdf..05aa46dfe5 100644 --- a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Neighbor.cs +++ b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Neighbor.cs @@ -72,8 +72,7 @@ private MemoryOwner SerializeNeighborRequest(bool highPriority) writer.WriteEndPoint(localNode); writer.Add(Unsafe.BitCast(highPriority)); - if (!writer.TryDetachBuffer(out result)) - result = writer.WrittenSpan.Copy(allocator); + result = writer.DetachOrCopyBuffer(); } finally { diff --git a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Shuffle.cs b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Shuffle.cs index f912524389..c1d8bfdce8 100644 --- a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Shuffle.cs +++ b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Discovery/HyParView/Http/HttpPeerController.Shuffle.cs @@ -77,8 +77,7 @@ private MemoryOwner SerializeShuffleReply(IReadOnlyCollection pe foreach (var peer in peers) writer.WriteEndPoint(peer); - if (!writer.TryDetachBuffer(out result)) - result = writer.WrittenSpan.Copy(allocator); + result = writer.DetachOrCopyBuffer(); } finally { @@ -169,8 +168,7 @@ private MemoryOwner SerializeShuffleRequest(EndPoint origin, IReadOnlyColl foreach (var peer in peers) writer.WriteEndPoint(peer); - if (!writer.TryDetachBuffer(out result)) - result = writer.WrittenSpan.Copy(allocator); + result = writer.DetachOrCopyBuffer(); } finally { diff --git a/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj b/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj index 79207c81e1..5e74eb11f5 100644 --- a/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj +++ b/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj @@ -8,7 +8,7 @@ enable true nullablePublicOnly - 5.14.0 + 5.16.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/Membership/InMemoryClusterConfigurationStorage.cs b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/Membership/InMemoryClusterConfigurationStorage.cs index d68a2d2eb9..3213a3171c 100644 --- a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/Membership/InMemoryClusterConfigurationStorage.cs +++ b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/Membership/InMemoryClusterConfigurationStorage.cs @@ -136,8 +136,7 @@ private MemoryOwner Encode(IReadOnlyCollection configuration) { Encode(configuration, ref writer); - if (!writer.TryDetachBuffer(out result)) - result = writer.WrittenSpan.Copy(allocator); + result = writer.DetachOrCopyBuffer(); } finally { diff --git a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/Membership/PersistentClusterConfigurationStorage.cs b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/Membership/PersistentClusterConfigurationStorage.cs index f44fc65c10..a2312b3cef 100644 --- a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/Membership/PersistentClusterConfigurationStorage.cs +++ b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/Membership/PersistentClusterConfigurationStorage.cs @@ -221,8 +221,7 @@ private MemoryOwner Encode(IReadOnlyCollection configuration, lo writer.WriteLittleEndian(fingerprint); Encode(configuration, ref writer); - if (!writer.TryDetachBuffer(out result)) - result = writer.WrittenSpan.Copy(allocator); + result = writer.DetachOrCopyBuffer(); } finally { diff --git a/src/cluster/DotNext.Net.Cluster/Net/EndPointFormatter.cs b/src/cluster/DotNext.Net.Cluster/Net/EndPointFormatter.cs index b3ab02c0e9..9fab84c5e5 100644 --- a/src/cluster/DotNext.Net.Cluster/Net/EndPointFormatter.cs +++ b/src/cluster/DotNext.Net.Cluster/Net/EndPointFormatter.cs @@ -52,8 +52,7 @@ public static MemoryOwner GetBytes(this EndPoint endPoint, MemoryAllocator { WriteEndPoint(ref writer, endPoint); - if (!writer.TryDetachBuffer(out result)) - result = writer.WrittenSpan.Copy(allocator); + result = writer.DetachOrCopyBuffer(); } finally {