diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc45d71c..0f715832e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,30 @@ Release Notes ==== +# 02-28-2024 +DotNext 5.1.0 +* Added `Span.Advance` extension method for spans +* `CollectionType.GetItemType` now correctly recognizes enumerable pattern even if target type doesn't implement `IEnumerable` + +DotNext.Metaprogramming 5.1.0 +* Updated dependencies + +DotNext.Unsafe 5.1.0 +* Added `UnmanagedMemory.AsMemory` static method that allows to wrap unmanaged pointer into [Memory<T>](https://learn.microsoft.com/en-us/dotnet/api/system.memory-1) + +DotNext.Threading 5.1.0 +* Updated dependencies + +DotNext.IO 5.1.0 +* Merged [225](https://github.com/dotnet/dotNext/pull/225) +* Added `AsUnbufferedStream` extension method for [SafeFileHandle](https://learn.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles.safefilehandle) class + +DotNext.Net.Cluster 5.1.0 +* Updated dependencies + +DotNext.AspNetCore.Cluster 5.1.0 +* Updated dependencies + # 02-25-2024 DotNext 5.0.3 * Fixed behavior to no-op when `GCLatencyModeScope` is initialized to default diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5d53a499e..d21fb135d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,8 @@ contact [conduct@dotnetfoundation.org](mailto:conduct@dotnetfoundation.org) with ## Branching Model This repository uses branching model known as [git flow](https://nvie.com/posts/a-successful-git-branching-model/). Use **develop** as the destination branch in your Pull Request. +Since 5.x release, squash commit is used to merge all commits related to the release when moving to `main` branch. + ## Backward Compatibility Contributions must not contain breaking changes such as backward incompatible modification of API signatures. The only exception is a new major version of the library. However, it should pass through code review and discussion. diff --git a/README.md b/README.md index 0a564aeee..e06c40b66 100644 --- a/README.md +++ b/README.md @@ -44,28 +44,30 @@ 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: 02-25-2024 +Release Date: 02-28-2024 -DotNext 5.0.3 -* Fixed behavior to no-op when `GCLatencyModeScope` is initialized to default +DotNext 5.1.0 +* Added `Span.Advance` extension method for spans +* `CollectionType.GetItemType` now correctly recognizes enumerable pattern even if target type doesn't implement `IEnumerable` -DotNext.Metaprogramming 5.0.3 +DotNext.Metaprogramming 5.1.0 * Updated dependencies -DotNext.Unsafe 5.0.3 -* Updated dependencies +DotNext.Unsafe 5.1.0 +* Added `UnmanagedMemory.AsMemory` static method that allows to wrap unmanaged pointer into [Memory<T>](https://learn.microsoft.com/en-us/dotnet/api/system.memory-1) -DotNext.Threading 5.0.3 +DotNext.Threading 5.1.0 * Updated dependencies -DotNext.IO 5.0.3 -* Updated dependencies +DotNext.IO 5.1.0 +* Merged [225](https://github.com/dotnet/dotNext/pull/225) +* Added `AsUnbufferedStream` extension method for [SafeFileHandle](https://learn.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles.safefilehandle) class -DotNext.Net.Cluster 5.0.3 -* Attempt to fix [221](https://github.com/dotnet/dotNext/issues/221) +DotNext.Net.Cluster 5.1.0 +* Updated dependencies -DotNext.AspNetCore.Cluster 5.0.3 -* Attempt to fix [221](https://github.com/dotnet/dotNext/issues/221) +DotNext.AspNetCore.Cluster 5.1.0 +* Updated dependencies Changelog for previous versions located [here](./CHANGELOG.md). diff --git a/src/DotNext.IO/DotNext.IO.csproj b/src/DotNext.IO/DotNext.IO.csproj index eb282ab74..3608bef3f 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.0.3 + 5.1.0 DotNext.IO MIT diff --git a/src/DotNext.IO/ExceptionMessages.cs b/src/DotNext.IO/ExceptionMessages.cs index 48197f7c4..6a0aebb7e 100644 --- a/src/DotNext.IO/ExceptionMessages.cs +++ b/src/DotNext.IO/ExceptionMessages.cs @@ -23,4 +23,6 @@ internal static string DirectoryNotFound(string path) internal static string WriterInReadMode => (string)Resources.Get(); internal static string NoConsumerProvided => (string)Resources.Get(); + + internal static string FileHandleClosed => (string)Resources.Get(); } \ No newline at end of file diff --git a/src/DotNext.IO/ExceptionMessages.restext b/src/DotNext.IO/ExceptionMessages.restext index 67779fc36..4dbd90f40 100644 --- a/src/DotNext.IO/ExceptionMessages.restext +++ b/src/DotNext.IO/ExceptionMessages.restext @@ -3,4 +3,5 @@ StreamNotWritable=Stream is not writable StreamNotReadable=Stream is not readable DirectoryNotFound=Directory {0} doesn't exist WriterInReadMode=The writer is in read-only mode. Dispose active memory manager obtained from writer -NoConsumerProvided=No actual consumer is provided \ No newline at end of file +NoConsumerProvided=No actual consumer is provided +FileHandleClosed=The file handle is closed \ No newline at end of file diff --git a/src/DotNext.IO/IO/StreamExtensions.cs b/src/DotNext.IO/IO/StreamExtensions.cs index 0a7fc1b20..edb7b6213 100644 --- a/src/DotNext.IO/IO/StreamExtensions.cs +++ b/src/DotNext.IO/IO/StreamExtensions.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using Microsoft.Win32.SafeHandles; namespace DotNext.IO; @@ -25,4 +26,24 @@ internal static void ThrowIfEmpty(in Memory buffer, [CallerArgumentExpress /// An object that represents multiple streams as one logical stream. public static Stream Combine(this Stream stream, ReadOnlySpan others) => others is { Length: > 0 } ? new SparseStream([stream, .. others]) : stream; + + /// + /// Creates a stream for the specified file handle. + /// + /// + /// The returned stream doesn't own the handle. + /// + /// The file handle. + /// Desired access to the file via stream. + /// The unbuffered file stream. + /// is . + /// is closed or invalid. + public static Stream AsUnbufferedStream(this SafeFileHandle handle, FileAccess access) + { + ArgumentNullException.ThrowIfNull(handle); + + return handle is { IsInvalid: false, IsClosed: false } + ? new UnbufferedFileStream(handle, access) + : throw new ArgumentException(ExceptionMessages.FileHandleClosed, nameof(handle)); + } } \ No newline at end of file diff --git a/src/DotNext.IO/IO/StreamSegment.cs b/src/DotNext.IO/IO/StreamSegment.cs index 58155d96d..150bedf5f 100644 --- a/src/DotNext.IO/IO/StreamSegment.cs +++ b/src/DotNext.IO/IO/StreamSegment.cs @@ -8,29 +8,16 @@ /// /// The segmentation is supported only for seekable streams. /// -public sealed class StreamSegment : Stream, IFlushable +/// The underlying stream represented by the segment. +/// to leave open after the object is disposed; otherwise, . +public sealed class StreamSegment(Stream stream, bool leaveOpen = true) : Stream, IFlushable { - private readonly bool leaveOpen; - private long length, offset; - - /// - /// Initializes a new segment of the specified stream. - /// - /// The underlying stream represented by the segment. - /// to leave open after the object is disposed; otherwise, . - /// is . - public StreamSegment(Stream stream, bool leaveOpen = true) - { - BaseStream = stream ?? throw new ArgumentNullException(nameof(stream)); - length = stream.Length; - offset = 0L; - this.leaveOpen = leaveOpen; - } + private long length = stream.Length, offset; /// /// Gets underlying stream. /// - public Stream BaseStream { get; } + public Stream BaseStream => stream; /// /// Establishes segment bounds. @@ -40,28 +27,28 @@ public StreamSegment(Stream stream, bool leaveOpen = true) /// /// The offset in the underlying stream. /// The length of the segment. - /// is larger than the reamining length of the underlying stream; or if greater than the length of the underlying stream. + /// is larger than the remaining length of the underlying stream; or if greater than the length of the underlying stream. public void Adjust(long offset, long length) { - ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)offset, (ulong)BaseStream.Length, nameof(offset)); - ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)offset, (ulong)(BaseStream.Length - offset), nameof(length)); + ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)offset, (ulong)stream.Length, nameof(offset)); + ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)length, (ulong)(stream.Length - offset), nameof(length)); this.length = length; this.offset = offset; - BaseStream.Position = offset; + stream.Position = offset; } /// /// Gets a value indicating whether the current stream supports reading. /// /// if the stream supports reading; otherwise, . - public override bool CanRead => BaseStream.CanRead; + public override bool CanRead => stream.CanRead; /// /// Gets a value indicating whether the current stream supports seeking. /// /// if the stream supports seeking; otherwise, . - public override bool CanSeek => BaseStream.CanSeek; + public override bool CanSeek => stream.CanSeek; /// /// Gets a value indicating whether the current stream supports writing. @@ -75,29 +62,29 @@ public void Adjust(long offset, long length) /// public override long Position { - get => BaseStream.Position - offset; + get => stream.Position - offset; set { ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)value, (ulong)length, nameof(value)); - BaseStream.Position = offset + value; + stream.Position = offset + value; } } private long RemainingBytes => length - Position; /// - public override void Flush() => BaseStream.Flush(); + public override void Flush() => stream.Flush(); /// - public override Task FlushAsync(CancellationToken token = default) => BaseStream.FlushAsync(token); + public override Task FlushAsync(CancellationToken token = default) => stream.FlushAsync(token); /// - public override bool CanTimeout => BaseStream.CanTimeout; + public override bool CanTimeout => stream.CanTimeout; /// public override int ReadByte() - => Position < length ? BaseStream.ReadByte() : -1; + => Position < length ? stream.ReadByte() : -1; /// public override void WriteByte(byte value) => throw new NotSupportedException(); @@ -107,30 +94,30 @@ public override int Read(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); - return BaseStream.Read(buffer, offset, (int)Math.Min(count, RemainingBytes)); + return stream.Read(buffer, offset, (int)Math.Min(count, RemainingBytes)); } /// public override int Read(Span buffer) - => BaseStream.Read(buffer.TrimLength(int.CreateSaturating(RemainingBytes))); + => stream.Read(buffer.TrimLength(int.CreateSaturating(RemainingBytes))); /// public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) { count = (int)Math.Min(count, RemainingBytes); - return BaseStream.BeginRead(buffer, offset, count, callback, state); + return stream.BeginRead(buffer, offset, count, callback, state); } /// - public override int EndRead(IAsyncResult asyncResult) => BaseStream.EndRead(asyncResult); + public override int EndRead(IAsyncResult asyncResult) => stream.EndRead(asyncResult); /// public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token = default) - => BaseStream.ReadAsync(buffer, offset, (int)Math.Min(count, RemainingBytes), token); + => stream.ReadAsync(buffer, offset, (int)Math.Min(count, RemainingBytes), token); /// public override ValueTask ReadAsync(Memory buffer, CancellationToken token = default) - => BaseStream.ReadAsync(buffer.TrimLength(int.CreateSaturating(RemainingBytes)), token); + => stream.ReadAsync(buffer.TrimLength(int.CreateSaturating(RemainingBytes)), token); /// public override long Seek(long offset, SeekOrigin origin) @@ -155,7 +142,7 @@ public override long Seek(long offset, SeekOrigin origin) /// public override void SetLength(long value) { - ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)value, (ulong)(BaseStream.Length - BaseStream.Position), nameof(value)); + ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)value, (ulong)(stream.Length - stream.Position), nameof(value)); length = value; } @@ -183,22 +170,22 @@ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, As /// public override int ReadTimeout { - get => BaseStream.ReadTimeout; - set => BaseStream.ReadTimeout = value; + get => stream.ReadTimeout; + set => stream.ReadTimeout = value; } /// public override int WriteTimeout { - get => BaseStream.WriteTimeout; - set => BaseStream.WriteTimeout = value; + get => stream.WriteTimeout; + set => stream.WriteTimeout = value; } /// protected override void Dispose(bool disposing) { if (disposing && !leaveOpen) - BaseStream.Dispose(); + stream.Dispose(); base.Dispose(disposing); } @@ -206,7 +193,7 @@ protected override void Dispose(bool disposing) public override async ValueTask DisposeAsync() { if (!leaveOpen) - await BaseStream.DisposeAsync().ConfigureAwait(false); + await stream.DisposeAsync().ConfigureAwait(false); await base.DisposeAsync().ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/DotNext.IO/IO/UnbufferedFileStream.Utils.cs b/src/DotNext.IO/IO/UnbufferedFileStream.Utils.cs new file mode 100644 index 000000000..9a2f24f08 --- /dev/null +++ b/src/DotNext.IO/IO/UnbufferedFileStream.Utils.cs @@ -0,0 +1,119 @@ +using System.Runtime.CompilerServices; +using System.Threading.Tasks.Sources; + +namespace DotNext.IO; + +internal partial class UnbufferedFileStream : IValueTaskSource, IValueTaskSource +{ + private ManualResetValueTaskSourceCore source; + private int bytesWritten; + private ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter writeTask; + private ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter readTask; + private Action? readCallback, writeCallback; + + internal ValueTask SubmitWrite(ValueTask writeTask, int bytesWritten) + { + this.bytesWritten = bytesWritten; + this.writeTask = writeTask.ConfigureAwait(false).GetAwaiter(); + if (this.writeTask.IsCompleted) + { + OnWriteCompleted(); + } + else + { + this.writeTask.UnsafeOnCompleted(writeCallback ??= OnWriteCompleted); + } + + return new(this, source.Version); + } + + internal ValueTask SubmitRead(ValueTask readTask) + { + this.readTask = readTask.ConfigureAwait(false).GetAwaiter(); + if (this.readTask.IsCompleted) + { + OnReadCompleted(); + } + else + { + this.readTask.UnsafeOnCompleted(readCallback ??= OnReadCompleted); + } + + return new(this, source.Version); + } + + private void OnWriteCompleted() + { + var awaiter = writeTask; + writeTask = default; + + try + { + awaiter.GetResult(); + } + catch (Exception e) + { + source.SetException(e); + return; + } + + source.SetResult(bytesWritten); + } + + private void OnReadCompleted() + { + var awaiter = readTask; + readTask = default; + + int bytesRead; + try + { + bytesRead = awaiter.GetResult(); + } + catch (Exception e) + { + source.SetException(e); + return; + } + + source.SetResult(bytesRead); + } + + public ValueTaskSourceStatus GetStatus(short token) => source.GetStatus(token); + + public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + => source.OnCompleted(continuation, state, token, flags); + + // write operation + void IValueTaskSource.GetResult(short token) + { + int bytesWritten; + try + { + bytesWritten = source.GetResult(token); + } + finally + { + source.Reset(); + } + + Advance(bytesWritten); + } + + // read operation + int IValueTaskSource.GetResult(short token) + { + int bytesRead; + try + { + bytesRead = source.GetResult(token); + } + finally + { + source.Reset(); + } + + Advance(bytesRead); + return bytesRead; + } +} \ No newline at end of file diff --git a/src/DotNext.IO/IO/UnbufferedFileStream.cs b/src/DotNext.IO/IO/UnbufferedFileStream.cs new file mode 100644 index 000000000..1b6abeed6 --- /dev/null +++ b/src/DotNext.IO/IO/UnbufferedFileStream.cs @@ -0,0 +1,152 @@ +using System.Runtime.CompilerServices; +using Microsoft.Win32.SafeHandles; + +namespace DotNext.IO; + +internal sealed partial class UnbufferedFileStream(SafeFileHandle handle, FileAccess access) : Stream, IFlushable +{ + private static readonly Action FlushToDiskAction = RandomAccess.FlushToDisk; + private long position; + + public override bool CanRead => access.HasFlag(FileAccess.Read); + + public override bool CanWrite => access.HasFlag(FileAccess.Write); + + public override bool CanSeek { get; } = CheckSeekable(handle); + + private static bool CheckSeekable(SafeFileHandle handle) + { + bool result; + try + { + result = CheckCanSeekImpl(handle); + } + catch (MissingMethodException) + { + result = false; + } + + return result; + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_CanSeek")] + static extern bool CheckCanSeekImpl(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); + } + + 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); + } + + public override void WriteByte(byte value) + => Write(new ReadOnlySpan(ref value)); + + private void Advance(int count) => position += count; + + 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.Metaprogramming/DotNext.Metaprogramming.csproj b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj index befc25c6f..9b8e2ea7e 100644 --- a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj +++ b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj @@ -8,7 +8,7 @@ true false nullablePublicOnly - 5.0.3 + 5.1.0 .NET Foundation .NEXT Family of Libraries diff --git a/src/DotNext.Tests/Buffers/UnmanagedMemoryPoolTests.cs b/src/DotNext.Tests/Buffers/UnmanagedMemoryPoolTests.cs index 337ec1a3a..6dff5a604 100644 --- a/src/DotNext.Tests/Buffers/UnmanagedMemoryPoolTests.cs +++ b/src/DotNext.Tests/Buffers/UnmanagedMemoryPoolTests.cs @@ -226,4 +226,13 @@ public static unsafe void Pinning() memory.Unpin(); handle.Dispose(); } + + [Fact] + public static unsafe void MarshalAsMemory() + { + int* ptr = stackalloc int[] { 10, 20, 30 }; + var memory = UnmanagedMemory.AsMemory(ptr, 3); + False(memory.IsEmpty); + Equal([10, 20, 30], memory.Span); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/IO/StreamSegmentTests.cs b/src/DotNext.Tests/IO/StreamSegmentTests.cs index a9b81b730..dfd908120 100644 --- a/src/DotNext.Tests/IO/StreamSegmentTests.cs +++ b/src/DotNext.Tests/IO/StreamSegmentTests.cs @@ -1,12 +1,28 @@ -namespace DotNext.IO; +using System.Text; + +namespace DotNext.IO; public sealed class StreamSegmentTests : Test { + [Theory] + [InlineData(0, 4, "This")] + [InlineData(5, 2, "is")] + [InlineData(10, 4, "test")] + public static void AdjustSetsSegmentOfStream(int offset, int length, string expected) + { + using var ms = new MemoryStream(Encoding.UTF8.GetBytes("This is a test")); + using var segment = new StreamSegment(ms); + segment.Adjust(offset, length); + using StreamReader reader = new(segment); + Equal(expected, reader.ReadToEnd()); + } + [Fact] public static void ReadByteSequentially() { using var ms = new MemoryStream([1, 3, 5, 8, 12]); using var segment = new StreamSegment(ms); + Same(ms, segment.BaseStream); Equal(0, segment.Position); segment.Adjust(0, 2); Equal(1, segment.ReadByte()); diff --git a/src/DotNext.Tests/IO/UnbufferedFileStreamTests.cs b/src/DotNext.Tests/IO/UnbufferedFileStreamTests.cs new file mode 100644 index 000000000..6b30901f1 --- /dev/null +++ b/src/DotNext.Tests/IO/UnbufferedFileStreamTests.cs @@ -0,0 +1,67 @@ +namespace DotNext.IO; + +public sealed class UnbufferedFileStreamTests : Test +{ + [Fact] + public static void ReadWriteSynchronously() + { + var fileName = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + using var handle = File.OpenHandle(fileName, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.DeleteOnClose); + using var stream = handle.AsUnbufferedStream(FileAccess.ReadWrite); + True(stream.CanRead); + True(stream.CanWrite); + True(stream.CanSeek); + Equal(0L, stream.Position); + Equal(0L, stream.Length); + + var expected = new byte[] { 10, 20, 30 }; + stream.SetLength(expected.Length); + Equal(3L, stream.Length); + Equal(0L, stream.Position); + + stream.Write(expected, 0, expected.Length); + Equal(3L, stream.Position); + stream.Flush(); + + stream.Position = 0L; + var actual = new byte[expected.Length]; + Equal(3, stream.Read(actual, 0, actual.Length)); + Equal(3L, stream.Position); + + Equal(expected, actual); + + stream.Position = 0L; + stream.WriteByte(42); + stream.Seek(-1L, SeekOrigin.Current); + Equal(42, stream.ReadByte()); + } + + [Fact] + public static async Task ReadWriteAsynchronously() + { + var fileName = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + using var handle = File.OpenHandle(fileName, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.DeleteOnClose); + await using var stream = handle.AsUnbufferedStream(FileAccess.ReadWrite); + True(stream.CanRead); + True(stream.CanWrite); + True(stream.CanSeek); + Equal(0L, stream.Position); + Equal(0L, stream.Length); + + var expected = new byte[] { 10, 20, 30 }; + stream.SetLength(expected.Length); + Equal(3L, stream.Length); + Equal(0L, stream.Position); + + await stream.WriteAsync(expected); + Equal(3L, stream.Position); + await stream.FlushAsync(CancellationToken.None); + + stream.Seek(0L, SeekOrigin.Begin); + var actual = new byte[expected.Length]; + Equal(3, await stream.ReadAsync(actual)); + Equal(3L, stream.Position); + + Equal(expected, actual); + } +} \ No newline at end of file diff --git a/src/DotNext.Tests/Reflection/TypeExtensionsTests.cs b/src/DotNext.Tests/Reflection/TypeExtensionsTests.cs index e0c0eb801..c960fcd56 100644 --- a/src/DotNext.Tests/Reflection/TypeExtensionsTests.cs +++ b/src/DotNext.Tests/Reflection/TypeExtensionsTests.cs @@ -32,8 +32,12 @@ public static void CollectionElement() { Equal(typeof(string), typeof(MyList).GetItemType(out var enumerable)); Equal(typeof(IEnumerable), enumerable); + Equal(typeof(int), typeof(int[]).GetItemType(out enumerable)); Equal(typeof(IEnumerable), enumerable); + + Equal(typeof(int).MakeByRefType(), typeof(Span).GetItemType(out enumerable)); + Null(enumerable); } private struct ManagedStruct diff --git a/src/DotNext.Tests/Runtime/InteropServices/PointerTests.cs b/src/DotNext.Tests/Runtime/InteropServices/PointerTests.cs index 1c7c7d386..aeb77269d 100644 --- a/src/DotNext.Tests/Runtime/InteropServices/PointerTests.cs +++ b/src/DotNext.Tests/Runtime/InteropServices/PointerTests.cs @@ -4,8 +4,6 @@ namespace DotNext.Runtime.InteropServices; -using Threading; - public sealed class PointerTests : Test { [Fact] diff --git a/src/DotNext.Tests/SpanTests.cs b/src/DotNext.Tests/SpanTests.cs index 8e9a79717..1617f23a4 100644 --- a/src/DotNext.Tests/SpanTests.cs +++ b/src/DotNext.Tests/SpanTests.cs @@ -534,4 +534,20 @@ public static void MoveRange() // out of range Throws(() => new int[] { 1, 2, 3, 4, 5, 6 }.AsSpan().Move(0..2, 1)); } + + [Fact] + public static void AdvanceReadOnlySpan() + { + ReadOnlySpan array = new int[] { 10, 20, 30 }; + Equal(10, array.Advance()); + Equal(new int[] { 20, 30 }, array.Advance(2)); + } + + [Fact] + public static void AdvanceSpan() + { + Span array = [10, 20, 30]; + Equal(10, array.Advance()); + Equal([20, 30], array.Advance(2)); + } } \ No newline at end of file diff --git a/src/DotNext.Threading/DotNext.Threading.csproj b/src/DotNext.Threading/DotNext.Threading.csproj index 6122db3c5..04e4f78ad 100644 --- a/src/DotNext.Threading/DotNext.Threading.csproj +++ b/src/DotNext.Threading/DotNext.Threading.csproj @@ -7,7 +7,7 @@ true true nullablePublicOnly - 5.0.3 + 5.1.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/DotNext.Unsafe/Buffers/UnmanagedMemory.cs b/src/DotNext.Unsafe/Buffers/UnmanagedMemory.cs index 6d5c5efc8..0eb708d29 100644 --- a/src/DotNext.Unsafe/Buffers/UnmanagedMemory.cs +++ b/src/DotNext.Unsafe/Buffers/UnmanagedMemory.cs @@ -62,6 +62,32 @@ static MemoryOwner Allocate(int length) static MemoryOwner AllocateZeroed(int length) => new(UnmanagedMemoryOwner.CreateZeroed, length); } + + /// + /// Wraps unmanaged pointer to . + /// + /// The type of elements in the memory. + /// The pointer to a sequence of elements. + /// The number of elements. + /// + [CLSCompliant(false)] + public static unsafe Memory AsMemory(T* pointer, int length) + where T : unmanaged + { + ArgumentNullException.ThrowIfNull(pointer); + ArgumentOutOfRangeException.ThrowIfNegative(length); + + if (length > 0) + { + MemoryManager manager = new UnmanagedMemory((nint)pointer, length); + + // GC perf: manager doesn't own the memory represented by the pointer, no need to call Dispose from finalizer + GC.SuppressFinalize(manager); + return manager.Memory; + } + + return Memory.Empty; + } } internal unsafe class UnmanagedMemory : MemoryManager diff --git a/src/DotNext.Unsafe/DotNext.Unsafe.csproj b/src/DotNext.Unsafe/DotNext.Unsafe.csproj index a185cb01b..af05c5a80 100644 --- a/src/DotNext.Unsafe/DotNext.Unsafe.csproj +++ b/src/DotNext.Unsafe/DotNext.Unsafe.csproj @@ -7,12 +7,12 @@ enable true true - 5.0.3 + 5.1.0 nullablePublicOnly .NET Foundation and Contributors .NEXT Family of Libraries - Rich data types to work with unmanaged memory in CLS-compliant way + Rich data types to work with unmanaged memory in a safe manner Copyright © .NET Foundation and Contributors MIT https://dotnet.github.io/dotNext/ diff --git a/src/DotNext/DotNext.csproj b/src/DotNext/DotNext.csproj index d20523fe3..2ae09a6a1 100644 --- a/src/DotNext/DotNext.csproj +++ b/src/DotNext/DotNext.csproj @@ -11,7 +11,7 @@ .NET Foundation and Contributors .NEXT Family of Libraries - 5.0.3 + 5.1.0 DotNext MIT diff --git a/src/DotNext/Reflection/CollectionType.cs b/src/DotNext/Reflection/CollectionType.cs index 24291c369..39a07f3d9 100644 --- a/src/DotNext/Reflection/CollectionType.cs +++ b/src/DotNext/Reflection/CollectionType.cs @@ -1,5 +1,6 @@ +using System.Collections; using System.Diagnostics.CodeAnalysis; -using IEnumerable = System.Collections.IEnumerable; +using System.Reflection; namespace DotNext.Reflection; @@ -17,7 +18,7 @@ public static class CollectionType /// Any collection type implementing . /// The type with actual generic argument. /// Type of items in the collection; or if is not a generic collection. - public static Type? GetItemType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type collectionType, out Type? enumerableInterface) + public static Type? GetItemType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods)] this Type collectionType, out Type? enumerableInterface) { enumerableInterface = collectionType.FindGenericInstance(typeof(IEnumerable<>)); if (enumerableInterface is not null) @@ -34,8 +35,11 @@ public static class CollectionType if (enumerableInterface is not null) return enumerableInterface.GetGenericArguments()[0]; - enumerableInterface = null; - return null; + // determine via GetEnumerator public method + return collectionType.GetMethod(nameof(IEnumerable.GetEnumerator), BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance, []) is { ReturnType: { } returnType } + && returnType.GetProperty(nameof(IEnumerator.Current), BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance) is { PropertyType: { } elementType } + ? elementType + : null; } /// @@ -43,7 +47,7 @@ public static class CollectionType /// /// Any collection type implementing . /// Type of items in the collection; or if is not a generic collection. - public static Type? GetItemType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type collectionType) + public static Type? GetItemType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods)] this Type collectionType) => collectionType.GetItemType(out _); /// @@ -58,8 +62,8 @@ public static class CollectionType /// public static Type? GetImplementedCollection([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type) { - var collectionTypes = (typeof(IReadOnlyCollection<>), typeof(ICollection<>)); - foreach (var collectionType in collectionTypes.AsReadOnlySpan()) + ReadOnlySpan collectionTypes = [typeof(IReadOnlyCollection<>), typeof(ICollection<>)]; + foreach (var collectionType in collectionTypes) { if (type.FindGenericInstance(collectionType) is { } result) return result; diff --git a/src/DotNext/Span.cs b/src/DotNext/Span.cs index e4c4346e3..5f3fe57c3 100644 --- a/src/DotNext/Span.cs +++ b/src/DotNext/Span.cs @@ -776,4 +776,70 @@ static void MoveCore(Span span, int sourceIndex, int destinationIndex, int le buffer.Dispose(); } } + + /// + /// Takes the specified number of elements and adjusts the span. + /// + /// The type of elements in the span. + /// The source span. + /// The number of elements to take. + /// The span containing elements. + /// is greater than the length of . + public static ReadOnlySpan Advance(this ref ReadOnlySpan source, int count) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)count, (uint)source.Length, nameof(count)); + + ref var ptr = ref MemoryMarshal.GetReference(source); + source = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref ptr, count), source.Length - count); + return MemoryMarshal.CreateReadOnlySpan(ref ptr, count); + } + + /// + /// Takes the first element and adjusts the span. + /// + /// The type of elements in the span. + /// The source span. + /// The reference to the first element in the span. + /// is empty. + public static ref readonly T Advance(this ref ReadOnlySpan source) + { + ArgumentOutOfRangeException.ThrowIfZero(source.Length, nameof(source)); + + ref T ptr = ref MemoryMarshal.GetReference(source); + source = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref ptr, 1), source.Length - 1); + return ref ptr; + } + + /// + /// Takes the specified number of elements and adjusts the span. + /// + /// The type of elements in the span. + /// The source span. + /// The number of elements to take. + /// The span containing elements. + /// is greater than the length of . + public static Span Advance(this ref Span source, int count) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)count, (uint)source.Length, nameof(count)); + + ref var ptr = ref MemoryMarshal.GetReference(source); + source = MemoryMarshal.CreateSpan(ref Unsafe.Add(ref ptr, count), source.Length - count); + return MemoryMarshal.CreateSpan(ref ptr, count); + } + + /// + /// Takes the first element and adjusts the span. + /// + /// The type of elements in the span. + /// The source span. + /// The reference to the first element in the span. + /// is empty. + public static ref T Advance(this ref Span source) + { + ArgumentOutOfRangeException.ThrowIfZero(source.Length, nameof(source)); + + ref T ptr = ref MemoryMarshal.GetReference(source); + source = MemoryMarshal.CreateSpan(ref Unsafe.Add(ref ptr, 1), source.Length - 1); + return ref ptr; + } } \ 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 ed24bdd05..d159df9bf 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.0.3 + 5.1.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj b/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj index c4d7eb6fe..e4ae7cfb7 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.0.3 + 5.1.0 .NET Foundation and Contributors .NEXT Family of Libraries