From df543b2c8ef193a9e04f93cd424b91c821aecb99 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Tue, 15 Nov 2016 09:29:21 -0800 Subject: [PATCH] Introducing System.IO.Pipelines (#980) * Introducing System.IO.Pipelines - Golang took the name channels so now we have pipelines --- corefxlab.sln | 198 ++++-- .../AspNetHttpServerSample.cs | 38 ++ .../CompressionSample.cs | 41 ++ .../Framing/Codec.cs | 142 ++++ .../HttpClient/LibuvHttpClientHandler.cs | 300 ++++++++ .../HttpClient/PipelineHttpContent.cs | 77 +++ .../HttpServer/FormReader.cs | 69 ++ .../HttpServer/HttpConnection.Features.cs | 184 +++++ .../HttpServer/HttpConnection.cs | 214 ++++++ .../HttpServer/HttpRequestParser.cs | 133 ++++ .../HttpServer/HttpRequestStream.cs | 148 ++++ .../HttpServer/HttpResponseStream.cs | 121 ++++ .../HttpServer/HttpServer.cs | 175 +++++ .../HttpServer/ReasonPhrases.cs | 184 +++++ .../HttpServer/RequestHeaderDictionary.cs | 243 +++++++ .../HttpServer/ResponseHeaderDictionary.cs | 120 ++++ .../HttpServer/ServerAddress.cs | 125 ++++ .../LibuvHttpClientSample.cs | 27 + .../Models/Models.cs | 247 +++++++ .../System.IO.Pipelines.Samples/Models/Pet.cs | 47 ++ .../System.IO.Pipelines.Samples/Program.cs | 20 + .../Properties/AssemblyInfo.cs | 19 + .../RawLibuvHttpClientSample.cs | 76 +++ .../RawLibuvHttpServerSample.cs | 91 +++ .../System.IO.Pipelines.Samples.xproj | 19 + .../System.IO.Pipelines.Samples/project.json | 44 ++ .../CompressionPipelineExtensions.cs | 272 ++++++++ .../Deflater.cs | 253 +++++++ .../Inflater.cs | 225 ++++++ .../Interop/Interop.zlib.Unix.cs | 47 ++ .../Interop/Interop.zlib.Windows.cs | 127 ++++ .../System.IO.Pipelines.Compression.xproj | 19 + .../ZLibException.cs | 109 +++ .../ZLibNative.Windows.cs | 49 ++ .../ZLibNative.cs | 410 +++++++++++ .../project.json | 29 + src/System.IO.Pipelines.File/FileReader.cs | 186 +++++ .../ReadableFilePipelineFactoryExtensions.cs | 19 + .../System.IO.Pipelines.File.xproj | 19 + src/System.IO.Pipelines.File/project.json | 29 + .../Internal/WorkQueue.cs | 103 +++ .../Internal/WriteReqPool.cs | 71 ++ .../Interop/PlatformApis.cs | 20 + .../Interop/SockAddr.cs | 110 +++ .../Interop/Uv.cs | 638 ++++++++++++++++++ .../Interop/UvAsyncHandle.cs | 72 ++ .../Interop/UvConnectRequest.cs | 94 +++ .../Interop/UvException.cs | 17 + .../Interop/UvHandle.cs | 66 ++ .../Interop/UvLoopHandle.cs | 52 ++ .../Interop/UvMemory.cs | 87 +++ .../Interop/UvPipeHandle.cs | 34 + .../Interop/UvRequest.cs | 32 + .../Interop/UvShutdownReq.cs | 47 ++ .../Interop/UvStreamHandle.cs | 151 +++++ .../Interop/UvTcpHandle.cs | 75 ++ .../Interop/UvWriteReq.cs | 148 ++++ .../Properties/AssemblyInfo.cs | 19 + ...System.IO.Pipelines.Networking.Libuv.xproj | 19 + .../UvTcpClient.cs | 56 ++ .../UvTcpConnection.cs | 221 ++++++ .../UvTcpListener.cs | 102 +++ .../UvThread.cs | 155 +++++ .../project.json | 33 + .../Internal/ContinuationMode.cs | 12 + .../Internal/MicroBufferPool.cs | 100 +++ .../Internal/Signal.cs | 87 +++ .../Internal/SocketExtensions.cs | 37 + .../Properties/AssemblyInfo.cs | 21 + .../SocketConnection.cs | 615 +++++++++++++++++ .../SocketListener.cs | 151 +++++ ...stem.IO.Pipelines.Networking.Sockets.xproj | 19 + .../project.json | 32 + .../Internal/CpuInfo.cs | 272 ++++++++ .../Internal/RioBufferSegment.cs | 23 + .../Internal/RioRequestResult.cs | 16 + .../Internal/RioThread.cs | 538 +++++++++++++++ .../Internal/RioThreadPool.cs | 90 +++ .../Internal/Winsock/AddressFamilies.cs | 10 + .../Internal/Winsock/Ipv4InternetAddress.cs | 20 + .../Winsock/NotificationCompletion.cs | 14 + .../Winsock/NotificationCompletionEvent.cs | 15 + .../Winsock/NotificationCompletionIocp.cs | 17 + .../Winsock/NotificationCompletionType.cs | 12 + .../Internal/Winsock/Protocol.cs | 10 + .../Internal/Winsock/RIOImports.cs | 151 +++++ .../Internal/Winsock/RegisteredIO.cs | 32 + .../Internal/Winsock/RioDelegates.cs | 63 ++ .../Winsock/RioExtensionFunctionTable.cs | 28 + .../Internal/Winsock/RioReceiveFlags.cs | 14 + .../Internal/Winsock/RioSendFlags.cs | 16 + .../Internal/Winsock/SocketAddress.cs | 16 + .../Internal/Winsock/SocketFlags.cs | 17 + .../Internal/Winsock/SocketType.cs | 10 + .../SuppressUnmanagedCodeSecurityAttribute.cs | 11 + .../Internal/Winsock/Version.cs | 42 ++ .../Internal/Winsock/WindowsSocketsData.cs | 22 + .../Properties/AssemblyInfo.cs | 19 + .../RioTcpConnection.cs | 301 +++++++++ .../RioTcpServer.cs | 124 ++++ ....IO.Pipelines.Networking.Windows.RIO.xproj | 19 + .../project.json | 36 + .../AsciiUtilities.cs | 81 +++ .../PipelineTextOutput.cs | 65 ++ .../PipelineWriterExtensions.cs | 13 + .../Properties/AssemblyInfo.cs | 19 + .../ReadableBufferExtensions.cs | 243 +++++++ .../SplitEnumerable.cs | 57 ++ .../SplitEnumerator.cs | 59 ++ .../System.IO.Pipelines.Text.Primitives.xproj | 19 + .../WritableBufferExtensions.cs | 148 ++++ .../project.json | 34 + src/System.IO.Pipelines/ArrayBufferPool.cs | 27 + src/System.IO.Pipelines/BufferSegment.cs | 145 ++++ src/System.IO.Pipelines/CommonVectors.cs | 17 + .../DefaultReadableBufferExtensions.cs | 111 +++ .../DefaultWritableBufferExtensions.cs | 79 +++ src/System.IO.Pipelines/Gate.cs | 77 +++ src/System.IO.Pipelines/IBufferPool.cs | 18 + .../IPipelineConnection.cs | 20 + src/System.IO.Pipelines/IPipelineReader.cs | 33 + src/System.IO.Pipelines/IPipelineWriter.cs | 33 + .../IReadableBufferAwaiter.cs | 13 + src/System.IO.Pipelines/MemoryEnumerator.cs | 88 +++ src/System.IO.Pipelines/MemoryPool.cs | 240 +++++++ src/System.IO.Pipelines/MemoryPoolBlock.cs | 102 +++ src/System.IO.Pipelines/MemoryPoolSlab.cs | 98 +++ .../PipelineConnectionExtensions.cs | 152 +++++ .../PipelineConnectionStream.cs | 191 ++++++ src/System.IO.Pipelines/PipelineFactory.cs | 106 +++ src/System.IO.Pipelines/PipelineReader.cs | 57 ++ .../PipelineReaderWriter.cs | 556 +++++++++++++++ src/System.IO.Pipelines/PipelineWriter.cs | 50 ++ src/System.IO.Pipelines/PreservedBuffer.cs | 49 ++ .../Properties/AssemblyInfo.cs | 21 + src/System.IO.Pipelines/ReadCursor.cs | 275 ++++++++ src/System.IO.Pipelines/ReadResult.cs | 15 + src/System.IO.Pipelines/ReadableBuffer.cs | 569 ++++++++++++++++ .../ReadableBufferAwaitable.cs | 28 + .../ReadableBufferReader.cs | 78 +++ src/System.IO.Pipelines/StreamExtensions.cs | 247 +++++++ .../StreamPipelineConnection.cs | 24 + .../System.IO.Pipelines.xproj | 19 + src/System.IO.Pipelines/TaskToApm.cs | 190 ++++++ src/System.IO.Pipelines/ThrowHelper.cs | 131 ++++ src/System.IO.Pipelines/UnownedBuffer.cs | 28 + .../UnownedBufferReader.cs | 366 ++++++++++ src/System.IO.Pipelines/WritableBuffer.cs | 103 +++ .../WriteableBufferStream.cs | 167 +++++ src/System.IO.Pipelines/project.json | 39 ++ .../BufferPoolFacts.cs | 53 ++ .../PipelineReaderWriterFacts.cs | 354 ++++++++++ .../PipelineWriterFacts.cs | 184 +++++ .../Properties/AssemblyInfo.cs | 19 + .../ReadableBufferFacts.cs | 538 +++++++++++++++ .../ReadableBufferReaderFacts.cs | 114 ++++ .../System.IO.Pipelines.Tests/SignalFacts.cs | 129 ++++ .../System.IO.Pipelines.Tests/SocketsFacts.cs | 281 ++++++++ .../System.IO.Pipelines.Tests.xproj | 19 + .../UnownedBufferReaderFacts.cs | 617 +++++++++++++++++ .../WritableBufferFacts.cs | 327 +++++++++ tests/System.IO.Pipelines.Tests/project.json | 43 ++ 162 files changed, 18258 insertions(+), 69 deletions(-) create mode 100644 samples/System.IO.Pipelines.Samples/AspNetHttpServerSample.cs create mode 100644 samples/System.IO.Pipelines.Samples/CompressionSample.cs create mode 100644 samples/System.IO.Pipelines.Samples/Framing/Codec.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpClient/LibuvHttpClientHandler.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpClient/PipelineHttpContent.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpServer/FormReader.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpServer/HttpConnection.Features.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpServer/HttpConnection.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpServer/HttpRequestParser.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpServer/HttpRequestStream.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpServer/HttpResponseStream.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpServer/HttpServer.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpServer/ReasonPhrases.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpServer/RequestHeaderDictionary.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpServer/ResponseHeaderDictionary.cs create mode 100644 samples/System.IO.Pipelines.Samples/HttpServer/ServerAddress.cs create mode 100644 samples/System.IO.Pipelines.Samples/LibuvHttpClientSample.cs create mode 100644 samples/System.IO.Pipelines.Samples/Models/Models.cs create mode 100644 samples/System.IO.Pipelines.Samples/Models/Pet.cs create mode 100644 samples/System.IO.Pipelines.Samples/Program.cs create mode 100644 samples/System.IO.Pipelines.Samples/Properties/AssemblyInfo.cs create mode 100644 samples/System.IO.Pipelines.Samples/RawLibuvHttpClientSample.cs create mode 100644 samples/System.IO.Pipelines.Samples/RawLibuvHttpServerSample.cs create mode 100644 samples/System.IO.Pipelines.Samples/System.IO.Pipelines.Samples.xproj create mode 100644 samples/System.IO.Pipelines.Samples/project.json create mode 100644 src/System.IO.Pipelines.Compression/CompressionPipelineExtensions.cs create mode 100644 src/System.IO.Pipelines.Compression/Deflater.cs create mode 100644 src/System.IO.Pipelines.Compression/Inflater.cs create mode 100644 src/System.IO.Pipelines.Compression/Interop/Interop.zlib.Unix.cs create mode 100644 src/System.IO.Pipelines.Compression/Interop/Interop.zlib.Windows.cs create mode 100644 src/System.IO.Pipelines.Compression/System.IO.Pipelines.Compression.xproj create mode 100644 src/System.IO.Pipelines.Compression/ZLibException.cs create mode 100644 src/System.IO.Pipelines.Compression/ZLibNative.Windows.cs create mode 100644 src/System.IO.Pipelines.Compression/ZLibNative.cs create mode 100644 src/System.IO.Pipelines.Compression/project.json create mode 100644 src/System.IO.Pipelines.File/FileReader.cs create mode 100644 src/System.IO.Pipelines.File/ReadableFilePipelineFactoryExtensions.cs create mode 100644 src/System.IO.Pipelines.File/System.IO.Pipelines.File.xproj create mode 100644 src/System.IO.Pipelines.File/project.json create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Internal/WorkQueue.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Internal/WriteReqPool.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/PlatformApis.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/SockAddr.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/Uv.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/UvAsyncHandle.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/UvConnectRequest.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/UvException.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/UvHandle.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/UvLoopHandle.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/UvMemory.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/UvPipeHandle.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/UvRequest.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/UvShutdownReq.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/UvStreamHandle.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/UvTcpHandle.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Interop/UvWriteReq.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/Properties/AssemblyInfo.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/System.IO.Pipelines.Networking.Libuv.xproj create mode 100644 src/System.IO.Pipelines.Networking.Libuv/UvTcpClient.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/UvTcpConnection.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/UvTcpListener.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/UvThread.cs create mode 100644 src/System.IO.Pipelines.Networking.Libuv/project.json create mode 100644 src/System.IO.Pipelines.Networking.Sockets/Internal/ContinuationMode.cs create mode 100644 src/System.IO.Pipelines.Networking.Sockets/Internal/MicroBufferPool.cs create mode 100644 src/System.IO.Pipelines.Networking.Sockets/Internal/Signal.cs create mode 100644 src/System.IO.Pipelines.Networking.Sockets/Internal/SocketExtensions.cs create mode 100644 src/System.IO.Pipelines.Networking.Sockets/Properties/AssemblyInfo.cs create mode 100644 src/System.IO.Pipelines.Networking.Sockets/SocketConnection.cs create mode 100644 src/System.IO.Pipelines.Networking.Sockets/SocketListener.cs create mode 100644 src/System.IO.Pipelines.Networking.Sockets/System.IO.Pipelines.Networking.Sockets.xproj create mode 100644 src/System.IO.Pipelines.Networking.Sockets/project.json create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/CpuInfo.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioBufferSegment.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioRequestResult.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioThread.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioThreadPool.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/AddressFamilies.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/Ipv4InternetAddress.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletion.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletionEvent.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletionIocp.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletionType.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/Protocol.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RIOImports.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RegisteredIO.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioDelegates.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioExtensionFunctionTable.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioReceiveFlags.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioSendFlags.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SocketAddress.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SocketFlags.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SocketType.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SuppressUnmanagedCodeSecurityAttribute.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/Version.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/WindowsSocketsData.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/Properties/AssemblyInfo.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/RioTcpConnection.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/RioTcpServer.cs create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/System.IO.Pipelines.Networking.Windows.RIO.xproj create mode 100644 src/System.IO.Pipelines.Networking.Windows.RIO/project.json create mode 100644 src/System.IO.Pipelines.Text.Primitives/AsciiUtilities.cs create mode 100644 src/System.IO.Pipelines.Text.Primitives/PipelineTextOutput.cs create mode 100644 src/System.IO.Pipelines.Text.Primitives/PipelineWriterExtensions.cs create mode 100644 src/System.IO.Pipelines.Text.Primitives/Properties/AssemblyInfo.cs create mode 100644 src/System.IO.Pipelines.Text.Primitives/ReadableBufferExtensions.cs create mode 100644 src/System.IO.Pipelines.Text.Primitives/SplitEnumerable.cs create mode 100644 src/System.IO.Pipelines.Text.Primitives/SplitEnumerator.cs create mode 100644 src/System.IO.Pipelines.Text.Primitives/System.IO.Pipelines.Text.Primitives.xproj create mode 100644 src/System.IO.Pipelines.Text.Primitives/WritableBufferExtensions.cs create mode 100644 src/System.IO.Pipelines.Text.Primitives/project.json create mode 100644 src/System.IO.Pipelines/ArrayBufferPool.cs create mode 100644 src/System.IO.Pipelines/BufferSegment.cs create mode 100644 src/System.IO.Pipelines/CommonVectors.cs create mode 100644 src/System.IO.Pipelines/DefaultReadableBufferExtensions.cs create mode 100644 src/System.IO.Pipelines/DefaultWritableBufferExtensions.cs create mode 100644 src/System.IO.Pipelines/Gate.cs create mode 100644 src/System.IO.Pipelines/IBufferPool.cs create mode 100644 src/System.IO.Pipelines/IPipelineConnection.cs create mode 100644 src/System.IO.Pipelines/IPipelineReader.cs create mode 100644 src/System.IO.Pipelines/IPipelineWriter.cs create mode 100644 src/System.IO.Pipelines/IReadableBufferAwaiter.cs create mode 100644 src/System.IO.Pipelines/MemoryEnumerator.cs create mode 100644 src/System.IO.Pipelines/MemoryPool.cs create mode 100644 src/System.IO.Pipelines/MemoryPoolBlock.cs create mode 100644 src/System.IO.Pipelines/MemoryPoolSlab.cs create mode 100644 src/System.IO.Pipelines/PipelineConnectionExtensions.cs create mode 100644 src/System.IO.Pipelines/PipelineConnectionStream.cs create mode 100644 src/System.IO.Pipelines/PipelineFactory.cs create mode 100644 src/System.IO.Pipelines/PipelineReader.cs create mode 100644 src/System.IO.Pipelines/PipelineReaderWriter.cs create mode 100644 src/System.IO.Pipelines/PipelineWriter.cs create mode 100644 src/System.IO.Pipelines/PreservedBuffer.cs create mode 100644 src/System.IO.Pipelines/Properties/AssemblyInfo.cs create mode 100644 src/System.IO.Pipelines/ReadCursor.cs create mode 100644 src/System.IO.Pipelines/ReadResult.cs create mode 100644 src/System.IO.Pipelines/ReadableBuffer.cs create mode 100644 src/System.IO.Pipelines/ReadableBufferAwaitable.cs create mode 100644 src/System.IO.Pipelines/ReadableBufferReader.cs create mode 100644 src/System.IO.Pipelines/StreamExtensions.cs create mode 100644 src/System.IO.Pipelines/StreamPipelineConnection.cs create mode 100644 src/System.IO.Pipelines/System.IO.Pipelines.xproj create mode 100644 src/System.IO.Pipelines/TaskToApm.cs create mode 100644 src/System.IO.Pipelines/ThrowHelper.cs create mode 100644 src/System.IO.Pipelines/UnownedBuffer.cs create mode 100644 src/System.IO.Pipelines/UnownedBufferReader.cs create mode 100644 src/System.IO.Pipelines/WritableBuffer.cs create mode 100644 src/System.IO.Pipelines/WriteableBufferStream.cs create mode 100644 src/System.IO.Pipelines/project.json create mode 100644 tests/System.IO.Pipelines.Tests/BufferPoolFacts.cs create mode 100644 tests/System.IO.Pipelines.Tests/PipelineReaderWriterFacts.cs create mode 100644 tests/System.IO.Pipelines.Tests/PipelineWriterFacts.cs create mode 100644 tests/System.IO.Pipelines.Tests/Properties/AssemblyInfo.cs create mode 100644 tests/System.IO.Pipelines.Tests/ReadableBufferFacts.cs create mode 100644 tests/System.IO.Pipelines.Tests/ReadableBufferReaderFacts.cs create mode 100644 tests/System.IO.Pipelines.Tests/SignalFacts.cs create mode 100644 tests/System.IO.Pipelines.Tests/SocketsFacts.cs create mode 100644 tests/System.IO.Pipelines.Tests/System.IO.Pipelines.Tests.xproj create mode 100644 tests/System.IO.Pipelines.Tests/UnownedBufferReaderFacts.cs create mode 100644 tests/System.IO.Pipelines.Tests/WritableBufferFacts.cs create mode 100644 tests/System.IO.Pipelines.Tests/project.json diff --git a/corefxlab.sln b/corefxlab.sln index 6a1bb1471f8..210e60b4a47 100644 --- a/corefxlab.sln +++ b/corefxlab.sln @@ -15,7 +15,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3079E458 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{E1DE693E-C1FB-4A1F-B6D8-A071D86D7354}" EndProject - Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "QotdService", "samples\QotdService\QotdService.xproj", "{FE69CE5F-2B54-417B-AFE1-88F190699901}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LowAllocationWebServer", "samples\LowAllocationWebServer\LowAllocationWebServer.xproj", "{1979687D-C3D1-4D40-9017-269D7C092283}" @@ -24,7 +23,6 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LibuvWithNonAllocatingForma EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "EchoService", "samples\EchoService\EchoService.xproj", "{D29DC7F0-0199-4759-B445-3E19ACCBB030}" EndProject - Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Binary", "src\System.Binary\System.Binary.xproj", "{9E0BF83B-CC6B-472A-AA6E-FFD507AE9D9F}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Slices", "src\System.Slices\System.Slices.xproj", "{94177D1E-1CD3-4878-9FB2-3EB8DFD5CC2C}" @@ -71,7 +69,6 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Collections.Sequence EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Text.Json.Dynamic", "src\System.Text.Json.Dynamic\System.Text.Json.Dynamic.xproj", "{BB6D79C1-783F-4B87-A281-5EAB22CA7BF0}" EndProject - Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Text.Json.Dynamic.Tests", "tests\System.Text.Json.Tests.Dynamic\System.Text.Json.Dynamic.Tests.xproj", "{B6FBB81F-241F-4603-9510-E269F843F6CB}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Binary.Tests", "tests\System.Binary.Tests\System.Binary.Tests.xproj", "{D37C62B5-C15C-4F2E-A67D-E8D6324FBCBB}" @@ -104,7 +101,7 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Threading.Tasks.Chan EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Text.Primitives.Tests", "tests\System.Text.Primitives.Tests\System.Text.Primitives.Tests.xproj", "{67B42C20-D98A-420B-82C0-04AFD963E650}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Binary.Base64.Tests", "tests\System.Binary.Base64.Tests\System.Binary.Base64.tests.xproj", "{481F242E-57E0-4904-89E2-345677D98C55}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Binary.Base64.tests", "tests\System.Binary.Base64.Tests\System.Binary.Base64.tests.xproj", "{481F242E-57E0-4904-89E2-345677D98C55}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Time.Tests", "tests\System.Time.Tests\System.Time.Tests.xproj", "{872C74A2-97BD-4672-9674-BC361419AF32}" EndProject @@ -116,16 +113,46 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Text.Encodings.Web.U EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.Slices.Tests", "tests\System.Slices.Tests\System.Slices.Tests.xproj", "{32B36B8F-C5F1-4426-8E38-528B22839F3E}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.IO.Pipelines.Samples", "samples\System.IO.Pipelines.Samples\System.IO.Pipelines.Samples.xproj", "{A53A869F-D581-4499-AC49-13C2B75C2380}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.IO.Pipelines", "src\System.IO.Pipelines\System.IO.Pipelines.xproj", "{A712AE2E-C98E-4E83-9B33-789141763541}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.IO.Pipelines.Compression", "src\System.IO.Pipelines.Compression\System.IO.Pipelines.Compression.xproj", "{2F02C533-538E-4F0D-A5DE-D029D99A49BD}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.IO.Pipelines.File", "src\System.IO.Pipelines.File\System.IO.Pipelines.File.xproj", "{7E8AC800-7D57-4645-83F5-FA101090D648}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.IO.Pipelines.Networking.Libuv", "src\System.IO.Pipelines.Networking.Libuv\System.IO.Pipelines.Networking.Libuv.xproj", "{5A7899EB-9366-477C-A0BC-A813D4211E78}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.IO.Pipelines.Networking.Windows.RIO", "src\System.IO.Pipelines.Networking.Windows.RIO\System.IO.Pipelines.Networking.Windows.RIO.xproj", "{2D38272E-5E48-4ADE-9321-C73952613E70}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.IO.Pipelines.Text.Primitives", "src\System.IO.Pipelines.Text.Primitives\System.IO.Pipelines.Text.Primitives.xproj", "{B9FA069D-D447-4303-9426-8BD4158E537C}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.IO.Pipelines.Tests", "tests\System.IO.Pipelines.Tests\System.IO.Pipelines.Tests.xproj", "{3A0C9831-F43A-420D-83B7-9ECC4EAEA39D}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "System.IO.Pipelines.Networking.Sockets", "src\System.IO.Pipelines.Networking.Sockets\System.IO.Pipelines.Networking.Sockets.xproj", "{A5BDAB00-68C2-4661-AD49-735D94ECED06}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B6FBB81F-241F-4603-9510-E269F843F6CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B6FBB81F-241F-4603-9510-E269F843F6CB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B6FBB81F-241F-4603-9510-E269F843F6CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B6FBB81F-241F-4603-9510-E269F843F6CB}.Release|Any CPU.Build.0 = Release|Any CPU + {FE69CE5F-2B54-417B-AFE1-88F190699901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE69CE5F-2B54-417B-AFE1-88F190699901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE69CE5F-2B54-417B-AFE1-88F190699901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE69CE5F-2B54-417B-AFE1-88F190699901}.Release|Any CPU.Build.0 = Release|Any CPU + {1979687D-C3D1-4D40-9017-269D7C092283}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1979687D-C3D1-4D40-9017-269D7C092283}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1979687D-C3D1-4D40-9017-269D7C092283}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1979687D-C3D1-4D40-9017-269D7C092283}.Release|Any CPU.Build.0 = Release|Any CPU + {8A4C8106-67EC-4E5D-BC81-1DB617413EEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A4C8106-67EC-4E5D-BC81-1DB617413EEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A4C8106-67EC-4E5D-BC81-1DB617413EEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A4C8106-67EC-4E5D-BC81-1DB617413EEA}.Release|Any CPU.Build.0 = Release|Any CPU + {D29DC7F0-0199-4759-B445-3E19ACCBB030}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D29DC7F0-0199-4759-B445-3E19ACCBB030}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D29DC7F0-0199-4759-B445-3E19ACCBB030}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D29DC7F0-0199-4759-B445-3E19ACCBB030}.Release|Any CPU.Build.0 = Release|Any CPU {9E0BF83B-CC6B-472A-AA6E-FFD507AE9D9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9E0BF83B-CC6B-472A-AA6E-FFD507AE9D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU {9E0BF83B-CC6B-472A-AA6E-FFD507AE9D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -190,10 +217,38 @@ Global {66579C6A-8316-4888-8D96-36D4B49D7D51}.Debug|Any CPU.Build.0 = Debug|Any CPU {66579C6A-8316-4888-8D96-36D4B49D7D51}.Release|Any CPU.ActiveCfg = Release|Any CPU {66579C6A-8316-4888-8D96-36D4B49D7D51}.Release|Any CPU.Build.0 = Release|Any CPU - {32B36B8F-C5F1-4426-8E38-528B22839F3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {32B36B8F-C5F1-4426-8E38-528B22839F3E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {32B36B8F-C5F1-4426-8E38-528B22839F3E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {32B36B8F-C5F1-4426-8E38-528B22839F3E}.Release|Any CPU.Build.0 = Release|Any CPU + {80E6332B-DC83-4DFC-9C19-8D89E988BA4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80E6332B-DC83-4DFC-9C19-8D89E988BA4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80E6332B-DC83-4DFC-9C19-8D89E988BA4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80E6332B-DC83-4DFC-9C19-8D89E988BA4E}.Release|Any CPU.Build.0 = Release|Any CPU + {329B9BE5-3F34-42A2-9E8C-D7BDD6B1EE55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {329B9BE5-3F34-42A2-9E8C-D7BDD6B1EE55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {329B9BE5-3F34-42A2-9E8C-D7BDD6B1EE55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {329B9BE5-3F34-42A2-9E8C-D7BDD6B1EE55}.Release|Any CPU.Build.0 = Release|Any CPU + {B9ECBAF7-524D-4134-AE37-EFF7802B221F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9ECBAF7-524D-4134-AE37-EFF7802B221F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9ECBAF7-524D-4134-AE37-EFF7802B221F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9ECBAF7-524D-4134-AE37-EFF7802B221F}.Release|Any CPU.Build.0 = Release|Any CPU + {12D7B81D-26FD-4BCA-9D72-AFDEA900BAAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12D7B81D-26FD-4BCA-9D72-AFDEA900BAAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12D7B81D-26FD-4BCA-9D72-AFDEA900BAAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12D7B81D-26FD-4BCA-9D72-AFDEA900BAAC}.Release|Any CPU.Build.0 = Release|Any CPU + {20974D06-EBDC-44B8-A034-DE1440CF7A45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20974D06-EBDC-44B8-A034-DE1440CF7A45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20974D06-EBDC-44B8-A034-DE1440CF7A45}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20974D06-EBDC-44B8-A034-DE1440CF7A45}.Release|Any CPU.Build.0 = Release|Any CPU + {2D178A3D-247D-46C4-BAC3-DAB5EFC064A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D178A3D-247D-46C4-BAC3-DAB5EFC064A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D178A3D-247D-46C4-BAC3-DAB5EFC064A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D178A3D-247D-46C4-BAC3-DAB5EFC064A7}.Release|Any CPU.Build.0 = Release|Any CPU + {BB6D79C1-783F-4B87-A281-5EAB22CA7BF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB6D79C1-783F-4B87-A281-5EAB22CA7BF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB6D79C1-783F-4B87-A281-5EAB22CA7BF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB6D79C1-783F-4B87-A281-5EAB22CA7BF0}.Release|Any CPU.Build.0 = Release|Any CPU + {B6FBB81F-241F-4603-9510-E269F843F6CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6FBB81F-241F-4603-9510-E269F843F6CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6FBB81F-241F-4603-9510-E269F843F6CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6FBB81F-241F-4603-9510-E269F843F6CB}.Release|Any CPU.Build.0 = Release|Any CPU {D37C62B5-C15C-4F2E-A67D-E8D6324FBCBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D37C62B5-C15C-4F2E-A67D-E8D6324FBCBB}.Debug|Any CPU.Build.0 = Debug|Any CPU {D37C62B5-C15C-4F2E-A67D-E8D6324FBCBB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -254,18 +309,6 @@ Global {67B42C20-D98A-420B-82C0-04AFD963E650}.Debug|Any CPU.Build.0 = Debug|Any CPU {67B42C20-D98A-420B-82C0-04AFD963E650}.Release|Any CPU.ActiveCfg = Release|Any CPU {67B42C20-D98A-420B-82C0-04AFD963E650}.Release|Any CPU.Build.0 = Release|Any CPU - {80E6332B-DC83-4DFC-9C19-8D89E988BA4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {80E6332B-DC83-4DFC-9C19-8D89E988BA4E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {80E6332B-DC83-4DFC-9C19-8D89E988BA4E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {80E6332B-DC83-4DFC-9C19-8D89E988BA4E}.Release|Any CPU.Build.0 = Release|Any CPU - {329B9BE5-3F34-42A2-9E8C-D7BDD6B1EE55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {329B9BE5-3F34-42A2-9E8C-D7BDD6B1EE55}.Debug|Any CPU.Build.0 = Debug|Any CPU - {329B9BE5-3F34-42A2-9E8C-D7BDD6B1EE55}.Release|Any CPU.ActiveCfg = Release|Any CPU - {329B9BE5-3F34-42A2-9E8C-D7BDD6B1EE55}.Release|Any CPU.Build.0 = Release|Any CPU - {B9ECBAF7-524D-4134-AE37-EFF7802B221F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B9ECBAF7-524D-4134-AE37-EFF7802B221F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B9ECBAF7-524D-4134-AE37-EFF7802B221F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B9ECBAF7-524D-4134-AE37-EFF7802B221F}.Release|Any CPU.Build.0 = Release|Any CPU {481F242E-57E0-4904-89E2-345677D98C55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {481F242E-57E0-4904-89E2-345677D98C55}.Debug|Any CPU.Build.0 = Debug|Any CPU {481F242E-57E0-4904-89E2-345677D98C55}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -278,52 +321,63 @@ Global {C57FDCE0-2A8A-4827-95D4-439239E32EB9}.Debug|Any CPU.Build.0 = Debug|Any CPU {C57FDCE0-2A8A-4827-95D4-439239E32EB9}.Release|Any CPU.ActiveCfg = Release|Any CPU {C57FDCE0-2A8A-4827-95D4-439239E32EB9}.Release|Any CPU.Build.0 = Release|Any CPU - {12D7B81D-26FD-4BCA-9D72-AFDEA900BAAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {12D7B81D-26FD-4BCA-9D72-AFDEA900BAAC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {12D7B81D-26FD-4BCA-9D72-AFDEA900BAAC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {12D7B81D-26FD-4BCA-9D72-AFDEA900BAAC}.Release|Any CPU.Build.0 = Release|Any CPU - {FE69CE5F-2B54-417B-AFE1-88F190699901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FE69CE5F-2B54-417B-AFE1-88F190699901}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FE69CE5F-2B54-417B-AFE1-88F190699901}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FE69CE5F-2B54-417B-AFE1-88F190699901}.Release|Any CPU.Build.0 = Release|Any CPU - {1979687D-C3D1-4D40-9017-269D7C092283}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1979687D-C3D1-4D40-9017-269D7C092283}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1979687D-C3D1-4D40-9017-269D7C092283}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1979687D-C3D1-4D40-9017-269D7C092283}.Release|Any CPU.Build.0 = Release|Any CPU - {8A4C8106-67EC-4E5D-BC81-1DB617413EEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8A4C8106-67EC-4E5D-BC81-1DB617413EEA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8A4C8106-67EC-4E5D-BC81-1DB617413EEA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8A4C8106-67EC-4E5D-BC81-1DB617413EEA}.Release|Any CPU.Build.0 = Release|Any CPU - {D29DC7F0-0199-4759-B445-3E19ACCBB030}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D29DC7F0-0199-4759-B445-3E19ACCBB030}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D29DC7F0-0199-4759-B445-3E19ACCBB030}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D29DC7F0-0199-4759-B445-3E19ACCBB030}.Release|Any CPU.Build.0 = Release|Any CPU - {20974D06-EBDC-44B8-A034-DE1440CF7A45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20974D06-EBDC-44B8-A034-DE1440CF7A45}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20974D06-EBDC-44B8-A034-DE1440CF7A45}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20974D06-EBDC-44B8-A034-DE1440CF7A45}.Release|Any CPU.Build.0 = Release|Any CPU - {2D178A3D-247D-46C4-BAC3-DAB5EFC064A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2D178A3D-247D-46C4-BAC3-DAB5EFC064A7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2D178A3D-247D-46C4-BAC3-DAB5EFC064A7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2D178A3D-247D-46C4-BAC3-DAB5EFC064A7}.Release|Any CPU.Build.0 = Release|Any CPU {8EC131AE-D04F-4B7D-B223-D82821D8AFCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8EC131AE-D04F-4B7D-B223-D82821D8AFCC}.Debug|Any CPU.Build.0 = Debug|Any CPU {8EC131AE-D04F-4B7D-B223-D82821D8AFCC}.Release|Any CPU.ActiveCfg = Release|Any CPU {8EC131AE-D04F-4B7D-B223-D82821D8AFCC}.Release|Any CPU.Build.0 = Release|Any CPU - {BB6D79C1-783F-4B87-A281-5EAB22CA7BF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BB6D79C1-783F-4B87-A281-5EAB22CA7BF0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BB6D79C1-783F-4B87-A281-5EAB22CA7BF0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BB6D79C1-783F-4B87-A281-5EAB22CA7BF0}.Release|Any CPU.Build.0 = Release|Any CPU {D38CF672-795E-41D2-B10D-CFA87927BBDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D38CF672-795E-41D2-B10D-CFA87927BBDC}.Debug|Any CPU.Build.0 = Debug|Any CPU {D38CF672-795E-41D2-B10D-CFA87927BBDC}.Release|Any CPU.ActiveCfg = Release|Any CPU {D38CF672-795E-41D2-B10D-CFA87927BBDC}.Release|Any CPU.Build.0 = Release|Any CPU + {32B36B8F-C5F1-4426-8E38-528B22839F3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32B36B8F-C5F1-4426-8E38-528B22839F3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32B36B8F-C5F1-4426-8E38-528B22839F3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32B36B8F-C5F1-4426-8E38-528B22839F3E}.Release|Any CPU.Build.0 = Release|Any CPU + {A53A869F-D581-4499-AC49-13C2B75C2380}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A53A869F-D581-4499-AC49-13C2B75C2380}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A53A869F-D581-4499-AC49-13C2B75C2380}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A53A869F-D581-4499-AC49-13C2B75C2380}.Release|Any CPU.Build.0 = Release|Any CPU + {A712AE2E-C98E-4E83-9B33-789141763541}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A712AE2E-C98E-4E83-9B33-789141763541}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A712AE2E-C98E-4E83-9B33-789141763541}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A712AE2E-C98E-4E83-9B33-789141763541}.Release|Any CPU.Build.0 = Release|Any CPU + {2F02C533-538E-4F0D-A5DE-D029D99A49BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F02C533-538E-4F0D-A5DE-D029D99A49BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F02C533-538E-4F0D-A5DE-D029D99A49BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F02C533-538E-4F0D-A5DE-D029D99A49BD}.Release|Any CPU.Build.0 = Release|Any CPU + {7E8AC800-7D57-4645-83F5-FA101090D648}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E8AC800-7D57-4645-83F5-FA101090D648}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E8AC800-7D57-4645-83F5-FA101090D648}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E8AC800-7D57-4645-83F5-FA101090D648}.Release|Any CPU.Build.0 = Release|Any CPU + {5A7899EB-9366-477C-A0BC-A813D4211E78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A7899EB-9366-477C-A0BC-A813D4211E78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A7899EB-9366-477C-A0BC-A813D4211E78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A7899EB-9366-477C-A0BC-A813D4211E78}.Release|Any CPU.Build.0 = Release|Any CPU + {2D38272E-5E48-4ADE-9321-C73952613E70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D38272E-5E48-4ADE-9321-C73952613E70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D38272E-5E48-4ADE-9321-C73952613E70}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D38272E-5E48-4ADE-9321-C73952613E70}.Release|Any CPU.Build.0 = Release|Any CPU + {B9FA069D-D447-4303-9426-8BD4158E537C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9FA069D-D447-4303-9426-8BD4158E537C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9FA069D-D447-4303-9426-8BD4158E537C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9FA069D-D447-4303-9426-8BD4158E537C}.Release|Any CPU.Build.0 = Release|Any CPU + {3A0C9831-F43A-420D-83B7-9ECC4EAEA39D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A0C9831-F43A-420D-83B7-9ECC4EAEA39D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A0C9831-F43A-420D-83B7-9ECC4EAEA39D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A0C9831-F43A-420D-83B7-9ECC4EAEA39D}.Release|Any CPU.Build.0 = Release|Any CPU + {A5BDAB00-68C2-4661-AD49-735D94ECED06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5BDAB00-68C2-4661-AD49-735D94ECED06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5BDAB00-68C2-4661-AD49-735D94ECED06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5BDAB00-68C2-4661-AD49-735D94ECED06}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {B6FBB81F-241F-4603-9510-E269F843F6CB} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} + {FE69CE5F-2B54-417B-AFE1-88F190699901} = {E1DE693E-C1FB-4A1F-B6D8-A071D86D7354} + {1979687D-C3D1-4D40-9017-269D7C092283} = {E1DE693E-C1FB-4A1F-B6D8-A071D86D7354} + {8A4C8106-67EC-4E5D-BC81-1DB617413EEA} = {E1DE693E-C1FB-4A1F-B6D8-A071D86D7354} + {D29DC7F0-0199-4759-B445-3E19ACCBB030} = {E1DE693E-C1FB-4A1F-B6D8-A071D86D7354} {9E0BF83B-CC6B-472A-AA6E-FFD507AE9D9F} = {4B000021-5278-4F2A-B734-DE49F55D4024} {94177D1E-1CD3-4878-9FB2-3EB8DFD5CC2C} = {4B000021-5278-4F2A-B734-DE49F55D4024} {60E282CB-156B-4A5E-9C7D-4E174EA2E024} = {4B000021-5278-4F2A-B734-DE49F55D4024} @@ -340,7 +394,14 @@ Global {D49453F3-A51A-49B8-B151-1C7CD92C229E} = {4B000021-5278-4F2A-B734-DE49F55D4024} {D1881B9D-6D47-40CC-83BA-23F102A30DD5} = {4B000021-5278-4F2A-B734-DE49F55D4024} {66579C6A-8316-4888-8D96-36D4B49D7D51} = {4B000021-5278-4F2A-B734-DE49F55D4024} - {32B36B8F-C5F1-4426-8E38-528B22839F3E} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} + {80E6332B-DC83-4DFC-9C19-8D89E988BA4E} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {329B9BE5-3F34-42A2-9E8C-D7BDD6B1EE55} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {B9ECBAF7-524D-4134-AE37-EFF7802B221F} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {12D7B81D-26FD-4BCA-9D72-AFDEA900BAAC} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {20974D06-EBDC-44B8-A034-DE1440CF7A45} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {2D178A3D-247D-46C4-BAC3-DAB5EFC064A7} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {BB6D79C1-783F-4B87-A281-5EAB22CA7BF0} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {B6FBB81F-241F-4603-9510-E269F843F6CB} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} {D37C62B5-C15C-4F2E-A67D-E8D6324FBCBB} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} {0935E538-179C-4768-89B0-C0D197B4D18B} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} {5177869A-5D73-428B-88A0-6E0D3BB04A64} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} @@ -356,21 +417,20 @@ Global {CA09C859-39AE-41B4-A977-4D26D7DE6C18} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} {D5281344-D323-4E13-8BC7-4F39814AD4A6} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} {67B42C20-D98A-420B-82C0-04AFD963E650} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} - {80E6332B-DC83-4DFC-9C19-8D89E988BA4E} = {4B000021-5278-4F2A-B734-DE49F55D4024} - {329B9BE5-3F34-42A2-9E8C-D7BDD6B1EE55} = {4B000021-5278-4F2A-B734-DE49F55D4024} - {B9ECBAF7-524D-4134-AE37-EFF7802B221F} = {4B000021-5278-4F2A-B734-DE49F55D4024} {481F242E-57E0-4904-89E2-345677D98C55} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} {872C74A2-97BD-4672-9674-BC361419AF32} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} {C57FDCE0-2A8A-4827-95D4-439239E32EB9} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} - {12D7B81D-26FD-4BCA-9D72-AFDEA900BAAC} = {4B000021-5278-4F2A-B734-DE49F55D4024} - {FE69CE5F-2B54-417B-AFE1-88F190699901} = {E1DE693E-C1FB-4A1F-B6D8-A071D86D7354} - {1979687D-C3D1-4D40-9017-269D7C092283} = {E1DE693E-C1FB-4A1F-B6D8-A071D86D7354} - {8A4C8106-67EC-4E5D-BC81-1DB617413EEA} = {E1DE693E-C1FB-4A1F-B6D8-A071D86D7354} - {D29DC7F0-0199-4759-B445-3E19ACCBB030} = {E1DE693E-C1FB-4A1F-B6D8-A071D86D7354} - {20974D06-EBDC-44B8-A034-DE1440CF7A45} = {4B000021-5278-4F2A-B734-DE49F55D4024} - {2D178A3D-247D-46C4-BAC3-DAB5EFC064A7} = {4B000021-5278-4F2A-B734-DE49F55D4024} {8EC131AE-D04F-4B7D-B223-D82821D8AFCC} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} - {BB6D79C1-783F-4B87-A281-5EAB22CA7BF0} = {4B000021-5278-4F2A-B734-DE49F55D4024} {D38CF672-795E-41D2-B10D-CFA87927BBDC} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} + {32B36B8F-C5F1-4426-8E38-528B22839F3E} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} + {A53A869F-D581-4499-AC49-13C2B75C2380} = {E1DE693E-C1FB-4A1F-B6D8-A071D86D7354} + {A712AE2E-C98E-4E83-9B33-789141763541} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {2F02C533-538E-4F0D-A5DE-D029D99A49BD} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {7E8AC800-7D57-4645-83F5-FA101090D648} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {5A7899EB-9366-477C-A0BC-A813D4211E78} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {2D38272E-5E48-4ADE-9321-C73952613E70} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {B9FA069D-D447-4303-9426-8BD4158E537C} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {3A0C9831-F43A-420D-83B7-9ECC4EAEA39D} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} + {A5BDAB00-68C2-4661-AD49-735D94ECED06} = {4B000021-5278-4F2A-B734-DE49F55D4024} EndGlobalSection EndGlobal diff --git a/samples/System.IO.Pipelines.Samples/AspNetHttpServerSample.cs b/samples/System.IO.Pipelines.Samples/AspNetHttpServerSample.cs new file mode 100644 index 00000000000..a97271309e2 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/AspNetHttpServerSample.cs @@ -0,0 +1,38 @@ +using System.Text; +using System.IO.Pipelines.Samples.Http; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace System.IO.Pipelines.Samples +{ + public class AspNetHttpServerSample + { + private static readonly UTF8Encoding _utf8Encoding = new UTF8Encoding(false); + private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!"); + + public static void Run() + { + using (var httpServer = new HttpServer()) + { + var host = new WebHostBuilder() + .UseUrls("http://*:5000") + .UseServer(httpServer) + // .UseKestrel() + .Configure(app => + { + app.Run(context => + { + context.Response.StatusCode = 200; + context.Response.ContentType = "text/plain"; + // HACK: Setting the Content-Length header manually avoids the cost of serializing the int to a string. + // This is instead of: httpContext.Response.ContentLength = _helloWorldPayload.Length; + context.Response.Headers["Content-Length"] = "13"; + return context.Response.Body.WriteAsync(_helloWorldPayload, 0, _helloWorldPayload.Length); + }); + }) + .Build(); + host.Run(); + } + } + } +} \ No newline at end of file diff --git a/samples/System.IO.Pipelines.Samples/CompressionSample.cs b/samples/System.IO.Pipelines.Samples/CompressionSample.cs new file mode 100644 index 00000000000..74501db7cd0 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/CompressionSample.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.IO.Pipelines.Compression; +using System.IO.Pipelines.File; + +namespace System.IO.Pipelines.Samples +{ + public class CompressionSample + { + public static void Run() + { + using (var factory = new PipelineFactory()) + { + var filePath = Path.GetFullPath("Program.cs"); + + // This is what Stream looks like + //var fs = File.OpenRead(filePath); + //var compressed = new MemoryStream(); + //var compressStream = new DeflateStream(compressed, CompressionMode.Compress); + //fs.CopyTo(compressStream); + //compressStream.Flush(); + //compressed.Seek(0, SeekOrigin.Begin); + + var input = factory.ReadFile(filePath) + .DeflateCompress(factory, CompressionLevel.Optimal) + .DeflateDecompress(factory); + + // Wrap the console in a pipeline writer + var output = factory.CreateWriter(Console.OpenStandardOutput()); + + // Copy from the file reader to the console writer + input.CopyToAsync(output).GetAwaiter().GetResult(); + + input.Complete(); + + output.Complete(); + } + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/Framing/Codec.cs b/samples/System.IO.Pipelines.Samples/Framing/Codec.cs new file mode 100644 index 00000000000..a76f7b3c462 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/Framing/Codec.cs @@ -0,0 +1,142 @@ +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Text.Formatting; +using System.Threading.Tasks; +using System.IO.Pipelines.Networking.Libuv; +using System.IO.Pipelines.Text.Primitives; + +namespace System.IO.Pipelines.Samples.Framing +{ + public static class ProtocolHandling + { + public static void Run() + { + var ip = IPAddress.Any; + int port = 5000; + var thread = new UvThread(); + var listener = new UvTcpListener(thread, new IPEndPoint(ip, port)); + listener.OnConnection(async connection => + { + var pipelineConnection = MakePipeline(connection); + + var decoder = new LineDecoder(); + var handler = new LineHandler(); + + // Initialize the handler with the connection + handler.Initialize(pipelineConnection); + + try + { + while (true) + { + // Wait for data + var result = await pipelineConnection.Input.ReadAsync(); + var input = result.Buffer; + + try + { + if (input.IsEmpty && result.IsCompleted) + { + // No more data + break; + } + + Line line; + while (decoder.TryDecode(ref input, out line)) + { + await handler.HandleAsync(line); + } + + if (!input.IsEmpty && result.IsCompleted) + { + // Didn't get the whole frame and the connection ended + throw new EndOfStreamException(); + } + } + finally + { + // Consume the input + pipelineConnection.Input.Advance(input.Start, input.End); + } + } + } + finally + { + // Close the input, which will tell the producer to stop producing + pipelineConnection.Input.Complete(); + + // Close the output, which will close the connection + pipelineConnection.Output.Complete(); + } + }); + + listener.StartAsync().GetAwaiter().GetResult(); + + Console.WriteLine($"Listening on {ip} on port {port}"); + Console.ReadKey(); + + listener.Dispose(); + thread.Dispose(); + } + + public static IPipelineConnection MakePipeline(IPipelineConnection connection) + { + // Do something fancy here to wrap the connection, SSL etc + return connection; + } + } + + public class Line + { + public string Data { get; set; } + } + + public class LineHandler : IFrameHandler + { + private PipelineTextOutput _textOutput; + + public void Initialize(IPipelineConnection connection) + { + _textOutput = new PipelineTextOutput(connection.Output, EncodingData.InvariantUtf8); + } + + public Task HandleAsync(Line message) + { + // Echo back to the caller + _textOutput.Append(message.Data); + return _textOutput.FlushAsync(); + } + } + + public class LineDecoder : IFrameDecoder + { + public bool TryDecode(ref ReadableBuffer input, out Line frame) + { + ReadableBuffer slice; + ReadCursor cursor; + if (input.TrySliceTo((byte)'\r', (byte)'\n', out slice, out cursor)) + { + frame = new Line { Data = slice.GetUtf8String() }; + input = input.Slice(cursor).Slice(1); + return true; + } + + frame = null; + return false; + } + } + + public interface IFrameDecoder + { + bool TryDecode(ref ReadableBuffer input, out TInput frame); + } + + public interface IFrameHandler + { + void Initialize(IPipelineConnection connection); + + Task HandleAsync(TInput message); + } +} diff --git a/samples/System.IO.Pipelines.Samples/HttpClient/LibuvHttpClientHandler.cs b/samples/System.IO.Pipelines.Samples/HttpClient/LibuvHttpClientHandler.cs new file mode 100644 index 00000000000..5c066b2a944 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpClient/LibuvHttpClientHandler.cs @@ -0,0 +1,300 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Sockets; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; +using System.IO.Pipelines.Networking.Libuv; +using System.IO.Pipelines.Text.Primitives; + +namespace System.IO.Pipelines.Samples +{ + public class LibuvHttpClientHandler : HttpClientHandler + { + private readonly UvThread _thread = new UvThread(); + + private ConcurrentDictionary _connectionPool = new ConcurrentDictionary(); + + public LibuvHttpClientHandler() + { + + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var key = request.RequestUri.GetComponents(UriComponents.HostAndPort, UriFormat.SafeUnescaped); + var path = request.RequestUri.GetComponents(UriComponents.PathAndQuery, UriFormat.SafeUnescaped); + + var state = _connectionPool.GetOrAdd(key, k => GetConnection(request)); + var connection = await state.ConnectionTask; + + var requestBuffer = connection.Output.Alloc(); + requestBuffer.WriteAsciiString($"{request.Method} {path} HTTP/1.1"); + WriteHeaders(request.Headers, ref requestBuffer); + + // End of the headers + requestBuffer.WriteAsciiString("\r\n\r\n"); + + if (request.Method != HttpMethod.Get && request.Method != HttpMethod.Head) + { + WriteHeaders(request.Content.Headers, ref requestBuffer); + + await requestBuffer.FlushAsync(); + + // Copy the body to the input + var body = await request.Content.ReadAsStreamAsync(); + + await body.CopyToAsync(connection.Output); + } + else + { + await requestBuffer.FlushAsync(); + } + + var response = new HttpResponseMessage(); + response.Content = new PipelineHttpContent(connection.Input); + + await ProduceResponse(state, connection, response); + + // Get off the libuv thread + await Task.Yield(); + + return response; + } + + private static async Task ProduceResponse(ConnectionState state, UvTcpConnection connection, HttpResponseMessage response) + { + // TODO: pipelining support! + while (true) + { + var result = await connection.Input.ReadAsync(); + var responseBuffer = result.Buffer; + + var consumed = responseBuffer.Start; + + var needMoreData = true; + + try + { + if (consumed == state.Consumed) + { + var oldBody = responseBuffer.Slice(0, state.PreviousContentLength); + + if (oldBody.Length != state.PreviousContentLength) + { + // Not enough data + continue; + } + + // The caller didn't read the body + responseBuffer = responseBuffer.Slice(state.PreviousContentLength); + consumed = responseBuffer.Start; + + state.Consumed = default(ReadCursor); + } + + if (responseBuffer.IsEmpty && result.IsCompleted) + { + break; + } + + ReadCursor delim; + ReadableBuffer responseLine; + if (!responseBuffer.TrySliceTo((byte)'\r', (byte)'\n', out responseLine, out delim)) + { + continue; + } + + responseBuffer = responseBuffer.Slice(delim).Slice(2); + + ReadableBuffer httpVersion; + if (!responseLine.TrySliceTo((byte)' ', out httpVersion, out delim)) + { + // Bad request + throw new InvalidOperationException(); + } + + consumed = responseBuffer.Start; + + responseLine = responseLine.Slice(delim).Slice(1); + + ReadableBuffer statusCode; + if (!responseLine.TrySliceTo((byte)' ', out statusCode, out delim)) + { + // Bad request + throw new InvalidOperationException(); + } + + response.StatusCode = (HttpStatusCode)statusCode.GetUInt32(); + responseLine = responseLine.Slice(delim).Slice(1); + + ReadableBuffer remaining; + if (!responseLine.TrySliceTo((byte)' ', out remaining, out delim)) + { + // Bad request + throw new InvalidOperationException(); + } + + while (!responseBuffer.IsEmpty) + { + var ch = responseBuffer.Peek(); + + if (ch == -1) + { + break; + } + + if (ch == '\r') + { + // Check for final CRLF. + responseBuffer = responseBuffer.Slice(1); + ch = responseBuffer.Peek(); + responseBuffer = responseBuffer.Slice(1); + + if (ch == -1) + { + break; + } + else if (ch == '\n') + { + consumed = responseBuffer.Start; + needMoreData = false; + break; + } + + // Headers don't end in CRLF line. + throw new Exception(); + } + + var headerName = default(ReadableBuffer); + var headerValue = default(ReadableBuffer); + + // End of the header + // \n + ReadableBuffer headerPair; + if (!responseBuffer.TrySliceTo((byte)'\n', out headerPair, out delim)) + { + break; + } + + responseBuffer = responseBuffer.Slice(delim).Slice(1); + + // : + if (!headerPair.TrySliceTo((byte)':', out headerName, out delim)) + { + throw new Exception(); + } + + headerName = headerName.TrimStart(); + headerPair = headerPair.Slice(headerName.End).Slice(1); + + // \r + if (!headerPair.TrySliceTo((byte)'\r', out headerValue, out delim)) + { + // Bad request + throw new Exception(); + } + + headerValue = headerValue.TrimStart(); + var hKey = headerName.GetAsciiString(); + var hValue = headerValue.GetAsciiString(); + + if (!response.Content.Headers.TryAddWithoutValidation(hKey, hValue)) + { + response.Headers.TryAddWithoutValidation(hKey, hValue); + } + + // Move the consumed + consumed = responseBuffer.Start; + } + } + catch (Exception ex) + { + // Close the connection + connection.Output.Complete(ex); + break; + } + finally + { + connection.Input.Advance(consumed); + } + + if (needMoreData) + { + continue; + } + + // Only handle content length for now + var length = response.Content.Headers.ContentLength; + + if (!length.HasValue) + { + throw new NotSupportedException(); + } + + checked + { + // BAD but it's a proof of concept ok? + state.PreviousContentLength = (int)length.Value; + ((PipelineHttpContent)response.Content).ContentLength = (int)length; + state.Consumed = consumed; + } + + break; + } + } + + private static void WriteHeaders(HttpHeaders headers, ref WritableBuffer buffer) + { + foreach (var header in headers) + { + buffer.WriteAsciiString($"{header.Key}:{string.Join(",", header.Value)}\r\n"); + } + } + + private ConnectionState GetConnection(HttpRequestMessage request) + { + var state = new ConnectionState + { + ConnectionTask = ConnectAsync(request) + }; + + return state; + } + + private async Task ConnectAsync(HttpRequestMessage request) + { + var addresses = await Dns.GetHostAddressesAsync(request.RequestUri.Host); + var port = request.RequestUri.Port; + + var address = addresses.First(a => a.AddressFamily == AddressFamily.InterNetwork); + var connection = new UvTcpClient(_thread, new IPEndPoint(address, port)); + return await connection.ConnectAsync(); + } + + protected override void Dispose(bool disposing) + { + foreach (var state in _connectionPool) + { + state.Value.ConnectionTask.GetAwaiter().GetResult().Output.Complete(); + } + + _thread.Dispose(); + + base.Dispose(disposing); + } + + private class ConnectionState + { + public Task ConnectionTask { get; set; } + + public int PreviousContentLength { get; set; } + + public ReadCursor Consumed { get; set; } + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/HttpClient/PipelineHttpContent.cs b/samples/System.IO.Pipelines.Samples/HttpClient/PipelineHttpContent.cs new file mode 100644 index 00000000000..db9ea0ef57f --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpClient/PipelineHttpContent.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.Samples +{ + public class PipelineHttpContent : HttpContent + { + private readonly IPipelineReader _output; + + public PipelineHttpContent(IPipelineReader output) + { + _output = output; + } + + public int ContentLength { get; set; } + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + int remaining = ContentLength; + + while (remaining > 0) + { + var result = await _output.ReadAsync(); + var inputBuffer = result.Buffer; + + var fin = result.IsCompleted; + + var consumed = inputBuffer.Start; + + try + { + if (inputBuffer.IsEmpty && fin) + { + return; + } + + var data = inputBuffer.Slice(0, remaining); + + foreach (var memory in data) + { + ArraySegment buffer; + + unsafe + { + if (!memory.TryGetArray(out buffer)) + { + // Fall back to copies if this was native memory and we were unable to get + // something we could write + buffer = new ArraySegment(memory.Span.ToArray()); + } + } + + await stream.WriteAsync(buffer.Array, buffer.Offset, buffer.Count); + } + + consumed = data.End; + remaining -= data.Length; + } + finally + { + _output.Advance(consumed); + } + } + } + + protected override bool TryComputeLength(out long length) + { + length = ContentLength; + return true; + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/HttpServer/FormReader.cs b/samples/System.IO.Pipelines.Samples/HttpServer/FormReader.cs new file mode 100644 index 00000000000..d4dacbf0f43 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpServer/FormReader.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Numerics; +using System.IO.Pipelines.Text.Primitives; +using Microsoft.Extensions.Primitives; + +namespace System.IO.Pipelines.Samples.Http +{ + public class FormReader + { + private Dictionary _data = new Dictionary(); + private long? _contentLength; + + public FormReader(long? contentLength) + { + _contentLength = contentLength; + } + + public Dictionary FormValues => _data; + + public bool TryParse(ref ReadableBuffer buffer) + { + if (buffer.IsEmpty || !_contentLength.HasValue) + { + return true; + } + + while (!buffer.IsEmpty && _contentLength > 0) + { + var next = buffer; + ReadCursor delim; + ReadableBuffer key; + if (!next.TrySliceTo((byte)'=', out key, out delim)) + { + break; + } + + next = next.Slice(delim).Slice(1); + + ReadableBuffer value; + if (next.TrySliceTo((byte)'&', out value, out delim)) + { + next = next.Slice(delim).Slice(1); + } + else + { + + var remaining = _contentLength - buffer.Length; + + if (remaining == 0) + { + value = next; + next = next.Slice(next.End); + } + else + { + break; + } + } + + // TODO: Combine multi value keys + _data[key.GetUtf8String()] = value.GetUtf8String(); + _contentLength -= (buffer.Length - next.Length); + buffer = next; + } + + return _contentLength == 0; + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/HttpServer/HttpConnection.Features.cs b/samples/System.IO.Pipelines.Samples/HttpServer/HttpConnection.Features.cs new file mode 100644 index 00000000000..d66eb83ab54 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpServer/HttpConnection.Features.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.IO.Pipelines.Text.Primitives; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace System.IO.Pipelines.Samples.Http +{ + public partial class HttpConnection : IHttpRequestFeature, IHttpResponseFeature, IFeatureCollection + { + private FeatureCollection _features = new FeatureCollection(); + + public object this[Type key] + { + get + { + return GetFeature(key); + } + + set + { + SetFeature(key, value); + } + } + + private object GetFeature(Type key) + { + if (key == typeof(IHttpRequestFeature)) + { + return this; + } + + if (key == typeof(IHttpResponseFeature)) + { + return this; + } + + return _features[key]; + } + + private void SetFeature(Type key, object value) + { + _features[key] = value; + } + + public bool HasStarted { get; set; } + + Stream IHttpRequestFeature.Body + { + get + { + return _requestBody; + } + set + { + + } + } + + Stream IHttpResponseFeature.Body + { + get + { + return _responseBody; + } + set + { + + } + } + + IHeaderDictionary IHttpResponseFeature.Headers + { + get + { + return ResponseHeaders; + } + + set + { + throw new NotSupportedException(); + } + } + + IHeaderDictionary IHttpRequestFeature.Headers + { + get + { + return RequestHeaders; + } + + set + { + throw new NotSupportedException(); + } + } + + public bool IsReadOnly => false; + + private string _method; + string IHttpRequestFeature.Method + { + get + { + if (_method == null) + { + _method = Method.GetAsciiString(); + } + + return _method; + } + set + { + _method = value; + } + } + + private string _path; + string IHttpRequestFeature.Path + { + get + { + if (_path == null) + { + _path = Path.GetAsciiString(); + } + return _path; + } + set + { + _path = value; + } + } + + public string PathBase { get; set; } + + public string Protocol { get; set; } + + public string QueryString { get; set; } + + public string RawTarget { get; set; } + + public string ReasonPhrase { get; set; } + + public int Revision { get; set; } + + public string Scheme { get; set; } = "http"; + + public int StatusCode { get; set; } + + public TFeature Get() + { + return (TFeature)this[typeof(TFeature)]; + } + + public IEnumerator> GetEnumerator() + { + throw new NotImplementedException(); + } + + public void OnCompleted(Func callback, object state) + { + throw new NotImplementedException(); + } + + public void OnStarting(Func callback, object state) + { + throw new NotImplementedException(); + } + + public void Set(TFeature instance) + { + this[typeof(TFeature)] = instance; + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotSupportedException(); + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/HttpServer/HttpConnection.cs b/samples/System.IO.Pipelines.Samples/HttpServer/HttpConnection.cs new file mode 100644 index 00000000000..f702823885c --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpServer/HttpConnection.cs @@ -0,0 +1,214 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.IO.Pipelines.Text.Primitives; +using Microsoft.AspNetCore.Hosting.Server; + +namespace System.IO.Pipelines.Samples.Http +{ + public partial class HttpConnection + { + private static readonly byte[] _http11Bytes = Encoding.UTF8.GetBytes("HTTP/1.1 "); + private static readonly byte[] _chunkedEndBytes = Encoding.UTF8.GetBytes("0\r\n\r\n"); + private static readonly byte[] _endChunkBytes = Encoding.ASCII.GetBytes("\r\n"); + + private readonly IPipelineReader _input; + private readonly IPipelineWriter _output; + private readonly IHttpApplication _application; + + public RequestHeaderDictionary RequestHeaders => _parser.RequestHeaders; + public ResponseHeaderDictionary ResponseHeaders { get; } = new ResponseHeaderDictionary(); + + public ReadableBuffer HttpVersion => _parser.HttpVersion; + public ReadableBuffer Path => _parser.Path; + public ReadableBuffer Method => _parser.Method; + + // TODO: Check the http version + public bool KeepAlive => true; //RequestHeaders.ContainsKey("Connection") && string.Equals(RequestHeaders["Connection"], "keep-alive"); + + private bool HasContentLength => ResponseHeaders.ContainsKey("Content-Length"); + private bool HasTransferEncoding => ResponseHeaders.ContainsKey("Transfer-Encoding"); + + private HttpRequestStream _requestBody; + private HttpResponseStream _responseBody; + + private bool _autoChunk; + + private HttpRequestParser _parser = new HttpRequestParser(); + + public HttpConnection(IHttpApplication application, IPipelineReader input, IPipelineWriter output) + { + _application = application; + _input = input; + _output = output; + _requestBody = new HttpRequestStream(this); + _responseBody = new HttpResponseStream(this); + } + + public IPipelineReader Input => _input; + + public IPipelineWriter Output => _output; + + public HttpRequestStream RequestBody { get; set; } + + public HttpResponseStream ResponseBody { get; set; } + + + public async Task ProcessAllRequests() + { + Reset(); + + while (true) + { + var result = await _input.ReadAsync(); + var buffer = result.Buffer; + + try + { + if (buffer.IsEmpty && result.IsCompleted) + { + // We're done with this connection + return; + } + + var parserResult = _parser.ParseRequest(ref buffer); + + switch (parserResult) + { + case HttpRequestParser.ParseResult.Incomplete: + if (result.IsCompleted) + { + // Didn't get the whole request and the connection ended + throw new EndOfStreamException(); + } + // Need more data + continue; + case HttpRequestParser.ParseResult.Complete: + // Done + break; + case HttpRequestParser.ParseResult.BadRequest: + // TODO: Don't throw here; + throw new Exception(); + default: + break; + } + + } + catch (Exception) + { + StatusCode = 400; + + await EndResponse(); + + return; + } + finally + { + _input.Advance(buffer.Start, buffer.End); + } + + var context = _application.CreateContext(this); + + try + { + await _application.ProcessRequestAsync(context); + } + catch (Exception ex) + { + StatusCode = 500; + + _application.DisposeContext(context, ex); + } + finally + { + await EndResponse(); + } + + if (!KeepAlive) + { + break; + } + + Reset(); + } + } + + private Task EndResponse() + { + var buffer = _output.Alloc(); + + if (!HasStarted) + { + WriteBeginResponseHeaders(buffer); + } + + if (_autoChunk) + { + WriteEndResponse(buffer); + } + + return buffer.FlushAsync(); + } + + private void Reset() + { + RequestBody = _requestBody; + ResponseBody = _responseBody; + _parser.Reset(); + ResponseHeaders.Reset(); + HasStarted = false; + StatusCode = 200; + _autoChunk = false; + _method = null; + _path = null; + } + + public Task WriteAsync(Span data) + { + var buffer = _output.Alloc(); + + if (!HasStarted) + { + WriteBeginResponseHeaders(buffer); + } + + if (_autoChunk) + { + buffer.WriteHex(data.Length); + buffer.Write(_endChunkBytes); + buffer.Write(data); + buffer.Write(_endChunkBytes); + } + else + { + buffer.Write(data); + } + + return buffer.FlushAsync(); + } + + private void WriteBeginResponseHeaders(WritableBuffer buffer) + { + if (HasStarted) + { + return; + } + + HasStarted = true; + + buffer.Write(_http11Bytes); + var status = ReasonPhrases.ToStatusBytes(StatusCode); + buffer.Write(status); + + _autoChunk = !HasContentLength && !HasTransferEncoding && KeepAlive; + + ResponseHeaders.CopyTo(_autoChunk, buffer); + } + + private void WriteEndResponse(WritableBuffer buffer) + { + buffer.Write(_chunkedEndBytes); + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/HttpServer/HttpRequestParser.cs b/samples/System.IO.Pipelines.Samples/HttpServer/HttpRequestParser.cs new file mode 100644 index 00000000000..2cf3540575d --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpServer/HttpRequestParser.cs @@ -0,0 +1,133 @@ +using System.IO.Pipelines.Samples.Http; +using System.IO.Pipelines.Text.Primitives; + +namespace System.IO.Pipelines.Samples +{ + public class HttpRequestParser + { + private ParsingState _state; + + private PreservedBuffer _httpVersion; + private PreservedBuffer _path; + private PreservedBuffer _method; + + public ReadableBuffer HttpVersion => _httpVersion.Buffer; + public ReadableBuffer Path => _path.Buffer; + public ReadableBuffer Method => _method.Buffer; + + public RequestHeaderDictionary RequestHeaders = new RequestHeaderDictionary(); + + public ParseResult ParseRequest(ref ReadableBuffer buffer) + { + if (_state == ParsingState.StartLine) + { + // Find \n + ReadCursor delim; + ReadableBuffer startLine; + if (!buffer.TrySliceTo((byte)'\r', (byte)'\n', out startLine, out delim)) + { + return ParseResult.Incomplete; + } + + // Move the buffer to the rest + buffer = buffer.Slice(delim).Slice(2); + + ReadableBuffer method; + if (!startLine.TrySliceTo((byte)' ', out method, out delim)) + { + return ParseResult.BadRequest; + } + + _method = method.Preserve(); + + // Skip ' ' + startLine = startLine.Slice(delim).Slice(1); + + ReadableBuffer path; + if (!startLine.TrySliceTo((byte)' ', out path, out delim)) + { + return ParseResult.BadRequest; + } + + _path = path.Preserve(); + + // Skip ' ' + startLine = startLine.Slice(delim).Slice(1); + + var httpVersion = startLine; + if (httpVersion.IsEmpty) + { + return ParseResult.BadRequest; + } + + _httpVersion = httpVersion.Preserve(); + + _state = ParsingState.Headers; + } + + // Parse headers + // key: value\r\n + + while (!buffer.IsEmpty) + { + var headerName = default(ReadableBuffer); + var headerValue = default(ReadableBuffer); + + // End of the header + // \n + ReadCursor delim; + ReadableBuffer headerPair; + if (!buffer.TrySliceTo((byte)'\r', (byte)'\n', out headerPair, out delim)) + { + return ParseResult.Incomplete; + } + + buffer = buffer.Slice(delim).Slice(2); + + // End of headers + if (headerPair.IsEmpty) + { + return ParseResult.Complete; + } + + // : + if (!headerPair.TrySliceTo((byte)':', out headerName, out delim)) + { + return ParseResult.BadRequest; + } + + headerName = headerName.TrimStart(); + headerPair = headerPair.Slice(delim).Slice(1); + + headerValue = headerPair.TrimStart(); + RequestHeaders.SetHeader(ref headerName, ref headerValue); + } + + return ParseResult.Incomplete; + } + + public void Reset() + { + _state = ParsingState.StartLine; + + _method.Dispose(); + _path.Dispose(); + _httpVersion.Dispose(); + + RequestHeaders.Reset(); + } + + public enum ParseResult + { + Incomplete, + Complete, + BadRequest, + } + + private enum ParsingState + { + StartLine, + Headers + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/HttpServer/HttpRequestStream.cs b/samples/System.IO.Pipelines.Samples/HttpServer/HttpRequestStream.cs new file mode 100644 index 00000000000..8e588188025 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpServer/HttpRequestStream.cs @@ -0,0 +1,148 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.Samples.Http +{ + public class HttpRequestStream : Stream + { + private readonly static Task _initialCachedTask = Task.FromResult(0); + private Task _cachedTask = _initialCachedTask; + + private readonly HttpConnection _connection; + + public HttpRequestStream(HttpConnection connection) + { + _connection = connection; + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length + { + get + { + throw new NotSupportedException(); + } + } + + public override long Position + { + get + { + throw new NotSupportedException(); + } + set + { + throw new NotSupportedException(); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + public override int Read(byte[] buffer, int offset, int count) + { + // ValueTask uses .GetAwaiter().GetResult() if necessary + // https://github.com/dotnet/corefx/blob/f9da3b4af08214764a51b2331f3595ffaf162abe/src/System.Threading.Tasks.Extensions/src/System/Threading/Tasks/ValueTask.cs#L156 + return ReadAsync(new ArraySegment(buffer, offset, count)).Result; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var task = ReadAsync(new ArraySegment(buffer, offset, count)); + + if (task.IsCompletedSuccessfully) + { + if (_cachedTask.Result != task.Result) + { + // Needs .AsTask to match Stream's Async method return types + _cachedTask = task.AsTask(); + } + } + else + { + // Needs .AsTask to match Stream's Async method return types + _cachedTask = task.AsTask(); + } + + return _cachedTask; + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + private ValueTask ReadAsync(ArraySegment buffer) + { + return _connection.Input.ReadAsync(new Span(buffer.Array, buffer.Offset, buffer.Count)); + } + +#if NET451 + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + var task = ReadAsync(buffer, offset, count, default(CancellationToken), state); + if (callback != null) + { + task.ContinueWith(t => callback.Invoke(t)); + } + return task; + } + + public override int EndRead(IAsyncResult asyncResult) + { + return ((Task)asyncResult).GetAwaiter().GetResult(); + } + + private Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) + { + var tcs = new TaskCompletionSource(state); + var task = ReadAsync(buffer, offset, count, cancellationToken); + task.ContinueWith((task2, state2) => + { + var tcs2 = (TaskCompletionSource)state2; + if (task2.IsCanceled) + { + tcs2.SetCanceled(); + } + else if (task2.IsFaulted) + { + tcs2.SetException(task2.Exception); + } + else + { + tcs2.SetResult(task2.Result); + } + }, tcs, cancellationToken); + return tcs.Task; + } +#endif + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _connection.Input.CopyToAsync(destination, bufferSize, cancellationToken); + } + + } +} \ No newline at end of file diff --git a/samples/System.IO.Pipelines.Samples/HttpServer/HttpResponseStream.cs b/samples/System.IO.Pipelines.Samples/HttpServer/HttpResponseStream.cs new file mode 100644 index 00000000000..c067bdc228c --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpServer/HttpResponseStream.cs @@ -0,0 +1,121 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.Samples.Http +{ + public class HttpResponseStream : Stream + { + private readonly static Task _initialCachedTask = Task.FromResult(0); + private Task _cachedTask = _initialCachedTask; + + private readonly HttpConnection _connection; + + public HttpResponseStream(HttpConnection connection) + { + _connection = connection; + } + + public override bool CanRead => false; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length + { + get + { + throw new NotSupportedException(); + } + } + + public override long Position + { + get + { + throw new NotSupportedException(); + } + set + { + throw new NotSupportedException(); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token) + { + return _connection.WriteAsync(new Span(buffer, offset, count)); + } + + public override void Flush() + { + // No-op since writes are immediate. + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + // No-op since writes are immediate. + return Task.FromResult(0); + } + +#if NET451 + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + var task = WriteAsync(buffer, offset, count, default(CancellationToken), state); + if (callback != null) + { + task.ContinueWith(t => callback.Invoke(t)); + } + return task; + } + + public override void EndWrite(IAsyncResult asyncResult) + { + ((Task)asyncResult).GetAwaiter().GetResult(); + } + + private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) + { + var tcs = new TaskCompletionSource(state); + var task = WriteAsync(buffer, offset, count, cancellationToken); + task.ContinueWith((task2, state2) => + { + var tcs2 = (TaskCompletionSource)state2; + if (task2.IsCanceled) + { + tcs2.SetCanceled(); + } + else if (task2.IsFaulted) + { + tcs2.SetException(task2.Exception); + } + else + { + tcs2.SetResult(null); + } + }, tcs, cancellationToken); + return tcs.Task; + } +#endif + } +} \ No newline at end of file diff --git a/samples/System.IO.Pipelines.Samples/HttpServer/HttpServer.cs b/samples/System.IO.Pipelines.Samples/HttpServer/HttpServer.cs new file mode 100644 index 00000000000..be87e6715f9 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpServer/HttpServer.cs @@ -0,0 +1,175 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.IO.Pipelines.Networking.Libuv; +using System.IO.Pipelines.Networking.Windows.RIO; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http.Features; + +namespace System.IO.Pipelines.Samples.Http +{ + public class HttpServer : IServer + { + public IFeatureCollection Features { get; } = new FeatureCollection(); + + private Socket _listenSocket; + private RioTcpServer _rioTcpServer; + private UvTcpListener _uvTcpListener; + private UvThread _uvThread; + + public HttpServer() + { + Features.Set(new ServerAddressesFeature()); + } + + public void Start(IHttpApplication application) + { + var feature = Features.Get(); + var address = feature.Addresses.FirstOrDefault(); + IPAddress ip; + int port; + GetIp(address, out ip, out port); + Task.Run(() => StartAcceptingLibuvConnections(application, ip, port)); + //Task.Factory.StartNew(() => StartAcceptingRIOConnections(application, ip, port), TaskCreationOptions.LongRunning); + // Task.Factory.StartNew(() => StartAcceptingConnections(application, ip, port), TaskCreationOptions.LongRunning); + } + + private void StartAcceptingLibuvConnections(IHttpApplication application, IPAddress ip, int port) + { + _uvThread = new UvThread(); + _uvTcpListener = new UvTcpListener(_uvThread, new IPEndPoint(ip, port)); + _uvTcpListener.OnConnection(async connection => + { + await ProcessClient(application, connection); + }); + + _uvTcpListener.StartAsync(); + } + + private void StartAcceptingRIOConnections(IHttpApplication application, IPAddress ip, int port) + { + Thread.CurrentThread.Name = "RIO Accept Thread"; + var addressBytes = ip.GetAddressBytes(); + + try + { + _rioTcpServer = new RioTcpServer((ushort)port, addressBytes[0], addressBytes[1], addressBytes[2], addressBytes[3]); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + + while (true) + { + try + { + var connection = _rioTcpServer.Accept(); + var task = ProcessRIOConnection(application, connection); + } + catch (ObjectDisposedException) + { + break; + } + catch (Exception ex) + { + Console.WriteLine(ex); + break; + } + } + } + + private async void StartAcceptingConnections(IHttpApplication application, IPAddress ip, int port) + { + Thread.CurrentThread.Name = "Socket Accept Thread"; + _listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); + _listenSocket.Bind(new IPEndPoint(ip, port)); + _listenSocket.Listen(10); + + using (var factory = new PipelineFactory()) + { + while (true) + { + try + { + var clientSocket = await _listenSocket.AcceptAsync(); + clientSocket.NoDelay = true; + var task = ProcessConnection(application, factory, clientSocket); + } + catch (ObjectDisposedException) + { + break; + } + catch (Exception) + { + /* Ignored */ + } + } + } + } + + public void Dispose() + { + _rioTcpServer?.Stop(); + _listenSocket?.Dispose(); + _uvTcpListener?.Dispose(); + _uvThread?.Dispose(); + + _rioTcpServer = null; + _listenSocket = null; + _uvTcpListener = null; + _uvThread = null; + } + + private static void GetIp(string url, out IPAddress ip, out int port) + { + ip = null; + + var address = ServerAddress.FromUrl(url); + switch (address.Host) + { + case "localhost": + ip = IPAddress.Loopback; + break; + case "*": + ip = IPAddress.Any; + break; + default: + break; + } + ip = ip ?? IPAddress.Parse(address.Host); + port = address.Port; + } + + private static async Task ProcessRIOConnection(IHttpApplication application, RioTcpConnection connection) + { + using (connection) + { + await ProcessClient(application, connection); + } + } + + private static async Task ProcessConnection(IHttpApplication application, PipelineFactory pipelineFactory, Socket socket) + { + using (var ns = new NetworkStream(socket)) + { + using (var connection = pipelineFactory.CreateConnection(ns)) + { + await ProcessClient(application, connection); + } + } + } + + private static async Task ProcessClient(IHttpApplication application, IPipelineConnection pipelineConnection) + { + var connection = new HttpConnection(application, pipelineConnection.Input, pipelineConnection.Output); + + await connection.ProcessAllRequests(); + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/HttpServer/ReasonPhrases.cs b/samples/System.IO.Pipelines.Samples/HttpServer/ReasonPhrases.cs new file mode 100644 index 00000000000..8f810f6cd69 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpServer/ReasonPhrases.cs @@ -0,0 +1,184 @@ +using System.Globalization; +using System.Text; + +namespace System.IO.Pipelines.Samples.Http +{ + public static class ReasonPhrases + { + private static readonly byte[] _bytesStatus100 = Encoding.ASCII.GetBytes("100 Continue"); + private static readonly byte[] _bytesStatus101 = Encoding.ASCII.GetBytes("101 Switching Protocols"); + private static readonly byte[] _bytesStatus102 = Encoding.ASCII.GetBytes("102 Processing"); + private static readonly byte[] _bytesStatus200 = Encoding.ASCII.GetBytes("200 OK"); + private static readonly byte[] _bytesStatus201 = Encoding.ASCII.GetBytes("201 Created"); + private static readonly byte[] _bytesStatus202 = Encoding.ASCII.GetBytes("202 Accepted"); + private static readonly byte[] _bytesStatus203 = Encoding.ASCII.GetBytes("203 Non-Authoritative Information"); + private static readonly byte[] _bytesStatus204 = Encoding.ASCII.GetBytes("204 No Content"); + private static readonly byte[] _bytesStatus205 = Encoding.ASCII.GetBytes("205 Reset Content"); + private static readonly byte[] _bytesStatus206 = Encoding.ASCII.GetBytes("206 Partial Content"); + private static readonly byte[] _bytesStatus207 = Encoding.ASCII.GetBytes("207 Multi-Status"); + private static readonly byte[] _bytesStatus226 = Encoding.ASCII.GetBytes("226 IM Used"); + private static readonly byte[] _bytesStatus300 = Encoding.ASCII.GetBytes("300 Multiple Choices"); + private static readonly byte[] _bytesStatus301 = Encoding.ASCII.GetBytes("301 Moved Permanently"); + private static readonly byte[] _bytesStatus302 = Encoding.ASCII.GetBytes("302 Found"); + private static readonly byte[] _bytesStatus303 = Encoding.ASCII.GetBytes("303 See Other"); + private static readonly byte[] _bytesStatus304 = Encoding.ASCII.GetBytes("304 Not Modified"); + private static readonly byte[] _bytesStatus305 = Encoding.ASCII.GetBytes("305 Use Proxy"); + private static readonly byte[] _bytesStatus306 = Encoding.ASCII.GetBytes("306 Reserved"); + private static readonly byte[] _bytesStatus307 = Encoding.ASCII.GetBytes("307 Temporary Redirect"); + private static readonly byte[] _bytesStatus308 = Encoding.ASCII.GetBytes("308 Permanent Redirect"); + private static readonly byte[] _bytesStatus400 = Encoding.ASCII.GetBytes("400 Bad Request"); + private static readonly byte[] _bytesStatus401 = Encoding.ASCII.GetBytes("401 Unauthorized"); + private static readonly byte[] _bytesStatus402 = Encoding.ASCII.GetBytes("402 Payment Required"); + private static readonly byte[] _bytesStatus403 = Encoding.ASCII.GetBytes("403 Forbidden"); + private static readonly byte[] _bytesStatus404 = Encoding.ASCII.GetBytes("404 Not Found"); + private static readonly byte[] _bytesStatus405 = Encoding.ASCII.GetBytes("405 Method Not Allowed"); + private static readonly byte[] _bytesStatus406 = Encoding.ASCII.GetBytes("406 Not Acceptable"); + private static readonly byte[] _bytesStatus407 = Encoding.ASCII.GetBytes("407 Proxy Authentication Required"); + private static readonly byte[] _bytesStatus408 = Encoding.ASCII.GetBytes("408 Request Timeout"); + private static readonly byte[] _bytesStatus409 = Encoding.ASCII.GetBytes("409 Conflict"); + private static readonly byte[] _bytesStatus410 = Encoding.ASCII.GetBytes("410 Gone"); + private static readonly byte[] _bytesStatus411 = Encoding.ASCII.GetBytes("411 Length Required"); + private static readonly byte[] _bytesStatus412 = Encoding.ASCII.GetBytes("412 Precondition Failed"); + private static readonly byte[] _bytesStatus413 = Encoding.ASCII.GetBytes("413 Payload Too Large"); + private static readonly byte[] _bytesStatus414 = Encoding.ASCII.GetBytes("414 URI Too Long"); + private static readonly byte[] _bytesStatus415 = Encoding.ASCII.GetBytes("415 Unsupported Media Type"); + private static readonly byte[] _bytesStatus416 = Encoding.ASCII.GetBytes("416 Range Not Satisfiable"); + private static readonly byte[] _bytesStatus417 = Encoding.ASCII.GetBytes("417 Expectation Failed"); + private static readonly byte[] _bytesStatus418 = Encoding.ASCII.GetBytes("418 I'm a Teapot"); + private static readonly byte[] _bytesStatus422 = Encoding.ASCII.GetBytes("422 Unprocessable Entity"); + private static readonly byte[] _bytesStatus423 = Encoding.ASCII.GetBytes("423 Locked"); + private static readonly byte[] _bytesStatus424 = Encoding.ASCII.GetBytes("424 Failed Dependency"); + private static readonly byte[] _bytesStatus426 = Encoding.ASCII.GetBytes("426 Upgrade Required"); + private static readonly byte[] _bytesStatus451 = Encoding.ASCII.GetBytes("451 Unavailable For Legal Reasons"); + private static readonly byte[] _bytesStatus500 = Encoding.ASCII.GetBytes("500 Internal Server Error"); + private static readonly byte[] _bytesStatus501 = Encoding.ASCII.GetBytes("501 Not Implemented"); + private static readonly byte[] _bytesStatus502 = Encoding.ASCII.GetBytes("502 Bad Gateway"); + private static readonly byte[] _bytesStatus503 = Encoding.ASCII.GetBytes("503 Service Unavailable"); + private static readonly byte[] _bytesStatus504 = Encoding.ASCII.GetBytes("504 Gateway Timeout"); + private static readonly byte[] _bytesStatus505 = Encoding.ASCII.GetBytes("505 HTTP Version Not Supported"); + private static readonly byte[] _bytesStatus506 = Encoding.ASCII.GetBytes("506 Variant Also Negotiates"); + private static readonly byte[] _bytesStatus507 = Encoding.ASCII.GetBytes("507 Insufficient Storage"); + private static readonly byte[] _bytesStatus510 = Encoding.ASCII.GetBytes("510 Not Extended"); + + public static byte[] ToStatusBytes(int statusCode, string reasonPhrase = null) + { + if (string.IsNullOrEmpty(reasonPhrase)) + { + switch (statusCode) + { + case 100: + return _bytesStatus100; + case 101: + return _bytesStatus101; + case 102: + return _bytesStatus102; + case 200: + return _bytesStatus200; + case 201: + return _bytesStatus201; + case 202: + return _bytesStatus202; + case 203: + return _bytesStatus203; + case 204: + return _bytesStatus204; + case 205: + return _bytesStatus205; + case 206: + return _bytesStatus206; + case 207: + return _bytesStatus207; + case 226: + return _bytesStatus226; + case 300: + return _bytesStatus300; + case 301: + return _bytesStatus301; + case 302: + return _bytesStatus302; + case 303: + return _bytesStatus303; + case 304: + return _bytesStatus304; + case 305: + return _bytesStatus305; + case 306: + return _bytesStatus306; + case 307: + return _bytesStatus307; + case 308: + return _bytesStatus308; + case 400: + return _bytesStatus400; + case 401: + return _bytesStatus401; + case 402: + return _bytesStatus402; + case 403: + return _bytesStatus403; + case 404: + return _bytesStatus404; + case 405: + return _bytesStatus405; + case 406: + return _bytesStatus406; + case 407: + return _bytesStatus407; + case 408: + return _bytesStatus408; + case 409: + return _bytesStatus409; + case 410: + return _bytesStatus410; + case 411: + return _bytesStatus411; + case 412: + return _bytesStatus412; + case 413: + return _bytesStatus413; + case 414: + return _bytesStatus414; + case 415: + return _bytesStatus415; + case 416: + return _bytesStatus416; + case 417: + return _bytesStatus417; + case 418: + return _bytesStatus418; + case 422: + return _bytesStatus422; + case 423: + return _bytesStatus423; + case 424: + return _bytesStatus424; + case 426: + return _bytesStatus426; + case 451: + return _bytesStatus451; + case 500: + return _bytesStatus500; + case 501: + return _bytesStatus501; + case 502: + return _bytesStatus502; + case 503: + return _bytesStatus503; + case 504: + return _bytesStatus504; + case 505: + return _bytesStatus505; + case 506: + return _bytesStatus506; + case 507: + return _bytesStatus507; + case 510: + return _bytesStatus510; + default: + return Encoding.ASCII.GetBytes(statusCode.ToString(CultureInfo.InvariantCulture) + " Unknown"); + } + } + return Encoding.ASCII.GetBytes(statusCode.ToString(CultureInfo.InvariantCulture) + " " + reasonPhrase); + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/HttpServer/RequestHeaderDictionary.cs b/samples/System.IO.Pipelines.Samples/HttpServer/RequestHeaderDictionary.cs new file mode 100644 index 00000000000..e2c5fa7b07a --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpServer/RequestHeaderDictionary.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.IO.Pipelines.Text.Primitives; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace System.IO.Pipelines.Samples.Http +{ + public class RequestHeaderDictionary : IHeaderDictionary + { + private static readonly byte[] ContentLengthKeyBytes = Encoding.ASCII.GetBytes("CONTENT-LENGTH"); + private static readonly byte[] ContentTypeKeyBytes = Encoding.ASCII.GetBytes("CONTENT-TYPE"); + private static readonly byte[] AcceptBytes = Encoding.ASCII.GetBytes("ACCEPT"); + private static readonly byte[] AcceptLanguageBytes = Encoding.ASCII.GetBytes("ACCEPT-LANGUAGE"); + private static readonly byte[] AcceptEncodingBytes = Encoding.ASCII.GetBytes("ACCEPT-ENCODING"); + private static readonly byte[] HostBytes = Encoding.ASCII.GetBytes("HOST"); + private static readonly byte[] ConnectionBytes = Encoding.ASCII.GetBytes("CONNECTION"); + private static readonly byte[] CacheControlBytes = Encoding.ASCII.GetBytes("CACHE-CONTROL"); + private static readonly byte[] UserAgentBytes = Encoding.ASCII.GetBytes("USER-AGENT"); + private static readonly byte[] UpgradeInsecureRequests = Encoding.ASCII.GetBytes("UPGRADE-INSECURE-REQUESTS"); + + private Dictionary _headers = new Dictionary(10, StringComparer.OrdinalIgnoreCase); + + public StringValues this[string key] + { + get + { + StringValues values; + TryGetValue(key, out values); + return values; + } + + set + { + SetHeader(key, value); + } + } + + public int Count => _headers.Count; + + public bool IsReadOnly => false; + + public ICollection Keys => _headers.Keys; + + public ICollection Values => _headers.Values.Select(v => v.GetValue()).ToList(); + + public void SetHeader(ref ReadableBuffer key, ref ReadableBuffer value) + { + string headerKey = GetHeaderKey(ref key); + _headers[headerKey] = new HeaderValue + { + Raw = value.Preserve() + }; + } + + public ReadableBuffer GetHeaderRaw(string key) + { + HeaderValue value; + if (_headers.TryGetValue(key, out value)) + { + return value.Raw.Value.Buffer; + } + return default(ReadableBuffer); + } + + private string GetHeaderKey(ref ReadableBuffer key) + { + // Uppercase the things + foreach (var memory in key) + { + var data = memory.Span; + for (int i = 0; i < memory.Length; i++) + { + var mask = IsAlpha(data[i]) ? 0xdf : 0xff; + data[i] = (byte)(data[i] & mask); + } + } + + if (EqualsIgnoreCase(ref key, AcceptBytes)) + { + return "Accept"; + } + + if (EqualsIgnoreCase(ref key, AcceptEncodingBytes)) + { + return "Accept-Encoding"; + } + + if (EqualsIgnoreCase(ref key, AcceptLanguageBytes)) + { + return "Accept-Language"; + } + + if (EqualsIgnoreCase(ref key, HostBytes)) + { + return "Host"; + } + + if (EqualsIgnoreCase(ref key, UserAgentBytes)) + { + return "User-Agent"; + } + + if (EqualsIgnoreCase(ref key, CacheControlBytes)) + { + return "Cache-Control"; + } + + if (EqualsIgnoreCase(ref key, ConnectionBytes)) + { + return "Connection"; + } + + if (EqualsIgnoreCase(ref key, UpgradeInsecureRequests)) + { + return "Upgrade-Insecure-Requests"; + } + + return key.GetAsciiString(); + } + + private bool EqualsIgnoreCase(ref ReadableBuffer key, byte[] buffer) + { + if (key.Length != buffer.Length) + { + return false; + } + + return key.Equals(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsAlpha(byte b) + { + return b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z'; + } + + private void SetHeader(string key, StringValues value) + { + _headers[key] = new HeaderValue + { + Value = value + }; + } + + public void Add(KeyValuePair item) + { + SetHeader(item.Key, item.Value); + } + + public void Add(string key, StringValues value) + { + SetHeader(key, value); + } + + public void Clear() + { + _headers.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return false; + } + + public bool ContainsKey(string key) + { + return _headers.ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotSupportedException(); + } + + public void Reset() + { + foreach (var pair in _headers) + { + pair.Value.Raw?.Dispose(); + } + + _headers.Clear(); + } + + public IEnumerator> GetEnumerator() + { + return _headers.Select(h => new KeyValuePair(h.Key, h.Value.GetValue())).GetEnumerator(); + } + + public bool Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public bool Remove(string key) + { + return _headers.Remove(key); + } + + public bool TryGetValue(string key, out StringValues value) + { + HeaderValue headerValue; + if (_headers.TryGetValue(key, out headerValue)) + { + value = headerValue.GetValue(); + return true; + } + + return false; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private struct HeaderValue + { + public PreservedBuffer? Raw; + public StringValues? Value; + + public StringValues GetValue() + { + if (!Value.HasValue) + { + if (!Raw.HasValue) + { + return StringValues.Empty; + } + + Value = Raw.Value.Buffer.GetAsciiString(); + } + + return Value.Value; + } + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/HttpServer/ResponseHeaderDictionary.cs b/samples/System.IO.Pipelines.Samples/HttpServer/ResponseHeaderDictionary.cs new file mode 100644 index 00000000000..8cc84b0b884 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpServer/ResponseHeaderDictionary.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Text.Formatting; +using System.IO.Pipelines; +using System.IO.Pipelines.Text.Primitives; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; +using Microsoft.Extensions.Primitives; + +namespace System.IO.Pipelines.Samples.Http +{ + public class ResponseHeaderDictionary : IHeaderDictionary + { + private static readonly DateHeaderValueManager _dateHeaderValueManager = new DateHeaderValueManager(); + private static readonly byte[] _serverHeaderBytes = Encoding.UTF8.GetBytes("\r\nServer: Pipelines"); + private static readonly byte[] _chunkedHeaderBytes = Encoding.UTF8.GetBytes("\r\nTransfer-Encoding: chunked"); + + private static readonly byte[] _headersStartBytes = Encoding.UTF8.GetBytes("\r\n"); + private static readonly byte[] _headersSeperatorBytes = Encoding.UTF8.GetBytes(": "); + private static readonly byte[] _headersEndBytes = Encoding.UTF8.GetBytes("\r\n\r\n"); + + private readonly HeaderDictionary _headers = new HeaderDictionary(); + + public StringValues this[string key] + { + get + { + return _headers[key]; + } + + set + { + _headers[key] = value; + } + } + + public int Count => _headers.Count; + + public bool IsReadOnly => false; + + public ICollection Keys => _headers.Keys; + + public ICollection Values => _headers.Values; + + public void Add(KeyValuePair item) => _headers.Add(item); + + public void Add(string key, StringValues value) => _headers.Add(key, value); + + public void Clear() + { + _headers.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return _headers.Contains(item); + } + + public bool ContainsKey(string key) + { + return _headers.ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + _headers.CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() + { + return _headers.GetEnumerator(); + } + + public bool Remove(KeyValuePair item) + { + return _headers.Remove(item); + } + + public bool Remove(string key) + { + return _headers.Remove(key); + } + + public bool TryGetValue(string key, out StringValues value) + { + return _headers.TryGetValue(key, out value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void CopyTo(bool chunk, WritableBuffer buffer) + { + foreach (var header in _headers) + { + buffer.Write(_headersStartBytes); + buffer.WriteUtf8String(header.Key); + buffer.Write(_headersSeperatorBytes); + buffer.WriteUtf8String(header.Value.ToString()); + } + + if (chunk) + { + buffer.Write(_chunkedHeaderBytes); + } + + buffer.Write(_serverHeaderBytes); + var date = _dateHeaderValueManager.GetDateHeaderValues().Bytes; + buffer.Write(date); + + buffer.Write(_headersEndBytes); + } + + public void Reset() => _headers.Clear(); + } +} diff --git a/samples/System.IO.Pipelines.Samples/HttpServer/ServerAddress.cs b/samples/System.IO.Pipelines.Samples/HttpServer/ServerAddress.cs new file mode 100644 index 00000000000..5b9469f6f16 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/HttpServer/ServerAddress.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.Samples.Http +{ + public class ServerAddress + { + public string Host { get; private set; } + public string PathBase { get; private set; } + public int Port { get; internal set; } + public string Scheme { get; private set; } + + public override string ToString() + { + return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant() + ":" + Port.ToString(CultureInfo.InvariantCulture) + PathBase.ToLowerInvariant(); + } + + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + + public override bool Equals(object obj) + { + var other = obj as ServerAddress; + if (other == null) + { + return false; + } + return string.Equals(Scheme, other.Scheme, StringComparison.OrdinalIgnoreCase) + && string.Equals(Host, other.Host, StringComparison.OrdinalIgnoreCase) + && Port == other.Port + && string.Equals(PathBase, other.PathBase, StringComparison.OrdinalIgnoreCase); + } + + public static ServerAddress FromUrl(string url) + { + url = url ?? string.Empty; + + int schemeDelimiterStart = url.IndexOf("://", StringComparison.Ordinal); + if (schemeDelimiterStart < 0) + { + throw new FormatException($"Invalid URL: {url}"); + } + int schemeDelimiterEnd = schemeDelimiterStart + "://".Length; + + int pathDelimiterStart = url.IndexOf("/", schemeDelimiterEnd, StringComparison.Ordinal); + int pathDelimiterEnd = pathDelimiterStart; + + if (pathDelimiterStart < 0) + { + pathDelimiterStart = pathDelimiterEnd = url.Length; + } + + var serverAddress = new ServerAddress(); + serverAddress.Scheme = url.Substring(0, schemeDelimiterStart); + + var hasSpecifiedPort = false; + + int portDelimiterStart = url.LastIndexOf(":", pathDelimiterStart - 1, pathDelimiterStart - schemeDelimiterEnd, StringComparison.Ordinal); + if (portDelimiterStart >= 0) + { + int portDelimiterEnd = portDelimiterStart + ":".Length; + + string portString = url.Substring(portDelimiterEnd, pathDelimiterStart - portDelimiterEnd); + int portNumber; + if (int.TryParse(portString, NumberStyles.Integer, CultureInfo.InvariantCulture, out portNumber)) + { + hasSpecifiedPort = true; + serverAddress.Host = url.Substring(schemeDelimiterEnd, portDelimiterStart - schemeDelimiterEnd); + serverAddress.Port = portNumber; + } + } + + if (!hasSpecifiedPort) + { + if (string.Equals(serverAddress.Scheme, "http", StringComparison.OrdinalIgnoreCase)) + { + serverAddress.Port = 80; + } + else if (string.Equals(serverAddress.Scheme, "https", StringComparison.OrdinalIgnoreCase)) + { + serverAddress.Port = 443; + } + } + + + if (!hasSpecifiedPort) + { + serverAddress.Host = url.Substring(schemeDelimiterEnd, pathDelimiterStart - schemeDelimiterEnd); + } + + if (string.IsNullOrEmpty(serverAddress.Host)) + { + throw new FormatException($"Invalid URL: {url}"); + } + + // Path should not end with a / since it will be used as PathBase later + if (url[url.Length - 1] == '/') + { + serverAddress.PathBase = url.Substring(pathDelimiterEnd, url.Length - pathDelimiterEnd - 1); + } + else + { + serverAddress.PathBase = url.Substring(pathDelimiterEnd); + } + + return serverAddress; + } + + internal ServerAddress WithHost(string host) + { + return new ServerAddress + { + Scheme = Scheme, + Host = host, + Port = Port, + PathBase = PathBase + }; + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/LibuvHttpClientSample.cs b/samples/System.IO.Pipelines.Samples/LibuvHttpClientSample.cs new file mode 100644 index 00000000000..8393198bb6c --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/LibuvHttpClientSample.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.Samples +{ + public class LibuvHttpClientSample + { + public static async Task Run() + { + var client = new HttpClient(new LibuvHttpClientHandler()); + + while (true) + { + var response = await client.GetAsync("http://localhost:5000"); + + Console.WriteLine(response); + + Console.WriteLine(await response.Content.ReadAsStringAsync()); + + await Task.Delay(1000); + } + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/Models/Models.cs b/samples/System.IO.Pipelines.Samples/Models/Models.cs new file mode 100644 index 00000000000..1b9ec8ef7df --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/Models/Models.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.Samples.Models +{ + public static class BigModels + { + public static readonly Model About100Fields = new Model() + { + CreatedDate = DateTime.Now, + UpdateBy = Guid.NewGuid().ToString(), + UpdatedDate = DateTime.Now, + + Provider = new HealthCareProvider() + { + CreatedDate = DateTime.Now, + Name = Guid.NewGuid().ToString(), + UpdateBy = Guid.NewGuid().ToString(), + UpdatedDate = DateTime.Now, + + Services = new List() + { + new Service() + { + Code = "abcd", + CreatedDate = DateTime.Now, + Name = Guid.NewGuid().ToString(), + SubName = Guid.NewGuid().ToString(), + UpdateBy = Guid.NewGuid().ToString(), + UpdatedDate = DateTime.Now, + }, + new Service() + { + Code = "abcd", + CreatedDate = DateTime.Now, + Name = Guid.NewGuid().ToString(), + SubName = Guid.NewGuid().ToString(), + UpdateBy = Guid.NewGuid().ToString(), + UpdatedDate = DateTime.Now, + }, + new Service() + { + Code = "abcd", + CreatedDate = DateTime.Now, + Name = Guid.NewGuid().ToString(), + SubName = Guid.NewGuid().ToString(), + UpdateBy = Guid.NewGuid().ToString(), + UpdatedDate = DateTime.Now, + } + } + }, + Enrollment = new Enrollment() + { + CreatedDate = DateTime.Now, + UpdateBy = Guid.NewGuid().ToString(), + UpdatedDate = DateTime.Now, + + Client = new Client() + { + CreatedDate = DateTime.Now, + UpdateBy = Guid.NewGuid().ToString(), + UpdatedDate = DateTime.Now, + + HomeAddress = new Address() + { + City = "Seattle", + State = "WA", + Street1 = Guid.NewGuid().ToString(), + Zip = 98004, + }, + }, + Program = new Program() + { + CreatedDate = DateTime.Now, + UpdateBy = Guid.NewGuid().ToString(), + UpdatedDate = DateTime.Now, + + Services = new List() + { + new Service() + { + Code = "abcd", + CreatedDate = DateTime.Now, + Name = Guid.NewGuid().ToString(), + SubName = Guid.NewGuid().ToString(), + UpdateBy = Guid.NewGuid().ToString(), + UpdatedDate = DateTime.Now, + }, + new Service() + { + Code = "abcd", + CreatedDate = DateTime.Now, + Name = Guid.NewGuid().ToString(), + SubName = Guid.NewGuid().ToString(), + UpdateBy = Guid.NewGuid().ToString(), + UpdatedDate = DateTime.Now, + }, + new Service() + { + Code = "abcd", + CreatedDate = DateTime.Now, + Name = Guid.NewGuid().ToString(), + SubName = Guid.NewGuid().ToString(), + UpdateBy = Guid.NewGuid().ToString(), + UpdatedDate = DateTime.Now, + } + } + }, + }, + Service = new Service() + { + Code = "abcd", + CreatedDate = DateTime.Now, + Name = Guid.NewGuid().ToString(), + SubName = Guid.NewGuid().ToString(), + UpdateBy = Guid.NewGuid().ToString(), + UpdatedDate = DateTime.Now, + }, + }; + } + + public abstract class BaseEntity + { + public abstract TId Id { get; } + public DateTime CreatedDate { get; set; } + public DateTime UpdatedDate { get; set; } + public string UpdateBy { get; set; } + + public virtual bool IsTransient() + { + return Id.Equals(default(TId)); + } + } + + public class Model : BaseEntity + { + public override int Id => EnrollmentServiceId; + public int EnrollmentServiceId { get; set; } + public int EnrollmentId { get; set; } + public int ServiceId { get; set; } + public int ProviderId { get; set; } + public virtual Enrollment Enrollment { get; set; } + public virtual Service Service { get; set; } + public virtual HealthCareProvider Provider { get; set; } + } + + public class Enrollment : BaseEntity + { + public override int Id => EnrollmentId; + public int EnrollmentId { get; set; } + public int ClientId { get; set; } + public virtual User PrimaryCaseManager { get; set; } + public virtual User SecondaryCaseManager { get; set; } + public int ProgramId { get; set; } + public virtual Program Program { get; set; } + public virtual Client Client { get; set; } + public virtual ICollection EnrollmentServices { get; set; } + public virtual ICollection Documents { get; set; } + public virtual ICollection Notes { get; set; } + } + + public class Service : BaseEntity + { + public override int Id => ServiceId; + public int ServiceId { get; set; } + public int ProgramId { get; set; } + public string Code { get; set; } + public string Name { get; set; } + public string SubName { get; set; } + public ICollection Providers { get; set; } + public Program Program { get; set; } + } + + public class HealthCareProvider : BaseEntity + { + public override int Id => ProviderId; + public int ProviderId { get; set; } + public string Name { get; set; } + public virtual ICollection Services { get; set; } + } + + public class Client : BaseEntity + { + public override int Id => ClientId; + public int ClientId { get; set; } + public virtual Address HomeAddress { get; set; } + public virtual ICollection Docs { get; set; } + public virtual ICollection Notes { get; set; } + public virtual ICollection Enrollments { get; set; } + public virtual ICollection Contacts { get; set; } + public virtual ICollection CareSettings { get; set; } + } + + public class Program : BaseEntity + { + public override int Id => ProgramId; + public int ProgramId { get; set; } + public string Name { get; set; } + public virtual ICollection Enrollments { get; set; } + public virtual ICollection Services { get; set; } + } + + public class Note : BaseEntity + { + public override int Id => NoteId; + public int NoteId { get; set; } + public string Content { get; set; } + } + + public class Doc : BaseEntity + { + public override int Id => DocId; + public int DocId { get; set; } + public string Filename { get; set; } + } + + public class Contact : BaseEntity + { + public override int Id => ContactId; + public int ContactId { get; set; } + } + + public class User : BaseEntity + { + public override int Id => UserId; + public int UserId { get; set; } + } + + public class ClientCareSetting : BaseEntity + { + public override int Id => ClientCareSettingId; + public int ClientCareSettingId { get; set; } + public string Key { get; set; } + public string Value { get; set; } + } + + public class Address + { + public string Street1 { get; set; } + public string Street2 { get; set; } + public int Zip { get; set; } + public string City { get; set; } + public string State { get; set; } + } +} diff --git a/samples/System.IO.Pipelines.Samples/Models/Pet.cs b/samples/System.IO.Pipelines.Samples/Models/Pet.cs new file mode 100644 index 00000000000..9023b968c04 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/Models/Pet.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.Samples.Models +{ + public class Pet + { + public int Id { get; set; } + + public int Age { get; set; } + + public Category Category { get; set; } + + public bool HasVaccinations { get; set; } + + public string Name { get; set; } + + public List Images { get; set; } + + public List Tags { get; set; } + + public string Status { get; set; } + } + + public class Image + { + public int Id { get; set; } + + public string Url { get; set; } + } + + public class Tag + { + public int Id { get; set; } + + public string Name { get; set; } + } + + public class Category + { + public int Id { get; set; } + + public string Name { get; set; } + } +} diff --git a/samples/System.IO.Pipelines.Samples/Program.cs b/samples/System.IO.Pipelines.Samples/Program.cs new file mode 100644 index 00000000000..c0a8a53934a --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/Program.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.IO.Pipelines.Samples.Framing; +using System.IO.Pipelines.Text.Primitives; + +namespace System.IO.Pipelines.Samples +{ + public class Program + { + public static void Main(string[] args) + { + // AspNetHttpServerSample.Run(); + RawLibuvHttpServerSample.Run(); + // ProtocolHandling.Run(); + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/Properties/AssemblyInfo.cs b/samples/System.IO.Pipelines.Samples/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..7babd2bd7f8 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("System.IO.Pipelines.Samples")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3dcb0f92-51ae-493e-a1b6-5b42449ea2bd")] diff --git a/samples/System.IO.Pipelines.Samples/RawLibuvHttpClientSample.cs b/samples/System.IO.Pipelines.Samples/RawLibuvHttpClientSample.cs new file mode 100644 index 00000000000..b13c4558d60 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/RawLibuvHttpClientSample.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using System.IO.Pipelines.Networking.Libuv; +using System.IO.Pipelines.Text.Primitives; + +namespace System.IO.Pipelines.Samples +{ + public class RawLibuvHttpClientSample + { + public static async Task Run() + { + var thread = new UvThread(); + var client = new UvTcpClient(thread, new IPEndPoint(IPAddress.Loopback, 5000)); + + var consoleOutput = thread.PipelineFactory.CreateWriter(Console.OpenStandardOutput()); + + var connection = await client.ConnectAsync(); + + while (true) + { + var buffer = connection.Output.Alloc(); + + buffer.WriteAsciiString("GET / HTTP/1.1"); + buffer.WriteAsciiString("\r\n\r\n"); + + await buffer.FlushAsync(); + + // Write the client output to the console + await CopyCompletedAsync(connection.Input, consoleOutput); + + await Task.Delay(1000); + } + } + private static async Task CopyCompletedAsync(IPipelineReader input, IPipelineWriter output) + { + var result = await input.ReadAsync(); + var inputBuffer = result.Buffer; + + while (true) + { + try + { + if (inputBuffer.IsEmpty && result.IsCompleted) + { + return; + } + + var buffer = output.Alloc(); + + buffer.Append(inputBuffer); + + await buffer.FlushAsync(); + } + finally + { + input.Advance(inputBuffer.End); + } + + var awaiter = input.ReadAsync(); + + if (!awaiter.IsCompleted) + { + // No more data + break; + } + + result = await input.ReadAsync(); + inputBuffer = result.Buffer; + } + } + + } +} diff --git a/samples/System.IO.Pipelines.Samples/RawLibuvHttpServerSample.cs b/samples/System.IO.Pipelines.Samples/RawLibuvHttpServerSample.cs new file mode 100644 index 00000000000..abea9ce06b8 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/RawLibuvHttpServerSample.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.IO.Pipelines.Networking.Libuv; +using System.IO.Pipelines.Text.Primitives; + +namespace System.IO.Pipelines.Samples +{ + public class RawLibuvHttpServerSample + { + public static void Run() + { + var ip = IPAddress.Any; + int port = 5000; + var thread = new UvThread(); + var listener = new UvTcpListener(thread, new IPEndPoint(ip, port)); + listener.OnConnection(async connection => + { + var httpParser = new HttpRequestParser(); + + while (true) + { + // Wait for data + var result = await connection.Input.ReadAsync(); + var input = result.Buffer; + + try + { + if (input.IsEmpty && result.IsCompleted) + { + // No more data + break; + } + + // Parse the input http request + var parseResult = httpParser.ParseRequest(ref input); + + switch (parseResult) + { + case HttpRequestParser.ParseResult.Incomplete: + if (result.IsCompleted) + { + // Didn't get the whole request and the connection ended + throw new EndOfStreamException(); + } + // Need more data + continue; + case HttpRequestParser.ParseResult.Complete: + break; + case HttpRequestParser.ParseResult.BadRequest: + throw new Exception(); + default: + break; + } + + // Writing directly to pooled buffers + var output = connection.Output.Alloc(); + output.WriteUtf8String("HTTP/1.1 200 OK"); + output.WriteUtf8String("\r\nContent-Length: 13"); + output.WriteUtf8String("\r\nContent-Type: text/plain"); + output.WriteUtf8String("\r\n\r\n"); + output.WriteUtf8String("Hello, World!"); + await output.FlushAsync(); + + httpParser.Reset(); + } + finally + { + // Consume the input + connection.Input.Advance(input.Start, input.End); + } + } + }); + + listener.StartAsync().GetAwaiter().GetResult(); + + Console.WriteLine($"Listening on {ip} on port {port}"); + var wh = new ManualResetEventSlim(); + Console.CancelKeyPress += (sender, e) => + { + wh.Set(); + }; + + wh.Wait(); + + listener.Dispose(); + thread.Dispose(); + } + } +} diff --git a/samples/System.IO.Pipelines.Samples/System.IO.Pipelines.Samples.xproj b/samples/System.IO.Pipelines.Samples/System.IO.Pipelines.Samples.xproj new file mode 100644 index 00000000000..725e525ee80 --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/System.IO.Pipelines.Samples.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + a53a869f-d581-4499-ac49-13c2b75c2380 + System.IO.Pipelines.Samples + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/samples/System.IO.Pipelines.Samples/project.json b/samples/System.IO.Pipelines.Samples/project.json new file mode 100644 index 00000000000..0178c09d56c --- /dev/null +++ b/samples/System.IO.Pipelines.Samples/project.json @@ -0,0 +1,44 @@ +{ + "version": "1.0.0-*", + "buildOptions": { + "allowUnsafe": true, + "emitEntryPoint": true + }, + + "dependencies": { + "System.IO.Pipelines": { + "target": "project" + }, + "System.IO.Pipelines.Text.Primitives": { + "target": "project" + }, + "System.IO.Pipelines.Networking.Libuv": { + "target": "project" + }, + "System.IO.Pipelines.Networking.Windows.RIO": { + "target": "project" + }, + "System.IO.Pipelines.File": { + "target": "project" + }, + "System.IO.Pipelines.Compression": { + "target": "project" + }, + "Newtonsoft.Json": "9.0.1", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0" + } + }, + + "frameworks": { + "netcoreapp1.0": { } + }, + + "runtimeOptions": { + "configProperties": { + "System.GC.Server": true + } + } +} diff --git a/src/System.IO.Pipelines.Compression/CompressionPipelineExtensions.cs b/src/System.IO.Pipelines.Compression/CompressionPipelineExtensions.cs new file mode 100644 index 00000000000..f1309aa1f10 --- /dev/null +++ b/src/System.IO.Pipelines.Compression/CompressionPipelineExtensions.cs @@ -0,0 +1,272 @@ +using System; +using System.IO.Compression; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.Compression +{ + public static class CompressionPipelineExtensions + { + public static IPipelineReader DeflateDecompress(this IPipelineReader reader, PipelineFactory factory) + { + var inflater = new ReadableDeflateTransform(ZLibNative.Deflate_DefaultWindowBits); + return factory.CreateReader(reader, inflater.Execute); + } + + public static IPipelineReader DeflateCompress(this IPipelineReader reader, PipelineFactory factory, CompressionLevel compressionLevel) + { + var deflater = new WritableDeflateTransform(compressionLevel, ZLibNative.Deflate_DefaultWindowBits); + return factory.CreateReader(reader, deflater.Execute); + } + + public static IPipelineReader GZipDecompress(this IPipelineReader reader, PipelineFactory factory) + { + var inflater = new ReadableDeflateTransform(ZLibNative.GZip_DefaultWindowBits); + return factory.CreateReader(reader, inflater.Execute); + } + + public static IPipelineWriter GZipCompress(this IPipelineWriter writer, PipelineFactory factory, CompressionLevel compressionLevel) + { + var deflater = new WritableDeflateTransform(compressionLevel, ZLibNative.GZip_DefaultWindowBits); + return factory.CreateWriter(writer, deflater.Execute); + } + + public static IPipelineReader GZipCompress(this IPipelineReader reader, PipelineFactory factory, CompressionLevel compressionLevel) + { + var deflater = new WritableDeflateTransform(compressionLevel, ZLibNative.GZip_DefaultWindowBits); + return factory.CreateReader(reader, deflater.Execute); + } + + public static IPipelineReader CreateDeflateDecompressReader(this PipelineFactory factory, IPipelineReader reader) + { + var inflater = new ReadableDeflateTransform(ZLibNative.Deflate_DefaultWindowBits); + return factory.CreateReader(reader, inflater.Execute); + } + + public static IPipelineReader CreateDeflateCompressReader(this PipelineFactory factory, IPipelineReader reader, CompressionLevel compressionLevel) + { + var deflater = new WritableDeflateTransform(compressionLevel, ZLibNative.Deflate_DefaultWindowBits); + return factory.CreateReader(reader, deflater.Execute); + } + + public static IPipelineReader CreateGZipDecompressReader(this PipelineFactory factory, IPipelineReader reader) + { + var inflater = new ReadableDeflateTransform(ZLibNative.GZip_DefaultWindowBits); + return factory.CreateReader(reader, inflater.Execute); + } + + public static IPipelineWriter CreateGZipCompressWriter(this PipelineFactory factory, IPipelineWriter writer, CompressionLevel compressionLevel) + { + var deflater = new WritableDeflateTransform(compressionLevel, ZLibNative.GZip_DefaultWindowBits); + return factory.CreateWriter(writer, deflater.Execute); + } + + public static IPipelineReader CreateGZipCompressReader(this PipelineFactory factory, IPipelineReader reader, CompressionLevel compressionLevel) + { + var deflater = new WritableDeflateTransform(compressionLevel, ZLibNative.GZip_DefaultWindowBits); + return factory.CreateReader(reader, deflater.Execute); + } + + private class WritableDeflateTransform + { + private readonly Deflater _deflater; + + public WritableDeflateTransform(CompressionLevel compressionLevel, int bits) + { + _deflater = new Deflater(compressionLevel, bits); + } + + public async Task Execute(IPipelineReader reader, IPipelineWriter writer) + { + while (true) + { + var result = await reader.ReadAsync(); + var inputBuffer = result.Buffer; + + if (inputBuffer.IsEmpty) + { + if (result.IsCompleted) + { + break; + } + + reader.Advance(inputBuffer.End); + continue; + } + + var writerBuffer = writer.Alloc(); + var memory = inputBuffer.First; + + unsafe + { + // TODO: Pin pointer if not pinned + void* inPointer; + if (memory.TryGetPointer(out inPointer)) + { + _deflater.SetInput((IntPtr)inPointer, memory.Length); + } + else + { + throw new InvalidOperationException("Pointer needs to be pinned"); + } + } + + while (!_deflater.NeedsInput()) + { + unsafe + { + void* outPointer; + writerBuffer.Ensure(); + if (writerBuffer.Memory.TryGetPointer(out outPointer)) + { + int written = _deflater.ReadDeflateOutput((IntPtr)outPointer, writerBuffer.Memory.Length); + writerBuffer.Advance(written); + } + else + { + throw new InvalidOperationException("Pointer needs to be pinned"); + } + } + } + + var consumed = memory.Length - _deflater.AvailableInput; + + inputBuffer = inputBuffer.Slice(0, consumed); + + reader.Advance(inputBuffer.End); + + await writerBuffer.FlushAsync(); + } + + bool flushed = false; + do + { + // Need to do more stuff here + var writerBuffer = writer.Alloc(); + + unsafe + { + void* pointer; + writerBuffer.Ensure(); + var memory = writerBuffer.Memory; + if (memory.TryGetPointer(out pointer)) + { + int compressedBytes; + flushed = _deflater.Flush((IntPtr)pointer, memory.Length, out compressedBytes); + writerBuffer.Advance(compressedBytes); + } + else + { + throw new InvalidOperationException("Pointer needs to be pinned"); + } + } + + await writerBuffer.FlushAsync(); + } + while (flushed); + + bool finished = false; + do + { + // Need to do more stuff here + var writerBuffer = writer.Alloc(); + + unsafe + { + void* pointer; + writerBuffer.Ensure(); + var memory = writerBuffer.Memory; + if (memory.TryGetPointer(out pointer)) + { + int compressedBytes; + finished = _deflater.Finish((IntPtr)pointer, memory.Length, out compressedBytes); + writerBuffer.Advance(compressedBytes); + } + } + + await writerBuffer.FlushAsync(); + } + while (!finished); + + reader.Complete(); + + writer.Complete(); + + _deflater.Dispose(); + } + } + + private class ReadableDeflateTransform + { + private readonly Inflater _inflater; + + public ReadableDeflateTransform(int bits) + { + _inflater = new Inflater(bits); + } + + public async Task Execute(IPipelineReader reader, IPipelineWriter writer) + { + while (true) + { + var result = await reader.ReadAsync(); + var inputBuffer = result.Buffer; + + if (inputBuffer.IsEmpty) + { + if (result.IsCompleted) + { + break; + } + + reader.Advance(inputBuffer.End); + continue; + } + + var writerBuffer = writer.Alloc(); + var memory = inputBuffer.First; + if (memory.Length > 0) + { + unsafe + { + void* pointer; + if (memory.TryGetPointer(out pointer)) + { + _inflater.SetInput((IntPtr)pointer, memory.Length); + + void* writePointer; + writerBuffer.Ensure(); + if (writerBuffer.Memory.TryGetPointer(out writePointer)) + { + int written = _inflater.Inflate((IntPtr)writePointer, writerBuffer.Memory.Length); + writerBuffer.Advance(written); + } + else + { + throw new InvalidOperationException("Pointer needs to be pinned"); + } + } + else + { + throw new InvalidOperationException("Pointer needs to be pinned"); + } + + var consumed = memory.Length - _inflater.AvailableInput; + + inputBuffer = inputBuffer.Slice(0, consumed); + } + } + + reader.Advance(inputBuffer.End); + + await writerBuffer.FlushAsync(); + } + + reader.Complete(); + + writer.Complete(); + + _inflater.Dispose(); + } + } + } +} diff --git a/src/System.IO.Pipelines.Compression/Deflater.cs b/src/System.IO.Pipelines.Compression/Deflater.cs new file mode 100644 index 00000000000..d3cbd8f1071 --- /dev/null +++ b/src/System.IO.Pipelines.Compression/Deflater.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.IO.Compression; +using System.Security; +using ZErrorCode = System.IO.Pipelines.Compression.ZLibNative.ErrorCode; +using ZFlushCode = System.IO.Pipelines.Compression.ZLibNative.FlushCode; + +namespace System.IO.Pipelines.Compression +{ + /// + /// Provides a wrapper around the ZLib compression API + /// + internal sealed class Deflater : IDisposable + { + private ZLibNative.ZLibStreamHandle _zlibStream; + private bool _isDisposed; + private const int minWindowBits = -15; // WindowBits must be between -8..-15 to write no header, 8..15 for a + private const int maxWindowBits = 31; // zlib header, or 24..31 for a GZip header + + // Note, DeflateStream or the deflater do not try to be thread safe. + // The lock is just used to make writing to unmanaged structures atomic to make sure + // that they do not get inconsistent fields that may lead to an unmanaged memory violation. + // To prevent *managed* buffer corruption or other weird behaviour users need to synchronise + // on the stream explicitly. + private readonly object _syncLock = new object(); + + public int AvailableInput => (int)_zlibStream.AvailIn; + + #region exposed members + + internal Deflater(CompressionLevel compressionLevel, int windowBits) + { + Debug.Assert(windowBits >= minWindowBits && windowBits <= maxWindowBits); + ZLibNative.CompressionLevel zlibCompressionLevel; + int memLevel; + + switch (compressionLevel) + { + // See the note in ZLibNative.CompressionLevel for the recommended combinations. + + case CompressionLevel.Optimal: + zlibCompressionLevel = ZLibNative.CompressionLevel.DefaultCompression; + memLevel = ZLibNative.Deflate_DefaultMemLevel; + break; + + case CompressionLevel.Fastest: + zlibCompressionLevel = ZLibNative.CompressionLevel.BestSpeed; + memLevel = ZLibNative.Deflate_DefaultMemLevel; + break; + + case CompressionLevel.NoCompression: + zlibCompressionLevel = ZLibNative.CompressionLevel.NoCompression; + memLevel = ZLibNative.Deflate_NoCompressionMemLevel; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(compressionLevel)); + } + + ZLibNative.CompressionStrategy strategy = ZLibNative.CompressionStrategy.DefaultStrategy; + + DeflateInit(zlibCompressionLevel, windowBits, memLevel, strategy); + } + + ~Deflater() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + [SecuritySafeCritical] + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _zlibStream.Dispose(); + + _isDisposed = true; + } + } + + public bool NeedsInput() + { + return 0 == _zlibStream.AvailIn; + } + + internal void SetInput(IntPtr buffer, int count) + { + Debug.Assert(NeedsInput(), "We have something left in previous input!"); + + if (0 == count) + return; + + lock (_syncLock) + { + _zlibStream.NextIn = buffer; + _zlibStream.AvailIn = (uint)count; + } + } + + public int ReadDeflateOutput(IntPtr buffer, int count) + { + Contract.Ensures(Contract.Result() >= 0 && Contract.Result() <= count); + + Debug.Assert(!NeedsInput(), "GetDeflateOutput should only be called after providing input"); + + try + { + int bytesRead; + ReadDeflateOutput(buffer, count, ZFlushCode.NoFlush, out bytesRead); + return bytesRead; + } + finally + { + // Before returning, make sure to release input buffer if necessary: + if (0 == _zlibStream.AvailIn) + DeallocateInputBufferHandle(); + } + } + + private unsafe ZErrorCode ReadDeflateOutput(IntPtr buffer, int count, ZFlushCode flushCode, out int bytesRead) + { + lock (_syncLock) + { + _zlibStream.NextOut = buffer; + _zlibStream.AvailOut = (uint)count; + + ZErrorCode errC = Deflate(flushCode); + bytesRead = count - (int)_zlibStream.AvailOut; + + return errC; + } + } + + internal bool Finish(IntPtr buffer, int count, out int bytesRead) + { + Debug.Assert(NeedsInput(), "We have something left in previous input!"); + + // Note: we require that NeedsInput() == true, i.e. that 0 == _zlibStream.AvailIn. + // If there is still input left we should never be getting here; instead we + // should be calling GetDeflateOutput. + + ZErrorCode errC = ReadDeflateOutput(buffer, count, ZFlushCode.Finish, out bytesRead); + return errC == ZErrorCode.StreamEnd; + } + + /// + /// Returns true if there was something to flush. Otherwise False. + /// + internal bool Flush(IntPtr buffer, int count, out int bytesRead) + { + Debug.Assert(NeedsInput(), "We have something left in previous input!"); + + // Note: we require that NeedsInput() == true, i.e. that 0 == _zlibStream.AvailIn. + // If there is still input left we should never be getting here; instead we + // should be calling GetDeflateOutput. + + return ReadDeflateOutput(buffer, count, ZFlushCode.SyncFlush, out bytesRead) == ZErrorCode.Ok; + } + + #endregion + + + #region helpers & native call wrappers + + private void DeallocateInputBufferHandle() + { + lock (_syncLock) + { + _zlibStream.AvailIn = 0; + _zlibStream.NextIn = ZLibNative.ZNullPtr; + } + } + + [SecuritySafeCritical] + private void DeflateInit(ZLibNative.CompressionLevel compressionLevel, int windowBits, int memLevel, + ZLibNative.CompressionStrategy strategy) + { + ZErrorCode errC; + try + { + errC = ZLibNative.CreateZLibStreamForDeflate(out _zlibStream, compressionLevel, + windowBits, memLevel, strategy); + } + catch (Exception cause) + { + throw new ZLibException("SR.ZLibErrorDLLLoadError", cause); + } + + switch (errC) + { + case ZErrorCode.Ok: + return; + + case ZErrorCode.MemError: + throw new ZLibException("SR.ZLibErrorNotEnoughMemory", "deflateInit2_", (int)errC, _zlibStream.GetErrorMessage()); + + case ZErrorCode.VersionError: + throw new ZLibException("SR.ZLibErrorVersionMismatch", "deflateInit2_", (int)errC, _zlibStream.GetErrorMessage()); + + case ZErrorCode.StreamError: + throw new ZLibException("SR.ZLibErrorIncorrectInitParameters", "deflateInit2_", (int)errC, _zlibStream.GetErrorMessage()); + + default: + throw new ZLibException("SR.ZLibErrorUnexpected", "deflateInit2_", (int)errC, _zlibStream.GetErrorMessage()); + } + } + + + [SecuritySafeCritical] + private ZErrorCode Deflate(ZFlushCode flushCode) + { + ZErrorCode errC; + try + { + errC = _zlibStream.Deflate(flushCode); + } + catch (Exception cause) + { + throw new ZLibException("SR.ZLibErrorDLLLoadError", cause); + } + + switch (errC) + { + case ZErrorCode.Ok: + case ZErrorCode.StreamEnd: + return errC; + + case ZErrorCode.BufError: + return errC; // This is a recoverable error + + case ZErrorCode.StreamError: + throw new ZLibException("SR.ZLibErrorInconsistentStream", "deflate", (int)errC, _zlibStream.GetErrorMessage()); + + default: + throw new ZLibException("SR.ZLibErrorUnexpected", "deflate", (int)errC, _zlibStream.GetErrorMessage()); + } + } + #endregion + + } +} diff --git a/src/System.IO.Pipelines.Compression/Inflater.cs b/src/System.IO.Pipelines.Compression/Inflater.cs new file mode 100644 index 00000000000..e6945ec44eb --- /dev/null +++ b/src/System.IO.Pipelines.Compression/Inflater.cs @@ -0,0 +1,225 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Security; + +namespace System.IO.Pipelines.Compression +{ + /// + /// Provides a wrapper around the ZLib decompression API + /// + internal sealed class Inflater : IDisposable + { + private bool _finished; // Whether the end of the stream has been reached + private bool _isDisposed; // Prevents multiple disposals + private ZLibNative.ZLibStreamHandle _zlibStream; // The handle to the primary underlying zlib stream + private readonly object _syncLock = new object(); // Used to make writing to unmanaged structures atomic + private const int minWindowBits = -15; // WindowBits must be between -8..-15 to ignore the header, 8..15 for + private const int maxWindowBits = 47; // zlib headers, 24..31 for GZip headers, or 40..47 for either Zlib or GZip + + #region Exposed Members + + /// + /// Initialized the Inflater with the given windowBits size + /// + internal Inflater(int windowBits) + { + Debug.Assert(windowBits >= minWindowBits && windowBits <= maxWindowBits); + _finished = false; + _isDisposed = false; + InflateInit(windowBits); + } + + public int AvailableOutput => (int)_zlibStream.AvailOut; + + public int AvailableInput => (int)_zlibStream.AvailIn; + + /// + /// Returns true if the end of the stream has been reached. + /// + public bool Finished() + { + return _finished && _zlibStream.AvailIn == 0 && _zlibStream.AvailOut == 0; + } + + public unsafe int Inflate(IntPtr buffer, int count) + { + // If Inflate is called on an invalid or unready inflater, return 0 to indicate no bytes have been read. + if (NeedsInput()) + { + return 0; + } + + try + { + int bytesRead; + if (ReadInflateOutput(buffer, count, ZLibNative.FlushCode.NoFlush, out bytesRead) == ZLibNative.ErrorCode.StreamEnd) + { + _finished = true; + } + return bytesRead; + } + finally + { + // Before returning, make sure to release input buffer if necessary: + if (_zlibStream.AvailIn == 0) + { + DeallocateInputBufferHandle(); + } + } + } + + public bool NeedsInput() + { + return _zlibStream.AvailIn == 0; + } + + public void SetInput(IntPtr buffer, int count) + { + lock (_syncLock) + { + _zlibStream.NextIn = buffer; + _zlibStream.AvailIn = (uint)count; + _finished = false; + } + } + + [SecuritySafeCritical] + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _zlibStream.Dispose(); + } + + _isDisposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~Inflater() + { + Dispose(false); + } + + #endregion + + #region Helper Methods + + /// + /// Creates the ZStream that will handle inflation + /// + [SecuritySafeCritical] + private void InflateInit(int windowBits) + { + ZLibNative.ErrorCode error; + try + { + error = ZLibNative.CreateZLibStreamForInflate(out _zlibStream, windowBits); + } + catch (Exception exception) // could not load the ZLib dll + { + throw new ZLibException("SR.ZLibErrorDLLLoadError", exception); + } + + switch (error) + { + case ZLibNative.ErrorCode.Ok: // Successful initialization + return; + + case ZLibNative.ErrorCode.MemError: // Not enough memory + throw new ZLibException("SR.ZLibErrorNotEnoughMemory", "inflateInit2_", (int)error, _zlibStream.GetErrorMessage()); + + case ZLibNative.ErrorCode.VersionError: //zlib library is incompatible with the version assumed + throw new ZLibException("SR.ZLibErrorVersionMismatch", "inflateInit2_", (int)error, _zlibStream.GetErrorMessage()); + + case ZLibNative.ErrorCode.StreamError: // Parameters are invalid + throw new ZLibException("SR.ZLibErrorIncorrectInitParameters", "inflateInit2_", (int)error, _zlibStream.GetErrorMessage()); + + default: + throw new ZLibException("SR.ZLibErrorUnexpected", "inflateInit2_", (int)error, _zlibStream.GetErrorMessage()); + } + } + + /// + /// Wrapper around the ZLib inflate function, configuring the stream appropriately. + /// + private unsafe ZLibNative.ErrorCode ReadInflateOutput(IntPtr bufPtr, int length, ZLibNative.FlushCode flushCode, out int bytesRead) + { + lock (_syncLock) + { + _zlibStream.NextOut = bufPtr; + _zlibStream.AvailOut = (uint)length; + + ZLibNative.ErrorCode errC = Inflate(flushCode); + bytesRead = length - (int)_zlibStream.AvailOut; + + return errC; + } + } + + /// + /// Wrapper around the ZLib inflate function + /// + [SecuritySafeCritical] + private ZLibNative.ErrorCode Inflate(ZLibNative.FlushCode flushCode) + { + ZLibNative.ErrorCode errC; + try + { + errC = _zlibStream.Inflate(flushCode); + } + catch (Exception cause) // could not load the Zlib DLL correctly + { + throw new ZLibException("SR.ZLibErrorDLLLoadError", cause); + } + switch (errC) + { + case ZLibNative.ErrorCode.Ok: // progress has been made inflating + case ZLibNative.ErrorCode.StreamEnd: // The end of the input stream has been reached + return errC; + + case ZLibNative.ErrorCode.BufError: // No room in the output buffer - inflate() can be called again with more space to continue + return errC; + + case ZLibNative.ErrorCode.MemError: // Not enough memory to complete the operation + throw new ZLibException("SR.ZLibErrorNotEnoughMemory", "inflate_", (int)errC, _zlibStream.GetErrorMessage()); + + case ZLibNative.ErrorCode.DataError: // The input data was corrupted (input stream not conforming to the zlib format or incorrect check value) + throw new InvalidDataException("SR.UnsupportedCompression"); + + case ZLibNative.ErrorCode.StreamError: //the stream structure was inconsistent (for example if next_in or next_out was NULL), + throw new ZLibException("SR.ZLibErrorInconsistentStream", "inflate_", (int)errC, _zlibStream.GetErrorMessage()); + + default: + throw new ZLibException("SR.ZLibErrorUnexpected", "inflate_", (int)errC, _zlibStream.GetErrorMessage()); + } + } + + /// + /// Frees the GCHandle being used to store the input buffer + /// + private void DeallocateInputBufferHandle() + { + lock (_syncLock) + { + _zlibStream.AvailIn = 0; + _zlibStream.NextIn = ZLibNative.ZNullPtr; + } + } + + #endregion + } +} diff --git a/src/System.IO.Pipelines.Compression/Interop/Interop.zlib.Unix.cs b/src/System.IO.Pipelines.Compression/Interop/Interop.zlib.Unix.cs new file mode 100644 index 00000000000..3fc34f01a93 --- /dev/null +++ b/src/System.IO.Pipelines.Compression/Interop/Interop.zlib.Unix.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO.Compression; +using System.Runtime.InteropServices; + +#if UNIX +internal static partial class Interop +{ + internal static partial class zlib + { + [DllImport(Libraries.CompressionNative, EntryPoint = "CompressionNative_DeflateInit2_")] + internal static extern ZLibNative.ErrorCode DeflateInit2_( + ref ZLibNative.ZStream stream, + ZLibNative.CompressionLevel level, + ZLibNative.CompressionMethod method, + int windowBits, + int memLevel, + ZLibNative.CompressionStrategy strategy); + + [DllImport(Libraries.CompressionNative, EntryPoint = "CompressionNative_Deflate")] + internal static extern ZLibNative.ErrorCode Deflate(ref ZLibNative.ZStream stream, ZLibNative.FlushCode flush); + + [DllImport(Libraries.CompressionNative, EntryPoint = "CompressionNative_DeflateEnd")] + internal static extern ZLibNative.ErrorCode DeflateEnd(ref ZLibNative.ZStream stream); + + [DllImport(Libraries.CompressionNative, EntryPoint = "CompressionNative_InflateInit2_")] + internal static extern ZLibNative.ErrorCode InflateInit2_(ref ZLibNative.ZStream stream, int windowBits); + + [DllImport(Libraries.CompressionNative, EntryPoint = "CompressionNative_Inflate")] + internal static extern ZLibNative.ErrorCode Inflate(ref ZLibNative.ZStream stream, ZLibNative.FlushCode flush); + + [DllImport(Libraries.CompressionNative, EntryPoint = "CompressionNative_InflateEnd")] + internal static extern ZLibNative.ErrorCode InflateEnd(ref ZLibNative.ZStream stream); + + internal static unsafe uint crc32(uint crc, byte[] buffer, int offset, int len) + { + fixed (byte* buf = &buffer[offset]) + return Crc32(crc, buf, len); + } + + [DllImport(Libraries.CompressionNative, EntryPoint = "CompressionNative_Crc32")] + private static unsafe extern uint Crc32(uint crc, byte* buffer, int len); + } +} +#endif \ No newline at end of file diff --git a/src/System.IO.Pipelines.Compression/Interop/Interop.zlib.Windows.cs b/src/System.IO.Pipelines.Compression/Interop/Interop.zlib.Windows.cs new file mode 100644 index 00000000000..bfa6178301b --- /dev/null +++ b/src/System.IO.Pipelines.Compression/Interop/Interop.zlib.Windows.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Compression +{ + internal static partial class Interop + { + internal static class Libraries + { + internal const string Zlib = "clrcompression.dll"; + internal const string CompressionNative = "System.IO.Compression.Native"; + } + + internal static partial class zlib + { + /* + private const string ZLibVersion = "1.2.3"; + */ + + private static readonly byte[] ZLibVersion = new byte[] + {(byte) '1', (byte) '.', (byte) '2', (byte) '.', (byte) '3', 0}; + + [DllImport(Libraries.Zlib)] + private extern unsafe static int deflateInit2_(byte* stream, int level, int method, int windowBits, + int memLevel, int strategy, + byte* version, int stream_size); + + [DllImport(Libraries.Zlib)] + private extern unsafe static int deflate(byte* stream, int flush); + + [DllImport(Libraries.Zlib)] + private extern unsafe static int deflateEnd(byte* strm); + + [DllImport(Libraries.Zlib)] + internal extern unsafe static uint crc32(uint crc, byte* buffer, int len); + + [DllImport(Libraries.Zlib)] + private extern unsafe static int inflateInit2_(byte* stream, int windowBits, byte* version, int stream_size); + + [DllImport(Libraries.Zlib)] + private extern unsafe static int inflate(byte* stream, int flush); + + [DllImport(Libraries.Zlib)] + private extern unsafe static int inflateEnd(byte* stream); + + internal static unsafe ZLibNative.ErrorCode DeflateInit2_( + ref ZLibNative.ZStream stream, + ZLibNative.CompressionLevel level, + ZLibNative.CompressionMethod method, + int windowBits, + int memLevel, + ZLibNative.CompressionStrategy strategy) + { + fixed (byte* versionString = ZLibVersion) + fixed (ZLibNative.ZStream* streamBytes = &stream) + { + byte* pBytes = (byte*) streamBytes; + return + (ZLibNative.ErrorCode) + deflateInit2_(pBytes, (int) level, (int) method, (int) windowBits, (int) memLevel, + (int) strategy, versionString, sizeof(ZLibNative.ZStream)); + } + } + + internal static unsafe uint crc32(uint crc, byte[] buffer, int offset, int len) + { + fixed (byte* buf = &buffer[offset]) + return crc32(crc, buf, len); + } + + internal static unsafe ZLibNative.ErrorCode Deflate(ref ZLibNative.ZStream stream, + ZLibNative.FlushCode flush) + { + fixed (ZLibNative.ZStream* streamBytes = &stream) + { + byte* pBytes = (byte*) streamBytes; + return (ZLibNative.ErrorCode) deflate(pBytes, (int) flush); + } + } + + internal static unsafe ZLibNative.ErrorCode DeflateEnd(ref ZLibNative.ZStream stream) + { + fixed (ZLibNative.ZStream* streamBytes = &stream) + { + byte* pBytes = (byte*) streamBytes; + return (ZLibNative.ErrorCode) deflateEnd(pBytes); + } + } + + internal static unsafe ZLibNative.ErrorCode InflateInit2_( + ref ZLibNative.ZStream stream, + int windowBits) + { + fixed (byte* versionString = ZLibVersion) + fixed (ZLibNative.ZStream* streamBytes = &stream) + { + byte* pBytes = (byte*) streamBytes; + return + (ZLibNative.ErrorCode) + inflateInit2_(pBytes, (int) windowBits, versionString, sizeof(ZLibNative.ZStream)); + } + } + + internal static unsafe ZLibNative.ErrorCode Inflate(ref ZLibNative.ZStream stream, + ZLibNative.FlushCode flush) + { + fixed (ZLibNative.ZStream* streamBytes = &stream) + { + byte* pBytes = (byte*) streamBytes; + return (ZLibNative.ErrorCode) inflate(pBytes, (int) flush); + } + } + + internal static unsafe ZLibNative.ErrorCode InflateEnd(ref ZLibNative.ZStream stream) + { + fixed (ZLibNative.ZStream* streamBytes = &stream) + { + byte* pBytes = (byte*) streamBytes; + return (ZLibNative.ErrorCode) inflateEnd(pBytes); + } + } + } + } +} diff --git a/src/System.IO.Pipelines.Compression/System.IO.Pipelines.Compression.xproj b/src/System.IO.Pipelines.Compression/System.IO.Pipelines.Compression.xproj new file mode 100644 index 00000000000..416a114474c --- /dev/null +++ b/src/System.IO.Pipelines.Compression/System.IO.Pipelines.Compression.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 2f02c533-538e-4f0d-a5de-d029d99a49bd + System.IO.Pipelines.Compression + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/System.IO.Pipelines.Compression/ZLibException.cs b/src/System.IO.Pipelines.Compression/ZLibException.cs new file mode 100644 index 00000000000..3faa561b74e --- /dev/null +++ b/src/System.IO.Pipelines.Compression/ZLibException.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Security; +using ZErrorCode = System.IO.Pipelines.Compression.ZLibNative.ErrorCode; + +namespace System.IO.Pipelines.Compression +{ + /// + /// This is the exception that is thrown when a ZLib returns an error code indicating an unrecoverable error. + /// + internal class ZLibException : IOException + { + private string _zlibErrorContext = null; + private string _zlibErrorMessage = null; + private ZErrorCode _zlibErrorCode = ZErrorCode.Ok; + + + + /// + /// This is the preferred constructor to use. + /// The other constructors are provided for compliance to Fx design guidelines. + /// + /// A (localised) human readable error description. + /// A description of the context within zlib where the error occurred (e.g. the function name). + /// The error code returned by a ZLib function that caused this exception. + /// The string provided by ZLib as error information (unlocalised). + public ZLibException(string message, string zlibErrorContext, int zlibErrorCode, string zlibErrorMessage) : + base(message) + { + Init(zlibErrorContext, (ZErrorCode)zlibErrorCode, zlibErrorMessage); + } + + + /// + /// This constructor is provided in compliance with common NetFx design patterns; + /// developers should prefer using the constructor + /// public ZLibException(string message, string zlibErrorContext, ZLibNative.ErrorCode zlibErrorCode, string zlibErrorMessage). + /// + public ZLibException() + : base() + { + Init(); + } + + + /// + /// This constructor is provided in compliance with common NetFx design patterns; + /// developers should prefer using the constructor + /// public ZLibException(string message, string zlibErrorContext, ZLibNative.ErrorCode zlibErrorCode, string zlibErrorMessage). + /// + /// The error message that explains the reason for the exception. + public ZLibException(string message) + : base(message) + { + Init(); + } + + + /// + /// This constructor is provided in compliance with common NetFx design patterns; + /// developers should prefer using the constructor + /// public ZLibException(string message, string zlibErrorContext, ZLibNative.ErrorCode zlibErrorCode, string zlibErrorMessage). + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null. + public ZLibException(string message, Exception inner) + : base(message, inner) + { + Init(); + } + + private void Init() + { + Init("", ZErrorCode.Ok, ""); + } + + private void Init(string zlibErrorContext, ZErrorCode zlibErrorCode, string zlibErrorMessage) + { + _zlibErrorContext = zlibErrorContext; + _zlibErrorCode = zlibErrorCode; + _zlibErrorMessage = zlibErrorMessage; + } + + + public string ZLibContext + { + [SecurityCritical] + get + { return _zlibErrorContext; } + } + + public int ZLibErrorCode + { + [SecurityCritical] + get + { return (int)_zlibErrorCode; } + } + + public string ZLibErrorMessage + { + [SecurityCritical] + get { return _zlibErrorMessage; } + } + } +} diff --git a/src/System.IO.Pipelines.Compression/ZLibNative.Windows.cs b/src/System.IO.Pipelines.Compression/ZLibNative.Windows.cs new file mode 100644 index 00000000000..ed6ebcf46d3 --- /dev/null +++ b/src/System.IO.Pipelines.Compression/ZLibNative.Windows.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Compression +{ + internal static partial class ZLibNative + { + /// + /// ZLib stream descriptor data structure + /// Do not construct instances of ZStream explicitly. + /// Always use ZLibNative.DeflateInit2_ or ZLibNative.InflateInit2_ instead. + /// Those methods will wrap this structure into a SafeHandle and thus make sure that it is always disposed correctly. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + internal struct ZStream + { + internal void Init() + { + zalloc = ZNullPtr; + zfree = ZNullPtr; + opaque = ZNullPtr; + } + + internal IntPtr nextIn; //Bytef *next_in; /* next input byte */ + internal uint availIn; //uInt avail_in; /* number of bytes available at next_in */ + internal uint totalIn; //uLong total_in; /* total nb of input bytes read so far */ + + internal IntPtr nextOut; //Bytef *next_out; /* next output byte should be put there */ + internal uint availOut; //uInt avail_out; /* remaining free space at next_out */ + internal uint totalOut; //uLong total_out; /* total nb of bytes output so far */ + + internal IntPtr msg; //char *msg; /* last error message, NULL if no error */ + + internal IntPtr state; //struct internal_state FAR *state; /* not visible by applications */ + + internal IntPtr zalloc; //alloc_func zalloc; /* used to allocate the internal state */ + internal IntPtr zfree; //free_func zfree; /* used to free the internal state */ + internal IntPtr opaque; //voidpf opaque; /* private data object passed to zalloc and zfree */ + + internal int dataType; //int data_type; /* best guess about the data type: binary or text */ + internal uint adler; //uLong adler; /* adler32 value of the uncompressed data */ + internal uint reserved; //uLong reserved; /* reserved for future use */ + } + } +} diff --git a/src/System.IO.Pipelines.Compression/ZLibNative.cs b/src/System.IO.Pipelines.Compression/ZLibNative.cs new file mode 100644 index 00000000000..df673a9bbcf --- /dev/null +++ b/src/System.IO.Pipelines.Compression/ZLibNative.cs @@ -0,0 +1,410 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using System.Security; + +namespace System.IO.Pipelines.Compression +{ + /// + /// This class provides declaration for constants and PInvokes as well as some basic tools for exposing the + /// native CLRCompression.dll (effectively, ZLib) library to managed code. + /// + /// See also: How to choose a compression level (in comments to CompressionLevel. + /// + internal static partial class ZLibNative + { + #region Constants defined in zlib.h + + // This is the NULL pointer for using with ZLib pointers; + // we prefer it to IntPtr.Zero to mimic the definition of Z_NULL in zlib.h: + internal static readonly IntPtr ZNullPtr = (IntPtr)((Int32)0); + + public enum FlushCode : int + { + NoFlush = 0, + SyncFlush = 2, + Finish = 4, + } + + public enum ErrorCode : int + { + Ok = 0, + StreamEnd = 1, + StreamError = -2, + DataError = -3, + MemError = -4, + BufError = -5, + VersionError = -6 + } + + /// + ///

ZLib can accept any integer value between 0 and 9 (inclusive) as a valid compression level parameter: + /// 1 gives best speed, 9 gives best compression, 0 gives no compression at all (the input data is simply copied a block at a time). + /// CompressionLevel.DefaultCompression = -1 requests a default compromise between speed and compression + /// (currently equivalent to level 6).

+ /// + ///

How to choose a compression level:

+ /// + ///

The names NoCompression, BestSpeed, DefaultCompression are taken over from the corresponding + /// ZLib definitions, which map to our public NoCompression, Fastest, and Optimal respectively. + ///

Optimal Compression:

+ ///

ZLibNative.CompressionLevel compressionLevel = ZLibNative.CompressionLevel.DefaultCompression;
+ /// Int32 windowBits = 15; // or -15 if no headers required
+ /// Int32 memLevel = 8;
+ /// ZLibNative.CompressionStrategy strategy = ZLibNative.CompressionStrategy.DefaultStrategy;

+ /// + ///

Fastest compression:

+ ///

ZLibNative.CompressionLevel compressionLevel = ZLibNative.CompressionLevel.BestSpeed;
+ /// Int32 windowBits = 15; // or -15 if no headers required
+ /// Int32 memLevel = 8;
+ /// ZLibNative.CompressionStrategy strategy = ZLibNative.CompressionStrategy.DefaultStrategy;

+ /// + ///

No compression (even faster, useful for data that cannot be compressed such some image formats):

+ ///

ZLibNative.CompressionLevel compressionLevel = ZLibNative.CompressionLevel.NoCompression;
+ /// Int32 windowBits = 15; // or -15 if no headers required
+ /// Int32 memLevel = 7;
+ /// ZLibNative.CompressionStrategy strategy = ZLibNative.CompressionStrategy.DefaultStrategy;

+ ///
+ public enum CompressionLevel : int + { + NoCompression = 0, + BestSpeed = 1, + DefaultCompression = -1 + } + + /// + ///

From the ZLib manual:

+ ///

CompressionStrategy is used to tune the compression algorithm.
+ /// Use the value DefaultStrategy for normal data, Filtered for data produced by a filter (or predictor), + /// HuffmanOnly to force Huffman encoding only (no string match), or Rle to limit match distances to one + /// (run-length encoding). Filtered data consists mostly of small values with a somewhat random distribution. In this case, the + /// compression algorithm is tuned to compress them better. The effect of Filtered is to force more Huffman coding and] + /// less string matching; it is somewhat intermediate between DefaultStrategy and HuffmanOnly. + /// Rle is designed to be almost as fast as HuffmanOnly, but give better compression for PNG image data. + /// The strategy parameter only affects the compression ratio but not the correctness of the compressed output even if it is not set + /// appropriately. Fixed prevents the use of dynamic Huffman codes, allowing for a simpler decoder for special applications.

+ /// + ///

For NetFx use:

+ ///

We have investigated compression scenarios for a bunch of different frequently occurring compression data and found that in all + /// cases we investigated so far, DefaultStrategy provided best results

+ ///

See also: How to choose a compression level (in comments to CompressionLevel.

+ ///
+ public enum CompressionStrategy : int + { + DefaultStrategy = 0 + } + + /// + /// In version 1.2.3, ZLib provides on the Deflated-CompressionMethod. + /// + public enum CompressionMethod : int + { + Deflated = 8 + } + + #endregion // Constants defined in zlib.h + + + #region Defaults for ZLib parameters + + /// + ///

From the ZLib manual:

+ ///

ZLib's windowBits parameter is the base two logarithm of the window size (the size of the history buffer). + /// It should be in the range 8..15 for this version of the library. Larger values of this parameter result in better compression + /// at the expense of memory usage. The default value is 15 if deflateInit is used instead.
+ /// Note: + /// windowBits can also be –8..–15 for raw deflate. In this case, -windowBits determines the window size. + /// Deflate will then generate raw deflate data with no ZLib header or trailer, and will not compute an adler32 check value.
+ ///

See also: How to choose a compression level (in comments to CompressionLevel.

+ ///
+ public const int Deflate_DefaultWindowBits = -15; // Legal values are 8..15 and -8..-15. 15 is the window size, + // negative val causes deflate to produce raw deflate data (no zlib header). + + /// + ///

Zlib's windowBits parameter is the base two logarithm of the window size (the size of the history buffer). + /// For GZip header encoding, windowBits should be equal to a value between 8..15 (to specify Window Size) added to + /// 16. The range of values for GZip encoding is therefore 24..31. + /// Note: + /// The GZip header will have no file name, no extra data, no comment, no modification time (set to zero), no header crc, and + /// the operating system will be set based on the OS that the ZLib library was compiled to. ZStream.adler + /// is a crc32 instead of an adler32.

+ ///
+ public const int GZip_DefaultWindowBits = 31; + + /// + ///

From the ZLib manual:

+ ///

The memLevel parameter specifies how much memory should be allocated for the internal compression state. + /// memLevel = 1 uses minimum memory but is slow and reduces compression ratio; memLevel = 9 uses maximum + /// memory for optimal speed. The default value is 8.

+ ///

See also: How to choose a compression level (in comments to CompressionLevel.

+ ///
+ public const int Deflate_DefaultMemLevel = 8; // Memory usage by deflate. Legal range: [1..9]. 8 is ZLib default. + // More is faster and better compression with more memory usage. + public const int Deflate_NoCompressionMemLevel = 7; + + #endregion // Defaults for ZLib parameters + + /** + * Do not remove the nested typing of types inside of System.IO.Compression.ZLibNative. + * This was done on purpose to: + * + * - Achieve the right encapsulation in a situation where ZLibNative may be compiled division-wide + * into different assemblies that wish to consume CLRCompression. Since internal + * scope is effectively like public scope when compiling ZLibNative into a higher + * level assembly, we need a combination of inner types and private-scope members to achieve + * the right encapsulation. + * + * - Achieve late dynamic loading of CLRCompression.dll at the right time. + * The native assembly will not be loaded unless it is actually used since the loading is performed by a static + * constructor of an inner type that is not directly referenced by user code. + * + * In Dev12 we would like to create a proper feature for loading native assemblies from user-specified + * directories in order to PInvoke into them. This would preferably happen in the native interop/PInvoke + * layer; if not we can add a Framework level feature. + */ + + #region ZLib Stream Handle type + + /// + /// The ZLibStreamHandle could be a CriticalFinalizerObject rather than a + /// SafeHandleMinusOneIsInvalid. This would save an IntPtr field since + /// ZLibStreamHandle does not actually use its handle field. + /// Instead it uses a private ZStream zStream field which is the actual handle data + /// structure requiring critical finalization. + /// However, we would like to take advantage if the better debugability offered by the fact that a + /// releaseHandleFailed MDA is raised if the ReleaseHandle method returns + /// false, which can for instance happen if the underlying ZLib XxxxEnd + /// routines return an failure error code. + /// + [SecurityCritical] + public sealed class ZLibStreamHandle : SafeHandle + { + #region ZLibStream-SafeHandle-related routines + + public enum State { NotInitialized, InitializedForDeflate, InitializedForInflate, Disposed } + + private ZStream _zStream; + + [SecurityCritical] + private volatile State _initializationState; + + + public ZLibStreamHandle() + : base(new IntPtr(-1), true) + { + _zStream = new ZStream(); + _zStream.Init(); + + _initializationState = State.NotInitialized; + this.SetHandle(IntPtr.Zero); + } + + public override bool IsInvalid + { + [SecurityCritical] + get { return handle == new IntPtr(-1); } + } + + public State InitializationState + { + [SecurityCritical] + get { return _initializationState; } + } + + + [SecurityCritical] + protected override bool ReleaseHandle() + { + switch (InitializationState) + { + case State.NotInitialized: return true; + case State.InitializedForDeflate: return (DeflateEnd() == ZLibNative.ErrorCode.Ok); + case State.InitializedForInflate: return (InflateEnd() == ZLibNative.ErrorCode.Ok); + case State.Disposed: return true; + default: return false; // This should never happen. Did we forget one of the State enum values in the switch? + } + } + + #endregion // ZLibStream-SafeHandle-related routines + + + #region Expose fields on ZStream for use by user / Fx code (add more as required) + + public IntPtr NextIn + { + [SecurityCritical] get { return _zStream.nextIn; } + [SecurityCritical] set { _zStream.nextIn = value; } + } + + public UInt32 AvailIn + { + [SecurityCritical] get { return _zStream.availIn; } + [SecurityCritical] set { _zStream.availIn = value; } + } + + public IntPtr NextOut + { + [SecurityCritical] get { return _zStream.nextOut; } + [SecurityCritical] set { _zStream.nextOut = value; } + } + + public UInt32 AvailOut + { + [SecurityCritical] get { return _zStream.availOut; } + [SecurityCritical] set { _zStream.availOut = value; } + } + + #endregion // Expose fields on ZStream for use by user / Fx code (add more as required) + + + #region Expose ZLib functions for use by user / Fx code (add more as required) + + [SecurityCritical] + private void EnsureNotDisposed() + { + if (InitializationState == State.Disposed) + throw new ObjectDisposedException(this.GetType().ToString()); + } + + + [SecurityCritical] + private void EnsureState(State requiredState) + { + if (InitializationState != requiredState) + throw new InvalidOperationException("InitializationState != " + requiredState.ToString()); + } + + + [SecurityCritical] + public ErrorCode DeflateInit2_(CompressionLevel level, int windowBits, int memLevel, CompressionStrategy strategy) + { + EnsureNotDisposed(); + EnsureState(State.NotInitialized); + + ErrorCode errC = Interop.zlib.DeflateInit2_(ref _zStream, level, CompressionMethod.Deflated, windowBits, memLevel, strategy); + _initializationState = State.InitializedForDeflate; + + return errC; + } + + + [SecurityCritical] + public ErrorCode Deflate(FlushCode flush) + { + EnsureNotDisposed(); + EnsureState(State.InitializedForDeflate); + return Interop.zlib.Deflate(ref _zStream, flush); + } + + + [SecurityCritical] + public ErrorCode DeflateEnd() + { + EnsureNotDisposed(); + EnsureState(State.InitializedForDeflate); + + ErrorCode errC = Interop.zlib.DeflateEnd(ref _zStream); + _initializationState = State.Disposed; + + return errC; + } + + + [SecurityCritical] + public ErrorCode InflateInit2_(int windowBits) + { + EnsureNotDisposed(); + EnsureState(State.NotInitialized); + + ErrorCode errC = Interop.zlib.InflateInit2_(ref _zStream, windowBits); + _initializationState = State.InitializedForInflate; + + return errC; + } + + + [SecurityCritical] + public ErrorCode Inflate(FlushCode flush) + { + EnsureNotDisposed(); + EnsureState(State.InitializedForInflate); + return Interop.zlib.Inflate(ref _zStream, flush); + } + + + [SecurityCritical] + public ErrorCode InflateEnd() + { + EnsureNotDisposed(); + EnsureState(State.InitializedForInflate); + + ErrorCode errC = Interop.zlib.InflateEnd(ref _zStream); + _initializationState = State.Disposed; + + return errC; + } + + /// + /// This function is equivalent to inflateEnd followed by inflateInit. + /// The stream will keep attributes that may have been set by inflateInit2. + /// + [SecurityCritical] + public ErrorCode InflateReset(int windowBits) + { + EnsureNotDisposed(); + EnsureState(State.InitializedForInflate); + + ErrorCode errC = Interop.zlib.InflateEnd(ref _zStream); + if (errC != ErrorCode.Ok) + { + _initializationState = State.Disposed; + return errC; + } + + errC = Interop.zlib.InflateInit2_(ref _zStream, windowBits); + _initializationState = State.InitializedForInflate; + + return errC; + } + + + [SecurityCritical] + public string GetErrorMessage() + { + // This can work even after XxflateEnd(). + return _zStream.msg != ZNullPtr ? Marshal.PtrToStringAnsi(_zStream.msg) : string.Empty; + } + + #endregion // Expose ZLib functions for use by user / Fx code (add more as required) + + } // class ZLibStreamHandle + + #endregion // ZLib Stream Handle type + + + #region public factory methods for ZLibStreamHandle + + + [SecurityCritical] + public static ErrorCode CreateZLibStreamForDeflate(out ZLibStreamHandle zLibStreamHandle, + CompressionLevel level, int windowBits, int memLevel, CompressionStrategy strategy) + { + zLibStreamHandle = new ZLibStreamHandle(); + return zLibStreamHandle.DeflateInit2_(level, windowBits, memLevel, strategy); + } + + + [SecurityCritical] + public static ErrorCode CreateZLibStreamForInflate(out ZLibStreamHandle zLibStreamHandle, int windowBits) + { + zLibStreamHandle = new ZLibStreamHandle(); + return zLibStreamHandle.InflateInit2_(windowBits); + } + + #endregion // public factory methods for ZLibStreamHandle + + } +} diff --git a/src/System.IO.Pipelines.Compression/project.json b/src/System.IO.Pipelines.Compression/project.json new file mode 100644 index 00000000000..85c756d088c --- /dev/null +++ b/src/System.IO.Pipelines.Compression/project.json @@ -0,0 +1,29 @@ +{ + "version": "0.1.0-*", + "description": "Compression algorithms pipeline implementation", + "copyright": "Microsoft Corporation, All rights reserved", + "packOptions": { + "releaseNotes": "Pre-release package, for testing only", + "licenseUrl": "http://go.microsoft.com/fwlink/?LinkId=329770", + "iconUrl": "http://go.microsoft.com/fwlink/?LinkID=288859", + "projectUrl": "https://github.com/dotnet/corefxlab", + "requireLicenseAcceptance": true + }, + "buildOptions": { + "allowUnsafe": true, + "keyFile": "../../tools/Key.snk" + }, + + "dependencies": { + "System.IO.Pipelines": { + "target": "project" + }, + "System.Diagnostics.Contracts": "4.0.1" + }, + + + "frameworks": { + "net451": {}, + "netstandard1.3": {} + } +} diff --git a/src/System.IO.Pipelines.File/FileReader.cs b/src/System.IO.Pipelines.File/FileReader.cs new file mode 100644 index 00000000000..3cde0be35bb --- /dev/null +++ b/src/System.IO.Pipelines.File/FileReader.cs @@ -0,0 +1,186 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.Win32.SafeHandles; + +namespace System.IO.Pipelines.File +{ + public class FileReader : PipelineReader + { + public FileReader(MemoryPool pool) : base(pool) + { + } + + public FileReader(PipelineReaderWriter input) : base(input) + { + } + + // Win32 file impl + // TODO: Other platforms + public unsafe void OpenReadFile(string path) + { + var fileHandle = CreateFile(path, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, EFileAttributes.Overlapped, IntPtr.Zero); + + var handle = ThreadPoolBoundHandle.BindHandle(fileHandle); + + var readOperation = new ReadOperation + { + Writer = _input, + FileHandle = fileHandle, + ThreadPoolBoundHandle = handle, + IOCallback = IOCallback + }; + + var overlapped = new PreAllocatedOverlapped(IOCallback, readOperation, null); + readOperation.PreAllocatedOverlapped = overlapped; + + _input.ReadingStarted.ContinueWith((t, state) => + { + ((ReadOperation)state).Read(); + }, + readOperation); + } + + public unsafe static void IOCallback(uint errorCode, uint numBytes, NativeOverlapped* pOverlapped) + { + var state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped); + var operation = (ReadOperation)state; + + operation.ThreadPoolBoundHandle.FreeNativeOverlapped(operation.Overlapped); + + operation.Offset += (int)numBytes; + + var buffer = operation.BoxedBuffer.Value; + + buffer.Advance((int)numBytes); + var task = buffer.FlushAsync(); + + if (numBytes == 0 || operation.Writer.Writing.IsCompleted) + { + operation.Writer.Complete(); + + // The operation can be disposed when there's nothing more to produce + operation.Dispose(); + } + else if (task.IsCompleted) + { + operation.Read(); + } + else + { + // Keep reading once we get the completion + task.ContinueWith((t, s) => ((ReadOperation)s).Read(), operation); + } + } + + private class ReadOperation + { + public IOCompletionCallback IOCallback { get; set; } + public SafeFileHandle FileHandle { get; set; } + + public PreAllocatedOverlapped PreAllocatedOverlapped { get; set; } + + public ThreadPoolBoundHandle ThreadPoolBoundHandle { get; set; } + + public unsafe NativeOverlapped* Overlapped { get; set; } + + public IPipelineWriter Writer { get; set; } + + public StrongBox BoxedBuffer { get; set; } + + public int Offset { get; set; } + + public unsafe void Read() + { + var buffer = Writer.Alloc(2048); + void* pointer; + if (!buffer.Memory.TryGetPointer(out pointer)) + { + throw new InvalidOperationException("Memory needs to be pinned"); + } + var data = (IntPtr)pointer; + var count = buffer.Memory.Length; + + var overlapped = ThreadPoolBoundHandle.AllocateNativeOverlapped(PreAllocatedOverlapped); + overlapped->OffsetLow = Offset; + + Overlapped = overlapped; + + BoxedBuffer = new StrongBox(buffer); + + int r = ReadFile(FileHandle, data, count, IntPtr.Zero, overlapped); + + // TODO: Error handling + + // 997 + int hr = Marshal.GetLastWin32Error(); + if (hr != 997) + { + Writer.Complete(Marshal.GetExceptionForHR(hr)); + } + } + + public void Dispose() + { + FileHandle.Dispose(); + + ThreadPoolBoundHandle.Dispose(); + + PreAllocatedOverlapped.Dispose(); + } + } + + [DllImport("kernel32.dll", SetLastError = true)] + static extern unsafe int ReadFile( + SafeFileHandle hFile, // handle to file + IntPtr pBuffer, // data buffer, should be fixed + int NumberOfBytesToRead, // number of bytes to read + IntPtr pNumberOfBytesRead, // number of bytes read, provide IntPtr.Zero here + NativeOverlapped* lpOverlapped // should be fixed, if not IntPtr.Zero + ); + + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static extern SafeFileHandle CreateFile( + string fileName, + [MarshalAs(UnmanagedType.U4)] FileAccess fileAccess, + [MarshalAs(UnmanagedType.U4)] FileShare fileShare, + IntPtr securityAttributes, + [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, + [MarshalAs(UnmanagedType.U4)] EFileAttributes flags, + IntPtr template + ); + + + [Flags] + private enum EFileAttributes : uint + { + Readonly = 0x00000001, + Hidden = 0x00000002, + System = 0x00000004, + Directory = 0x00000010, + Archive = 0x00000020, + Device = 0x00000040, + Normal = 0x00000080, + Temporary = 0x00000100, + SparseFile = 0x00000200, + ReparsePoint = 0x00000400, + Compressed = 0x00000800, + Offline = 0x00001000, + NotContentIndexed = 0x00002000, + Encrypted = 0x00004000, + Write_Through = 0x80000000, + Overlapped = 0x40000000, + NoBuffering = 0x20000000, + RandomAccess = 0x10000000, + SequentialScan = 0x08000000, + DeleteOnClose = 0x04000000, + BackupSemantics = 0x02000000, + PosixSemantics = 0x01000000, + OpenReparsePoint = 0x00200000, + OpenNoRecall = 0x00100000, + FirstPipeInstance = 0x00080000 + } + } +} diff --git a/src/System.IO.Pipelines.File/ReadableFilePipelineFactoryExtensions.cs b/src/System.IO.Pipelines.File/ReadableFilePipelineFactoryExtensions.cs new file mode 100644 index 00000000000..a3d11eb3c08 --- /dev/null +++ b/src/System.IO.Pipelines.File/ReadableFilePipelineFactoryExtensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.File +{ + public static class ReadableFilePipelineFactoryExtensions + { + public static IPipelineReader ReadFile(this PipelineFactory factory, string path) + { + var reader = factory.Create(); + + var file = new FileReader(reader); + file.OpenReadFile(path); + return file; + } + } +} diff --git a/src/System.IO.Pipelines.File/System.IO.Pipelines.File.xproj b/src/System.IO.Pipelines.File/System.IO.Pipelines.File.xproj new file mode 100644 index 00000000000..809df1d562b --- /dev/null +++ b/src/System.IO.Pipelines.File/System.IO.Pipelines.File.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 7e8ac800-7d57-4645-83f5-fa101090d648 + System.IO.Pipelines.File + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/System.IO.Pipelines.File/project.json b/src/System.IO.Pipelines.File/project.json new file mode 100644 index 00000000000..d1d8f554ca5 --- /dev/null +++ b/src/System.IO.Pipelines.File/project.json @@ -0,0 +1,29 @@ +{ + "version": "0.1.0-*", + "description": "File I/O pipeline implementation", + "copyright": "Microsoft Corporation, All rights reserved", + "packOptions": { + "releaseNotes": "Pre-release package, for testing only", + "licenseUrl": "http://go.microsoft.com/fwlink/?LinkId=329770", + "iconUrl": "http://go.microsoft.com/fwlink/?LinkID=288859", + "projectUrl": "https://github.com/dotnet/corefxlab", + "requireLicenseAcceptance": true + }, + "buildOptions": { + "allowUnsafe": true, + "keyFile": "../../tools/Key.snk" + }, + + "dependencies": { + "System.IO.Pipelines": { + "target": "project" + }, + "System.Threading.Overlapped": "4.0.1" + }, + + + "frameworks": { + "netstandard1.3": {}, + "net46": {} + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/Internal/WorkQueue.cs b/src/System.IO.Pipelines.Networking.Libuv/Internal/WorkQueue.cs new file mode 100644 index 00000000000..99dfd79faf8 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Internal/WorkQueue.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; + +namespace System.IO.Pipelines.Networking.Libuv.Internal +{ + // Lock free linked list that for multi producers and a single consumer + internal class WorkQueue + { + private Node _head; + + public void Add(T value) + { + Node node = new Node(), oldHead; + node.Value = value; + + do + { + oldHead = _head; + node.Next = _head; + node.Count = 1 + (oldHead?.Count ?? 0); + } while (Interlocked.CompareExchange(ref _head, node, oldHead) != oldHead); + } + + + public Enumerable DequeAll() + { + // swap out the head + var node = Interlocked.Exchange(ref _head, null); + + // we now have a detatched head, but we're backwards + // note: 0/1 are a trivial case + if (node == null || node.Count == 1) + { + return new Enumerable(node); + } + // otherwise, we need to reverse the linked-list + // note: use the iterative method to avoid a stack-dive + Node prev = null; + int count = 1; // rebuild the counts + while (node != null) + { + var next = node.Next; + node.Next = prev; + node.Count = count++; + prev = node; + node = next; + } + return new Enumerable(prev); + } + + public struct Enumerable : IEnumerable + { + private Node _node; + public int Count => _node?.Count ?? 0; + internal Enumerable(Node node) + { + _node = node; + } + public Enumerator GetEnumerator() => new Enumerator(_node); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + } + + public struct Enumerator : IEnumerator + { + private Node _next; + private T _current; + internal Enumerator(Node node) + { + _current = default(T); + _next = node; + } + object IEnumerator.Current => _current; + public T Current => _current; + + void IDisposable.Dispose() { } + + public bool MoveNext() + { + if (_next == null) + { + _current = default(T); + return false; + } + _current = _next.Value; + _next = _next.Next; + return true; + } + public void Reset() { throw new NotSupportedException(); } + } + + internal class Node // need internal for Enumerator / Enumerable + { + public T Value; + public Node Next; + public int Count; + } + + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Libuv/Internal/WriteReqPool.cs b/src/System.IO.Pipelines.Networking.Libuv/Internal/WriteReqPool.cs new file mode 100644 index 00000000000..4b55501f64f --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Internal/WriteReqPool.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO.Pipelines.Networking.Libuv.Interop; + +namespace System.IO.Pipelines.Networking.Libuv.Internal +{ + public class WriteReqPool + { + private const int _maxPooledWriteReqs = 1024; + + private readonly UvThread _thread; + private readonly Queue _pool = new Queue(_maxPooledWriteReqs); + private bool _disposed; + + public WriteReqPool(UvThread thread) + { + _thread = thread; + } + + public UvWriteReq Allocate() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + + UvWriteReq req; + if (_pool.Count > 0) + { + req = _pool.Dequeue(); + } + else + { + req = new UvWriteReq(); + req.Init(_thread.Loop); + } + + return req; + } + + public void Return(UvWriteReq req) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + + if (_pool.Count < _maxPooledWriteReqs) + { + _pool.Enqueue(req); + } + else + { + req.Dispose(); + } + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + + while (_pool.Count > 0) + { + _pool.Dequeue().Dispose(); + } + } + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/PlatformApis.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/PlatformApis.cs new file mode 100644 index 00000000000..d58e52264cb --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/PlatformApis.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + public static class PlatformApis + { + static PlatformApis() + { + IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + IsDarwin = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + } + + public static bool IsWindows { get; } + + public static bool IsDarwin { get; } + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/SockAddr.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/SockAddr.cs new file mode 100644 index 00000000000..d7cee01e953 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/SockAddr.cs @@ -0,0 +1,110 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + public struct SockAddr + { + // this type represents native memory occupied by sockaddr struct + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms740496(v=vs.85).aspx + // although the c/c++ header defines it as a 2-byte short followed by a 14-byte array, + // the simplest way to reserve the same size in c# is with four nameless long values + private long _field0; + private long _field1; + private long _field2; + private long _field3; + + public SockAddr(long ignored) + { + _field3 = _field0 = _field1 = _field2 = _field3 = 0; + } + + public unsafe IPEndPoint GetIPEndPoint() + { + // The bytes are represented in network byte order. + // + // Example 1: [2001:4898:e0:391:b9ef:1124:9d3e:a354]:39179 + // + // 0000 0000 0b99 0017 => The third and fourth bytes 990B is the actual port + // 9103 e000 9848 0120 => IPv6 address is represented in the 128bit field1 and field2. + // 54a3 3e9d 2411 efb9 Read these two 64-bit long from right to left byte by byte. + // 0000 0000 0000 0000 + // + // Example 2: 10.135.34.141:39178 when adopt dual-stack sockets, IPv4 is mapped to IPv6 + // + // 0000 0000 0a99 0017 => The port representation are the same + // 0000 0000 0000 0000 + // 8d22 870a ffff 0000 => IPv4 occupies the last 32 bit: 0A.87.22.8d is the actual address. + // 0000 0000 0000 0000 + // + // Example 3: 10.135.34.141:12804, not dual-stack sockets + // + // 8d22 870a fd31 0002 => sa_family == AF_INET (02) + // 0000 0000 0000 0000 + // 0000 0000 0000 0000 + // 0000 0000 0000 0000 + // + // Example 4: 127.0.0.1:52798, on a Mac OS + // + // 0100 007F 3ECE 0210 => sa_family == AF_INET (02) Note that struct sockaddr on mac use + // 0000 0000 0000 0000 the second unint8 field for sa family type + // 0000 0000 0000 0000 http://www.opensource.apple.com/source/xnu/xnu-1456.1.26/bsd/sys/socket.h + // 0000 0000 0000 0000 + // + // Reference: + // - Windows: https://msdn.microsoft.com/en-us/library/windows/desktop/ms740506(v=vs.85).aspx + // - Linux: https://github.com/torvalds/linux/blob/6a13feb9c82803e2b815eca72fa7a9f5561d7861/include/linux/socket.h + // - Apple: http://www.opensource.apple.com/source/xnu/xnu-1456.1.26/bsd/sys/socket.h + + // Quick calculate the port by mask the field and locate the byte 3 and byte 4 + // and then shift them to correct place to form a int. + var port = ((int)(_field0 & 0x00FF0000) >> 8) | (int)((_field0 & 0xFF000000) >> 24); + + int family = (int)_field0; + if (PlatformApis.IsDarwin) + { + // see explaination in example 4 + family = family >> 8; + } + family = family & 0xFF; + + if (family == 2) + { + // AF_INET => IPv4 + return new IPEndPoint(new IPAddress((_field0 >> 32) & 0xFFFFFFFF), port); + } + else if (IsIPv4MappedToIPv6()) + { + var ipv4bits = (_field2 >> 32) & 0x00000000FFFFFFFF; + return new IPEndPoint(new IPAddress(ipv4bits), port); + } + else + { + // otherwise IPv6 + var bytes = new byte[16]; + fixed (byte* b = bytes) + { + *((long*)b) = _field1; + *((long*)(b + 8)) = _field2; + } + + return new IPEndPoint(new IPAddress(bytes), port); + } + } + + private bool IsIPv4MappedToIPv6() + { + // If the IPAddress is an IPv4 mapped to IPv6, return the IPv4 representation instead. + // For example [::FFFF:127.0.0.1] will be transform to IPAddress of 127.0.0.1 + if (_field1 != 0) + { + return false; + } + + return (_field2 & 0xFFFFFFFF) == 0xFFFF0000; + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/Uv.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/Uv.cs new file mode 100644 index 00000000000..d7060f08d17 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/Uv.cs @@ -0,0 +1,638 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + public class Uv + { + public Uv() + { + IsWindows = PlatformApis.IsWindows; + + _uv_loop_init = NativeMethods.uv_loop_init; + _uv_loop_close = NativeMethods.uv_loop_close; + _uv_run = NativeMethods.uv_run; + _uv_stop = NativeMethods.uv_stop; + _uv_ref = NativeMethods.uv_ref; + _uv_unref = NativeMethods.uv_unref; + _uv_fileno = NativeMethods.uv_fileno; + _uv_close = NativeMethods.uv_close; + _uv_async_init = NativeMethods.uv_async_init; + _uv_async_send = NativeMethods.uv_async_send; + _uv_unsafe_async_send = NativeMethods.uv_unsafe_async_send; + _uv_tcp_init = NativeMethods.uv_tcp_init; + _uv_tcp_bind = NativeMethods.uv_tcp_bind; + _uv_tcp_connect = NativeMethods.uv_tcp_connect; + _uv_tcp_open = NativeMethods.uv_tcp_open; + _uv_tcp_nodelay = NativeMethods.uv_tcp_nodelay; + _uv_pipe_init = NativeMethods.uv_pipe_init; + _uv_pipe_bind = NativeMethods.uv_pipe_bind; + _uv_listen = NativeMethods.uv_listen; + _uv_accept = NativeMethods.uv_accept; + _uv_pipe_connect = NativeMethods.uv_pipe_connect; + _uv_pipe_pending_count = NativeMethods.uv_pipe_pending_count; + _uv_read_start = NativeMethods.uv_read_start; + _uv_read_stop = NativeMethods.uv_read_stop; + _uv_try_write = NativeMethods.uv_try_write; + unsafe + { + _uv_write = NativeMethods.uv_write; + _uv_write2 = NativeMethods.uv_write2; + } + _uv_shutdown = NativeMethods.uv_shutdown; + _uv_err_name = NativeMethods.uv_err_name; + _uv_strerror = NativeMethods.uv_strerror; + _uv_loop_size = NativeMethods.uv_loop_size; + _uv_handle_size = NativeMethods.uv_handle_size; + _uv_req_size = NativeMethods.uv_req_size; + _uv_ip4_addr = NativeMethods.uv_ip4_addr; + _uv_ip6_addr = NativeMethods.uv_ip6_addr; + _uv_tcp_getpeername = NativeMethods.uv_tcp_getpeername; + _uv_tcp_getsockname = NativeMethods.uv_tcp_getsockname; + _uv_walk = NativeMethods.uv_walk; + } + + // Second ctor that doesn't set any fields only to be used by MockLibuv + internal Uv(bool onlyForTesting) + { + } + + public readonly bool IsWindows; + + public void ThrowIfErrored(int statusCode) + { + // Note: method is explicitly small so the success case is easily inlined + if (statusCode < 0) + { + ThrowError(statusCode); + } + } + + private void ThrowError(int statusCode) + { + // Note: only has one throw block so it will marked as "Does not return" by the jit + // and not inlined into previous function, while also marking as a function + // that does not need cpu register prep to call (see: https://github.com/dotnet/coreclr/pull/6103) + throw GetError(statusCode); + } + + public void Check(int statusCode, out Exception error) + { + // Note: method is explicitly small so the success case is easily inlined + error = statusCode < 0 ? GetError(statusCode) : null; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private UvException GetError(int statusCode) + { + // Note: method marked as NoInlining so it doesn't bloat either of the two preceeding functions + // Check and ThrowError and alter their jit heuristics. + var errorName = err_name(statusCode); + var errorDescription = strerror(statusCode); + return new UvException("Error " + statusCode + " " + errorName + " " + errorDescription, statusCode); + } + + protected Func _uv_loop_init; + public void loop_init(UvLoopHandle handle) + { + ThrowIfErrored(_uv_loop_init(handle)); + } + + protected Func _uv_loop_close; + public void loop_close(UvLoopHandle handle) + { + handle.Validate(closed: true); + ThrowIfErrored(_uv_loop_close(handle.InternalGetHandle())); + } + + protected Func _uv_run; + public void run(UvLoopHandle handle, int mode) + { + handle.Validate(); + ThrowIfErrored(_uv_run(handle, mode)); + } + + protected Action _uv_stop; + public void stop(UvLoopHandle handle) + { + handle.Validate(); + _uv_stop(handle); + } + + protected Action _uv_ref; + public void @ref(UvHandle handle) + { + handle.Validate(); + _uv_ref(handle); + } + + protected Action _uv_unref; + public void unref(UvHandle handle) + { + handle.Validate(); + _uv_unref(handle); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + protected delegate int uv_fileno_func(UvHandle handle, ref IntPtr socket); + protected uv_fileno_func _uv_fileno; + public void uv_fileno(UvHandle handle, ref IntPtr socket) + { + handle.Validate(); + ThrowIfErrored(_uv_fileno(handle, ref socket)); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_close_cb(IntPtr handle); + protected Action _uv_close; + public void close(UvHandle handle, uv_close_cb close_cb) + { + handle.Validate(closed: true); + _uv_close(handle.InternalGetHandle(), close_cb); + } + + public void close(IntPtr handle, uv_close_cb close_cb) + { + _uv_close(handle, close_cb); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_async_cb(IntPtr handle); + protected Func _uv_async_init; + public void async_init(UvLoopHandle loop, UvAsyncHandle handle, uv_async_cb cb) + { + loop.Validate(); + handle.Validate(); + ThrowIfErrored(_uv_async_init(loop, handle, cb)); + } + + protected Func _uv_async_send; + public void async_send(UvAsyncHandle handle) + { + ThrowIfErrored(_uv_async_send(handle)); + } + + protected Func _uv_unsafe_async_send; + public void unsafe_async_send(IntPtr handle) + { + ThrowIfErrored(_uv_unsafe_async_send(handle)); + } + + protected Func _uv_tcp_init; + public void tcp_init(UvLoopHandle loop, UvTcpHandle handle) + { + loop.Validate(); + handle.Validate(); + ThrowIfErrored(_uv_tcp_init(loop, handle)); + } + + protected delegate int uv_tcp_bind_func(UvTcpHandle handle, ref SockAddr addr, int flags); + protected uv_tcp_bind_func _uv_tcp_bind; + public void tcp_bind(UvTcpHandle handle, ref SockAddr addr, int flags) + { + handle.Validate(); + ThrowIfErrored(_uv_tcp_bind(handle, ref addr, flags)); + if (PlatformApis.IsWindows) + { + tcp_bind_windows_extras(handle); + } + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_connect_cb(IntPtr req, int status); + + protected delegate void tcp_connect_func(UvConnectRequest req, UvTcpHandle handle, ref SockAddr addr, uv_connect_cb cb); + protected tcp_connect_func _uv_tcp_connect; + public void tcp_connect(UvConnectRequest req, UvTcpHandle handle, ref SockAddr addr, uv_connect_cb cb) + { + req.Validate(); + handle.Validate(); + _uv_tcp_connect(req, handle, ref addr, cb); + } + + private unsafe void tcp_bind_windows_extras(UvTcpHandle handle) + { + const int SIO_LOOPBACK_FAST_PATH = -1744830448; // IOC_IN | IOC_WS2 | 16; + const int WSAEOPNOTSUPP = 10000 + 45; // (WSABASEERR+45) + const int SOCKET_ERROR = -1; + + var socket = IntPtr.Zero; + ThrowIfErrored(_uv_fileno(handle, ref socket)); + + // Enable loopback fast-path for lower latency for localhost comms, like HttpPlatformHandler fronting + // http://blogs.technet.com/b/wincat/archive/2012/12/05/fast-tcp-loopback-performance-and-low-latency-with-windows-server-2012-tcp-loopback-fast-path.aspx + // https://github.com/libuv/libuv/issues/489 + var optionValue = 1; + uint dwBytes = 0u; + + var result = NativeMethods.WSAIoctl(socket, SIO_LOOPBACK_FAST_PATH, &optionValue, sizeof(int), null, 0, out dwBytes, IntPtr.Zero, IntPtr.Zero); + if (result == SOCKET_ERROR) + { + var errorId = NativeMethods.WSAGetLastError(); + if (errorId == WSAEOPNOTSUPP) + { + // This system is not >= Windows Server 2012, and the call is not supported. + } + else + { + ThrowIfErrored(errorId); + } + } + } + + protected Func _uv_tcp_open; + public void tcp_open(UvTcpHandle handle, IntPtr hSocket) + { + handle.Validate(); + ThrowIfErrored(_uv_tcp_open(handle, hSocket)); + } + + protected Func _uv_tcp_nodelay; + public void tcp_nodelay(UvTcpHandle handle, bool enable) + { + handle.Validate(); + ThrowIfErrored(_uv_tcp_nodelay(handle, enable ? 1 : 0)); + } + + protected Func _uv_pipe_init; + public void pipe_init(UvLoopHandle loop, UvPipeHandle handle, bool ipc) + { + loop.Validate(); + handle.Validate(); + ThrowIfErrored(_uv_pipe_init(loop, handle, ipc ? -1 : 0)); + } + + protected Func _uv_pipe_bind; + public void pipe_bind(UvPipeHandle handle, string name) + { + handle.Validate(); + ThrowIfErrored(_uv_pipe_bind(handle, name)); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_connection_cb(IntPtr server, int status); + protected Func _uv_listen; + public void listen(UvStreamHandle handle, int backlog, uv_connection_cb cb) + { + handle.Validate(); + ThrowIfErrored(_uv_listen(handle, backlog, cb)); + } + + protected Func _uv_accept; + public void accept(UvStreamHandle server, UvStreamHandle client) + { + server.Validate(); + client.Validate(); + ThrowIfErrored(_uv_accept(server, client)); + } + + protected Action _uv_pipe_connect; + public void pipe_connect(UvConnectRequest req, UvPipeHandle handle, string name, uv_connect_cb cb) + { + req.Validate(); + handle.Validate(); + _uv_pipe_connect(req, handle, name, cb); + } + + protected Func _uv_pipe_pending_count; + public int pipe_pending_count(UvPipeHandle handle) + { + handle.Validate(); + return _uv_pipe_pending_count(handle); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_alloc_cb(IntPtr server, int suggested_size, out uv_buf_t buf); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_read_cb(IntPtr server, int nread, ref uv_buf_t buf); + protected Func _uv_read_start; + public void read_start(UvStreamHandle handle, uv_alloc_cb alloc_cb, uv_read_cb read_cb) + { + handle.Validate(); + ThrowIfErrored(_uv_read_start(handle, alloc_cb, read_cb)); + } + + protected Func _uv_read_stop; + public void read_stop(UvStreamHandle handle) + { + handle.Validate(); + ThrowIfErrored(_uv_read_stop(handle)); + } + + protected Func _uv_try_write; + public int try_write(UvStreamHandle handle, uv_buf_t[] bufs, int nbufs) + { + handle.Validate(); + var count = _uv_try_write(handle, bufs, nbufs); + ThrowIfErrored(count); + return count; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_write_cb(IntPtr req, int status); + + unsafe protected delegate int uv_write_func(UvRequest req, UvStreamHandle handle, uv_buf_t* bufs, int nbufs, uv_write_cb cb); + protected uv_write_func _uv_write; + unsafe public void write(UvRequest req, UvStreamHandle handle, uv_buf_t* bufs, int nbufs, uv_write_cb cb) + { + req.Validate(); + handle.Validate(); + ThrowIfErrored(_uv_write(req, handle, bufs, nbufs, cb)); + } + + unsafe protected delegate int uv_write2_func(UvRequest req, UvStreamHandle handle, uv_buf_t* bufs, int nbufs, UvStreamHandle sendHandle, uv_write_cb cb); + protected uv_write2_func _uv_write2; + unsafe public void write2(UvRequest req, UvStreamHandle handle, Uv.uv_buf_t* bufs, int nbufs, UvStreamHandle sendHandle, uv_write_cb cb) + { + req.Validate(); + handle.Validate(); + ThrowIfErrored(_uv_write2(req, handle, bufs, nbufs, sendHandle, cb)); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_shutdown_cb(IntPtr req, int status); + protected Func _uv_shutdown; + public void shutdown(UvShutdownReq req, UvStreamHandle handle, uv_shutdown_cb cb) + { + req.Validate(); + handle.Validate(); + ThrowIfErrored(_uv_shutdown(req, handle, cb)); + } + + protected Func _uv_err_name; + public string err_name(int err) + { + IntPtr ptr = _uv_err_name(err); + return ptr == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(ptr); + } + + protected Func _uv_strerror; + public string strerror(int err) + { + IntPtr ptr = _uv_strerror(err); + return ptr == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(ptr); + } + + protected Func _uv_loop_size; + public int loop_size() + { + return _uv_loop_size(); + } + + protected Func _uv_handle_size; + public int handle_size(HandleType handleType) + { + return _uv_handle_size(handleType); + } + + protected Func _uv_req_size; + public int req_size(RequestType reqType) + { + return _uv_req_size(reqType); + } + + protected delegate int uv_ip4_addr_func(string ip, int port, out SockAddr addr); + protected uv_ip4_addr_func _uv_ip4_addr; + public void ip4_addr(string ip, int port, out SockAddr addr, out Exception error) + { + Check(_uv_ip4_addr(ip, port, out addr), out error); + } + + protected delegate int uv_ip6_addr_func(string ip, int port, out SockAddr addr); + protected uv_ip6_addr_func _uv_ip6_addr; + public void ip6_addr(string ip, int port, out SockAddr addr, out Exception error) + { + Check(_uv_ip6_addr(ip, port, out addr), out error); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_walk_cb(IntPtr handle, IntPtr arg); + protected Func _uv_walk; + public void walk(UvLoopHandle loop, uv_walk_cb walk_cb, IntPtr arg) + { + loop.Validate(); + _uv_walk(loop, walk_cb, arg); + } + + public delegate int uv_tcp_getsockname_func(UvTcpHandle handle, out SockAddr addr, ref int namelen); + protected uv_tcp_getsockname_func _uv_tcp_getsockname; + public void tcp_getsockname(UvTcpHandle handle, out SockAddr addr, ref int namelen) + { + handle.Validate(); + ThrowIfErrored(_uv_tcp_getsockname(handle, out addr, ref namelen)); + } + + public delegate int uv_tcp_getpeername_func(UvTcpHandle handle, out SockAddr addr, ref int namelen); + protected uv_tcp_getpeername_func _uv_tcp_getpeername; + public void tcp_getpeername(UvTcpHandle handle, out SockAddr addr, ref int namelen) + { + handle.Validate(); + ThrowIfErrored(_uv_tcp_getpeername(handle, out addr, ref namelen)); + } + + public uv_buf_t buf_init(IntPtr memory, int len) + { + return new uv_buf_t(memory, len, IsWindows); + } + + public struct uv_buf_t + { + // this type represents a WSABUF struct on Windows + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms741542(v=vs.85).aspx + // and an iovec struct on *nix + // http://man7.org/linux/man-pages/man2/readv.2.html + // because the order of the fields in these structs is different, the field + // names in this type don't have meaningful symbolic names. instead, they are + // assigned in the correct order by the constructor at runtime + + private readonly IntPtr _field0; + private readonly IntPtr _field1; + + public uv_buf_t(IntPtr memory, int len, bool IsWindows) + { + if (IsWindows) + { + _field0 = (IntPtr)len; + _field1 = memory; + } + else + { + _field0 = memory; + _field1 = (IntPtr)len; + } + } + } + + public enum HandleType + { + Unknown = 0, + ASYNC, + CHECK, + FS_EVENT, + FS_POLL, + HANDLE, + IDLE, + NAMED_PIPE, + POLL, + PREPARE, + PROCESS, + STREAM, + TCP, + TIMER, + TTY, + UDP, + SIGNAL, + } + + public enum RequestType + { + Unknown = 0, + REQ, + CONNECT, + WRITE, + SHUTDOWN, + UDP_SEND, + FS, + WORK, + GETADDRINFO, + GETNAMEINFO, + } + + private static class NativeMethods + { + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_loop_init(UvLoopHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_loop_close(IntPtr a0); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_run(UvLoopHandle handle, int mode); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern void uv_stop(UvLoopHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern void uv_ref(UvHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern void uv_unref(UvHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_fileno(UvHandle handle, ref IntPtr socket); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern void uv_close(IntPtr handle, uv_close_cb close_cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_async_init(UvLoopHandle loop, UvAsyncHandle handle, uv_async_cb cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public extern static int uv_async_send(UvAsyncHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl, EntryPoint = "uv_async_send")] + public extern static int uv_unsafe_async_send(IntPtr handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_tcp_init(UvLoopHandle loop, UvTcpHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_tcp_bind(UvTcpHandle handle, ref SockAddr addr, int flags); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_tcp_open(UvTcpHandle handle, IntPtr hSocket); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_tcp_nodelay(UvTcpHandle handle, int enable); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public static extern void uv_tcp_connect(UvConnectRequest req, UvTcpHandle handle, ref SockAddr addr, uv_connect_cb cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_pipe_init(UvLoopHandle loop, UvPipeHandle handle, int ipc); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_pipe_bind(UvPipeHandle loop, string name); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_listen(UvStreamHandle handle, int backlog, uv_connection_cb cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_accept(UvStreamHandle server, UvStreamHandle client); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public static extern void uv_pipe_connect(UvConnectRequest req, UvPipeHandle handle, string name, uv_connect_cb cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public extern static int uv_pipe_pending_count(UvPipeHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public extern static int uv_read_start(UvStreamHandle handle, uv_alloc_cb alloc_cb, uv_read_cb read_cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_read_stop(UvStreamHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_try_write(UvStreamHandle handle, uv_buf_t[] bufs, int nbufs); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + unsafe public static extern int uv_write(UvRequest req, UvStreamHandle handle, uv_buf_t* bufs, int nbufs, uv_write_cb cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + unsafe public static extern int uv_write2(UvRequest req, UvStreamHandle handle, uv_buf_t* bufs, int nbufs, UvStreamHandle sendHandle, uv_write_cb cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_shutdown(UvShutdownReq req, UvStreamHandle handle, uv_shutdown_cb cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr uv_err_name(int err); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr uv_strerror(int err); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_loop_size(); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_handle_size(HandleType handleType); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_req_size(RequestType reqType); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_ip4_addr(string ip, int port, out SockAddr addr); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_ip6_addr(string ip, int port, out SockAddr addr); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_tcp_getsockname(UvTcpHandle handle, out SockAddr name, ref int namelen); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_tcp_getpeername(UvTcpHandle handle, out SockAddr name, ref int namelen); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_walk(UvLoopHandle loop, uv_walk_cb walk_cb, IntPtr arg); + + [DllImport("WS2_32.dll", CallingConvention = CallingConvention.Winapi)] + unsafe public static extern int WSAIoctl( + IntPtr socket, + int dwIoControlCode, + int* lpvInBuffer, + uint cbInBuffer, + int* lpvOutBuffer, + int cbOutBuffer, + out uint lpcbBytesReturned, + IntPtr lpOverlapped, + IntPtr lpCompletionRoutine + ); + + [DllImport("WS2_32.dll", CallingConvention = CallingConvention.Winapi)] + public static extern int WSAGetLastError(); + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/UvAsyncHandle.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvAsyncHandle.cs new file mode 100644 index 00000000000..363c18a6f0e --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvAsyncHandle.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Threading; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + public class UvAsyncHandle : UvHandle + { + private static readonly Uv.uv_close_cb _destroyMemory = (handle) => DestroyMemory(handle); + + private static readonly Uv.uv_async_cb _uv_async_cb = (handle) => AsyncCb(handle); + private Action _callback; + private Action, IntPtr> _queueCloseHandle; + + public UvAsyncHandle() : base() + { + } + + public void Init(UvLoopHandle loop, Action callback, Action, IntPtr> queueCloseHandle) + { + CreateMemory( + loop.Libuv, + loop.ThreadId, + loop.Libuv.handle_size(Uv.HandleType.ASYNC)); + + _callback = callback; + _queueCloseHandle = queueCloseHandle; + _uv.async_init(loop, this, _uv_async_cb); + } + + public void Send() + { + _uv.async_send(this); + } + + private static void AsyncCb(IntPtr handle) + { + FromIntPtr(handle)._callback.Invoke(); + } + + protected override bool ReleaseHandle() + { + var memory = handle; + if (memory != IntPtr.Zero) + { + handle = IntPtr.Zero; + + if (Thread.CurrentThread.ManagedThreadId == ThreadId) + { + _uv.close(memory, _destroyMemory); + } + else if (_queueCloseHandle != null) + { + // This can be called from the finalizer. + // Ensure the closure doesn't reference "this". + var uv = _uv; + _queueCloseHandle(memory2 => uv.close(memory2, _destroyMemory), memory); + uv.unsafe_async_send(memory); + } + else + { + Debug.Assert(false, "UvAsyncHandle not initialized with queueCloseHandle action"); + return false; + } + } + return true; + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/UvConnectRequest.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvConnectRequest.cs new file mode 100644 index 00000000000..a2a42a9ce1d --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvConnectRequest.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + /// + /// Summary description for UvWriteRequest + /// + public class UvConnectRequest : UvRequest + { + private readonly static Uv.uv_connect_cb _uv_connect_cb = (req, status) => UvConnectCb(req, status); + + private Action _callback; + private object _state; + + public UvConnectRequest() : base() + { + } + + public void Init(UvLoopHandle loop) + { + var requestSize = loop.Libuv.req_size(Uv.RequestType.CONNECT); + CreateMemory( + loop.Libuv, + loop.ThreadId, + requestSize); + } + + public void Connect( + UvTcpHandle socket, + IPEndPoint endpoint, + Action callback, + object state) + { + _callback = callback; + _state = state; + + SockAddr addr; + var addressText = endpoint.Address.ToString(); + + Exception error1; + _uv.ip4_addr(addressText, endpoint.Port, out addr, out error1); + + if (error1 != null) + { + Exception error2; + _uv.ip6_addr(addressText, endpoint.Port, out addr, out error2); + if (error2 != null) + { + throw error1; + } + } + + Pin(); + Libuv.tcp_connect(this, socket, ref addr, _uv_connect_cb); + } + + public void Connect( + UvPipeHandle pipe, + string name, + Action callback, + object state) + { + _callback = callback; + _state = state; + + Pin(); + Libuv.pipe_connect(this, pipe, name, _uv_connect_cb); + } + + private static void UvConnectCb(IntPtr ptr, int status) + { + var req = FromIntPtr(ptr); + req.Unpin(); + + var callback = req._callback; + req._callback = null; + + var state = req._state; + req._state = null; + + Exception error = null; + if (status < 0) + { + req.Libuv.Check(status, out error); + } + + callback(req, status, error, state); + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/UvException.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvException.cs new file mode 100644 index 00000000000..6e7dd07a9b4 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvException.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + public class UvException : Exception + { + public UvException(string message, int statusCode) : base(message) + { + StatusCode = statusCode; + } + + public int StatusCode { get; } + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/UvHandle.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvHandle.cs new file mode 100644 index 00000000000..4cf701b8f50 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvHandle.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Threading; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + public abstract class UvHandle : UvMemory + { + private static readonly Uv.uv_close_cb _destroyMemory = (handle) => DestroyMemory(handle); + private Action, IntPtr> _queueCloseHandle; + + protected UvHandle() : base () + { + } + + protected void CreateHandle( + Uv uv, + int threadId, + int size, + Action, IntPtr> queueCloseHandle) + { + _queueCloseHandle = queueCloseHandle; + CreateMemory(uv, threadId, size); + } + + protected override bool ReleaseHandle() + { + var memory = handle; + if (memory != IntPtr.Zero) + { + handle = IntPtr.Zero; + + if (Thread.CurrentThread.ManagedThreadId == ThreadId) + { + _uv.close(memory, _destroyMemory); + } + else if (_queueCloseHandle != null) + { + // This can be called from the finalizer. + // Ensure the closure doesn't reference "this". + var uv = _uv; + _queueCloseHandle(memory2 => uv.close(memory2, _destroyMemory), memory); + } + else + { + Debug.Assert(false, "UvHandle not initialized with queueCloseHandle action"); + return false; + } + } + return true; + } + + public void Reference() + { + _uv.@ref(this); + } + + public void Unreference() + { + _uv.unref(this); + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/UvLoopHandle.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvLoopHandle.cs new file mode 100644 index 00000000000..4820b93ca1e --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvLoopHandle.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + public class UvLoopHandle : UvMemory + { + public UvLoopHandle() : base() + { + } + + public void Init(Uv uv) + { + CreateMemory( + uv, + Thread.CurrentThread.ManagedThreadId, + uv.loop_size()); + + _uv.loop_init(this); + } + + public void Run(int mode = 0) + { + _uv.run(this, mode); + } + + public void Stop() + { + _uv.stop(this); + } + + unsafe protected override bool ReleaseHandle() + { + var memory = handle; + if (memory != IntPtr.Zero) + { + // loop_close clears the gcHandlePtr + var gcHandlePtr = *(IntPtr*)memory; + + _uv.loop_close(this); + handle = IntPtr.Zero; + + DestroyMemory(memory, gcHandlePtr); + } + + return true; + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/UvMemory.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvMemory.cs new file mode 100644 index 00000000000..4a86694e822 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvMemory.cs @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + /// + /// Summary description for UvMemory + /// + public abstract class UvMemory : SafeHandle + { + protected Uv _uv; + protected int _threadId; + + protected UvMemory() : base(IntPtr.Zero, true) + { + } + + public Uv Libuv => _uv; + + public override bool IsInvalid + { + get + { + return handle == IntPtr.Zero; + } + } + + public int ThreadId + { + get + { + return _threadId; + } + private set + { + _threadId = value; + } + } + + unsafe protected void CreateMemory(Uv uv, int threadId, int size) + { + _uv = uv; + ThreadId = threadId; + + handle = Marshal.AllocCoTaskMem(size); + *(IntPtr*)handle = GCHandle.ToIntPtr(GCHandle.Alloc(this, GCHandleType.Weak)); + } + + unsafe protected static void DestroyMemory(IntPtr memory) + { + var gcHandlePtr = *(IntPtr*)memory; + DestroyMemory(memory, gcHandlePtr); + } + + protected static void DestroyMemory(IntPtr memory, IntPtr gcHandlePtr) + { + if (gcHandlePtr != IntPtr.Zero) + { + var gcHandle = GCHandle.FromIntPtr(gcHandlePtr); + gcHandle.Free(); + } + Marshal.FreeCoTaskMem(memory); + } + + internal IntPtr InternalGetHandle() + { + return handle; + } + + public void Validate(bool closed = false) + { + Debug.Assert(closed || !IsClosed, "Handle is closed"); + Debug.Assert(!IsInvalid, "Handle is invalid"); + Debug.Assert(_threadId == Thread.CurrentThread.ManagedThreadId, "ThreadId is incorrect"); + } + + unsafe public static THandle FromIntPtr(IntPtr handle) + { + GCHandle gcHandle = GCHandle.FromIntPtr(*(IntPtr*)handle); + return (THandle)gcHandle.Target; + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/UvPipeHandle.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvPipeHandle.cs new file mode 100644 index 00000000000..24c7ae66cc0 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvPipeHandle.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + public class UvPipeHandle : UvStreamHandle + { + public UvPipeHandle() : base() + { + } + + public void Init(UvLoopHandle loop, Action, IntPtr> queueCloseHandle, bool ipc = false) + { + CreateHandle( + loop.Libuv, + loop.ThreadId, + loop.Libuv.handle_size(Uv.HandleType.NAMED_PIPE), queueCloseHandle); + + _uv.pipe_init(loop, this, ipc); + } + + public void Bind(string name) + { + _uv.pipe_bind(this, name); + } + + public int PendingCount() + { + return _uv.pipe_pending_count(this); + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/UvRequest.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvRequest.cs new file mode 100644 index 00000000000..4db50707515 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvRequest.cs @@ -0,0 +1,32 @@ +using System; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + public class UvRequest : UvMemory + { + private GCHandle _pin; + + protected UvRequest() : base () + { + } + + protected override bool ReleaseHandle() + { + DestroyMemory(handle); + handle = IntPtr.Zero; + return true; + } + + public virtual void Pin() + { + _pin = GCHandle.Alloc(this, GCHandleType.Normal); + } + + public virtual void Unpin() + { + _pin.Free(); + } + } +} + diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/UvShutdownReq.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvShutdownReq.cs new file mode 100644 index 00000000000..bdaf7c492cd --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvShutdownReq.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + /// + /// Summary description for UvShutdownRequest + /// + public class UvShutdownReq : UvRequest + { + private readonly static Uv.uv_shutdown_cb _uv_shutdown_cb = UvShutdownCb; + + private Action _callback; + private object _state; + + public UvShutdownReq() : base () + { + } + + public void Init(UvLoopHandle loop) + { + CreateMemory( + loop.Libuv, + loop.ThreadId, + loop.Libuv.req_size(Uv.RequestType.SHUTDOWN)); + } + + public void Shutdown(UvStreamHandle handle, Action callback, object state) + { + _callback = callback; + _state = state; + Pin(); + _uv.shutdown(this, handle, _uv_shutdown_cb); + } + + private static void UvShutdownCb(IntPtr ptr, int status) + { + var req = FromIntPtr(ptr); + req.Unpin(); + req._callback(req, status, req._state); + req._callback = null; + req._state = null; + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/UvStreamHandle.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvStreamHandle.cs new file mode 100644 index 00000000000..ad192a28afa --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvStreamHandle.cs @@ -0,0 +1,151 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + public abstract class UvStreamHandle : UvHandle + { + private readonly static Uv.uv_connection_cb _uv_connection_cb = (handle, status) => UvConnectionCb(handle, status); + // Ref and out lamda params must be explicitly typed + private readonly static Uv.uv_alloc_cb _uv_alloc_cb = (IntPtr handle, int suggested_size, out Uv.uv_buf_t buf) => UvAllocCb(handle, suggested_size, out buf); + private readonly static Uv.uv_read_cb _uv_read_cb = (IntPtr handle, int status, ref Uv.uv_buf_t buf) => UvReadCb(handle, status, ref buf); + + private Action _listenCallback; + private object _listenState; + private GCHandle _listenVitality; + + private Func _allocCallback; + private Action _readCallback; + private object _readState; + private GCHandle _readVitality; + + protected UvStreamHandle() : base() + { + } + + protected override bool ReleaseHandle() + { + if (_listenVitality.IsAllocated) + { + _listenVitality.Free(); + } + if (_readVitality.IsAllocated) + { + _readVitality.Free(); + } + return base.ReleaseHandle(); + } + + public void Listen(int backlog, Action callback, object state) + { + if (_listenVitality.IsAllocated) + { + throw new InvalidOperationException("TODO: Listen may not be called more than once"); + } + try + { + _listenCallback = callback; + _listenState = state; + _listenVitality = GCHandle.Alloc(this, GCHandleType.Normal); + _uv.listen(this, backlog, _uv_connection_cb); + } + catch + { + _listenCallback = null; + _listenState = null; + if (_listenVitality.IsAllocated) + { + _listenVitality.Free(); + } + throw; + } + } + + public void Accept(UvStreamHandle handle) + { + _uv.accept(this, handle); + } + + public void ReadStart( + Func allocCallback, + Action readCallback, + object state) + { + if (_readVitality.IsAllocated) + { + throw new InvalidOperationException("TODO: ReadStop must be called before ReadStart may be called again"); + } + + try + { + _allocCallback = allocCallback; + _readCallback = readCallback; + _readState = state; + _readVitality = GCHandle.Alloc(this, GCHandleType.Normal); + _uv.read_start(this, _uv_alloc_cb, _uv_read_cb); + } + catch + { + _allocCallback = null; + _readCallback = null; + _readState = null; + if (_readVitality.IsAllocated) + { + _readVitality.Free(); + } + throw; + } + } + + // UvStreamHandle.ReadStop() should be idempotent to match uv_read_stop() + public void ReadStop() + { + if (_readVitality.IsAllocated) + { + _readVitality.Free(); + } + _allocCallback = null; + _readCallback = null; + _readState = null; + _uv.read_stop(this); + } + + public int TryWrite(Uv.uv_buf_t buf) + { + return _uv.try_write(this, new[] { buf }, 1); + } + + private static void UvConnectionCb(IntPtr handle, int status) + { + var stream = FromIntPtr(handle); + + Exception error; + stream.Libuv.Check(status, out error); + stream._listenCallback(stream, status, error, stream._listenState); + } + + private static void UvAllocCb(IntPtr handle, int suggested_size, out Uv.uv_buf_t buf) + { + var stream = FromIntPtr(handle); + try + { + buf = stream._allocCallback(stream, suggested_size, stream._readState); + } + catch + { + buf = stream.Libuv.buf_init(IntPtr.Zero, 0); + throw; + } + } + + private static void UvReadCb(IntPtr handle, int status, ref Uv.uv_buf_t buf) + { + var stream = FromIntPtr(handle); + stream._readCallback(stream, status, stream._readState); + } + + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/UvTcpHandle.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvTcpHandle.cs new file mode 100644 index 00000000000..a49e896b96d --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvTcpHandle.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + public class UvTcpHandle : UvStreamHandle + { + public UvTcpHandle() : base() + { + } + + public void Init(UvLoopHandle loop, Action, IntPtr> queueCloseHandle) + { + CreateHandle( + loop.Libuv, + loop.ThreadId, + loop.Libuv.handle_size(Uv.HandleType.TCP), queueCloseHandle); + + _uv.tcp_init(loop, this); + } + + public void Bind(IPEndPoint endpoint) + { + SockAddr addr; + var addressText = endpoint.Address.ToString(); + + Exception error1; + _uv.ip4_addr(addressText, endpoint.Port, out addr, out error1); + + if (error1 != null) + { + Exception error2; + _uv.ip6_addr(addressText, endpoint.Port, out addr, out error2); + if (error2 != null) + { + throw error1; + } + } + + _uv.tcp_bind(this, ref addr, 0); + } + + public IPEndPoint GetPeerIPEndPoint() + { + SockAddr socketAddress; + int namelen = Marshal.SizeOf(); + _uv.tcp_getpeername(this, out socketAddress, ref namelen); + + return socketAddress.GetIPEndPoint(); + } + + public IPEndPoint GetSockIPEndPoint() + { + SockAddr socketAddress; + int namelen = Marshal.SizeOf(); + _uv.tcp_getsockname(this, out socketAddress, ref namelen); + + return socketAddress.GetIPEndPoint(); + } + + public void Open(IntPtr hSocket) + { + _uv.tcp_open(this, hSocket); + } + + public void NoDelay(bool enable) + { + _uv.tcp_nodelay(this, enable); + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/Interop/UvWriteReq.cs b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvWriteReq.cs new file mode 100644 index 00000000000..5d5f106f79a --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Interop/UvWriteReq.cs @@ -0,0 +1,148 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Libuv.Interop +{ + /// + /// Summary description for UvWriteRequest + /// + public class UvWriteReq : UvRequest + { + private readonly static Uv.uv_write_cb _uv_write_cb = (IntPtr ptr, int status) => UvWriteCb(ptr, status); + + private IntPtr _bufs; + + private Action _callback; + private PreservedBuffer _buffer; + private object _state; + private const int BUFFER_COUNT = 4; + + private List _pins = new List(BUFFER_COUNT + 1); + + public UvWriteReq() : base() + { + } + + public void Init(UvLoopHandle loop) + { + var requestSize = loop.Libuv.req_size(Uv.RequestType.WRITE); + var bufferSize = Marshal.SizeOf() * BUFFER_COUNT; + CreateMemory( + loop.Libuv, + loop.ThreadId, + requestSize + bufferSize); + _bufs = handle + requestSize; + } + + public unsafe void Write( + UvStreamHandle handle, + ReadableBuffer buffer, + Action callback, + object state) + { + try + { + // Preserve the buffer for the async call + _buffer = buffer.Preserve(); + buffer = _buffer.Buffer; + + int nBuffers = 0; + if (buffer.IsSingleSpan) + { + nBuffers = 1; + } + else + { + foreach (var span in buffer) + { + nBuffers++; + } + } + + // add GCHandle to keeps this SafeHandle alive while request processing + _pins.Add(GCHandle.Alloc(this, GCHandleType.Normal)); + + var pBuffers = (Uv.uv_buf_t*)_bufs; + if (nBuffers > BUFFER_COUNT) + { + // create and pin buffer array when it's larger than the pre-allocated one + var bufArray = new Uv.uv_buf_t[nBuffers]; + var gcHandle = GCHandle.Alloc(bufArray, GCHandleType.Pinned); + _pins.Add(gcHandle); + pBuffers = (Uv.uv_buf_t*)gcHandle.AddrOfPinnedObject(); + } + + if (nBuffers == 1) + { + var memory = buffer.First; + void* pointer; + if (memory.TryGetPointer(out pointer)) + { + pBuffers[0] = Libuv.buf_init((IntPtr)pointer, memory.Length); + } + else + { + throw new InvalidOperationException("Memory needs to be pinned"); + } + } + else + { + int i = 0; + void* pointer; + foreach (var memory in buffer) + { + if (memory.TryGetPointer(out pointer)) + { + pBuffers[i++] = Libuv.buf_init((IntPtr)pointer, memory.Length); + } + else + { + throw new InvalidOperationException("Memory needs to be pinned"); + } + } + } + + _callback = callback; + _state = state; + _uv.write(this, handle, pBuffers, nBuffers, _uv_write_cb); + } + catch + { + _callback = null; + _state = null; + _buffer.Dispose(); + Unpin(this); + throw; + } + } + + private static void Unpin(UvWriteReq req) + { + foreach (var pin in req._pins) + { + pin.Free(); + } + req._pins.Clear(); + } + + private static void UvWriteCb(IntPtr ptr, int status) + { + var req = FromIntPtr(ptr); + Unpin(req); + + req._buffer.Dispose(); + + var callback = req._callback; + req._callback = null; + + var state = req._state; + req._state = null; + + callback(req, status, state); + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Libuv/Properties/AssemblyInfo.cs b/src/System.IO.Pipelines.Networking.Libuv/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..bc6a91d2c9b --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("System.IO.Pipelines.Networking.Libuv")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("11d6b886-a303-4d13-bca8-a917d5740fb6")] diff --git a/src/System.IO.Pipelines.Networking.Libuv/System.IO.Pipelines.Networking.Libuv.xproj b/src/System.IO.Pipelines.Networking.Libuv/System.IO.Pipelines.Networking.Libuv.xproj new file mode 100644 index 00000000000..20b4d386dd1 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/System.IO.Pipelines.Networking.Libuv.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 5a7899eb-9366-477c-a0bc-a813d4211e78 + System.IO.Pipelines.Networking.Libuv + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Libuv/UvTcpClient.cs b/src/System.IO.Pipelines.Networking.Libuv/UvTcpClient.cs new file mode 100644 index 00000000000..e3bd1a181ab --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/UvTcpClient.cs @@ -0,0 +1,56 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using System.IO.Pipelines.Networking.Libuv.Interop; + +namespace System.IO.Pipelines.Networking.Libuv +{ + public class UvTcpClient + { + private static readonly Action _connectCallback = OnConnection; + private static readonly Action _startConnect = state => ((UvTcpClient)state).DoConnect(); + + private readonly TaskCompletionSource _connectTcs = new TaskCompletionSource(); + private readonly IPEndPoint _ipEndPoint; + private readonly UvThread _thread; + + private UvTcpHandle _connectSocket; + + public UvTcpClient(UvThread thread, IPEndPoint endPoint) + { + _thread = thread; + _ipEndPoint = endPoint; + } + + public async Task ConnectAsync() + { + _thread.Post(_startConnect, this); + + var connection = await _connectTcs.Task; + + // Get back onto the current context + await Task.Yield(); + + return connection; + } + + private void DoConnect() + { + _connectSocket = new UvTcpHandle(); + _connectSocket.Init(_thread.Loop, null); + + var connectReq = new UvConnectRequest(); + connectReq.Init(_thread.Loop); + connectReq.Connect(_connectSocket, _ipEndPoint, _connectCallback, this); + } + + private static void OnConnection(UvConnectRequest req, int status, Exception exception, object state) + { + var client = (UvTcpClient)state; + + var connection = new UvTcpConnection(client._thread, client._connectSocket); + + client._connectTcs.TrySetResult(connection); + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/UvTcpConnection.cs b/src/System.IO.Pipelines.Networking.Libuv/UvTcpConnection.cs new file mode 100644 index 00000000000..80827a6a834 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/UvTcpConnection.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.IO.Pipelines.Networking.Libuv.Interop; + +namespace System.IO.Pipelines.Networking.Libuv +{ + public class UvTcpConnection : IPipelineConnection + { + private const int EOF = -4095; + + private static readonly Action _readCallback = ReadCallback; + private static readonly Func _allocCallback = AllocCallback; + private static readonly Action _writeCallback = WriteCallback; + + protected readonly PipelineReaderWriter _input; + protected readonly PipelineReaderWriter _output; + private readonly UvThread _thread; + private readonly UvTcpHandle _handle; + + private int _pendingWrites; + + private TaskCompletionSource _drainWrites; + private Task _sendingTask; + private WritableBuffer _inputBuffer; + + public UvTcpConnection(UvThread thread, UvTcpHandle handle) + { + _thread = thread; + _handle = handle; + + _input = _thread.PipelineFactory.Create(); + _output = _thread.PipelineFactory.Create(); + + StartReading(); + _sendingTask = ProcessWrites(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + protected virtual void Dispose(bool disposing) + { + _output.CompleteWriter(); + _output.CompleteReader(); + + _input.CompleteWriter(); + _input.CompleteReader(); + } + + public IPipelineWriter Output => _output; + + public IPipelineReader Input => _input; + + private async Task ProcessWrites() + { + try + { + while (true) + { + var result = await _output.ReadAsync(); + var buffer = result.Buffer; + + try + { + // Make sure we're on the libuv thread + await _thread; + + if (buffer.IsEmpty && result.IsCompleted) + { + break; + } + + if (!buffer.IsEmpty) + { + BeginWrite(buffer); + } + } + finally + { + _output.Advance(buffer.End); + } + } + } + catch (Exception ex) + { + _output.CompleteReader(ex); + } + finally + { + _output.CompleteReader(); + + // Drain the pending writes + if (_pendingWrites > 0) + { + _drainWrites = new TaskCompletionSource(); + + await _drainWrites.Task; + } + + _handle.Dispose(); + + // We'll never call the callback after disposing the handle + _input.CompleteWriter(); + } + } + + private void BeginWrite(ReadableBuffer buffer) + { + var writeReq = _thread.WriteReqPool.Allocate(); + + _pendingWrites++; + + writeReq.Write(_handle, buffer, _writeCallback, this); + } + + private static void WriteCallback(UvWriteReq writeReq, int status, object state) + { + ((UvTcpConnection)state).EndWrite(writeReq); + } + + private void EndWrite(UvWriteReq writeReq) + { + _pendingWrites--; + + _thread.WriteReqPool.Return(writeReq); + + if (_drainWrites != null) + { + if (_pendingWrites == 0) + { + _drainWrites.TrySetResult(null); + } + } + } + + private void StartReading() + { + _handle.ReadStart(_allocCallback, _readCallback, this); + } + + private static void ReadCallback(UvStreamHandle handle, int status, object state) + { + ((UvTcpConnection)state).OnRead(handle, status); + } + + private void OnRead(UvStreamHandle handle, int status) + { + if (status == 0) + { + // A zero status does not indicate an error or connection end. It indicates + // there is no data to be read right now. + // See the note at http://docs.libuv.org/en/v1.x/stream.html#c.uv_read_cb. + _inputBuffer.Commit(); + return; + } + + var normalRead = status > 0; + var normalDone = status == EOF; + var errorDone = !(normalDone || normalRead); + var readCount = normalRead ? status : 0; + + if (!normalRead) + { + handle.ReadStop(); + } + + IOException error = null; + if (errorDone) + { + Exception uvError; + handle.Libuv.Check(status, out uvError); + error = new IOException(uvError.Message, uvError); + + // REVIEW: Should we treat ECONNRESET as an error? + // Ignore the error for now + _input.CompleteWriter(); + } + else if (readCount == 0 || _input.Writing.IsCompleted) + { + _input.CompleteWriter(); + } + else + { + _inputBuffer.Advance(readCount); + + var task = _inputBuffer.FlushAsync(); + + if (!task.IsCompleted) + { + // If there's back pressure + handle.ReadStop(); + + // Resume reading when task continues + task.ContinueWith((t, state) => ((UvTcpConnection)state).StartReading(), this); + } + } + } + + private static Uv.uv_buf_t AllocCallback(UvStreamHandle handle, int status, object state) + { + return ((UvTcpConnection)state).OnAlloc(handle, status); + } + + private unsafe Uv.uv_buf_t OnAlloc(UvStreamHandle handle, int status) + { + _inputBuffer = _input.Alloc(2048); + + void* pointer; + if (!_inputBuffer.Memory.TryGetPointer(out pointer)) + { + throw new InvalidOperationException("Pointer must be pinned"); + } + + return handle.Libuv.buf_init((IntPtr)pointer, _inputBuffer.Memory.Length); + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/UvTcpListener.cs b/src/System.IO.Pipelines.Networking.Libuv/UvTcpListener.cs new file mode 100644 index 00000000000..31dd0c28132 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/UvTcpListener.cs @@ -0,0 +1,102 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using System.IO.Pipelines.Networking.Libuv.Interop; + +namespace System.IO.Pipelines.Networking.Libuv +{ + public class UvTcpListener : IDisposable + { + private static Action _onConnectionCallback = OnConnectionCallback; + private static Action _startListeningCallback = state => ((UvTcpListener)state).Listen(); + private static Action _stopListeningCallback = state => ((UvTcpListener)state).Shutdown(); + + private readonly IPEndPoint _endpoint; + private readonly UvThread _thread; + + private UvTcpHandle _listenSocket; + private Func _callback; + + private TaskCompletionSource _startedTcs = new TaskCompletionSource(); + + public UvTcpListener(UvThread thread, IPEndPoint endpoint) + { + _thread = thread; + _endpoint = endpoint; + } + + public void OnConnection(Func callback) + { + _callback = callback; + } + + public Task StartAsync() + { + // TODO: Make idempotent + _thread.Post(_startListeningCallback, this); + + return _startedTcs.Task; + } + + public void Dispose() + { + // TODO: Make idempotent + _thread.Post(_stopListeningCallback, this); + } + + private void Shutdown() + { + _listenSocket.Dispose(); + } + + private void Listen() + { + // TODO: Error handling + _listenSocket = new UvTcpHandle(); + _listenSocket.Init(_thread.Loop, null); + _listenSocket.NoDelay(true); + _listenSocket.Bind(_endpoint); + _listenSocket.Listen(10, _onConnectionCallback, this); + + // Don't complete the task on the UV thread + Task.Run(() => _startedTcs.TrySetResult(null)); + } + + private static void OnConnectionCallback(UvStreamHandle listenSocket, int status, Exception error, object state) + { + var listener = (UvTcpListener)state; + + var acceptSocket = new UvTcpHandle(); + + try + { + acceptSocket.Init(listener._thread.Loop, null); + acceptSocket.NoDelay(true); + listenSocket.Accept(acceptSocket); + var connection = new UvTcpConnection(listener._thread, acceptSocket); + ExecuteCallback(listener, connection); + } + catch (UvException) + { + acceptSocket.Dispose(); + } + } + + private static async void ExecuteCallback(UvTcpListener listener, UvTcpConnection connection) + { + try + { + await listener._callback?.Invoke(connection); + } + catch + { + // Swallow exceptions + } + finally + { + // Dispose the connection on task completion + connection.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Libuv/UvThread.cs b/src/System.IO.Pipelines.Networking.Libuv/UvThread.cs new file mode 100644 index 00000000000..3368e5b477c --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/UvThread.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.IO.Pipelines.Networking.Libuv.Interop; +using System.IO.Pipelines.Networking.Libuv.Internal; + +namespace System.IO.Pipelines.Networking.Libuv +{ + // This class needs a bunch of work to make sure it's thread safe + public class UvThread : ICriticalNotifyCompletion, IDisposable + { + private readonly Thread _thread = new Thread(OnStart) + { + Name = "Libuv event loop" + }; + private readonly ManualResetEventSlim _running = new ManualResetEventSlim(); + private readonly WorkQueue _workQueue = new WorkQueue(); + + private bool _stopping; + private UvAsyncHandle _postHandle; + + public UvThread() + { + WriteReqPool = new WriteReqPool(this); + } + + public Uv Uv { get; private set; } + + public UvLoopHandle Loop { get; private set; } + + public PipelineFactory PipelineFactory { get; } = new PipelineFactory(); + + public WriteReqPool WriteReqPool { get; } + + public void Post(Action callback, object state) + { + if (_stopping) + { + return; + } + + EnsureStarted(); + + var work = new Work + { + Callback = callback, + State = state + }; + + _workQueue.Add(work); + + _postHandle.Send(); + } + + // Awaiter impl + public bool IsCompleted => Thread.CurrentThread.ManagedThreadId == _thread.ManagedThreadId; + + public UvThread GetAwaiter() => this; + + public void GetResult() + { + + } + + private static void OnStart(object state) + { + ((UvThread)state).RunLoop(); + } + + private void RunLoop() + { + Uv = new Uv(); + + Loop = new UvLoopHandle(); + Loop.Init(Uv); + + _postHandle = new UvAsyncHandle(); + _postHandle.Init(Loop, OnPost, null); + + _running.Set(); + + Uv.run(Loop, 0); + + _postHandle.Reference(); + _postHandle.Dispose(); + + Uv.run(Loop, 0); + + Loop.Dispose(); + } + + private void OnPost() + { + foreach (var work in _workQueue.DequeAll()) + { + work.Callback(work.State); + } + + if (_stopping) + { + WriteReqPool.Dispose(); + + _postHandle.Unreference(); + } + } + + private void EnsureStarted() + { + if (!_running.IsSet) + { + _thread.Start(this); + + _running.Wait(); + } + } + + private void Stop() + { + if (!_stopping) + { + _stopping = true; + + _postHandle.Send(); + + _thread.Join(); + + // REVIEW: Can you restart the thread? + } + } + + public void UnsafeOnCompleted(Action continuation) + { + OnCompleted(continuation); + } + + public void OnCompleted(Action continuation) + { + Post(state => ((Action)state)(), continuation); + } + + public void Dispose() + { + Stop(); + + PipelineFactory.Dispose(); + } + + private struct Work + { + public object State; + public Action Callback; + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Libuv/project.json b/src/System.IO.Pipelines.Networking.Libuv/project.json new file mode 100644 index 00000000000..90bef2ba34c --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Libuv/project.json @@ -0,0 +1,33 @@ +{ + "version": "0.1.0-*", + "description": "Networking implementation of pipelines based on Libuv", + "copyright": "Microsoft Corporation, All rights reserved", + "packOptions": { + "releaseNotes": "Pre-release package, for testing only", + "licenseUrl": "http://go.microsoft.com/fwlink/?LinkId=329770", + "iconUrl": "http://go.microsoft.com/fwlink/?LinkID=288859", + "projectUrl": "https://github.com/dotnet/corefxlab", + "requireLicenseAcceptance": true + }, + "buildOptions": { + "allowUnsafe": true, + "keyFile": "../../tools/Key.snk" + }, + + "dependencies": { + "NETStandard.Library": "1.6.0", + "System.IO.Pipelines": { + "target": "project" + }, + "Libuv": "1.9.0" + }, + + "frameworks": { + "net451": {}, + "netstandard1.3": { + "dependencies": { + "System.Threading.Thread": "4.0.0" + } + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Sockets/Internal/ContinuationMode.cs b/src/System.IO.Pipelines.Networking.Sockets/Internal/ContinuationMode.cs new file mode 100644 index 00000000000..81de8f9a485 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Sockets/Internal/ContinuationMode.cs @@ -0,0 +1,12 @@ +namespace System.IO.Pipelines.Networking.Sockets.Internal +{ + /// + /// Used by Signal to control how callbacks are invoked + /// + internal enum ContinuationMode + { + Synchronous, + ThreadPool, + // TODO: sync-context? but if so: whose? the .Current at creation? at SetResult? + } +} diff --git a/src/System.IO.Pipelines.Networking.Sockets/Internal/MicroBufferPool.cs b/src/System.IO.Pipelines.Networking.Sockets/Internal/MicroBufferPool.cs new file mode 100644 index 00000000000..1d600b6b10b --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Sockets/Internal/MicroBufferPool.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; + +namespace System.IO.Pipelines.Networking.Sockets.Internal +{ + /// + /// Simple pool over a byte[] that returns segments, using a queue to + /// handle recycling of segments. + /// + internal class MicroBufferPool + { + private readonly byte[] _buffer; + private readonly Queue _recycled; + private ushort _next; + private readonly ushort _count; + private readonly int _bytesPerItem; + + public int BytesPerItem => _bytesPerItem; + + public int Available + { + get + { + lock (_recycled) + { + return (_count - _next) + _recycled.Count; + } + } + } + + public int InUse + { + get + { + lock (_recycled) + { + return _next - _recycled.Count; + } + } + } + + public MicroBufferPool(int bytesPerItem, int count) + { + if (count <= 0 || count > ushort.MaxValue) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (bytesPerItem <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bytesPerItem)); + } + _buffer = new byte[(ulong)bytesPerItem * (ulong)count]; + _next = 0; + _count = (ushort)count; + _bytesPerItem = bytesPerItem; + _recycled = new Queue(); + } + + public bool TryTake(out ArraySegment segment) + { + int index; + lock (_recycled) + { + if (_recycled.Count != 0) + { + index = _recycled.Dequeue(); + } + else if (_next < _count) + { + index = _next++; + } + else + { + segment = default(ArraySegment); + return false; + } + } + + segment = new ArraySegment(_buffer, index * _bytesPerItem, _bytesPerItem); + return true; + } + + public void Recycle(ArraySegment segment) + { + // only put it back if it is a buffer we might have issued - + // needs same array and count, aligned by count, and not out-of-range + int index; + if (segment.Array == _buffer && segment.Count == _bytesPerItem + && (segment.Offset % _bytesPerItem) == 0 + && (index = segment.Offset / _bytesPerItem) >= 0 + && index < _count) + { + lock (_recycled) + { + _recycled.Enqueue((ushort)index); + } + } + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Sockets/Internal/Signal.cs b/src/System.IO.Pipelines.Networking.Sockets/Internal/Signal.cs new file mode 100644 index 00000000000..c7bb6dc99d8 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Sockets/Internal/Signal.cs @@ -0,0 +1,87 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace System.IO.Pipelines.Networking.Sockets.Internal +{ + /// + /// Very lightweight awaitable gate - intended for use in high-volume single-producer/single-consumer + /// scenario, in particular targeting the bridge between async IO operations + /// and the async method that is pumping the read/write queue. A key consideration is that + /// no objects (in particular Task/TaskCompletionSource) are allocated even in the await case. Instead, + /// a custom awaiter is provided. Works like a - the method must + /// be called between operations. + /// + internal class Signal : ICriticalNotifyCompletion + { + private readonly ContinuationMode _continuationMode; + + private Action _continuation; + private static readonly Action _completedSentinel = delegate { }; + + public Signal(ContinuationMode continuationMode = ContinuationMode.Synchronous) + { + _continuationMode = continuationMode; + } + + public bool IsCompleted => ReferenceEquals(_completedSentinel, Volatile.Read(ref _continuation)); + + private object SyncLock => this; + + public Signal GetAwaiter() => this; + + public void GetResult() { } + + public void UnsafeOnCompleted(Action continuation) => OnCompleted(continuation); + + public void OnCompleted(Action continuation) + { + if (continuation != null) + { + var oldValue = Interlocked.CompareExchange(ref _continuation, continuation, null); + + if (ReferenceEquals(oldValue, _completedSentinel)) + { + // already complete; calback sync + continuation.Invoke(); + } + else if (oldValue != null) + { + ThrowMultipleCallbacksNotSupported(); + } + } + } + private static void ThrowMultipleCallbacksNotSupported() + { + throw new NotSupportedException("Multiple callbacks via Signal.OnCompleted are not supported"); + } + + + public void Reset() + { + Volatile.Write(ref _continuation, null); + } + + public void Set() + { + Action continuation = Interlocked.Exchange(ref _continuation, _completedSentinel); + + if (continuation != null && !ReferenceEquals(continuation, _completedSentinel)) + { + switch (_continuationMode) + { + case ContinuationMode.Synchronous: + continuation.Invoke(); + break; + case ContinuationMode.ThreadPool: + ThreadPool.QueueUserWorkItem(state => ((Action)state).Invoke(), continuation); + break; + } + } + } + + // utility method for people who don't feel comfortable with `await obj;` and prefer `await obj.WaitAsync();` + internal Signal WaitAsync() => this; + } + +} diff --git a/src/System.IO.Pipelines.Networking.Sockets/Internal/SocketExtensions.cs b/src/System.IO.Pipelines.Networking.Sockets/Internal/SocketExtensions.cs new file mode 100644 index 00000000000..1c38ec0c734 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Sockets/Internal/SocketExtensions.cs @@ -0,0 +1,37 @@ +using System.Net.Sockets; + +namespace System.IO.Pipelines.Networking.Sockets.Internal +{ + internal static class SocketExtensions + { + /// + /// Note that this presumes that args.UserToken is already a Signal, and that args.Completed + /// knows to call .Set on the Signal + /// + public static Signal ReceiveSignalAsync(this Socket socket, SocketAsyncEventArgs args) + { + var signal = (Signal)args.UserToken; + signal.Reset(); + if (!socket.ReceiveAsync(args)) + { // mark it as already complete (probably an error) + signal.Set(); + } + return signal; + } + + /// + /// Note that this presumes that args.UserToken is already a Signal, and that args.Completed + /// knows to call .Set on the Signal + /// + public static Signal SendSignalAsync(this Socket socket, SocketAsyncEventArgs args) + { + var signal = (Signal)args.UserToken; + signal.Reset(); + if (!socket.SendAsync(args)) + { // mark it as already complete (probably an error) + signal.Set(); + } + return signal; + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Sockets/Properties/AssemblyInfo.cs b/src/System.IO.Pipelines.Networking.Sockets/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..1b0aa031746 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Sockets/Properties/AssemblyInfo.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("System.IO.Pipelines.Networking.Sockets")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1932e4b5-a40e-4eef-ad8f-012dc01f5e5f")] + +[assembly: InternalsVisibleTo("System.IO.Pipelines.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100F33A29044FA9D740C9B3213A93E57C84B472C84E0B8A0E1AE48E67A9F8F6DE9D5F7F3D52AC23E48AC51801F1DC950ABE901DA34D2A9E3BAADB141A17C77EF3C565DD5EE5054B91CF63BB3C6AB83F72AB3AAFE93D0FC3C2348B764FAFB0B1C0733DE51459AEAB46580384BF9D74C4E28164B7CDE247F891BA07891C9D872AD2BB")] \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Sockets/SocketConnection.cs b/src/System.IO.Pipelines.Networking.Sockets/SocketConnection.cs new file mode 100644 index 00000000000..42468a52e02 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Sockets/SocketConnection.cs @@ -0,0 +1,615 @@ +using System.IO.Pipelines.Networking.Sockets.Internal; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.Networking.Sockets +{ + /// + /// Represents an implementation using the async Socket API + /// + public class SocketConnection : IPipelineConnection + { + private static readonly EventHandler _asyncCompleted = OnAsyncCompleted; + + private static readonly Queue _argsPool = new Queue(); + + private static readonly MicroBufferPool _smallBuffers; + + internal static int SmallBuffersInUse = _smallBuffers?.InUse ?? 0; + + const int SmallBufferSize = 8; + + // track the state of which strategy to use; need to use a known-safe + // strategy until we can decide which to use (by observing behavior) + private static BufferStyle _bufferStyle; + private static bool _seenReceiveZeroWithAvailable, _seenReceiveZeroWithEOF; + + private static readonly byte[] _zeroLengthBuffer = new byte[0]; + + + private readonly bool _ownsFactory; + private PipelineFactory _factory; + private PipelineReaderWriter _input, _output; + private Socket _socket; + + static SocketConnection() + { + + // validated styles for known OSes + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // zero-length receive works fine + _bufferStyle = BufferStyle.UseZeroLengthBuffer; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + // zero-length receive is unreliable + _bufferStyle = BufferStyle.UseSmallBuffer; + } + else + { + // default to "figure it out based on what happens" + _bufferStyle = BufferStyle.Unknown; + } + + if (_bufferStyle != BufferStyle.UseZeroLengthBuffer) + { + // we're going to need to use small buffers for awaiting input + _smallBuffers = new MicroBufferPool(SmallBufferSize, ushort.MaxValue); + } + } + + internal SocketConnection(Socket socket, PipelineFactory factory) + { + socket.NoDelay = true; + _socket = socket; + if (factory == null) + { + _ownsFactory = true; + factory = new PipelineFactory(); + } + _factory = factory; + + _input = PipelineFactory.Create(); + _output = PipelineFactory.Create(); + + ShutdownSocketWhenWritingCompletedAsync(); + ReceiveFromSocketAndPushToWriterAsync(); + ReadFromReaderAndWriteToSocketAsync(); + } + + /// + /// Provides access to data received from the socket + /// + public IPipelineReader Input => _input; + + /// + /// Provides access to write data to the socket + /// + public IPipelineWriter Output => _output; + + private PipelineFactory PipelineFactory => _factory; + + private Socket Socket => _socket; + + /// + /// Begins an asynchronous connect operation to the designated endpoint + /// + /// The endpoint to which to connect + /// Optionally allows the underlying (and hence memory pool) to be specified; if one is not provided, a will be instantiated and owned by the connection + public static Task ConnectAsync(IPEndPoint endPoint, PipelineFactory factory = null) + { + var args = new SocketAsyncEventArgs(); + args.RemoteEndPoint = endPoint; + args.Completed += _asyncCompleted; + var tcs = new TaskCompletionSource(factory); + args.UserToken = tcs; + if (!Socket.ConnectAsync(SocketType.Stream, ProtocolType.Tcp, args)) + { + OnConnect(args); // completed sync - usually means failure + } + return tcs.Task; + } + /// + /// Releases all resources owned by the connection + /// + public void Dispose() => Dispose(true); + + internal static SocketAsyncEventArgs GetOrCreateSocketAsyncEventArgs() + { + SocketAsyncEventArgs args = null; + lock (_argsPool) + { + if (_argsPool.Count != 0) + { + args = _argsPool.Dequeue(); + } + } + if (args == null) + { + args = new SocketAsyncEventArgs(); + args.Completed += _asyncCompleted; // only for new, otherwise multi-fire + } + if (args.UserToken is Signal) + { + ((Signal)args.UserToken).Reset(); + } + else + { + args.UserToken = new Signal(); + } + return args; + } + + internal static void RecycleSocketAsyncEventArgs(SocketAsyncEventArgs args) + { + if (args != null) + { + args.SetBuffer(null, 0, 0); // make sure we don't keep a slab alive + lock (_argsPool) + { + if (_argsPool.Count < 2048) + { + _argsPool.Enqueue(args); + } + } + } + } + /// + /// Releases all resources owned by the connection + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _output.CompleteWriter(); + _output.CompleteReader(); + + _input.CompleteWriter(); + _input.CompleteReader(); + + GC.SuppressFinalize(this); + _socket?.Dispose(); + _socket = null; + if (_ownsFactory) { _factory?.Dispose(); } + _factory = null; + } + } + + private static void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + { + try + { + switch (e.LastOperation) + { + case SocketAsyncOperation.Connect: + OnConnect(e); + break; + + case SocketAsyncOperation.Send: + case SocketAsyncOperation.Receive: + ReleasePending(e); + break; + } + } + catch { } + } + + private static void OnConnect(SocketAsyncEventArgs e) + { + var tcs = (TaskCompletionSource)e.UserToken; + try + { + if (e.SocketError == SocketError.Success) + { + tcs.TrySetResult(new SocketConnection(e.ConnectSocket, (PipelineFactory)tcs.Task.AsyncState)); + } + else + { + tcs.TrySetException(new SocketException((int)e.SocketError)); + } + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + } + + private static void ReleasePending(SocketAsyncEventArgs e) + { + var pending = (Signal)e.UserToken; + pending.Set(); + } + + private enum BufferStyle + { + Unknown, + UseZeroLengthBuffer, + UseSmallBuffer + } + + private async void ShutdownSocketWhenWritingCompletedAsync() + { + // the intent of this is so that *external* callers can cause the + // socket to become shut down; a natural consequence is that this + // will also run if we shut it down from inside, but... that isn't + // a huge problem + try + { + await _input.Writing; + } + catch { } // lots of swallowing here; this is all in crazy conditions + try + { + Socket.Shutdown(SocketShutdown.Receive); + } + catch { } + } + private async void ReceiveFromSocketAndPushToWriterAsync() + { + SocketAsyncEventArgs args = null; + try + { + // if the consumer says they don't want the data, we need to shut down the receive + GC.KeepAlive(_input.Writing.ContinueWith(delegate + {// GC.KeepAlive here just to shut the compiler up + try { Socket.Shutdown(SocketShutdown.Receive); } catch { } + })); + + // wait for someone to be interested in data before we + // start allocating buffers and probing the socket + await _input.ReadingStarted; + + args = GetOrCreateSocketAsyncEventArgs(); + while (!_input.Writing.IsCompleted) + { + bool haveWriteBuffer = false; + WritableBuffer buffer = default(WritableBuffer); + var initialSegment = default(ArraySegment); + + try + { + + int bytesFromInitialDataBuffer = 0; + + if (Socket.Available == 0) + { + // now, this gets a bit messy unfortunately, because support for the ideal option + // (zero-length reads) is platform dependent + switch (_bufferStyle) + { + case BufferStyle.Unknown: + try + { + initialSegment = await ReceiveInitialDataUnknownStrategyAsync(args); + } + catch + { + initialSegment = default(ArraySegment); + } + if (initialSegment.Array == null) + { + continue; // redo from start + } + break; + case BufferStyle.UseZeroLengthBuffer: + // if we already have a buffer, use that (but: zero count); otherwise use a shared + // zero-length; this avoids constantly changing the buffer that the args use, which + // avoids some overheads + args.SetBuffer(args.Buffer ?? _zeroLengthBuffer, 0, 0); + + // await async for the io work to be completed + await Socket.ReceiveSignalAsync(args); + break; + case BufferStyle.UseSmallBuffer: + // We need to do a speculative receive with a *cheap* buffer while we wait for input; it would be *nice* if + // we could do a zero-length receive, but this is not supported equally on all platforms (fine on Windows, but + // linux hates it). The key aim here is to make sure that we don't tie up an entire block from the memory pool + // waiting for input on a socket; fine for 1 socket, not so fine for 100,000 sockets + + // do a short receive while we wait (async) for data + initialSegment = LeaseSmallBuffer(); + args.SetBuffer(initialSegment.Array, initialSegment.Offset, initialSegment.Count); + + // await async for the io work to be completed + await Socket.ReceiveSignalAsync(args); + break; + } + if (args.SocketError != SocketError.Success) + { + throw new SocketException((int)args.SocketError); + } + + // note we can't check BytesTransferred <= 0, as we always + // expect 0; but if we returned, we expect data to be + // buffered *on the socket*, else EOF + if ((bytesFromInitialDataBuffer = args.BytesTransferred) <= 0) + { + if (ReferenceEquals(initialSegment.Array, _zeroLengthBuffer)) + { + // sentinel value that means we should just + // consume sync (we expect there to be data) + initialSegment = default(ArraySegment); + } + else + { + // socket reported EOF + RecycleSmallBuffer(ref initialSegment); + } + if (Socket.Available == 0) + { + // yup, definitely an EOF + break; + } + } + } + + + // note that we will try to coalesce things here to reduce the number of flushes; we + // certainly want to coalesce the initial buffer (from the speculative receive) with the initial + // data, but we probably don't want to buffer indefinitely; for now, it will buffer up to 4 pages + // before flushing (entirely arbitrarily) - might want to make this configurable later + buffer = _input.Alloc(SmallBufferSize * 2); + haveWriteBuffer = true; + + const int FlushInputEveryBytes = 4 * MemoryPool.MaxPooledBlockLength; + + if (initialSegment.Array != null) + { + // need to account for anything that we got in the speculative receive + if (bytesFromInitialDataBuffer != 0) + { + buffer.Write(new Span(initialSegment.Array, initialSegment.Offset, bytesFromInitialDataBuffer)); + } + // make the small buffer available to other consumers + RecycleSmallBuffer(ref initialSegment); + } + + bool isEOF = false; + while (Socket.Available != 0 && buffer.BytesWritten < FlushInputEveryBytes) + { + buffer.Ensure(); // ask for *something*, then use whatever is available (usually much much more) + SetBuffer(buffer.Memory, args); + // await async for the io work to be completed + await Socket.ReceiveSignalAsync(args); + + // either way, need to validate + if (args.SocketError != SocketError.Success) + { + throw new SocketException((int)args.SocketError); + } + int len = args.BytesTransferred; + if (len <= 0) + { + // socket reported EOF + isEOF = true; + break; + } + + // record what data we filled into the buffer + buffer.Advance(len); + } + if (isEOF) + { + break; + } + } + finally + { + RecycleSmallBuffer(ref initialSegment); + if (haveWriteBuffer) + { + await buffer.FlushAsync(); + } + } + } + _input.CompleteWriter(); + } + catch (Exception ex) + { + // don't trust signal after an error; someone else could + // still have it and invoke Set + if (args != null) + { + args.UserToken = null; + } + _input?.CompleteWriter(ex); + } + finally + { + RecycleSocketAsyncEventArgs(args); + } + } + + + private static ArraySegment LeaseSmallBuffer() + { + ArraySegment result; + if (!_smallBuffers.TryTake(out result)) + { + // use a throw-away buffer as a fallback + result = new ArraySegment(new byte[_smallBuffers.BytesPerItem]); + } + return result; + } + private void RecycleSmallBuffer(ref ArraySegment buffer) + { + if (buffer.Array != null) + { + _smallBuffers?.Recycle(buffer); + } + buffer = default(ArraySegment); + } + + /// returns null if the caller should redo from start; returns + /// a non-null result to preocess the data + private async Task> ReceiveInitialDataUnknownStrategyAsync(SocketAsyncEventArgs args) + { + + // to prove that it works OK, we need (after a read): + // - have seen return 0 and Available > 0 + // - have reen return <= 0 and Available == 0 and is true EOF + // + // if we've seen both, we can switch to the simpler approach; + // until then, if we just see return 0 and Available > 0, well... + // we're happy + // + // note: if we see return 0 and available == 0 and not EOF, + // then we know that zero-length receive is not supported + + try + { + args.SetBuffer(_zeroLengthBuffer, 0, 0); + // we'll do a receive and see what happens + await Socket.ReceiveSignalAsync(args); + } + catch + { + // well, it didn't like that... switch to small buffers + _bufferStyle = BufferStyle.UseSmallBuffer; + return default(ArraySegment); + } + if (args.SocketError != SocketError.Success) + { // let the calling code explode + return new ArraySegment(_zeroLengthBuffer); + } + + if (Socket.Available > 0) + { + _seenReceiveZeroWithAvailable = true; + if (_seenReceiveZeroWithEOF) + { + _bufferStyle = BufferStyle.UseZeroLengthBuffer; + } + // we'll let the calling method pull the data out + return new ArraySegment(_zeroLengthBuffer); + } + + // so now we need to detect if this is a genuine EOF; if it isn't, + // that isn't conclusive, because could just be timing; but if it is: great + var buffer = LeaseSmallBuffer(); + try + { + args.SetBuffer(buffer.Array, buffer.Offset, buffer.Count); + // we'll do a receive and see what happens + await Socket.ReceiveSignalAsync(args); + + if (args.SocketError != SocketError.Success) + { // we can't actually conclude anything + RecycleSmallBuffer(ref buffer); + throw new SocketException((int)args.SocketError); + } + if (args.BytesTransferred <= 0) + { + RecycleSmallBuffer(ref buffer); + _seenReceiveZeroWithEOF = true; + if (_seenReceiveZeroWithAvailable) + { + _bufferStyle = BufferStyle.UseZeroLengthBuffer; + } + // we'll let the calling method shut everything down + return new ArraySegment(_zeroLengthBuffer); + } + + // otherwise, we got something that looked like an EOF from receive, + // but which wasn't really; we'll have to do things the hard way :( + _bufferStyle = BufferStyle.UseSmallBuffer; + return buffer; + } + catch + { + // already recycled (or not) correctly in the success cases + RecycleSmallBuffer(ref buffer); + throw; + } + } + + private async void ReadFromReaderAndWriteToSocketAsync() + { + SocketAsyncEventArgs args = null; + try + { + args = GetOrCreateSocketAsyncEventArgs(); + + while (true) + { + var result = await _output.ReadAsync(); + var buffer = result.Buffer; + try + { + if (buffer.IsEmpty && result.IsCompleted) + { + break; + } + + foreach (var memory in buffer) + { + int remaining = memory.Length; + while (remaining != 0) + { + SetBuffer(memory, args, memory.Length - remaining); + + // await async for the io work to be completed + await Socket.SendSignalAsync(args); + + // either way, need to validate + if (args.SocketError != SocketError.Success) + { + throw new SocketException((int)args.SocketError); + } + + remaining -= args.BytesTransferred; + } + } + } + finally + { + _output.Advance(buffer.End); + } + } + _output.CompleteReader(); + } + catch (Exception ex) + { + // don't trust signal after an error; someone else could + // still have it and invoke Set + if (args != null) + { + args.UserToken = null; + } + _output?.CompleteReader(ex); + } + finally + { + try // we're not going to be sending anything else + { + Socket.Shutdown(SocketShutdown.Send); + } + catch { } + RecycleSocketAsyncEventArgs(args); + } + } + + // unsafe+async not good friends + private unsafe void SetBuffer(Memory memory, SocketAsyncEventArgs args, int ignore = 0) + { + ArraySegment segment; + if (!memory.TryGetArray(out segment)) + { + throw new InvalidOperationException("Memory is not backed by an array; oops!"); + } + args.SetBuffer(segment.Array, segment.Offset + ignore, segment.Count - ignore); + } + + private void Shutdown() + { + Socket?.Shutdown(SocketShutdown.Both); + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Sockets/SocketListener.cs b/src/System.IO.Pipelines.Networking.Sockets/SocketListener.cs new file mode 100644 index 00000000000..3559e46e9d3 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Sockets/SocketListener.cs @@ -0,0 +1,151 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.Networking.Sockets +{ + /// + /// Allows a managed socket to be used as a server, listening on a designated address and accepting connections from clients + /// + public class SocketListener : IDisposable + { + private readonly bool _ownsFactory; + private Socket _socket; + private Socket Socket => _socket; + private PipelineFactory _factory; + private PipelineFactory PipelineFactory => _factory; + private Func Callback { get; set; } + static readonly EventHandler _asyncCompleted = OnAsyncCompleted; + + /// + /// Creates a new SocketListener instance + /// + /// Optionally allows the underlying (and hence memory pool) to be specified; if one is not provided, a will be instantiated and owned by the listener + public SocketListener(PipelineFactory factory = null) + { + _ownsFactory = factory == null; + _factory = factory ?? new PipelineFactory(); + } + + /// + /// Releases all resources owned by the listener + /// + public void Dispose() => Dispose(true); + /// + /// Releases all resources owned by the listener + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + GC.SuppressFinalize(this); + _socket?.Dispose(); + _socket = null; + if (_ownsFactory) { _factory?.Dispose(); } + _factory = null; + } + } + + /// + /// Commences listening for and accepting connection requests from clients + /// + /// The endpoint on which to listen + public void Start(IPEndPoint endpoint) + { + if (_socket == null) + { + _socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + _socket.Bind(endpoint); + _socket.Listen(10); + var args = new SocketAsyncEventArgs(); // note: we keep re-using same args over for successive accepts + // so usefulness of pooling here minimal; this is per listener + args.Completed += _asyncCompleted; + args.UserToken = this; + BeginAccept(args); + } + } + + /// + /// Stops the server from listening; no further connections will be accepted + /// + public void Stop() + { + if (_socket != null) + { + try + { + _socket.Shutdown(SocketShutdown.Both); + } + catch { /* nom nom */ } + _socket.Dispose(); + _socket = null; + } + } + + private Socket GetReusableSocket() => null; // TODO: socket pooling / re-use + + private void BeginAccept(SocketAsyncEventArgs args) + { + // keep trying to take sync; break when it goes async + args.AcceptSocket = GetReusableSocket(); + while (!Socket.AcceptAsync(args)) + { + OnAccept(args); + args.AcceptSocket = GetReusableSocket(); + } + } + /// + /// Specifies a callback to be invoked whenever a connection is accepted + /// + public void OnConnection(Func callback) + { + Callback = callback; + } + + private static void OnAsyncCompleted(object sender, SocketAsyncEventArgs e) + { + try + { + switch (e.LastOperation) + { + case SocketAsyncOperation.Accept: + var obj = (SocketListener)e.UserToken; + obj.OnAccept(e); + obj.BeginAccept(e); + break; + } + } + catch { } + } + + private void OnAccept(SocketAsyncEventArgs e) + { + if (e.SocketError == SocketError.Success) + { + var conn = new SocketConnection(e.AcceptSocket, PipelineFactory); + e.AcceptSocket = null; + ExecuteConnection(conn); + } + + // note that we don't want to call BeginAccept at the end of OnAccept, as that + // will cause a stack-dive in the sync (backlog) case + } + + private async void ExecuteConnection(SocketConnection conn) + { + try + { + await Callback?.Invoke(conn); + } + catch + { + + } + finally + { + conn.Dispose(); + } + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Sockets/System.IO.Pipelines.Networking.Sockets.xproj b/src/System.IO.Pipelines.Networking.Sockets/System.IO.Pipelines.Networking.Sockets.xproj new file mode 100644 index 00000000000..bfa9f4ac5a3 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Sockets/System.IO.Pipelines.Networking.Sockets.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + a5bdab00-68c2-4661-ad49-735d94eced06 + System.IO.Pipelines.Networking.Sockets + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Sockets/project.json b/src/System.IO.Pipelines.Networking.Sockets/project.json new file mode 100644 index 00000000000..bf6a5de37fa --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Sockets/project.json @@ -0,0 +1,32 @@ +{ + "version": "0.1.0-*", + "description": "Networking implementation of pipelines based on System.Net.Socket using the SocketAsyncEventArgs API", + "copyright": "Microsoft Corporation, All rights reserved", + "packOptions": { + "releaseNotes": "Pre-release package, for testing only", + "licenseUrl": "http://go.microsoft.com/fwlink/?LinkId=329770", + "iconUrl": "http://go.microsoft.com/fwlink/?LinkID=288859", + "projectUrl": "https://github.com/dotnet/corefxlab", + "requireLicenseAcceptance": true + }, + "buildOptions": { + "allowUnsafe": true, + "keyFile": "../../tools/Key.snk" + }, + + "dependencies": { + "NETStandard.Library": "1.6.0", + "System.IO.Pipelines": { + "target": "project" + } + }, + + "frameworks": { + "net451": {}, + "netstandard1.3": { + "dependencies": { + "System.Threading.ThreadPool": "4.0.10" + } + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/CpuInfo.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/CpuInfo.cs new file mode 100644 index 00000000000..6ff142b183d --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/CpuInfo.cs @@ -0,0 +1,272 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal +{ + public static class CpuInfo + { + const string Kernel_32 = "Kernel32"; + const int ErrorInsufficientBuffer = 0x7A; + private static readonly int SLPISize = Marshal.SizeOf(); + + private static int _numaNodeCount; + private static ulong _physicalCoreMask; + private static ulong _secondaryCoreMask; + private static int _physicalCoreCount; + private static int _logicalProcessorCount; + private static int _processorL1CacheCount; + private static int _processorL2CacheCount; + private static int _processorL3CacheCount; + private static int _processorPackageCount; + + public static int PhysicalCoreCount => _physicalCoreCount != 0 ? _physicalCoreCount : GetPhysicalProcessorCount(); + public static int LogicalProcessorCount => _logicalProcessorCount != 0 ? _logicalProcessorCount : GetLogicalProcessorCount(); + public static ulong PhysicalCoreMask => _physicalCoreMask != 0 ? _physicalCoreMask : GetPhysicalProcessorMask(); + public static ulong SecondaryCoreMask => _secondaryCoreMask != 0 ? _secondaryCoreMask : GetSecondaryProcessorMask(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static int GetPhysicalProcessorCount() + { + QueryCpuInfo(); + return _physicalCoreCount; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static int GetLogicalProcessorCount() + { + QueryCpuInfo(); + return _logicalProcessorCount; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ulong GetPhysicalProcessorMask() + { + QueryCpuInfo(); + return _physicalCoreMask; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ulong GetSecondaryProcessorMask() + { + QueryCpuInfo(); + return _secondaryCoreMask; + } + + private static int CountSetBits(ulong bitMask) + { + const int lshift = sizeof(ulong) * 8 - 1; + var bitSetCount = 0; + + for (var i = 0; i <= lshift; i++) + { + var bitTest = 1UL << i; + bitSetCount += ((bitMask & bitTest) == bitTest ? 1 : 0); + } + + return bitSetCount; + } + + private static ulong FirstSetBit(ulong bitMask) + { + const int lshift = sizeof(ulong) * 8 - 1; + + for (var i = 0; i <= lshift; i++) + { + var bitTest = 1UL << i; + + if ((bitMask & bitTest) == bitTest) + { + return bitTest; + } + } + + return 0; + } + + private static ulong SecondSetBit(ulong bitMask) + { + const int lshift = sizeof(ulong) * 8 - 1; + + var isFirst = true; + for (var i = 0; i <= lshift; i++) + { + var bitTest = 1UL << i; + + if ((bitMask & bitTest) == bitTest) + { + if (isFirst) + { + isFirst = false; + continue; + } + + return bitTest; + } + } + + return 0; + } + + private static unsafe void QueryCpuInfo() + { + uint returnLength = 1; + + while (true) + { + var buffer = stackalloc byte[(int)returnLength]; + if (!GetLogicalProcessorInformation(new IntPtr(buffer), ref returnLength)) + { + var error = GetLastError(); + if (error == 0 || error == ErrorInsufficientBuffer) + { + continue; + } + else + { + throw new Exception(); + } + } + + var byteOffset = 0; + while (byteOffset + SLPISize <= returnLength) + { + var slpi = Unsafe.Read(buffer + byteOffset); + + switch (slpi.Relationship) + { + case LogicalProcessorRelationship.RelationNumaNode: + // Non-NUMA systems report a single record of this type. + _numaNodeCount++; + break; + + case LogicalProcessorRelationship.RelationProcessorCore: + _physicalCoreCount++; + + // A hyperthreaded core supplies more than one logical processor. + var mask = slpi.ProcessorMask; + var bits = CountSetBits(mask); + _logicalProcessorCount += bits; + + if (bits == 1) + { + _physicalCoreMask |= mask; + _secondaryCoreMask |= mask; + } + else + { + _physicalCoreMask |= FirstSetBit(mask); + _secondaryCoreMask |= SecondSetBit(mask); + } + + break; + + case LogicalProcessorRelationship.RelationCache: + // Cache data is in ptr->Cache, one CACHE_DESCRIPTOR structure for each cache. + var cache = slpi.Info.Cache; + if (cache.Level == 1) + { + _processorL1CacheCount++; + } + else if (cache.Level == 2) + { + _processorL2CacheCount++; + } + else if (cache.Level == 3) + { + _processorL3CacheCount++; + } + break; + + case LogicalProcessorRelationship.RelationProcessorPackage: + // Logical processors share a physical package. + _processorPackageCount++; + break; + + default: + throw new Exception("Error: Unsupported LOGICAL_PROCESSOR_RELATIONSHIP value."); + } + byteOffset += SLPISize; + } + + break; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct SystemLogicalProcessorInformation + { + public ulong ProcessorMask; + public LogicalProcessorRelationship Relationship; + public ProcessorInfo Info; + } + + [StructLayout(LayoutKind.Explicit)] + public struct ProcessorInfo + { + [FieldOffset(0)] + public ProcessorCore ProcessorCore; + [FieldOffset(0)] + public Node NumaNode; + [FieldOffset(0)] + public CacheDescriptor Cache; + [FieldOffset(0)] + public Reserved Reserved; + } + + public enum LogicalProcessorRelationship + { + RelationProcessorCore, + RelationNumaNode, + RelationCache, + RelationProcessorPackage + } + + [StructLayout(LayoutKind.Sequential)] + public struct ProcessorCore + { + public byte Flags; + } + + [StructLayout(LayoutKind.Sequential)] + public struct Reserved + { + public ulong Reserved0; + public ulong Reserved1; + } + + [StructLayout(LayoutKind.Sequential)] + public struct Node + { + public uint NodeNumber; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CacheDescriptor + { + public byte Level; + public byte Associativity; + public ushort LineSize; + public uint Size; + public ProcessorCacheType Type; + } + + public enum ProcessorCacheType + { + CacheUnified, + CacheInstruction, + CacheData, + CacheTrace + } + + [DllImport(Kernel_32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetLogicalProcessorInformation(IntPtr Buffer, ref uint returnedLength); + + [DllImport(Kernel_32, SetLastError = true)] + private static extern long GetLastError(); + } +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioBufferSegment.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioBufferSegment.cs new file mode 100644 index 00000000000..9a0bad5a110 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioBufferSegment.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal +{ + [StructLayout(LayoutKind.Sequential)] + public struct RioBufferSegment + { + public RioBufferSegment(IntPtr bufferId, uint offset, uint length) + { + BufferId = bufferId; + Offset = offset; + Length = length; + } + + IntPtr BufferId; + public readonly uint Offset; + public uint Length; + } +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioRequestResult.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioRequestResult.cs new file mode 100644 index 00000000000..b6c6dee4b32 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioRequestResult.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal +{ + [StructLayout(LayoutKind.Sequential)] + public struct RioRequestResult + { + public int Status; + public uint BytesTransferred; + public long ConnectionCorrelation; + public long RequestCorrelation; + } +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioThread.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioThread.cs new file mode 100644 index 00000000000..5bce82da42e --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioThread.cs @@ -0,0 +1,538 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal +{ + internal unsafe class RioThread + { + const int maxResults = 1024; + const string Kernel_32 = "Kernel32"; + const long INVALID_HANDLE_VALUE = -1; + + private readonly RegisteredIO _rio; + private readonly int _id; + private readonly IntPtr _completionPort; + private readonly IntPtr _completionQueue; + private readonly Thread _completionThread; + private readonly object _notify; + private readonly Thread _notifyThread; + private readonly CancellationToken _token; + private readonly Queue _notifyBatches; + private readonly Queue _processedBatches; + + private PipelineFactory _factory; + private Dictionary _connections; + private List _bufferIdMappings; + + public IntPtr ReceiveCompletionQueue => _completionQueue; + + public IntPtr SendCompletionQueue => _completionQueue; + + public IntPtr CompletionPort => _completionPort; + + public PipelineFactory PipelineFactory => _factory; + + public RioThread(int id, CancellationToken token, IntPtr completionPort, IntPtr completionQueue, RegisteredIO rio) + { + _id = id; + _rio = rio; + _token = token; + + if (CpuInfo.LogicalProcessorCount > CpuInfo.PhysicalCoreCount) + { + _completionThread = new Thread(RunLogicalCompletions) + { + Name = $"RIO Completion Thread {id:00}", + IsBackground = true + }; + + _notifyBatches = new Queue(16); + _notify = new object(); + + _notifyThread = new Thread(RunNotifies) + { + Name = $"RIO Notify Thread {id:00}", + IsBackground = true + }; + _processedBatches = new Queue(16); + } + else + { + _completionThread = new Thread(RunPhysicalCompletions) + { + Name = $"RIO Completion Thread {id:00}", + IsBackground = true + }; + } + + _completionPort = completionPort; + _completionQueue = completionQueue; + } + + public void AddConnection(long key, RioTcpConnection value) + { + lock (_connections) + { + _connections.Add(key, value); + } + } + + public void RemoveConnection(long key) + { + lock (_connections) + { + _connections.Remove(key); + } + } + + public IntPtr GetBufferId(IntPtr address, out long startAddress) + { + var id = IntPtr.Zero; + startAddress = 0; + + lock (_bufferIdMappings) + { + var addressLong = address.ToInt64(); + + // Can binary search if it's too slow + foreach (var mapping in _bufferIdMappings) + { + if (addressLong >= mapping.Start && addressLong <= mapping.End) + { + id = mapping.Id; + startAddress = mapping.Start; + break; + } + } + } + + return id; + } + + private void OnSlabAllocated(MemoryPoolSlab slab) + { + lock (_bufferIdMappings) + { + var memoryPtr = slab.NativePointer; + var bufferId = _rio.RioRegisterBuffer(memoryPtr, (uint)slab.Length); + var addressLong = memoryPtr.ToInt64(); + + _bufferIdMappings.Add(new BufferMapping + { + Id = bufferId, + Start = addressLong, + End = addressLong + slab.Length + }); + } + } + + private void OnSlabDeallocated(MemoryPoolSlab slab) + { + var memoryPtr = slab.NativePointer; + var addressLong = memoryPtr.ToInt64(); + + lock (_bufferIdMappings) + { + for (int i = _bufferIdMappings.Count - 1; i >= 0; i--) + { + if (addressLong == _bufferIdMappings[i].Start) + { + _bufferIdMappings.RemoveAt(i); + break; + } + } + } + } + + public void Start() + { + _completionThread.Start(this); + _notifyThread?.Start(this); + } + + private static void RunLogicalCompletions(object state) + { + + var thread = ((RioThread)state); +#if NET451 + Thread.BeginThreadAffinity(); +#endif + var nativeThread = GetCurrentThread(); + var affinity = GetAffinity(thread._id); + nativeThread.ProcessorAffinity = new IntPtr((long)affinity); + + thread._connections = new Dictionary(); + thread._bufferIdMappings = new List(); + + var memoryPool = new MemoryPool(); + memoryPool.RegisterSlabAllocationCallback((slab) => thread.OnSlabAllocated(slab)); + memoryPool.RegisterSlabDeallocationCallback((slab) => thread.OnSlabDeallocated(slab)); + thread._factory = new PipelineFactory(memoryPool); + + thread.ProcessLogicalCompletions(); + +#if NET451 + Thread.EndThreadAffinity(); +#endif + } + + private static void RunPhysicalCompletions(object state) + { + + var thread = ((RioThread)state); +#if NET451 + Thread.BeginThreadAffinity(); +#endif + var nativeThread = GetCurrentThread(); + var affinity = GetAffinity(thread._id); + nativeThread.ProcessorAffinity = new IntPtr((long)affinity); + + thread._connections = new Dictionary(); + thread._bufferIdMappings = new List(); + + var memoryPool = new MemoryPool(); + memoryPool.RegisterSlabAllocationCallback((slab) => thread.OnSlabAllocated(slab)); + memoryPool.RegisterSlabDeallocationCallback((slab) => thread.OnSlabDeallocated(slab)); + thread._factory = new PipelineFactory(memoryPool); + + thread.ProcessPhysicalCompletions(); + +#if NET451 + Thread.EndThreadAffinity(); +#endif + } + + private static void RunNotifies(object state) + { + + var thread = ((RioThread)state); +#if NET451 + Thread.BeginThreadAffinity(); +#endif + var nativeThread = GetCurrentThread(); + var affinity = GetPairedAffinity(thread._id); + nativeThread.ProcessorAffinity = new IntPtr((long)affinity); + + thread.ProcessNotifies(); +#if NET451 + Thread.EndThreadAffinity(); +#endif + } + + private struct NotifyBatch + { + public RioTcpConnection[] ConnectionsToSignal; + public uint Count; + } + + private void ProcessNotifies() + { + while (!_token.IsCancellationRequested) + { + NotifyBatch batch; + lock (_notify) + { + if (_notifyBatches.Count == 0) + { + Monitor.Wait(_notify); + } + + batch = _notifyBatches.Dequeue(); + } + + var count = batch.Count; + var connectionsToSignal = batch.ConnectionsToSignal; + + Notify(connectionsToSignal, count); + + lock (_processedBatches) + { + _processedBatches.Enqueue(batch); + } + } + } + + private static void Notify(RioTcpConnection[] connectionsToSignal, uint count) + { + for (var i = 0; i < connectionsToSignal.Length; i++) + { + if (i >= count) + { + break; + } + + var connection = connectionsToSignal[i]; + + if (connection != null) + { + connection.ReceiveEndComplete(); + connectionsToSignal[i] = null; + } + } + } + + + private void ProcessLogicalCompletions() + { + RioRequestResult* results = stackalloc RioRequestResult[maxResults]; + + _rio.Notify(ReceiveCompletionQueue); + while (!_token.IsCancellationRequested) + { + NativeOverlapped* overlapped; + uint bytes, key; + var success = GetQueuedCompletionStatus(CompletionPort, out bytes, out key, out overlapped, -1); + if (success) + { + var activatedNotify = false; + while (true) + { + var count = _rio.DequeueCompletion(ReceiveCompletionQueue, (IntPtr)results, maxResults); + if (count == 0) + { + if (!activatedNotify) + { + activatedNotify = true; + _rio.Notify(ReceiveCompletionQueue); + continue; + } + + break; + } + + var gotBatch = false; + var batch = default(NotifyBatch); + lock (_processedBatches) + { + if (_processedBatches.Count > 0) + { + batch = _processedBatches.Dequeue(); + batch.Count = count; + gotBatch = true; + } + } + + if (!gotBatch) + { + batch = new NotifyBatch() + { + ConnectionsToSignal = new RioTcpConnection[maxResults], + Count = count + }; + } + + var connectionsToSignal = batch.ConnectionsToSignal; + + Complete(results, count, connectionsToSignal); + + lock (_notify) + { + _notifyBatches.Enqueue(batch); + + Monitor.Pulse(_notify); + } + + if (!activatedNotify) + { + activatedNotify = true; + _rio.Notify(ReceiveCompletionQueue); + + } + } + } + else + { + var error = GetLastError(); + if (error != 258) + { + throw new Exception($"ERROR: GetQueuedCompletionStatusEx returned {error}"); + } + } + } + } + + private unsafe void Complete(RioRequestResult* results, uint count, RioTcpConnection[] connectionsToSignal) + { + for (var i = 0; i < count; i++) + { + var result = results[i]; + + RioTcpConnection connection; + bool found; + lock (_connections) + { + found = _connections.TryGetValue(result.ConnectionCorrelation, out connection); + } + + if (found) + { + if (result.RequestCorrelation >= 0) + { + connection.ReceiveBeginComplete(result.BytesTransferred); + connectionsToSignal[i] = connection; + } + else + { + connection.SendComplete(result.RequestCorrelation); + connectionsToSignal[i] = null; + } + } + else + { + connectionsToSignal[i] = null; + } + } + } + + private void ProcessPhysicalCompletions() + { + RioRequestResult* results = stackalloc RioRequestResult[maxResults]; + var connectionsToSignal = new RioTcpConnection[maxResults]; + + _rio.Notify(ReceiveCompletionQueue); + while (!_token.IsCancellationRequested) + { + NativeOverlapped* overlapped; + uint bytes, key; + var success = GetQueuedCompletionStatus(CompletionPort, out bytes, out key, out overlapped, -1); + if (success) + { + var activatedNotify = false; + while (true) + { + var count = _rio.DequeueCompletion(ReceiveCompletionQueue, (IntPtr)results, maxResults); + if (count == 0) + { + if (!activatedNotify) + { + activatedNotify = true; + _rio.Notify(ReceiveCompletionQueue); + continue; + } + + break; + } + + Complete(results, count, connectionsToSignal); + + Notify(connectionsToSignal, count); + + if (!activatedNotify) + { + activatedNotify = true; + _rio.Notify(ReceiveCompletionQueue); + + } + } + } + else + { + var error = GetLastError(); + if (error != 258) + { + throw new Exception($"ERROR: GetQueuedCompletionStatusEx returned {error}"); + } + } + } + } + + [DllImport(Kernel_32, SetLastError = true)] + private static extern bool GetQueuedCompletionStatus(IntPtr CompletionPort, out uint lpNumberOfBytes, out uint lpCompletionKey, out NativeOverlapped* lpOverlapped, int dwMilliseconds); + + [DllImport(Kernel_32, SetLastError = true)] + private static extern long GetLastError(); + + [DllImport("kernel32.dll")] + private static extern int GetCurrentThreadId(); + + private static ProcessThread GetCurrentThread() + { + var id = GetCurrentThreadId(); + var processThreads = Process.GetCurrentProcess().Threads; + + for (var i = 0; i < processThreads.Count; i++) + { + var thread = processThreads[i]; + if (thread.Id == id) + { + return thread; + } + } + + return null; + } + + private static ulong GetAffinity(int threadId) + { + const int lshift = sizeof(ulong) * 8 - 1; + + var bitMask = CpuInfo.PhysicalCoreMask; + var coreId = 0; + + for (var i = 0; i <= lshift; i++) + { + var bitTest = 1UL << i; + + if ((bitMask & bitTest) == bitTest) + { + if (coreId == threadId) + { + return bitTest; + } + coreId++; + } + } + + unchecked + { + return (ulong)-1; + } + } + + private static ulong GetPairedAffinity(int threadId) + { + const int lshift = sizeof(ulong) * 8 - 1; + + var bitMask = CpuInfo.SecondaryCoreMask; + var coreId = 0; + + for (var i = 0; i <= lshift; i++) + { + var bitTest = 1UL << i; + + if ((bitMask & bitTest) == bitTest) + { + if (coreId == threadId) + { + return bitTest; + } + coreId++; + } + } + + unchecked + { + return (ulong)-1; + } + } + + private struct BufferMapping + { + public IntPtr Id; + public long Start; + public long End; + + public override string ToString() + { + return $"{Id} ({Start}) - ({End})"; + } + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioThreadPool.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioThreadPool.cs new file mode 100644 index 00000000000..ee2d1aa28ae --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/RioThreadPool.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal +{ + internal class RioThreadPool + { + const string Kernel_32 = "Kernel32"; + const long INVALID_HANDLE_VALUE = -1; + + private RegisteredIO _rio; + private CancellationToken _token; + private int _maxThreads; + + private IntPtr _socket; + private RioThread[] _rioThreads; + + public unsafe RioThreadPool(RegisteredIO rio, IntPtr socket, CancellationToken token) + { + _socket = socket; + _rio = rio; + _token = token; + + // Count non-HT cores only + var procCount = CpuInfo.PhysicalCoreCount; + // RSS only supports up to 16 cores + _maxThreads = procCount > 16 ? 16 : procCount; + + _rioThreads = new RioThread[_maxThreads]; + for (var i = 0; i < _rioThreads.Length; i++) + { + IntPtr completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, IntPtr.Zero, 0, 0); + + if (completionPort == IntPtr.Zero) + { + var error = GetLastError(); + RioImports.WSACleanup(); + throw new Exception($"ERROR: CreateIoCompletionPort returned {error}"); + } + + var completionMethod = new NotificationCompletion + { + Type = NotificationCompletionType.IocpCompletion, + Iocp = new NotificationCompletionIocp + { + IocpHandle = completionPort, + QueueCorrelation = (ulong) i, + Overlapped = (NativeOverlapped*) (-1) // nativeOverlapped + } + }; + + IntPtr completionQueue = _rio.RioCreateCompletionQueue(RioTcpServer.MaxOutsandingCompletionsPerThread, + completionMethod); + + if (completionQueue == IntPtr.Zero) + { + var error = RioImports.WSAGetLastError(); + RioImports.WSACleanup(); + throw new Exception($"ERROR: RioCreateCompletionQueue returned {error}"); + } + + var thread = new RioThread(i, _token, completionPort, completionQueue, rio); + _rioThreads[i] = thread; + } + + for (var i = 0; i < _rioThreads.Length; i++) + { + var thread = _rioThreads[i]; + thread.Start(); + } + } + + internal RioThread GetThread(long connetionId) + { + return _rioThreads[(connetionId % _maxThreads)]; + } + + [DllImport(Kernel_32, SetLastError = true)] + private static extern IntPtr CreateIoCompletionPort(long handle, IntPtr hExistingCompletionPort, + int puiCompletionKey, uint uiNumberOfConcurrentThreads); + + [DllImport(Kernel_32, SetLastError = true)] + private static extern long GetLastError(); + } +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/AddressFamilies.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/AddressFamilies.cs new file mode 100644 index 00000000000..5e0f95c1e84 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/AddressFamilies.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + public enum AddressFamilies : short + { + Internet = 2, + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/Ipv4InternetAddress.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/Ipv4InternetAddress.cs new file mode 100644 index 00000000000..0222f9d6b23 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/Ipv4InternetAddress.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + [StructLayout(LayoutKind.Explicit, Size = 4)] + public struct Ipv4InternetAddress + { + [FieldOffset(0)] + public byte Byte1; + [FieldOffset(1)] + public byte Byte2; + [FieldOffset(2)] + public byte Byte3; + [FieldOffset(3)] + public byte Byte4; + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletion.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletion.cs new file mode 100644 index 00000000000..cc881353deb --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletion.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + [StructLayout(LayoutKind.Sequential)] + public struct NotificationCompletion + { + public NotificationCompletionType Type; + public NotificationCompletionIocp Iocp; + } +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletionEvent.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletionEvent.cs new file mode 100644 index 00000000000..d6f88c4543a --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletionEvent.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + [StructLayout(LayoutKind.Sequential)] + public struct NotificationCompletionEvent + { + public IntPtr EventHandle; + public bool NotifyReset; + } +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletionIocp.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletionIocp.cs new file mode 100644 index 00000000000..b26aa3ddae7 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletionIocp.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + [StructLayout(LayoutKind.Sequential)] + public unsafe struct NotificationCompletionIocp + { + public IntPtr IocpHandle; + public ulong QueueCorrelation; + public NativeOverlapped* Overlapped; + } +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletionType.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletionType.cs new file mode 100644 index 00000000000..e25aed627ad --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/NotificationCompletionType.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + public enum NotificationCompletionType : int + { + Polling = 0, + EventCompletion = 1, + IocpCompletion = 2 + } +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/Protocol.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/Protocol.cs new file mode 100644 index 00000000000..1347b31ec6c --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/Protocol.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + public enum Protocol : short + { + IpProtocolTcp = 6, + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RIOImports.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RIOImports.cs new file mode 100644 index 00000000000..9c710d4b391 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RIOImports.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + public static class RioImports + { + const string Ws232 = "WS2_32.dll"; + + static readonly IntPtr RioInvalidBufferid = (IntPtr)0xFFFFFFFF; + + + const uint IocOut = 0x40000000; + const uint IocIn = 0x80000000; + const uint IOC_INOUT = IocIn | IocOut; + const uint IocWs2 = 0x08000000; + const uint IocVendor = 0x18000000; + const uint SioGetMultipleExtensionFunctionPointer = IOC_INOUT | IocWs2 | 36; + + const int SioLoopbackFastPath = -1744830448;// IOC_IN | IOC_WS2 | 16; + + const int TcpNodelay = 0x0001; + const int IPPROTO_TCP = 6; + + public unsafe static RegisteredIO Initalize(IntPtr socket) + { + + UInt32 dwBytes = 0; + RioExtensionFunctionTable rio = new RioExtensionFunctionTable(); + Guid rioFunctionsTableId = new Guid("8509e081-96dd-4005-b165-9e2ee8c79e3f"); + + + int True = -1; + + int result = setsockopt(socket, IPPROTO_TCP, TcpNodelay, (char*)&True, 4); + if (result != 0) + { + var error = WSAGetLastError(); + WSACleanup(); + throw new Exception($"ERROR: setsockopt TCP_NODELAY returned {error}"); + } + + result = WSAIoctlGeneral(socket, SioLoopbackFastPath, + &True, 4, null, 0, + out dwBytes, IntPtr.Zero, IntPtr.Zero); + + if (result != 0) + { + var error = WSAGetLastError(); + WSACleanup(); + throw new Exception($"ERROR: WSAIoctl SIO_LOOPBACK_FAST_PATH returned {error}"); + } + + result = WSAIoctl(socket, SioGetMultipleExtensionFunctionPointer, + ref rioFunctionsTableId, 16, ref rio, + sizeof(RioExtensionFunctionTable), + out dwBytes, IntPtr.Zero, IntPtr.Zero); + + if (result != 0) + { + var error = WSAGetLastError(); + WSACleanup(); + throw new Exception($"ERROR: RIOInitalize returned {error}"); + } + + var rioFunctions = new RegisteredIO(); + + rioFunctions.RioRegisterBuffer = Marshal.GetDelegateForFunctionPointer(rio.RIORegisterBuffer); + + rioFunctions.RioCreateCompletionQueue = Marshal.GetDelegateForFunctionPointer(rio.RIOCreateCompletionQueue); + + rioFunctions.RioCreateRequestQueue = Marshal.GetDelegateForFunctionPointer(rio.RIOCreateRequestQueue); + + rioFunctions.Notify = Marshal.GetDelegateForFunctionPointer(rio.RIONotify); + rioFunctions.DequeueCompletion = Marshal.GetDelegateForFunctionPointer(rio.RIODequeueCompletion); + + rioFunctions.RioReceive = Marshal.GetDelegateForFunctionPointer(rio.RIOReceive); + rioFunctions.Send = Marshal.GetDelegateForFunctionPointer(rio.RIOSend); + + rioFunctions.CloseCompletionQueue = Marshal.GetDelegateForFunctionPointer(rio.RIOCloseCompletionQueue); + rioFunctions.DeregisterBuffer = Marshal.GetDelegateForFunctionPointer(rio.RIODeregisterBuffer); + rioFunctions.ResizeCompletionQueue = Marshal.GetDelegateForFunctionPointer(rio.RIOResizeCompletionQueue); + rioFunctions.ResizeRequestQueue = Marshal.GetDelegateForFunctionPointer(rio.RIOResizeRequestQueue); + + return rioFunctions; + } + + [DllImport(Ws232, SetLastError = true)] + private static extern int WSAIoctl( + [In] IntPtr socket, + [In] uint dwIoControlCode, + [In] ref Guid lpvInBuffer, + [In] uint cbInBuffer, + [In, Out] ref RioExtensionFunctionTable lpvOutBuffer, + [In] int cbOutBuffer, + [Out] out uint lpcbBytesReturned, + [In] IntPtr lpOverlapped, + [In] IntPtr lpCompletionRoutine + ); + + [DllImport(Ws232, SetLastError = true, EntryPoint = "WSAIoctl")] + private unsafe static extern int WSAIoctlGeneral( + [In] IntPtr socket, + [In] int dwIoControlCode, + [In] int* lpvInBuffer, + [In] uint cbInBuffer, + [In] int* lpvOutBuffer, + [In] int cbOutBuffer, + [Out] out uint lpcbBytesReturned, + [In] IntPtr lpOverlapped, + [In] IntPtr lpCompletionRoutine + ); + + [DllImport(Ws232, SetLastError = true, CharSet = CharSet.Ansi, BestFitMapping = true, ThrowOnUnmappableChar = true)] + internal static extern SocketError WSAStartup([In] short wVersionRequested, [Out] out WindowsSocketsData lpWindowsSocketsData ); + + [DllImport(Ws232, SetLastError = true, CharSet = CharSet.Ansi)] + public static extern IntPtr WSASocket([In] AddressFamilies af, [In] SocketType type, [In] Protocol protocol, [In] IntPtr lpProtocolInfo, [In] Int32 group, [In] SocketFlags dwFlags ); + + [DllImport(Ws232, SetLastError = true)] + public static extern ushort htons([In] ushort hostshort); + + [DllImport(Ws232, SetLastError = true, CharSet = CharSet.Ansi)] + public static extern int bind(IntPtr s, ref SocketAddress name, int namelen); + + [DllImport(Ws232, SetLastError = true)] + public static extern int listen(IntPtr s, int backlog); + + [DllImport(Ws232, SetLastError = true)] + public unsafe static extern int setsockopt(IntPtr s, int level, int optname, char* optval, int optlen); + + [DllImport(Ws232, SetLastError = true)] + public static extern IntPtr accept(IntPtr s, IntPtr addr, int addrlen); + + [DllImport(Ws232)] + public static extern Int32 WSAGetLastError(); + + [DllImport(Ws232, SetLastError = true)] + public static extern Int32 WSACleanup(); + + [DllImport(Ws232, SetLastError = true)] + public static extern int closesocket(IntPtr s); + + public const int SocketError = -1; + public const int InvalidSocket = -1; + } +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RegisteredIO.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RegisteredIO.cs new file mode 100644 index 00000000000..095ed62cf17 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RegisteredIO.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + public sealed class RegisteredIO + { + public RioRegisterBuffer RioRegisterBuffer; + + public RioCreateCompletionQueue RioCreateCompletionQueue; + public RioCreateRequestQueue RioCreateRequestQueue; + + + public RioReceive RioReceive; + public RioSend Send; + + public RioNotify Notify; + + public RioCloseCompletionQueue CloseCompletionQueue; + public RioDequeueCompletion DequeueCompletion; + public RioDeregisterBuffer DeregisterBuffer; + public RioResizeCompletionQueue ResizeCompletionQueue; + public RioResizeRequestQueue ResizeRequestQueue; + + + public const long CachedValue = long.MinValue; + + public RegisteredIO() + { + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioDelegates.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioDelegates.cs new file mode 100644 index 00000000000..e96fb6d29dc --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioDelegates.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Security; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + [SuppressUnmanagedCodeSecurity] + [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] + public delegate IntPtr RioRegisterBuffer([In] IntPtr dataBuffer, [In] UInt32 dataLength); + + [SuppressUnmanagedCodeSecurity] + [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] + public delegate void RioDeregisterBuffer([In] IntPtr bufferId); + + [SuppressUnmanagedCodeSecurity] + [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] + public delegate bool RioSend([In] IntPtr socketQueue, [In] ref RioBufferSegment rioBuffer, [In] UInt32 dataBufferCount, [In] RioSendFlags flags, [In] long requestCorrelation); + + [SuppressUnmanagedCodeSecurity] + [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] + public delegate bool RioReceive([In] IntPtr socketQueue, [In] ref RioBufferSegment rioBuffer, [In] UInt32 dataBufferCount, [In] RioReceiveFlags flags, [In] long requestCorrelation); + + [SuppressUnmanagedCodeSecurity] + [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] + public delegate IntPtr RioCreateCompletionQueue([In] uint queueSize, [In] NotificationCompletion notificationCompletion); + + [SuppressUnmanagedCodeSecurity] + [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] + public delegate void RioCloseCompletionQueue([In] IntPtr cq); + + [SuppressUnmanagedCodeSecurity] + [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] + public delegate IntPtr RioCreateRequestQueue( + [In] IntPtr socket, + [In] UInt32 maxOutstandingReceive, + [In] UInt32 maxReceiveDataBuffers, + [In] UInt32 maxOutstandingSend, + [In] UInt32 maxSendDataBuffers, + [In] IntPtr receiveCq, + [In] IntPtr sendCq, + [In] long connectionCorrelation + ); + + [SuppressUnmanagedCodeSecurity] + [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] + public delegate uint RioDequeueCompletion([In] IntPtr cq, [In] IntPtr resultArray, [In] uint resultArrayLength); + + [SuppressUnmanagedCodeSecurity] + [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] + public delegate Int32 RioNotify([In] IntPtr cq); + + [SuppressUnmanagedCodeSecurity] + [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] + public delegate bool RioResizeCompletionQueue([In] IntPtr cq, [In] UInt32 queueSize); + + [SuppressUnmanagedCodeSecurity] + [UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)] + public delegate bool RioResizeRequestQueue([In] IntPtr rq, [In] UInt32 maxOutstandingReceive, [In] UInt32 maxOutstandingSend); + +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioExtensionFunctionTable.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioExtensionFunctionTable.cs new file mode 100644 index 00000000000..af5726187e8 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioExtensionFunctionTable.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + [StructLayout(LayoutKind.Sequential)] + public struct RioExtensionFunctionTable + { + public UInt32 Size; + + public IntPtr RIOReceive; + public IntPtr RIOReceiveEx; + public IntPtr RIOSend; + public IntPtr RIOSendEx; + public IntPtr RIOCloseCompletionQueue; + public IntPtr RIOCreateCompletionQueue; + public IntPtr RIOCreateRequestQueue; + public IntPtr RIODequeueCompletion; + public IntPtr RIODeregisterBuffer; + public IntPtr RIONotify; + public IntPtr RIORegisterBuffer; + public IntPtr RIOResizeCompletionQueue; + public IntPtr RIOResizeRequestQueue; + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioReceiveFlags.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioReceiveFlags.cs new file mode 100644 index 00000000000..e4941ee754e --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioReceiveFlags.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + public enum RioReceiveFlags : uint + { + None = 0x00000000, + DontNotify = 0x00000001, + Defer = 0x00000002, + Waitall = 0x00000004, + CommitOnly = 0x00000008 + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioSendFlags.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioSendFlags.cs new file mode 100644 index 00000000000..e23aed00ce6 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/RioSendFlags.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + [Flags] + public enum RioSendFlags : uint + { + None = 0x00000000, + DontNotify = 0x00000001, + Defer = 0x00000002, + CommitOnly = 0x00000008 + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SocketAddress.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SocketAddress.cs new file mode 100644 index 00000000000..3ffcd06f2dd --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SocketAddress.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + [StructLayout(LayoutKind.Sequential)] + public unsafe struct SocketAddress + { + public AddressFamilies Family; + public ushort Port; + public Ipv4InternetAddress IpAddress; + public fixed byte Padding[8]; + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SocketFlags.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SocketFlags.cs new file mode 100644 index 00000000000..8dbb6082ef3 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SocketFlags.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + public enum SocketFlags : uint + { + Overlapped = 0x01, + MultipointCRoot = 0x02, + MultipointCLeaf = 0x04, + MultipointDRoot = 0x08, + MultipointDLeaf = 0x10, + AccessSystemSecurity = 0x40, + NoHandleInherit = 0x80, + RegisteredIO = 0x100 + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SocketType.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SocketType.cs new file mode 100644 index 00000000000..aed2b184175 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SocketType.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + public enum SocketType : short + { + Stream = 1, + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SuppressUnmanagedCodeSecurityAttribute.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SuppressUnmanagedCodeSecurityAttribute.cs new file mode 100644 index 00000000000..035611bb7cc --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/SuppressUnmanagedCodeSecurityAttribute.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NET451 +namespace System.Security +{ + public class SuppressUnmanagedCodeSecurityAttribute : Attribute + { + } +} +#endif \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/Version.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/Version.cs new file mode 100644 index 00000000000..42d1440fa80 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/Version.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + public struct Version + { + public ushort Raw; + + public Version(byte major, byte minor) + { + Raw = major; + Raw <<= 8; + Raw += minor; + } + + public byte Major + { + get + { + ushort result = Raw; + result >>= 8; + return (byte)result; + } + } + + public byte Minor + { + get + { + ushort result = Raw; + result &= 0x00FF; + return (byte)result; + } + } + + public override string ToString() + { + return string.Format("{0}.{1}", Major, Minor); + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/WindowsSocketsData.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/WindowsSocketsData.cs new file mode 100644 index 00000000000..221bdb1df29 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Internal/Winsock/WindowsSocketsData.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock +{ + [StructLayout(LayoutKind.Sequential)] + internal struct WindowsSocketsData + { + internal short Version; + internal short HighVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 257)] + internal string Description; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 129)] + internal string SystemStatus; + internal short MaxSockets; + internal short MaxDatagramSize; + internal IntPtr VendorInfo; + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/Properties/AssemblyInfo.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..d7336fa2036 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("System.IO.Pipelines.Networking.Windows.RIO")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("86a0b10d-8c7a-4b20-a033-d6f679454e30")] diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/RioTcpConnection.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/RioTcpConnection.cs new file mode 100644 index 00000000000..e55c1d80be5 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/RioTcpConnection.cs @@ -0,0 +1,301 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Threading; +using System.Threading.Tasks; +using System.IO.Pipelines.Networking.Windows.RIO.Internal; +using System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock; + +namespace System.IO.Pipelines.Networking.Windows.RIO +{ + public sealed class RioTcpConnection : IPipelineConnection + { + private const RioSendFlags MessagePart = RioSendFlags.Defer | RioSendFlags.DontNotify; + private const RioSendFlags MessageEnd = RioSendFlags.None; + + private const long PartialSendCorrelation = -1; + private const long RestartSendCorrelations = -2; + + private readonly static Task _completedTask = Task.FromResult(0); + + private readonly long _connectionId; + private readonly IntPtr _socket; + private readonly IntPtr _requestQueue; + private readonly RegisteredIO _rio; + private readonly RioThread _rioThread; + private bool _disposedValue; + + private long _previousSendCorrelation = RestartSendCorrelations; + + private readonly PipelineReaderWriter _input; + private readonly PipelineReaderWriter _output; + + private readonly SemaphoreSlim _outgoingSends = new SemaphoreSlim(RioTcpServer.MaxWritesPerSocket); + private readonly SemaphoreSlim _previousSendsComplete = new SemaphoreSlim(1); + + private Task _sendTask; + + private PreservedBuffer _sendingBuffer; + private WritableBuffer _buffer; + + internal RioTcpConnection(IntPtr socket, long connectionId, IntPtr requestQueue, RioThread rioThread, RegisteredIO rio) + { + _socket = socket; + _connectionId = connectionId; + _rio = rio; + _rioThread = rioThread; + + _input = rioThread.PipelineFactory.Create(); + _output = rioThread.PipelineFactory.Create(); + + _requestQueue = requestQueue; + + rioThread.AddConnection(connectionId, this); + + ProcessReceives(); + _sendTask = ProcessSends(); + } + + public IPipelineReader Input => _input; + public IPipelineWriter Output => _output; + + private void ProcessReceives() + { + _buffer = _input.Alloc(2048); + var receiveBufferSeg = GetSegmentFromMemory(_buffer.Memory); + + if (!_rio.RioReceive(_requestQueue, ref receiveBufferSeg, 1, RioReceiveFlags.None, 0)) + { + ThrowError(ErrorType.Receive); + } + } + + private async Task ProcessSends() + { + while (true) + { + var result = await _output.ReadAsync(); + var buffer = result.Buffer; + + if (buffer.IsEmpty && result.IsCompleted) + { + break; + } + + var enumerator = buffer.GetEnumerator(); + + if (enumerator.MoveNext()) + { + var current = enumerator.Current; + + while (enumerator.MoveNext()) + { + var next = enumerator.Current; + + await SendAsync(current, endOfMessage: false); + current = next; + } + + await PreviousSendingComplete; + + _sendingBuffer = buffer.Preserve(); + + await SendAsync(current, endOfMessage: true); + } + + _output.Advance(buffer.End); + } + + _output.CompleteReader(); + } + + private Task SendAsync(Memory memory, bool endOfMessage) + { + if (!IsReadyToSend) + { + return SendAsyncAwaited(memory, endOfMessage); + } + + var flushSends = endOfMessage || MaxOutstandingSendsReached; + + Send(GetSegmentFromMemory(memory), flushSends); + + if (flushSends && !endOfMessage) + { + return AwaitReadyToSend(); + } + + return _completedTask; + } + + private async Task SendAsyncAwaited(Memory memory, bool endOfMessage) + { + await ReadyToSend; + + var flushSends = endOfMessage || MaxOutstandingSendsReached; + + Send(GetSegmentFromMemory(memory), flushSends); + + if (flushSends && !endOfMessage) + { + await ReadyToSend; + } + } + + private async Task AwaitReadyToSend() + { + await ReadyToSend; + } + + private void Send(RioBufferSegment segment, bool flushSends) + { + var sendCorrelation = flushSends ? CompleteSendCorrelation() : PartialSendCorrelation; + var sendFlags = flushSends ? MessageEnd : MessagePart; + + if (!_rio.Send(_requestQueue, ref segment, 1, sendFlags, sendCorrelation)) + { + ThrowError(ErrorType.Send); + } + } + + private Task PreviousSendingComplete => _previousSendsComplete.WaitAsync(); + + private Task ReadyToSend => _outgoingSends.WaitAsync(); + + private bool IsReadyToSend => _outgoingSends.Wait(0); + + private bool MaxOutstandingSendsReached => _outgoingSends.CurrentCount == 0; + + private void CompleteSend() => _outgoingSends.Release(); + + private void CompletePreviousSending() + { + _sendingBuffer.Dispose(); + _previousSendsComplete.Release(); + } + + private long CompleteSendCorrelation() + { + var sendCorrelation = _previousSendCorrelation; + if (sendCorrelation == int.MinValue) + { + _previousSendCorrelation = RestartSendCorrelations; + return RestartSendCorrelations; + } + + _previousSendCorrelation = sendCorrelation - 1; + return sendCorrelation - 1; + } + + public void SendComplete(long sendCorrelation) + { + CompleteSend(); + + if (sendCorrelation == _previousSendCorrelation) + { + CompletePreviousSending(); + } + } + + public void ReceiveBeginComplete(uint bytesTransferred) + { + if (bytesTransferred == 0 || _input.Writing.IsCompleted) + { + _input.CompleteWriter(); + } + else + { + _buffer.Advance((int)bytesTransferred); + _buffer.Commit(); + + ProcessReceives(); + } + } + + public void ReceiveEndComplete() + { + _buffer.FlushAsync(); + } + + private unsafe RioBufferSegment GetSegmentFromMemory(Memory memory) + { + void* pointer; + if(!memory.TryGetPointer(out pointer)) + { + throw new InvalidOperationException("Memory needs to be pinned"); + } + var spanPtr = (IntPtr)pointer; + long startAddress; + long spanAddress = spanPtr.ToInt64(); + var bufferId = _rioThread.GetBufferId(spanPtr, out startAddress); + + checked + { + var offset = (uint)(spanAddress - startAddress); + return new RioBufferSegment(bufferId, offset, (uint)memory.Length); + } + } + + private static void ThrowError(ErrorType type) + { + var errorNo = RioImports.WSAGetLastError(); + + string errorMessage; + switch (errorNo) + { + case 10014: // WSAEFAULT + errorMessage = $"{type} failed: WSAEFAULT - The system detected an invalid pointer address in attempting to use a pointer argument in a call."; + break; + case 10022: // WSAEINVAL + errorMessage = $"{type} failed: WSAEINVAL - the SocketQueue parameter is not valid, the Flags parameter contains an value not valid for a send operation, or the integrity of the completion queue has been compromised."; + break; + case 10055: // WSAENOBUFS + errorMessage = $"{type} failed: WSAENOBUFS - Sufficient memory could not be allocated, the I/O completion queue associated with the SocketQueue parameter is full."; + break; + case 997: // WSA_IO_PENDING + errorMessage = $"{type} failed? WSA_IO_PENDING - The operation has been successfully initiated and the completion will be queued at a later time."; + break; + case 995: // WSA_OPERATION_ABORTED + errorMessage = $"{type} failed. WSA_OPERATION_ABORTED - The operation has been canceled while the receive operation was pending."; + break; + default: + errorMessage = $"{type} failed: WSA error code {errorNo}"; + break; + } + + throw new InvalidOperationException(errorMessage); + } + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + _rioThread.RemoveConnection(_connectionId); + RioImports.closesocket(_socket); + + _disposedValue = true; + } + } + + ~RioTcpConnection() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + GC.SuppressFinalize(this); + } + + private enum ErrorType + { + Send, + Receive + } + } +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/RioTcpServer.cs b/src/System.IO.Pipelines.Networking.Windows.RIO/RioTcpServer.cs new file mode 100644 index 00000000000..752fbf2868c --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/RioTcpServer.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.IO.Pipelines.Networking.Windows.RIO.Internal; +using System.IO.Pipelines.Networking.Windows.RIO.Internal.Winsock; + +namespace System.IO.Pipelines.Networking.Windows.RIO +{ + public sealed class RioTcpServer + { + private readonly IntPtr _socket; + private readonly RegisteredIO _rio; + private readonly RioThreadPool _pool; + + private const int MaxSocketsPerThread = 256000; + private const int MaxReadsPerSocket = 1; + public const int MaxWritesPerSocket = 2; + public const int MaxOutsandingCompletionsPerThread = (MaxReadsPerSocket + MaxWritesPerSocket) * MaxSocketsPerThread; + + private long _connectionId; + + public RioTcpServer(ushort port, byte address1, byte address2, byte address3, byte address4) + { + var version = new Internal.Winsock.Version(2, 2); + WindowsSocketsData wsaData; + System.Net.Sockets.SocketError result = RioImports.WSAStartup((short)version.Raw, out wsaData); + if (result != System.Net.Sockets.SocketError.Success) + { + var error = RioImports.WSAGetLastError(); + throw new Exception(string.Format("ERROR: WSAStartup returned {0}", error)); + } + + _socket = RioImports.WSASocket(AddressFamilies.Internet, SocketType.Stream, Protocol.IpProtocolTcp, IntPtr.Zero, 0, SocketFlags.RegisteredIO); + if (_socket == IntPtr.Zero) + { + var error = RioImports.WSAGetLastError(); + RioImports.WSACleanup(); + throw new Exception(string.Format("ERROR: WSASocket returned {0}", error)); + } + + _rio = RioImports.Initalize(_socket); + + + _pool = new RioThreadPool(_rio, _socket, CancellationToken.None); + _connectionId = 0; + Start(port, address1, address2, address3, address4); + } + + private void Start(ushort port, byte address1, byte address2, byte address3, byte address4) + { + // BIND + var inAddress = new Ipv4InternetAddress(); + inAddress.Byte1 = address1; + inAddress.Byte2 = address2; + inAddress.Byte3 = address3; + inAddress.Byte4 = address4; + + var sa = new SocketAddress(); + sa.Family = AddressFamilies.Internet; + sa.Port = RioImports.htons(port); + sa.IpAddress = inAddress; + + int result; + unsafe + { + var size = sizeof(SocketAddress); + result = RioImports.bind(_socket, ref sa, size); + } + if (result == RioImports.SocketError) + { + RioImports.WSACleanup(); + throw new Exception("bind failed"); + } + + // LISTEN + result = RioImports.listen(_socket, 2048); + if (result == RioImports.SocketError) + { + RioImports.WSACleanup(); + throw new Exception("listen failed"); + } + } + + public RioTcpConnection Accept() + { + var accepted = RioImports.accept(_socket, IntPtr.Zero, 0); + if (accepted == new IntPtr(-1)) + { + var error = RioImports.WSAGetLastError(); + RioImports.WSACleanup(); + throw new Exception($"listen failed with {error}"); + } + var connectionId = Interlocked.Increment(ref _connectionId); + var thread = _pool.GetThread(connectionId); + + var requestQueue = _rio.RioCreateRequestQueue( + accepted, + maxOutstandingReceive: MaxReadsPerSocket, + maxReceiveDataBuffers: 1, + maxOutstandingSend: MaxWritesPerSocket, + maxSendDataBuffers: 1, + receiveCq: thread.ReceiveCompletionQueue, + sendCq: thread.SendCompletionQueue, + connectionCorrelation: connectionId); + + if (requestQueue == IntPtr.Zero) + { + var error = RioImports.WSAGetLastError(); + RioImports.WSACleanup(); + throw new Exception($"ERROR: RioCreateRequestQueue returned {error}"); + } + + return new RioTcpConnection(accepted, connectionId, requestQueue, thread, _rio); + } + + public void Stop() + { + RioImports.WSACleanup(); + } + } + +} diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/System.IO.Pipelines.Networking.Windows.RIO.xproj b/src/System.IO.Pipelines.Networking.Windows.RIO/System.IO.Pipelines.Networking.Windows.RIO.xproj new file mode 100644 index 00000000000..4fa72dbe963 --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/System.IO.Pipelines.Networking.Windows.RIO.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 2d38272e-5e48-4ade-9321-c73952613e70 + System.IO.Pipelines.Networking.Windows.RIO + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/System.IO.Pipelines.Networking.Windows.RIO/project.json b/src/System.IO.Pipelines.Networking.Windows.RIO/project.json new file mode 100644 index 00000000000..34e3a11f36e --- /dev/null +++ b/src/System.IO.Pipelines.Networking.Windows.RIO/project.json @@ -0,0 +1,36 @@ +{ + "version": "0.1.0-*", + "description": "Networking implementation of pipelines based on Windows RIO", + "copyright": "Microsoft Corporation, All rights reserved", + "packOptions": { + "releaseNotes": "Pre-release package, for testing only", + "licenseUrl": "http://go.microsoft.com/fwlink/?LinkId=329770", + "iconUrl": "http://go.microsoft.com/fwlink/?LinkID=288859", + "projectUrl": "https://github.com/dotnet/corefxlab", + "requireLicenseAcceptance": true + }, + "buildOptions": { + "allowUnsafe": true, + "keyFile": "../../tools/Key.snk" + }, + + "dependencies": { + "System.IO.Pipelines": { + "target": "project" + } + }, + + "frameworks": { + "net451": {}, + "netstandard1.3": { + "dependencies": { + "NETStandard.Library": "1.6.0", + "System.Threading.Thread": "4.0.0", + "System.Threading.Overlapped": "4.0.1", + "System.Threading.ThreadPool": "4.0.10", + "System.Diagnostics.Process": "4.1.0", + "System.Runtime.CompilerServices.Unsafe": "4.0.0" + } + } + } +} diff --git a/src/System.IO.Pipelines.Text.Primitives/AsciiUtilities.cs b/src/System.IO.Pipelines.Text.Primitives/AsciiUtilities.cs new file mode 100644 index 00000000000..31ec0ca29a4 --- /dev/null +++ b/src/System.IO.Pipelines.Text.Primitives/AsciiUtilities.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.Text.Primitives +{ + internal class AsciiUtilities + { + public static unsafe bool TryGetAsciiString(byte* input, char* output, int count) + { + var i = 0; + sbyte* signedInput = (sbyte*)input; + + bool isValid = true; + while (i < count - 11) + { + isValid = isValid && *signedInput > 0 && *(signedInput + 1) > 0 && *(signedInput + 2) > 0 && + *(signedInput + 3) > 0 && *(signedInput + 4) > 0 && *(signedInput + 5) > 0 && *(signedInput + 6) > 0 && + *(signedInput + 7) > 0 && *(signedInput + 8) > 0 && *(signedInput + 9) > 0 && *(signedInput + 10) > 0 && + *(signedInput + 11) > 0; + + i += 12; + *(output) = (char)*(signedInput); + *(output + 1) = (char)*(signedInput + 1); + *(output + 2) = (char)*(signedInput + 2); + *(output + 3) = (char)*(signedInput + 3); + *(output + 4) = (char)*(signedInput + 4); + *(output + 5) = (char)*(signedInput + 5); + *(output + 6) = (char)*(signedInput + 6); + *(output + 7) = (char)*(signedInput + 7); + *(output + 8) = (char)*(signedInput + 8); + *(output + 9) = (char)*(signedInput + 9); + *(output + 10) = (char)*(signedInput + 10); + *(output + 11) = (char)*(signedInput + 11); + output += 12; + signedInput += 12; + } + if (i < count - 5) + { + isValid = isValid && *signedInput > 0 && *(signedInput + 1) > 0 && *(signedInput + 2) > 0 && + *(signedInput + 3) > 0 && *(signedInput + 4) > 0 && *(signedInput + 5) > 0; + + i += 6; + *(output) = (char)*(signedInput); + *(output + 1) = (char)*(signedInput + 1); + *(output + 2) = (char)*(signedInput + 2); + *(output + 3) = (char)*(signedInput + 3); + *(output + 4) = (char)*(signedInput + 4); + *(output + 5) = (char)*(signedInput + 5); + output += 6; + signedInput += 6; + } + if (i < count - 3) + { + isValid = isValid && *signedInput > 0 && *(signedInput + 1) > 0 && *(signedInput + 2) > 0 && + *(signedInput + 3) > 0; + + i += 4; + *(output) = (char)*(signedInput); + *(output + 1) = (char)*(signedInput + 1); + *(output + 2) = (char)*(signedInput + 2); + *(output + 3) = (char)*(signedInput + 3); + output += 4; + signedInput += 4; + } + + while (i < count) + { + isValid = isValid && *signedInput > 0; + + i++; + *output = (char)*signedInput; + output++; + signedInput++; + } + + return isValid; + } + } +} diff --git a/src/System.IO.Pipelines.Text.Primitives/PipelineTextOutput.cs b/src/System.IO.Pipelines.Text.Primitives/PipelineTextOutput.cs new file mode 100644 index 00000000000..deba251936d --- /dev/null +++ b/src/System.IO.Pipelines.Text.Primitives/PipelineTextOutput.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Text.Formatting; +using System.Text; + +namespace System.IO.Pipelines.Text.Primitives +{ + public class PipelineTextOutput : ITextOutput + { + private readonly IPipelineWriter _writer; + private WritableBuffer _writableBuffer; + private bool _needAlloc = true; + + public PipelineTextOutput(IPipelineWriter writer, EncodingData encoding) + { + _writer = writer; + Encoding = encoding; + } + + public EncodingData Encoding { get; } + + public Span Buffer + { + get + { + EnsureBuffer(); + + return _writableBuffer.Memory.Span; + } + } + + public void Advance(int bytes) + { + _writableBuffer.Advance(bytes); + } + + public void Enlarge(int desiredFreeBytesHint = 0) + { + _writableBuffer.Ensure(desiredFreeBytesHint == 0 ? 2048 : desiredFreeBytesHint); + } + + public void Write(Span data) + { + EnsureBuffer(); + _writableBuffer.Write(data); + } + + public async Task FlushAsync() + { + await _writableBuffer.FlushAsync(); + _needAlloc = true; + } + + private void EnsureBuffer() + { + if (_needAlloc) + { + _writableBuffer = _writer.Alloc(); + _needAlloc = false; + } + } + } +} diff --git a/src/System.IO.Pipelines.Text.Primitives/PipelineWriterExtensions.cs b/src/System.IO.Pipelines.Text.Primitives/PipelineWriterExtensions.cs new file mode 100644 index 00000000000..ae5ea3d8504 --- /dev/null +++ b/src/System.IO.Pipelines.Text.Primitives/PipelineWriterExtensions.cs @@ -0,0 +1,13 @@ +using System; +using System.Text; + +namespace System.IO.Pipelines.Text.Primitives +{ + public static class PipelineWriterExtensions + { + public static PipelineTextOutput AsTextOutput(this IPipelineWriter writer, EncodingData formattingData) + { + return new PipelineTextOutput(writer, formattingData); + } + } +} diff --git a/src/System.IO.Pipelines.Text.Primitives/Properties/AssemblyInfo.cs b/src/System.IO.Pipelines.Text.Primitives/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..25f3293bd99 --- /dev/null +++ b/src/System.IO.Pipelines.Text.Primitives/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("System.IO.Pipelines.Text.Primitives")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a98068ae-c895-4b1f-adcb-60c70c64f118")] diff --git a/src/System.IO.Pipelines.Text.Primitives/ReadableBufferExtensions.cs b/src/System.IO.Pipelines.Text.Primitives/ReadableBufferExtensions.cs new file mode 100644 index 00000000000..06a86274681 --- /dev/null +++ b/src/System.IO.Pipelines.Text.Primitives/ReadableBufferExtensions.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Parsing; +using System.Text.Utf8; + +namespace System.IO.Pipelines.Text.Primitives +{ + /// + /// Extension methods + /// + public static class ReadableBufferExtensions + { + /// + /// Trim whitespace starting from the specified . + /// + /// The to trim + /// A new with the starting whitespace trimmed. + public static ReadableBuffer TrimStart(this ReadableBuffer buffer) + { + int start = 0; + foreach (var memory in buffer) + { + var span = memory.Span; + for (int i = 0; i < span.Length; i++) + { + if (!IsWhitespaceChar(span[i])) + { + break; + } + + start++; + } + } + + return buffer.Slice(start); + } + + /// + /// Trim whitespace starting from the specified . + /// + /// The to trim + /// A new with the starting whitespace trimmed. + public static ReadableBuffer TrimEnd(this ReadableBuffer buffer) + { + var end = -1; + var i = 0; + foreach (var memory in buffer) + { + var span = memory.Span; + for (int j = 0; j < span.Length; j++) + { + i++; + if (IsWhitespaceChar(span[j])) + { + if (end == -1) + { + end = i; + } + } + else + { + end = -1; + } + } + } + + return end != -1 ? buffer.Slice(0, end - 1) : buffer; + } + + private static bool IsWhitespaceChar(int ch) + { + return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; + } + + /// + /// Parses a from the specified + /// + /// The to parse + public static uint GetUInt32(this ReadableBuffer buffer) + { + uint value; + int consumed; + if(!buffer.TryParseUInt32(out value, out consumed)) + { + throw new InvalidOperationException("could not parse uint"); + } + return value; + } + + /// + /// Parses a from the specified + /// + /// The to parse + public unsafe static ulong GetUInt64(this ReadableBuffer buffer) + { + byte* addr; + ulong value; + int consumed, len = buffer.Length; + if (buffer.IsSingleSpan) + { + // It fits! + void* pointer; + ArraySegment data; + if (buffer.First.TryGetPointer(out pointer)) + { + if (!PrimitiveParser.TryParseUInt64((byte*)pointer, 0, len, EncodingData.InvariantUtf8, Format.Parsed.HexUppercase, out value, out consumed)) + { + throw new InvalidOperationException(); + } + } + else if (buffer.First.TryGetArray(out data)) + { + if (!PrimitiveParser.TryParseUInt64(data.Array, 0, EncodingData.InvariantUtf8, Format.Parsed.HexUppercase, out value, out consumed)) + { + throw new InvalidOperationException(); + } + } + else + { + throw new InvalidOperationException(); + } + } + else if (len < 128) // REVIEW: What's a good number + { + var data = stackalloc byte[len]; + buffer.CopyTo(new Span(data, len)); + addr = data; + + if (!PrimitiveParser.TryParseUInt64(addr, 0, len, EncodingData.InvariantUtf8, Format.Parsed.HexUppercase, out value, out consumed)) + { + throw new InvalidOperationException(); + } + } + else + { + // Heap allocated copy to parse into array (should be rare) + var arr = buffer.ToArray(); + if (!PrimitiveParser.TryParseUInt64(arr, 0, EncodingData.InvariantUtf8, Format.Parsed.HexUppercase, out value, out consumed)) + { + throw new InvalidOperationException(); + } + + return value; + } + + return value; + } + + /// + /// Decodes the ASCII encoded bytes in the into a + /// + /// The buffer to decode + public unsafe static string GetAsciiString(this ReadableBuffer buffer) + { + if (buffer.IsEmpty) + { + return null; + } + + var asciiString = new string('\0', buffer.Length); + + fixed (char* outputStart = asciiString) + { + int offset = 0; + var output = outputStart; + + foreach (var memory in buffer) + { + void* pointer; + if (memory.TryGetPointer(out pointer)) + { + if (!AsciiUtilities.TryGetAsciiString((byte*)pointer, output + offset, memory.Length)) + { + throw new InvalidOperationException(); + } + } + else + { + ArraySegment data; + if (memory.TryGetArray(out data)) + { + fixed (byte* ptr = &data.Array[0]) + { + if (!AsciiUtilities.TryGetAsciiString(ptr + data.Offset, output + offset, memory.Length)) + { + throw new InvalidOperationException(); + } + } + } + } + + offset += memory.Length; + } + } + + return asciiString; + } + + /// + /// Decodes the utf8 encoded bytes in the into a + /// + /// The buffer to decode + public static unsafe string GetUtf8String(this ReadableBuffer buffer) + { + if (buffer.IsEmpty) + { + return null; + } + + ReadOnlySpan textSpan; + + if (buffer.IsSingleSpan) + { + textSpan = buffer.First.Span; + } + else if (buffer.Length < 128) // REVIEW: What's a good number + { + var data = stackalloc byte[128]; + var destination = new Span(data, 128); + + buffer.CopyTo(destination); + + textSpan = destination.Slice(0, buffer.Length); + } + else + { + // Heap allocated copy to parse into array (should be rare) + textSpan = new ReadOnlySpan(buffer.ToArray()); + } + + return new Utf8String(textSpan).ToString(); + } + + /// + /// Split a buffer into a sequence of tokens using a delimiter + /// + public static SplitEnumerable Split(this ReadableBuffer buffer, byte delimiter) + => new SplitEnumerable(buffer, delimiter); + } +} diff --git a/src/System.IO.Pipelines.Text.Primitives/SplitEnumerable.cs b/src/System.IO.Pipelines.Text.Primitives/SplitEnumerable.cs new file mode 100644 index 00000000000..37b0b1272f3 --- /dev/null +++ b/src/System.IO.Pipelines.Text.Primitives/SplitEnumerable.cs @@ -0,0 +1,57 @@ +using System.Collections; +using System.Collections.Generic; + +namespace System.IO.Pipelines.Text.Primitives +{ + /// + /// Exposes the enumerator, which supports a simple iteration over a collection of a specified type. + /// + public struct SplitEnumerable : IEnumerable + { + private ReadableBuffer _buffer; + + private int _count; + + private byte _delimiter; + + internal SplitEnumerable(ReadableBuffer buffer, byte delimiter) + { + _buffer = buffer; + _delimiter = delimiter; + _count = buffer.IsEmpty ? 0 : -1; + } + + /// + /// Count the number of elemnts in this sequence + /// + public int Count() + { + if (_count >= 0) + { + return _count; + } + + int count = 1; + var current = _buffer; + ReadableBuffer ignore; + ReadCursor cursor; + while (current.TrySliceTo(_delimiter, out ignore, out cursor)) + { + current = current.Slice(cursor).Slice(1); + count++; + } + return _count = count; + } + /// + /// Returns an enumerator that iterates through the collection. + /// + public SplitEnumerator GetEnumerator() + => new SplitEnumerator(_buffer, _delimiter); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + } +} diff --git a/src/System.IO.Pipelines.Text.Primitives/SplitEnumerator.cs b/src/System.IO.Pipelines.Text.Primitives/SplitEnumerator.cs new file mode 100644 index 00000000000..6ce5ff579b7 --- /dev/null +++ b/src/System.IO.Pipelines.Text.Primitives/SplitEnumerator.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace System.IO.Pipelines.Text.Primitives +{ + /// + /// Supports a simple iteration over a sequence of buffers from a Split operation. + /// + public struct SplitEnumerator : IEnumerator + { + private readonly byte _delimiter; + private ReadableBuffer _current, _remainder; + internal SplitEnumerator(ReadableBuffer remainder, byte delimiter) + { + _current = default(ReadableBuffer); + _remainder = remainder; + _delimiter = delimiter; + } + + /// + /// Gets the element in the collection at the current position of the enumerator. + /// + public ReadableBuffer Current => _current; + + object IEnumerator.Current => _current; + + /// + /// Releases all resources owned by the instance + /// + public void Dispose() { } + + void IEnumerator.Reset() + { + throw new NotSupportedException(); + } + + /// + /// Advances the enumerator to the next element of the collection. + /// + public bool MoveNext() + { + ReadCursor cursor; + if (_remainder.TrySliceTo(_delimiter, out _current, out cursor)) + { + _remainder = _remainder.Slice(cursor).Slice(1); + return true; + } + // once we're out of splits, yield whatever is left + if (_remainder.IsEmpty) + { + return false; + } + _current = _remainder; + _remainder = default(ReadableBuffer); + return true; + } + } +} diff --git a/src/System.IO.Pipelines.Text.Primitives/System.IO.Pipelines.Text.Primitives.xproj b/src/System.IO.Pipelines.Text.Primitives/System.IO.Pipelines.Text.Primitives.xproj new file mode 100644 index 00000000000..fccec2fddef --- /dev/null +++ b/src/System.IO.Pipelines.Text.Primitives/System.IO.Pipelines.Text.Primitives.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + b9fa069d-d447-4303-9426-8bd4158e537c + System.IO.Pipelines.Text.Primitives + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/System.IO.Pipelines.Text.Primitives/WritableBufferExtensions.cs b/src/System.IO.Pipelines.Text.Primitives/WritableBufferExtensions.cs new file mode 100644 index 00000000000..ce9f5fe15b2 --- /dev/null +++ b/src/System.IO.Pipelines.Text.Primitives/WritableBufferExtensions.cs @@ -0,0 +1,148 @@ +using System; +using System.Runtime; +using System.Text; + +namespace System.IO.Pipelines.Text.Primitives +{ + // These APIs suck since you can't pass structs by ref to extension methods and they are mutable structs... + public static class WritableBufferExtensions + { + private static readonly byte[] HexChars = new byte[] { + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', + (byte)'7', (byte)'8', (byte)'9', (byte)'a', (byte)'b', (byte)'c', (byte)'d', + (byte)'e', (byte)'f' + }; + + private static readonly Encoding Utf8Encoding = Encoding.UTF8; + private static readonly Encoding ASCIIEncoding = Encoding.ASCII; + + public static void WriteAsciiString(this WritableBuffer buffer, string value) + => WriteString(buffer, value, ASCIIEncoding); + + public static void WriteUtf8String(this WritableBuffer buffer, string value) + => WriteString(buffer, value, Utf8Encoding); + + // review: make public? + private static unsafe void WriteString(this WritableBuffer buffer, string value, Encoding encoding) + { + int bytesPerChar = encoding.GetMaxByteCount(1); + fixed (char* s = value) + { + int remainingChars = value.Length, charOffset = 0; + while (remainingChars != 0) + { + buffer.Ensure(bytesPerChar); + + var memory = buffer.Memory; + var charsThisBatch = Math.Min(remainingChars, memory.Length / bytesPerChar); + int bytesWritten = 0; + + void* pointer; + ArraySegment data; + if (memory.TryGetPointer(out pointer)) + { + bytesWritten = encoding.GetBytes(s + charOffset, charsThisBatch, + (byte*)pointer, memory.Length); + } + else if (memory.TryGetArray(out data)) + { + bytesWritten = encoding.GetBytes(value, charOffset, charsThisBatch, data.Array, data.Offset); + } + + charOffset += charsThisBatch; + remainingChars -= charsThisBatch; + buffer.Advance(bytesWritten); + } + } + } + + public unsafe static void WriteHex(this WritableBuffer buffer, int value) + { + if (value < 16) + { + buffer.Write(new Span(HexChars, value, 1)); + return; + } + + // TODO: Don't use 2 passes + int length = 0; + var val = value; + while (val > 0) + { + val >>= 4; + length++; + } + + // Allocate space for writing the hex number + byte* digits = stackalloc byte[length]; + var span = new Span(digits, length); + int index = span.Length - 1; + + while (value > 0) + { + span[index--] = HexChars[value & 0x0f]; + value >>= 4; + } + + // Write the span to the buffer + buffer.Write(span); + } + + // REVIEW: See if we can use IFormatter here + public static void WriteUInt32(this WritableBuffer buffer, uint value) => WriteUInt64(buffer, value); + + public static void WriteUInt64(this WritableBuffer buffer, ulong value) + { + // optimized versions for 0-1000 + int len; + if (value < 10) + { + buffer.Ensure(len = 1); + var span = buffer.Memory.Span; + span[0] = (byte)('0' + value); + } + else if (value < 100) + { + buffer.Ensure(len = 2); + var span = buffer.Memory.Span; + span[0] = (byte)('0' + value / 10); + span[1] = (byte)('0' + value % 10); + } + else if (value < 1000) + { + buffer.Ensure(len = 3); + var span = buffer.Memory.Span; + span[2] = (byte)('0' + value % 10); + value /= 10; + span[0] = (byte)('0' + value / 10); + span[1] = (byte)('0' + value % 10); + } + else + { + + // more generic version for all other numbers; first find the number of digits; + // lost of ways to do this, but: http://stackoverflow.com/a/6655759/23354 + ulong remaining = value; + len = 1; + if (remaining >= 10000000000000000) { remaining /= 10000000000000000; len += 16; } + if (remaining >= 100000000) { remaining /= 100000000; len += 8; } + if (remaining >= 10000) { remaining /= 10000; len += 4; } + if (remaining >= 100) { remaining /= 100; len += 2; } + if (remaining >= 10) { remaining /= 10; len += 1; } + buffer.Ensure(len); + + // now we'll walk *backwards* from the last character, adding the digit each time + // and dividing by 10 + int index = len - 1; + var span = buffer.Memory.Span; + + do + { + span[index--] = (byte)('0' + value % 10); + value /= 10; + } while (value != 0); + } + buffer.Advance(len); + } + } +} diff --git a/src/System.IO.Pipelines.Text.Primitives/project.json b/src/System.IO.Pipelines.Text.Primitives/project.json new file mode 100644 index 00000000000..8bea310efde --- /dev/null +++ b/src/System.IO.Pipelines.Text.Primitives/project.json @@ -0,0 +1,34 @@ +{ + "version": "0.1.0-*", + "description": "Primitives for dealing with reading and writing text to and from pipelines", + "copyright": "Microsoft Corporation, All rights reserved", + "packOptions": { + "releaseNotes": "Pre-release package, for testing only", + "licenseUrl": "http://go.microsoft.com/fwlink/?LinkId=329770", + "iconUrl": "http://go.microsoft.com/fwlink/?LinkID=288859", + "projectUrl": "https://github.com/dotnet/corefxlab", + "requireLicenseAcceptance": true + }, + "buildOptions": { + "allowUnsafe": true, + "keyFile": "../../tools/Key.snk" + }, + + "dependencies": { + "NETStandard.Library": "1.6.0", + "System.Text.Formatting": { + "target": "project" + }, + "System.Text.Primitives": { + "target": "project" + }, + "System.IO.Pipelines": { + "target": "project" + } + }, + + "frameworks": { + "net451": {}, + "netstandard1.3": {} + } +} diff --git a/src/System.IO.Pipelines/ArrayBufferPool.cs b/src/System.IO.Pipelines/ArrayBufferPool.cs new file mode 100644 index 00000000000..2a42a8b6f3a --- /dev/null +++ b/src/System.IO.Pipelines/ArrayBufferPool.cs @@ -0,0 +1,27 @@ +using System.Buffers; + +namespace System.IO.Pipelines +{ + public class ArrayBufferPool : IBufferPool + { + public static readonly ArrayBufferPool Instance = new ArrayBufferPool(ArrayPool.Shared); + + private readonly ArrayPool _pool; + + public ArrayBufferPool(ArrayPool pool) + { + _pool = pool; + } + + public OwnedMemory Lease(int size) + { + // Unfortunately this allocates.... (we could pool the owned array objects though) + return new OwnedArray(_pool.Rent(size)); + } + + public void Dispose() + { + // Nothing to do here + } + } +} diff --git a/src/System.IO.Pipelines/BufferSegment.cs b/src/System.IO.Pipelines/BufferSegment.cs new file mode 100644 index 00000000000..41d962dd296 --- /dev/null +++ b/src/System.IO.Pipelines/BufferSegment.cs @@ -0,0 +1,145 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Text; + +namespace System.IO.Pipelines +{ + // TODO: Pool segments + internal class BufferSegment : IDisposable + { + /// + /// The Start represents the offset into Array where the range of "active" bytes begins. At the point when the block is leased + /// the Start is guaranteed to be equal to 0. The value of Start may be assigned anywhere between 0 and + /// Buffer.Length, and must be equal to or less than End. + /// + public int Start; + + /// + /// The End represents the offset into Array where the range of "active" bytes ends. At the point when the block is leased + /// the End is guaranteed to be equal to Start. The value of Start may be assigned anywhere between 0 and + /// Buffer.Length, and must be equal to or less than End. + /// + public int End; + + /// + /// Reference to the next block of data when the overall "active" bytes spans multiple blocks. At the point when the block is + /// leased Next is guaranteed to be null. Start, End, and Next are used together in order to create a linked-list of discontiguous + /// working memory. The "active" memory is grown when bytes are copied in, End is increased, and Next is assigned. The "active" + /// memory is shrunk when bytes are consumed, Start is increased, and blocks are returned to the pool. + /// + public BufferSegment Next; + + /// + /// The buffer being tracked + /// + private OwnedMemory _buffer; + + public BufferSegment(OwnedMemory buffer) + { + _buffer = buffer; + Start = 0; + End = 0; + + _buffer.AddReference(); + } + + public BufferSegment(OwnedMemory buffer, int start, int end) + { + _buffer = buffer; + Start = start; + End = end; + ReadOnly = true; + + // For unowned buffers, we need to make a copy here so that the caller can + // give up the give this buffer back to the caller + var unowned = buffer as UnownedBuffer; + if (unowned != null) + { + _buffer = unowned.MakeCopy(start, end - start, out Start, out End); + } + + _buffer.AddReference(); + } + + public Memory Memory => _buffer.Memory; + + /// + /// If true, data should not be written into the backing block after the End offset. Data between start and end should never be modified + /// since this would break cloning. + /// + public bool ReadOnly { get; } + + /// + /// The amount of readable bytes in this segment. Is is the amount of bytes between Start and End. + /// + public int ReadableBytes => End - Start; + + /// + /// The amount of writable bytes in this segment. It is the amount of bytes between Length and End + /// + public int WritableBytes => _buffer.Length - End; + + public void Dispose() + { + Debug.Assert(_buffer.ReferenceCount >= 1); + + _buffer.Release(); + + if (_buffer.ReferenceCount == 0) + { + _buffer.Dispose(); + } + } + + + /// + /// ToString overridden for debugger convenience. This displays the "active" byte information in this block as ASCII characters. + /// + /// + public override string ToString() + { + var builder = new StringBuilder(); + var data = _buffer.Memory.Slice(Start, ReadableBytes).Span; + + for (int i = 0; i < ReadableBytes; i++) + { + builder.Append((char)data[i]); + } + return builder.ToString(); + } + + public static BufferSegment Clone(ReadCursor beginBuffer, ReadCursor endBuffer, out BufferSegment lastSegment) + { + var beginOrig = beginBuffer.Segment; + var endOrig = endBuffer.Segment; + + if (beginOrig == endOrig) + { + lastSegment = new BufferSegment(beginOrig._buffer, beginBuffer.Index, endBuffer.Index); + return lastSegment; + } + + var beginClone = new BufferSegment(beginOrig._buffer, beginBuffer.Index, beginOrig.End); + var endClone = beginClone; + + beginOrig = beginOrig.Next; + + while (beginOrig != endOrig) + { + endClone.Next = new BufferSegment(beginOrig._buffer, beginOrig.Start, beginOrig.End); + + endClone = endClone.Next; + beginOrig = beginOrig.Next; + } + + lastSegment = new BufferSegment(endOrig._buffer, endOrig.Start, endBuffer.Index); + endClone.Next = lastSegment; + + return beginClone; + } + } +} diff --git a/src/System.IO.Pipelines/CommonVectors.cs b/src/System.IO.Pipelines/CommonVectors.cs new file mode 100644 index 00000000000..26edbbb1472 --- /dev/null +++ b/src/System.IO.Pipelines/CommonVectors.cs @@ -0,0 +1,17 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace System.IO.Pipelines +{ + // Move to text library? + internal class CommonVectors + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector GetVector(byte vectorByte) + { + // Vector .ctor is a bit fussy to get working; however this always seems to work + // https://github.com/dotnet/coreclr/issues/7459#issuecomment-253965670 + return Vector.AsVectorByte(new Vector(vectorByte * 0x0101010101010101ul)); + } + } +} diff --git a/src/System.IO.Pipelines/DefaultReadableBufferExtensions.cs b/src/System.IO.Pipelines/DefaultReadableBufferExtensions.cs new file mode 100644 index 00000000000..ea7e427277f --- /dev/null +++ b/src/System.IO.Pipelines/DefaultReadableBufferExtensions.cs @@ -0,0 +1,111 @@ +using System; +using System.Binary; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + /// + /// Common extension methods against readable buffers + /// + public static class DefaultReadableBufferExtensions + { + /// + /// Copies a to a asynchronously + /// + /// The to copy + /// The target + /// + public static Task CopyToAsync(this ReadableBuffer buffer, Stream stream) + { + if (buffer.IsSingleSpan) + { + return WriteToStream(stream, buffer.First); + } + + return CopyMultipleToStreamAsync(buffer, stream); + } + + private static async Task CopyMultipleToStreamAsync(this ReadableBuffer buffer, Stream stream) + { + foreach (var memory in buffer) + { + await WriteToStream(stream, memory); + } + } + + private static async Task WriteToStream(Stream stream, Memory memory) + { + ArraySegment data; + if (memory.TryGetArray(out data)) + { + await stream.WriteAsync(data.Array, data.Offset, data.Count).ConfigureAwait(continueOnCapturedContext: false); + } + else + { + // Copy required + var array = memory.Span.ToArray(); + await stream.WriteAsync(array, 0, array.Length).ConfigureAwait(continueOnCapturedContext: false); + } + } + + public static async Task ReadToEndAsync(this IPipelineReader input) + { + while (true) + { + // Wait for more data + var result = await input.ReadAsync(); + + if (result.IsCompleted) + { + // Read all the data, return it + return result.Buffer; + } + + // Don't advance the buffer so remains in buffer + input.Advance(result.Buffer.Start, result.Buffer.End); + } + } + + /// + /// Reads a structure of type out of a buffer of bytes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T ReadBigEndian<[Primitive]T>(this ReadableBuffer buffer) where T : struct + { + var memory = buffer.First; + int len = Unsafe.SizeOf(); + var value = memory.Length >= len ? memory.Span.ReadBigEndian() : ReadMultiBig(buffer, len); + return value; + } + + /// + /// Reads a structure of type out of a buffer of bytes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T ReadLittleEndian<[Primitive]T>(this ReadableBuffer buffer) where T : struct + { + var memory = buffer.First; + int len = Unsafe.SizeOf(); + var value = memory.Length >= len ? memory.Span.ReadLittleEndian() : ReadMultiLittle(buffer, len); + return value; + } + + private static unsafe T ReadMultiBig<[Primitive]T>(ReadableBuffer buffer, int len) where T : struct + { + byte* local = stackalloc byte[len]; + var localSpan = new Span(local, len); + buffer.Slice(0, len).CopyTo(localSpan); + return localSpan.ReadBigEndian(); + } + + private static unsafe T ReadMultiLittle<[Primitive]T>(ReadableBuffer buffer, int len) where T : struct + { + byte* local = stackalloc byte[len]; + var localSpan = new Span(local, len); + buffer.Slice(0, len).CopyTo(localSpan); + return localSpan.ReadLittleEndian(); + } + } +} diff --git a/src/System.IO.Pipelines/DefaultWritableBufferExtensions.cs b/src/System.IO.Pipelines/DefaultWritableBufferExtensions.cs new file mode 100644 index 00000000000..d2e54f927d4 --- /dev/null +++ b/src/System.IO.Pipelines/DefaultWritableBufferExtensions.cs @@ -0,0 +1,79 @@ +using System; +using System.Binary; +using System.Runtime.CompilerServices; + +namespace System.IO.Pipelines +{ + /// + /// Common extension methods against writable buffers + /// + public static class DefaultWritableBufferExtensions + { + /// + /// Writes the source to the . + /// + /// The + /// The to write + public static void Write(this WritableBuffer buffer, Span source) + { + if (buffer.Memory.IsEmpty) + { + buffer.Ensure(); + } + + // Fast path, try copying to the available memory directly + if (source.Length <= buffer.Memory.Length) + { + source.CopyTo(buffer.Memory.Span); + buffer.Advance(source.Length); + return; + } + + var remaining = source.Length; + var offset = 0; + + while (remaining > 0) + { + var writable = Math.Min(remaining, buffer.Memory.Length); + + buffer.Ensure(writable); + + if (writable == 0) + { + continue; + } + + source.Slice(offset, writable).CopyTo(buffer.Memory.Span); + + remaining -= writable; + offset += writable; + + buffer.Advance(writable); + } + } + + /// + /// Reads a structure of type T out of a buffer of bytes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteBigEndian<[Primitive]T>(this WritableBuffer buffer, T value) where T : struct + { + int len = Unsafe.SizeOf(); + buffer.Ensure(len); + buffer.Memory.Span.WriteBigEndian(value); + buffer.Advance(len); + } + + /// + /// Reads a structure of type T out of a buffer of bytes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLittleEndian<[Primitive]T>(this WritableBuffer buffer, T value) where T : struct + { + int len = Unsafe.SizeOf(); + buffer.Ensure(len); + buffer.Memory.Span.WriteLittleEndian(value); + buffer.Advance(len); + } + } +} diff --git a/src/System.IO.Pipelines/Gate.cs b/src/System.IO.Pipelines/Gate.cs new file mode 100644 index 00000000000..af65413406e --- /dev/null +++ b/src/System.IO.Pipelines/Gate.cs @@ -0,0 +1,77 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + /// + /// Simple awaitable gate - intended to synchronize a single producer with a single consumer to ensure the producer doesn't + /// produce until the consumer is ready. Similar to a but reusable so we don't have + /// to keep allocating new ones every time. + /// + /// + /// The gate can be in one of two states: "Open", indicating that an await will immediately return and "Closed", meaning that an await + /// will block until the gate is opened. The gate is initially "Closed" and can be opened by a call to . Upon the completion + /// of an await, it will automatically return to the "Closed" state (this is done in the call that is injected by the + /// compiler's async/await logic). + /// + internal class Gate : ICriticalNotifyCompletion + { + private static readonly Action _gateIsOpen = () => {}; + + private volatile Action _gateState; + + /// + /// Returns a boolean indicating if the gate is "open" + /// + public bool IsCompleted => _gateState == _gateIsOpen; + + public void UnsafeOnCompleted(Action continuation) => OnCompleted(continuation); + + public void OnCompleted(Action continuation) + { + // If we're already completed, call the continuation immediately + if (_gateState == _gateIsOpen) + { + continuation(); + } + else + { + // Otherwise, if the current continuation is null, atomically store the new continuation in the field and return the old value + var previous = Interlocked.CompareExchange(ref _gateState, continuation, null); + if (previous == _gateIsOpen) + { + // It got completed in the time between the previous the method and the cmpexch. + // So call the continuation (the value of _continuation will remain _completed because cmpexch is atomic, + // so we didn't accidentally replace it). + continuation(); + } + } + } + + /// + /// Resets the gate to continue blocking the waiter. This is called immediately after awaiting the signal. + /// + public void GetResult() + { + // Clear the active continuation to "reset" the state of this event + Interlocked.Exchange(ref _gateState, null); + } + + /// + /// Set the gate to allow the waiter to continue. + /// + public void Open() + { + // Set the stored continuation value to a sentinel that indicates the state is completed, then call the previous value. + var completion = Interlocked.Exchange(ref _gateState, _gateIsOpen); + if (completion != _gateIsOpen) + { + completion?.Invoke(); + } + } + + public Gate GetAwaiter() => this; + } +} diff --git a/src/System.IO.Pipelines/IBufferPool.cs b/src/System.IO.Pipelines/IBufferPool.cs new file mode 100644 index 00000000000..5af6d77c475 --- /dev/null +++ b/src/System.IO.Pipelines/IBufferPool.cs @@ -0,0 +1,18 @@ +using System; +using System.Buffers; + +namespace System.IO.Pipelines +{ + /// + /// An interface that represents a that will be used to allocate memory. + /// + public interface IBufferPool : IDisposable + { + /// + /// Leases a from the + /// + /// The size of the requested buffer + /// A which is a wrapper around leased memory + OwnedMemory Lease(int size); + } +} diff --git a/src/System.IO.Pipelines/IPipelineConnection.cs b/src/System.IO.Pipelines/IPipelineConnection.cs new file mode 100644 index 00000000000..a44eaf40c50 --- /dev/null +++ b/src/System.IO.Pipelines/IPipelineConnection.cs @@ -0,0 +1,20 @@ +using System; + +namespace System.IO.Pipelines +{ + /// + /// Defines a class that provides a connection from which data can be read from and written to. + /// + public interface IPipelineConnection : IDisposable + { + /// + /// Gets the half of the duplex connection. + /// + IPipelineReader Input { get; } + + /// + /// Gets the half of the duplex connection. + /// + IPipelineWriter Output { get; } + } +} diff --git a/src/System.IO.Pipelines/IPipelineReader.cs b/src/System.IO.Pipelines/IPipelineReader.cs new file mode 100644 index 00000000000..2a664c91c85 --- /dev/null +++ b/src/System.IO.Pipelines/IPipelineReader.cs @@ -0,0 +1,33 @@ +using System; + +namespace System.IO.Pipelines +{ + /// + /// Defines a class that provides a pipeline from which data can be read. + /// + public interface IPipelineReader + { + /// + /// Asynchronously reads a sequence of bytes from the current . + /// + /// A representing the asynchronous read operation. + ReadableBufferAwaitable ReadAsync(); + + /// + /// Moves forward the pipeline's read cursor to after the consumed data. + /// + /// Marks the extent of the data that has been succesfully proceesed. + /// Marks the extent of the data that has been read and examined. + /// + /// The memory for the consumed data will be released and no longer available. + /// The examined data communicates to the pipeline when it should signal more data is available. + /// + void Advance(ReadCursor consumed, ReadCursor examined); + + /// + /// Signal to the producer that the consumer is done reading. + /// + /// Optional Exception indicating a failure that's causing the pipeline to complete. + void Complete(Exception exception = null); + } +} diff --git a/src/System.IO.Pipelines/IPipelineWriter.cs b/src/System.IO.Pipelines/IPipelineWriter.cs new file mode 100644 index 00000000000..bf07a14945b --- /dev/null +++ b/src/System.IO.Pipelines/IPipelineWriter.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + /// + /// Defines a class that provides a pipeline to which data can be written. + /// + public interface IPipelineWriter + { + /// + /// Gets a task that completes when no more data will be read from the pipeline. + /// + /// + /// This task indicates the consumer has completed and will not read anymore data. + /// When this task is triggered, the producer should stop producing data. + /// + Task Writing { get; } + + /// + /// Allocates memory from the pipeline to write into. + /// + /// The minimum size buffer to allocate + /// A that can be written to. + WritableBuffer Alloc(int minimumSize = 0); + + /// + /// Marks the pipeline as being complete, meaning no more items will be written to it. + /// + /// Optional Exception indicating a failure that's causing the pipeline to complete. + void Complete(Exception exception = null); + } +} diff --git a/src/System.IO.Pipelines/IReadableBufferAwaiter.cs b/src/System.IO.Pipelines/IReadableBufferAwaiter.cs new file mode 100644 index 00000000000..2ff72f4fbc4 --- /dev/null +++ b/src/System.IO.Pipelines/IReadableBufferAwaiter.cs @@ -0,0 +1,13 @@ +using System; + +namespace System.IO.Pipelines +{ + public interface IReadableBufferAwaiter + { + bool IsCompleted { get; } + + ReadResult GetResult(); + + void OnCompleted(Action continuation); + } +} diff --git a/src/System.IO.Pipelines/MemoryEnumerator.cs b/src/System.IO.Pipelines/MemoryEnumerator.cs new file mode 100644 index 00000000000..dcb662bfbf0 --- /dev/null +++ b/src/System.IO.Pipelines/MemoryEnumerator.cs @@ -0,0 +1,88 @@ +using System; + +namespace System.IO.Pipelines +{ + /// + /// An enumerator over the + /// + public struct MemoryEnumerator + { + private BufferSegment _segment; + private Memory _current; + private int _startIndex; + private readonly int _endIndex; + private readonly BufferSegment _endSegment; + + /// + /// + /// + public MemoryEnumerator(ReadCursor start, ReadCursor end) + { + _startIndex = start.Index; + _segment = start.Segment; + _endSegment = end.Segment; + _endIndex = end.Index; + _current = Memory.Empty; + } + + /// + /// The current + /// + public Memory Current => _current; + + /// + /// + /// + public void Dispose() + { + + } + + /// + /// Moves to the next in the + /// + /// + public bool MoveNext() + { + if (_segment == null) + { + return false; + } + + int start = _segment.Start; + int end = _segment.End; + + if (_startIndex != 0) + { + start = _startIndex; + _startIndex = 0; + } + + if (_segment == _endSegment) + { + end = _endIndex; + } + + _current = _segment.Memory.Slice(start, end - start); + + if (_segment == _endSegment) + { + _segment = null; + } + else + { + _segment = _segment.Next; + } + + return true; + } + + /// + /// + /// + public void Reset() + { + ThrowHelper.ThrowNotSupportedException(); + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines/MemoryPool.cs b/src/System.IO.Pipelines/MemoryPool.cs new file mode 100644 index 00000000000..3ec8c7af45c --- /dev/null +++ b/src/System.IO.Pipelines/MemoryPool.cs @@ -0,0 +1,240 @@ +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.IO.Pipelines +{ + /// + /// Used to allocate and distribute re-usable blocks of memory. + /// + public class MemoryPool : IBufferPool + { + /// + /// The gap between blocks' starting address. 4096 is chosen because most operating systems are 4k pages in size and alignment. + /// + private const int _blockStride = 4096; + + /// + /// The last 64 bytes of a block are unused to prevent CPU from pre-fetching the next 64 byte into it's memory cache. + /// See https://github.com/aspnet/KestrelHttpServer/issues/117 and https://www.youtube.com/watch?v=L7zSU9HI-6I + /// + private const int _blockUnused = 64; + + /// + /// Allocating 32 contiguous blocks per slab makes the slab size 128k. This is larger than the 85k size which will place the memory + /// in the large object heap. This means the GC will not try to relocate this array, so the fact it remains pinned does not negatively + /// affect memory management's compactification. + /// + private const int _blockCount = 32; + + /// + /// 4096 - 64 gives you a blockLength of 4032 usable bytes per block. + /// + private const int _blockLength = _blockStride - _blockUnused; + + /// + /// Max allocation block size for pooled blocks, + /// larger values can be leased but they will be disposed after use rather than returned to the pool. + /// + public const int MaxPooledBlockLength = _blockLength; + + /// + /// 4096 * 32 gives you a slabLength of 128k contiguous bytes allocated per slab + /// + private const int _slabLength = _blockStride * _blockCount; + + /// + /// Thread-safe collection of blocks which are currently in the pool. A slab will pre-allocate all of the block tracking objects + /// and add them to this collection. When memory is requested it is taken from here first, and when it is returned it is re-added. + /// + private readonly ConcurrentQueue _blocks = new ConcurrentQueue(); + + /// + /// Thread-safe collection of slabs which have been allocated by this pool. As long as a slab is in this collection and slab.IsActive, + /// the blocks will be added to _blocks when returned. + /// + private readonly ConcurrentStack _slabs = new ConcurrentStack(); + + /// + /// This is part of implementing the IDisposable pattern. + /// + private bool _disposedValue = false; // To detect redundant calls + + private Action _slabAllocationCallback; + + private Action _slabDeallocationCallback; + + public OwnedMemory Lease(int size) + { + if (size > _blockLength) + { + ThrowHelper.ThrowArgumentOutOfRangeException_BufferRequestTooLarge(_blockLength); + } + + var block = Lease(); + block.Initialize(); + return block; + } + + public void RegisterSlabAllocationCallback(Action callback) + { + _slabAllocationCallback = callback; + } + + public void RegisterSlabDeallocationCallback(Action callback) + { + _slabDeallocationCallback = callback; + } + + /// + /// Called to take a block from the pool. + /// + /// The block that is reserved for the called. It must be passed to Return when it is no longer being used. +#if DEBUG + private MemoryPoolBlock Lease() + { + Debug.Assert(!_disposedValue, "Block being leased from disposed pool!"); +#else + public MemoryPoolBlock Lease() + { +#endif + MemoryPoolBlock block; + if (_blocks.TryDequeue(out block)) + { + // block successfully taken from the stack - return it +#if DEBUG + block.Leaser = Environment.StackTrace; + block.IsLeased = true; +#endif + return block; + } + // no blocks available - grow the pool + block = AllocateSlab(); +#if DEBUG + block.Leaser = Environment.StackTrace; + block.IsLeased = true; +#endif + return block; + } + + /// + /// Internal method called when a block is requested and the pool is empty. It allocates one additional slab, creates all of the + /// block tracking objects, and adds them all to the pool. + /// + private MemoryPoolBlock AllocateSlab() + { + var slab = MemoryPoolSlab.Create(_slabLength); + _slabs.Push(slab); + + _slabAllocationCallback?.Invoke(slab); + slab._deallocationCallback = _slabDeallocationCallback; + + var basePtr = slab.NativePointer; + var firstOffset = (int)((_blockStride - 1) - ((ulong)(basePtr + _blockStride - 1) % _blockStride)); + + var poolAllocationLength = _slabLength - _blockStride; + + var offset = firstOffset; + for (; + offset + _blockLength < poolAllocationLength; + offset += _blockStride) + { + var block = MemoryPoolBlock.Create( + offset, + _blockLength, + this, + slab); +#if DEBUG + block.IsLeased = true; +#endif + Return(block); + } + + // return last block rather than adding to pool + var newBlock = MemoryPoolBlock.Create( + offset, + _blockLength, + this, + slab); + + return newBlock; + } + + /// + /// Called to return a block to the pool. Once Return has been called the memory no longer belongs to the caller, and + /// Very Bad Things will happen if the memory is read of modified subsequently. If a caller fails to call Return and the + /// block tracking object is garbage collected, the block tracking object's finalizer will automatically re-create and return + /// a new tracking object into the pool. This will only happen if there is a bug in the server, however it is necessary to avoid + /// leaving "dead zones" in the slab due to lost block tracking objects. + /// + /// The block to return. It must have been acquired by calling Lease on the same memory pool instance. + public void Return(MemoryPoolBlock block) + { +#if DEBUG + Debug.Assert(block.Pool == this, "Returned block was not leased from this pool"); + Debug.Assert(block.IsLeased, $"Block being returned to pool twice: {block.Leaser}{Environment.NewLine}"); + block.IsLeased = false; +#endif + + if (block.Slab != null && block.Slab.IsActive) + { + _blocks.Enqueue(block); + } + else + { + GC.SuppressFinalize(block); + } + } + + protected void Dispose(bool disposing) + { + if (!_disposedValue) + { + _disposedValue = true; +#if DEBUG + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); +#endif + if (disposing) + { + MemoryPoolSlab slab; + while (_slabs.TryPop(out slab)) + { + // dispose managed state (managed objects). + slab.Dispose(); + } + } + + // Discard blocks in pool + MemoryPoolBlock block; + while (_blocks.TryDequeue(out block)) + { + GC.SuppressFinalize(block); + } + + // N/A: free unmanaged resources (unmanaged objects) and override a finalizer below. + + // N/A: set large fields to null. + + } + } + + // N/A: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~MemoryPool2() { + // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + // Dispose(false); + // } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // N/A: uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines/MemoryPoolBlock.cs b/src/System.IO.Pipelines/MemoryPoolBlock.cs new file mode 100644 index 00000000000..cdf8c51be1e --- /dev/null +++ b/src/System.IO.Pipelines/MemoryPoolBlock.cs @@ -0,0 +1,102 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Text; + +namespace System.IO.Pipelines +{ + /// + /// Block tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller blocks. The + /// individual blocks are then treated as independent array segments. + /// + public class MemoryPoolBlock : OwnedMemory + { + private readonly int _offset; + private readonly int _length; + + /// + /// This object cannot be instantiated outside of the static Create method + /// + protected unsafe MemoryPoolBlock(MemoryPool pool, MemoryPoolSlab slab, int offset, int length) : base(slab.Array, offset, length, slab.NativePointer + offset) + { + _offset = offset; + _length = length; + + Pool = pool; + Slab = slab; + } + + /// + /// Back-reference to the memory pool which this block was allocated from. It may only be returned to this pool. + /// + public MemoryPool Pool { get; } + + /// + /// Back-reference to the slab from which this block was taken, or null if it is one-time-use memory. + /// + public MemoryPoolSlab Slab { get; } + +#if DEBUG + public bool IsLeased { get; set; } + public string Leaser { get; set; } +#endif + + ~MemoryPoolBlock() + { +#if DEBUG + Debug.Assert(Slab == null || !Slab.IsActive, $"{Environment.NewLine}{Environment.NewLine}*** Block being garbage collected instead of returned to pool: {Leaser} ***{Environment.NewLine}"); +#endif + if (Slab != null && Slab.IsActive) + { + // Need to make a new object because this one is being finalized + Pool.Return(new MemoryPoolBlock(Pool, Slab, _offset, _length)); + } + } + + internal void Initialize() + { + if (IsDisposed) + { + Initialize(Slab.Array, _offset, _length, Slab.NativePointer + _offset); + } + } + + internal static MemoryPoolBlock Create( + int offset, + int length, + MemoryPool pool, + MemoryPoolSlab slab) + { + return new MemoryPoolBlock(pool, slab, offset, length) + { +#if DEBUG + Leaser = Environment.StackTrace, +#endif + }; + } + + /// + /// ToString overridden for debugger convenience. This displays the "active" byte information in this block as ASCII characters. + /// ToString overridden for debugger convenience. This displays the byte information in this block as ASCII characters. + /// + /// + public override string ToString() + { + var builder = new StringBuilder(); + var data = Memory.Span; + + for (int i = 0; i < data.Length; i++) + { + builder.Append((char)data[i]); + } + return builder.ToString(); + } + + protected override void Dispose(bool disposing) + { + Pool.Return(this); + + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines/MemoryPoolSlab.cs b/src/System.IO.Pipelines/MemoryPoolSlab.cs new file mode 100644 index 00000000000..ae35b884bba --- /dev/null +++ b/src/System.IO.Pipelines/MemoryPoolSlab.cs @@ -0,0 +1,98 @@ +using System; +using System.Runtime.InteropServices; + +namespace System.IO.Pipelines +{ + /// + /// Slab tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller blocks. The + /// individual blocks are then treated as independant array segments. + /// + public class MemoryPoolSlab : IDisposable + { + /// + /// This handle pins the managed array in memory until the slab is disposed. This prevents it from being + /// relocated and enables any subsections of the array to be used as native memory pointers to P/Invoked API calls. + /// + private readonly GCHandle _gcHandle; + private readonly IntPtr _nativePointer; + private byte[] _data; + + private bool _isActive; + internal Action _deallocationCallback; + private bool _disposedValue; + + public MemoryPoolSlab(byte[] data) + { + _data = data; + _gcHandle = GCHandle.Alloc(data, GCHandleType.Pinned); + _nativePointer = _gcHandle.AddrOfPinnedObject(); + _isActive = true; + } + + /// + /// True as long as the blocks from this slab are to be considered returnable to the pool. In order to shrink the + /// memory pool size an entire slab must be removed. That is done by (1) setting IsActive to false and removing the + /// slab from the pool's _slabs collection, (2) as each block currently in use is Return()ed to the pool it will + /// be allowed to be garbage collected rather than re-pooled, and (3) when all block tracking objects are garbage + /// collected and the slab is no longer references the slab will be garbage collected and the memory unpinned will + /// be unpinned by the slab's Dispose. + /// + public bool IsActive => _isActive; + + public IntPtr NativePointer => _nativePointer; + + public byte[] Array => _data; + + public int Length => _data.Length; + + public static MemoryPoolSlab Create(int length) + { + // allocate and pin requested memory length + var array = new byte[length]; + + // allocate and return slab tracking object + return new MemoryPoolSlab(array); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // N/A: dispose managed state (managed objects). + } + + _isActive = false; + + _deallocationCallback?.Invoke(this); + + if (_gcHandle.IsAllocated) + { + _gcHandle.Free(); + } + + // set large fields to null. + _data = null; + + _disposedValue = true; + } + } + + // override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + ~MemoryPoolSlab() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(false); + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // uncomment the following line if the finalizer is overridden above. + GC.SuppressFinalize(this); + } + } +} diff --git a/src/System.IO.Pipelines/PipelineConnectionExtensions.cs b/src/System.IO.Pipelines/PipelineConnectionExtensions.cs new file mode 100644 index 00000000000..b78204b8d33 --- /dev/null +++ b/src/System.IO.Pipelines/PipelineConnectionExtensions.cs @@ -0,0 +1,152 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + public static class PipelineConnectionExtensions + { + public static Stream GetStream(this IPipelineConnection connection) + { + return new PipelineConnectionStream(connection); + } + } + + public static class PipelineWriterExtensions + { + public static Task WriteAsync(this IPipelineWriter output, Span source) + { + var writeBuffer = output.Alloc(); + writeBuffer.Write(source); + return writeBuffer.FlushAsync(); + } + } + + public static class PipelineReaderExtensions + { + public static void Advance(this IPipelineReader input, ReadCursor cursor) + { + input.Advance(cursor, cursor); + } + + public static ValueTask ReadAsync(this IPipelineReader input, Span destination) + { + while (true) + { + var awaiter = input.ReadAsync(); + + if (!awaiter.IsCompleted) + { + break; + } + + var result = awaiter.GetResult(); + var inputBuffer = result.Buffer; + + var fin = result.IsCompleted; + var sliced = inputBuffer.Slice(0, destination.Length); + sliced.CopyTo(destination); + int actual = sliced.Length; + input.Advance(sliced.End); + + if (actual != 0) + { + return new ValueTask(actual); + } + else if (fin) + { + return new ValueTask(0); + } + } + + return new ValueTask(input.ReadAsyncAwaited(destination)); + } + + public static Task CopyToAsync(this IPipelineReader input, Stream stream) + { + return input.CopyToAsync(stream, 4096, CancellationToken.None); + } + + public static async Task CopyToAsync(this IPipelineReader input, Stream stream, int bufferSize, CancellationToken cancellationToken) + { + // TODO: Use bufferSize argument + while (!cancellationToken.IsCancellationRequested) + { + var result = await input.ReadAsync(); + var inputBuffer = result.Buffer; + try + { + if (inputBuffer.IsEmpty && result.IsCompleted) + { + return; + } + + await inputBuffer.CopyToAsync(stream); + } + finally + { + input.Advance(inputBuffer.End); + } + } + } + + public static async Task CopyToAsync(this IPipelineReader input, IPipelineWriter output) + { + while (true) + { + var result = await input.ReadAsync(); + var inputBuffer = result.Buffer; + + var fin = result.IsCompleted; + + try + { + if (inputBuffer.IsEmpty && fin) + { + return; + } + + var buffer = output.Alloc(); + + buffer.Append(inputBuffer); + + await buffer.FlushAsync(); + } + finally + { + input.Advance(inputBuffer.End); + } + } + } + + private static async Task ReadAsyncAwaited(this IPipelineReader input, Span destination) + { + while (true) + { + var result = await input.ReadAsync(); + var inputBuffer = result.Buffer; + + var fin = result.IsCompleted; + + var sliced = inputBuffer.Slice(0, destination.Length); + sliced.CopyTo(destination); + int actual = sliced.Length; + input.Advance(sliced.End); + + if (actual != 0) + { + return actual; + } + else if (fin) + { + return 0; + } + } + } + } +} diff --git a/src/System.IO.Pipelines/PipelineConnectionStream.cs b/src/System.IO.Pipelines/PipelineConnectionStream.cs new file mode 100644 index 00000000000..91c0ba7324c --- /dev/null +++ b/src/System.IO.Pipelines/PipelineConnectionStream.cs @@ -0,0 +1,191 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + public class PipelineConnectionStream : Stream + { + private readonly static Task _initialCachedTask = Task.FromResult(0); + private Task _cachedTask = _initialCachedTask; + + private readonly IPipelineConnection _connection; + + public PipelineConnectionStream(IPipelineConnection connection) + { + _connection = connection; + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length + { + get + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + } + + public override long Position + { + get + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + set + { + ThrowHelper.ThrowNotSupportedException(); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + + public override void SetLength(long value) + { + ThrowHelper.ThrowNotSupportedException(); + } + public override int Read(byte[] buffer, int offset, int count) + { + // ValueTask uses .GetAwaiter().GetResult() if necessary + // https://github.com/dotnet/corefx/blob/f9da3b4af08214764a51b2331f3595ffaf162abe/src/System.Threading.Tasks.Extensions/src/System/Threading/Tasks/ValueTask.cs#L156 + return ReadAsync(new ArraySegment(buffer, offset, count)).Result; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var task = ReadAsync(new ArraySegment(buffer, offset, count)); + + if (task.IsCompletedSuccessfully) + { + if (_cachedTask.Result != task.Result) + { + // Needs .AsTask to match Stream's Async method return types + _cachedTask = task.AsTask(); + } + } + else + { + // Needs .AsTask to match Stream's Async method return types + _cachedTask = task.AsTask(); + } + + return _cachedTask; + } + + public override void Write(byte[] buffer, int offset, int count) + { + _connection.Output.WriteAsync(new Span(buffer, offset, count)).GetAwaiter().GetResult(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token) + { + return _connection.Output.WriteAsync(new Span(buffer, offset, count)); + } + + public override void Flush() + { + // No-op since writes are immediate. + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + // No-op since writes are immediate. + return Task.FromResult(0); + } + + private ValueTask ReadAsync(ArraySegment buffer) + { + return _connection.Input.ReadAsync(new Span(buffer.Array, buffer.Offset, buffer.Count)); + } + +#if NET451 + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + var task = ReadAsync(buffer, offset, count, default(CancellationToken), state); + return TaskToApm.Begin(task, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return TaskToApm.End(asyncResult); + } + + private Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) + { + var tcs = new TaskCompletionSource(state); + var task = ReadAsync(buffer, offset, count, cancellationToken); + task.ContinueWith((task2, state2) => + { + var tcs2 = (TaskCompletionSource)state2; + if (task2.IsCanceled) + { + tcs2.SetCanceled(); + } + else if (task2.IsFaulted) + { + tcs2.SetException(task2.Exception); + } + else + { + tcs2.SetResult(task2.Result); + } + }, tcs, cancellationToken); + return tcs.Task; + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + var task = WriteAsync(buffer, offset, count, default(CancellationToken), state); + return TaskToApm.Begin(task, callback, state); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + TaskToApm.End(asyncResult); + } + + private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) + { + var tcs = new TaskCompletionSource(state); + var task = WriteAsync(buffer, offset, count, cancellationToken); + task.ContinueWith((task2, state2) => + { + var tcs2 = (TaskCompletionSource)state2; + if (task2.IsCanceled) + { + tcs2.SetCanceled(); + } + else if (task2.IsFaulted) + { + tcs2.SetException(task2.Exception); + } + else + { + tcs2.SetResult(null); + } + }, tcs, cancellationToken); + return tcs.Task; + } +#endif + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _connection.Input.CopyToAsync(destination, bufferSize, cancellationToken); + } + + protected override void Dispose(bool disposing) + { + _connection.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines/PipelineFactory.cs b/src/System.IO.Pipelines/PipelineFactory.cs new file mode 100644 index 00000000000..cdba48ebdd2 --- /dev/null +++ b/src/System.IO.Pipelines/PipelineFactory.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + /// + /// Factory used to creaet instances of various pipelines. + /// + public class PipelineFactory : IDisposable + { + private readonly IBufferPool _pool; + + public PipelineFactory() : this(new MemoryPool()) + { + } + + public PipelineFactory(IBufferPool pool) + { + _pool = pool; + } + + public PipelineReaderWriter Create() => new PipelineReaderWriter(_pool); + + public IPipelineReader CreateReader(Stream stream) + { + if (!stream.CanRead) + { + ThrowHelper.ThrowNotSupportedException(); + } + + var output = new PipelineReaderWriter(_pool); + ExecuteCopyToAsync(output, stream); + return output; + } + + private async void ExecuteCopyToAsync(PipelineReaderWriter output, Stream stream) + { + await output.ReadingStarted; + + await stream.CopyToAsync(output); + } + + public IPipelineConnection CreateConnection(NetworkStream stream) + { + return new StreamPipelineConnection(this, stream); + } + + public IPipelineWriter CreateWriter(Stream stream) + { + if (!stream.CanWrite) + { + ThrowHelper.ThrowNotSupportedException(); + } + + var input = new PipelineReaderWriter(_pool); + + input.CopyToAsync(stream).ContinueWith((task, state) => + { + var innerInput = (PipelineReaderWriter)state; + if (task.IsFaulted) + { + innerInput.CompleteReader(task.Exception.InnerException); + } + else + { + innerInput.CompleteReader(); + } + }, + input, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + + return input; + } + + public IPipelineWriter CreateWriter(IPipelineWriter writer, Func consume) + { + var newWriter = new PipelineReaderWriter(_pool); + + consume(newWriter, writer).ContinueWith(t => + { + }); + + return newWriter; + } + + public IPipelineReader CreateReader(IPipelineReader reader, Func produce) + { + var newReader = new PipelineReaderWriter(_pool); + Execute(reader, newReader, produce); + return newReader; + } + + private async void Execute(IPipelineReader reader, PipelineReaderWriter writer, Func produce) + { + await writer.ReadingStarted; + + await produce(reader, writer); + } + + public void Dispose() => _pool.Dispose(); + } +} diff --git a/src/System.IO.Pipelines/PipelineReader.cs b/src/System.IO.Pipelines/PipelineReader.cs new file mode 100644 index 00000000000..dcf58d06a2b --- /dev/null +++ b/src/System.IO.Pipelines/PipelineReader.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + /// + /// Represents a pipeline from which data can be read. + /// + public abstract class PipelineReader : IPipelineReader + { + /// + /// The underlying the communicates over. + /// + protected readonly PipelineReaderWriter _input; + + /// + /// Creates a base . + /// + /// The that buffers will be allocated from. + protected PipelineReader(IBufferPool pool) + { + _input = new PipelineReaderWriter(pool); + } + + /// + /// Creates a base . + /// + /// The the communicates over. + protected PipelineReader(PipelineReaderWriter input) + { + _input = input; + } + + /// + /// Moves forward the pipelines read cursor to after the consumed data. + /// + /// Marks the extent of the data that has been succesfully proceesed. + /// Marks the extent of the data that has been read and examined. + /// + /// The memory for the consumed data will be released and no longer available. + /// The examined data communicates to the pipeline when it should signal more data is available. + /// + public void Advance(ReadCursor consumed, ReadCursor examined) => _input.AdvanceReader(consumed, examined); + + /// + /// Signal to the producer that the consumer is done reading. + /// + /// Optional Exception indicating a failure that's causing the pipeline to complete. + public void Complete(Exception exception = null) => _input.CompleteReader(exception); + + /// + /// Asynchronously reads a sequence of bytes from the current . + /// + /// A representing the asynchronous read operation. + public ReadableBufferAwaitable ReadAsync() => _input.ReadAsync(); + } +} diff --git a/src/System.IO.Pipelines/PipelineReaderWriter.cs b/src/System.IO.Pipelines/PipelineReaderWriter.cs new file mode 100644 index 00000000000..897ce86496f --- /dev/null +++ b/src/System.IO.Pipelines/PipelineReaderWriter.cs @@ -0,0 +1,556 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + /// + /// Default and implementation. + /// + public class PipelineReaderWriter : IPipelineReader, IPipelineWriter, IReadableBufferAwaiter + { + private static readonly Action _awaitableIsCompleted = () => { }; + private static readonly Action _awaitableIsNotCompleted = () => { }; + + private static Task _completedTask = Task.FromResult(0); + + private readonly IBufferPool _pool; + + private Action _awaitableState; + + // The read head which is the extent of the IPipelineReader's consumed bytes + private BufferSegment _readHead; + + // The commit head which is the extent of the bytes available to the IPipelineReader to consume + private BufferSegment _commitHead; + private int _commitHeadIndex; + + // The write head which is the extent of the IPipelineWriter's written bytes + private BufferSegment _writingHead; + + private int _consumingState; + private int _producingState; + private object _sync = new object(); + + // REVIEW: This object might be getting a little big :) + private readonly TaskCompletionSource _readingTcs = new TaskCompletionSource(); + private readonly TaskCompletionSource _writingTcs = new TaskCompletionSource(); + private readonly TaskCompletionSource _startingReadingTcs = new TaskCompletionSource(); +#if DEBUG + private string _consumingLocation; +#endif + /// + /// Initializes the with the specifed . + /// + /// + public PipelineReaderWriter(IBufferPool pool) + { + _pool = pool; + _awaitableState = _awaitableIsNotCompleted; + } + + /// + /// A that completes when the consumer starts consuming the . + /// + public Task ReadingStarted => _startingReadingTcs.Task; + + /// + /// Gets a task that completes when no more data will be added to the pipeline. + /// + /// This task indicates the producer has completed and will not write anymore data. + private Task Reading => _readingTcs.Task; + + /// + /// Gets a task that completes when no more data will be read from the pipeline. + /// + /// + /// This task indicates the consumer has completed and will not read anymore data. + /// When this task is triggered, the producer should stop producing data. + /// + public Task Writing => _writingTcs.Task; + + bool IReadableBufferAwaiter.IsCompleted => IsCompleted; + + private bool IsCompleted => ReferenceEquals(_awaitableState, _awaitableIsCompleted); + + internal Memory Memory => _writingHead == null ? Memory.Empty : _writingHead.Memory.Slice(_writingHead.End, _writingHead.WritableBytes); + + /// + /// Allocates memory from the pipeline to write into. + /// + /// The minimum size buffer to allocate + /// A that can be written to. + public WritableBuffer Alloc(int minimumSize = 0) + { + // CompareExchange not required as its setting to current value if test fails + if (Interlocked.Exchange(ref _producingState, State.Active) != State.NotActive) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.AlreadyProducing); + } + + if (minimumSize < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumSize); + } + else if (minimumSize > 0) + { + AllocateWriteHead(minimumSize); + } + + return new WritableBuffer(this); + } + + internal void Ensure(int count = 1) + { + EnsureAlloc(); + + var segment = _writingHead; + if (segment == null) + { + segment = AllocateWriteHead(count); + } + + var bytesLeftInBuffer = segment.WritableBytes; + + // If inadequate bytes left or if the segment is readonly + if (bytesLeftInBuffer == 0 || bytesLeftInBuffer < count || segment.ReadOnly) + { + var nextBuffer = _pool.Lease(count); + var nextSegment = new BufferSegment(nextBuffer); + + segment.Next = nextSegment; + + _writingHead = nextSegment; + } + } + + private BufferSegment AllocateWriteHead(int count) + { + BufferSegment segment = null; + + if (_commitHead != null && !_commitHead.ReadOnly) + { + // Try to return the tail so the calling code can append to it + int remaining = _commitHead.WritableBytes; + + if (count <= remaining) + { + // Free tail space of the right amount, use that + segment = _commitHead; + } + } + + if (segment == null) + { + // No free tail space, allocate a new segment + segment = new BufferSegment(_pool.Lease(count)); + } + + // Changing commit head shared with Reader + lock (_sync) + { + if (_commitHead == null) + { + // No previous writes have occurred + _commitHead = segment; + } + else if (segment != _commitHead && _commitHead.Next == null) + { + // Append the segment to the commit head if writes have been committed + // and it isn't the same segment (unused tail space) + _commitHead.Next = segment; + } + } + + // Set write head to assigned segment + _writingHead = segment; + + return segment; + } + + internal void Append(ReadableBuffer buffer) + { + if (buffer.IsEmpty) + { + return; // nothing to do + } + + EnsureAlloc(); + + BufferSegment clonedEnd; + var clonedBegin = BufferSegment.Clone(buffer.Start, buffer.End, out clonedEnd); + + if (_writingHead == null) + { + // No active write + + if (_commitHead == null) + { + // No allocated buffers yet, not locking as _readHead will be null + _commitHead = clonedBegin; + } + else + { + Debug.Assert(_commitHead.Next == null); + // Allocated buffer, append as next segment + _commitHead.Next = clonedBegin; + } + } + else + { + Debug.Assert(_writingHead.Next == null); + // Active write, append as next segment + _writingHead.Next = clonedBegin; + } + + // Move write head to end of buffer + _writingHead = clonedEnd; + } + + private void EnsureAlloc() + { + if (_producingState == State.NotActive) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.NotProducingNoAlloc); + } + } + + internal void Commit() + { + // CompareExchange not required as its setting to current value if test fails + if (Interlocked.Exchange(ref _producingState, State.NotActive) != State.Active) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.NotProducingToComplete); + } + + if (_writingHead == null) + { + // Nothing written to commit + return; + } + + // Changing commit head shared with Reader + lock (_sync) + { + if (_readHead == null) + { + // Update the head to point to the head of the buffer. + // This happens if we called alloc(0) then write + _readHead = _commitHead; + } + + // Always move the commit head to the write head + _commitHead = _writingHead; + _commitHeadIndex = _writingHead.End; + } + + // Clear the writing state + _writingHead = null; + } + + public void AdvanceWriter(int bytesWritten) + { + EnsureAlloc(); + + if (bytesWritten > 0) + { + Debug.Assert(_writingHead != null); + Debug.Assert(!_writingHead.ReadOnly); + Debug.Assert(_writingHead.Next == null); + + var buffer = _writingHead.Memory; + var bufferIndex = _writingHead.End + bytesWritten; + + Debug.Assert(bufferIndex <= buffer.Length); + + _writingHead.End = bufferIndex; + } + else if (bytesWritten < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.bytesWritten); + } // and if zero, just do nothing; don't need to validate tail etc + } + + internal Task FlushAsync() + { + if (_producingState == State.Active) + { + // Commit the data as not already committed + Commit(); + } + + return CompleteWriteAsync(); + } + + internal ReadableBuffer AsReadableBuffer() + { + if (_writingHead == null) + { + return new ReadableBuffer(); // Nothing written return empty + } + + return new ReadableBuffer(new ReadCursor(_commitHead, _commitHeadIndex), new ReadCursor(_writingHead, _writingHead.End)); + } + + private Task CompleteWriteAsync() + { + // TODO: Can factor out this lock + lock (_sync) + { + Complete(); + + // Apply back pressure here + return _completedTask; + } + } + + private void Complete() + { + var awaitableState = Interlocked.Exchange( + ref _awaitableState, + _awaitableIsCompleted); + + if (!ReferenceEquals(awaitableState, _awaitableIsCompleted) && + !ReferenceEquals(awaitableState, _awaitableIsNotCompleted)) + { + awaitableState(); + } + } + + private ReadableBuffer Read() + { + // CompareExchange not required as its setting to current value if test fails + if (Interlocked.Exchange(ref _consumingState, State.Active) != State.NotActive) + { +#if DEBUG + var message = "Already consuming."; + message += " From: " + _consumingLocation; + throw new InvalidOperationException(message); +#else + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.AlreadyConsuming); +#endif + } +#if DEBUG + _consumingLocation = Environment.StackTrace; +#endif + ReadCursor readEnd; + // Reading commit head shared with writer + lock (_sync) + { + readEnd = new ReadCursor(_commitHead, _commitHeadIndex); + } + + return new ReadableBuffer(new ReadCursor(_readHead), readEnd); + } + + void IPipelineReader.Advance(ReadCursor consumed, ReadCursor examined) => AdvanceReader(consumed, examined); + + public void AdvanceReader(ReadCursor consumed, ReadCursor examined) + { + BufferSegment returnStart = null; + BufferSegment returnEnd = null; + + if (!consumed.IsDefault) + { + returnStart = _readHead; + returnEnd = consumed.Segment; + _readHead = consumed.Segment; + _readHead.Start = consumed.Index; + } + + // Reading commit head shared with writer + lock (_sync) + { + if (!examined.IsDefault && + examined.Segment == _commitHead && + examined.Index == _commitHeadIndex && + Reading.Status == TaskStatus.WaitingForActivation) + { + Interlocked.CompareExchange( + ref _awaitableState, + _awaitableIsNotCompleted, + _awaitableIsCompleted); + } + } + + while (returnStart != returnEnd) + { + var returnSegment = returnStart; + returnStart = returnStart.Next; + returnSegment.Dispose(); + } + +#if DEBUG + _consumingLocation = null; +#endif + // CompareExchange not required as its setting to current value if test fails + if (Interlocked.Exchange(ref _consumingState, State.NotActive) != State.Active) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.NotConsumingToComplete); + } + } + + void IPipelineWriter.Complete(Exception exception) => CompleteWriter(exception); + + /// + /// Marks the pipeline as being complete, meaning no more items will be written to it. + /// + /// Optional Exception indicating a failure that's causing the pipeline to complete. + public void CompleteWriter(Exception exception = null) + { + // TODO: Review this lock? + lock (_sync) + { + SignalReader(exception); + + if (Writing.IsCompleted) + { + Dispose(); + } + } + } + + private void SignalReader(Exception exception) + { + if (exception != null) + { + _readingTcs.TrySetException(exception); + } + else + { + _readingTcs.TrySetResult(null); + } + + FlushAsync(); + } + + void IPipelineReader.Complete(Exception exception) => CompleteReader(exception); + + /// + /// Signal to the producer that the consumer is done reading. + /// + /// Optional Exception indicating a failure that's causing the pipeline to complete. + public void CompleteReader(Exception exception = null) + { + // TODO: Review this lock? + lock (_sync) + { + // Trigger this if it's never been triggered + _startingReadingTcs.TrySetResult(null); + + SignalWriter(exception); + + if (Reading.IsCompleted) + { + Dispose(); + } + } + } + + private void SignalWriter(Exception exception) + { + if (exception != null) + { + _writingTcs.TrySetException(exception); + } + else + { + _writingTcs.TrySetResult(null); + } + } + + /// + /// Asynchronously reads a sequence of bytes from the current . + /// + /// A representing the asynchronous read operation. + public ReadableBufferAwaitable ReadAsync() + { + _startingReadingTcs.TrySetResult(null); + + return new ReadableBufferAwaitable(this); + } + + void IReadableBufferAwaiter.OnCompleted(Action continuation) + { + var awaitableState = Interlocked.CompareExchange( + ref _awaitableState, + continuation, + _awaitableIsNotCompleted); + + if (ReferenceEquals(awaitableState, _awaitableIsNotCompleted)) + { + return; + } + else if (ReferenceEquals(awaitableState, _awaitableIsCompleted)) + { + // Dispatch here to avoid stack diving + // Task.Run(continuation); + continuation(); + } + else + { + _readingTcs.SetException(ThrowHelper.GetInvalidOperationException(ExceptionResource.NoConcurrentReads)); + + Interlocked.Exchange( + ref _awaitableState, + _awaitableIsCompleted); + + Task.Run(continuation); + Task.Run(awaitableState); + } + } + + ReadResult IReadableBufferAwaiter.GetResult() + { + if (!IsCompleted) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.GetResultNotCompleted); + } + + var readingIsCompleted = Reading.IsCompleted; + if (readingIsCompleted) + { + // Observe any exceptions if the reading task is completed + Reading.GetAwaiter().GetResult(); + } + + return new ReadResult(Read(), readingIsCompleted); + } + + private void Dispose() + { + Debug.Assert(Writing.IsCompleted, "Not completed writing"); + Debug.Assert(Reading.IsCompleted, "Not completed reading"); + + // TODO: Review throw if not completed? + + lock (_sync) + { + // Return all segments + var segment = _readHead; + while (segment != null) + { + var returnSegment = segment; + segment = segment.Next; + + returnSegment.Dispose(); + } + + _readHead = null; + _commitHead = null; + } + } + + // Can't use enums with Interlocked + private static class State + { + public static int NotActive = 0; + public static int Active = 1; + } + } +} diff --git a/src/System.IO.Pipelines/PipelineWriter.cs b/src/System.IO.Pipelines/PipelineWriter.cs new file mode 100644 index 00000000000..cc8444117ed --- /dev/null +++ b/src/System.IO.Pipelines/PipelineWriter.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + public abstract class PipelineWriter : IPipelineWriter + { + private readonly PipelineReaderWriter _output; + + public PipelineWriter(IBufferPool pool) + { + _output = new PipelineReaderWriter(pool); + + Consume(_output); + } + + protected abstract Task WriteAsync(ReadableBuffer buffer); + + public Task Writing => _output.Writing; + + public WritableBuffer Alloc(int minimumSize = 0) => _output.Alloc(minimumSize); + + public void Complete(Exception exception = null) => _output.CompleteWriter(exception); + + private async void Consume(IPipelineReader input) + { + while (true) + { + var result = await input.ReadAsync(); + var buffer = result.Buffer; + + try + { + if (buffer.IsEmpty && result.IsCompleted) + { + break; + } + + await WriteAsync(buffer); + } + finally + { + input.Advance(buffer.End); + } + } + + input.Complete(); + } + } +} diff --git a/src/System.IO.Pipelines/PreservedBuffer.cs b/src/System.IO.Pipelines/PreservedBuffer.cs new file mode 100644 index 00000000000..d53f6c91e07 --- /dev/null +++ b/src/System.IO.Pipelines/PreservedBuffer.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace System.IO.Pipelines +{ + /// + /// Represents a buffer that can read a sequential series of bytes. + /// + public struct PreservedBuffer : IDisposable + { + private ReadableBuffer _buffer; + + internal PreservedBuffer(ref ReadableBuffer buffer) + { + _buffer = buffer; + } + + /// + /// Returns the preserved . + /// + public ReadableBuffer Buffer => _buffer; + + /// + /// Dispose the preserved buffer. + /// + public void Dispose() + { + var returnStart = _buffer.Start.Segment; + var returnEnd = _buffer.End.Segment; + + while (true) + { + var returnSegment = returnStart; + returnStart = returnStart?.Next; + returnSegment?.Dispose(); + + if (returnSegment == returnEnd) + { + break; + } + } + + _buffer.ClearCursors(); + } + + } +} diff --git a/src/System.IO.Pipelines/Properties/AssemblyInfo.cs b/src/System.IO.Pipelines/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..251e7a05781 --- /dev/null +++ b/src/System.IO.Pipelines/Properties/AssemblyInfo.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("System.IO.Pipelines")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d6b28fc7-8d6d-4e9e-962f-9505d7c0e958")] + +[assembly: InternalsVisibleTo("System.IO.Pipelines.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100F33A29044FA9D740C9B3213A93E57C84B472C84E0B8A0E1AE48E67A9F8F6DE9D5F7F3D52AC23E48AC51801F1DC950ABE901DA34D2A9E3BAADB141A17C77EF3C565DD5EE5054B91CF63BB3C6AB83F72AB3AAFE93D0FC3C2348B764FAFB0B1C0733DE51459AEAB46580384BF9D74C4E28164B7CDE247F891BA07891C9D872AD2BB")] \ No newline at end of file diff --git a/src/System.IO.Pipelines/ReadCursor.cs b/src/System.IO.Pipelines/ReadCursor.cs new file mode 100644 index 00000000000..ab29a581408 --- /dev/null +++ b/src/System.IO.Pipelines/ReadCursor.cs @@ -0,0 +1,275 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Text; + +namespace System.IO.Pipelines +{ + public struct ReadCursor : IEquatable + { + private BufferSegment _segment; + private int _index; + + internal ReadCursor(BufferSegment segment) + { + _segment = segment; + _index = segment?.Start ?? 0; + } + + internal ReadCursor(BufferSegment segment, int index) + { + _segment = segment; + _index = index; + } + + internal BufferSegment Segment => _segment; + + internal int Index => _index; + + internal bool IsDefault => _segment == null; + + internal bool IsEnd + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var segment = _segment; + + if (segment == null) + { + return true; + } + else if (_index < segment.End) + { + return false; + } + else if (segment.Next == null) + { + return true; + } + else + { + return IsEndMultiSegment(); + } + } + } + + private bool IsEndMultiSegment() + { + var segment = _segment.Next; + while (segment != null) + { + if (segment.Start < segment.End) + { + return false; // subsequent block has data - IsEnd is false + } + segment = segment.Next; + } + return true; + } + + internal int GetLength(ReadCursor end) + { + if (IsDefault) + { + return 0; + } + + var segment = _segment; + var index = _index; + var length = 0; + checked + { + while (true) + { + if (segment == end._segment) + { + return length + end._index - index; + } + else if (segment.Next == null) + { + return length; + } + else + { + length += segment.End - index; + segment = segment.Next; + index = segment.Start; + } + } + } + } + + internal ReadCursor Seek(int bytes) + { + int count; + return Seek(bytes, out count); + } + + internal ReadCursor Seek(int bytes, out int bytesSeeked) + { + if (IsEnd) + { + bytesSeeked = 0; + return this; + } + + var wasLastSegment = _segment.Next == null; + var following = _segment.End - _index; + + if (following >= bytes) + { + bytesSeeked = bytes; + return new ReadCursor(Segment, _index + bytes); + } + + var segment = _segment; + var index = _index; + while (true) + { + if (wasLastSegment) + { + bytesSeeked = following; + return new ReadCursor(segment, index + following); + } + else + { + bytes -= following; + segment = segment.Next; + index = segment.Start; + } + + wasLastSegment = segment.Next == null; + following = segment.End - index; + + if (following >= bytes) + { + bytesSeeked = bytes; + return new ReadCursor(segment, index + bytes); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryGetBuffer(ReadCursor end, out Memory data, out ReadCursor cursor) + { + if (IsDefault) + { + data = Memory.Empty; + cursor = this; + return false; + } + + var segment = _segment; + var index = _index; + + if (end.Segment == segment) + { + var following = end.Index - index; + + if (following > 0) + { + data = segment.Memory.Slice(index, following); + cursor = new ReadCursor(segment, index + following); + return true; + } + + data = Memory.Empty; + cursor = this; + return false; + } + else + { + return TryGetBufferMultiBlock(end, out data, out cursor); + } + } + + private bool TryGetBufferMultiBlock(ReadCursor end, out Memory data, out ReadCursor cursor) + { + var segment = _segment; + var index = _index; + + // Determine if we might attempt to copy data from segment.Next before + // calculating "following" so we don't risk skipping data that could + // be added after segment.End when we decide to copy from segment.Next. + // segment.End will always be advanced before segment.Next is set. + + int following = 0; + + while (true) + { + var wasLastSegment = segment.Next == null || end.Segment == segment; + + if (end.Segment == segment) + { + following = end.Index - index; + } + else + { + following = segment.End - index; + } + + if (following > 0) + { + break; + } + + if (wasLastSegment) + { + data = Memory.Empty; + cursor = this; + return false; + } + else + { + segment = segment.Next; + index = segment.Start; + } + } + + data = segment.Memory.Slice(index, following); + cursor = new ReadCursor(segment, index + following); + return true; + } + + public override string ToString() + { + var sb = new StringBuilder(); + Span span = Segment.Memory.Span.Slice(Index, Segment.End - Index); + for (int i = 0; i < span.Length; i++) + { + sb.Append((char)span[i]); + } + return sb.ToString(); + } + + public static bool operator ==(ReadCursor c1, ReadCursor c2) + { + return c1.Equals(c2); + } + + public static bool operator !=(ReadCursor c1, ReadCursor c2) + { + return !c1.Equals(c2); + } + + public bool Equals(ReadCursor other) + { + return other._segment == _segment && other._index == _index; + } + + public override bool Equals(object obj) + { + return Equals((ReadCursor)obj); + } + + public override int GetHashCode() + { + var h1 = _segment?.GetHashCode() ?? 0; + var h2 = _index.GetHashCode(); + + var shift5 = ((uint)h1 << 5) | ((uint)h1 >> 27); + return ((int)shift5 + h1) ^ h2; + } + } +} diff --git a/src/System.IO.Pipelines/ReadResult.cs b/src/System.IO.Pipelines/ReadResult.cs new file mode 100644 index 00000000000..9603e0982ee --- /dev/null +++ b/src/System.IO.Pipelines/ReadResult.cs @@ -0,0 +1,15 @@ +namespace System.IO.Pipelines +{ + public struct ReadResult + { + public ReadResult(ReadableBuffer buffer, bool isCompleted) + { + Buffer = buffer; + IsCompleted = isCompleted; + } + + public ReadableBuffer Buffer { get; } + + public bool IsCompleted { get; } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines/ReadableBuffer.cs b/src/System.IO.Pipelines/ReadableBuffer.cs new file mode 100644 index 00000000000..5edbbfa6343 --- /dev/null +++ b/src/System.IO.Pipelines/ReadableBuffer.cs @@ -0,0 +1,569 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Collections.Sequences; +using System.Numerics; +using System.Text; + +namespace System.IO.Pipelines +{ + /// + /// Represents a buffer that can read a sequential series of bytes. + /// + public struct ReadableBuffer : ISequence> + { + private static readonly int VectorWidth = Vector.Count; + + private Memory _first; + + private ReadCursor _start; + private ReadCursor _end; + private int _length; + + /// + /// Length of the in bytes. + /// + public int Length => _length >= 0 ? _length : GetLength(); + + int? ISequence>.Length => Length; + + /// + /// Determines if the is empty. + /// + public bool IsEmpty => Length == 0; + + /// + /// Determins if the is a single . + /// + public bool IsSingleSpan => _start.Segment == _end.Segment; + + /// + /// The first in the . + /// + public Memory First => _first; + + /// + /// A cursor to the start of the . + /// + public ReadCursor Start => _start; + + /// + /// A cursor to the end of the + /// + public ReadCursor End => _end; + + internal ReadableBuffer(ReadCursor start, ReadCursor end) + { + _start = start; + _end = end; + + start.TryGetBuffer(end, out _first, out start); + + _length = -1; + } + + private ReadableBuffer(ref ReadableBuffer buffer) + { + var begin = buffer._start; + var end = buffer._end; + + BufferSegment segmentTail; + var segmentHead = BufferSegment.Clone(begin, end, out segmentTail); + + begin = new ReadCursor(segmentHead); + end = new ReadCursor(segmentTail, segmentTail.End); + + _start = begin; + _end = end; + + _length = buffer._length; + + begin.TryGetBuffer(end, out _first, out begin); + } + + /// + /// Searches for 2 sequential bytes in the and returns a sliced that + /// contains all data up to and excluding the first byte, and a that points to the second byte. + /// + /// The first byte to search for + /// The second byte to search for + /// A slice that contains all data up to and excluding the first byte. + /// A that points to the second byte + /// True if the byte sequence was found, false if not found + public unsafe bool TrySliceTo(byte b1, byte b2, out ReadableBuffer slice, out ReadCursor cursor) + { + // use address of ushort rather than stackalloc as the inliner won't inline functions with stackalloc + ushort twoBytes; + byte* byteArray = (byte*)&twoBytes; + byteArray[0] = b1; + byteArray[1] = b2; + return TrySliceTo(new Span(byteArray, 2), out slice, out cursor); + } + + /// + /// Searches for a span of bytes in the and returns a sliced that + /// contains all data up to and excluding the first byte of the span, and a that points to the last byte of the span. + /// + /// The byte to search for + /// A that matches all data up to and excluding the first byte + /// A that points to the second byte + /// True if the byte sequence was found, false if not found + public bool TrySliceTo(Span span, out ReadableBuffer slice, out ReadCursor cursor) + { + var result = false; + var buffer = this; + do + { + // Find the first byte + if (!buffer.TrySliceTo(span[0], out slice, out cursor)) + { + break; + } + + // Move the buffer to where you fonud the first byte then search for the next byte + buffer = buffer.Slice(cursor); + + if (buffer.StartsWith(span)) + { + slice = Slice(_start, cursor); + result = true; + break; + } + + // REVIEW: We need to check the performance of Slice in a loop like this + // Not a match so skip(1) + buffer = buffer.Slice(1); + } while (!buffer.IsEmpty); + + return result; + } + + /// + /// Searches for a byte in the and returns a sliced that + /// contains all data up to and excluding the byte, and a that points to the byte. + /// + /// The first byte to search for + /// A slice that contains all data up to and excluding the first byte. + /// A that points to the second byte + /// True if the byte sequence was found, false if not found + public bool TrySliceTo(byte b1, out ReadableBuffer slice, out ReadCursor cursor) + { + if (IsEmpty) + { + slice = default(ReadableBuffer); + cursor = default(ReadCursor); + return false; + } + + var byte0Vector = CommonVectors.GetVector(b1); + + var seek = 0; + + foreach (var memory in this) + { + var currentSpan = memory.Span; + var found = false; + + if (Vector.IsHardwareAccelerated) + { + while (currentSpan.Length >= VectorWidth) + { + var data = currentSpan.Read>(); + var byte0Equals = Vector.Equals(data, byte0Vector); + + if (byte0Equals.Equals(Vector.Zero)) + { + currentSpan = currentSpan.Slice(VectorWidth); + seek += VectorWidth; + } + else + { + var index = FindFirstEqualByte(ref byte0Equals); + seek += index; + found = true; + break; + } + } + } + + if (!found) + { + // Slow search + for (int i = 0; i < currentSpan.Length; i++) + { + if (currentSpan[i] == b1) + { + found = true; + break; + } + seek++; + } + } + + if (found) + { + cursor = _start.Seek(seek); + slice = Slice(_start, cursor); + return true; + } + } + + slice = default(ReadableBuffer); + cursor = default(ReadCursor); + return false; + } + + /// + /// Forms a slice out of the given , beginning at 'start', and is at most length bytes + /// + /// The index at which to begin this slice. + /// The length of the slice + public ReadableBuffer Slice(int start, int length) + { + var begin = _start.Seek(start); + return Slice(begin, begin.Seek(length)); + } + + /// + /// Forms a slice out of the given , beginning at 'start', ending at 'end' (inclusive). + /// + /// The index at which to begin this slice. + /// The end (inclusive) of the slice + public ReadableBuffer Slice(int start, ReadCursor end) + { + return Slice(_start.Seek(start), end); + } + + /// + /// Forms a slice out of the given , beginning at 'start', ending at 'end' (inclusive). + /// + /// The starting (inclusive) at which to begin this slice. + /// The ending (inclusive) of the slice + public ReadableBuffer Slice(ReadCursor start, ReadCursor end) + { + return new ReadableBuffer(start, end); + } + + /// + /// Forms a slice out of the given , beginning at 'start', and is at most length bytes + /// + /// The starting (inclusive) at which to begin this slice. + /// The length of the slice + public ReadableBuffer Slice(ReadCursor start, int length) + { + return Slice(start, start.Seek(length)); + } + + /// + /// Forms a slice out of the given , beginning at 'start', ending at the existing 's end. + /// + /// The starting (inclusive) at which to begin this slice. + public ReadableBuffer Slice(ReadCursor start) + { + return new ReadableBuffer(start, _end); + } + + /// + /// Forms a slice out of the given , beginning at 'start', ending at the existing 's end. + /// + /// The start index at which to begin this slice. + public ReadableBuffer Slice(int start) + { + if (start == 0) return this; + + return new ReadableBuffer(_start.Seek(start), _end); + } + + /// + /// Returns the first byte in the . + /// + /// -1 if the buffer is empty, the first byte otherwise. + public int Peek() + { + if (IsEmpty) + { + return -1; + } + + var span = First.Span; + return span[0]; + } + + /// + /// This transfers ownership of the buffer from the to the caller of this method. Preserved buffers must be disposed to avoid + /// memory leaks. + /// + public PreservedBuffer Preserve() + { + var buffer = new ReadableBuffer(ref this); + return new PreservedBuffer(ref buffer); + } + + /// + /// Copy the to the specified . + /// + /// The destination . + public void CopyTo(Span destination) + { + if (Length > destination.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.destination); + } + + foreach (var memory in this) + { + memory.Span.CopyTo(destination); + destination = destination.Slice(memory.Length); + } + } + + /// + /// Converts the to a + /// + public byte[] ToArray() + { + var buffer = new byte[Length]; + CopyTo(buffer); + return buffer; + } + + private int GetLength() + { + var begin = _start; + var length = begin.GetLength(_end); + _length = length; + return length; + } + + /// + /// + /// + /// + public override string ToString() + { + var sb = new StringBuilder(); + foreach (var memory in this) + { + foreach (var b in memory.Span) + { + sb.Append((char)b); + } + } + return sb.ToString(); + } + + /// + /// Returns an enumerator over the + /// + public MemoryEnumerator GetEnumerator() + { + return new MemoryEnumerator(_start, _end); + } + + /// + /// Checks to see if the starts with the specified . + /// + /// The to compare to + /// True if the bytes StartsWith, false if not + public bool StartsWith(Span value) + { + if (Length < value.Length) + { + // just nope + return false; + } + + return Slice(0, value.Length).Equals(value); + } + + /// + /// Checks to see if the is Equal to the specified . + /// + /// The to compare to + /// True if the bytes are equal, false if not + public bool Equals(Span value) + { + if (value.Length != Length) + { + return false; + } + + if (IsSingleSpan) + { + return First.Span.BlockEquals(value); + } + + foreach (var memory in this) + { + var compare = value.Slice(0, memory.Length); + if (!memory.Span.BlockEquals(compare)) + { + return false; + } + + value = value.Slice(memory.Length); + } + return true; + } + + internal void ClearCursors() + { + _start = default(ReadCursor); + _end = default(ReadCursor); + } + + /// + /// Find first byte + /// + /// + /// The first index of the result vector + /// byteEquals = 0 + internal static int FindFirstEqualByte(ref Vector byteEquals) + { + if (!BitConverter.IsLittleEndian) return FindFirstEqualByteSlow(ref byteEquals); + + // Quasi-tree search + var vector64 = Vector.AsVectorInt64(byteEquals); + for (var i = 0; i < Vector.Count; i++) + { + var longValue = vector64[i]; + if (longValue == 0) continue; + + return (i << 3) + + ((longValue & 0x00000000ffffffff) > 0 + ? (longValue & 0x000000000000ffff) > 0 + ? (longValue & 0x00000000000000ff) > 0 ? 0 : 1 + : (longValue & 0x0000000000ff0000) > 0 ? 2 : 3 + : (longValue & 0x0000ffff00000000) > 0 + ? (longValue & 0x000000ff00000000) > 0 ? 4 : 5 + : (longValue & 0x00ff000000000000) > 0 ? 6 : 7); + } + throw new InvalidOperationException(); + } + + // Internal for testing + internal static int FindFirstEqualByteSlow(ref Vector byteEquals) + { + // Quasi-tree search + var vector64 = Vector.AsVectorInt64(byteEquals); + for (var i = 0; i < Vector.Count; i++) + { + var longValue = vector64[i]; + if (longValue == 0) continue; + + var shift = i << 1; + var offset = shift << 2; + var vector32 = Vector.AsVectorInt32(byteEquals); + if (vector32[shift] != 0) + { + if (byteEquals[offset] != 0) return offset; + if (byteEquals[offset + 1] != 0) return offset + 1; + if (byteEquals[offset + 2] != 0) return offset + 2; + return offset + 3; + } + if (byteEquals[offset + 4] != 0) return offset + 4; + if (byteEquals[offset + 5] != 0) return offset + 5; + if (byteEquals[offset + 6] != 0) return offset + 6; + return offset + 7; + } + throw new InvalidOperationException(); + } + + /// + /// Create a over an array. + /// + public static ReadableBuffer Create(byte[] data) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + return Create(data, 0, data.Length); + } + + /// + /// Create a over an array. + /// + public static ReadableBuffer Create(byte[] data, int offset, int length) + { + if (data == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.data); + } + + if (offset < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.offset); + } + + if (length < 0 || (offset + length) > data.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } + + var buffer = new OwnedArray(data); + var segment = new BufferSegment(buffer); + segment.Start = offset; + segment.End = offset + length; + return new ReadableBuffer(new ReadCursor(segment, offset), new ReadCursor(segment, offset + length)); + } + + bool ISequence>.TryGet(ref Position position, out ReadOnlyMemory item, bool advance = false) + { + if (position == Position.First) + { + item = First.Slice(_start.Index); + if (advance) + { + if (_start.IsEnd) + { + position = Position.AfterLast; + } + else + { + position.ObjectPosition = _start.Segment.Next; + } + } + return true; + } + else if (position == Position.BeforeFirst) + { + if (advance) position = Position.First; + item = default(ReadOnlyMemory); + return false; + } + else if (position == Position.AfterLast) + { + item = default(ReadOnlyMemory); + return false; + } + + var currentSegment = (BufferSegment)position.ObjectPosition; + if (advance) + { + position.ObjectPosition = currentSegment.Next; + if (position.ObjectPosition == null) + { + position = Position.AfterLast; + } + } + if (currentSegment == _end.Segment) + { + item = currentSegment.Memory.Slice(currentSegment.Start, _end.Index); + } + else + { + item = currentSegment.Memory.Slice(currentSegment.Start, currentSegment.End); + } + return true; + } + + SequenceEnumerator> ISequence>.GetEnumerator() + { + return new SequenceEnumerator>(this); + } + } +} diff --git a/src/System.IO.Pipelines/ReadableBufferAwaitable.cs b/src/System.IO.Pipelines/ReadableBufferAwaitable.cs new file mode 100644 index 00000000000..5280551db14 --- /dev/null +++ b/src/System.IO.Pipelines/ReadableBufferAwaitable.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.CompilerServices; + +namespace System.IO.Pipelines +{ + /// + /// An awaitable object that represents an asynchronous read operation + /// + public struct ReadableBufferAwaitable : ICriticalNotifyCompletion + { + private readonly IReadableBufferAwaiter _awaiter; + + public ReadableBufferAwaitable(IReadableBufferAwaiter awaiter) + { + _awaiter = awaiter; + } + + public bool IsCompleted => _awaiter.IsCompleted; + + public ReadResult GetResult() => _awaiter.GetResult(); + + public ReadableBufferAwaitable GetAwaiter() => this; + + public void UnsafeOnCompleted(Action continuation) => _awaiter.OnCompleted(continuation); + + public void OnCompleted(Action continuation) => _awaiter.OnCompleted(continuation); + } +} diff --git a/src/System.IO.Pipelines/ReadableBufferReader.cs b/src/System.IO.Pipelines/ReadableBufferReader.cs new file mode 100644 index 00000000000..c0f2959a0f2 --- /dev/null +++ b/src/System.IO.Pipelines/ReadableBufferReader.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace System.IO.Pipelines +{ + public struct ReadableBufferReader + { + private Span _currentMemory; + private int _index; + private MemoryEnumerator _enumerator; + private int _overallIndex; + private bool _end; + + public ReadableBufferReader(ReadableBuffer buffer) + { + _end = false; + _index = 0; + _overallIndex = 0; + _enumerator = buffer.GetEnumerator(); + _currentMemory = default(Span); + while (_enumerator.MoveNext()) + { + if (!_enumerator.Current.IsEmpty) + { + _currentMemory = _enumerator.Current.Span; + return; + } + } + _end = true; + } + + public bool End => _end; + + public int Index => _overallIndex; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Peek() + { + if (_end) + { + return -1; + } + return _currentMemory[_index]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Take() + { + var value = Peek(); + _index++; + _overallIndex++; + + if (_index >= _currentMemory.Length) + { + MoveNext(); + } + + return value; + } + + private void MoveNext() + { + if (_enumerator.MoveNext()) + { + _currentMemory = _enumerator.Current.Span; + _index = 0; + } + else + { + _end = true; + } + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines/StreamExtensions.cs b/src/System.IO.Pipelines/StreamExtensions.cs new file mode 100644 index 00000000000..bc2062d789f --- /dev/null +++ b/src/System.IO.Pipelines/StreamExtensions.cs @@ -0,0 +1,247 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + public static class StreamExtensions + { + /// + /// Adapts a into a . + /// + /// + /// + public static IPipelineWriter AsPipelineWriter(this Stream stream) + { + return (stream as IPipelineWriter) ?? stream.AsPipelineWriter(ArrayBufferPool.Instance); + } + + /// + /// Adapts a into a . + /// + /// + /// + /// + public static IPipelineWriter AsPipelineWriter(this Stream stream, IBufferPool pool) + { + var writer = new PipelineReaderWriter(pool); + writer.CopyToAsync(stream).ContinueWith((task, state) => + { + var innerWriter = (PipelineReaderWriter)state; + + if (task.IsFaulted) + { + innerWriter.CompleteReader(task.Exception.InnerException); + } + else + { + innerWriter.CompleteReader(); + } + }, + writer, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + + return writer; + } + + /// + /// Adapts a into a . + /// + /// + /// + public static IPipelineReader AsPipelineReader(this Stream stream) => AsPipelineReader(stream, CancellationToken.None); + + /// + /// Adapts a into a . + /// + /// + /// + /// + public static IPipelineReader AsPipelineReader(this Stream stream, CancellationToken cancellationToken) + { + if (stream is IPipelineReader) + { + return (IPipelineReader)stream; + } + + var streamAdaptor = new UnownedBufferStream(stream); + streamAdaptor.Produce(cancellationToken); + return streamAdaptor.Reader; + } + + /// + /// Copies the content of a into a . + /// + /// + /// + /// + public static Task CopyToAsync(this Stream stream, IPipelineWriter writer) + { + return stream.CopyToAsync(new PipelineWriterStream(writer)); + } + + private class UnownedBufferStream : Stream + { + private readonly Stream _stream; + private readonly UnownedBufferReader _reader; + + public IPipelineReader Reader => _reader; + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + + public override long Length + { + get + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + } + + public override long Position + { + get + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + + set + { + ThrowHelper.ThrowNotSupportedException(); + } + } + + public UnownedBufferStream(Stream stream) + { + _stream = stream; + _reader = new UnownedBufferReader(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + WriteAsync(buffer, offset, count).Wait(); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await _reader.WriteAsync(new ArraySegment(buffer, offset, count), cancellationToken); + } + + // *gasp* Async Void!? It works here because we still have _reader.Writing to track completion. + internal async void Produce(CancellationToken cancellationToken) + { + // Wait for a reader + await _reader.ReadingStarted; + + try + { + // We have to provide a buffer size in order to provide a cancellation token. Weird but meh. + // 4096 is the "default" value. + await _stream.CopyToAsync(this, 4096, cancellationToken); + _reader.CompleteWriter(); + } + catch (Exception ex) + { + _reader.CompleteWriter(ex); + } + } + + public override void Flush() + { + } + + public override long Seek(long offset, SeekOrigin origin) + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + + public override void SetLength(long value) + { + ThrowHelper.ThrowNotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + } + + private class PipelineWriterStream : Stream + { + private IPipelineWriter _writer; + + public PipelineWriterStream(IPipelineWriter writer) + { + _writer = writer; + } + + public override bool CanRead => false; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length + { + get + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + } + + public override long Position + { + get + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + + set + { + ThrowHelper.ThrowNotSupportedException(); + } + } + + public override void Flush() + { + ThrowHelper.ThrowNotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + + public override long Seek(long offset, SeekOrigin origin) + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + + public override void SetLength(long value) + { + ThrowHelper.ThrowNotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + ThrowHelper.ThrowNotSupportedException(); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var output = _writer.Alloc(); + output.Write(new Span(buffer, offset, count)); + await output.FlushAsync(); + } + } + } +} diff --git a/src/System.IO.Pipelines/StreamPipelineConnection.cs b/src/System.IO.Pipelines/StreamPipelineConnection.cs new file mode 100644 index 00000000000..aa930dfa46c --- /dev/null +++ b/src/System.IO.Pipelines/StreamPipelineConnection.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; + +namespace System.IO.Pipelines +{ + internal class StreamPipelineConnection : IPipelineConnection + { + public StreamPipelineConnection(PipelineFactory factory, Stream stream) + { + Input = factory.CreateReader(stream); + Output = factory.CreateWriter(stream); + } + + public IPipelineReader Input { get; } + + public IPipelineWriter Output { get; } + + public void Dispose() + { + Input.Complete(); + Output.Complete(); + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines/System.IO.Pipelines.xproj b/src/System.IO.Pipelines/System.IO.Pipelines.xproj new file mode 100644 index 00000000000..acfbbdd667d --- /dev/null +++ b/src/System.IO.Pipelines/System.IO.Pipelines.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + a712ae2e-c98e-4e83-9b33-789141763541 + System.IO.Pipelines + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/System.IO.Pipelines/TaskToApm.cs b/src/System.IO.Pipelines/TaskToApm.cs new file mode 100644 index 00000000000..802eadf599d --- /dev/null +++ b/src/System.IO.Pipelines/TaskToApm.cs @@ -0,0 +1,190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Helper methods for using Tasks to implement the APM pattern. +// +// Example usage, wrapping a Task-returning FooAsync method with Begin/EndFoo methods: +// +// public IAsyncResult BeginFoo(..., AsyncCallback callback, object state) +// { +// Task t = FooAsync(...); +// return TaskToApm.Begin(t, callback, state); +// } +// public int EndFoo(IAsyncResult asyncResult) +// { +// return TaskToApm.End(asyncResult); +// } + +using System.Diagnostics; +using System.IO; + +namespace System.Threading.Tasks +{ + /// + /// Provides support for efficiently using Tasks to implement the APM (Begin/End) pattern. + /// + internal static class TaskToApm + { + /// + /// Marshals the Task as an IAsyncResult, using the supplied callback and state + /// to implement the APM pattern. + /// + /// The Task to be marshaled. + /// The callback to be invoked upon completion. + /// The state to be stored in the IAsyncResult. + /// An IAsyncResult to represent the task's asynchronous operation. + public static IAsyncResult Begin(Task task, AsyncCallback callback, object state) + { + Debug.Assert(task != null); + + // If the task has already completed, then since the Task's CompletedSynchronously==false + // and we want it to be true, we need to create a new IAsyncResult. (We also need the AsyncState to match.) + IAsyncResult asyncResult; + if (task.IsCompleted) + { + // Synchronous completion. + asyncResult = new TaskWrapperAsyncResult(task, state, completedSynchronously: true); + callback?.Invoke(asyncResult); + } + else + { + // For asynchronous completion we need to schedule a callback. Whether we can use the Task as the IAsyncResult + // depends on whether the Task's AsyncState has reference equality with the requested state. + asyncResult = task.AsyncState == state ? (IAsyncResult)task : new TaskWrapperAsyncResult(task, state, completedSynchronously: false); + if (callback != null) + { + InvokeCallbackWhenTaskCompletes(task, callback, asyncResult); + } + } + return asyncResult; + } + + /// Processes an IAsyncResult returned by Begin. + /// The IAsyncResult to unwrap. + public static void End(IAsyncResult asyncResult) + { + Task task; + + // If the IAsyncResult is our task-wrapping IAsyncResult, extract the Task. + var twar = asyncResult as TaskWrapperAsyncResult; + if (twar != null) + { + task = twar.Task; + Debug.Assert(task != null, "TaskWrapperAsyncResult should never wrap a null Task."); + } + else + { + // Otherwise, the IAsyncResult should be a Task. + task = asyncResult as Task; + } + + // Make sure we actually got a task, then complete the operation by waiting on it. + if (task == null) + { + throw new ArgumentNullException(); + } + + task.GetAwaiter().GetResult(); + } + + /// Processes an IAsyncResult returned by Begin. + /// The IAsyncResult to unwrap. + public static TResult End(IAsyncResult asyncResult) + { + Task task; + + // If the IAsyncResult is our task-wrapping IAsyncResult, extract the Task. + var twar = asyncResult as TaskWrapperAsyncResult; + if (twar != null) + { + task = twar.Task as Task; + Debug.Assert(twar.Task != null, "TaskWrapperAsyncResult should never wrap a null Task."); + } + else + { + // Otherwise, the IAsyncResult should be a Task. + task = asyncResult as Task; + } + + // Make sure we actually got a task, then complete the operation by waiting on it. + if (task == null) + { + throw new ArgumentNullException(); + } + + return task.GetAwaiter().GetResult(); + } + + /// Invokes the callback asynchronously when the task has completed. + /// The Task to await. + /// The callback to invoke when the Task completes. + /// The Task used as the IAsyncResult. + private static void InvokeCallbackWhenTaskCompletes(Task antecedent, AsyncCallback callback, IAsyncResult asyncResult) + { + Debug.Assert(antecedent != null); + Debug.Assert(callback != null); + Debug.Assert(asyncResult != null); + + // We use OnCompleted rather than ContinueWith in order to avoid running synchronously + // if the task has already completed by the time we get here. This is separated out into + // its own method currently so that we only pay for the closure if necessary. + antecedent.ConfigureAwait(continueOnCapturedContext: false) + .GetAwaiter() + .OnCompleted(() => callback(asyncResult)); + + // PERFORMANCE NOTE: + // Assuming we're in the default ExecutionContext, the "slow path" of an incomplete + // task will result in four allocations: the new IAsyncResult, the delegate+closure + // in this method, and the continuation object inside of OnCompleted (necessary + // to capture both the Action delegate and the ExecutionContext in a single object). + // In the future, if performance requirements drove a need, those four + // allocations could be reduced to one. This would be achieved by having TaskWrapperAsyncResult + // also implement ITaskCompletionAction (and optionally IThreadPoolWorkItem). It would need + // additional fields to store the AsyncCallback and an ExecutionContext. Once configured, + // it would be set into the Task as a continuation. Its Invoke method would then be run when + // the antecedent completed, and, doing all of the necessary work to flow ExecutionContext, + // it would invoke the AsyncCallback. It could also have a field on it for the antecedent, + // so that the End method would have access to the completed antecedent. For related examples, + // see other implementations of ITaskCompletionAction, and in particular ReadWriteTask + // used in Stream.Begin/EndXx's implementation. + } + + /// + /// Provides a simple IAsyncResult that wraps a Task. This, in effect, allows + /// for overriding what's seen for the CompletedSynchronously and AsyncState values. + /// + private sealed class TaskWrapperAsyncResult : IAsyncResult + { + /// The wrapped Task. + internal readonly Task Task; + /// The new AsyncState value. + private readonly object _state; + /// The new CompletedSynchronously value. + private readonly bool _completedSynchronously; + + /// Initializes the IAsyncResult with the Task to wrap and the overriding AsyncState and CompletedSynchronously values. + /// The Task to wrap. + /// The new AsyncState value + /// The new CompletedSynchronously value. + internal TaskWrapperAsyncResult(Task task, object state, bool completedSynchronously) + { + Debug.Assert(task != null); + Debug.Assert(!completedSynchronously || task.IsCompleted, "If completedSynchronously is true, the task must be completed."); + + this.Task = task; + _state = state; + _completedSynchronously = completedSynchronously; + } + + // The IAsyncResult implementation. + // - IsCompleted and AsyncWaitHandle just pass through to the Task. + // - AsyncState and CompletedSynchronously return the corresponding values stored in this object. + + object IAsyncResult.AsyncState { get { return _state; } } + bool IAsyncResult.CompletedSynchronously { get { return _completedSynchronously; } } + bool IAsyncResult.IsCompleted { get { return this.Task.IsCompleted; } } + WaitHandle IAsyncResult.AsyncWaitHandle { get { return ((IAsyncResult)this.Task).AsyncWaitHandle; } } + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines/ThrowHelper.cs b/src/System.IO.Pipelines/ThrowHelper.cs new file mode 100644 index 00000000000..38809aa0b43 --- /dev/null +++ b/src/System.IO.Pipelines/ThrowHelper.cs @@ -0,0 +1,131 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.IO.Pipelines +{ + internal class ThrowHelper + { + + public static void ThrowArgumentOutOfRangeException(ExceptionArgument argument) + { + throw GetArgumentOutOfRangeException(argument); + } + + public static void ThrowInvalidOperationException(ExceptionResource resource) + { + throw GetInvalidOperationException(resource); + } + + public static void ThrowArgumentNullException(ExceptionArgument argument) + { + throw GetArgumentNullException(argument); + } + + public static void ThrowNotSupportedException() + { + throw GetNotSupportedException(); + } + + public static void ThrowArgumentOutOfRangeException_BufferRequestTooLarge(int maxSize) + { + throw GetArgumentOutOfRangeException_BufferRequestTooLarge(maxSize); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static InvalidOperationException GetInvalidOperationException(ExceptionResource resource) + { + return new InvalidOperationException(GetResourceString(resource)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static NotSupportedException GetNotSupportedException() + { + return new NotSupportedException(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ArgumentNullException GetArgumentNullException(ExceptionArgument argument) + { + return new ArgumentNullException(GetArgumentName(argument)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static ArgumentOutOfRangeException GetArgumentOutOfRangeException_BufferRequestTooLarge(int maxSize) + { + return new ArgumentOutOfRangeException(GetArgumentName(ExceptionArgument.size), + $"Cannot allocate more than {maxSize} bytes in a single buffer"); + } + + private static string GetArgumentName(ExceptionArgument argument) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), + "The enum value is not defined, please check the ExceptionArgument Enum."); + + return argument.ToString(); + } + + private static string GetResourceString(ExceptionResource argument) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionResource), argument), + "The enum value is not defined, please check the ExceptionResource Enum."); + + // Should be look up with enviorment resources + string resourceString = null; + switch (argument) + { + case ExceptionResource.AlreadyProducing: + resourceString = "Already producing."; + break; + case ExceptionResource.NotProducingNoAlloc: + resourceString = "No ongoing producing operation. Make sure Alloc() was called."; + break; + case ExceptionResource.NotProducingToComplete: + resourceString = "No ongoing producing operation to complete."; + break; + case ExceptionResource.AlreadyConsuming: + resourceString = "Already consuming."; + break; + case ExceptionResource.NotConsumingToComplete: + resourceString = "No ongoing consuming operation to complete."; + break; + case ExceptionResource.NoConcurrentReads: + resourceString = "Concurrent reads are not supported."; + break; + case ExceptionResource.GetResultNotCompleted: + resourceString = "can't GetResult unless completed"; + break; + + } + return resourceString ?? $"Error ResourceKey not defined {argument}."; + } + } + + internal enum ExceptionArgument + { + minimumSize, + bytesWritten, + destination, + offset, + length, + data, + size + } + + internal enum ExceptionResource + { + AlreadyProducing, + NotProducingNoAlloc, + NotProducingToComplete, + AlreadyConsuming, + NotConsumingToComplete, + NoConcurrentReads, + GetResultNotCompleted + } +} diff --git a/src/System.IO.Pipelines/UnownedBuffer.cs b/src/System.IO.Pipelines/UnownedBuffer.cs new file mode 100644 index 00000000000..0a947bd1bad --- /dev/null +++ b/src/System.IO.Pipelines/UnownedBuffer.cs @@ -0,0 +1,28 @@ +using System; +using System.Buffers; + +namespace System.IO.Pipelines +{ + /// + /// Represents a buffer that is owned by an external component. + /// + public class UnownedBuffer : OwnedMemory + { + private ArraySegment _buffer; + + public UnownedBuffer(ArraySegment buffer) : base(buffer.Array, buffer.Offset, buffer.Count) + { + _buffer = buffer; + } + + public OwnedMemory MakeCopy(int offset, int length, out int newStart, out int newEnd) + { + // Copy to a new Owned Buffer. + var buffer = new byte[length]; + Buffer.BlockCopy(_buffer.Array, _buffer.Offset + offset, buffer, 0, length); + newStart = 0; + newEnd = length; + return new OwnedArray(buffer); + } + } +} diff --git a/src/System.IO.Pipelines/UnownedBufferReader.cs b/src/System.IO.Pipelines/UnownedBufferReader.cs new file mode 100644 index 00000000000..0480dda35f7 --- /dev/null +++ b/src/System.IO.Pipelines/UnownedBufferReader.cs @@ -0,0 +1,366 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + /// + /// Works in buffers which it does not own, as opposed to using a . Designed + /// to allow Streams to be easily adapted to via + /// + public class UnownedBufferReader : IPipelineReader, IReadableBufferAwaiter + { + private static readonly Action _awaitableIsCompleted = () => { }; + private static readonly Action _awaitableIsNotCompleted = () => { }; + + private static Task _completedTask = Task.FromResult(0); + + private Action _awaitableState; + + private BufferSegment _head; + private BufferSegment _tail; + + private bool _consuming; + + // REVIEW: This object might be getting a little big :) + private readonly TaskCompletionSource _readingTcs = new TaskCompletionSource(); + private readonly TaskCompletionSource _writingTcs = new TaskCompletionSource(); + private readonly TaskCompletionSource _startingReadingTcs = new TaskCompletionSource(); + + private Gate _readWaiting = new Gate(); + + /// + /// Constructs a new instance of + /// + public UnownedBufferReader() + { + _awaitableState = _awaitableIsNotCompleted; + } + + /// + /// A that completes when the consumer starts consuming the . + /// + public Task ReadingStarted => _startingReadingTcs.Task; + + /// + /// Gets a task that completes when no more data will be added to the pipeline. + /// + /// This task indicates the producer has completed and will not write anymore data. + public Task Reading => _readingTcs.Task; + + /// + /// Gets a task that completes when no more data will be read from the pipeline. + /// + /// + /// This task indicates the consumer has completed and will not read anymore data. + /// When this task is triggered, the producer should stop producing data. + /// + public Task Writing => _writingTcs.Task; + + bool IReadableBufferAwaiter.IsCompleted => IsCompleted; + + private bool IsCompleted => ReferenceEquals(_awaitableState, _awaitableIsCompleted); + + /// + /// Writes a new buffer into the pipeline. The task returned by this operation only completes when the next + /// Read has been queued, or the Reader has completed, since the buffer provided here needs to be kept alive + /// until the matching Read finishes (because we don't have ownership tracking when working with unowned buffers) + /// + /// + /// + /// + public async Task WriteAsync(ArraySegment buffer, CancellationToken cancellationToken) + { + using (var unowned = new UnownedBuffer(buffer)) + { + await WriteAsync(unowned, cancellationToken); + } + } + + /// + /// Writes a new buffer into the pipeline. The task returned by this operation only completes when the next + /// Read has been queued, or the Reader has completed, since the buffer provided here needs to be kept alive + /// until the matching Read finishes (because we don't have ownership tracking when working with unowned buffers) + /// + /// + /// + /// + // Called by the WRITER + public async Task WriteAsync(OwnedMemory buffer, CancellationToken cancellationToken) + { + // If Writing has stopped, why is the caller writing?? + if (Writing.Status != TaskStatus.WaitingForActivation) + { + throw new OperationCanceledException("Writing has ceased on this pipeline"); + } + + // If Reading has stopped, we cancel. We don't write unless there's a reader ready in this pipeline. + if (Reading.Status != TaskStatus.WaitingForActivation) + { + throw new OperationCanceledException("Reading has ceased on this pipeline"); + } + + // Register for cancellation on this token for the duration of the write + using (cancellationToken.Register(state => ((UnownedBufferReader)state).CancelWriter(), this)) + { + // Wait for reading to start + await ReadingStarted; + + // Cancel this task if this write is cancelled + cancellationToken.ThrowIfCancellationRequested(); + + // Allocate a new segment to hold the buffer being written. + using (var segment = new BufferSegment(buffer)) + { + segment.End = buffer.Memory.Length; + + if (_head == null || _head.ReadableBytes == 0) + { + // Update the head to point to the head of the buffer. + _head = segment; + } + else if (_tail != null) + { + // Add this segment to the end of the chain + _tail.Next = segment; + } + + // Always update tail to the buffer's tail + _tail = segment; + + // Trigger the continuation + Complete(); + + // Wait for another read to come (or for the end of Reading, which will also trigger this gate to open) in before returning + await _readWaiting; + + if (_head.ReadableBytes > 0) + { + // We need to preserve any buffers that haven't been consumed + _head = BufferSegment.Clone(new ReadCursor(_head), new ReadCursor(_tail, _tail?.End ?? 0), out _tail); + } + } + + // Cancel this task if this write is cancelled + cancellationToken.ThrowIfCancellationRequested(); + } + } + + // Called by the WRITER via WriteAsync to run the READER + private void Complete() + { + // Don't need to interlock here. We have one reader and one writer and the ReadingStarted/ReadWaiting gates + // ensure that the read is blocked while we write and vice-versa. + var awaitableState = _awaitableState; + _awaitableState = _awaitableIsCompleted; + + if (!ReferenceEquals(awaitableState, _awaitableIsCompleted) && + !ReferenceEquals(awaitableState, _awaitableIsNotCompleted)) + { + awaitableState(); + } + } + + // Called by the READER + private ReadableBuffer Read() + { + if (_consuming) + { + throw new InvalidOperationException("Cannot Read until the previous read has been acknowledged by calling Advance"); + } + _consuming = true; + + return new ReadableBuffer(new ReadCursor(_head), new ReadCursor(_tail, _tail?.End ?? 0)); + } + + // Called by the READER + void IPipelineReader.Advance(ReadCursor consumed, ReadCursor examined) + { + BufferSegment returnStart = null; + BufferSegment returnEnd = null; + + if (!consumed.IsDefault) + { + returnStart = _head; + returnEnd = consumed.Segment; + _head = consumed.Segment; + _head.Start = consumed.Index; + } + + // Again, we don't need an interlock here because Read and Write proceed serially. + if (!examined.IsDefault && + examined.IsEnd && + Reading.Status == TaskStatus.WaitingForActivation && + _awaitableState == _awaitableIsCompleted) + { + _awaitableState = _awaitableIsNotCompleted; + } + + while (returnStart != returnEnd) + { + var returnSegment = returnStart; + returnStart = returnStart.Next; + returnSegment.Dispose(); + } + + if (!_consuming) + { + throw new InvalidOperationException("No ongoing consuming operation to complete."); + } + _consuming = false; + } + + /// + /// Signal to the producer that the consumer is done reading. + /// + /// Optional Exception indicating a failure that's causing the pipeline to complete. + // Called by the READER + void IPipelineReader.Complete(Exception exception) + { + if (exception != null) + { + _writingTcs.TrySetException(exception); + } + else + { + _writingTcs.TrySetResult(null); + } + + if (Reading.IsCompleted) + { + Dispose(); + } + } + + /// + /// Marks the pipeline as being complete, meaning no more items will be written to it. + /// + /// Optional Exception indicating a failure that's causing the pipeline to complete. + // Called by the WRITER + public void CompleteWriter(Exception exception = null) + { + if (exception != null) + { + _readingTcs.TrySetException(exception); + } + else + { + _readingTcs.TrySetResult(null); + } + + // Fire the completion so the Reader knows the Writer has completed. + Complete(); + + if (Writing.IsCompleted) + { + Dispose(); + } + } + + /// + /// Asynchronously reads a sequence of bytes from the current . + /// + /// A representing the asynchronous read operation. + // Called by the READER + public ReadableBufferAwaitable ReadAsync() + { + return new ReadableBufferAwaitable(this); + } + + // Called by the READER + void IReadableBufferAwaiter.OnCompleted(Action continuation) + { + if (ReferenceEquals(_awaitableState, _awaitableIsNotCompleted)) + { + // Register our continuation + _awaitableState = continuation; + + // NOTE(anurse): NEVER open both gates at once, it can cause WriteAsync to unblock before a new reader has actually arrived. + if (!_startingReadingTcs.TrySetResult(null)) + { + // We've already started reading, so open the ReadWaiting gate instead + _readWaiting.Open(); + } + } + else if (ReferenceEquals(_awaitableState, _awaitableIsCompleted)) + { + // NOTE(anurse): This shouldn't happen because everything is serialized... IsCompleted will be true so the generated code will never call OnCompleted + + // Dispatch here to avoid stack diving + continuation(); + + // We don't open the ReadWaiting gate here because we are continuing to work with the previous buffer + // (since _awaitableState was _awaitableIsCompleted) + } + else + { + _readingTcs.SetException(new InvalidOperationException("Concurrent reads are not supported.")); + + Interlocked.Exchange( + ref _awaitableState, + _awaitableIsCompleted); + + Task.Run(continuation); + Task.Run(_awaitableState); + } + + } + + ReadResult IReadableBufferAwaiter.GetResult() + { + if (!IsCompleted) + { + throw new InvalidOperationException("can't GetResult unless completed"); + } + + var readingIsCompleted = Reading.IsCompleted; + if (readingIsCompleted) + { + // Observe any exceptions if the reading task is completed + Reading.GetAwaiter().GetResult(); + } + + return new ReadResult(Read(), readingIsCompleted); + } + + private void Dispose() + { + Debug.Assert(Writing.IsCompleted, "Not completed writing"); + Debug.Assert(Reading.IsCompleted, "Not completed reading"); + + // Return all segments + var segment = _head; + while (segment != null) + { + var returnSegment = segment; + segment = segment.Next; + + returnSegment.Dispose(); + } + + _head = null; + _tail = null; + } + + // Called by the WRITER + private void CancelWriter() + { + // Cancel the reader + _readingTcs.TrySetCanceled(); + + // Allow the reader to observe this + Complete(); + + // Allow the WriteAsync end to throw the OperationCanceledException + _readWaiting.Open(); + + if (Writing.IsCompleted) + { + Dispose(); + } + } + } +} diff --git a/src/System.IO.Pipelines/WritableBuffer.cs b/src/System.IO.Pipelines/WritableBuffer.cs new file mode 100644 index 00000000000..b9771b457a8 --- /dev/null +++ b/src/System.IO.Pipelines/WritableBuffer.cs @@ -0,0 +1,103 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + /// + /// Represents a buffer that can write a sequential series of bytes. + /// + public struct WritableBuffer : IOutput + { + private PipelineReaderWriter _output; + + internal WritableBuffer(PipelineReaderWriter output) + { + _output = output; + } + + /// + /// Available memory. + /// + public Memory Memory => _output.Memory; + + /// + /// Returns the number of bytes currently written and uncommitted. + /// + public int BytesWritten => AsReadableBuffer().Length; + + Span IOutput.Buffer => Memory.Span; + + void IOutput.Enlarge(int desiredBufferLength) => Ensure(desiredBufferLength); + + /// + /// Obtain a readable buffer over the data written but uncommitted to this buffer. + /// + public ReadableBuffer AsReadableBuffer() + { + return _output.AsReadableBuffer(); + } + + /// + /// Ensures the specified number of bytes are available. + /// Will assign more memory to the if requested amount not currently available. + /// + /// number of bytes + /// + /// Used when writing to directly. + /// + /// + /// More requested than underlying can allocate in a contiguous block. + /// + public void Ensure(int count = 1) + { + _output.Ensure(count); + } + + /// + /// Appends the to the in-place without copies. + /// + /// The to append + public void Append(ReadableBuffer buffer) + { + _output.Append(buffer); + } + + /// + /// Moves forward the underlying 's write cursor but does not commit the data. + /// + /// number of bytes to be marked as written. + /// Forwards the start of available by . + /// is larger than the current data available data. + /// is negative. + public void Advance(int bytesWritten) + { + _output.AdvanceWriter(bytesWritten); + } + + /// + /// Commits all outstanding written data to the underlying so they can be read + /// and seals the so no more data can be committed. + /// + /// + /// While an on-going conncurent read may pick up the data, should be called to signal the reader. + /// + public void Commit() + { + _output.Commit(); + } + + /// + /// Signals the data is available. + /// Will if necessary. + /// + /// A task that completes when the data is fully flushed. + public Task FlushAsync() + { + return _output.FlushAsync(); + } + } +} diff --git a/src/System.IO.Pipelines/WriteableBufferStream.cs b/src/System.IO.Pipelines/WriteableBufferStream.cs new file mode 100644 index 00000000000..1a0360dc46a --- /dev/null +++ b/src/System.IO.Pipelines/WriteableBufferStream.cs @@ -0,0 +1,167 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + public class WriteableBufferStream : Stream + { + private readonly static Task _completedTask = Task.FromResult(0); + + private WritableBuffer _buffer; + + public WriteableBufferStream(WritableBuffer buffer) + { + _buffer = buffer; + } + + public override bool CanRead => false; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length + { + get + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + } + + public override long Position + { + get + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + set + { + ThrowHelper.ThrowNotSupportedException(); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + + public override void SetLength(long value) + { + ThrowHelper.ThrowNotSupportedException(); + } + public override int Read(byte[] buffer, int offset, int count) + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ThrowHelper.ThrowNotSupportedException(); + return null; + } + + public override void Write(byte[] buffer, int offset, int count) + { + _buffer.Write(new Span(buffer, offset, count)); + // No Flush or Commit since caller may want to turn stream writes into a readable buffer. + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token) + { + _buffer.Write(new Span(buffer, offset, count)); + // No Flush or Commit since caller may want to turn stream writes into a readable buffer. + return _completedTask; + } + + public override void Flush() + { + // No Flush since caller may want to turn stream writes into a readable buffer. + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + // No Flush since caller may want to turn stream writes into a readable buffer. + return _completedTask; + } + + private ValueTask ReadAsync(ArraySegment buffer) + { + ThrowHelper.ThrowNotSupportedException(); + return default(ValueTask); + } + +#if NET451 + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + ThrowHelper.ThrowNotSupportedException(); + return null; + } + + public override int EndRead(IAsyncResult asyncResult) + { + ThrowHelper.ThrowNotSupportedException(); + return 0; + } + + private Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) + { + ThrowHelper.ThrowNotSupportedException(); + return null; + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + var task = WriteAsync(buffer, offset, count, default(CancellationToken), state); + if (callback != null) + { + task.ContinueWith(t => callback.Invoke(t)); + } + return task; + } + + public override void EndWrite(IAsyncResult asyncResult) + { + ((Task)asyncResult).GetAwaiter().GetResult(); + } + + private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) + { + var tcs = new TaskCompletionSource(state); + var task = WriteAsync(buffer, offset, count, cancellationToken); + task.ContinueWith((task2, state2) => + { + var tcs2 = (TaskCompletionSource)state2; + if (task2.IsCanceled) + { + tcs2.SetCanceled(); + } + else if (task2.IsFaulted) + { + tcs2.SetException(task2.Exception); + } + else + { + tcs2.SetResult(null); + } + }, tcs, cancellationToken); + return tcs.Task; + } +#endif + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + ThrowHelper.ThrowNotSupportedException(); + return null; + } + + protected override void Dispose(bool disposing) + { + // No Flush or Commit since caller may want to turn stream writes into a readable buffer. + } + } +} \ No newline at end of file diff --git a/src/System.IO.Pipelines/project.json b/src/System.IO.Pipelines/project.json new file mode 100644 index 00000000000..e92e8828a46 --- /dev/null +++ b/src/System.IO.Pipelines/project.json @@ -0,0 +1,39 @@ +{ + "version": "0.1.0-*", + "description": "An abstraction for doing efficient asynchronous IO", + "authors": [ + "Microsoft Corporation" + ], + "copyright": "Microsoft Corporation, All rights reserved", + "packOptions": { + "releaseNotes": "Pre-release package, for testing only", + "licenseUrl": "http://go.microsoft.com/fwlink/?LinkId=329770", + "iconUrl": "http://go.microsoft.com/fwlink/?LinkID=288859", + "projectUrl": "https://github.com/dotnet/corefxlab", + "requireLicenseAcceptance": true + }, + "buildOptions": { + "allowUnsafe": true, + "keyFile": "../../tools/Key.snk" + }, + + "dependencies": { + "NETStandard.Library": "1.6.0", + "System.Slices": { + "target": "project" + }, + "System.Binary": { + "target": "project" + }, + "System.Buffers": "4.0.0", + "System.Runtime.CompilerServices.Unsafe": "4.0.0", + "System.Numerics.Vectors": "4.1.1", + "System.Threading.Tasks.Extensions": "4.0.0", + "System.Collections.Sequences": "0.1.0-*" + }, + + "frameworks": { + "net451": {}, + "netstandard1.3": {} + } +} diff --git a/tests/System.IO.Pipelines.Tests/BufferPoolFacts.cs b/tests/System.IO.Pipelines.Tests/BufferPoolFacts.cs new file mode 100644 index 00000000000..b783fe8bc19 --- /dev/null +++ b/tests/System.IO.Pipelines.Tests/BufferPoolFacts.cs @@ -0,0 +1,53 @@ +using System.IO.Pipelines.Networking.Sockets.Internal; +using System; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class BufferPoolFacts + { + [Fact] + public void BufferPoolBasicUsage() + { + var pool = new MicroBufferPool(8, 4); + + ArraySegment[] segments = new ArraySegment[5]; + + Assert.Equal(0, pool.InUse); + Assert.Equal(4, pool.Available); + Assert.True(pool.TryTake(out segments[0])); + Assert.True(pool.TryTake(out segments[1])); + Assert.Equal(2, pool.InUse); + Assert.Equal(2, pool.Available); + Assert.True(pool.TryTake(out segments[2])); + Assert.True(pool.TryTake(out segments[3])); + Assert.False(pool.TryTake(out segments[4])); + Assert.Equal(4, pool.InUse); + Assert.Equal(0, pool.Available); + for (int i = 0; i < 4; i++) + { + Assert.Equal(i * 8, segments[i].Offset); + Assert.Equal(8, segments[i].Count); + } + + pool.Recycle(segments[3]); + pool.Recycle(segments[1]); + Assert.Equal(2, pool.InUse); + Assert.Equal(2, pool.Available); + Assert.True(pool.TryTake(out segments[1])); + Assert.True(pool.TryTake(out segments[3])); + Assert.False(pool.TryTake(out segments[4])); + Assert.Equal(4, pool.InUse); + Assert.Equal(0, pool.Available); + + Assert.Equal(24, segments[1].Offset); + Assert.Equal(8, segments[3].Offset); + for(int i = 0; i < 4; i++) + { + pool.Recycle(segments[i]); + } + Assert.Equal(0, pool.InUse); + Assert.Equal(4, pool.Available); + } + } +} diff --git a/tests/System.IO.Pipelines.Tests/PipelineReaderWriterFacts.cs b/tests/System.IO.Pipelines.Tests/PipelineReaderWriterFacts.cs new file mode 100644 index 00000000000..561db70b21b --- /dev/null +++ b/tests/System.IO.Pipelines.Tests/PipelineReaderWriterFacts.cs @@ -0,0 +1,354 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.IO.Pipelines.Text.Primitives; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class PipelineReaderWriterFacts + { + [Fact] + public async Task ReaderShouldNotGetUnflushedBytesWhenOverflowingSegments() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + + // Fill the block with stuff leaving 5 bytes at the end + var buffer = readerWriter.Alloc(1); + + var len = buffer.Memory.Length; + // Fill the buffer with garbage + // block 1 -> block2 + // [padding..hello] -> [ world ] + var paddingBytes = Enumerable.Repeat((byte)'a', len - 5).ToArray(); + buffer.Write(paddingBytes); + await buffer.FlushAsync(); + + // Write 10 and flush + buffer = readerWriter.Alloc(); + buffer.WriteLittleEndian(10); + + // Write 9 + buffer.WriteLittleEndian(9); + + // Write 8 + buffer.WriteLittleEndian(8); + + // Make sure we don't see it yet + var result = await readerWriter.ReadAsync(); + var reader = result.Buffer; + + Assert.Equal(len - 5, reader.Length); + + // Don't move + readerWriter.Advance(reader.End); + + // Now flush + await buffer.FlushAsync(); + + reader = (await readerWriter.ReadAsync()).Buffer; + + Assert.Equal(12, reader.Length); + Assert.Equal(10, reader.ReadLittleEndian()); + Assert.Equal(9, reader.Slice(4).ReadLittleEndian()); + Assert.Equal(8, reader.Slice(8).ReadLittleEndian()); + } + } + + [Fact] + public async Task ReaderShouldNotGetUnflushedBytes() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + + // Write 10 and flush + var buffer = readerWriter.Alloc(); + buffer.WriteLittleEndian(10); + await buffer.FlushAsync(); + + // Write 9 + buffer = readerWriter.Alloc(); + buffer.WriteLittleEndian(9); + + // Write 8 + buffer.WriteLittleEndian(8); + + // Make sure we don't see it yet + var result = await readerWriter.ReadAsync(); + var reader = result.Buffer; + + Assert.Equal(4, reader.Length); + Assert.Equal(10, reader.ReadLittleEndian()); + + // Don't move + readerWriter.Advance(reader.Start); + + // Now flush + await buffer.FlushAsync(); + + reader = (await readerWriter.ReadAsync()).Buffer; + + Assert.Equal(12, reader.Length); + Assert.Equal(10, reader.ReadLittleEndian()); + Assert.Equal(9, reader.Slice(4).ReadLittleEndian()); + Assert.Equal(8, reader.Slice(8).ReadLittleEndian()); + } + } + + [Fact] + public async Task ReaderShouldNotGetUnflushedBytesWithAppend() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + + // Write 10 and flush + var buffer = readerWriter.Alloc(); + buffer.WriteLittleEndian(10); + await buffer.FlushAsync(); + + // Write Hello to another pipeline and get the buffer + var bytes = Encoding.ASCII.GetBytes("Hello"); + + var c2 = factory.Create(); + await c2.WriteAsync(bytes); + var result = await c2.ReadAsync(); + var c2Buffer = result.Buffer; + + Assert.Equal(bytes.Length, c2Buffer.Length); + + // Write 9 to the buffer + buffer = readerWriter.Alloc(); + buffer.WriteLittleEndian(9); + + // Append the data from the other pipeline + buffer.Append(c2Buffer); + + // Mark it as consumed + c2.Advance(c2Buffer.End); + + // Now read and make sure we only see the comitted data + result = await readerWriter.ReadAsync(); + var reader = result.Buffer; + + Assert.Equal(4, reader.Length); + Assert.Equal(10, reader.ReadLittleEndian()); + + // Consume nothing + readerWriter.Advance(reader.Start); + + // Flush the second set of writes + await buffer.FlushAsync(); + + reader = (await readerWriter.ReadAsync()).Buffer; + + // int, int, "Hello" + Assert.Equal(13, reader.Length); + Assert.Equal(10, reader.ReadLittleEndian()); + Assert.Equal(9, reader.Slice(4).ReadLittleEndian()); + Assert.Equal("Hello", reader.Slice(8).GetUtf8String()); + } + } + + [Fact] + public async Task WritingDataMakesDataReadableViaPipeline() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var bytes = Encoding.ASCII.GetBytes("Hello World"); + + await readerWriter.WriteAsync(bytes); + var result = await readerWriter.ReadAsync(); + var buffer = result.Buffer; + + Assert.Equal(11, buffer.Length); + Assert.True(buffer.IsSingleSpan); + var array = new byte[11]; + buffer.First.Span.CopyTo(array); + Assert.Equal("Hello World", Encoding.ASCII.GetString(array)); + } + } + + [Fact] + public async Task ReadingCanBeCancelled() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var cts = new CancellationTokenSource(); + cts.Token.Register(() => + { + readerWriter.CompleteWriter(new OperationCanceledException(cts.Token)); + }); + + var ignore = Task.Run(async () => + { + await Task.Delay(1000); + cts.Cancel(); + }); + + await Assert.ThrowsAsync(async () => + { + var result = await readerWriter.ReadAsync(); + var buffer = result.Buffer; + }); + } + } + + [Fact] + public async Task HelloWorldAcrossTwoBlocks() + { + const int blockSize = 4032; + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + // block 1 -> block2 + // [padding..hello] -> [ world ] + var paddingBytes = Enumerable.Repeat((byte)'a', blockSize - 5).ToArray(); + var bytes = Encoding.ASCII.GetBytes("Hello World"); + var writeBuffer = readerWriter.Alloc(); + writeBuffer.Write(paddingBytes); + writeBuffer.Write(bytes); + await writeBuffer.FlushAsync(); + + var result = await readerWriter.ReadAsync(); + var buffer = result.Buffer; + Assert.False(buffer.IsSingleSpan); + var helloBuffer = buffer.Slice(blockSize - 5); + Assert.False(helloBuffer.IsSingleSpan); + var memory = new List>(); + foreach (var m in helloBuffer) + { + memory.Add(m); + } + var spans = memory; + Assert.Equal(2, memory.Count); + var helloBytes = new byte[spans[0].Length]; + spans[0].Span.CopyTo(helloBytes); + var worldBytes = new byte[spans[1].Length]; + spans[1].Span.CopyTo(worldBytes); + Assert.Equal("Hello", Encoding.ASCII.GetString(helloBytes)); + Assert.Equal(" World", Encoding.ASCII.GetString(worldBytes)); + } + } + + [Fact] + public async Task IndexOfNotFoundReturnsEnd() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var bytes = Encoding.ASCII.GetBytes("Hello World"); + + await readerWriter.WriteAsync(bytes); + var result = await readerWriter.ReadAsync(); + var buffer = result.Buffer; + ReadableBuffer slice; + ReadCursor cursor; + + Assert.False(buffer.TrySliceTo(10, out slice, out cursor)); + } + } + + [Fact] + public async Task FastPathIndexOfAcrossBlocks() + { + var vecUpperR = new Vector((byte)'R'); + + const int blockSize = 4032; + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + // block 1 -> block2 + // [padding..hello] -> [ world ] + var paddingBytes = Enumerable.Repeat((byte)'a', blockSize - 5).ToArray(); + var bytes = Encoding.ASCII.GetBytes("Hello World"); + var writeBuffer = readerWriter.Alloc(); + writeBuffer.Write(paddingBytes); + writeBuffer.Write(bytes); + await writeBuffer.FlushAsync(); + + var result = await readerWriter.ReadAsync(); + var buffer = result.Buffer; + ReadableBuffer slice; + ReadCursor cursor; + Assert.False(buffer.TrySliceTo((byte)'R', out slice, out cursor)); + } + } + + [Fact] + public async Task SlowPathIndexOfAcrossBlocks() + { + const int blockSize = 4032; + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + // block 1 -> block2 + // [padding..hello] -> [ world ] + var paddingBytes = Enumerable.Repeat((byte)'a', blockSize - 5).ToArray(); + var bytes = Encoding.ASCII.GetBytes("Hello World"); + var writeBuffer = readerWriter.Alloc(); + writeBuffer.Write(paddingBytes); + writeBuffer.Write(bytes); + await writeBuffer.FlushAsync(); + + var result = await readerWriter.ReadAsync(); + var buffer = result.Buffer; + ReadableBuffer slice; + ReadCursor cursor; + Assert.False(buffer.IsSingleSpan); + Assert.True(buffer.TrySliceTo((byte)' ', out slice, out cursor)); + + slice = buffer.Slice(cursor).Slice(1); + var array = slice.ToArray(); + + Assert.Equal("World", Encoding.ASCII.GetString(array)); + } + } + + [Fact] + public void AllocMoreThanPoolBlockSizeThrows() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + Assert.Throws(() => readerWriter.Alloc(8192)); + } + } + + [Fact] + public void ReadingStartedCompletesOnCompleteReader() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + + readerWriter.CompleteReader(); + + Assert.True(readerWriter.ReadingStarted.IsCompleted); + } + } + + [Fact] + public void ReadingStartedCompletesOnCallToReadAsync() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + + readerWriter.ReadAsync(); + + Assert.True(readerWriter.ReadingStarted.IsCompleted); + } + } + } +} diff --git a/tests/System.IO.Pipelines.Tests/PipelineWriterFacts.cs b/tests/System.IO.Pipelines.Tests/PipelineWriterFacts.cs new file mode 100644 index 00000000000..8643f13651f --- /dev/null +++ b/tests/System.IO.Pipelines.Tests/PipelineWriterFacts.cs @@ -0,0 +1,184 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.IO.Pipelines.Text.Primitives; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class PipelineWriterFacts + { + [Fact] + public async Task StreamAsPipelineWriter() + { + var stream = new MemoryStream(); + + var writer = stream.AsPipelineWriter(); + + var buffer = writer.Alloc(); + buffer.WriteUtf8String("Hello World"); + await buffer.FlushAsync(); + + Assert.Equal("Hello World", Encoding.UTF8.GetString(stream.ToArray())); + } + + [Fact] + public async Task StreamAsPipelineWriterTwiceWritesToSameUnderlyingStream() + { + var stream = new MemoryStream(); + + var writer = stream.AsPipelineWriter(); + + var buffer = writer.Alloc(); + buffer.WriteUtf8String("Hello World"); + await buffer.FlushAsync(); + + Assert.Equal("Hello World", Encoding.UTF8.GetString(stream.ToArray())); + + writer.Complete(); + + writer = stream.AsPipelineWriter(); + + buffer = writer.Alloc(); + buffer.WriteUtf8String("Hello World"); + await buffer.FlushAsync(); + + Assert.Equal("Hello WorldHello World", Encoding.UTF8.GetString(stream.ToArray())); + + writer.Complete(); + } + + [Fact] + public async Task StreamAsPipelineWriterWriteToWriterThenWriteToStream() + { + var stream = new MemoryStream(); + + var writer = stream.AsPipelineWriter(); + + var buffer = writer.Alloc(); + buffer.WriteUtf8String("Hello World"); + await buffer.FlushAsync(); + + Assert.Equal("Hello World", Encoding.UTF8.GetString(stream.ToArray())); + + writer.Complete(); + + var sw = new StreamWriter(stream); + sw.Write("Hello World"); + sw.Flush(); + + Assert.Equal("Hello WorldHello World", Encoding.UTF8.GetString(stream.ToArray())); + } + + [Fact] + public void StreamAsPipelineWriterNothingWrittenIfNotFlushed() + { + var stream = new MemoryStream(); + + var writer = stream.AsPipelineWriter(); + + var buffer = writer.Alloc(); + buffer.WriteUtf8String("Hello World"); + + Assert.Equal(0, stream.Length); + + writer.Complete(); + } + + [Fact] + public async Task StreamAsPipelineWriterUsesUnderlyingWriter() + { + using (var stream = new MyCustomStream()) + { + var writer = stream.AsPipelineWriter(); + + var output = writer.Alloc(); + output.WriteUtf8String("Hello World"); + await output.FlushAsync(); + writer.Complete(); + + var sw = new StreamReader(stream); + + Assert.Equal("Hello World", sw.ReadToEnd()); + } + } + + private class MyCustomStream : Stream, IPipelineWriter + { + private readonly PipelineReaderWriter _readerWriter = new PipelineReaderWriter(ArrayBufferPool.Instance); + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length + { + get + { + throw new NotSupportedException(); + } + } + + public override long Position + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + public Task Writing => _readerWriter.Writing; + + public WritableBuffer Alloc(int minimumSize = 0) + { + return _readerWriter.Alloc(minimumSize); + } + + public void Complete(Exception exception = null) + { + _readerWriter.CompleteWriter(exception); + } + + public override void Flush() + { + + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _readerWriter.ReadAsync(new Span(buffer, offset, count)).GetAwaiter().GetResult(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _readerWriter.WriteAsync(new Span(buffer, offset, count)).GetAwaiter().GetResult(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _readerWriter.CompleteReader(); + _readerWriter.CompleteWriter(); + } + } + } +} diff --git a/tests/System.IO.Pipelines.Tests/Properties/AssemblyInfo.cs b/tests/System.IO.Pipelines.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..1d671d26349 --- /dev/null +++ b/tests/System.IO.Pipelines.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("System.IO.Pipelines.Tests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b9967782-565b-4b0b-97b9-043e35022674")] diff --git a/tests/System.IO.Pipelines.Tests/ReadableBufferFacts.cs b/tests/System.IO.Pipelines.Tests/ReadableBufferFacts.cs new file mode 100644 index 00000000000..e11466bdba5 --- /dev/null +++ b/tests/System.IO.Pipelines.Tests/ReadableBufferFacts.cs @@ -0,0 +1,538 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Collections.Sequences; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO.Pipelines.Text.Primitives; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class ReadableBufferFacts + { + [Fact] + public async Task TestIndexOfWorksForAllLocations() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + const int Size = 5 * 4032; // multiple blocks + + // populate with a pile of dummy data + byte[] data = new byte[512]; + for (int i = 0; i < data.Length; i++) data[i] = 42; + int totalBytes = 0; + var writeBuffer = readerWriter.Alloc(); + for (int i = 0; i < Size / data.Length; i++) + { + writeBuffer.Write(data); + totalBytes += data.Length; + } + await writeBuffer.FlushAsync(); + + // now read it back + var result = await readerWriter.ReadAsync(); + var readBuffer = result.Buffer; + Assert.False(readBuffer.IsSingleSpan); + Assert.Equal(totalBytes, readBuffer.Length); + TestIndexOfWorksForAllLocations(ref readBuffer, 42); + } + } + + [Fact] + public async Task EqualsDetectsDeltaForAllLocations() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + + // populate with dummy data + const int DataSize = 10000; + byte[] data = new byte[DataSize]; + var rand = new Random(12345); + rand.NextBytes(data); + + var writeBuffer = readerWriter.Alloc(); + writeBuffer.Write(data); + await writeBuffer.FlushAsync(); + + // now read it back + var result = await readerWriter.ReadAsync(); + var readBuffer = result.Buffer; + Assert.False(readBuffer.IsSingleSpan); + Assert.Equal(data.Length, readBuffer.Length); + + // check the entire buffer + EqualsDetectsDeltaForAllLocations(readBuffer, data, 0, data.Length); + + // check the first 32 sub-lengths + for (int i = 0; i <= 32; i++) + { + var slice = readBuffer.Slice(0, i); + EqualsDetectsDeltaForAllLocations(slice, data, 0, i); + } + + // check the last 32 sub-lengths + for (int i = 0; i <= 32; i++) + { + var slice = readBuffer.Slice(data.Length - i, i); + EqualsDetectsDeltaForAllLocations(slice, data, data.Length - i, i); + } + } + } + + private void EqualsDetectsDeltaForAllLocations(ReadableBuffer slice, byte[] expected, int offset, int length) + { + Assert.Equal(length, slice.Length); + Assert.True(slice.Equals(new Span(expected, offset, length))); + // change one byte in buffer, for every position + for (int i = 0; i < length; i++) + { + expected[offset + i] ^= 42; + Assert.False(slice.Equals(new Span(expected, offset, length))); + expected[offset + i] ^= 42; + } + } + + [Fact] + public async Task GetUInt64GivesExpectedValues() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + + var writeBuffer = readerWriter.Alloc(); + writeBuffer.Ensure(50); + writeBuffer.Advance(50); // not even going to pretend to write data here - we're going to cheat + await writeBuffer.FlushAsync(); // by overwriting the buffer in-situ + + // now read it back + var result = await readerWriter.ReadAsync(); + var readBuffer = result.Buffer; + + ReadUInt64GivesExpectedValues(ref readBuffer); + } + } + + [Theory] + [InlineData(" hello", "hello")] + [InlineData(" hello", "hello")] + [InlineData("\r\n hello", "hello")] + [InlineData("\rhe llo", "he llo")] + [InlineData("\thell o ", "hell o ")] + public async Task TrimStartTrimsWhitespaceAtStart(string input, string expected) + { + using (var readerWriter = new PipelineFactory()) + { + var connection = readerWriter.Create(); + + var writeBuffer = connection.Alloc(); + var bytes = Encoding.ASCII.GetBytes(input); + writeBuffer.Write(bytes); + await writeBuffer.FlushAsync(); + + var result = await connection.ReadAsync(); + var buffer = result.Buffer; + var trimmed = buffer.TrimStart(); + var outputBytes = trimmed.ToArray(); + + Assert.Equal(expected, Encoding.ASCII.GetString(outputBytes)); + } + } + + [Theory] + [InlineData("hello ", "hello")] + [InlineData("hello ", "hello")] + [InlineData("hello \r\n", "hello")] + [InlineData("he llo\r", "he llo")] + [InlineData(" hell o\t", " hell o")] + public async Task TrimEndTrimsWhitespaceAtEnd(string input, string expected) + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + + var writeBuffer = readerWriter.Alloc(); + var bytes = Encoding.ASCII.GetBytes(input); + writeBuffer.Write(bytes); + await writeBuffer.FlushAsync(); + + var result = await readerWriter.ReadAsync(); + var buffer = result.Buffer; + var trimmed = buffer.TrimEnd(); + var outputBytes = trimmed.ToArray(); + + Assert.Equal(expected, Encoding.ASCII.GetString(outputBytes)); + } + } + + [Theory] + [InlineData("foo\rbar\r\n", "\r\n", "foo\rbar")] + [InlineData("foo\rbar\r\n", "\rbar", "foo")] + [InlineData("/pathpath/", "path/", "/path")] + [InlineData("hellzhello", "hell", null)] + public async Task TrySliceToSpan(string input, string sliceTo, string expected) + { + var sliceToBytes = Encoding.UTF8.GetBytes(sliceTo); + + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + + var writeBuffer = readerWriter.Alloc(); + var bytes = Encoding.UTF8.GetBytes(input); + writeBuffer.Write(bytes); + await writeBuffer.FlushAsync(); + + var result = await readerWriter.ReadAsync(); + var buffer = result.Buffer; + ReadableBuffer slice; + ReadCursor cursor; + Assert.True(buffer.TrySliceTo(sliceToBytes, out slice, out cursor)); + Assert.Equal(expected, slice.GetUtf8String()); + } + } + + private unsafe void TestIndexOfWorksForAllLocations(ref ReadableBuffer readBuffer, byte emptyValue) + { + byte huntValue = (byte)~emptyValue; + + // we're going to fully index the final locations of the buffer, so that we + // can mutate etc in constant time + var addresses = BuildPointerIndex(ref readBuffer); + + // check it isn't there to start with + ReadableBuffer slice; + ReadCursor cursor; + var found = readBuffer.TrySliceTo(huntValue, out slice, out cursor); + Assert.False(found); + + // correctness test all values + for (int i = 0; i < readBuffer.Length; i++) + { + *addresses[i] = huntValue; + found = readBuffer.TrySliceTo(huntValue, out slice, out cursor); + *addresses[i] = emptyValue; + + Assert.True(found); + var remaining = readBuffer.Slice(cursor); + void* pointer; + Assert.True(remaining.First.TryGetPointer(out pointer)); + Assert.True((byte*)pointer == addresses[i]); + } + } + + private static unsafe byte*[] BuildPointerIndex(ref ReadableBuffer readBuffer) + { + + byte*[] addresses = new byte*[readBuffer.Length]; + int index = 0; + foreach (var memory in readBuffer) + { + void* pointer; + memory.TryGetPointer(out pointer); + var ptr = (byte*)pointer; + for (int i = 0; i < memory.Length; i++) + { + addresses[index++] = ptr++; + } + } + return addresses; + } + + private unsafe void ReadUInt64GivesExpectedValues(ref ReadableBuffer readBuffer) + { + Assert.True(readBuffer.IsSingleSpan); + + for (ulong i = 0; i < 1024; i++) + { + TestValue(ref readBuffer, i); + } + TestValue(ref readBuffer, ulong.MinValue); + TestValue(ref readBuffer, ulong.MaxValue); + + var rand = new Random(41234); + // low numbers + for (int i = 0; i < 10000; i++) + { + TestValue(ref readBuffer, (ulong)rand.Next()); + } + // wider range of numbers + for (int i = 0; i < 10000; i++) + { + ulong x = (ulong)rand.Next(), y = (ulong)rand.Next(); + TestValue(ref readBuffer, (x << 32) | y); + TestValue(ref readBuffer, (y << 32) | x); + } + } + + private unsafe void TestValue(ref ReadableBuffer readBuffer, ulong value) + { + void* pointer; + Assert.True(readBuffer.First.TryGetPointer(out pointer)); + var ptr = (byte*)pointer; + string s = value.ToString(CultureInfo.InvariantCulture); + int written; + fixed (char* c = s) + { + written = Encoding.ASCII.GetBytes(c, s.Length, ptr, readBuffer.Length); + } + var slice = readBuffer.Slice(0, written); + Assert.Equal(value, slice.GetUInt64()); + } + + [Theory] + [InlineData("abc,def,ghi", ',')] + [InlineData("a;b;c;d", ';')] + [InlineData("a;b;c;d", ',')] + [InlineData("", ',')] + public Task Split(string input, char delimiter) + { + // note: different expectation to string.Split; empty has 0 outputs + var expected = input == "" ? new string[0] : input.Split(delimiter); + + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var output = readerWriter.Alloc(); + output.WriteUtf8String(input); + + var readable = output.AsReadableBuffer(); + + // via struct API + var iter = readable.Split((byte)delimiter); + Assert.Equal(expected.Length, iter.Count()); + int i = 0; + foreach (var item in iter) + { + Assert.Equal(expected[i++], item.GetUtf8String()); + } + Assert.Equal(expected.Length, i); + + // via objects/LINQ etc + IEnumerable asObject = iter; + Assert.Equal(expected.Length, asObject.Count()); + i = 0; + foreach (var item in asObject) + { + Assert.Equal(expected[i++], item.GetUtf8String()); + } + Assert.Equal(expected.Length, i); + + return output.FlushAsync(); + } + } + + [Fact] + public async Task ReadTWorksAgainstSimpleBuffers() + { + byte[] chunk = { 0, 1, 2, 3, 4, 5, 6, 7 }; + var span = new Span(chunk); + + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var output = readerWriter.Alloc(); + output.Write(span); + var readable = output.AsReadableBuffer(); + Assert.True(readable.IsSingleSpan); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + await output.FlushAsync(); + } + } + + [Fact] + public async Task ReadTWorksAgainstMultipleBuffers() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var output = readerWriter.Alloc(); + + // we're going to try to force 3 buffers for 8 bytes + output.Write(new byte[] { 0, 1, 2 }); + output.Ensure(4031); + output.Write(new byte[] { 3, 4, 5 }); + output.Ensure(4031); + output.Write(new byte[] { 6, 7, 9 }); + + var readable = output.AsReadableBuffer(); + Assert.Equal(9, readable.Length); + + int spanCount = 0; + foreach (var _ in readable) + { + spanCount++; + } + Assert.Equal(3, spanCount); + + byte[] local = new byte[9]; + readable.CopyTo(local); + var span = new Span(local); + + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + Assert.Equal(span.Read(), readable.ReadLittleEndian()); + await output.FlushAsync(); + } + } + + [Fact] + public async Task CopyToAsync() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var output = readerWriter.Alloc(); + output.WriteAsciiString("Hello World"); + await output.FlushAsync(); + var ms = new MemoryStream(); + var result = await readerWriter.ReadAsync(); + var rb = result.Buffer; + await rb.CopyToAsync(ms); + ms.Position = 0; + Assert.Equal(11, rb.Length); + Assert.Equal(11, ms.Length); + Assert.Equal(rb.ToArray(), ms.ToArray()); + Assert.Equal("Hello World", Encoding.ASCII.GetString(ms.ToArray())); + } + } + + [Fact] + public async Task CopyToAsyncNativeMemory() + { + using (var pool = new NativePool()) + using (var factory = new PipelineFactory(pool)) + { + var readerWriter = factory.Create(); + var output = readerWriter.Alloc(); + output.WriteAsciiString("Hello World"); + await output.FlushAsync(); + var ms = new MemoryStream(); + var result = await readerWriter.ReadAsync(); + var rb = result.Buffer; + await rb.CopyToAsync(ms); + ms.Position = 0; + Assert.Equal(11, rb.Length); + Assert.Equal(11, ms.Length); + Assert.Equal(rb.ToArray(), ms.ToArray()); + Assert.Equal("Hello World", Encoding.ASCII.GetString(ms.ToArray())); + } + } + + + [Fact] + public void CanUseArrayBasedReadableBuffers() + { + var data = Encoding.ASCII.GetBytes("***abc|def|ghijk****"); // note sthe padding here - verifying that it is omitted correctly + var buffer = ReadableBuffer.Create(data, 3, data.Length - 7); + Assert.Equal(13, buffer.Length); + var split = buffer.Split((byte)'|'); + Assert.Equal(3, split.Count()); + using (var iter = split.GetEnumerator()) + { + Assert.True(iter.MoveNext()); + var current = iter.Current; + Assert.Equal("abc", current.GetAsciiString()); + using (var preserved = iter.Current.Preserve()) + { + Assert.Equal("abc", preserved.Buffer.GetAsciiString()); + } + + Assert.True(iter.MoveNext()); + current = iter.Current; + Assert.Equal("def", current.GetAsciiString()); + using (var preserved = iter.Current.Preserve()) + { + Assert.Equal("def", preserved.Buffer.GetAsciiString()); + } + + Assert.True(iter.MoveNext()); + current = iter.Current; + Assert.Equal("ghijk", current.GetAsciiString()); + using (var preserved = iter.Current.Preserve()) + { + Assert.Equal("ghijk", preserved.Buffer.GetAsciiString()); + } + + Assert.False(iter.MoveNext()); + } + } + + [Fact] + public void ReadableBufferSequenceWorks() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var output = readerWriter.Alloc(); + + { + // empty buffer + var readable = output.AsReadableBuffer() as ISequence>; + var position = Position.First; + ReadOnlyMemory memory; + int spanCount = 0; + while (readable.TryGet(ref position, out memory, advance: true)) + { + spanCount++; + Assert.Equal(0, memory.Length); + } + Assert.Equal(1, spanCount); + } + + { // 3 segment buffer + output.Write(new byte[] { 1 }); + output.Ensure(4032); + output.Write(new byte[] { 2, 2 }); + output.Ensure(4031); + output.Write(new byte[] { 3, 3, 3 }); + + var readable = output.AsReadableBuffer() as ISequence>; + var position = Position.First; + ReadOnlyMemory memory; + int spanCount = 0; + while (readable.TryGet(ref position, out memory, advance: true)) + { + spanCount++; + Assert.Equal(spanCount, memory.Length); + } + Assert.Equal(3, spanCount); + } + } + } + + private class NativePool : IBufferPool + { + public void Dispose() + { + + } + + public OwnedMemory Lease(int size) + { + return NativeBufferPool.Shared.Rent(size); + } + } + } +} diff --git a/tests/System.IO.Pipelines.Tests/ReadableBufferReaderFacts.cs b/tests/System.IO.Pipelines.Tests/ReadableBufferReaderFacts.cs new file mode 100644 index 00000000000..e5f06bd3287 --- /dev/null +++ b/tests/System.IO.Pipelines.Tests/ReadableBufferReaderFacts.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class ReadableBufferReaderFacts + { + [Fact] + public void PeekReturnsByteWithoutMoving() + { + var reader = new ReadableBufferReader(ReadableBuffer.Create(new byte[] { 1, 2 }, 0, 2)); + Assert.Equal(1, reader.Peek()); + Assert.Equal(1, reader.Peek()); + } + + [Fact] + public void TakeReturnsByteAndMoves() + { + var reader = new ReadableBufferReader(ReadableBuffer.Create(new byte[] { 1, 2 }, 0, 2)); + Assert.Equal(1, reader.Take()); + Assert.Equal(2, reader.Take()); + Assert.Equal(-1, reader.Take()); + } + + [Fact] + public void PeekReturnsMinuOneByteInTheEnd() + { + var reader = new ReadableBufferReader(ReadableBuffer.Create(new byte[] { 1, 2 }, 0, 2)); + Assert.Equal(1, reader.Take()); + Assert.Equal(2, reader.Take()); + Assert.Equal(-1, reader.Peek()); + } + + [Fact] + public async Task TakeTraversesSegments() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var w = readerWriter.Alloc(); + w.Append(ReadableBuffer.Create(new byte[] { 1 }, 0, 1)); + w.Append(ReadableBuffer.Create(new byte[] { 2 }, 0, 1)); + w.Append(ReadableBuffer.Create(new byte[] { 3 }, 0, 1)); + await w.FlushAsync(); + + var result = await readerWriter.ReadAsync(); + var buffer = result.Buffer; + var reader = new ReadableBufferReader(buffer); + + Assert.Equal(1, reader.Take()); + Assert.Equal(2, reader.Take()); + Assert.Equal(3, reader.Take()); + Assert.Equal(-1, reader.Take()); + } + } + + [Fact] + public async Task PeekTraversesSegments() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var w = readerWriter.Alloc(); + w.Append(ReadableBuffer.Create(new byte[] { 1 }, 0, 1)); + w.Append(ReadableBuffer.Create(new byte[] { 2 }, 0, 1)); + await w.FlushAsync(); + + var result = await readerWriter.ReadAsync(); + var buffer = result.Buffer; + var reader = new ReadableBufferReader(buffer); + + Assert.Equal(1, reader.Take()); + Assert.Equal(2, reader.Peek()); + Assert.Equal(2, reader.Take()); + Assert.Equal(-1, reader.Peek()); + Assert.Equal(-1, reader.Take()); + } + } + + [Fact] + public async Task PeekWorkesWithEmptySegments() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var w = readerWriter.Alloc(); + w.Append(ReadableBuffer.Create(new byte[] { 0 }, 0, 0)); + w.Append(ReadableBuffer.Create(new byte[] { 1 }, 0, 1)); + await w.FlushAsync(); + + var result = await readerWriter.ReadAsync(); + var buffer = result.Buffer; + var reader = new ReadableBufferReader(buffer); + + Assert.Equal(1, reader.Peek()); + Assert.Equal(1, reader.Take()); + Assert.Equal(-1, reader.Peek()); + Assert.Equal(-1, reader.Take()); + } + } + + [Fact] + public void WorkesWithEmptyBuffer() + { + var reader = new ReadableBufferReader(ReadableBuffer.Create(new byte[] { 0 }, 0, 0)); + + Assert.Equal(-1, reader.Peek()); + Assert.Equal(-1, reader.Take()); + } + } +} diff --git a/tests/System.IO.Pipelines.Tests/SignalFacts.cs b/tests/System.IO.Pipelines.Tests/SignalFacts.cs new file mode 100644 index 00000000000..addb47d576c --- /dev/null +++ b/tests/System.IO.Pipelines.Tests/SignalFacts.cs @@ -0,0 +1,129 @@ +using System.IO.Pipelines.Networking.Sockets.Internal; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class SignalFacts + { + [Fact] + public void SignalIsNotCompletedByDefault() + { + Assert.False(new Signal().IsCompleted); + } + + [Fact] + public void SignalBecomesCompletedWhenSet() + { + var signal = new Signal(); + signal.Set(); + Assert.True(signal.IsCompleted); + } + + [Fact] + public void SignalDoesNotBecomeCompletedWhenResultFetched() + { + var signal = new Signal(); + signal.Set(); + signal.GetResult(); + Assert.True(signal.IsCompleted); + } + + [Fact] + public void AlreadySetSignalManuallyAwaitableWithoutExternalCaller() + { + // here we're simulating the thread-race scenario: + // thread A: checks IsCompleted, sees false + // thread B: sets the status + // thread A: asks for the awaiter and adds a continuation + + var signal = new Signal(); + + // A + Assert.False(signal.IsCompleted); + + // B + signal.Set(); + + int wasInvoked = 0; + + // A + signal.OnCompleted(() => + { + signal.GetResult(); // compiler awaiter always does this + Interlocked.Increment(ref wasInvoked); + }); + + Assert.Equal(1, Volatile.Read(ref wasInvoked)); + Assert.True(signal.IsCompleted); + } + + [Fact] + public async Task AlreadySetSignalCompilerAwaitableWithoutExternalCaller() + { + var signal = new Signal(); + signal.Set(); + await signal; + Assert.True(signal.IsCompleted); + signal.Reset(); + Assert.False(signal.IsCompleted); + } + + [Fact] + public async Task SignalCompilerAwaitableWithExternalCaller() + { + var signal = new Signal(); + ThreadPool.QueueUserWorkItem(_ => + { + Thread.Sleep(100); + signal.Set(); + }); + await signal; + Assert.True(signal.IsCompleted); + } + + [Fact] + public void ResetClearsContinuation() + { + var signal = new Signal(); + bool wasInvoked = false; + signal.OnCompleted(() => + { + signal.GetResult(); + wasInvoked = true; + }); + signal.Reset(); + signal.Set(); + Assert.False(wasInvoked); + } + + [Fact] + public void CallingSetTwiceHasNoBacklog() + { + var signal = new Signal(); + signal.Set(); + signal.Set(); + Assert.True(signal.IsCompleted); + signal.GetResult(); + Assert.True(signal.IsCompleted); + signal.Reset(); + Assert.False(signal.IsCompleted); // only set "once" + } + + [Fact] + public void CallingSetTwiceOnlyInvokesContinuationOnce() + { + var signal = new Signal(); + int count = 0; + + signal.OnCompleted(() => Interlocked.Increment(ref count)); + signal.Set(); + signal.GetResult(); + signal.Set(); + signal.GetResult(); + + Assert.Equal(1, Volatile.Read(ref count)); + } + } +} diff --git a/tests/System.IO.Pipelines.Tests/SocketsFacts.cs b/tests/System.IO.Pipelines.Tests/SocketsFacts.cs new file mode 100644 index 00000000000..484a52c7fa7 --- /dev/null +++ b/tests/System.IO.Pipelines.Tests/SocketsFacts.cs @@ -0,0 +1,281 @@ +using System.IO.Pipelines.Networking.Libuv; +using System.IO.Pipelines.Networking.Sockets; +using System.IO.Pipelines.Text.Primitives; +using System; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class SocketsFacts : IDisposable + { + public void Dispose() + { + // am I leaking small buffers? + Assert.Equal(0, SocketConnection.SmallBuffersInUse); + } + static readonly Span _ping = new Span(Encoding.ASCII.GetBytes("PING")), _pong = new Span(Encoding.ASCII.GetBytes("PING")); + + [Fact] + public async Task CanCreateWorkingEchoServer_PipelineLibuvServer_NonPipelineClient() + { + var endpoint = new IPEndPoint(IPAddress.Loopback, 5010); + const string MessageToSend = "Hello world!"; + string reply = null; + + using (var thread = new UvThread()) + using (var server = new UvTcpListener(thread, endpoint)) + { + server.OnConnection(Echo); + await server.StartAsync(); + + reply = SendBasicSocketMessage(endpoint, MessageToSend); + } + Assert.Equal(MessageToSend, reply); + } + + [Fact] + public async Task CanCreateWorkingEchoServer_PipelineSocketServer_PipelineSocketClient() + { + var endpoint = new IPEndPoint(IPAddress.Loopback, 5010); + const string MessageToSend = "Hello world!"; + string reply = null; + + using (var server = new SocketListener()) + { + server.OnConnection(Echo); + server.Start(endpoint); + + + using (var client = await SocketConnection.ConnectAsync(endpoint)) + { + var output = client.Output.Alloc(); + output.WriteUtf8String(MessageToSend); + await output.FlushAsync(); + client.Output.Complete(); + + while (true) + { + var result = await client.Input.ReadAsync(); + var input = result.Buffer; + + // wait for the end of the data before processing anything + if (result.IsCompleted) + { + reply = input.GetUtf8String(); + client.Input.Advance(input.End); + break; + } + else + { + client.Input.Advance(input.Start, input.End); + } + } + } + } + Assert.Equal(MessageToSend, reply); + } + + [Fact] + public void CanCreateWorkingEchoServer_PipelineSocketServer_NonPipelineClient() + { + var endpoint = new IPEndPoint(IPAddress.Loopback, 5010); + const string MessageToSend = "Hello world!"; + string reply = null; + + using (var server = new SocketListener()) + { + server.OnConnection(Echo); + server.Start(endpoint); + + reply = SendBasicSocketMessage(endpoint, MessageToSend); + } + Assert.Equal(MessageToSend, reply); + } + + [Fact] + public async Task RunStressPingPongTest_Libuv() + { + var endpoint = new IPEndPoint(IPAddress.Loopback, 5020); + + using (var thread = new UvThread()) + using (var server = new UvTcpListener(thread, endpoint)) + { + server.OnConnection(PongServer); + await server.StartAsync(); + + const int SendCount = 500, ClientCount = 5; + for (int loop = 0; loop < ClientCount; loop++) + { + using (var client = await new UvTcpClient(thread, endpoint).ConnectAsync()) + { + var tuple = await PingClient(client, SendCount); + Assert.Equal(SendCount, tuple.Item1); + Assert.Equal(SendCount, tuple.Item2); + Console.WriteLine($"Ping: {tuple.Item1}; Pong: {tuple.Item2}; Time: {tuple.Item3}ms"); + } + } + } + } + + + [Fact] + public async Task RunStressPingPongTest_Socket() + { + var endpoint = new IPEndPoint(IPAddress.Loopback, 5020); + + using (var server = new SocketListener()) + { + server.OnConnection(PongServer); + server.Start(endpoint); + + const int SendCount = 500, ClientCount = 5; + for (int loop = 0; loop < ClientCount; loop++) + { + using (var client = await SocketConnection.ConnectAsync(endpoint)) + { + var tuple = await PingClient(client, SendCount); + Assert.Equal(SendCount, tuple.Item1); + Assert.Equal(SendCount, tuple.Item2); + Console.WriteLine($"Ping: {tuple.Item1}; Pong: {tuple.Item2}; Time: {tuple.Item3}ms"); + } + } + } + } + + static async Task> PingClient(IPipelineConnection connection, int messagesToSend) + { + int count = 0; + var watch = Stopwatch.StartNew(); + int sendCount = 0, replyCount = 0; + for (int i = 0; i < messagesToSend; i++) + { + await connection.Output.WriteAsync(_ping); + sendCount++; + + bool havePong = false; + while (true) + { + var result = await connection.Input.ReadAsync(); + var inputBuffer = result.Buffer; + + if (inputBuffer.IsEmpty && result.IsCompleted) + { + connection.Input.Advance(inputBuffer.End); + break; + } + if (inputBuffer.Length < 4) + { + connection.Input.Advance(inputBuffer.Start, inputBuffer.End); + } + else + { + havePong = inputBuffer.Equals(_ping); + if (havePong) + { + count++; + } + connection.Input.Advance(inputBuffer.End); + break; + } + } + + if (havePong) + { + replyCount++; + } + else + { + break; + } + } + connection.Input.Complete(); + connection.Output.Complete(); + watch.Stop(); + + return Tuple.Create(sendCount, replyCount, (int)watch.ElapsedMilliseconds); + + } + + private static async Task PongServer(IPipelineConnection connection) + { + while (true) + { + var result = await connection.Input.ReadAsync(); + var inputBuffer = result.Buffer; + + if (inputBuffer.IsEmpty && result.IsCompleted) + { + connection.Input.Advance(inputBuffer.End); + break; + } + + if (inputBuffer.Length < 4) + { + connection.Input.Advance(inputBuffer.Start, inputBuffer.End); + } + else + { + bool isPing = inputBuffer.Equals(_ping); + if (isPing) + { + await connection.Output.WriteAsync(_pong); + } + else + { + break; + } + + connection.Input.Advance(inputBuffer.End); + } + } + } + + private static string SendBasicSocketMessage(IPEndPoint endpoint, string message) + { + // create the client the old way + using (var socket = new Socket(SocketType.Stream, ProtocolType.Tcp)) + { + socket.Connect(endpoint); + var data = Encoding.UTF8.GetBytes(message); + socket.Send(data); + socket.Shutdown(SocketShutdown.Send); + + byte[] buffer = new byte[data.Length]; + int offset = 0, bytesReceived; + while (offset <= buffer.Length + && (bytesReceived = socket.Receive(buffer, offset, buffer.Length - offset, SocketFlags.None)) > 0) + { + offset += bytesReceived; + } + socket.Shutdown(SocketShutdown.Receive); + return Encoding.UTF8.GetString(buffer, 0, offset); + } + } + + private async Task Echo(IPipelineConnection connection) + { + while (true) + { + var result = await connection.Input.ReadAsync(); + var request = result.Buffer; + + if (request.IsEmpty && result.IsCompleted) + { + connection.Input.Advance(request.End); + break; + } + + int len = request.Length; + var response = connection.Output.Alloc(); + response.Append(request); + await response.FlushAsync(); + connection.Input.Advance(request.End); + } + } + } +} diff --git a/tests/System.IO.Pipelines.Tests/System.IO.Pipelines.Tests.xproj b/tests/System.IO.Pipelines.Tests/System.IO.Pipelines.Tests.xproj new file mode 100644 index 00000000000..a9209491b28 --- /dev/null +++ b/tests/System.IO.Pipelines.Tests/System.IO.Pipelines.Tests.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 3a0c9831-f43a-420d-83b7-9ecc4eaea39d + System.IO.Pipelines.Tests + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/tests/System.IO.Pipelines.Tests/UnownedBufferReaderFacts.cs b/tests/System.IO.Pipelines.Tests/UnownedBufferReaderFacts.cs new file mode 100644 index 00000000000..6e38929686b --- /dev/null +++ b/tests/System.IO.Pipelines.Tests/UnownedBufferReaderFacts.cs @@ -0,0 +1,617 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class UnownedBufferReaderFacts + { + [Fact] + public async Task CanConsumeData() + { + var stream = new CallbackStream(async (s, token) => + { + var sw = new StreamWriter(s); + await sw.WriteAsync("Hello"); + await sw.FlushAsync(); + await sw.WriteAsync("World"); + await sw.FlushAsync(); + }); + + var reader = stream.AsPipelineReader(); + + int calls = 0; + + while (true) + { + var result = await reader.ReadAsync(); + var buffer = result.Buffer; + calls++; + if (buffer.IsEmpty && result.IsCompleted) + { + // Done + break; + } + + var segment = buffer.ToArray(); + + var data = Encoding.UTF8.GetString(segment); + if (calls == 1) + { + Assert.Equal("Hello", data); + } + else + { + Assert.Equal("World", data); + } + + reader.Advance(buffer.End); + } + } + + [Fact] + public async Task CanCancelConsumingData() + { + var cts = new CancellationTokenSource(); + var stream = new CallbackStream(async (s, token) => + { + var hello = Encoding.UTF8.GetBytes("Hello"); + var world = Encoding.UTF8.GetBytes("World"); + await s.WriteAsync(hello, 0, hello.Length, token); + cts.Cancel(); + await s.WriteAsync(world, 0, world.Length, token); + }); + + var reader = stream.AsPipelineReader(cts.Token); + + int calls = 0; + + while (true) + { + ReadResult result; + ReadableBuffer buffer; + try + { + result = await reader.ReadAsync(); + buffer = result.Buffer; + } + catch (OperationCanceledException) + { + break; + } + finally + { + calls++; + } + if (buffer.IsEmpty && result.IsCompleted) + { + // Done + break; + } + + var segment = buffer.ToArray(); + + var data = Encoding.UTF8.GetString(segment); + Assert.Equal("Hello", data); + + reader.Advance(buffer.End); + } + + Assert.Equal(2, calls); + } + + [Fact] + public async Task CanConsumeLessDataThanProduced() + { + var stream = new CallbackStream(async (s, token) => + { + var sw = new StreamWriter(s); + await sw.WriteAsync("Hello "); + await sw.FlushAsync(); + await sw.WriteAsync("World"); + await sw.FlushAsync(); + }); + + var reader = stream.AsPipelineReader(); + + int index = 0; + var message = "Hello World"; + + while (true) + { + var result = await reader.ReadAsync(); + var buffer = result.Buffer; + + if (buffer.IsEmpty && result.IsCompleted) + { + // Done + break; + } + + var ch = (char)buffer.First.Span[0]; + Assert.Equal(message[index++], ch); + reader.Advance(buffer.Start.Seek(1)); + } + + Assert.Equal(message.Length, index); + } + + [Fact] + public async Task AccessingUnownedMemoryThrowsIfUsedAfterAdvance() + { + var stream = new CallbackStream(async (s, token) => + { + var sw = new StreamWriter(s); + await sw.WriteAsync("Hello "); + await sw.FlushAsync(); + }); + + var reader = stream.AsPipelineReader(); + + var data = Memory.Empty; + + while (true) + { + var result = await reader.ReadAsync(); + var buffer = result.Buffer; + + if (buffer.IsEmpty && result.IsCompleted) + { + // Done + break; + } + data = buffer.First; + reader.Advance(buffer.End); + } + + Assert.Throws(() => data.Span); + } + + [Fact] + public async Task PreservingUnownedBufferCopies() + { + var stream = new CallbackStream(async (s, token) => + { + var sw = new StreamWriter(s); + await sw.WriteAsync("Hello "); + await sw.FlushAsync(); + }); + + var reader = stream.AsPipelineReader(); + + var preserved = default(PreservedBuffer); + + while (true) + { + var result = await reader.ReadAsync(); + var buffer = result.Buffer; + + if (buffer.IsEmpty && result.IsCompleted) + { + // Done + break; + } + + preserved = buffer.Preserve(); + + // Make sure we can acccess the span + var span = buffer.First.Span; + + reader.Advance(buffer.End); + } + + using (preserved) + { + Assert.Equal("Hello ", Encoding.UTF8.GetString(preserved.Buffer.ToArray())); + } + + Assert.Throws(() => preserved.Buffer.First.Span); + } + + [Fact] + public async Task CanConsumeLessDataThanProducedAndPreservingOwnedBuffers() + { + var stream = new CallbackStream(async (s, token) => + { + var sw = new StreamWriter(s); + await sw.WriteAsync("Hello "); + await sw.FlushAsync(); + await sw.WriteAsync("World"); + await sw.FlushAsync(); + }); + + var reader = stream.AsPipelineReader(); + + int index = 0; + var message = "Hello World"; + + while (true) + { + var result = await reader.ReadAsync(); + var buffer = result.Buffer; + + if (buffer.IsEmpty && result.IsCompleted) + { + // Done + break; + } + + using (buffer.Preserve()) + { + var ch = (char)buffer.First.Span[0]; + Assert.Equal(message[index++], ch); + reader.Advance(buffer.Start.Seek(1), buffer.End); + } + } + + Assert.Equal(message.Length, index); + } + + [Fact] + public async Task CanConsumeLessDataThanProducedAndPreservingUnOwnedBuffers() + { + var stream = new CallbackStream(async (s, token) => + { + var sw = new StreamWriter(s); + await sw.WriteAsync("Hello "); + await sw.FlushAsync(); + await sw.WriteAsync("World"); + await sw.FlushAsync(); + }); + + var reader = stream.AsPipelineReader(); + + int index = 0; + var message = "Hello World"; + + while (true) + { + var result = await reader.ReadAsync(); + var buffer = result.Buffer; + + if (buffer.IsEmpty && result.IsCompleted) + { + // Done + break; + } + + using (buffer.Preserve()) + { + var ch = (char)buffer.First.Span[0]; + Assert.Equal(message[index++], ch); + reader.Advance(buffer.Start.Seek(1)); + } + } + + Assert.Equal(message.Length, index); + } + + [Fact] + public async Task CanConsumeLessDataThanProducedWithBufferReuse() + { + var stream = new CallbackStream(async (s, token) => + { + var data = new byte[4096]; + Encoding.UTF8.GetBytes("Hello ", 0, 6, data, 0); + await s.WriteAsync(data, 0, 6); + Encoding.UTF8.GetBytes("World", 0, 5, data, 0); + await s.WriteAsync(data, 0, 5); + }); + + var reader = stream.AsPipelineReader(); + + int index = 0; + var message = "Hello World"; + + while (index <= message.Length) + { + var result = await reader.ReadAsync(); + var buffer = result.Buffer; + + var ch = Encoding.UTF8.GetString(buffer.Slice(0, index).ToArray()); + Assert.Equal(message.Substring(0, index), ch); + + // Never consume, to force buffers to be copied + reader.Advance(buffer.Start, buffer.Start.Seek(index)); + + // Yield the task. This will ensure that we don't have any Tasks idling + // around in UnownedBufferReader.OnCompleted + await Task.Yield(); + + index++; + } + + Assert.Equal(message.Length + 1, index); + } + + [Fact] + public async Task NotCallingAdvanceWillCauseReadToThrow() + { + var stream = new CallbackStream(async (s, token) => + { + var sw = new StreamWriter(s); + await sw.WriteAsync("Hello"); + await sw.FlushAsync(); + await sw.WriteAsync("World"); + await sw.FlushAsync(); + }); + + var reader = stream.AsPipelineReader(); + + int calls = 0; + + InvalidOperationException thrown = null; + while (true) + { + ReadResult result; + ReadableBuffer buffer; + try + { + result = await reader.ReadAsync(); + buffer = result.Buffer; + } + catch (InvalidOperationException ex) + { + thrown = ex; + break; + } + + calls++; + if (buffer.IsEmpty && result.IsCompleted) + { + // Done + break; + } + + var segment = buffer.ToArray(); + + var data = Encoding.UTF8.GetString(segment); + if (calls == 1) + { + Assert.Equal("Hello", data); + } + else + { + Assert.Equal("World", data); + } + } + Assert.Equal(1, calls); + Assert.NotNull(thrown); + Assert.Equal("Cannot Read until the previous read has been acknowledged by calling Advance", thrown.Message); + } + + [Fact] + public async Task StreamAsPipelineReaderUsesUnderlyingPipelineReaderIfAvailable() + { + var stream = new StreamAndPipelineReader(); + var sw = new StreamWriter(stream); + sw.Write("Hello"); + sw.Flush(); + stream.FinishWriting(); + + var reader = stream.AsPipelineReader(); + + while (true) + { + var result = await reader.ReadAsync(); + var buffer = result.Buffer; + if (buffer.IsEmpty && result.IsCompleted) + { + // Done + break; + } + + var segment = buffer.ToArray(); + + var data = Encoding.UTF8.GetString(segment); + Assert.Equal("Hello", data); + reader.Advance(buffer.End); + } + + } + + [Fact] + public async Task StreamAsPipelineReaderReadStream() + { + var stream = new StreamAndPipelineReader(); + var sw = new StreamWriter(stream); + sw.Write("Hello"); + sw.Flush(); + + var reader = stream.AsPipelineReader(); + var result = await reader.ReadAsync(); + var buffer = result.Buffer; + var segment = buffer.ToArray(); + var data = Encoding.UTF8.GetString(segment); + Assert.Equal("Hello", data); + reader.Advance(buffer.End); + + sw.Write("World"); + sw.Flush(); + stream.FinishWriting(); + + var readBuf = new byte[512]; + int read = await stream.ReadAsync(readBuf, 0, readBuf.Length); + Assert.Equal("World", Encoding.UTF8.GetString(readBuf, 0, read)); + } + + private class StreamAndPipelineReader : Stream, IPipelineReader + { + private readonly PipelineReaderWriter _readerWriter = new PipelineReaderWriter(ArrayBufferPool.Instance); + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length + { + get + { + throw new NotSupportedException(); + } + } + + public override long Position + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + public void Advance(ReadCursor consumed, ReadCursor examined) + { + _readerWriter.AdvanceReader(consumed, examined); + } + + public void Complete(Exception exception = null) + { + _readerWriter.CompleteReader(exception); + } + + public override void Flush() + { + + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _readerWriter.ReadAsync(new Span(buffer, offset, count)).GetAwaiter().GetResult(); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return await _readerWriter.ReadAsync(new Span(buffer, offset, count)); + } + + public ReadableBufferAwaitable ReadAsync() + { + return _readerWriter.ReadAsync(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _readerWriter.WriteAsync(new Span(buffer, offset, count)).GetAwaiter().GetResult(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _readerWriter.WriteAsync(new Span(buffer, offset, count)); + } + + public void FinishWriting() => _readerWriter.CompleteWriter(); + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _readerWriter.CompleteReader(); + _readerWriter.CompleteWriter(); + } + } + + private class CallbackStream : Stream + { + private readonly Func _callback; + public CallbackStream(Func callback) + { + _callback = callback; + } + + public override bool CanRead + { + get + { + throw new NotImplementedException(); + } + } + + public override bool CanSeek + { + get + { + throw new NotImplementedException(); + } + } + + public override bool CanWrite + { + get + { + throw new NotImplementedException(); + } + } + + public override long Length + { + get + { + throw new NotImplementedException(); + } + } + + public override long Position + { + get + { + throw new NotImplementedException(); + } + + set + { + throw new NotImplementedException(); + } + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _callback(destination, cancellationToken); + } + } + } +} diff --git a/tests/System.IO.Pipelines.Tests/WritableBufferFacts.cs b/tests/System.IO.Pipelines.Tests/WritableBufferFacts.cs new file mode 100644 index 00000000000..779b689dfbc --- /dev/null +++ b/tests/System.IO.Pipelines.Tests/WritableBufferFacts.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO.Pipelines.Text.Primitives; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class WritableBufferFacts + { + [Fact] + public async Task CanWriteNothingToBuffer() + { + using (var memoryPool = new MemoryPool()) + { + var readerWriter = new PipelineReaderWriter(memoryPool); + var buffer = readerWriter.Alloc(); + buffer.Advance(0); // doing nothing, the hard way + await buffer.FlushAsync(); + } + } + + [Theory] + [InlineData(1, "1")] + [InlineData(20, "20")] + [InlineData(300, "300")] + [InlineData(4000, "4000")] + [InlineData(500000, "500000")] + [InlineData(60000000000000000, "60000000000000000")] + public async Task CanWriteUInt64ToBuffer(ulong value, string valueAsString) + { + using (var memoryPool = new MemoryPool()) + { + var readerWriter = new PipelineReaderWriter(memoryPool); + var buffer = readerWriter.Alloc(); + buffer.WriteUInt64(value); + await buffer.FlushAsync(); + + var result = await readerWriter.ReadAsync(); + var inputBuffer = result.Buffer; + + Assert.Equal(valueAsString, inputBuffer.GetUtf8String()); + } + } + + [Theory] + [InlineData(5)] + [InlineData(50)] + [InlineData(500)] + [InlineData(5000)] + [InlineData(50000)] + public async Task WriteLargeDataBinary(int length) + { + byte[] data = new byte[length]; + new Random(length).NextBytes(data); + using (var memoryPool = new MemoryPool()) + { + var readerWriter = new PipelineReaderWriter(memoryPool); + + var output = readerWriter.Alloc(); + output.Write(data); + var foo = output.Memory.IsEmpty; // trying to see if .Memory breaks + await output.FlushAsync(); + readerWriter.CompleteWriter(); + + int offset = 0; + while (true) + { + var result = await readerWriter.ReadAsync(); + var input = result.Buffer; + if (input.Length == 0) break; + + Assert.True(input.Equals(new Span(data, offset, input.Length))); + offset += input.Length; + readerWriter.Advance(input.End); + } + Assert.Equal(data.Length, offset); + } + } + + [Theory] + [InlineData(5)] + [InlineData(50)] + [InlineData(500)] + [InlineData(5000)] + [InlineData(50000)] + public async Task WriteLargeDataTextUtf8(int length) + { + string data = new string('#', length); + FillRandomStringData(data, length); + using (var memoryPool = new MemoryPool()) + { + var readerWriter = new PipelineReaderWriter(memoryPool); + + var output = readerWriter.Alloc(); + output.WriteUtf8String(data); + var foo = output.Memory.IsEmpty; // trying to see if .Memory breaks + await output.FlushAsync(); + readerWriter.CompleteWriter(); + + int offset = 0; + while (true) + { + var result = await readerWriter.ReadAsync(); + var input = result.Buffer; + if (input.Length == 0) break; + + string s = ReadableBufferExtensions.GetUtf8String(input); + Assert.Equal(data.Substring(offset, input.Length), s); + offset += input.Length; + readerWriter.Advance(input.End); + } + Assert.Equal(data.Length, offset); + } + } + [Theory] + [InlineData(5)] + [InlineData(50)] + [InlineData(500)] + [InlineData(5000)] + [InlineData(50000)] + public async Task WriteLargeDataTextAscii(int length) + { + string data = new string('#', length); + FillRandomStringData(data, length); + using (var memoryPool = new MemoryPool()) + { + var readerWriter = new PipelineReaderWriter(memoryPool); + + var output = readerWriter.Alloc(); + output.WriteAsciiString(data); + var foo = output.Memory.IsEmpty; // trying to see if .Memory breaks + await output.FlushAsync(); + readerWriter.CompleteWriter(); + + int offset = 0; + while (true) + { + var result = await readerWriter.ReadAsync(); + var input = result.Buffer; + if (input.Length == 0) break; + + string s = ReadableBufferExtensions.GetAsciiString(input); + Assert.Equal(data.Substring(offset, input.Length), s); + offset += input.Length; + readerWriter.Advance(input.End); + } + Assert.Equal(data.Length, offset); + } + } + + private unsafe void FillRandomStringData(string data, int seed) + { + Random rand = new Random(seed); + fixed (char* c = data) + { + for (int i = 0; i < data.Length; i++) + { + c[i] = (char)(rand.Next(127) + 1); // want range 1-127 + } + } + } + + + [Fact] + public void CanReReadDataThatHasNotBeenCommitted_SmallData() + { + using (var memoryPool = new MemoryPool()) + { + var readerWriter = new PipelineReaderWriter(memoryPool); + var output = readerWriter.Alloc(); + + Assert.True(output.AsReadableBuffer().IsEmpty); + Assert.Equal(0, output.AsReadableBuffer().Length); + + + output.WriteUtf8String("hello world"); + var readable = output.AsReadableBuffer(); + + // check that looks about right + Assert.False(readable.IsEmpty); + Assert.Equal(11, readable.Length); + Assert.True(readable.Equals(Encoding.UTF8.GetBytes("hello world"))); + Assert.True(readable.Slice(1, 3).Equals(Encoding.UTF8.GetBytes("ell"))); + + // check it all works after we write more + output.WriteUtf8String("more data"); + + // note that the snapshotted readable should not have changed by this + Assert.False(readable.IsEmpty); + Assert.Equal(11, readable.Length); + Assert.True(readable.Equals(Encoding.UTF8.GetBytes("hello world"))); + Assert.True(readable.Slice(1, 3).Equals(Encoding.UTF8.GetBytes("ell"))); + + // if we fetch it again, we can see everything + readable = output.AsReadableBuffer(); + Assert.False(readable.IsEmpty); + Assert.Equal(20, readable.Length); + Assert.True(readable.Equals(Encoding.UTF8.GetBytes("hello worldmore data"))); + } + } + + [Fact] + public void CanReReadDataThatHasNotBeenCommitted_LargeData() + { + using (var memoryPool = new MemoryPool()) + { + var readerWriter = new PipelineReaderWriter(memoryPool); + + var output = readerWriter.Alloc(); + + byte[] predictablyGibberish = new byte[512]; + const int SEED = 1235412; + Random random = new Random(SEED); + for (int i = 0; i < 50; i++) + { + for (int j = 0; j < predictablyGibberish.Length; j++) + { + // doing it this way to be 100% sure about repeating the PRNG order + predictablyGibberish[j] = (byte)random.Next(0, 256); + } + output.Write(predictablyGibberish); + } + + var readable = output.AsReadableBuffer(); + Assert.False(readable.IsSingleSpan); + Assert.False(readable.IsEmpty); + Assert.Equal(50 * 512, readable.Length); + + random = new Random(SEED); + int correctCount = 0; + foreach (var memory in readable) + { + var span = memory.Span; + for (int i = 0; i < span.Length; i++) + { + if (span[i] == (byte)random.Next(0, 256)) correctCount++; + } + } + Assert.Equal(50 * 512, correctCount); + } + } + + [Fact] + public async Task CanAppendSelfWhileEmpty() + { // not really an expectation; just an accepted caveat + using (var memoryPool = new MemoryPool()) + { + var readerWriter = new PipelineReaderWriter(memoryPool); + + var output = readerWriter.Alloc(); + var readable = output.AsReadableBuffer(); + output.Append(readable); + Assert.Equal(0, output.AsReadableBuffer().Length); + + await output.FlushAsync(); + } + } + + [Fact] + public async Task CanAppendSelfWhileNotEmpty() + { + byte[] chunk = new byte[512]; + new Random().NextBytes(chunk); + using (var memoryPool = new MemoryPool()) + { + var readerWriter = new PipelineReaderWriter(memoryPool); + + var output = readerWriter.Alloc(); + + for (int i = 0; i < 20; i++) + { + output.Write(chunk); + } + var readable = output.AsReadableBuffer(); + Assert.Equal(512 * 20, readable.Length); + + output.Append(readable); + Assert.Equal(512 * 20, readable.Length); + + readable = output.AsReadableBuffer(); + Assert.Equal(2 * 512 * 20, readable.Length); + + await output.FlushAsync(); + } + } + + [Fact] + public void EnsureMoreThanPoolBlockSizeThrows() + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var buffer = readerWriter.Alloc(); + Assert.Throws(() => buffer.Ensure(8192)); + } + } + + public static IEnumerable HexNumbers + { + get + { + yield return new object[] { 0, "0" }; + for (int i = 1; i < 50; i++) + { + yield return new object[] { i, i.ToString("x2").TrimStart('0') }; + } + } + } + + [Theory] + [MemberData(nameof(HexNumbers))] + public void WriteHex(int value, string hex) + { + using (var factory = new PipelineFactory()) + { + var readerWriter = factory.Create(); + var buffer = readerWriter.Alloc(); + buffer.WriteHex(value); + + Assert.Equal(hex, buffer.AsReadableBuffer().GetAsciiString()); + } + } + } +} diff --git a/tests/System.IO.Pipelines.Tests/project.json b/tests/System.IO.Pipelines.Tests/project.json new file mode 100644 index 00000000000..c62e82886f1 --- /dev/null +++ b/tests/System.IO.Pipelines.Tests/project.json @@ -0,0 +1,43 @@ +{ + "buildOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk", + "allowUnsafe": true + }, + "dependencies": { + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "System.IO.Pipelines": { + "target": "project" + }, + "System.IO.Pipelines.Text.Primitives": { + "target": "project" + }, + "System.IO.Pipelines.Networking.Sockets": { + "target": "project" + }, + "System.IO.Pipelines.Networking.Libuv": { + "target": "project" + }, + "System.IO.Pipelines.Networking.Windows.RIO": { + "target": "project" + }, + "xunit": "2.2.0-beta2-build3300", + "System.Collections.Sequences": "0.1.0-*" + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0-*", + "type": "platform" + }, + "Microsoft.CodeCoverage": { + "type": "build", + "version": "1.0.1" + } + }, + "imports": "dnxcore50" + } + }, + "testRunner": "xunit" +} \ No newline at end of file