diff options
Diffstat (limited to 'lib')
61 files changed, 1734 insertions, 822 deletions
diff --git a/lib/Net.Compression/VNLib.Net.CompressionTests/VNLib.Net.CompressionTests.csproj b/lib/Net.Compression/VNLib.Net.CompressionTests/VNLib.Net.CompressionTests.csproj index adf9496..56c67ed 100644 --- a/lib/Net.Compression/VNLib.Net.CompressionTests/VNLib.Net.CompressionTests.csproj +++ b/lib/Net.Compression/VNLib.Net.CompressionTests/VNLib.Net.CompressionTests.csproj @@ -8,7 +8,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" /> <PackageReference Include="MSTest.TestAdapter" Version="3.1.1" /> <PackageReference Include="MSTest.TestFramework" Version="3.1.1" /> <PackageReference Include="coverlet.collector" Version="6.0.0"> diff --git a/lib/Net.Compression/vnlib_compress/CMakeLists.txt b/lib/Net.Compression/vnlib_compress/CMakeLists.txt index 90fe75d..a5dfaa3 100644 --- a/lib/Net.Compression/vnlib_compress/CMakeLists.txt +++ b/lib/Net.Compression/vnlib_compress/CMakeLists.txt @@ -11,7 +11,7 @@ set(VNLIB_COMPRESS_SOURCES compression.c) #set options for enable botli and zlib option(ENABLE_BROTLI "Enable brotli compression" ON) option(ENABLE_ZLIB "Enable zlib compression" ON) -option(ENABLE_RPMALLOC "Link local vnlib_rpmalloc allocator" ON) +option(ENABLE_RPMALLOC "Link local vnlib_rpmalloc allocator" OFF) #add feature specific source files to the project if(ENABLE_BROTLI) diff --git a/lib/Net.Compression/vnlib_compress/Taskfile.yaml b/lib/Net.Compression/vnlib_compress/Taskfile.yaml index 51aaf79..74ab24a 100644 --- a/lib/Net.Compression/vnlib_compress/Taskfile.yaml +++ b/lib/Net.Compression/vnlib_compress/Taskfile.yaml @@ -8,14 +8,50 @@ version: '3' +vars: + THIRD_PARTY_DIR: '../third-party' + PROJECT_NAME: 'vnlib_compress' + tasks: - + + default: + cmds: + - cmd: echo "Building vnlib_compress" + silent: true + + #make dirs on non-win + - cmd: mkdir {{.THIRD_PARTY_DIR}} + platforms: ['linux', 'darwin'] + + #make dirs on windows + - cmd: powershell -Command "mkdir {{.THIRD_PARTY_DIR}} -Force" + platforms: ['windows'] + + #clone libs + - cmd: cd {{.THIRD_PARTY_DIR}} && git clone https://github.com/cloudflare/zlib.git + ignore_error: true + + - cmd: cd {{.THIRD_PARTY_DIR}} && git clone https://github.com/google/brotli.git + ignore_error: true + + #invoke cmake for build + - cmake -B./build -DCMAKE_BUILD_TYPE=RELEASE {{.CMAKE_ARGS}} + + #build for Windows + - cmd: cd build && msbuild {{.PROJECT_NAME}}.sln /p:Configuration=release {{.BUILD_FLAGS}} + platforms: ['windows'] + + #using make + - cmd: cd build && make + platforms: ['linux', 'darwin'] + + #when build succeeds, archive the output into a tgz postbuild_success: cmds: - cmd: powershell mkdir -Force './bin' #copy source code to target - - powershell -Command "Get-ChildItem -Include *.c,*.h,*.txt -Path * | Resolve-Path -Relative | tar --files-from - -czf 'bin/src.tgz'" + - powershell -Command "tar --exclude build/* --exclude .vs/* --exclude bin/* -czvf bin/src.tgz ." postbuild_failed: cmds: [] diff --git a/lib/Net.Compression/vnlib_compress/vnlib_compress.vcxitems b/lib/Net.Compression/vnlib_compress/vnlib_compress.vcxitems index 9249ad9..0bfdcbf 100644 --- a/lib/Net.Compression/vnlib_compress/vnlib_compress.vcxitems +++ b/lib/Net.Compression/vnlib_compress/vnlib_compress.vcxitems @@ -27,4 +27,7 @@ <ItemGroup> <Text Include="$(MSBuildThisFileDirectory)CMakeLists.txt" /> </ItemGroup> + <ItemGroup> + <None Include="$(MSBuildThisFileDirectory)Taskfile.yaml" /> + </ItemGroup> </Project>
\ No newline at end of file diff --git a/lib/Net.Http/src/Core/Buffering/SplitHttpBufferElement.cs b/lib/Net.Http/src/Core/Buffering/SplitHttpBufferElement.cs index e65ffd8..103d723 100644 --- a/lib/Net.Http/src/Core/Buffering/SplitHttpBufferElement.cs +++ b/lib/Net.Http/src/Core/Buffering/SplitHttpBufferElement.cs @@ -26,6 +26,8 @@ using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using VNLib.Utils.Memory; + namespace VNLib.Net.Http.Core.Buffering { internal abstract class SplitHttpBufferElement : HttpBufferElement, ISplitHttpBuffer @@ -65,6 +67,6 @@ namespace VNLib.Net.Http.Core.Buffering /// </summary> /// <param name="binSize">The desired size of the binary buffer</param> /// <returns>The total size of the binary buffer required to store the binary and character buffer</returns> - public static int GetfullSize(int binSize) => binSize + (binSize * sizeof(char)); + public static int GetfullSize(int binSize) => binSize + MemoryUtil.ByteCount<char>(binSize); } } diff --git a/lib/Net.Http/src/Core/TransportReader.cs b/lib/Net.Http/src/Core/TransportReader.cs index 58c23df..8d605d1 100644 --- a/lib/Net.Http/src/Core/TransportReader.cs +++ b/lib/Net.Http/src/Core/TransportReader.cs @@ -25,6 +25,9 @@ using System; using System.IO; using System.Text; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using VNLib.Utils; using VNLib.Utils.IO; @@ -36,8 +39,17 @@ namespace VNLib.Net.Http.Core /// <summary> /// Structure implementation of <see cref="IVnTextReader"/> /// </summary> - internal struct TransportReader : IVnTextReader + internal readonly struct TransportReader : IVnTextReader { + /* + * To make this structure read-only we can store the + * mutable values in a private segment of the internal + * buffer. 8 bytes are reserved at the beining and an + * additional word is added for padding incase small/wild + * under/over run occurs. + */ + const int PrivateBufferOffset = 4 * sizeof(int); + ///<inheritdoc/> public readonly Encoding Encoding { get; } @@ -47,10 +59,25 @@ namespace VNLib.Net.Http.Core ///<inheritdoc/> public readonly Stream BaseStream { get; } + /* + * Store the window start/end in the begging of the + * data buffer. Then use a constant offset to get the + * start of the buffer + */ + private readonly int BufWindowStart + { + get => MemoryMarshal.Read<int>(Buffer.GetBinSpan()); + set => MemoryMarshal.Write(Buffer.GetBinSpan(), ref value); + } + + private readonly int BufWindowEnd + { + get => MemoryMarshal.Read<int>(Buffer.GetBinSpan()[sizeof(int)..]); + set => MemoryMarshal.Write(Buffer.GetBinSpan()[sizeof(int)..], ref value); + } + private readonly IHttpHeaderParseBuffer Buffer; - - private int BufWindowStart; - private int BufWindowEnd; + private readonly int MAxBufferSize; /// <summary> /// Initializes a new <see cref="TransportReader"/> for reading text lines from the transport stream @@ -61,29 +88,50 @@ namespace VNLib.Net.Http.Core /// <param name="lineTermination">The line delimiter to search for</param> public TransportReader(Stream transport, IHttpHeaderParseBuffer buffer, Encoding encoding, ReadOnlyMemory<byte> lineTermination) { - BufWindowEnd = 0; - BufWindowStart = 0; Encoding = encoding; BaseStream = transport; LineTermination = lineTermination; Buffer = buffer; + MAxBufferSize = buffer.BinSize - PrivateBufferOffset; + + //Initialize the buffer window + SafeZeroPrivateSegments(Buffer); + + Debug.Assert(BufWindowEnd == 0 && BufWindowStart == 0); } + + /// <summary> + /// Clears the initial window start/end values with the + /// extra padding + /// </summary> + /// <param name="buffer">The buffer segment to initialize</param> + private static void SafeZeroPrivateSegments(IHttpHeaderParseBuffer buffer) + { + ref byte start = ref MemoryMarshal.GetReference(buffer.GetBinSpan()); + Unsafe.InitBlock(ref start, 0, PrivateBufferOffset); + } + + /// <summary> + /// Gets the data segment of the buffer after the private segment + /// </summary> + /// <returns></returns> + private readonly Span<byte> GetDataSegment() => Buffer.GetBinSpan()[PrivateBufferOffset..]; ///<inheritdoc/> public readonly int Available => BufWindowEnd - BufWindowStart; ///<inheritdoc/> - public readonly Span<byte> BufferedDataWindow => Buffer.GetBinSpan()[BufWindowStart..BufWindowEnd]; + public readonly Span<byte> BufferedDataWindow => GetDataSegment()[BufWindowStart..BufWindowEnd]; ///<inheritdoc/> - public void Advance(int count) => BufWindowStart += count; + public readonly void Advance(int count) => BufWindowStart += count; ///<inheritdoc/> - public void FillBuffer() + public readonly void FillBuffer() { //Get a buffer from the end of the current window to the end of the buffer - Span<byte> bufferWindow = Buffer.GetBinSpan()[BufWindowEnd..]; + Span<byte> bufferWindow = GetDataSegment()[BufWindowEnd..]; //Read from stream int read = BaseStream.Read(bufferWindow); @@ -93,15 +141,15 @@ namespace VNLib.Net.Http.Core } ///<inheritdoc/> - public ERRNO CompactBufferWindow() + public readonly ERRNO CompactBufferWindow() { //No data to compact if window is not shifted away from start if (BufWindowStart > 0) { //Get span over engire buffer - Span<byte> buffer = Buffer.GetBinSpan(); + Span<byte> buffer = GetDataSegment(); - //Get data within window + //Get used data segment within window Span<byte> usedData = buffer[BufWindowStart..BufWindowEnd]; //Copy remaining to the begining of the buffer @@ -115,7 +163,7 @@ namespace VNLib.Net.Http.Core } //Return the number of bytes of available space from the end of the current window - return Buffer.BinSize - BufWindowEnd; + return MAxBufferSize - BufWindowEnd; } } } diff --git a/lib/Net.Messaging.FBM/src/Client/FBMBuffer.cs b/lib/Net.Messaging.FBM/src/Client/FBMBuffer.cs index fac41a6..698a98b 100644 --- a/lib/Net.Messaging.FBM/src/Client/FBMBuffer.cs +++ b/lib/Net.Messaging.FBM/src/Client/FBMBuffer.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM @@ -28,28 +28,50 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using VNLib.Utils.IO; +using VNLib.Utils.Memory.Caching; namespace VNLib.Net.Messaging.FBM.Client { /// <summary> /// Represents a shared internal character and bianry buffer for /// </summary> - internal sealed class FBMBuffer : IFBMHeaderBuffer, IDisposable + internal sealed class FBMBuffer : IFBMHeaderBuffer, IDisposable, IReusable { - private readonly IMemoryOwner<byte> Handle; + private readonly IFBMMemoryHandle Handle; + private readonly IFBMMemoryManager _memoryManager; + private readonly int _size; private readonly BufferWriter _writer; private readonly BinaryRequestAccumulator _requestAccumulator; - internal FBMBuffer(IMemoryOwner<byte> handle) + internal FBMBuffer(IFBMMemoryManager manager, int bufferSize) { - Handle = handle; + _memoryManager = manager; + Handle = manager.InitHandle(); _writer = new(this); - _requestAccumulator = new(handle.Memory); + _size = bufferSize; + _requestAccumulator = new(this, bufferSize); } + /* + * Reusable methods will alloc and free buffers during + * normal operation. + */ + + ///<inheritdoc/> + public void Prepare() => _memoryManager.AllocBuffer(Handle, _size); + + ///<inheritdoc/> + public bool Release() + { + _memoryManager.FreeBuffer(Handle); + return true; + } + + public void Dispose() => _ = Release(); + /// <summary> /// Gets the internal request data accumulator /// </summary> @@ -73,13 +95,6 @@ namespace VNLib.Net.Messaging.FBM.Client //Return the internal writer return _writer; } - - - public void Dispose() - { - //Dispose handle - Handle.Dispose(); - } /// <summary> /// Resets the request accumulator and writes the initial message id @@ -91,7 +106,7 @@ namespace VNLib.Net.Messaging.FBM.Client _requestAccumulator.Reset(); //Write message id to accumulator, it should already be reset - Helpers.WriteMessageid(RequestBuffer, messageId); + Helpers.WriteMessageid(_requestAccumulator, messageId); } ///<inheritdoc/> @@ -99,24 +114,24 @@ namespace VNLib.Net.Messaging.FBM.Client Span<char> IFBMHeaderBuffer.GetSpan(int offset, int count) { //Get the character span - Span<char> span = MemoryMarshal.Cast<byte, char>(Handle.Memory.Span); + Span<char> span = MemoryMarshal.Cast<byte, char>(Handle.GetSpan()); return span.Slice(offset, count); } ///<inheritdoc/> [MethodImpl(MethodImplOptions.AggressiveInlining)] - Span<char> IFBMHeaderBuffer.GetSpan() => MemoryMarshal.Cast<byte, char>(Handle.Memory.Span); + Span<char> IFBMHeaderBuffer.GetSpan() => MemoryMarshal.Cast<byte, char>(Handle.GetSpan()); private sealed class BinaryRequestAccumulator : IDataAccumulator<byte> { private readonly int Size; - private readonly Memory<byte> Buffer; + private readonly FBMBuffer Buffer; - internal BinaryRequestAccumulator(Memory<byte> buffer) + internal BinaryRequestAccumulator(FBMBuffer buffer, int size) { Buffer = buffer; - Size = buffer.Length; + Size = size; } ///<inheritdoc/> @@ -126,52 +141,47 @@ namespace VNLib.Net.Messaging.FBM.Client public int RemainingSize => Size - AccumulatedSize; ///<inheritdoc/> - public Span<byte> Remaining => Buffer.Span.Slice(AccumulatedSize, RemainingSize); + public Span<byte> Remaining => Buffer.Handle.GetSpan().Slice(AccumulatedSize, RemainingSize); + ///<inheritdoc/> - public Span<byte> Accumulated => Buffer.Span[..AccumulatedSize]; + public Span<byte> Accumulated => Buffer.Handle.GetSpan()[..AccumulatedSize]; /// <summary> /// Gets the accumulated data as a memory segment /// </summary> - public Memory<byte> AccumulatedMemory => Buffer[..AccumulatedSize]; + public Memory<byte> AccumulatedMemory => Buffer.Handle.GetMemory()[..AccumulatedSize]; /// <summary> /// Gets the remaining buffer segment as a memory segment /// </summary> - public Memory<byte> RemainingMemory => Buffer.Slice(AccumulatedSize, RemainingSize); + public Memory<byte> RemainingMemory => Buffer.Handle.GetMemory().Slice(AccumulatedSize, RemainingSize); ///<inheritdoc/> public void Advance(int count) => AccumulatedSize += count; + ///<inheritdoc/> public void Reset() => AccumulatedSize = 0; } + /* + * A buffer writer that wraps the request accumulator to allow + * a finite amount of data to be written to the accumulator since + * it uses a fixed size internal buffer. + */ private sealed class BufferWriter : IBufferWriter<byte> { private readonly FBMBuffer Buffer; - public BufferWriter(FBMBuffer buffer) - { - Buffer = buffer; - } + public BufferWriter(FBMBuffer buffer) => Buffer = buffer; - public void Advance(int count) - { - //Advance the writer - Buffer.RequestBuffer.Advance(count); - } + ///<inheritdoc/> + public void Advance(int count) => Buffer._requestAccumulator.Advance(count); - public Memory<byte> GetMemory(int sizeHint = 0) - { - //Get the remaining memory segment - return Buffer._requestAccumulator.RemainingMemory; - } + ///<inheritdoc/> + public Memory<byte> GetMemory(int sizeHint = 0) => Buffer._requestAccumulator.RemainingMemory; - public Span<byte> GetSpan(int sizeHint = 0) - { - //Get the remaining data segment - return Buffer.RequestBuffer.Remaining; - } + ///<inheritdoc/> + public Span<byte> GetSpan(int sizeHint = 0) => Buffer._requestAccumulator.Remaining; } } } diff --git a/lib/Net.Messaging.FBM/src/Client/FBMClient.cs b/lib/Net.Messaging.FBM/src/Client/FBMClient.cs index 5184c38..c8319fa 100644 --- a/lib/Net.Messaging.FBM/src/Client/FBMClient.cs +++ b/lib/Net.Messaging.FBM/src/Client/FBMClient.cs @@ -24,7 +24,6 @@ using System; using System.IO; -using System.Buffers; using System.Threading; using System.Net.WebSockets; using System.Threading.Tasks; @@ -34,9 +33,12 @@ using System.Collections.Concurrent; using VNLib.Net.Http; using VNLib.Utils; using VNLib.Utils.IO; +using VNLib.Utils.Memory; +using VNLib.Utils.Memory.Caching; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; -using VNLib.Utils.Memory.Caching; + +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task namespace VNLib.Net.Messaging.FBM.Client { @@ -60,6 +62,8 @@ namespace VNLib.Net.Messaging.FBM.Client /// </summary> public const string REQ_MAX_MESS_QUERY_ARG = "mx"; + public const int MAX_STREAM_BUFFER_SIZE = 128 * 1024; + /// <summary> /// Raised when the websocket has been closed because an error occured. /// You may inspect the event args to determine the cause of the error. @@ -74,12 +78,14 @@ namespace VNLib.Net.Messaging.FBM.Client private readonly SemaphoreSlim SendLock; private readonly ConcurrentDictionary<int, FBMRequest> ActiveRequests; private readonly ObjectRental<FBMRequest> RequestRental; + private readonly FBMClientConfig _config; + private readonly byte[] _streamBuffer; /// <summary> /// The configuration for the current client /// </summary> - public FBMClientConfig Config { get; } - + public ref readonly FBMClientConfig Config => ref _config; + /// <summary> /// A handle that is reset when a connection has been successfully set, and is set /// when the connection exists @@ -103,9 +109,13 @@ namespace VNLib.Net.Messaging.FBM.Client ConnectionStatusHandle = new(true); ActiveRequests = new(Environment.ProcessorCount, 100); - Config = config; + _config = config; //Init the new client socket ClientSocket = new(config.RecvBufferSize, config.RecvBufferSize, config.KeepAliveInterval, config.SubProtocol); + + //Init the stream buffer + int maxStrmBufSize = Math.Min(config.MaxMessageSize, MAX_STREAM_BUFFER_SIZE); + _streamBuffer = new byte[maxStrmBufSize]; } private void Debug(string format, params string[] strings) @@ -127,7 +137,7 @@ namespace VNLib.Net.Messaging.FBM.Client /// Allocates and configures a new <see cref="FBMRequest"/> message object for use within the reusable store /// </summary> /// <returns>The configured <see cref="FBMRequest"/></returns> - protected virtual FBMRequest ReuseableRequestConstructor() => new(Config); + protected virtual FBMRequest ReuseableRequestConstructor() => new(in _config); /// <summary> /// Asynchronously opens a websocket connection with the specifed remote server @@ -180,10 +190,7 @@ namespace VNLib.Net.Messaging.FBM.Client /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="InvalidOperationException"></exception> /// <exception cref="FBMInvalidRequestException"></exception> - public Task<FBMResponse> SendAsync(FBMRequest request, CancellationToken cancellationToken = default) - { - return SendAsync(request, Config.RequestTimeout, cancellationToken); - } + public Task<FBMResponse> SendAsync(FBMRequest request, CancellationToken cancellationToken = default) => SendAsync(request, _config.RequestTimeout, cancellationToken); /// <summary> /// Sends a <see cref="FBMRequest"/> to the connected server @@ -211,6 +218,8 @@ namespace VNLib.Net.Messaging.FBM.Client try { + FBMResponse response = new(); + //Get the request data segment ReadOnlyMemory<byte> requestData = request.GetRequestData(); @@ -224,22 +233,26 @@ namespace VNLib.Net.Messaging.FBM.Client } //wait for the response to be set - await request.Waiter.WaitAsync(timeout, cancellationToken).ConfigureAwait(true); + await request.Waiter.GetTask(timeout, cancellationToken).ConfigureAwait(true); Debug("Received {size} bytes for message {id}", request.Response?.Length ?? 0, request.MessageId); - return request.GetResponse(); + //Get the response data + request.GetResponse(ref response); + + return response; } catch { //Remove the request since packet was never sent ActiveRequests.Remove(request.MessageId, out _); - - //Cleanup waiter - request.Waiter.OnEndRequest(); - throw; } + finally + { + //Always cleanup waiter + request.Waiter.OnEndRequest(); + } } /// <summary> @@ -247,30 +260,28 @@ namespace VNLib.Net.Messaging.FBM.Client /// </summary> /// <param name="request">The request message to send to the server</param> /// <param name="payload">Data to stream to the server</param> - /// <param name="ct">The content type of the stream of data</param> + /// <param name="contentType">The content type of the stream of data</param> /// <param name="cancellationToken">A token to cancel the operation</param> /// <returns>A task that resolves when the data is sent and the resonse is received</returns> /// <exception cref="ArgumentException"></exception> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="InvalidOperationException"></exception> - public Task StreamDataAsync(FBMRequest request, Stream payload, ContentType ct, CancellationToken cancellationToken = default) - { - return StreamDataAsync(request, payload, ct, Config.RequestTimeout, cancellationToken); - } + public Task<FBMResponse> StreamDataAsync(FBMRequest request, Stream payload, ContentType contentType, CancellationToken cancellationToken = default) + => StreamDataAsync(request, payload, contentType, _config.RequestTimeout, cancellationToken); /// <summary> /// Streams arbitrary binary data to the server with the initial request message /// </summary> /// <param name="request">The request message to send to the server</param> /// <param name="payload">Data to stream to the server</param> - /// <param name="ct">The content type of the stream of data</param> + /// <param name="contentType">The content type of the stream of data</param> /// <param name="cancellationToken">A token to cancel the operation</param> /// <param name="timeout">A maxium wait timeout period. If -1 or 0 the timeout is disabled</param> /// <returns>A task that resolves when the data is sent and the resonse is received</returns> /// <exception cref="ArgumentException"></exception> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="InvalidOperationException"></exception> - public async Task StreamDataAsync(FBMRequest request, Stream payload, ContentType ct, TimeSpan timeout, CancellationToken cancellationToken = default) + public async Task<FBMResponse> StreamDataAsync(FBMRequest request, Stream payload, ContentType contentType, TimeSpan timeout, CancellationToken cancellationToken = default) { Check(); @@ -282,19 +293,15 @@ namespace VNLib.Net.Messaging.FBM.Client try { + FBMResponse response = new(); + //Get the request data segment ReadOnlyMemory<byte> requestData = request.GetRequestData(); Debug("Streaming {bytes} with id {id}", requestData.Length, request.MessageId); - //Write an empty body in the request - request.WriteBody(ReadOnlySpan<byte>.Empty, ct); - - //Calc buffer size - int bufSize = (int)Math.Clamp(payload.Length, Config.MessageBufferSize, Config.MaxMessageSize); - - //Alloc a streaming buffer - using IMemoryOwner<byte> buffer = Config.BufferHeap.DirectAlloc<byte>(bufSize); + //Write an empty body in the request so a content type header is writen + request.WriteBody(ReadOnlySpan<byte>.Empty, contentType); //Wait for send-lock using (SemSlimReleaser releaser = await SendLock.GetReleaserAsync(cancellationToken)) @@ -306,7 +313,7 @@ namespace VNLib.Net.Messaging.FBM.Client do { //Read data - int read = await payload.ReadAsync(buffer.Memory, cancellationToken); + int read = await payload.ReadAsync(_streamBuffer, cancellationToken); if (read == 0) { @@ -315,29 +322,32 @@ namespace VNLib.Net.Messaging.FBM.Client } //write message to socket, if the read data was smaller than the buffer, we can send the last packet - await ClientSocket.SendAsync(buffer.Memory[..read], WebSocketMessageType.Binary, read < bufSize, cancellationToken); + await ClientSocket.SendAsync(_streamBuffer[..read], WebSocketMessageType.Binary, read < _streamBuffer.Length, cancellationToken); } while (true); } //wait for the server to respond - await request.Waiter.WaitAsync(timeout, cancellationToken).ConfigureAwait(true); + await request.Waiter.GetTask(timeout, cancellationToken).ConfigureAwait(true); Debug("Response recieved {size} bytes for message {id}", request.Response?.Length ?? 0, request.MessageId); + + request.GetResponse(ref response); + return response; } catch { //Remove the request since packet was never sent or cancelled _ = ActiveRequests.TryRemove(request.MessageId, out _); - - //Cleanup request waiter - request.Waiter.OnEndRequest(); - throw; } + finally + { + //Always cleanup waiter + request.Waiter.OnEndRequest(); + } } - /// <summary> /// Closes the underlying <see cref="WebSocket"/> and cancels all pending operations /// </summary> @@ -399,14 +409,20 @@ namespace VNLib.Net.Messaging.FBM.Client Debug("Begining receive loop"); //Alloc recv buffer - IMemoryOwner<byte> recvBuffer = Config.BufferHeap.DirectAlloc<byte>(Config.RecvBufferSize); + IFBMMemoryHandle recvBuffer = _config.MemoryManager.InitHandle(); + _config.MemoryManager.AllocBuffer(recvBuffer, _config.RecvBufferSize); try { + if(!_config.MemoryManager.TryGetHeap(out IUnmangedHeap? heap)) + { + throw new NotSupportedException("The memory manager must support using IUnmanagedHeaps"); + } + //Recv event loop while (true) { //Listen for incoming packets with the intial data buffer - ValueWebSocketReceiveResult result = await socket.ReceiveAsync(recvBuffer.Memory, CancellationToken.None); + ValueWebSocketReceiveResult result = await socket.ReceiveAsync(recvBuffer.GetMemory(), CancellationToken.None); //If the message is a close message, its time to exit if (result.MessageType == WebSocketMessageType.Close) @@ -422,19 +438,19 @@ namespace VNLib.Net.Messaging.FBM.Client } //Alloc data buffer and write initial data - VnMemoryStream responseBuffer = new(Config.BufferHeap); + VnMemoryStream responseBuffer = new(heap); //Copy initial data - responseBuffer.Write(recvBuffer.Memory.Span[..result.Count]); + responseBuffer.Write(recvBuffer.GetSpan()[..result.Count]); //Receive packets until the EOF is reached while (!result.EndOfMessage) { //recive more data - result = await socket.ReceiveAsync(recvBuffer.Memory, CancellationToken.None); + result = await socket.ReceiveAsync(recvBuffer.GetMemory(), CancellationToken.None); //Make sure the buffer is not too large - if ((responseBuffer.Length + result.Count) > Config.MaxMessageSize) + if ((responseBuffer.Length + result.Count) > _config.MaxMessageSize) { //Dispose the buffer before exiting responseBuffer.Dispose(); @@ -443,7 +459,7 @@ namespace VNLib.Net.Messaging.FBM.Client } //Copy continuous data - responseBuffer.Write(recvBuffer.Memory.Span[..result.Count]); + responseBuffer.Write(recvBuffer.GetSpan()[..result.Count]); } //Reset the buffer stream position @@ -472,7 +488,7 @@ namespace VNLib.Net.Messaging.FBM.Client finally { //Dispose the recv buffer - recvBuffer.Dispose(); + _config.MemoryManager.FreeBuffer(recvBuffer); //Cleanup the socket when exiting ClientSocket.Cleanup(); //Set status handle as unset @@ -522,7 +538,12 @@ namespace VNLib.Net.Messaging.FBM.Client if (ActiveRequests.TryRemove(messageId, out FBMRequest? request)) { //Set the new response message - request.Waiter.Complete(responseMessage); + if (!request.Waiter.Complete(responseMessage)) + { + //Falied to complete, dispose the message data + responseMessage.Dispose(); + Debug("Failed to transition waiting request {id}. Message was dropped", messageId, 0); + } } else { diff --git a/lib/Net.Messaging.FBM/src/Client/FBMClientConfig.cs b/lib/Net.Messaging.FBM/src/Client/FBMClientConfig.cs index 30e9a95..735a0a8 100644 --- a/lib/Net.Messaging.FBM/src/Client/FBMClientConfig.cs +++ b/lib/Net.Messaging.FBM/src/Client/FBMClientConfig.cs @@ -25,7 +25,6 @@ using System; using System.Text; -using VNLib.Utils.Memory; using VNLib.Utils.Logging; namespace VNLib.Net.Messaging.FBM.Client @@ -59,7 +58,7 @@ namespace VNLib.Net.Messaging.FBM.Client /// <summary> /// The heap to allocate internal (and message) buffers from /// </summary> - public readonly IUnmangedHeap BufferHeap { get; init; } + public readonly IFBMMemoryManager MemoryManager { get; init; } /// <summary> /// The websocket keepalive interval to use (leaving this property default disables keepalives) /// </summary> diff --git a/lib/Net.Messaging.FBM/src/Client/FBMRequest.cs b/lib/Net.Messaging.FBM/src/Client/FBMRequest.cs index c4fb493..418a9ec 100644 --- a/lib/Net.Messaging.FBM/src/Client/FBMRequest.cs +++ b/lib/Net.Messaging.FBM/src/Client/FBMRequest.cs @@ -26,6 +26,7 @@ using System; using System.Text; using System.Buffers; using System.Threading; +using System.Diagnostics; using System.Threading.Tasks; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -37,7 +38,6 @@ using VNLib.Utils.Memory; using VNLib.Utils.Extensions; using VNLib.Utils.Memory.Caching; - namespace VNLib.Net.Messaging.FBM.Client { /// <summary> @@ -97,29 +97,29 @@ namespace VNLib.Net.Messaging.FBM.Client /// <param name="messageId">The custom message id</param> /// <param name="config">The fbm client config storing required config variables</param> public FBMRequest(int messageId, in FBMClientConfig config) - :this(messageId, config.BufferHeap, config.MessageBufferSize, config.HeaderEncoding) + :this(messageId, config.MemoryManager, config.MessageBufferSize, config.HeaderEncoding) { } /// <summary> /// Initializes a new <see cref="FBMRequest"/> with the sepcified message buffer size and a custom MessageId /// </summary> /// <param name="messageId">The custom message id</param> - /// <param name="heap">The heap to allocate the internal buffer from</param> + /// <param name="manager">The memory manager used to allocate the internal buffers</param> /// <param name="bufferSize">The size of the internal buffer</param> /// <param name="headerEncoding">The encoding instance used for header character encoding</param> - public FBMRequest(int messageId, IUnmangedHeap heap, int bufferSize, Encoding headerEncoding) + public FBMRequest(int messageId, IFBMMemoryManager manager, int bufferSize, Encoding headerEncoding) { MessageId = messageId; - HeaderEncoding = headerEncoding; + HeaderEncoding = headerEncoding ?? throw new ArgumentNullException(nameof(headerEncoding)); + _ = manager ?? throw new ArgumentNullException(nameof(manager)); //Configure waiter Waiter = new FBMMessageWaiter(this); - - //Alloc the buffer as a memory owner so a memory buffer can be used - IMemoryOwner<byte> HeapBuffer = heap.DirectAlloc<byte>(bufferSize); - Buffer = new(HeapBuffer); + + Buffer = new(manager, bufferSize); //Prepare the message incase the request is fresh + Buffer.Prepare(); Reset(); } @@ -152,19 +152,13 @@ namespace VNLib.Net.Messaging.FBM.Client /// The request message packet, this may cause side effects /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ReadOnlyMemory<byte> GetRequestData() - { - return Buffer.RequestData; - } + public ReadOnlyMemory<byte> GetRequestData() => Buffer.RequestData; /// <summary> /// Resets the internal buffer and allows for writing a new message with /// the same message-id /// </summary> - public void Reset() - { - Buffer.Reset(MessageId); - } + public void Reset() => Buffer.Reset(MessageId); ///<inheritdoc/> protected override void Free() @@ -175,7 +169,12 @@ namespace VNLib.Net.Messaging.FBM.Client (Waiter as FBMMessageWaiter)!.Dispose(); } - void IReusable.Prepare() => Reset(); + void IReusable.Prepare() + { + //MUST BE CALLED FIRST! + Buffer.Prepare(); + Reset(); + } bool IReusable.Release() { @@ -186,6 +185,9 @@ namespace VNLib.Net.Messaging.FBM.Client Response?.Dispose(); Response = null; + //Free buffer + Buffer.Release(); + return true; } @@ -195,7 +197,7 @@ namespace VNLib.Net.Messaging.FBM.Client /// Gets the response of the sent message /// </summary> /// <returns>The response message for the current request</returns> - internal FBMResponse GetResponse() + internal void GetResponse(ref FBMResponse response) { if (Response != null) { @@ -215,11 +217,7 @@ namespace VNLib.Net.Messaging.FBM.Client HeaderParseError statusFlags = Helpers.ParseHeaders(Response, Buffer, ResponseHeaderList, HeaderEncoding); //return response structure - return new(Response, statusFlags, ResponseHeaderList, OnResponseDisposed); - } - else - { - return new(); + response = new(Response, statusFlags, ResponseHeaderList, OnResponseDisposed); } } @@ -272,12 +270,13 @@ namespace VNLib.Net.Messaging.FBM.Client #endregion #region waiter - private sealed class FBMMessageWaiter : IFBMMessageWaiter, IDisposable + private sealed class FBMMessageWaiter : IFBMMessageWaiter, IDisposable, IThreadPoolWorkItem { private readonly Timer _timer; private readonly FBMRequest _request; private TaskCompletionSource? _tcs; + private CancellationTokenRegistration _token; public FBMMessageWaiter(FBMRequest request) { @@ -298,79 +297,89 @@ namespace VNLib.Net.Messaging.FBM.Client public void OnEndRequest() { //Cleanup tcs ref - _ = Interlocked.Exchange(ref _tcs, null); + _tcs = null; - //Stop timer if set + //Always stop timer if set _timer.Stop(); + + //Cleanup cancellation token + _token.Dispose(); } ///<inheritdoc/> - public void Complete(VnMemoryStream ms) + public bool Complete(VnMemoryStream ms) { - //Read the current state of the tcs TaskCompletionSource? tcs = _tcs; - if (tcs == null) + //Work is done/cancelled + if (tcs != null && tcs.Task.IsCompleted) { - //Work is done/cancelled, dispose the ms and leave - ms.Dispose(); + return false; } //Store response _request.Response = ms; - //Transition to completed state in background thread - static void OnTpCallback(object? state) - { - _ = (state as TaskCompletionSource)!.TrySetResult(); - } - /* * The calling thread may be a TP thread proccessing an async event loop. * We do not want to block this worker thread. */ - ThreadPool.UnsafeQueueUserWorkItem(OnTpCallback, tcs); + return ThreadPool.UnsafeQueueUserWorkItem(this, true); } + /* + * Called when scheduled on the TP thread pool + */ ///<inheritdoc/> - public async Task WaitAsync(TimeSpan timeout, CancellationToken cancellation) + public void Execute() => _tcs?.TrySetResult(); + + + ///<inheritdoc/> + public Task GetTask(TimeSpan timeout, CancellationToken cancellation) { - if (timeout.Ticks > 0) - { - //Restart timer if timeout is configured - _timer.Restart(timeout); - } + TaskCompletionSource? tcs = _tcs; - //Confim the token may be cancelled - if (cancellation.CanBeCanceled) - { - //Register cancellation - using CancellationTokenRegistration reg = cancellation.Register(OnCancelled, this, false); + Debug.Assert(tcs != null, "A call to GetTask was made outside of the request flow, the TaskCompletionSource was null"); - //await task that may be canclled - await _tcs.Task.ConfigureAwait(false); - } - else + /* + * Get task will only be called after the message has been sent. + * The Complete method may have already scheduled a completion by + * the time this method is called, so we may avoid setting up the + * timer and cancellation if possible. Also since this mthod is + * called from the request side, we know the tcs cannot be null + */ + + if (!tcs.Task.IsCompleted) { - //await the task directly - await _tcs.Task.ConfigureAwait(false); + if (timeout.Ticks > 0) + { + //Restart timer if timeout is configured + _timer.Restart(timeout); + } + + if (cancellation.CanBeCanceled) + { + //Register cancellation + _token = cancellation.Register(OnCancelled, this); + } } + + return tcs.Task; } ///<inheritdoc/> public void ManualCancellation() => OnCancelled(this); - private void OnCancelled(object? state) - { - //Set cancelled state if exists - _ = _tcs?.TrySetCanceled(); - } + //Set cancelled state if exists, the task may have already completed + private void OnCancelled(object? state) => _tcs?.TrySetCanceled(); ///<inheritdoc/> public void Dispose() { _timer.Dispose(); + _token.Dispose(); } + } #endregion diff --git a/lib/Net.Messaging.FBM/src/Client/FBMResponse.cs b/lib/Net.Messaging.FBM/src/Client/FBMResponse.cs index 6f8fec4..f1148f1 100644 --- a/lib/Net.Messaging.FBM/src/Client/FBMResponse.cs +++ b/lib/Net.Messaging.FBM/src/Client/FBMResponse.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM @@ -90,17 +90,16 @@ namespace VNLib.Net.Messaging.FBM.Client /// <summary> /// Releases any resources associated with the response message /// </summary> - public void Dispose() => _onDispose?.Invoke(); + public readonly void Dispose() => _onDispose?.Invoke(); ///<inheritdoc/> public override bool Equals(object? obj) => obj is FBMResponse response && Equals(response); ///<inheritdoc/> + public bool Equals(FBMResponse other) => IsSet && other.IsSet && ReferenceEquals(MessagePacket, other.MessagePacket); + ///<inheritdoc/> public override int GetHashCode() => IsSet ? MessagePacket!.GetHashCode() : 0; ///<inheritdoc/> public static bool operator ==(FBMResponse left, FBMResponse right) => left.Equals(right); ///<inheritdoc/> public static bool operator !=(FBMResponse left, FBMResponse right) => !(left == right); - ///<inheritdoc/> - public bool Equals(FBMResponse other) => IsSet && other.IsSet && MessagePacket == other.MessagePacket; - } } diff --git a/lib/Net.Messaging.FBM/src/Client/IFBMMessageWaiter.cs b/lib/Net.Messaging.FBM/src/Client/IFBMMessageWaiter.cs index 5000711..cc8e1c4 100644 --- a/lib/Net.Messaging.FBM/src/Client/IFBMMessageWaiter.cs +++ b/lib/Net.Messaging.FBM/src/Client/IFBMMessageWaiter.cs @@ -48,9 +48,9 @@ namespace VNLib.Net.Messaging.FBM.Client /// or a timeout /// </summary> /// <param name="timeout">The maxium time to wait for the server to respond (may be default/0)</param> - /// <param name="cancellation">The cancellation token to observe</param> + /// <param name="cancellation">A token to cancel the wait task</param> /// <returns>A task that completes when the server responds</returns> - Task WaitAsync(TimeSpan timeout, CancellationToken cancellation); + Task GetTask(TimeSpan timeout, CancellationToken cancellation); /// <summary> /// Called by the client to cleanup the waiter when the request is completed @@ -63,7 +63,7 @@ namespace VNLib.Net.Messaging.FBM.Client /// Set by the client when the response has been successfully received by the client /// </summary> /// <param name="ms">The response data to pass to the response</param> - void Complete(VnMemoryStream ms); + bool Complete(VnMemoryStream ms); /// <summary> /// Called to invoke a manual cancellation of a request waiter. This method should diff --git a/lib/Net.Messaging.FBM/src/Client/ManagedClientWebSocket.cs b/lib/Net.Messaging.FBM/src/Client/ManagedClientWebSocket.cs index d0352d3..fc2e417 100644 --- a/lib/Net.Messaging.FBM/src/Client/ManagedClientWebSocket.cs +++ b/lib/Net.Messaging.FBM/src/Client/ManagedClientWebSocket.cs @@ -43,7 +43,7 @@ namespace VNLib.Net.Messaging.FBM.Client private readonly int TxBufferSize; private readonly int RxBufferSize; private readonly TimeSpan KeepAliveInterval; - private readonly VnTempBuffer<byte> _dataBuffer; + private readonly ArrayPoolBuffer<byte> _dataBuffer; private readonly string? _subProtocol; /// <summary> @@ -95,7 +95,7 @@ namespace VNLib.Net.Messaging.FBM.Client try { //Set buffer - _socket.Options.SetBuffer(RxBufferSize, TxBufferSize, _dataBuffer); + _socket.Options.SetBuffer(RxBufferSize, TxBufferSize, _dataBuffer.AsArraySegment()); //Set remaining stored options _socket.Options.ClientCertificates = Certificates; _socket.Options.KeepAliveInterval = KeepAliveInterval; diff --git a/lib/Net.Messaging.FBM/src/FBMMessageHeader.cs b/lib/Net.Messaging.FBM/src/FBMMessageHeader.cs index d1f4f1c..180ce7d 100644 --- a/lib/Net.Messaging.FBM/src/FBMMessageHeader.cs +++ b/lib/Net.Messaging.FBM/src/FBMMessageHeader.cs @@ -87,6 +87,7 @@ namespace VNLib.Net.Messaging.FBM ///<inheritdoc/> public static bool operator ==(FBMMessageHeader left, FBMMessageHeader right) => left.Equals(right); + ///<inheritdoc/> public static bool operator !=(FBMMessageHeader left, FBMMessageHeader right) => !(left == right); diff --git a/lib/Net.Messaging.FBM/src/FallbackFBMMemoryManager.cs b/lib/Net.Messaging.FBM/src/FallbackFBMMemoryManager.cs new file mode 100644 index 0000000..260cbd6 --- /dev/null +++ b/lib/Net.Messaging.FBM/src/FallbackFBMMemoryManager.cs @@ -0,0 +1,140 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Messaging.FBM +* File: FallbackFBMMemoryManager.cs +* +* FallbackFBMMemoryManager.cs is part of VNLib.Net.Messaging.FBM which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Messaging.FBM is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Messaging.FBM is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; + +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + +namespace VNLib.Net.Messaging.FBM +{ + /// <summary> + /// A default/fallback implementation of a <see cref="IFBMMemoryManager"/> that + /// uses an <see cref="IUnmangedHeap"/> to allocate buffers from + /// </summary> + public sealed class FallbackFBMMemoryManager : IFBMMemoryManager + { + private readonly IUnmangedHeap _heap; + + /// <summary> + /// Initializes a new instance of <see cref="FallbackFBMMemoryManager"/> allocationg + /// memory from the specified <see cref="IUnmangedHeap"/> + /// </summary> + /// <param name="heap">The heap to allocate memory from</param> + /// <exception cref="ArgumentNullException"></exception> + public FallbackFBMMemoryManager(IUnmangedHeap heap) => _heap = heap ?? throw new ArgumentNullException(nameof(heap)); + + ///<inheritdoc/> + public void AllocBuffer(IFBMSpanOnlyMemoryHandle state, int size) + { + _ = state ?? throw new ArgumentNullException(nameof(state)); + (state as IFBMBufferHolder)!.AllocBuffer(size); + } + + ///<inheritdoc/> + public void FreeBuffer(IFBMSpanOnlyMemoryHandle state) + { + _ = state ?? throw new ArgumentNullException(nameof(state)); + (state as IFBMBufferHolder)!.FreeBuffer(); + } + + ///<inheritdoc/> + public IFBMMemoryHandle InitHandle() => new FBMMemHandle(_heap); + + ///<inheritdoc/> + public IFBMSpanOnlyMemoryHandle InitSpanOnly() => new FBMSpanOnlyMemHandle(_heap); + + ///<inheritdoc/> + public bool TryGetHeap([NotNullWhen(true)] out IUnmangedHeap? heap) + { + heap = _heap; + return true; + } + + interface IFBMBufferHolder + { + void AllocBuffer(int size); + + void FreeBuffer(); + } + + private sealed record class FBMMemHandle(IUnmangedHeap Heap) : IFBMMemoryHandle, IFBMBufferHolder + { + private MemoryHandle<byte>? _handle; + private IMemoryOwner<byte>? _memHandle; + + ///<inheritdoc/> + public Memory<byte> GetMemory() + { + _ = _memHandle ?? throw new InvalidOperationException("Buffer has not allocated"); + return _memHandle.Memory; + } + + ///<inheritdoc/> + public Span<byte> GetSpan() + { + _ = _handle ?? throw new InvalidOperationException("Buffer has not allocated"); + return _handle.Span; + } + + ///<inheritdoc/> + public void AllocBuffer(int size) + { + //Alloc buffer and memory manager to wrap it + _handle = Heap.Alloc<byte>(size, false); + _memHandle = _handle.ToMemoryManager(false); + } + + ///<inheritdoc/> + public void FreeBuffer() + { + _handle?.Dispose(); + _memHandle?.Dispose(); + + _handle = null; + _memHandle = null; + } + } + + private sealed record class FBMSpanOnlyMemHandle(IUnmangedHeap Heap) : IFBMSpanOnlyMemoryHandle, IFBMBufferHolder + { + private MemoryHandle<byte>? _handle; + + ///<inheritdoc/> + public void AllocBuffer(int size) => _handle = Heap.Alloc<byte>(size, false); + + ///<inheritdoc/> + public void FreeBuffer() => _handle?.Dispose(); + + ///<inheritdoc/> + public Span<byte> GetSpan() + { + _ = _handle ?? throw new InvalidOperationException("Buffer has not allocated"); + return _handle.Span; + } + } + } +} diff --git a/lib/Net.Messaging.FBM/src/IFBMMemoryHandle.cs b/lib/Net.Messaging.FBM/src/IFBMMemoryHandle.cs new file mode 100644 index 0000000..97a41b4 --- /dev/null +++ b/lib/Net.Messaging.FBM/src/IFBMMemoryHandle.cs @@ -0,0 +1,43 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Messaging.FBM +* File: IFBMMemoryHandle.cs +* +* IFBMMemoryHandle.cs is part of VNLib.Net.Messaging.FBM which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Messaging.FBM is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Messaging.FBM is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + + +namespace VNLib.Net.Messaging.FBM +{ + /// <summary> + /// Represents a configurable handle to a memory block + /// </summary> + public interface IFBMMemoryHandle : IFBMSpanOnlyMemoryHandle + { + /// <summary> + /// Gets the block as a <see cref="Memory{T}"/> + /// structure + /// </summary> + /// <returns>The memory structure wrapping the underlying memory block</returns> + Memory<byte> GetMemory(); + } + +} diff --git a/lib/Net.Messaging.FBM/src/IFBMMemoryManager.cs b/lib/Net.Messaging.FBM/src/IFBMMemoryManager.cs new file mode 100644 index 0000000..9342993 --- /dev/null +++ b/lib/Net.Messaging.FBM/src/IFBMMemoryManager.cs @@ -0,0 +1,71 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Messaging.FBM +* File: IFBMMemoryManager.cs +* +* IFBMMemoryManager.cs is part of VNLib.Net.Messaging.FBM which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Messaging.FBM is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Messaging.FBM is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Diagnostics.CodeAnalysis; + +using VNLib.Utils.Memory; + +namespace VNLib.Net.Messaging.FBM +{ + /// <summary> + /// Manages memory blocks required by the FBM server messages + /// </summary> + public interface IFBMMemoryManager + { + /// <summary> + /// Initializes a new <see cref="IFBMMemoryHandle"/> + /// </summary> + /// <returns>The initialized handle</returns> + IFBMMemoryHandle InitHandle(); + + /// <summary> + /// Initializes a new <see cref="IFBMSpanOnlyMemoryHandle"/> + /// </summary> + /// <returns>The initialized handle</returns> + IFBMSpanOnlyMemoryHandle InitSpanOnly(); + + /// <summary> + /// Allocates the <see cref="IFBMMemoryHandle"/> internal buffer + /// for use + /// </summary> + /// <param name="state">The memory handle to allocate the buffer for</param> + /// <param name="size">The size of the buffer required</param> + void AllocBuffer(IFBMSpanOnlyMemoryHandle state, int size); + + /// <summary> + /// Frees the <see cref="IFBMSpanOnlyMemoryHandle"/> internal buffer + /// </summary> + /// <param name="state">The buffer handle holding the memory to free</param> + void FreeBuffer(IFBMSpanOnlyMemoryHandle state); + + /// <summary> + /// Tries to get the internal <see cref="IUnmangedHeap"/> to allocate internal + /// buffers from + /// </summary> + /// <param name="heap">The internal heap</param> + /// <returns>A value that indicates if a backing heap is supported and can be recovered</returns> + bool TryGetHeap([NotNullWhen(true)]out IUnmangedHeap? heap); + } + +} diff --git a/lib/Net.Messaging.FBM/src/Client/IFBMMessage.cs b/lib/Net.Messaging.FBM/src/IFBMMessage.cs index 18f19ec..dba605d 100644 --- a/lib/Net.Messaging.FBM/src/Client/IFBMMessage.cs +++ b/lib/Net.Messaging.FBM/src/IFBMMessage.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM @@ -26,7 +26,7 @@ using System; using VNLib.Net.Http; -namespace VNLib.Net.Messaging.FBM.Client +namespace VNLib.Net.Messaging.FBM { /// <summary> /// Represents basic Fixed Buffer Message protocol operations diff --git a/lib/Net.Messaging.FBM/src/IFBMSpanOnlyMemoryHandle.cs b/lib/Net.Messaging.FBM/src/IFBMSpanOnlyMemoryHandle.cs new file mode 100644 index 0000000..0078357 --- /dev/null +++ b/lib/Net.Messaging.FBM/src/IFBMSpanOnlyMemoryHandle.cs @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Messaging.FBM +* File: IFBMSpanOnlyMemoryHandle.cs +* +* IFBMSpanOnlyMemoryHandle.cs is part of VNLib.Net.Messaging.FBM which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Messaging.FBM is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Messaging.FBM is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + + +namespace VNLib.Net.Messaging.FBM +{ + /// <summary> + /// Represents a configurable handle to a memory block + /// </summary> + public interface IFBMSpanOnlyMemoryHandle + { + /// <summary> + /// Gets the block as a <see cref="Span{T}"/> + /// </summary> + /// <returns>The memory block as a span</returns> + Span<byte> GetSpan(); + } + +} diff --git a/lib/Net.Messaging.FBM/src/Server/FBMContext.cs b/lib/Net.Messaging.FBM/src/Server/FBMContext.cs index 6d5f3bd..f2a2fea 100644 --- a/lib/Net.Messaging.FBM/src/Server/FBMContext.cs +++ b/lib/Net.Messaging.FBM/src/Server/FBMContext.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM @@ -55,10 +55,11 @@ namespace VNLib.Net.Messaging.FBM.Server /// <param name="requestHeaderBufferSize">The size in characters of the request header buffer</param> /// <param name="responseBufferSize">The size in characters of the response header buffer</param> /// <param name="headerEncoding">The message header encoding instance</param> - public FBMContext(int requestHeaderBufferSize, int responseBufferSize, Encoding headerEncoding) + /// <param name="manager">The context memory manager</param> + public FBMContext(int requestHeaderBufferSize, int responseBufferSize, Encoding headerEncoding, IFBMMemoryManager manager) { - _request = Request = new(requestHeaderBufferSize); - _response = Response = new(responseBufferSize, headerEncoding); + _request = Request = new(requestHeaderBufferSize, manager); + _response = Response = new(responseBufferSize, headerEncoding, manager); _headerEncoding = headerEncoding; } diff --git a/lib/Net.Messaging.FBM/src/Server/FBMListener.cs b/lib/Net.Messaging.FBM/src/Server/FBMListener.cs index 46ee160..30fa1ac 100644 --- a/lib/Net.Messaging.FBM/src/Server/FBMListener.cs +++ b/lib/Net.Messaging.FBM/src/Server/FBMListener.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM @@ -24,7 +24,6 @@ using System; using System.IO; -using System.Buffers; using System.Threading; using System.Net.WebSockets; using System.Threading.Tasks; @@ -32,7 +31,6 @@ using System.Threading.Tasks; using VNLib.Utils.IO; using VNLib.Utils.Async; using VNLib.Utils.Memory; -using VNLib.Utils.Extensions; using VNLib.Utils.Memory.Caching; using VNLib.Plugins.Essentials; @@ -40,16 +38,6 @@ namespace VNLib.Net.Messaging.FBM.Server { /// <summary> - /// Method delegate for processing FBM messages from an <see cref="FBMListener"/> - /// when messages are received - /// </summary> - /// <param name="context">The message/connection context</param> - /// <param name="userState">The state parameter passed on client connected</param> - /// <param name="cancellationToken">A token that reflects the state of the listener</param> - /// <returns>A <see cref="Task"/> that resolves when processing is complete</returns> - public delegate Task RequestHandler(FBMContext context, object? userState, CancellationToken cancellationToken); - - /// <summary> /// A FBM protocol listener. Listens for messages on a <see cref="WebSocketSession"/> /// and raises events on requests. /// </summary> @@ -58,22 +46,16 @@ namespace VNLib.Net.Messaging.FBM.Server public const int SEND_SEMAPHORE_TIMEOUT_MS = 10 * 1000; - private readonly IUnmangedHeap Heap; - - /// <summary> - /// Raised when a response processing error occured - /// </summary> - public event EventHandler<Exception>? OnProcessError; + private readonly IFBMMemoryManager MemoryManger; /// <summary> /// Creates a new <see cref="FBMListener"/> instance ready for /// processing connections /// </summary> /// <param name="heap">The heap to alloc buffers from</param> - public FBMListener(IUnmangedHeap heap) - { - Heap = heap; - } + /// <exception cref="ArgumentNullException"></exception> + public FBMListener(IFBMMemoryManager heap) => MemoryManger = heap ?? throw new ArgumentNullException(nameof(heap)); + #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task /// <summary> @@ -83,31 +65,38 @@ namespace VNLib.Net.Messaging.FBM.Server /// <param name="wss">The <see cref="WebSocketSession"/> to receive messages on</param> /// <param name="handler">The callback method to handle incoming requests</param> /// <param name="args">The arguments used to configured this listening session</param> - /// <param name="userState">A state parameter</param> /// <returns>A <see cref="Task"/> that completes when the connection closes</returns> - public async Task ListenAsync(WebSocketSession wss, RequestHandler handler, FBMListenerSessionParams args, object? userState) + public async Task ListenAsync(WebSocketSession wss, IFBMServerMessageHandler handler, FBMListenerSessionParams args) { _ = wss ?? throw new ArgumentNullException(nameof(wss)); _ = handler ?? throw new ArgumentNullException(nameof(handler)); - ListeningSession session = new(wss, handler, in args, userState); - - //Alloc a recieve buffer - using IMemoryOwner<byte> recvBuffer = Heap.DirectAlloc<byte>(args.RecvBufferSize); + ListeningSession session = new(wss, handler, in args, MemoryManger); //Init new queue for dispatching work AsyncQueue<VnMemoryStream> workQueue = new(true, true); //Start a task to process the queue Task queueWorker = QueueWorkerDoWork(workQueue, session); - + + //Alloc buffer + IFBMMemoryHandle memHandle = MemoryManger.InitHandle(); + MemoryManger.AllocBuffer(memHandle, args.RecvBufferSize); + try { + if(!MemoryManger.TryGetHeap(out IUnmangedHeap? heap)) + { + throw new NotSupportedException("The memory manager must export an unmanaged heap"); + } + + Memory<byte> recvBuffer = memHandle.GetMemory(); + //Listen for incoming messages while (true) { //Receive a message - ValueWebSocketReceiveResult result = await wss.ReceiveAsync(recvBuffer.Memory); + ValueWebSocketReceiveResult result = await wss.ReceiveAsync(recvBuffer); //If a close message has been received, we can gracefully exit if (result.MessageType == WebSocketMessageType.Close) { @@ -118,13 +107,13 @@ namespace VNLib.Net.Messaging.FBM.Server } //create buffer for storing data, pre alloc with initial data - VnMemoryStream request = new(Heap, recvBuffer.Memory[..result.Count]); + VnMemoryStream request = new(heap, recvBuffer[..result.Count]); //Streaming read while (!result.EndOfMessage) { //Read more data - result = await wss.ReceiveAsync(recvBuffer.Memory); + result = await wss.ReceiveAsync(recvBuffer); //Make sure the request is small enough to buffer if (request.Length + result.Count > args.MaxMessageSize) { @@ -135,8 +124,9 @@ namespace VNLib.Net.Messaging.FBM.Server //break listen loop goto Exit; } + //write to buffer - request.Write(recvBuffer.Memory.Span[..result.Count]); + request.Write(memHandle.GetSpan()[..result.Count]); } //Make sure data is available if (request.Length == 0) @@ -195,14 +185,19 @@ namespace VNLib.Net.Messaging.FBM.Server if ((context.Request.ParseStatus & HeaderParseError.InvalidId) > 0) { - OnProcessError?.Invoke(this, new FBMException($"Invalid messageid {context.Request.MessageId}, message length {data.Length}")); - return; + Exception cause = new FBMException($"Invalid messageid {context.Request.MessageId}, message length {data.Length}"); + _ = session.Handler.OnInvalidMessage(context, cause); + return; //Cannot continue on invalid message id } //Check parse status flags if ((context.Request.ParseStatus & HeaderParseError.HeaderOutOfMem) > 0) { - OnProcessError?.Invoke(this, new FBMException("Packet received with not enough space to store headers")); + Exception cause = new FBMException("Packet received with not enough space to store headers"); + if(!session.Handler.OnInvalidMessage(context, cause)) + { + return; + } } //Determine if request is an out-of-band message else if (context.Request.MessageId == Helpers.CONTROL_FRAME_MID) @@ -213,18 +208,15 @@ namespace VNLib.Net.Messaging.FBM.Server else { //Invoke normal message handler - await session.OnRecieved.Invoke(context, session.UserState, session.CancellationToken); + await session.Handler.HandleMessage(context, session.CancellationToken); } - //Get response data - - await using IAsyncMessageReader messageEnumerator = await context.Response.GetResponseDataAsync(session.CancellationToken); - + //Get response data reader + await using IAsyncMessageReader messageEnumerator = context.Response.GetResponseData(); //Load inital segment if (await messageEnumerator.MoveNextAsync() && !session.CancellationToken.IsCancellationRequested) - { - ValueTask sendTask; + { //Syncrhonize access to send data because we may need to stream data to the client await session.ResponseLock.WaitAsync(SEND_SEMAPHORE_TIMEOUT_MS); @@ -233,10 +225,8 @@ namespace VNLib.Net.Messaging.FBM.Server { do { - bool eof = !messageEnumerator.DataRemaining; - - //Send first segment - sendTask = session.Socket.SendAsync(messageEnumerator.Current, WebSocketMessageType.Binary, eof); + //Send current segment + await session.Socket.SendAsync(messageEnumerator.Current, WebSocketMessageType.Binary, !messageEnumerator.DataRemaining); /* * WARNING! @@ -250,9 +240,6 @@ namespace VNLib.Net.Messaging.FBM.Server { break; } - - //Await previous send - await sendTask; } while (true); } @@ -261,15 +248,13 @@ namespace VNLib.Net.Messaging.FBM.Server //release semaphore session.ResponseLock.Release(); } - - await sendTask; } //No data to send } catch (Exception ex) { - OnProcessError?.Invoke(this, ex); + session.Handler.OnProcessError(ex); } finally { @@ -295,25 +280,23 @@ namespace VNLib.Net.Messaging.FBM.Server private readonly CancellationTokenSource Cancellation; private readonly CancellationTokenRegistration Registration; private readonly FBMListenerSessionParams Params; + private readonly IFBMMemoryManager MemManager; - public readonly object? UserState; - public readonly SemaphoreSlim ResponseLock; public readonly WebSocketSession Socket; - public readonly RequestHandler OnRecieved; + public readonly IFBMServerMessageHandler Handler; public CancellationToken CancellationToken => Cancellation.Token; - - public ListeningSession(WebSocketSession session, RequestHandler onRecieved, in FBMListenerSessionParams args, object? userState) + public ListeningSession(WebSocketSession session, IFBMServerMessageHandler handler, in FBMListenerSessionParams args, IFBMMemoryManager memManager) { Params = args; Socket = session; - UserState = userState; - OnRecieved = onRecieved; + Handler = handler; + MemManager = memManager; //Create cancellation and register for session close Cancellation = new(); @@ -323,7 +306,7 @@ namespace VNLib.Net.Messaging.FBM.Server CtxStore = ObjectRental.CreateReusable(ContextCtor); } - private FBMContext ContextCtor() => new(Params.MaxHeaderBufferSize, Params.ResponseBufferSize, Params.HeaderEncoding); + private FBMContext ContextCtor() => new(Params.MaxHeaderBufferSize, Params.ResponseBufferSize, Params.HeaderEncoding, MemManager); /// <summary> /// Cancels any pending opreations relating to the current session @@ -358,7 +341,6 @@ namespace VNLib.Net.Messaging.FBM.Server /// <exception cref="ObjectDisposedException"></exception> public FBMContext RentContext() { - if (Cancellation.IsCancellationRequested) { throw new ObjectDisposedException("The instance has been disposed"); diff --git a/lib/Net.Messaging.FBM/src/Server/FBMListenerBase.cs b/lib/Net.Messaging.FBM/src/Server/FBMListenerBase.cs index 3e9fde2..71b1c8f 100644 --- a/lib/Net.Messaging.FBM/src/Server/FBMListenerBase.cs +++ b/lib/Net.Messaging.FBM/src/Server/FBMListenerBase.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM @@ -27,67 +27,29 @@ using System.Threading; using System.Threading.Tasks; using VNLib.Utils.Logging; -using VNLib.Utils.Memory; using VNLib.Plugins.Essentials; namespace VNLib.Net.Messaging.FBM.Server { + /// <summary> /// Provides a simple base class for an <see cref="FBMListener"/> /// processor /// </summary> - public abstract class FBMListenerBase + public abstract class FBMListenerBase<T> : IFBMServerErrorHandler { /// <summary> /// The initialzied listener /// </summary> - protected FBMListener? Listener { get; private set; } + protected abstract FBMListener Listener { get; } + /// <summary> /// A provider to write log information to /// </summary> protected abstract ILogProvider Log { get; } /// <summary> - /// Initializes the <see cref="FBMListener"/> - /// </summary> - /// <param name="heap">The heap to alloc buffers from</param> - protected void InitListener(IUnmangedHeap heap) - { - Listener = new(heap); - //Attach service handler - Listener.OnProcessError += Listener_OnProcessError; - } - - /// <summary> - /// A single event service routine for servicing errors that occur within - /// the listener loop - /// </summary> - /// <param name="sender"></param> - /// <param name="e">The exception that was raised</param> - protected virtual void Listener_OnProcessError(object? sender, Exception e) - { - //Write the error to the log - Log.Error(e); - } - - private async Task OnReceivedAsync(FBMContext context, object? userState, CancellationToken token) - { - try - { - await ProcessAsync(context, userState, token); - } - catch (OperationCanceledException) - { - Log.Debug("Async operation cancelled"); - } - catch(Exception ex) - { - Log.Error(ex); - } - } - - /// <summary> /// Begins listening for requests on the current websocket until /// a close message is received or an error occurs /// </summary> @@ -95,10 +57,13 @@ namespace VNLib.Net.Messaging.FBM.Server /// <param name="args">The arguments used to configured this listening session</param> /// <param name="userState">A state token to use for processing events for this connection</param> /// <returns>A <see cref="Task"/> that completes when the connection closes</returns> - public virtual async Task ListenAsync(WebSocketSession wss, FBMListenerSessionParams args, object? userState) + /// <exception cref="InvalidOperationException"></exception> + public virtual Task ListenAsync(WebSocketSession wss, T userState, FBMListenerSessionParams args) { _ = Listener ?? throw new InvalidOperationException("The listener has not been intialized"); - await Listener.ListenAsync(wss, OnReceivedAsync, args, userState); + //Initn new event handler + FBMEventHandler handler = new(userState, this); + return Listener.ListenAsync(wss, handler, args); } /// <summary> @@ -108,6 +73,30 @@ namespace VNLib.Net.Messaging.FBM.Server /// <param name="userState">A state token passed on client connected</param> /// <param name="exitToken">A token that reflects the state of the listener</param> /// <returns>A task that completes when the message has been serviced</returns> - protected abstract Task ProcessAsync(FBMContext context, object? userState, CancellationToken exitToken); + protected abstract Task ProcessAsync(FBMContext context, T? userState, CancellationToken exitToken); + + ///<inheritdoc/> + public virtual bool OnInvalidMessage(FBMContext context, Exception ex) + { + Log.Error("Invalid message received for session {ses}\n{ex}", context.Request.ConnectionId, ex); + //Invalid id should be captured already, so if oom, do not allow, but if a single header is invalid, it will be ignored by default + return !context.Request.ParseStatus.HasFlag(HeaderParseError.HeaderOutOfMem); + } + + ///<inheritdoc/> + public virtual void OnProcessError(Exception ex) => Log.Error(ex); + + + private sealed record class FBMEventHandler(T State, FBMListenerBase<T> Lb) : IFBMServerMessageHandler + { + ///<inheritdoc/> + public Task HandleMessage(FBMContext context, CancellationToken cancellationToken) => Lb.ProcessAsync(context, State, cancellationToken); + + ///<inheritdoc/> + public bool OnInvalidMessage(FBMContext context, Exception ex) => Lb.OnInvalidMessage(context, ex); + + ///<inheritdoc/> + public void OnProcessError(Exception ex) => Lb.OnProcessError(ex); + } } } diff --git a/lib/Net.Messaging.FBM/src/Server/FBMListenerSessionParams.cs b/lib/Net.Messaging.FBM/src/Server/FBMListenerSessionParams.cs index ccf79db..0b4fa5b 100644 --- a/lib/Net.Messaging.FBM/src/Server/FBMListenerSessionParams.cs +++ b/lib/Net.Messaging.FBM/src/Server/FBMListenerSessionParams.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM @@ -30,7 +30,7 @@ namespace VNLib.Net.Messaging.FBM.Server /// Represents a configuration structure for an <see cref="FBMListener"/> /// listening session /// </summary> - public readonly struct FBMListenerSessionParams + public readonly record struct FBMListenerSessionParams { /// <summary> /// The size of the buffer to use while reading data from the websocket diff --git a/lib/Net.Messaging.FBM/src/Server/FBMRequestMessage.cs b/lib/Net.Messaging.FBM/src/Server/FBMRequestMessage.cs index db0655a..e9ff9f5 100644 --- a/lib/Net.Messaging.FBM/src/Server/FBMRequestMessage.cs +++ b/lib/Net.Messaging.FBM/src/Server/FBMRequestMessage.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM @@ -24,13 +24,12 @@ using System; using System.Text; -using System.Buffers; -using System.Text.Json; using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Runtime.CompilerServices; using VNLib.Utils.IO; -using VNLib.Utils.Extensions; +using VNLib.Utils.Memory; using VNLib.Utils.Memory.Caching; namespace VNLib.Net.Messaging.FBM.Server @@ -42,18 +41,21 @@ namespace VNLib.Net.Messaging.FBM.Server { private readonly List<FBMMessageHeader> _headers; private readonly int HeaderBufferSize; + private readonly IFBMMemoryManager _memoryManager; + private readonly IFBMSpanOnlyMemoryHandle _memHandle; /// <summary> /// Creates a new resusable <see cref="FBMRequestMessage"/> /// </summary> /// <param name="headerBufferSize">The size of the buffer to alloc during initialization</param> - internal FBMRequestMessage(int headerBufferSize) + /// <param name="manager">The memory manager to use for the message</param> + internal FBMRequestMessage(int headerBufferSize, IFBMMemoryManager manager) { HeaderBufferSize = headerBufferSize; _headers = new(); + _memoryManager = manager; + _memHandle = _memoryManager.InitSpanOnly(); } - - private char[]? _headerBuffer; /// <summary> /// The ID of the current message @@ -115,35 +117,13 @@ namespace VNLib.Net.Messaging.FBM.Server //Parse headers ParseStatus = Helpers.ParseHeaders(vms, this, _headers, dataEncoding); } - - /// <summary> - /// Deserializes the request body into a new specified object type - /// </summary> - /// <typeparam name="T">The type of the object to deserialize</typeparam> - /// <param name="jso">The <see cref="JsonSerializerOptions"/> to use while deserializing data</param> - /// <returns>The deserialized object from the request body</returns> - /// <exception cref="JsonException"></exception> - public T? DeserializeBody<T>(JsonSerializerOptions? jso = default) - { - return BodyData.IsEmpty ? default : BodyData.AsJsonObject<T>(jso); - } - - /// <summary> - /// Gets a <see cref="JsonDocument"/> of the request body - /// </summary> - /// <returns>The parsed <see cref="JsonDocument"/> if parsed successfully, or null otherwise</returns> - /// <exception cref="JsonException"></exception> - public JsonDocument? GetBodyAsJson() - { - Utf8JsonReader reader = new(BodyData); - return JsonDocument.TryParseValue(ref reader, out JsonDocument? jdoc) ? jdoc : default; - } + void IReusable.Prepare() { ParseStatus = HeaderParseError.None; //Alloc header buffer - _headerBuffer = ArrayPool<char>.Shared.Rent(HeaderBufferSize); + _memoryManager.AllocBuffer(_memHandle, MemoryUtil.ByteCount<char>(HeaderBufferSize)); } @@ -155,8 +135,7 @@ namespace VNLib.Net.Messaging.FBM.Server //Clear headers before freeing buffer _headers.Clear(); //Free header-buffer - ArrayPool<char>.Shared.Return(_headerBuffer!); - _headerBuffer = null; + _memoryManager.FreeBuffer(_memHandle); ConnectionId = null; MessageId = 0; IsControlFrame = false; @@ -165,11 +144,16 @@ namespace VNLib.Net.Messaging.FBM.Server [MethodImpl(MethodImplOptions.AggressiveInlining)] - Span<char> IFBMHeaderBuffer.GetSpan(int offset, int count) - => _headerBuffer != null ? _headerBuffer.AsSpan(offset, count) : throw new InvalidOperationException("The buffer is no longer available"); + Span<char> IFBMHeaderBuffer.GetSpan(int offset, int count) + { + //Cast to char buffer + Span<char> chars = MemoryMarshal.Cast<byte, char>(_memHandle.GetSpan()); + //Return the requested span + return chars.Slice(offset, count); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - Span<char> IFBMHeaderBuffer.GetSpan() => _headerBuffer ?? throw new InvalidOperationException("The buffer is no longer available"); + Span<char> IFBMHeaderBuffer.GetSpan() => MemoryMarshal.Cast<byte, char>(_memHandle.GetSpan()); } } diff --git a/lib/Net.Messaging.FBM/src/Server/FBMResponseMessage.cs b/lib/Net.Messaging.FBM/src/Server/FBMResponseMessage.cs index 9ca6b4d..1e26140 100644 --- a/lib/Net.Messaging.FBM/src/Server/FBMResponseMessage.cs +++ b/lib/Net.Messaging.FBM/src/Server/FBMResponseMessage.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM @@ -24,14 +24,12 @@ using System; using System.Text; -using System.Threading; using System.Threading.Tasks; using VNLib.Net.Http; using VNLib.Utils.IO; using VNLib.Utils.Extensions; using VNLib.Utils.Memory.Caching; -using VNLib.Net.Messaging.FBM.Client; namespace VNLib.Net.Messaging.FBM.Server { @@ -41,9 +39,9 @@ namespace VNLib.Net.Messaging.FBM.Server /// </summary> public sealed class FBMResponseMessage : IReusable, IFBMMessage { - internal FBMResponseMessage(int internalBufferSize, Encoding headerEncoding) + internal FBMResponseMessage(int internalBufferSize, Encoding headerEncoding, IFBMMemoryManager manager) { - _headerAccumulator = new HeaderDataAccumulator(internalBufferSize); + _headerAccumulator = new HeaderDataAccumulator(internalBufferSize, manager); _headerEncoding = headerEncoding; _messageEnumerator = new(this); } @@ -134,92 +132,76 @@ namespace VNLib.Net.Messaging.FBM.Server /// <summary> /// Gets the internal message body enumerator and prepares the message for sending /// </summary> - /// <param name="cancellationToken">A cancellation token</param> /// <returns>A value task that returns the message body enumerator</returns> - internal async ValueTask<IAsyncMessageReader> GetResponseDataAsync(CancellationToken cancellationToken) - { - //try to buffer as much data in the header segment first - if(MessageBody?.RemainingSize > 0 && _headerAccumulator.RemainingSize > 0) - { - //Read data from the message - int read = await MessageBody.ReadAsync(_headerAccumulator.RemainingBuffer, cancellationToken); - - //Advance accumulator to the read bytes - _headerAccumulator.Advance(read); - } - //return reusable enumerator - return _messageEnumerator; - } + internal IAsyncMessageReader GetResponseData() => _messageEnumerator; private sealed class MessageSegmentEnumerator : IAsyncMessageReader { private readonly FBMResponseMessage _message; + private readonly ISlindingWindowBuffer<byte> _accumulator; bool HeadersRead; public MessageSegmentEnumerator(FBMResponseMessage message) { _message = message; + _accumulator = _message._headerAccumulator; } - public ReadOnlyMemory<byte> Current { get; private set; } + ///<inheritdoc/> + public ReadOnlyMemory<byte> Current => _accumulator.AccumulatedBuffer; - public bool DataRemaining { get; private set; } + ///<inheritdoc/> + public bool DataRemaining => _message.MessageBody?.RemainingSize > 0; + ///<inheritdoc/> public async ValueTask<bool> MoveNextAsync() { //Attempt to read header segment first if (!HeadersRead) { - //Set the accumulated buffer - Current = _message._headerAccumulator.AccumulatedBuffer; + /* + * If headers have not been read yet, we can attempt to buffer as much + * of the message body into the header accumulator buffer as possible. This will + * reduce message fragmentation. + */ + if (DataRemaining && _accumulator.RemainingSize > 0) + { + int read = await _message.MessageBody.ReadAsync(_accumulator.RemainingBuffer).ConfigureAwait(false); - //Update data remaining flag - DataRemaining = _message.MessageBody?.RemainingSize > 0; + //Advance accumulator to the read bytes + _accumulator.Advance(read); + } //Set headers read flag HeadersRead = true; return true; } - else if (_message.MessageBody?.RemainingSize > 0) + else if (DataRemaining) { - //Use the header buffer as the buffer for the message body - Memory<byte> buffer = _message._headerAccumulator.Buffer; + //Reset the accumulator so we can read another segment + _accumulator.Reset(); //Read body segment - int read = await _message.MessageBody.ReadAsync(buffer); + int read = await _message.MessageBody.ReadAsync(_accumulator.RemainingBuffer); - //Update data remaining flag - DataRemaining = _message.MessageBody.RemainingSize > 0; + //Advance accumulator to the read bytes + _accumulator.Advance(read); - if (read > 0) - { - //Store the read segment - Current = buffer[..read]; - return true; - } + return read > 0; } return false; } + ///<inheritdoc/> public ValueTask DisposeAsync() { - //Clear current segment - Current = default; - //Reset headers read flag HeadersRead = false; - + //Dispose the message body if set - if (_message.MessageBody != null) - { - return _message.MessageBody.DisposeAsync(); - } - else - { - return ValueTask.CompletedTask; - } + return _message.MessageBody != null ? _message.MessageBody.DisposeAsync() : ValueTask.CompletedTask; } } } diff --git a/lib/Net.Messaging.FBM/src/Server/HeaderDataAccumulator.cs b/lib/Net.Messaging.FBM/src/Server/HeaderDataAccumulator.cs index 423a26e..891c583 100644 --- a/lib/Net.Messaging.FBM/src/Server/HeaderDataAccumulator.cs +++ b/lib/Net.Messaging.FBM/src/Server/HeaderDataAccumulator.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM @@ -23,25 +23,28 @@ */ using System; -using System.Buffers; using VNLib.Utils.IO; namespace VNLib.Net.Messaging.FBM.Server { + /// <summary> /// Reusable sliding window impl /// </summary> internal sealed class HeaderDataAccumulator : ISlindingWindowBuffer<byte> { - private readonly int BufferSize; - - private byte[]? _memHandle; + private readonly int _bufferSize; + private readonly IFBMMemoryManager _memManager; + private readonly IFBMMemoryHandle _handle; + - public HeaderDataAccumulator(int bufferSize) + public HeaderDataAccumulator(int bufferSize, IFBMMemoryManager memManager) { - BufferSize = bufferSize; + _bufferSize = bufferSize; + _memManager = memManager; + _handle = memManager.InitHandle(); } ///<inheritdoc/> @@ -49,7 +52,7 @@ namespace VNLib.Net.Messaging.FBM.Server ///<inheritdoc/> public int WindowEndPos { get; private set; } ///<inheritdoc/> - public Memory<byte> Buffer => _memHandle.AsMemory(); + public Memory<byte> Buffer => _handle.GetMemory(); ///<inheritdoc/> public void Advance(int count) => WindowEndPos += count; @@ -67,22 +70,13 @@ namespace VNLib.Net.Messaging.FBM.Server /// <summary> /// Allocates the internal message buffer /// </summary> - public void Prepare() - { - _memHandle ??= ArrayPool<byte>.Shared.Rent(BufferSize); - } + public void Prepare() => _memManager.AllocBuffer(_handle, _bufferSize); ///<inheritdoc/> public void Close() { Reset(); - - if (_memHandle != null) - { - //Return the buffer to the pool - ArrayPool<byte>.Shared.Return(_memHandle); - _memHandle = null; - } + _memManager.FreeBuffer(_handle); } } diff --git a/lib/Net.Messaging.FBM/src/Server/IAsyncMessageReader.cs b/lib/Net.Messaging.FBM/src/Server/IAsyncMessageReader.cs index b2abe8d..abb3600 100644 --- a/lib/Net.Messaging.FBM/src/Server/IAsyncMessageReader.cs +++ b/lib/Net.Messaging.FBM/src/Server/IAsyncMessageReader.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM @@ -34,7 +34,7 @@ namespace VNLib.Net.Messaging.FBM.Server internal interface IAsyncMessageReader : IAsyncEnumerator<ReadOnlyMemory<byte>> { /// <summary> - /// A value that indicates if there is data remaining after a + /// A value that indicates if there is data remaining after a read /// </summary> bool DataRemaining { get; } } diff --git a/lib/Net.Messaging.FBM/src/Server/IFBMServerErrorHandler.cs b/lib/Net.Messaging.FBM/src/Server/IFBMServerErrorHandler.cs new file mode 100644 index 0000000..caa4f96 --- /dev/null +++ b/lib/Net.Messaging.FBM/src/Server/IFBMServerErrorHandler.cs @@ -0,0 +1,49 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Messaging.FBM +* File: IFBMServerErrorHandler.cs +* +* IFBMServerErrorHandler.cs is part of VNLib.Net.Messaging.FBM which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Messaging.FBM is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Messaging.FBM is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Net.Messaging.FBM.Server +{ + /// <summary> + /// An server side FBM protocol error handler abstraction + /// </summary> + public interface IFBMServerErrorHandler + { + /// <summary> + /// An exception handler for unhandled events that occur during a listening session + /// </summary> + /// <param name="ex">The exception that caused this handler to be invoked</param> + void OnProcessError(Exception ex); + + /// <summary> + /// An exception handler for invalid messages that occur during a listening session. + /// NOTE: The context parameter is likely in an invlaid state and should be read carefully + /// </summary> + /// <param name="context">The context that the error occured while parsing on</param> + /// <param name="ex">The exception explaining the reason this handler was invoked</param> + /// <returns>A value that indicates if the server should continue processing</returns> + bool OnInvalidMessage(FBMContext context, Exception ex); + } +} diff --git a/lib/Net.Messaging.FBM/src/Server/IFBMServerMessageHandler.cs b/lib/Net.Messaging.FBM/src/Server/IFBMServerMessageHandler.cs new file mode 100644 index 0000000..532db5f --- /dev/null +++ b/lib/Net.Messaging.FBM/src/Server/IFBMServerMessageHandler.cs @@ -0,0 +1,43 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Messaging.FBM +* File: IFBMServerMessageHandler.cs +* +* IFBMServerMessageHandler.cs is part of VNLib.Net.Messaging.FBM which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Messaging.FBM is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Messaging.FBM is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Net.Messaging.FBM.Server +{ + /// <summary> + /// A server side FBM protocol handler + /// </summary> + public interface IFBMServerMessageHandler : IFBMServerErrorHandler + { + /// <summary> + /// Handles processing of a normal incoming message + /// </summary> + /// <param name="context">The context to process for this new message</param> + /// <param name="cancellationToken">A token that signals the session has been cancelled</param> + /// <returns>A task representing the asynchronous work</returns> + Task HandleMessage(FBMContext context, CancellationToken cancellationToken); + } +} diff --git a/lib/Net.Messaging.FBM/src/VNLib.Net.Messaging.FBM.csproj b/lib/Net.Messaging.FBM/src/VNLib.Net.Messaging.FBM.csproj index 7fade2c..70a640d 100644 --- a/lib/Net.Messaging.FBM/src/VNLib.Net.Messaging.FBM.csproj +++ b/lib/Net.Messaging.FBM/src/VNLib.Net.Messaging.FBM.csproj @@ -35,7 +35,6 @@ </ItemGroup> <ItemGroup> - <ProjectReference Include="..\..\Net.Http\src\VNLib.Net.Http.csproj" /> <ProjectReference Include="..\..\Plugins.Essentials\src\VNLib.Plugins.Essentials.csproj" /> <ProjectReference Include="..\..\Utils\src\VNLib.Utils.csproj" /> </ItemGroup> diff --git a/lib/Net.Transport.SimpleTCP/src/ReusableNetworkStream.cs b/lib/Net.Transport.SimpleTCP/src/ReusableNetworkStream.cs index 7174a99..ce989b3 100644 --- a/lib/Net.Transport.SimpleTCP/src/ReusableNetworkStream.cs +++ b/lib/Net.Transport.SimpleTCP/src/ReusableNetworkStream.cs @@ -56,6 +56,10 @@ namespace VNLib.Net.Transport.Tcp public override long Position { get => throw new NotSupportedException(); set => throw new NotImplementedException(); } public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + => throw new NotSupportedException("CopyToAsync is not supported"); + + public override void CopyTo(Stream destination, int bufferSize) => throw new NotSupportedException("CopyTo is not supported"); #endregion //Read timeout to use when receiving data @@ -84,70 +88,54 @@ namespace VNLib.Net.Transport.Tcp ///<inheritdoc/> public override void Close() - { } + { + //Call sync + Task closing = Transport.CloseAsync(); + closing.GetAwaiter().GetResult(); + } + ///<inheritdoc/> public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + ///<inheritdoc/> public override void Flush() { } ///<inheritdoc/> public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count)); + ///<inheritdoc/> public override int Read(Span<byte> buffer) => Transport.Recv(buffer); ///<inheritdoc/> public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - //Since read returns a value, it isnt any cheaper not to alloc a task around the value-task - return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); - } + //Since read returns a value, it isnt any cheaper not to alloc a task around the value-task + => ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + ///<inheritdoc/> - public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) - { - return Transport.RecvAsync(buffer, cancellationToken); - } + public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) + => Transport.RecvAsync(buffer, cancellationToken); ///<inheritdoc/> public override void Write(byte[] buffer, int offset, int count) => Write(buffer.AsSpan(offset, count)); + ///<inheritdoc/> public override void Write(ReadOnlySpan<byte> buffer) => Transport.Send(buffer); - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - //Allow synchronous complete to avoid alloc - return WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); - } + ///<inheritdoc/> + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); ///<inheritdoc/> ///<exception cref="IOException"></exception> ///<exception cref="ObjectDisposedException"></exception> - public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellation = default) - { - return Transport.SendAsync(buffer, cancellation); - } + public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellation = default) + => Transport.SendAsync(buffer, cancellation); /* * Override dispose to intercept base cleanup until the internal release */ - /// <summary> - /// Not supported - /// </summary> - public new void Dispose() - { - //Call sync - Task closing = Transport.CloseAsync(); - closing.GetAwaiter().GetResult(); - } - public override ValueTask DisposeAsync() - { - return new ValueTask(Transport.CloseAsync()); - } - - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - throw new NotSupportedException("CopyToAsync is not supported"); - } + public override ValueTask DisposeAsync() => new (Transport.CloseAsync()); } }
\ No newline at end of file diff --git a/lib/Plugins.Essentials.ServiceStack/src/Construction/SsBuilderExtensions.cs b/lib/Plugins.Essentials.ServiceStack/src/Construction/SsBuilderExtensions.cs index abb1cc1..7b391a0 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/Construction/SsBuilderExtensions.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Construction/SsBuilderExtensions.cs @@ -241,7 +241,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction //Add middleware to the chain foreach (IHttpMiddleware mw in Config.CustomMiddleware) { - Instance.MiddlewareChain.AddLast(mw); + Instance.MiddlewareChain.Add(mw); } } @@ -263,7 +263,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction plugin.OnPluginServiceEvent<IAccountSecurityProvider>(Instance.SetAccountSecProvider); //Add all middleware to the chain - plugin.OnPluginServiceEvent<IHttpMiddleware[]>(p => Array.ForEach(p, mw => Instance.MiddlewareChain.AddLast(mw))); + plugin.OnPluginServiceEvent<IHttpMiddleware[]>(p => Array.ForEach(p, mw => Instance.MiddlewareChain.Add(mw))); } ///<inheritdoc/> diff --git a/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs b/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs index 3c56866..83e6a06 100644 --- a/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs +++ b/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs @@ -24,9 +24,9 @@ using System.Threading.Tasks; - namespace VNLib.Plugins.Essentials.Middleware { + /// <summary> /// Represents a low level intermediate request processor with high privilages, meant to add /// functionality to entity processing. diff --git a/lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.cs b/lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.cs index ace0c86..0a05c70 100644 --- a/lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.cs +++ b/lib/Plugins.Essentials/src/Middleware/IHttpMiddlewareChain.cs @@ -43,13 +43,7 @@ namespace VNLib.Plugins.Essentials.Middleware /// Adds a middleware handler to the end of the chain /// </summary> /// <param name="middleware">The middleware processor to add</param> - void AddLast(IHttpMiddleware middleware); - - /// <summary> - /// Adds a middleware handler to the beginning of the chain - /// </summary> - /// <param name="middleware">The middleware processor to add</param> - void AddFirst(IHttpMiddleware middleware); + void Add(IHttpMiddleware middleware); /// <summary> /// Removes a middleware handler from the chain diff --git a/lib/Plugins.Essentials/src/Middleware/MiddlewareImplAttribute.cs b/lib/Plugins.Essentials/src/Middleware/MiddlewareImplAttribute.cs new file mode 100644 index 0000000..d5a66a4 --- /dev/null +++ b/lib/Plugins.Essentials/src/Middleware/MiddlewareImplAttribute.cs @@ -0,0 +1,48 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: MiddlewareImplAttribute.cs +* +* MiddlewareImplAttribute.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Plugins.Essentials.Middleware +{ + /// <summary> + /// Specifies optional implementation flags for a middleware instance + /// that loaders may use during soriting of the middleware chain + /// </summary> + [AttributeUsage(AttributeTargets.Class)] + public sealed class MiddlewareImplAttribute : Attribute + { + /// <summary> + /// The option flags for a middleware instance + /// </summary> + public MiddlewareImplOptions ImplOptions { get; } + + /// <summary> + /// Creates a new <see cref="MiddlewareImplAttribute"/> instance + /// with the specified <see cref="MiddlewareImplOptions"/> + /// </summary> + /// <param name="implOptions">Implementation option flags</param> + public MiddlewareImplAttribute(MiddlewareImplOptions implOptions) => ImplOptions = implOptions; + } +} diff --git a/lib/Plugins.Essentials/src/Middleware/MiddlewareImplOptions.cs b/lib/Plugins.Essentials/src/Middleware/MiddlewareImplOptions.cs new file mode 100644 index 0000000..1e325e3 --- /dev/null +++ b/lib/Plugins.Essentials/src/Middleware/MiddlewareImplOptions.cs @@ -0,0 +1,46 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: MiddlewareImplOptions.cs +* +* MiddlewareImplOptions.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + + +namespace VNLib.Plugins.Essentials.Middleware +{ + /// <summary> + /// Implementation flags for midleware implementations + /// </summary> + [Flags] + public enum MiddlewareImplOptions + { + /// <summary> + /// No flags + /// </summary> + None = 0x00, + /// <summary> + /// Prioritizes a middleware instance in the chain because + /// it is required for security purposes + /// </summary> + SecurityCritical = 0x01 + } +} diff --git a/lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs b/lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs index 1e1db22..5d0c472 100644 --- a/lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs +++ b/lib/Plugins.Essentials/src/Middleware/SemiConistentMiddlewareChain.cs @@ -22,6 +22,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System.Reflection; using System.Collections.Generic; namespace VNLib.Plugins.Essentials.Middleware @@ -36,20 +37,23 @@ namespace VNLib.Plugins.Essentials.Middleware private LinkedList<IHttpMiddleware> _middlewares = new(); ///<inheritdoc/> - public void AddFirst(IHttpMiddleware middleware) + public void Add(IHttpMiddleware middleware) { - lock (_middlewares) - { - _middlewares.AddFirst(middleware); - } - } + //Get security critical flag + bool isSecCritical = middleware.GetType().GetCustomAttribute<MiddlewareImplAttribute>() + ?.ImplOptions.HasFlag(MiddlewareImplOptions.SecurityCritical) ?? false; - ///<inheritdoc/> - public void AddLast(IHttpMiddleware middleware) - { lock (_middlewares) { - _middlewares.AddLast(middleware); + //Always add security critical middleware to the front of the chain + if (isSecCritical) + { + _middlewares.AddFirst(middleware); + } + else + { + _middlewares.AddLast(middleware); + } } } diff --git a/lib/Plugins.PluginBase/src/PluginBase.cs b/lib/Plugins.PluginBase/src/PluginBase.cs index bb42c97..839e331 100644 --- a/lib/Plugins.PluginBase/src/PluginBase.cs +++ b/lib/Plugins.PluginBase/src/PluginBase.cs @@ -107,6 +107,11 @@ namespace VNLib.Plugins protected virtual string LogTemplate => $"{{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}} [{{Level:u3}}] {PluginName}: {{Message:lj}}{{NewLine}}{{Exception}}"; /// <summary> + /// Arguments passed to the plugin by the host application + /// </summary> + public ArgumentList HostArgs { get; private set; } + + /// <summary> /// The host application may invoke this method when the assembly is loaded and this plugin is constructed to pass /// a configuration object to the instance. This method populates the configuration objects if applicable. /// </summary> @@ -133,16 +138,16 @@ namespace VNLib.Plugins [LogInitializer] public virtual void InitLog(string[] cmdArgs) { - ArgumentList args = new(cmdArgs); + HostArgs = new(cmdArgs); //Open new logger config LoggerConfiguration logConfig = new(); //Check for verbose - if (args.HasArgument("-v")) + if (HostArgs.HasArgument("-v")) { logConfig.MinimumLevel.Verbose(); } //Check for debug mode - else if (args.HasArgument("-d")) + else if (HostArgs.HasArgument("-d")) { logConfig.MinimumLevel.Debug(); } @@ -153,7 +158,7 @@ namespace VNLib.Plugins } //Init console log - InitConsoleLog(args, logConfig); + InitConsoleLog(logConfig); //Init file log InitFileLog(logConfig); @@ -162,10 +167,10 @@ namespace VNLib.Plugins Log = new VLogProvider(logConfig); } - private void InitConsoleLog(ArgumentList args, LoggerConfiguration logConfig) + private void InitConsoleLog(LoggerConfiguration logConfig) { //If silent arg is not specified, open log to console - if (!(args.HasArgument("--silent") || args.HasArgument("-s"))) + if (!(HostArgs.HasArgument("--silent") || HostArgs.HasArgument("-s"))) { _ = logConfig.WriteTo.Console(outputTemplate: LogTemplate, formatProvider:null); } diff --git a/lib/Utils.Memory/vnlib_mimalloc/Taskfile.yaml b/lib/Utils.Memory/vnlib_mimalloc/Taskfile.yaml index ddb5d0e..e1e0da5 100644 --- a/lib/Utils.Memory/vnlib_mimalloc/Taskfile.yaml +++ b/lib/Utils.Memory/vnlib_mimalloc/Taskfile.yaml @@ -42,7 +42,7 @@ tasks: - cd ../mimalloc/build && msbuild libmimalloc.sln /p:Configuration=release {{.BUILD_FLAGS}} {{.MS_ARGS}} #init cmake build with greedy enabled - - cmake -B./build -DENABLE_GREEDY=1 + - cmake -B./build -DENABLE_GREEDY=1 {{.MIMALLOC_CMAKE_ARGS}} #build solution in debug mode - cd build && msbuild {{.PROJECT_NAME}}.sln /p:Configuration=debug {{.BUILD_FLAGS}} {{.MS_ARGS}} diff --git a/lib/Utils/src/Async/IAsyncEventSink.cs b/lib/Utils/src/Async/IAsyncEventSink.cs new file mode 100644 index 0000000..634b3e6 --- /dev/null +++ b/lib/Utils/src/Async/IAsyncEventSink.cs @@ -0,0 +1,48 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IAsyncEventSink.cs +* +* IAsyncEventSink.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +namespace VNLib.Utils.Async +{ + /// <summary> + /// A type that receives events from asynchronous event sources and publishes + /// them to subscribers. + /// </summary> + /// <typeparam name="T">The event type</typeparam> + public interface IAsyncEventSink<T> + { + /// <summary> + /// Publishes a single event to all subscribers + /// </summary> + /// <param name="evnt">The event to publish</param> + /// <returns>A value that indicates if the event was successfully published to subscribers</returns> + bool PublishEvent(T evnt); + + /// <summary> + /// Publishes an array of events to all subscribers + /// </summary> + /// <param name="events">The array of events to publish</param> + /// <returns>A value that indicates if the events were successfully published to subscribers</returns> + bool PublishEvents(T[] events); + } +} diff --git a/lib/Utils/src/Extensions/MemoryExtensions.cs b/lib/Utils/src/Extensions/MemoryExtensions.cs index 6525db4..d21ceee 100644 --- a/lib/Utils/src/Extensions/MemoryExtensions.cs +++ b/lib/Utils/src/Extensions/MemoryExtensions.cs @@ -48,11 +48,7 @@ namespace VNLib.Utils.Extensions /// <param name="size">The minimum size array to allocate</param> /// <param name="zero">Should elements from 0 to size be set to default(T)</param> /// <returns>A new <see cref="OpenResourceHandle{T}"/> encapsulating the rented array</returns> - public static UnsafeMemoryHandle<T> Lease<T>(this ArrayPool<T> pool, int size, bool zero = false) where T: unmanaged - { - //Pool buffer handles are considered "safe" so im reusing code for now - return new(pool, size, zero); - } + public static UnsafeMemoryHandle<T> Lease<T>(this ArrayPool<T> pool, int size, bool zero = false) where T : unmanaged => new(pool, size, zero); /// <summary> /// Retreives a buffer that is at least the reqested length, and clears the array from 0-size. @@ -81,32 +77,10 @@ namespace VNLib.Utils.Extensions /// </summary> /// <returns>The string representation of the buffer</returns> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string ToString<T>(this T charBuffer) where T: IMemoryHandle<char> - { - return charBuffer.Span.ToString(); - } - - /// <summary> - /// Wraps the <see cref="MemoryHandle{T}"/> instance in System.Buffers.MemoryManager - /// wrapper to provide <see cref="Memory{T}"/> buffers from umanaged handles. - /// </summary> - /// <typeparam name="T">The unmanaged data type</typeparam> - /// <param name="handle"></param> - /// <param name="ownsHandle"> - /// A value that indicates if the new <see cref="MemoryManager{T}"/> owns the handle. - /// When <c>true</c>, the new <see cref="MemoryManager{T}"/> maintains the lifetime of the handle. - /// </param> - /// <returns>The <see cref="MemoryManager{T}"/> wrapper</returns> - /// <remarks>NOTE: This wrapper now manages the lifetime of the current handle</remarks> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MemoryManager<T> ToMemoryManager<T>(this MemoryHandle<T> handle, bool ownsHandle = true) where T : unmanaged - { - _ = handle ?? throw new ArgumentNullException(nameof(handle)); - return new SysBufferMemoryManager<T>(handle, ownsHandle); - } + public static string ToString<T>(this T charBuffer) where T : IMemoryHandle<char> => charBuffer.Span.ToString(); /// <summary> - /// Wraps the <see cref="VnTempBuffer{T}"/> instance in System.Buffers.MemoryManager + /// Wraps the <see cref="IMemoryHandle{T}"/> instance in System.Buffers.MemoryManager /// wrapper to provide <see cref="Memory{T}"/> buffers from umanaged handles. /// </summary> /// <typeparam name="T">The unmanaged data type</typeparam> @@ -117,15 +91,12 @@ namespace VNLib.Utils.Extensions /// </param> /// <returns>The <see cref="MemoryManager{T}"/> wrapper</returns> /// <remarks>NOTE: This wrapper now manages the lifetime of the current handle</remarks> + /// <exception cref="ArgumentNullException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MemoryManager<T> ToMemoryManager<T>(this VnTempBuffer<T> handle, bool ownsHandle = true) where T : unmanaged - { - _ = handle ?? throw new ArgumentNullException(nameof(handle)); - return new SysBufferMemoryManager<T>(handle, ownsHandle); - } + public static MemoryManager<T> ToMemoryManager<T>(this IMemoryHandle<T> handle, bool ownsHandle) => new SysBufferMemoryManager<T>(handle, ownsHandle); /// <summary> - /// Allows direct allocation of a fixed size <see cref="MemoryManager{T}"/> from a <see cref="Win32PrivateHeap"/> instance + /// Allows direct allocation of a fixed size <see cref="MemoryManager{T}"/> from a <see cref="IUnmangedHeap"/> instance /// of the specified number of elements /// </summary> /// <typeparam name="T">The unmanaged data type</typeparam> @@ -133,10 +104,14 @@ namespace VNLib.Utils.Extensions /// <param name="size">The number of elements to allocate on the heap</param> /// <param name="zero">Optionally zeros conents of the block when allocated</param> /// <returns>The <see cref="MemoryManager{T}"/> wrapper around the block of memory</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ObjectDisposedException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MemoryManager<T> DirectAlloc<T>(this IUnmangedHeap heap, nuint size, bool zero = false) where T : unmanaged { - return new SysBufferMemoryManager<T>(heap, size, zero); + MemoryHandle<T> handle = heap.Alloc<T>(size, zero); + return new SysBufferMemoryManager<T>(handle, true); } /// <summary> @@ -163,10 +138,10 @@ namespace VNLib.Utils.Extensions /// </returns> //Method only exists for consistancy since unsafe handles are always 32bit [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetIntLength<T>(this in UnsafeMemoryHandle<T> handle) where T: unmanaged => handle.IntLength; + public static int GetIntLength<T>(this in UnsafeMemoryHandle<T> handle) where T : unmanaged => handle.IntLength; /// <summary> - /// Allows direct allocation of a fixed size <see cref="MemoryManager{T}"/> from a <see cref="Win32PrivateHeap"/> instance + /// Allows direct allocation of a fixed size <see cref="MemoryManager{T}"/> from a <see cref="IUnmangedHeap"/> instance /// of the specified number of elements /// </summary> /// <typeparam name="T">The unmanaged data type</typeparam> @@ -181,6 +156,7 @@ namespace VNLib.Utils.Extensions { return size >= 0 ? DirectAlloc<T>(heap, (nuint)size, zero) : throw new ArgumentOutOfRangeException(nameof(size), "The size paramter must be a positive integer"); } + /// <summary> /// Gets an offset pointer from the base postion to the number of bytes specified. Performs bounds checks /// </summary> @@ -194,6 +170,7 @@ namespace VNLib.Utils.Extensions { return elements >= 0 ? memory.GetOffset((nuint)elements) : throw new ArgumentOutOfRangeException(nameof(elements), "The elements paramter must be a positive integer"); } + /// <summary> /// Resizes the current handle on the heap /// </summary> @@ -204,7 +181,7 @@ namespace VNLib.Utils.Extensions /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Resize<T>(this MemoryHandle<T> memory, nint elements) where T : unmanaged + public static void Resize<T>(this IResizeableMemoryHandle<T> memory, nint elements) { if (elements < 0) { @@ -224,7 +201,7 @@ namespace VNLib.Utils.Extensions /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ResizeIfSmaller<T>(this MemoryHandle<T> handle, nint count) where T : unmanaged + public static void ResizeIfSmaller<T>(this IResizeableMemoryHandle<T> handle, nint count) { if(count < 0) { @@ -244,7 +221,7 @@ namespace VNLib.Utils.Extensions /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ResizeIfSmaller<T>(this MemoryHandle<T> handle, nuint count) where T : unmanaged + public static void ResizeIfSmaller<T>(this IResizeableMemoryHandle<T> handle, nuint count) { //Check handle size if(handle.Length < count) @@ -254,6 +231,52 @@ namespace VNLib.Utils.Extensions } } + /// <summary> + /// Gets a reference to the element at the specified offset from the base + /// address of the <see cref="MemoryHandle{T}"/> + /// </summary> + /// <param name="block"></param> + /// <param name="offset">The element offset from the base address to add to the returned reference</param> + /// <returns>The reference to the item at the desired offset</returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetOffsetRef<T>(this IMemoryHandle<T> block, nuint offset) + { + _ = block ?? throw new ArgumentNullException(nameof(block)); + + if (offset >= block.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + return ref Unsafe.Add(ref block.GetReference(), offset); + } + + /// <summary> + /// Gets a reference to the element at the specified offset from the base + /// address of the <see cref="MemoryHandle{T}"/> and casts it to a byte reference + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="block"></param> + /// <param name="offset">The number of elements to offset the base reference by</param> + /// <returns>The reinterpreted byte reference at the first byte of the element offset</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref byte GetByteOffsetRef<T>(this IMemoryHandle<T> block, nuint offset) + { + _ = block ?? throw new ArgumentNullException(nameof(block)); + + if (offset >= block.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + //Get the base reference, then offset by the desired number of elements and cast to a byte reference + ref T baseRef = ref block.GetReference(); + ref T offsetRef = ref Unsafe.Add(ref baseRef, offset); + return ref Unsafe.As<T, byte>(ref offsetRef); + } /// <summary> /// Gets a 64bit friendly span offset for the current <see cref="MemoryHandle{T}"/> @@ -265,7 +288,7 @@ namespace VNLib.Utils.Extensions /// <returns>The offset span</returns> /// <exception cref="ArgumentOutOfRangeException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe Span<T> GetOffsetSpan<T>(this MemoryHandle<T> block, nuint offset, int size) where T: unmanaged + public static Span<T> GetOffsetSpan<T>(this IMemoryHandle<T> block, nuint offset, int size) { _ = block ?? throw new ArgumentNullException(nameof(block)); @@ -282,9 +305,10 @@ namespace VNLib.Utils.Extensions MemoryUtil.CheckBounds(block, offset, (nuint)size); //Get long offset from the destination handle - void* ofPtr = block.GetOffset(offset); - return new Span<T>(ofPtr, size); + ref T ofPtr = ref GetOffsetRef(block, offset); + return MemoryMarshal.CreateSpan(ref ofPtr, size); } + /// <summary> /// Gets a 64bit friendly span offset for the current <see cref="MemoryHandle{T}"/> /// </summary> @@ -295,7 +319,7 @@ namespace VNLib.Utils.Extensions /// <returns>The offset span</returns> /// <exception cref="ArgumentOutOfRangeException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe Span<T> GetOffsetSpan<T>(this MemoryHandle<T> block, nint offset, int size) where T : unmanaged + public static unsafe Span<T> GetOffsetSpan<T>(this IMemoryHandle<T> block, nint offset, int size) { return offset >= 0 ? block.GetOffsetSpan((nuint)offset, size) : throw new ArgumentOutOfRangeException(nameof(offset)); } @@ -310,7 +334,7 @@ namespace VNLib.Utils.Extensions /// <returns>The new <see cref="SubSequence{T}"/> within the block</returns> /// <exception cref="ArgumentOutOfRangeException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static SubSequence<T> GetSubSequence<T>(this MemoryHandle<T> block, nuint offset, int size) where T : unmanaged => new (block, offset, size); + public static SubSequence<T> GetSubSequence<T>(this IMemoryHandle<T> block, nuint offset, int size) => new (block, offset, size); /// <summary> /// Gets a <see cref="SubSequence{T}"/> window within the current block @@ -322,7 +346,7 @@ namespace VNLib.Utils.Extensions /// <returns>The new <see cref="SubSequence{T}"/> within the block</returns> /// <exception cref="ArgumentOutOfRangeException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static SubSequence<T> GetSubSequence<T>(this MemoryHandle<T> block, nint offset, int size) where T : unmanaged + public static SubSequence<T> GetSubSequence<T>(this IMemoryHandle<T> block, nint offset, int size) { return offset >= 0 ? new (block, (nuint)offset, size) : throw new ArgumentOutOfRangeException(nameof(offset)); } @@ -334,10 +358,7 @@ namespace VNLib.Utils.Extensions /// <typeparam name="T">The unmanged data type to provide allocations from</typeparam> /// <returns>The new <see cref="MemoryPool{T}"/> heap wrapper.</returns> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MemoryPool<T> ToPool<T>(this IUnmangedHeap heap, int maxBufferSize = int.MaxValue) where T : unmanaged - { - return new PrivateBuffersMemoryPool<T>(heap, maxBufferSize); - } + public static MemoryPool<T> ToPool<T>(this IUnmangedHeap heap, int maxBufferSize = int.MaxValue) where T : unmanaged => new PrivateBuffersMemoryPool<T>(heap, maxBufferSize); /// <summary> /// Allocates a structure of the specified type on the current unmanged heap and zero's its memory @@ -349,14 +370,8 @@ namespace VNLib.Utils.Extensions /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="ObjectDisposedException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe T* StructAlloc<T>(this IUnmangedHeap heap) where T : unmanaged - { - //Allocate the struct on the heap and zero memory it points to - IntPtr handle = heap.Alloc(1, (nuint)sizeof(T), true); - //returns the handle - return (T*)handle; - } - + public static unsafe T* StructAlloc<T>(this IUnmangedHeap heap) where T : unmanaged => (T*)heap.Alloc(1, (nuint)sizeof(T), true); + /// <summary> /// Frees a structure at the specified address from the this heap. /// This must be the same heap the structure was allocated from @@ -468,7 +483,7 @@ namespace VNLib.Utils.Extensions /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="ObjectDisposedException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteAndResize<T>(this MemoryHandle<T> handle, ReadOnlySpan<T> input) where T: unmanaged + public static void WriteAndResize<T>(this IResizeableMemoryHandle<T> handle, ReadOnlySpan<T> input) where T: unmanaged { handle.Resize(input.Length); MemoryUtil.Copy(input, handle, 0); @@ -618,6 +633,7 @@ namespace VNLib.Utils.Extensions { return GetBytes(enc, chars.AsSpan(offset, charCount), ref writer, flush); } + /// <summary> /// Encodes a set of characters in the input characters span and any characters /// in the internal buffer into a sequence of bytes that are stored in the input @@ -639,6 +655,7 @@ namespace VNLib.Utils.Extensions writer.Advance(written); return written; } + /// <summary> /// Encodes a set of characters in the input characters span and any characters /// in the internal buffer into a sequence of bytes that are stored in the input @@ -657,6 +674,7 @@ namespace VNLib.Utils.Extensions writer.Advance(written); return written; } + /// <summary> /// Decodes a character buffer in the input characters span and any characters /// in the internal buffer into a sequence of bytes that are stored in the input @@ -683,6 +701,7 @@ namespace VNLib.Utils.Extensions /// <returns>A <see cref="PrivateString"/> instance that owns the underlying string memory</returns> [MethodImpl(MethodImplOptions.AggressiveInlining)] public static PrivateString ToPrivate(this ref ForwardOnlyWriter<char> buffer) => new(buffer.ToString(), true); + /// <summary> /// Gets a <see cref="Span{T}"/> over the modified section of the internal buffer /// </summary> @@ -711,6 +730,7 @@ namespace VNLib.Utils.Extensions Range sliceRange = new(start, arr.Length - start); return RuntimeHelpers.GetSubArray(arr, sliceRange); } + /// <summary> /// Slices the current array by the specified starting offset to including the /// speciifed number of items diff --git a/lib/Utils/src/IO/VnMemoryStream.cs b/lib/Utils/src/IO/VnMemoryStream.cs index 97cef03..7ac56a6 100644 --- a/lib/Utils/src/IO/VnMemoryStream.cs +++ b/lib/Utils/src/IO/VnMemoryStream.cs @@ -24,7 +24,9 @@ using System; using System.IO; +using System.Buffers; using System.Threading; +using System.Diagnostics; using System.Threading.Tasks; using System.Runtime.InteropServices; @@ -33,6 +35,7 @@ using VNLib.Utils.Extensions; namespace VNLib.Utils.IO { + /// <summary> /// Provides an unmanaged memory stream. Desigend to help reduce garbage collector load for /// high frequency memory operations. Similar to <see cref="UnmanagedMemoryStream"/> @@ -44,35 +47,39 @@ namespace VNLib.Utils.IO private bool _isReadonly; //Memory - private readonly MemoryHandle<byte> _buffer; + private readonly IResizeableMemoryHandle<byte> _buffer; //Default owns handle private readonly bool OwnsHandle = true; /// <summary> /// Creates a new <see cref="VnMemoryStream"/> pointing to the begining of memory, and consumes the handle. /// </summary> - /// <param name="handle"><see cref="MemoryHandle{T}"/> to consume</param> + /// <param name="handle"><see cref="IResizeableMemoryHandle{T}"/> to consume</param> /// <param name="length">Length of the stream</param> /// <param name="readOnly">Should the stream be readonly?</param> /// <exception cref="ArgumentException"></exception> /// <returns>A <see cref="VnMemoryStream"/> wrapper to access the handle data</returns> - public static VnMemoryStream ConsumeHandle(MemoryHandle<byte> handle, nint length, bool readOnly) => FromHandle(handle, true, length, readOnly); + public static VnMemoryStream ConsumeHandle(IResizeableMemoryHandle<byte> handle, nint length, bool readOnly) => FromHandle(handle, true, length, readOnly); /// <summary> /// Creates a new <see cref="VnMemoryStream"/> from the supplied memory handle /// of the initial length. This function also accepts a value that indicates if this stream /// owns the memory handle, which will cause it to be disposed when the stream is disposed. /// </summary> - /// <param name="handle"><see cref="MemoryHandle{T}"/> to consume</param> + /// <param name="handle"><see cref="IResizeableMemoryHandle{T}"/> to consume</param> /// <param name="length">The initial length of the stream</param> /// <param name="readOnly">Should the stream be readonly?</param> /// <param name="ownsHandle">A value that indicates if the current stream owns the memory handle</param> /// <exception cref="ArgumentException"></exception> /// <returns>A <see cref="VnMemoryStream"/> wrapper to access the handle data</returns> - public static VnMemoryStream FromHandle(MemoryHandle<byte> handle, bool ownsHandle, nint length, bool readOnly) + public static VnMemoryStream FromHandle(IResizeableMemoryHandle<byte> handle, bool ownsHandle, nint length, bool readOnly) { - handle.ThrowIfClosed(); - return new VnMemoryStream(handle, length, readOnly, ownsHandle); + //Check the handle + _ = handle ?? throw new ArgumentNullException(nameof(handle)); + + return handle.CanRealloc || readOnly + ? new VnMemoryStream(handle, length, readOnly, ownsHandle) + : throw new ArgumentException("The supplied memory handle must be resizable on a writable stream", nameof(handle)); } /// <summary> @@ -155,8 +162,11 @@ namespace VNLib.Utils.IO /// <param name="length">The length property of the stream</param> /// <param name="readOnly">Is the stream readonly (should mostly be true!)</param> /// <param name="ownsHandle">Does the new stream own the memory -> <paramref name="buffer"/></param> - private VnMemoryStream(MemoryHandle<byte> buffer, nint length, bool readOnly, bool ownsHandle) + private VnMemoryStream(IResizeableMemoryHandle<byte> buffer, nint length, bool readOnly, bool ownsHandle) { + Debug.Assert(length >= 0, "Length must be positive"); + Debug.Assert(buffer.CanRealloc || readOnly, "The supplied buffer is not resizable on a writable stream"); + OwnsHandle = ownsHandle; _buffer = buffer; //Consume the handle _length = length; //Store length of the buffer @@ -200,8 +210,8 @@ namespace VNLib.Utils.IO { throw new IOException("The destinaion stream is not writeable"); } - - do + + while (LenToPosDiff > 0) { //Calc the remaining bytes to read no larger than the buffer size int bytesToRead = (int)Math.Min(LenToPosDiff, bufferSize); @@ -213,8 +223,7 @@ namespace VNLib.Utils.IO //Update position _position += bytesToRead; - - } while (LenToPosDiff > 0); + } } /// <summary> @@ -231,37 +240,47 @@ namespace VNLib.Utils.IO public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { _ = destination ?? throw new ArgumentNullException(nameof(destination)); - + + if (bufferSize < 1) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize), "Buffer size must be greater than 0"); + } + if (!destination.CanWrite) { throw new IOException("The destinaion stream is not writeable"); } - cancellationToken.ThrowIfCancellationRequested(); - - /* - * Alloc temp copy buffer. This is a requirement because - * the stream may be larger than an int32 so it must be - * copied by segment - */ + cancellationToken.ThrowIfCancellationRequested(); - using VnTempBuffer<byte> copyBuffer = new(bufferSize); - - do + if(_length < Int32.MaxValue) { - //read from internal stream - int read = Read(copyBuffer); + //Safe to alloc a memory manager to do copy + using MemoryManager<byte> asMemManager = _buffer.ToMemoryManager(false); + + /* + * CopyTo starts at the current position, as if calling Read() + * so the reader must be offset to match and the _length gives us the + * actual length of the stream and therefor the segment size + */ - if(read <= 0) + while(LenToPosDiff > 0) { - break; - } + int blockSize = Math.Min((int)LenToPosDiff, bufferSize); + Memory<byte> window = asMemManager.Memory.Slice((int)_position, blockSize); - //write async - await destination.WriteAsync(copyBuffer.AsMemory(0, read), cancellationToken); + //write async + await destination.WriteAsync(window, cancellationToken); - } while (true); - + //Update position + _position+= bufferSize; + } + } + else + { + //TODO support 64bit memory stream copy + throw new NotSupportedException("64bit async copies are currently not supported"); + } } /// <summary> @@ -349,13 +368,13 @@ namespace VNLib.Utils.IO } //get the value at the current position - byte* ptr = _buffer.GetOffset(_position); + ref byte ptr = ref _buffer.GetByteOffsetRef((nuint)_position); //Increment position _position++; //Return value - return *ptr; + return ptr; } /* diff --git a/lib/Utils/src/Memory/VnTempBuffer.cs b/lib/Utils/src/Memory/ArrayPoolBuffer.cs index 5f5f831..92a2022 100644 --- a/lib/Utils/src/Memory/VnTempBuffer.cs +++ b/lib/Utils/src/Memory/ArrayPoolBuffer.cs @@ -24,6 +24,8 @@ using System; using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using VNLib.Utils.Extensions; @@ -33,7 +35,7 @@ namespace VNLib.Utils.Memory /// A disposable temporary buffer from shared ArrayPool /// </summary> /// <typeparam name="T">Type of buffer to create</typeparam> - public sealed class VnTempBuffer<T> : VnDisposeable, IIndexable<int, T>, IMemoryHandle<T>, IMemoryOwner<T> + public sealed class ArrayPoolBuffer<T> : VnDisposeable, IIndexable<int, T>, IMemoryHandle<T>, IMemoryOwner<T> { private readonly ArrayPool<T> Pool; @@ -64,25 +66,29 @@ namespace VNLib.Utils.Memory } ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T GetReference() => ref MemoryMarshal.GetArrayDataReference(Buffer); + + ///<inheritdoc/> Memory<T> IMemoryOwner<T>.Memory => AsMemory(); /// <summary> - /// Allocates a new <see cref="VnTempBuffer{BufType}"/> with a new buffer from shared array-pool + /// Allocates a new <see cref="ArrayPoolBuffer{BufType}"/> with a new buffer from shared array-pool /// </summary> /// <param name="minSize">Minimum size of the buffer</param> /// <param name="zero">Set the zero memory flag on close</param> - public VnTempBuffer(int minSize, bool zero = false) :this(ArrayPool<T>.Shared, minSize, zero) - {} + public ArrayPoolBuffer(int minSize, bool zero = false) :this(ArrayPool<T>.Shared, minSize, zero) + { } /// <summary> - /// Allocates a new <see cref="VnTempBuffer{BufType}"/> with a new buffer from specified array-pool + /// Allocates a new <see cref="ArrayPoolBuffer{BufType}"/> with a new buffer from specified array-pool /// </summary> /// <param name="pool">The <see cref="ArrayPool{T}"/> to allocate from and return to</param> /// <param name="minSize">Minimum size of the buffer</param> /// <param name="zero">Set the zero memory flag on close</param> - public VnTempBuffer(ArrayPool<T> pool, int minSize, bool zero = false) + public ArrayPoolBuffer(ArrayPool<T> pool, int minSize, bool zero = false) { - Pool = pool; + Pool = pool ?? throw new ArgumentNullException(nameof(pool)); Buffer = pool.Rent(minSize, zero); InitSize = minSize; } @@ -126,44 +132,64 @@ namespace VNLib.Utils.Memory Check(); return new Memory<T>(Buffer, 0, InitSize); } - + /// <summary> /// Gets a memory structure around the internal buffer /// </summary> /// <param name="count">The number of elements included in the result</param> - /// <param name="start">A value specifying the begining index of the buffer to include</param> /// <returns>A memory structure over the buffer</returns> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> - public Memory<T> AsMemory(int start, int count) - { - Check(); - return new Memory<T>(Buffer, start, count); - } + public Memory<T> AsMemory(int count) => AsMemory()[..count]; /// <summary> /// Gets a memory structure around the internal buffer /// </summary> /// <param name="count">The number of elements included in the result</param> + /// <param name="start">A value specifying the begining index of the buffer to include</param> /// <returns>A memory structure over the buffer</returns> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> - public Memory<T> AsMemory(int count) + public Memory<T> AsMemory(int start, int count) => AsMemory().Slice(start, count); + + /// <summary> + /// Gets an array segment around the internal buffer + /// </summary> + /// <returns>The internal array segment</returns> + /// <exception cref="ObjectDisposedException"></exception> + public ArraySegment<T> AsArraySegment() { Check(); - return new Memory<T>(Buffer, 0, count); + return new ArraySegment<T>(Buffer, 0, InitSize); } - - /* - * Allow implict casts to span/arrayseg/memory - */ - public static implicit operator Memory<T>(VnTempBuffer<T> buf) => buf == null ? Memory<T>.Empty : buf.ToMemory(); - public static implicit operator Span<T>(VnTempBuffer<T> buf) => buf == null ? Span<T>.Empty : buf.ToSpan(); - public static implicit operator ArraySegment<T>(VnTempBuffer<T> buf) => buf == null ? ArraySegment<T>.Empty : buf.ToArraySegment(); - public Memory<T> ToMemory() => Disposed ? Memory<T>.Empty : Buffer.AsMemory(0, InitSize); - public Span<T> ToSpan() => Disposed ? Span<T>.Empty : Buffer.AsSpan(0, InitSize); - public ArraySegment<T> ToArraySegment() => Disposed ? ArraySegment<T>.Empty : new(Buffer, 0, InitSize); + /// <summary> + /// Gets an array segment around the internal buffer + /// </summary> + /// <returns>The internal array segment</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public ArraySegment<T> AsArraySegment(int start, int count) + { + if(start< 0 || count < 0) + { + throw new ArgumentOutOfRangeException(start < 0 ? nameof(start) : nameof(count), "Cannot be less than zero"); + } + + MemoryUtil.CheckBounds(Buffer, (uint)start, (uint)count); + + Check(); + return new ArraySegment<T>(Buffer, start, count); + } + + //Pin, will also check bounds + ///<inheritdoc/> + public MemoryHandle Pin(int elementIndex) => MemoryUtil.PinArrayAndGetHandle(Buffer, elementIndex); + + void IPinnable.Unpin() + { + //Gchandle will manage the unpin + } /// <summary> /// Returns buffer to shared array-pool @@ -179,16 +205,7 @@ namespace VNLib.Utils.Memory #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - //Pin, will also check bounds - ///<inheritdoc/> - public MemoryHandle Pin(int elementIndex) => MemoryUtil.PinArrayAndGetHandle(Buffer, elementIndex); - - void IPinnable.Unpin() - { - //Gchandle will manage the unpin - } - ///<inheritdoc/> - ~VnTempBuffer() => Free(); + ~ArrayPoolBuffer() => Free(); } }
\ No newline at end of file diff --git a/lib/Utils/src/Memory/Caching/IReusable.cs b/lib/Utils/src/Memory/Caching/IReusable.cs index 618878f..4472ad3 100644 --- a/lib/Utils/src/Memory/Caching/IReusable.cs +++ b/lib/Utils/src/Memory/Caching/IReusable.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Utils @@ -25,14 +25,20 @@ namespace VNLib.Utils.Memory.Caching { /// <summary> - /// Allows for use within a <see cref="ReusableStore{T}"/>, this object is intended to be reused heavily + /// Allows for use within a <see cref="ObjectRental{T}"/>, this object is intended to be reused heavily /// </summary> public interface IReusable { /// <summary> /// The instance should prepare itself for use (or re-use) + /// <para> + /// This method is guarunteed to be called directly after a constructor + /// when a new instance is allocated and before it is ever returned to a + /// caller. + /// </para> /// </summary> void Prepare(); + /// <summary> /// The intance is being returned and should determine if it's state is reusabled /// </summary> diff --git a/lib/Utils/src/Memory/HeapCreation.cs b/lib/Utils/src/Memory/HeapCreation.cs index 9ef9fdb..835226c 100644 --- a/lib/Utils/src/Memory/HeapCreation.cs +++ b/lib/Utils/src/Memory/HeapCreation.cs @@ -49,6 +49,10 @@ namespace VNLib.Utils.Memory /// <summary> /// Specifies that the requested heap will be a shared heap for the process/library /// </summary> - Shared = 0x04 + Shared = 0x04, + /// <summary> + /// Specifies that the heap will support block reallocation + /// </summary> + SupportsRealloc = 0x08, } }
\ No newline at end of file diff --git a/lib/Utils/src/Memory/IMemoryHandle.cs b/lib/Utils/src/Memory/IMemoryHandle.cs index cf19ce9..f4e1a36 100644 --- a/lib/Utils/src/Memory/IMemoryHandle.cs +++ b/lib/Utils/src/Memory/IMemoryHandle.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Utils @@ -42,6 +42,12 @@ namespace VNLib.Utils.Memory /// Gets the internal block as a span /// </summary> Span<T> Span { get; } + + /// <summary> + /// Gets a reference to the first element in the block + /// </summary> + /// <returns>The reference</returns> + ref T GetReference(); } } diff --git a/lib/Utils/src/Memory/IResizeableMemoryHandle.cs b/lib/Utils/src/Memory/IResizeableMemoryHandle.cs new file mode 100644 index 0000000..f788b48 --- /dev/null +++ b/lib/Utils/src/Memory/IResizeableMemoryHandle.cs @@ -0,0 +1,52 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IResizeableMemoryHandle.cs +* +* IResizeableMemoryHandle.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Utils.Memory +{ + + /// <summary> + /// Represents a memory handle that can be resized in place. + /// </summary> + /// <typeparam name="T">The data type</typeparam> + public interface IResizeableMemoryHandle<T> : IMemoryHandle<T> + { + /// <summary> + /// Gets a value indicating whether the handle supports resizing in place + /// </summary> + bool CanRealloc { get; } + + /// <summary> + /// Resizes a memory handle to a new number of elements. + /// </summary> + /// <remarks> + /// Even if a handle is resizable resizing may not be supported for all types of handles. + /// </remarks> + /// <param name="elements">The new number of elements to resize the handle to</param> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="NotSupportedException"></exception> + void Resize(nuint elements); + } +}
\ No newline at end of file diff --git a/lib/Utils/src/Memory/MemoryHandle.cs b/lib/Utils/src/Memory/MemoryHandle.cs index 30d2b99..4d2ff0c 100644 --- a/lib/Utils/src/Memory/MemoryHandle.cs +++ b/lib/Utils/src/Memory/MemoryHandle.cs @@ -40,8 +40,16 @@ namespace VNLib.Utils.Memory /// Handles are configured to address blocks larger than 2GB, /// so some properties may raise exceptions if large blocks are used. /// </remarks> - public sealed class MemoryHandle<T> : SafeHandleZeroOrMinusOneIsInvalid, IMemoryHandle<T>, IEquatable<MemoryHandle<T>> where T : unmanaged + public sealed class MemoryHandle<T> : + SafeHandleZeroOrMinusOneIsInvalid, + IResizeableMemoryHandle<T>, + IMemoryHandle<T>, + IEquatable<MemoryHandle<T>> + where T : unmanaged { + private readonly bool ZeroMemory; + private readonly IUnmangedHeap Heap; + private nuint _length; /// <summary> /// New <typeparamref name="T"/>* pointing to the base of the allocated block @@ -79,15 +87,11 @@ namespace VNLib.Utils.Memory } } - private readonly bool ZeroMemory; - private readonly IUnmangedHeap Heap; - private nuint _length; - ///<inheritdoc/> - public nuint Length + public nuint Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _length; + get => _length; } /// <summary> @@ -101,6 +105,13 @@ namespace VNLib.Utils.Memory get => MemoryUtil.ByteCount<T>(_length); } + ///<inheritdoc/> + public bool CanRealloc + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Heap != null && Heap.CreationFlags.HasFlag(HeapCreation.SupportsRealloc); + } + /// <summary> /// Creates a new memory handle, for which is holds ownership, and allocates the number of elements specified on the heap. /// </summary> @@ -128,7 +139,7 @@ namespace VNLib.Utils.Memory /// when accessed, however <see cref="IMemoryHandle{T}"/> operations are /// considered "safe" meaning they should never raise excpetions /// </summary> - public MemoryHandle():base(false) + public MemoryHandle() : base(false) { _length = 0; Heap = null!; @@ -151,7 +162,7 @@ namespace VNLib.Utils.Memory * If resize raises an exception the current block pointer * should still be valid, if its not, the pointer should * be set to 0/-1, which will be considered invalid anyway - */ + */ Heap.Resize(ref handle, elements, (nuint)sizeof(T), ZeroMemory); @@ -161,14 +172,14 @@ namespace VNLib.Utils.Memory //Catch the disposed exception so we can invalidate the current ptr catch (ObjectDisposedException) { - base.handle = IntPtr.Zero; + SetHandle(IntPtr.Zero); //Set as invalid so release does not get called - base.SetHandleAsInvalid(); + SetHandleAsInvalid(); //Propagate the exception throw; } } - + /// <summary> /// Gets an offset pointer from the base postion to the number of bytes specified. Performs bounds checks /// </summary> @@ -190,7 +201,14 @@ namespace VNLib.Utils.Memory T* bs = ((T*)handle) + elements; return bs; } - + + ///<inheritdoc/> + public ref T GetReference() + { + this.ThrowIfClosed(); + return ref MemoryUtil.GetRef<T>(handle); + } + ///<inheritdoc/> ///<exception cref="ObjectDisposedException"></exception> ///<exception cref="ArgumentOutOfRangeException"></exception> @@ -200,14 +218,14 @@ namespace VNLib.Utils.Memory ///</remarks> public unsafe MemoryHandle Pin(int elementIndex) { - if(elementIndex < 0) + if (elementIndex < 0) { throw new ArgumentOutOfRangeException(nameof(elementIndex)); } //Get ptr and guard checks before adding the referrence T* ptr = GetOffset((nuint)elementIndex); - + bool addRef = false; //use the pinned field as success val DangerousAddRef(ref addRef); @@ -216,7 +234,7 @@ namespace VNLib.Utils.Memory ? throw new ObjectDisposedException("Failed to increase referrence count on the memory handle because it was released") : new MemoryHandle(ptr, pinnable: this); } - + ///<inheritdoc/> ///<exception cref="ObjectDisposedException"></exception> public void Unpin() @@ -226,11 +244,7 @@ namespace VNLib.Utils.Memory } ///<inheritdoc/> - protected override bool ReleaseHandle() - { - //Return result of free, only if the handle is valid - return Heap.Free(ref handle); - } + protected override bool ReleaseHandle() => Heap.Free(ref handle); /// <summary> /// Determines if the memory blocks are equal by comparing their base addresses. @@ -243,14 +257,13 @@ namespace VNLib.Utils.Memory { return other != null && (IsClosed | other.IsClosed) == false && _length == other._length && handle == other.handle; } - + ///<inheritdoc/> public override bool Equals(object? obj) => obj is MemoryHandle<T> oHandle && Equals(oHandle); - + ///<inheritdoc/> public override int GetHashCode() => base.GetHashCode(); - ///<inheritdoc/> public static implicit operator Span<T>(MemoryHandle<T> handle) { diff --git a/lib/Utils/src/Memory/MemoryUtil.cs b/lib/Utils/src/Memory/MemoryUtil.cs index 8cc9736..0261bdf 100644 --- a/lib/Utils/src/Memory/MemoryUtil.cs +++ b/lib/Utils/src/Memory/MemoryUtil.cs @@ -31,7 +31,7 @@ using System.Globalization; using System.Runtime.InteropServices; using System.Runtime.CompilerServices; -using VNLib.Utils.Extensions; +using VNLib.Utils.Resources; using VNLib.Utils.Memory.Diagnostics; namespace VNLib.Utils.Memory @@ -113,7 +113,7 @@ namespace VNLib.Utils.Memory Trace.WriteLineIf(globalZero, "Shared heap global zero enabled"); Lazy<IUnmangedHeap> heap = new (() => InitHeapInternal(true, diagEnable, globalZero), LazyThreadSafetyMode.PublicationOnly); - + //Cleanup the heap on process exit AppDomain.CurrentDomain.DomainUnload += DomainUnloaded; @@ -165,7 +165,7 @@ namespace VNLib.Utils.Memory string? rawFlagsEnv = Environment.GetEnvironmentVariable(SHARED_HEAP_RAW_FLAGS); //Default flags - HeapCreation cFlags = HeapCreation.UseSynchronization; + HeapCreation cFlags = HeapCreation.UseSynchronization | HeapCreation.SupportsRealloc; /* * We need to set the shared flag and the synchronziation flag. @@ -235,7 +235,7 @@ namespace VNLib.Utils.Memory /// <typeparam name="T">Unmanged datatype</typeparam> /// <param name="block">Block of memory to be cleared</param> [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public static void UnsafeZeroMemory<T>(ReadOnlySpan<T> block) where T : unmanaged + public static void UnsafeZeroMemory<T>(ReadOnlySpan<T> block) where T : struct { if (block.IsEmpty) { @@ -244,11 +244,11 @@ namespace VNLib.Utils.Memory uint byteSize = ByteCount<T>((uint)block.Length); - fixed (void* ptr = &MemoryMarshal.GetReference(block)) - { - //Calls memset - Unsafe.InitBlock(ptr, 0, byteSize); - } + ref T r0 = ref MemoryMarshal.GetReference(block); + ref byte byteRef = ref Unsafe.As<T, byte>(ref r0); + + //Calls memset + Unsafe.InitBlock(ref byteRef, 0, byteSize); } /// <summary> @@ -257,7 +257,7 @@ namespace VNLib.Utils.Memory /// <typeparam name="T">Unmanged datatype</typeparam> /// <param name="block">Block of memory to be cleared</param> [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - public static void UnsafeZeroMemory<T>(ReadOnlyMemory<T> block) where T : unmanaged + public static void UnsafeZeroMemory<T>(ReadOnlyMemory<T> block) where T : struct { if (block.IsEmpty) { @@ -284,7 +284,7 @@ namespace VNLib.Utils.Memory /// <typeparam name="T">The unmanaged</typeparam> /// <param name="block">The block of memory to initialize</param> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InitializeBlock<T>(Span<T> block) where T : unmanaged => UnsafeZeroMemory<T>(block); + public static void InitializeBlock<T>(Span<T> block) where T : struct => UnsafeZeroMemory<T>(block); /// <summary> /// Initializes a block of memory with zeros @@ -292,7 +292,7 @@ namespace VNLib.Utils.Memory /// <typeparam name="T">The unmanaged</typeparam> /// <param name="block">The block of memory to initialize</param> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InitializeBlock<T>(Memory<T> block) where T : unmanaged => UnsafeZeroMemory<T>(block); + public static void InitializeBlock<T>(Memory<T> block) where T : struct => UnsafeZeroMemory<T>(block); /// <summary> /// Zeroes a block of memory of the given unmanaged type @@ -303,16 +303,13 @@ namespace VNLib.Utils.Memory [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static void InitializeBlock<T>(T* block, int itemCount) where T : unmanaged { - if (itemCount == 0) + if (itemCount <= 0 || block == null) { return; } - //Get the size of the structure - int size = sizeof(T); - //Zero block - Unsafe.InitBlock(block, 0, (uint)(size * itemCount)); + Unsafe.InitBlock(block, 0, ByteCount<T>((uint)itemCount)); } /// <summary> @@ -321,33 +318,24 @@ namespace VNLib.Utils.Memory /// <typeparam name="T">The unmanaged type to zero</typeparam> /// <param name="block">A pointer to the block of memory to zero</param> /// <param name="itemCount">The number of elements in the block to zero</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InitializeBlock<T>(IntPtr block, int itemCount) where T : unmanaged => InitializeBlock((T*)block, itemCount); /// <summary> /// Zeroes a block of memory pointing to the structure /// </summary> /// <typeparam name="T">The structure type</typeparam> - /// <param name="block">The pointer to the allocated structure</param> - public static void ZeroStruct<T>(IntPtr block) - { - //get thes size of the structure does not have to be primitive type - int size = Unsafe.SizeOf<T>(); - //Zero block - Unsafe.InitBlock(block.ToPointer(), 0, (uint)size); - } + /// <param name="structPtr">The pointer to the allocated structure</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroStruct<T>(void* structPtr) => Unsafe.InitBlock(structPtr, 0, (uint)Unsafe.SizeOf<T>()); /// <summary> /// Zeroes a block of memory pointing to the structure /// </summary> /// <typeparam name="T">The structure type</typeparam> - /// <param name="structPtr">The pointer to the allocated structure</param> - public static void ZeroStruct<T>(void* structPtr) - { - //get thes size of the structure - int size = Unsafe.SizeOf<T>(); - //Zero block - Unsafe.InitBlock(structPtr, 0, (uint)size); - } + /// <param name="block">The pointer to the allocated structure</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroStruct<T>(IntPtr block) => ZeroStruct<T>(block.ToPointer()); /// <summary> /// Zeroes a block of memory pointing to the structure @@ -355,12 +343,20 @@ namespace VNLib.Utils.Memory /// <typeparam name="T">The structure type</typeparam> /// <param name="structPtr">The pointer to the allocated structure</param> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ZeroStruct<T>(T* structPtr) where T : unmanaged => Unsafe.InitBlock(structPtr, 0, (uint)sizeof(T)); + public static void ZeroStruct<T>(T* structPtr) where T : unmanaged => ZeroStruct<T>((void*)structPtr); #endregion #region Copy + /* + * Dirty little trick to access internal Buffer.Memmove method for + * large references. May not always be supported, so optional safe + * guards are in place. + */ + private delegate void BigMemmove(ref byte dest, ref byte src, nuint len); + private static readonly BigMemmove? _sysMemmove = ManagedLibrary.TryGetStaticMethod<BigMemmove>(typeof(Buffer), "Memmove", System.Reflection.BindingFlags.NonPublic); + /// <summary> /// Copies data from source memory to destination memory of an umanged data type /// </summary> @@ -369,7 +365,7 @@ namespace VNLib.Utils.Memory /// <param name="dest">Destination <see cref="MemoryHandle{T}"/></param> /// <param name="destOffset">Dest offset</param> /// <exception cref="ArgumentOutOfRangeException"></exception> - public static void Copy<T>(ReadOnlySpan<T> source, MemoryHandle<T> dest, nuint destOffset) where T : unmanaged + public static void Copy<T>(ReadOnlySpan<T> source, IMemoryHandle<T> dest, nuint destOffset) where T: struct { if (dest is null) { @@ -381,11 +377,17 @@ namespace VNLib.Utils.Memory return; } - //Get long offset from the destination handle (also checks bounds) - Span<T> dst = dest.GetOffsetSpan(destOffset, source.Length); + //Check memhandle bounds + CheckBounds(dest, destOffset, (uint)source.Length); - //Copy data - source.CopyTo(dst); + //Get byte ref and byte count + nuint byteCount = ByteCount<T>((uint)source.Length); + ref T src = ref MemoryMarshal.GetReference(source); + ref T dst = ref dest.GetReference(); + + //Use memmove by ref + bool success = MemmoveByRef(ref src, 0, ref dst, (uint)destOffset, byteCount); + Debug.Assert(success, "Memmove by ref call failed during a 32bit copy"); } /// <summary> @@ -396,24 +398,7 @@ namespace VNLib.Utils.Memory /// <param name="dest">Destination <see cref="MemoryHandle{T}"/></param> /// <param name="destOffset">Dest offset</param> /// <exception cref="ArgumentOutOfRangeException"></exception> - public static void Copy<T>(ReadOnlyMemory<T> source, MemoryHandle<T> dest, nuint destOffset) where T : unmanaged - { - if (dest is null) - { - throw new ArgumentNullException(nameof(dest)); - } - - if (source.IsEmpty) - { - return; - } - - //Get long offset from the destination handle (also checks bounds) - Span<T> dst = dest.GetOffsetSpan(destOffset, source.Length); - - //Copy data - source.Span.CopyTo(dst); - } + public static void Copy<T>(ReadOnlyMemory<T> source, IMemoryHandle<T> dest, nuint destOffset) where T : struct => Copy(source.Span, dest, destOffset); /// <summary> /// Copies data from source memory to destination memory of an umanged data type @@ -425,10 +410,12 @@ namespace VNLib.Utils.Memory /// <param name="destOffset">Dest offset</param> /// <param name="count">Number of elements to copy</param> /// <exception cref="ArgumentOutOfRangeException"></exception> - public static void Copy<T>(MemoryHandle<T> source, nint sourceOffset, Span<T> dest, int destOffset, int count) where T : unmanaged + public static void Copy<T>(IMemoryHandle<T> source, nint sourceOffset, Span<T> dest, int destOffset, int count) where T : struct { + _ = source ?? throw new ArgumentNullException(nameof(source)); + //Validate source/dest/count - ValidateArgs(sourceOffset, destOffset, count); + ValidateCopyArgs(sourceOffset, destOffset, count); //Check count last for debug reasons if (count == 0) @@ -436,14 +423,17 @@ namespace VNLib.Utils.Memory return; } - //Get offset span, also checks bounts - Span<T> src = source.GetOffsetSpan(sourceOffset, count); - - //slice the dest span - Span<T> dst = dest.Slice(destOffset, count); + //Check source bounds + CheckBounds(source, (nuint)sourceOffset, (nuint)count); - //Copy data - src.CopyTo(dst); + //Get byte ref and byte count + nuint byteCount = ByteCount<T>((uint)count); + ref T src = ref source.GetReference(); + ref T dst = ref MemoryMarshal.GetReference(dest); + + //Use memmove by ref + bool success = MemmoveByRef(ref src, (uint)sourceOffset, ref dst, (uint)destOffset, byteCount); + Debug.Assert(success, "Memmove by ref call failed during a 32bit copy"); } /// <summary> @@ -458,13 +448,51 @@ namespace VNLib.Utils.Memory /// <exception cref="ArgumentNullException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Copy<T>(MemoryHandle<T> source, nint sourceOffset, Memory<T> dest, int destOffset, int count) where T : unmanaged + public static void Copy<T>(IMemoryHandle<T> source, nint sourceOffset, Memory<T> dest, int destOffset, int count) where T : struct + => Copy(source, sourceOffset, dest.Span, destOffset, count); + + /// <summary> + /// Copies data from source memory to destination memory of an umanged data type + /// using references for blocks smaller than <see cref="UInt32.MaxValue"/> and + /// pinning for larger blocks + /// </summary> + /// <typeparam name="T">Unmanged type</typeparam> + /// <param name="source">Source data <see cref="MemoryHandle{T}"/></param> + /// <param name="sourceOffset">Number of elements to offset source data</param> + /// <param name="dest">Destination <see cref="Memory{T}"/></param> + /// <param name="destOffset">Dest offset</param> + /// <param name="count">Number of elements to copy</param> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public static void Copy<T>(IMemoryHandle<T> source, nuint sourceOffset, IMemoryHandle<T> dest, nuint destOffset, nuint count) where T : unmanaged { - //Call copy method with dest as span - Copy(source, sourceOffset, dest.Span, destOffset, count); + _ = source ?? throw new ArgumentNullException(nameof(source)); + _ = dest ?? throw new ArgumentNullException(nameof(dest)); + + CheckBounds(source, sourceOffset, count); + CheckBounds(dest, destOffset, count); + + //Get byte ref and byte count + nuint byteCount = ByteCount<T>(count); + ref T src = ref source.GetReference(); + ref T dst = ref dest.GetReference(); + + if (!MemmoveByRef(ref src, sourceOffset, ref dst, destOffset, byteCount)) + { + //Copying block larger than 32bit must be done with pointers + using MemoryHandle srcH = source.Pin(0); + using MemoryHandle dstH = dest.Pin(0); + + //Get pointers and add offsets + T* srcOffset = ((T*)srcH.Pointer) + sourceOffset; + T* dstOffset = ((T*)dstH.Pointer) + destOffset; + + //Copy memory + Buffer.MemoryCopy(srcOffset, dstOffset, byteCount, byteCount); + } } - private static void ValidateArgs(nint sourceOffset, nint destOffset, nint count) + private static void ValidateCopyArgs(nint sourceOffset, nint destOffset, nint count) { if(sourceOffset < 0) { @@ -483,17 +511,19 @@ namespace VNLib.Utils.Memory } /// <summary> - /// 32/64 bit large block copy + /// Preforms a fast referrence based copy on very large blocks of memory + /// using pinning and pointers only when the number of bytes to copy is + /// larger than <see cref="UInt32.MaxValue"/> /// </summary> /// <typeparam name="T"></typeparam> /// <param name="source">The source memory handle to copy data from</param> - /// <param name="offset">The element offset to begin reading from</param> + /// <param name="sourceOffset">The element offset to begin reading from</param> /// <param name="dest">The destination array to write data to</param> /// <param name="destOffset"></param> /// <param name="count">The number of elements to copy</param> /// <exception cref="ArgumentNullException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> - public static void Copy<T>(IMemoryHandle<T> source, nuint offset, T[] dest, nuint destOffset, nuint count) where T : unmanaged + public static void Copy<T>(IMemoryHandle<T> source, nuint sourceOffset, T[] dest, nuint destOffset, nuint count) where T : unmanaged { if (source is null) { @@ -511,41 +541,62 @@ namespace VNLib.Utils.Memory } //Check source bounds - CheckBounds(source, offset, count); + CheckBounds(source, sourceOffset, count); - //Check dest bounts + //Check dest bounds CheckBounds(dest, destOffset, count); - //Check if 64bit - if(sizeof(void*) == 8) + //Get byte refs and byte count + nuint byteCount = ByteCount<T>(count); + ref T src = ref source.GetReference(); + ref T dst = ref MemoryMarshal.GetArrayDataReference(dest); + + //Try to memove by ref first, otherwise fallback to pinning + if (!MemmoveByRef(ref src, sourceOffset, ref dst, destOffset, byteCount)) { - //Get the number of bytes to copy - nuint byteCount = ByteCount<T>(count); + //Copying block larger than 32bit must be done with pointers + using MemoryHandle srcH = source.Pin(0); + using MemoryHandle dstH = PinArrayAndGetHandle(dest, 0); - //Get memory handle from source - using MemoryHandle srcHandle = source.Pin(0); + //Get pointers and add offsets + T* srcOffset = ((T*)srcH.Pointer) + sourceOffset; + T* dstOffset = ((T*)dstH.Pointer) + destOffset; - //get source offset - T* src = (T*)srcHandle.Pointer + offset; + //Copy memory + Buffer.MemoryCopy(srcOffset, dstOffset, byteCount, byteCount); + } + } + - //pin array - fixed (T* dst = &MemoryMarshal.GetArrayDataReference(dest)) - { - //Offset dest ptr - T* dstOffset = dst + destOffset; + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool MemmoveByRef<T>(ref T src, nuint srcOffset, ref T dst, nuint dstOffset, nuint byteCount) where T : struct + { + Debug.Assert(!Unsafe.IsNullRef(ref src), "Null source reference passed to MemmoveByRef"); + Debug.Assert(!Unsafe.IsNullRef(ref dst), "Null destination reference passed to MemmoveByRef"); - //Copy src to set - Buffer.MemoryCopy(src, dstOffset, byteCount, byteCount); - } + //Get offset referrences to the source and destination + ref T srcOffsetPtr = ref Unsafe.Add(ref src, srcOffset); + ref T dstOffsetPtr = ref Unsafe.Add(ref dst, dstOffset); + + //Cast to byte pointers + ref byte srcByte = ref Unsafe.As<T, byte>(ref srcOffsetPtr); + ref byte dstByte = ref Unsafe.As<T, byte>(ref dstOffsetPtr); + + if (_sysMemmove != null) + { + //Call sysinternal memmove + _sysMemmove(ref dstByte, ref srcByte, byteCount); + return true; + } + else if(byteCount < uint.MaxValue) + { + //Use safe 32bit block copy + Unsafe.CopyBlock(ref dstByte, ref srcByte, (uint)byteCount); + return true; } else { - //If 32bit its safe to use spans - - Span<T> src = source.AsSpan((int)offset, (int)count); - Span<T> dst = dest.AsSpan((int)destOffset, (int)count); - //Copy - src.CopyTo(dst); + return false; } } @@ -598,6 +649,26 @@ namespace VNLib.Utils.Memory public static uint ByteCount<T>(uint elementCount) => checked(elementCount * (uint)Unsafe.SizeOf<T>()); /// <summary> + /// Gets the byte multiple of the length parameter. NOTE: Does not verify negative values + /// </summary> + /// <typeparam name="T">The type to get the byte offset of</typeparam> + /// <param name="elementCount">The number of elements to get the byte count of</param> + /// <returns>The byte multiple of the number of elments</returns> + /// <exception cref="OverflowException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint ByteCount<T>(nint elementCount) => checked(elementCount * Unsafe.SizeOf<T>()); + + /// <summary> + /// Gets the byte multiple of the length parameter. NOTE: Does not verify negative values + /// </summary> + /// <typeparam name="T">The type to get the byte offset of</typeparam> + /// <param name="elementCount">The number of elements to get the byte count of</param> + /// <returns>The byte multiple of the number of elments</returns> + /// <exception cref="OverflowException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ByteCount<T>(int elementCount) => checked(elementCount * Unsafe.SizeOf<T>()); + + /// <summary> /// Checks if the offset/count paramters for the given memory handle /// point outside the block wrapped in the handle /// </summary> @@ -682,21 +753,21 @@ namespace VNLib.Utils.Memory throw new ArgumentOutOfRangeException(nameof(elementOffset)); } + _ = array ?? throw new ArgumentNullException(nameof(array)); + //Quick verify index exists, may be the very last index CheckBounds(array, (nuint)elementOffset, 1); //Pin the array GCHandle arrHandle = GCHandle.Alloc(array, GCHandleType.Pinned); - //Get array base address - void* basePtr = (void*)arrHandle.AddrOfPinnedObject(); - - Debug.Assert(basePtr != null); + //safe to get array basee pointer + ref T arrBase = ref MemoryMarshal.GetArrayDataReference(array); //Get element offset - void* indexOffet = Unsafe.Add<T>(basePtr, elementOffset); + ref T indexOffet = ref Unsafe.Add(ref arrBase, elementOffset); - return new(indexOffet, arrHandle); + return new(Unsafe.AsPointer(ref indexOffet), arrHandle); } /// <summary> @@ -741,6 +812,29 @@ namespace VNLib.Utils.Memory public static Span<T> GetSpan<T>(MemoryHandle handle, int size) => new(handle.Pointer, size); /// <summary> + /// Recovers a reference to the supplied pointer + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="address">The base address to cast to a reference</param> + /// <returns>The reference to the supplied address</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetRef<T>(IntPtr address) => ref Unsafe.AsRef<T>(address.ToPointer()); + + /// <summary> + /// Recovers a reference to the supplied pointer + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="address">The base address to cast to a reference</param> + /// <param name="offset">The offset to add to the base address</param> + /// <returns>The reference to the supplied address</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetRef<T>(IntPtr address, nuint offset) + { + ref T baseRef = ref GetRef<T>(address); + return ref Unsafe.Add(ref baseRef, (nint)offset); + } + + /// <summary> /// Rounds the requested byte size up to the nearest page /// number of bytes /// </summary> diff --git a/lib/Utils/src/Memory/MemoryUtilAlloc.cs b/lib/Utils/src/Memory/MemoryUtilAlloc.cs index e4210e7..9e305d0 100644 --- a/lib/Utils/src/Memory/MemoryUtilAlloc.cs +++ b/lib/Utils/src/Memory/MemoryUtilAlloc.cs @@ -92,7 +92,7 @@ namespace VNLib.Utils.Memory } //Round to nearest page (in bytes) - nint np = NearestPage(elements * sizeof(T)); + nint np = NearestPage(ByteCount<T>(elements)); //Resize to element size np /= sizeof(T); @@ -131,7 +131,7 @@ namespace VNLib.Utils.Memory } else { - return new VnTempBuffer<T>(ArrayPool<T>.Shared, elements, zero); + return new ArrayPoolBuffer<T>(ArrayPool<T>.Shared, elements, zero); } } @@ -154,7 +154,7 @@ namespace VNLib.Utils.Memory } //Round to nearest page (in bytes) - nint np = NearestPage(elements * sizeof(T)); + nint np = NearestPage(ByteCount<T>(elements)); //Resize to element size np /= sizeof(T); @@ -258,7 +258,7 @@ namespace VNLib.Utils.Memory } else { - return new VnTempBuffer<byte>(ArrayPool<byte>.Shared, elements, zero); + return new ArrayPoolBuffer<byte>(ArrayPool<byte>.Shared, elements, zero); } } diff --git a/lib/Utils/src/Memory/PrivateBuffersMemoryPool.cs b/lib/Utils/src/Memory/PrivateBuffersMemoryPool.cs index f2bbd51..a17a906 100644 --- a/lib/Utils/src/Memory/PrivateBuffersMemoryPool.cs +++ b/lib/Utils/src/Memory/PrivateBuffersMemoryPool.cs @@ -25,6 +25,8 @@ using System; using System.Buffers; +using VNLib.Utils.Extensions; + namespace VNLib.Utils.Memory { /// <summary> @@ -48,7 +50,7 @@ namespace VNLib.Utils.Memory ///<exception cref="OutOfMemoryException"></exception> ///<exception cref="ObjectDisposedException"></exception> ///<exception cref="ArgumentOutOfRangeException"></exception> - public override IMemoryOwner<T> Rent(int minBufferSize = 0) => new SysBufferMemoryManager<T>(Heap, (uint)minBufferSize, false); + public override IMemoryOwner<T> Rent(int minBufferSize = 0) => Heap.DirectAlloc<T>(minBufferSize, false); /// <summary> /// Allocates a new <see cref="MemoryManager{T}"/> of a different data type from the pool @@ -56,7 +58,7 @@ namespace VNLib.Utils.Memory /// <typeparam name="TDifType">The unmanaged data type to allocate for</typeparam> /// <param name="minBufferSize">Minumum size of the buffer</param> /// <returns>The memory owner of a different data type</returns> - public IMemoryOwner<TDifType> Rent<TDifType>(int minBufferSize = 0) where TDifType : unmanaged => new SysBufferMemoryManager<TDifType>(Heap, (uint)minBufferSize, false); + public IMemoryOwner<TDifType> Rent<TDifType>(int minBufferSize = 0) where TDifType : unmanaged => Heap.DirectAlloc<TDifType>(minBufferSize, false); ///<inheritdoc/> protected override void Dispose(bool disposing) diff --git a/lib/Utils/src/Memory/ProcessHeap.cs b/lib/Utils/src/Memory/ProcessHeap.cs index 3d581cd..5d1bee6 100644 --- a/lib/Utils/src/Memory/ProcessHeap.cs +++ b/lib/Utils/src/Memory/ProcessHeap.cs @@ -49,7 +49,7 @@ namespace VNLib.Utils.Memory /// process heap. Meaining memory will be shared across the process /// </para> /// </summary> - public HeapCreation CreationFlags { get; } = HeapCreation.Shared; + public HeapCreation CreationFlags { get; } = HeapCreation.Shared | HeapCreation.SupportsRealloc; /// <summary> /// Initalizes a new global (cross platform) process heap diff --git a/lib/Utils/src/Memory/SubSequence.cs b/lib/Utils/src/Memory/SubSequence.cs index 1db0ba5..86b2347 100644 --- a/lib/Utils/src/Memory/SubSequence.cs +++ b/lib/Utils/src/Memory/SubSequence.cs @@ -32,14 +32,14 @@ namespace VNLib.Utils.Memory /// Represents a subset (or window) of data within a <see cref="MemoryHandle{T}"/> /// </summary> /// <typeparam name="T">The unmanaged type to wrap</typeparam> - public readonly record struct SubSequence<T> where T: unmanaged + public readonly record struct SubSequence<T> { readonly nuint _offset; /// <summary> /// The handle that owns the memory block /// </summary> - public readonly MemoryHandle<T> Handle { get; } + public readonly IMemoryHandle<T> Handle { get; } /// <summary> /// The number of elements in the current sequence @@ -54,7 +54,7 @@ namespace VNLib.Utils.Memory /// <param name="size"></param> /// <exception cref="ArgumentNullException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> - public SubSequence(MemoryHandle<T> block, nuint offset, int size) + public SubSequence(IMemoryHandle<T> block, nuint offset, int size) { Handle = block ?? throw new ArgumentNullException(nameof(block)); Size = size >= 0 ? size : throw new ArgumentOutOfRangeException(nameof(size)); diff --git a/lib/Utils/src/Memory/SysBufferMemoryManager.cs b/lib/Utils/src/Memory/SysBufferMemoryManager.cs index aca2543..26c3688 100644 --- a/lib/Utils/src/Memory/SysBufferMemoryManager.cs +++ b/lib/Utils/src/Memory/SysBufferMemoryManager.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Utils @@ -25,8 +25,6 @@ using System; using System.Buffers; -using VNLib.Utils.Extensions; - namespace VNLib.Utils.Memory { /// <summary> @@ -34,7 +32,7 @@ namespace VNLib.Utils.Memory /// as a memory provider which implements a <see cref="System.Runtime.InteropServices.SafeHandle"/> /// </summary> /// <typeparam name="T">Unmanaged memory type</typeparam> - public sealed class SysBufferMemoryManager<T> : MemoryManager<T> where T :unmanaged + public sealed class SysBufferMemoryManager<T> : MemoryManager<T> { private readonly IMemoryHandle<T> BackingMemory; private readonly bool _ownsHandle; @@ -45,30 +43,19 @@ namespace VNLib.Utils.Memory /// </summary> /// <param name="existingHandle">The existing handle to consume</param> /// <param name="ownsHandle">A value that indicates if the memory manager owns the handle reference</param> - internal SysBufferMemoryManager(IMemoryHandle<T> existingHandle, bool ownsHandle) + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="OverflowException"></exception> + public SysBufferMemoryManager(IMemoryHandle<T> existingHandle, bool ownsHandle) { - BackingMemory = existingHandle; - _ownsHandle = ownsHandle; - } - - /// <summary> - /// Allocates a fized size buffer from the specified unmanaged <see cref="Win32PrivateHeap"/> - /// </summary> - /// <param name="heap">The heap to perform allocations from</param> - /// <param name="elements">The number of elements to allocate</param> - /// <param name="zero">Zero allocations</param> - public SysBufferMemoryManager(IUnmangedHeap heap, nuint elements, bool zero) - { - BackingMemory = heap.Alloc<T>(elements, zero); - _ownsHandle = true; - } + BackingMemory = existingHandle ?? throw new ArgumentNullException(nameof(existingHandle)); + + //check for overflow + if(existingHandle.Length > Int32.MaxValue) + { + throw new OverflowException("This memory manager does not accept handles larger than Int32.MaxValue"); + } - ///<inheritdoc/> - protected override bool TryGetArray(out ArraySegment<T> segment) - { - //Always false since no array is available - segment = default; - return false; + _ownsHandle = ownsHandle; } ///<inheritdoc/> @@ -81,11 +68,8 @@ namespace VNLib.Utils.Memory /// </summary> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> - public unsafe override MemoryHandle Pin(int elementIndex = 0) - { - return BackingMemory.Pin(elementIndex); - } - + public unsafe override MemoryHandle Pin(int elementIndex = 0) => BackingMemory.Pin(elementIndex); + ///<inheritdoc/> public override void Unpin() {} diff --git a/lib/Utils/src/Memory/UnmanagedHeapBase.cs b/lib/Utils/src/Memory/UnmanagedHeapBase.cs index bfa6736..0310582 100644 --- a/lib/Utils/src/Memory/UnmanagedHeapBase.cs +++ b/lib/Utils/src/Memory/UnmanagedHeapBase.cs @@ -39,21 +39,12 @@ namespace VNLib.Utils.Memory /// </summary> public abstract class UnmanagedHeapBase : SafeHandleZeroOrMinusOneIsInvalid, IUnmangedHeap { + private readonly HeapCreation _flags; + /// <summary> /// The heap synchronization handle /// </summary> protected readonly object HeapLock; - - /// <summary> - /// The global heap zero flag - /// </summary> - protected readonly bool GlobalZero; - - /// <summary> - /// A value that inidicates that locking will - /// be used when invoking heap operations - /// </summary> - protected readonly bool UseSynchronization; /// <summary> /// Initalizes the unmanaged heap base class (init synchronization handle) @@ -63,13 +54,11 @@ namespace VNLib.Utils.Memory protected UnmanagedHeapBase(HeapCreation flags, bool ownsHandle) : base(ownsHandle) { HeapLock = new(); - GlobalZero = flags.HasFlag(HeapCreation.GlobalZero); - UseSynchronization = flags.HasFlag(HeapCreation.UseSynchronization); - CreationFlags = flags; + _flags = flags; } ///<inheritdoc/> - public HeapCreation CreationFlags { get; } + public HeapCreation CreationFlags => _flags; ///<inheritdoc/> ///<remarks>Increments the handle count, free must be called to decrement the handle count</remarks> @@ -82,7 +71,7 @@ namespace VNLib.Utils.Memory _ = checked(elements * size); //Force zero if global flag is set - zero |= GlobalZero; + zero |= (_flags & HeapCreation.GlobalZero) > 0; bool handleCountIncremented = false; //Increment handle count to prevent premature release @@ -99,7 +88,7 @@ namespace VNLib.Utils.Memory LPVOID block; //Check if lock should be used - if (UseSynchronization) + if ((_flags & HeapCreation.UseSynchronization) > 0) { //Enter lock lock(HeapLock) @@ -138,7 +127,7 @@ namespace VNLib.Utils.Memory return true; } - if (UseSynchronization) + if ((_flags & HeapCreation.UseSynchronization) > 0) { //wait for lock lock (HeapLock) @@ -173,7 +162,7 @@ namespace VNLib.Utils.Memory LPVOID newBlock; //Global zero flag will cause a zero - zero |= GlobalZero; + zero |= (_flags & HeapCreation.GlobalZero) > 0; /* * Realloc may return a null pointer if allocation fails @@ -182,7 +171,7 @@ namespace VNLib.Utils.Memory * be left untouched */ - if (UseSynchronization) + if ((_flags & HeapCreation.UseSynchronization) > 0) { lock (HeapLock) { diff --git a/lib/Utils/src/Memory/UnsafeMemoryHandle.cs b/lib/Utils/src/Memory/UnsafeMemoryHandle.cs index 6d566f1..e4857d1 100644 --- a/lib/Utils/src/Memory/UnsafeMemoryHandle.cs +++ b/lib/Utils/src/Memory/UnsafeMemoryHandle.cs @@ -165,8 +165,7 @@ namespace VNLib.Utils.Memory if (elementIndex < 0 || elementIndex >= IntLength) { throw new ArgumentOutOfRangeException(nameof(elementIndex)); - } - + } if (_handleType == HandleType.Pool) { @@ -174,10 +173,11 @@ namespace VNLib.Utils.Memory } else { - //Get offset pointer and pass self as pinnable argument, (nothing happens but support it) - void* basePtr = Unsafe.Add<T>(_memoryPtr.ToPointer(), elementIndex); + //Add an offset to the base address of the memory block + int byteOffset = MemoryUtil.ByteCount<T>(elementIndex); + IntPtr offset = IntPtr.Add(_memoryPtr, byteOffset); //Unmanaged memory is always pinned, so no need to pass this as IPinnable, since it will cause a box - return new (basePtr); + return MemoryUtil.GetMemoryHandleFromPointer(offset); } } ///<inheritdoc/> @@ -186,6 +186,20 @@ namespace VNLib.Utils.Memory //Nothing to do since gc handle takes care of array, and unmanaged pointers are not pinned } + ///<inheritdoc/> + public readonly ref T GetReference() + { + switch (_handleType) + { + case HandleType.Pool: + return ref MemoryMarshal.GetArrayDataReference(_poolArr!); + case HandleType.PrivateHeap: + return ref MemoryUtil.GetRef<T>(_memoryPtr); + default: + throw new InvalidOperationException("The handle is empty, and cannot capture a reference"); + } + } + /// <summary> /// Determines if the other handle represents the same memory block as the /// current handle. diff --git a/lib/Utils/src/Memory/VnString.cs b/lib/Utils/src/Memory/VnString.cs index 8542688..c937ccc 100644 --- a/lib/Utils/src/Memory/VnString.cs +++ b/lib/Utils/src/Memory/VnString.cs @@ -44,7 +44,7 @@ namespace VNLib.Utils.Memory [ImmutableObject(true)] public sealed class VnString : VnDisposeable, IEquatable<VnString>, IEquatable<string>, IEquatable<char[]>, IComparable<VnString>, IComparable<string> { - private readonly MemoryHandle<char>? Handle; + private readonly IMemoryHandle<char>? Handle; private readonly SubSequence<char> _stringSequence; @@ -63,7 +63,7 @@ namespace VNLib.Utils.Memory _stringSequence = sequence; } - private VnString(MemoryHandle<char> handle, nuint start, int length) + private VnString(IMemoryHandle<char> handle, nuint start, int length) { Handle = handle ?? throw new ArgumentNullException(nameof(handle)); //get sequence @@ -517,14 +517,10 @@ namespace VNLib.Utils.Memory /// </remarks> /// <exception cref="ObjectDisposedException"></exception> public int GetHashCode(StringComparison stringComparison) => string.GetHashCode(AsSpan(), stringComparison); - + ///<inheritdoc/> - protected override void Free() - { - //Dispose the handle if we own it (null if we do not have the parent handle) - Handle?.Dispose(); - } - + protected override void Free() => Handle?.Dispose(); + public static bool operator ==(VnString left, VnString right) => left is null ? right is not null : left.Equals(right, StringComparison.Ordinal); public static bool operator !=(VnString left, VnString right) => !(left == right); diff --git a/lib/Utils/src/Memory/Win32PrivateHeap.cs b/lib/Utils/src/Memory/Win32PrivateHeap.cs index 41fe33a..42f0328 100644 --- a/lib/Utils/src/Memory/Win32PrivateHeap.cs +++ b/lib/Utils/src/Memory/Win32PrivateHeap.cs @@ -177,8 +177,7 @@ namespace VNLib.Utils.Memory //validate the block on the current heap result = HeapValidate(handle, HEAP_NO_FLAGS, block); } - return result; - + return result; } /// <summary> diff --git a/lib/Utils/src/Resources/ManagedLibrary.cs b/lib/Utils/src/Resources/ManagedLibrary.cs index f9813a1..56835c7 100644 --- a/lib/Utils/src/Resources/ManagedLibrary.cs +++ b/lib/Utils/src/Resources/ManagedLibrary.cs @@ -28,6 +28,7 @@ using System.Linq; using System.Threading; using System.Reflection; using System.Runtime.Loader; +using System.Collections.Generic; using System.Runtime.InteropServices; using VNLib.Utils.IO; @@ -76,11 +77,8 @@ namespace VNLib.Utils.Resources _lazyAssembly = new(LoadAssembly, LazyThreadSafetyMode.PublicationOnly); } - private Assembly LoadAssembly() - { - //Load the assembly into the parent context - return _loadContext.LoadFromAssemblyPath(AssemblyPath); - } + //Load the assembly into the parent context + private Assembly LoadAssembly() => _loadContext.LoadFromAssemblyPath(AssemblyPath); /// <summary> /// Raised when the load context that owns this assembly @@ -136,18 +134,50 @@ namespace VNLib.Utils.Resources /// <exception cref="EntryPointNotFoundException"></exception> public T LoadTypeFromAssembly<T>() { - Type resourceType = typeof(T); - //See if the type is exported - Type exp = (from type in Assembly.GetExportedTypes() - where resourceType.IsAssignableFrom(type) - select type) - .FirstOrDefault() - ?? throw new EntryPointNotFoundException($"Imported assembly does not export desired type {resourceType.FullName}"); + Type exp = TryGetExportedType<T>() ?? throw new EntryPointNotFoundException($"Imported assembly does not export desired type {typeof(T).FullName}"); //Create instance return (T)Activator.CreateInstance(exp)!; - } + } + + /// <summary> + /// Gets the type exported from the current assembly that is + /// assignable to the desired type. + /// </summary> + /// <typeparam name="T">The desired base type to get the exported type of</typeparam> + /// <returns>The exported type that matches the desired type from the current assembly</returns> + public Type? TryGetExportedType<T>() => TryGetExportedType(typeof(T)); + + /// <summary> + /// Gets the type exported from the current assembly that is + /// assignable to the desired type. + /// </summary> + /// <param name="resourceType">The desired base type to get the exported type of</param> + /// <returns>The exported type that matches the desired type from the current assembly</returns> + public Type? TryGetExportedType(Type resourceType) => TryGetAllMatchingTypes(resourceType).FirstOrDefault(); + + /// <summary> + /// Gets all exported types from the current assembly that are + /// assignable to the desired type. + /// </summary> + /// <typeparam name="T">The desired resource type</typeparam> + /// <returns>An enumeration of acceptable types</returns> + public IEnumerable<Type> TryGetAllMatchingTypes<T>() => TryGetAllMatchingTypes(typeof(T)); + + /// <summary> + /// Gets all exported types from the current assembly that are + /// assignable to the desired type. + /// </summary> + /// <param name="resourceType">The desired resource type</param> + /// <returns>An enumeration of acceptable types</returns> + public IEnumerable<Type> TryGetAllMatchingTypes(Type resourceType) + { + //try to get all exported types that match the desired type + return from type in Assembly.GetExportedTypes() + where resourceType.IsAssignableFrom(type) + select type; + } /// <summary> /// Creates a new loader for the desired assembly. The assembly and its dependencies @@ -171,5 +201,87 @@ namespace VNLib.Utils.Resources FileInfo fi = new(assemblyName); return new(fi.FullName, loadContext); } + + /// <summary> + /// A helper method that will attempt to get a named method of the desired + /// delegate type from the specified object. + /// </summary> + /// <typeparam name="TDelegate">The method delegate that matches the signature of the desired method</typeparam> + /// <param name="obj">The object to discover and bind the found method to</param> + /// <param name="methodName">The name of the method to capture</param> + /// <param name="flags">The method binding flags</param> + /// <returns>The namaed method delegate for the object type, or null if the method was not found</returns> + /// <exception cref="ArgumentNullException"></exception> + public static TDelegate? TryGetMethod<TDelegate>( + object obj, + string methodName, + BindingFlags flags = BindingFlags.Public + ) where TDelegate : Delegate + { + _ = obj ?? throw new ArgumentNullException(nameof(obj)); + return TryGetMethodInternal<TDelegate>(obj.GetType(), methodName, obj, flags | BindingFlags.Instance); + } + + /// <summary> + /// A helper method that will attempt to get a named method of the desired + /// delegate type from the specified object. + /// </summary> + /// <typeparam name="TDelegate">The method delegate that matches the signature of the desired method</typeparam> + /// <param name="obj">The object to discover and bind the found method to</param> + /// <param name="methodName">The name of the method to capture</param> + /// <param name="flags">The method binding flags</param> + /// <returns>The namaed method delegate for the object type or an exception if not found</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="MissingMethodException"></exception> + public static TDelegate GetMethod<TDelegate>( + object obj, + string methodName, + BindingFlags flags = BindingFlags.Public + ) where TDelegate : Delegate + { + return TryGetMethod<TDelegate>(obj, methodName, flags) + ?? throw new MissingMethodException($"Type {obj.GetType().FullName} is missing desired method {methodName}"); + } + + /// <summary> + /// A helper method that will attempt to get a named static method of the desired + /// delegate type from the specified type. + /// </summary> + /// <typeparam name="TDelegate"></typeparam> + /// <param name="type">The type to get the static method for</param> + /// <param name="methodName">The name of the static method</param> + /// <param name="flags">The optional method binind flags</param> + /// <returns>The delegate if found <see langword="null"/> otherwise</returns> + /// <exception cref="ArgumentNullException"></exception> + public static TDelegate? TryGetStaticMethod<TDelegate>(Type type, string methodName, BindingFlags flags = BindingFlags.Public) where TDelegate : Delegate + => TryGetMethodInternal<TDelegate>(type, methodName, null, flags | BindingFlags.Static); + + /// <summary> + /// A helper method that will attempt to get a named static method of the desired + /// delegate type from the specified type. + /// </summary> + /// <typeparam name="TDelegate">The delegate method type</typeparam> + /// <typeparam name="TType">The type to get the static method for</typeparam> + /// <param name="methodName">The name of the static method</param> + /// <param name="flags">The optional method binind flags</param> + /// <returns>The delegate if found <see langword="null"/> otherwise</returns> + /// <exception cref="ArgumentNullException"></exception> + public static TDelegate? TryGetStaticMethod<TDelegate, TType>(string methodName,BindingFlags flags = BindingFlags.Public) where TDelegate : Delegate + => TryGetMethodInternal<TDelegate>(typeof(TType), methodName, null, flags | BindingFlags.Static); + + private static TDelegate? TryGetMethodInternal<TDelegate>(Type type, string methodName, object? target, BindingFlags flags) where TDelegate : Delegate + { + _ = type ?? throw new ArgumentNullException(nameof(type)); + + //Get delegate argument types incase of a method overload + Type[] delegateArgs = typeof(TDelegate).GetMethod("Invoke")! + .GetParameters() + .Select(static p => p.ParameterType) + .ToArray(); + + //get the named method and always add the static flag + return type.GetMethod(methodName, flags, delegateArgs) + ?.CreateDelegate<TDelegate>(target); + } } } diff --git a/lib/Utils/tests/Memory/MemoryHandleTest.cs b/lib/Utils/tests/Memory/MemoryHandleTest.cs index 212eb0c..8880010 100644 --- a/lib/Utils/tests/Memory/MemoryHandleTest.cs +++ b/lib/Utils/tests/Memory/MemoryHandleTest.cs @@ -23,6 +23,7 @@ */ using System; +using System.Runtime.CompilerServices; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -197,6 +198,8 @@ namespace VNLib.Utils.Memory.Tests //Pin should throw Assert.ThrowsException<ArgumentOutOfRangeException>(() => _ = thandle.Pin(0)); + + Assert.ThrowsException<ObjectDisposedException>(() => _ = thandle.GetReference()); } //Full ref to mhandle check status @@ -217,6 +220,8 @@ namespace VNLib.Utils.Memory.Tests Assert.ThrowsException<ObjectDisposedException>(() => mHandle.Resize(10)); Assert.ThrowsException<ArgumentOutOfRangeException>(() => mHandle.BasePtr); + + Assert.ThrowsException<ObjectDisposedException>(() => _ = mHandle.GetReference()); } } } diff --git a/lib/Utils/tests/VNLib.UtilsTests.csproj b/lib/Utils/tests/VNLib.UtilsTests.csproj index c29915d..9053c51 100644 --- a/lib/Utils/tests/VNLib.UtilsTests.csproj +++ b/lib/Utils/tests/VNLib.UtilsTests.csproj @@ -16,7 +16,7 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" /> <PackageReference Include="MSTest.TestAdapter" Version="3.1.1" /> <PackageReference Include="MSTest.TestFramework" Version="3.1.1" /> <PackageReference Include="coverlet.collector" Version="6.0.0"> |