Skip to content

Commit

Permalink
Implement Span/Memory overloads for .NET 6 Streams, TextReaders, and …
Browse files Browse the repository at this point in the history
…TextWriters.

fix #97
  • Loading branch information
madelson committed Feb 28, 2023
1 parent 8a80bf4 commit 6f72d43
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 88 deletions.
16 changes: 16 additions & 0 deletions MedallionShell.Tests/Streams/PipeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,22 @@ public void TestPipeChainWithFixedLengthPipes()
asyncRead.Result.ShouldEqual(longText);
}

#if NETCOREAPP
[Test]
public async Task TestBasicReadAndWriteSpans()
{
Pipe pipe = new();
pipe.InputStream.Write(new byte[] { 1, 2, 3, 4 }.AsSpan());
await pipe.InputStream.WriteAsync(new byte[] { 5, 6, 7, 8, 9 }.AsMemory());

var buffer = new byte[5];
(await pipe.OutputStream.ReadAsync(buffer.AsMemory())).ShouldEqual(5);
CollectionAssert.AreEqual(new[] { 1, 2, 3, 4, 5 }, buffer);
pipe.OutputStream.Read(buffer.AsSpan()).ShouldEqual(4);
CollectionAssert.AreEqual(new[] { 6, 7, 8, 9, 5 }, buffer);
}
#endif

private static List<Pipe> CreatePipeChain(int length)
{
var pipes = Enumerable.Range(0, length).Select(_ => new Pipe())
Expand Down
55 changes: 55 additions & 0 deletions MedallionShell.Tests/Streams/StreamImplementationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Medallion.Shell;
using NUnit.Framework;

namespace MedallionShell.Tests.Streams;

internal class StreamImplementationTest
{
#if NETCOREAPP
[TestCase(typeof(Stream))]
[TestCase(typeof(TextWriter))]
[TestCase(typeof(TextReader))]
public void TestAllStreamImplementationsOverrideSpanMethods(Type baseType)
{
var requiredMethods = baseType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(
m => m.IsVirtual
&& (m.IsPublic || m.Attributes.HasFlag(MethodAttributes.FamORAssem) || m.Attributes.HasFlag(MethodAttributes.Family))
&& m.GetParameters().Select(p => p.ParameterType)
.Any(
t => t.IsConstructedGenericType
&& new[] { typeof(Span<>), typeof(ReadOnlySpan<>), typeof(Memory<>), typeof(ReadOnlyMemory<>) }.Contains(t.GetGenericTypeDefinition())
)
)
.ToArray();

var implementingTypes = typeof(Command).Assembly.GetTypes()
.Where(t => !t.IsAbstract && t.IsSubclassOf(baseType));

List<string> violations = new();
foreach (var implementation in implementingTypes)
{
var implementationMethods = implementation.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

foreach (var method in requiredMethods)
{
var implementingMethod = implementationMethods.Single(
m => m.Name == method.Name
&& m.GetParameters().Select(p => p.ParameterType).SequenceEqual(method.GetParameters().Select(p => p.ParameterType))
);
if (implementingMethod.DeclaringType!.Assembly != typeof(Command).Assembly)
{
violations.Add($"{implementation} must implement {method}");
}
}
}

Assert.IsEmpty(violations);
}
#endif
}
67 changes: 67 additions & 0 deletions MedallionShell/Shims.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Medallion.Shell;

internal static class Shims
{
public static T[] EmptyArray<T>() =>
#if !NET45
Array.Empty<T>();
#else
Empty<T>.Array;

private static class Empty<T>
{
public static readonly T[] Array = new T[0];
}
#endif

#if !NETCOREAPP2_1_OR_GREATER && !NETSTANDARD2_1
public static Span<T> AsSpan<T>(this T[] array, int start, int length) => new(new(array, start, length));
#endif
}

#pragma warning disable SA1306 // Field names should begin with lower-case letter

#if !NETCOREAPP2_1_OR_GREATER && !NETSTANDARD2_1
internal readonly struct Memory<T>
{
public readonly T[] Array;
public readonly int Offset;
public readonly int Length;

public Span<T> Span => new(this);

public Memory(T[] array, int offset, int length)
{
Debug.Assert(offset >= 0 && length >= 0 && offset + length <= array.Length, "buffer must be valid");

this.Array = array;
this.Offset = offset;
this.Length = length;
}

public Memory<T> Slice(int start) => this.Slice(start, this.Length - start);
public Memory<T> Slice(int start, int length) => new(this.Array, this.Offset + start, length);

public void CopyTo(Memory<T> destination) =>
System.Array.Copy(sourceArray: this.Array, sourceIndex: this.Offset, destinationArray: destination.Array, destinationIndex: destination.Offset, length: this.Length);
}

internal readonly ref struct Span<T>
{
public readonly Memory<T> Memory;

public int Length => this.Memory.Length;

public Span(Memory<T> memory) { this.Memory = memory; }

public static implicit operator Span<T>(T[] array) => new(new(array, 0, array.Length));

public void CopyTo(Span<T> destination) => this.Memory.CopyTo(destination.Memory);

public Span<T> Slice(int start) => new(this.Memory.Slice(start));
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ public async override Task WriteAsync(byte[] buffer, int offset, int count, Canc
}

#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
public override int Read(Span<byte> buffer) => this.stream.Read(buffer);

public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken) =>
this.stream.ReadAsync(buffer, cancellationToken);

public override void Write(ReadOnlySpan<byte> buffer)
{
// see comment in Write()
Expand Down
13 changes: 12 additions & 1 deletion MedallionShell/Streams/InternalProcessStreamReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Medallion.Shell.Streams
Expand All @@ -23,7 +24,7 @@ public InternalProcessStreamReader(StreamReader processStreamReader)
this.processStream = processStreamReader.BaseStream;
this.pipe = new Pipe();
this.reader = new StreamReader(this.pipe.OutputStream, processStreamReader.CurrentEncoding);
this.Task = Task.Run(() => this.BufferLoop());
this.Task = Task.Run(this.BufferLoop);
}

public Task Task { get; }
Expand Down Expand Up @@ -130,6 +131,16 @@ public override Task<string> ReadToEndAsync()
return this.reader.ReadToEndAsync();
}

#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
public override int Read(Span<char> buffer) => this.reader.Read(buffer);

public override int ReadBlock(Span<char> buffer) => this.reader.ReadBlock(buffer);

public override ValueTask<int> ReadAsync(Memory<char> buffer, CancellationToken cancellationToken = default) => this.reader.ReadAsync(buffer);

public override ValueTask<int> ReadBlockAsync(Memory<char> buffer, CancellationToken cancellationToken = default) => this.reader.ReadBlockAsync(buffer, cancellationToken);
#endif

protected override void Dispose(bool disposing)
{
if (disposing)
Expand Down
Loading

0 comments on commit 6f72d43

Please sign in to comment.